流和并发

流和并发

一、流和事件概述

CUDA流是一系列异步的CUDA操作,这些操作按照主机代码确定的顺序在设备上执行。流能够封装这些操作,保持操作的顺序,允许操作在流中排队,并使它们在先前的操作之后执行,并且可以查询排队操作的状态。这些操作包括在主机和设备间进行数据传输,内核启动以及大多数由主机发起但由设备执行的其他命令。流中操作的执行相对于主机总是异步的。CUDA运行时决定何时可以在设备上执行操作。使用流可以充分利用GPU和CPU的闲置资源(包括计算资源和内存带宽)。

一般来说,CUDA典型的编程模式为:

1.将数据从主机复制到设备

2.执行核函数

3.将数据从设备复制到主机

一般来看,这个过程必须按顺序执行,但事实上,执行1时,计算资源和到主机的pcie带宽浪费,执行2时,两个pcie带宽浪费,执行3时计算资源和主机到设备的pcie带宽浪费。而这些资源浪费时可以通过将内核调度到不同的流中,使操作重叠来减少浪费,更加充分的利用GPU资源。

从软件看来,不同的流中并发是完全并行的,但从硬件来看,不一定总是并行的,根据PCIe总线占用或SM资源的可用性,完成不同的CUDA流可能仍需要互相等待。

1.1 CUDA流

流的两种类型:

隐式声明的流(空流)

显示声明的流(非空流)

在之前的操作里,并没有提到流的概念,是因为,所有的操作都被加载到空流中了。

非空流可以被显式的创建和管理。想要重叠不同的CUDA操作,需要使用非空流。异步流可以支持以下几种粗粒度并发:

1.重叠主机计算和设备计算

2.重叠主机计算和主机与设备间的数据传输

3.重叠主机与设备间的数据传输和设备计算

4.并发设备计算

cudaError_t cudaMemcpyAsync(void *dst, const void *src, size_t count, enum cudaMemcpyKind kind, cudaStream_t stream __dv(0));

是cudaMemcpy的异步形式(调用之后立刻返回),最后一个参数为流标识符,默认情况下为空流。

cudaStream_t stream;//定义一个流标识符

cudaError_t cudaStreamCreate(cudaStream_t *pStream);//创建一个可以显式管理的流

。。。使用一个流。一般api函数中流有其自己的形参,核函数的话,将流放在核函数执行配置的第四个参数即可(第三个是动态申请的共享内存的大小)

cudaError_t cudaStreamDestroy(cudaStream_t stream);//销毁一个流当执行资源释放的时候,如果仍然有stream的工作没干完,那么虽然该函数仍然会立刻返回,但是相关的工作做完后,这些资源才会自动的释放掉。

使用异步数据传输时,必须使用固定主机内存(因为主机端已经无法知道什么时候这些内存会被拷贝,分页内存会导致未定义行为)。即使用cudaMallocHost函数或cudaHostAlloc函数分配固定内存。

使用异步函数时,该函数没有执行完就已经返回,因此可能导致无法正确探查执行过程中的错误,或返回的错误是先前的异步操作的错误代码。

cudaError_t cudaStreamSynchronize(cudaStream_t stream);

cudaError_t cudaStreamQuery(cudaStream_t stream);

cudaStreamSynchronize强制阻塞主机,知道给定流中所有操作都完成

cudaStreamQuery会检查流中所有操作是否都完成,但不会阻塞主机。当所有操作都完成时,返回cudaSuccess,有任务还在执行时返回cudaErrorNotReady。

一个比较简单的例子就是cudaSamples里面的simpleStream,广度优先方式(先将一个任务分的段分别加载到各个流中,然后将下一个任务的各个段再加载到任务中)。

异步方式:

// asynchronously launch nstreams kernels, each operating on its own portion of data
for (int i = 0; i < nstreams; i++)
{
  init_array<<<blocks, threads, 0, streams[i]>>>(d_a + i *n / nstreams, d_c, niterations);
}
// asynchronously launch nstreams memcopies.  Note that memcopy in stream x will only
//   commence executing when all previous CUDA calls in stream x have completed
for (int i = 0; i < nstreams; i++)
{
  checkCudaErrors(cudaMemcpyAsync(hAligned_a + i * n / nstreams, d_a + i * n / nstreams, nbytes / nstreams, cudaMemcpyDeviceToHost, streams[i]));
}

结果如下图所示。

同步方式:


init_array<<<blocks, threads>>>(d_a, d_c, niterations);//这里的blocks和threads和上面代码不一样,维度分别应该是上面的nstreams倍
checkCudaErrors(cudaMemcpy(hAligned_a, d_a, nbytes, cudaMemcpyDeviceToHost));

可以看出核函数和D2H方向的数据拷贝在时间轴上是重叠的,达到了我们的目的。比下图中默认流的方式要好。

某些PCIe总线是双工的,可以重叠两个不同流中且不同方向的数据传输

并发内核的最大数量依赖于设备。Fermi支持16路并发,Kepler支持32路并发。而当前可用的计算资源如计算单元、寄存器、共享内存等都会限制并发内核的数量,如果当前设备是满负荷的,那么不能进一步并发。

1.2 流调度

Hyper-Q技术。

从Kepler架构开始有。目的是为了减少虚假的依赖关系。Hyper-Q技术通过在主机和设备之间维持多个硬件管理上的链接,允许多个CPU线程或进程在单一GPU上同时启动工作(其实没懂什么意思,但现在我们用的Maxwell,Pascal应该都支持这种技术)。

流的优先级。

从计算能力3.5开始有。使用


cudaError_t cudaStreamCreateWithPriority(cudaStream_t* pStream, unsigned int flags, int priority);

创建一个有指定整数优先级的流。并在pStream中返回一个句柄,用于工作调度。优先级整数越小,越优先。流优先级不会影响数据传输操作,只对计算内核有影响。高优先级网格队列可以占用低优先级已经执行的工作。如果给定整数超过设备定义范围,则被限制为最大值或最小值。可用下函数查询优先级

cudaError_t cudaDeviceGetStreamPriorityRange(int leastPriority, int greatestPriority);

若不支持流优先级返回两个0.

CUDA事件。

本质是CUDA流中的标记,它与该流内操作流中特定点相关联。不涉及。

流同步(阻塞和非阻塞)。

使用cudaStreamCreate创建的流是阻塞流,这些流的操作被会被空流阻塞。即考虑如下情况:

kernel_1<<<1,1,0,stream_1>>>();
kernel_2<<<1,1>>>();
kernel_3<<<1,1,0,stream_2>>>();

kernel_1和kernel_3被kernel_2阻塞,所以kernel_1执行完才会执行kernel_2,kernel_2执行完才能执行kernel_3,但对于主机,仍然是所有都是异步的,对于设备则是被阻塞的。

使用下面函数可以让流成为非阻塞流

cudaError_t cudaStreamCreateWithFlags(cudaStream_t* pStream, unsigned int flags);

flags参数使用cudaStreamNonBlocking即可。

隐式同步。

显式调用同步函数为显式同步。隐式同步为调用类似cudaMemcpy的函数,造成同步。

可能造成隐式同步的操作有:

锁页主机内存分配

设备内存分配

设备内存初始化

同一设备上两个地址之间的内存复制

一级缓存/共享内存配置的修改

显式同步。

cudaDeviceSynchronize、cudaStreamSynchronize、cudaStreamQuery、cudaEventSynchronize、cudaEventquery、cudaStreamWaitEvent等函数。

可配置事件。

不涉及。

2.并发内核执行

Fermi架构中存在虚假依赖关系(一直没懂是什么,反正见不到这种设备了),需要使用广度优先方法避免(深度优先比广度优先慢很多)。而又Hyper-Q技术的架构则深度优先和广度优先速度差不多。不多探讨了。

cudaSamples中simpleHyperQ例子,结果如下(有八个流能够同时执行)

使用OpenMP的调度操作

用环境变量调整流行为

使用CUDA_DEVICE_MAX_CONNECTIONS环境变量来调整并行硬件连接数量(对于Kepler架构最大为32),由于每个kernel分配的资源多少不同,实际可并发的可能更少。

建立流间的依赖关系

由于实际情况复杂,引入流的依赖关系可能是有用的。

for (int i = 0; i < n_streams; i++)
{
    CHECK(cudaEventCreateWithFlags(&(kernelEvent[i]),
                cudaEventDisableTiming));
}

// record start event
CHECK(cudaEventRecord(start, 0));

// dispatch job with depth first ordering
for (int i = 0; i < n_streams; i++)
{
    kernel_1<<<grid, block, 0, streams[i]>>>();
    kernel_2<<<grid, block, 0, streams[i]>>>();
    kernel_3<<<grid, block, 0, streams[i]>>>();
    kernel_4<<<grid, block, 0, streams[i]>>>();

    CHECK(cudaEventRecord(kernelEvent[i], streams[i]));
    CHECK(cudaStreamWaitEvent(streams[n_streams - 1], kernelEvent[i], 0));
}

注意第n_streams-1个流需要等待其他流完成才能开始启动工作。

3 重叠内核执行和数据传输

实际上就是1.1中的例子。同步方式和异步方式的对比。1.1就是将1个完整的复制、计算、复制,分块成n个小块的复制1、计算1、复制1,复制2、计算2、复制2。。。复制n、计算n、复制n。实现重叠。提高资源利用。

将一个大的问题拆分成相同的子问题,然后通信和计算就可以重叠了。

4 重叠CPU和GPU的执行

异步方式执行设备操作,使用cudaEventRecord记录一个停止事件,然后执行主机端操作,需要设备数据时使用cudaEventquery查询停止事件是否执行完毕。详细例子如cudaSamples中asyncAPI。

5 流回调

看起来挺高级的功能,暂时不用,略。

注意:

使用nvvp可视化查看cuda操作的时间轴重叠对于本节实验很重要。

流只在GPU有资源可用的时候才能起作用。比如计算满负荷时,内存拷贝是可以执行的,但新的计算时不能被执行的。双工PCIe的D2H和H2D是可以被重叠的。
参考:CUDA C编程权威指南,第六章cudaSamples asyncAPI、simpleHyperQ、simplestreams

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值