CUDA执行

整理一下CUDA同步和异步的执行。相关内容大部分来源于相关网络http://blog.163.com/wujiaxing009@126/blog/static/71988399201712035958365/
cudaMemcpy(d_a, a, numBytes, cudaMemcpyHostToDevice); increment<<<1,N>>>(d_a) cudaMemcpy(a, d_a, numBytes, cudaMemcpyDeviceToHost);

从设备端来看,这三个操作都在默认stream中,并且按顺序执行;从主机端来看,数据传输是阻塞的或者同步传输,而kernel是异步的。第一步主机到设备的数据传输是同步的,CPU线程不能到达第二行直到主机到设备的数据传输完成。一旦kernel被处理,CPU线程移到第三行,但是改行的传输不能开始,因为设备端正在执行第二行的内容。

cudaStreamCreate可以创建多个Stream。当执行一次异步数据传输时,我们必须使用pinned(或者non-pageable)memory。Pinned memory的分配如下:

cudaError_t cudaMallocHost(void **ptr, size_t size);
cudaError_t cudaHostAlloc(void **pHost, size_t size, unsigned int flags);


由于CUDA streams的操作是异步的,就需要一些CUDA API函数在必要的时候做同步操作:
cudaError_t cudaStreamSynchronize(cudaStream_t stream);
cudaError_t cudaStreamQuery(cudaStream_t stream);
第一个函数会强制host阻塞等待,直至stream中所有操作完成为止;第二个会检查stream中的操作是否全部完成,即使有操作没完成也不会阻塞host。如果所有操作都完成了,则返回cudaSuccess,否则返回cudaErrorNotReady。

for (int i = 0; i < nStreams; i++) 
{
    int offset = i * bytesPerStream;
    cudaMemcpyAsync(&d_a[offset], &a[offset], bytePerStream, streams[i]);
    kernel<<grid, block, 0, streams[i]>>(&d_a[offset]);
    cudaMemcpyAsync(&a[offset], &d_a[offset], bytesPerStream, streams[i]);
}

for (int i = 0; i < nStreams; i++) 
{
    cudaStreamSynchronize(streams[i]);
}
该段代码使用了三个stream,数据传输和kernel运算都被分配在了这几个并发的stream中。
尽管Fermi架构GPU最高支持16路并行,但是在物理层面上,所有stream是被塞进硬件上唯一一个工作队列来调度的,当选中一个grid来执行时,runtime会查看task的依赖关系,如果当前task依赖前面的task,该task就会阻塞,由于只有一个队列,后面的都会跟着等待,即使后面的task是别的stream上的任务。如下图所示:


cudaError_t cudaStreamCreateWithPriority(cudaStream_t* pStream, unsigned int flags, int priority);
该函数创建一个stream,赋予priority的优先级,高优先级的grid可以抢占低优先级执行。不过优先级属性只对kernel有效,对数据传输无效。此外,如果设置的优先级超出了可设置范围,则会自动设置成最高或者最低。有效可设置范围可用下列函数查询:
cudaError_t cudaDeviceGetStreamPriorityRange(int *leastPriority, int *greatestPriority);
其中,leastPriority是下限,gretestPriority是上限。数值较小则拥有较高优先级。如果device不支持优先级设置,则这两个值都返回0。
Event是stream相关的一个重要概念,其用来标记stream执行过程的某个特定的点。其主要用途是:
同步stream执行
监视device运行进程
CUDA API提供了相关函数来插入event到stream中并且可以查询该event是否完成(比如计算程序运行时间)。只有当该event标记的stream位置的所有操作都被执行完毕,该event才算完成。关联到默认stream上的event则对所有的stream有效。
Creation and Destruction
// 声明
cudaEvent_t event;
// 创建
cudaError_t cudaEventCreate(cudaEvent_t* event);
// 销毁
cudaError_t cudaEventDestroy(cudaEvent_t event);
同理stream的释放,在调用cudaEventDestroy函数的时候,如果相关操作没完成,则会在操作完成后自动释放资源。
Recording Events and Mesuring Elapsed Time
Events标记了stream执行过程中的一个点,我们就可以检查正在执行的stream中的操作是否到达该点,我们可以把event当成一个操作插入到stream中的众多操作中,当执行到该操作时,所做工作就是设置CPU的一个flag来标记表示完成。下面函数将event关联到指定stream。
cudaError_t cudaEventRecord(cudaEvent_t event, cudaStream_t stream = 0);
该函数可用来在特定的Stream中等待和测试设置点之前的所有操作的完成情况。等待event会阻塞调用host线程,同步操作调用下面的函数:
cudaError_t cudaEventSynchronize(cudaEvent_t event);
cudaError_t cudaEventQuery(cudaEvent_t event);
第一个函数类似于cudaStreamSynchronize,只不过是等待一个event而不是整个stream执行完毕。第二个函数用来查询event是否完成,该函数不会阻塞host。
此外,还有专门的API函数可以度量两个event之间的时间间隔:
cudaError_t cudaEventElapsedTime(float* ms, cudaEvent_t start, cudaEvent_t stop);
该函数返回start和stop之间的时间间隔,单位是毫秒。start和stop不必关联到同一个stream上,但是要注意,如果二者任意一个关联到了non-NULL stream上,时间间隔可能要比期望的大。这是因为cudaEventRecord是异步发生的,我们没办法保证度量出来的时间恰好就是两个event之间,所以只是想要gpu工作的时间间隔,则stop和strat都关联到默认的stream上就好了。
下面代码简单展示了如何使用event来度量运行时间:
// create two events
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// record start event on the default stream
cudaEventRecord(start);
// execute kernel
kernel<<<grid, block>>>(arguments);
// record stop event on the default stream
cudaEventRecord(stop);
// wait until the stop event completes
cudaEventSynchronize(stop);
// calculate the elapsed time between two events
float time;
cudaEventElapsedTime(&time, start, stop);
// clean up the two events
cudaEventDestroy(start);
cudaEventDestroy(stop);
这里的start和stop两个event被关联到默认的Stream中。



猜你喜欢

转载自blog.csdn.net/B1009/article/details/79096919