11.3_流

11.3 流

对于那些能够得益于内核处理与数据传输并发进行(CPU/GPU重叠)的负载,CUDA流可以用来协调它们的执行。stream3Streams.cu应用程序将输入和输出数组分成k个流,然后引发k个主机到设备的内存复制、内核执行和设备到主机的内存复制,它们各自运行在自己独立的流里。把传输和计算关联到不同流,促使CUDA知道这些计算是完全独立的,这样CUDA将会利用硬件支持的任何并行机会。对于包含多个复制引擎的GPU,GPU可能在把数据传入和传出设备内存的同时,可以让SM处理其他数据。

代码清单11-6显示了来自stream3Streams.cu的节选,具有如代码清单11-5相同的功能。在测试系统上,这个应用程序的输出内容如下。

Measuring times with 128M floats  
Testing with default max of 8 streams (set with --maxStreams <count>)  
Streams Time (ms) MB/s  
1 290.77 ms 5471.45  
2 273.46 ms 5820.34  
3 277.14 ms 5744.49  
4 278.06 ms 5725.76  
5 277.44 ms 5736.52  
6 276.56 ms 5751.87  
7 274.75 ms 5793.43  
8 275.41 ms 5779.51

所使用的GPU仅具有一个复制引擎,因此很容易理解它在使用2个流的情况下,如何达到最高的性能。如果内核执行时间超过传输时

间,它可能会得益于把数组分割为多于2个的子数组。根据实际情况看,第一个内核启动只有在完成了第一个主机到设备的内存复制操作后才能开始执行,而最后一个设备到主机的内存复制操作只有在最后一个内核启动执行完毕后才能开始。如果内核处理消耗更多的时间,这种“突出部分”会更明显。对于我们的应用程序,273毫秒的系统时钟时间表明,大多数内核处理的开销(13.87毫秒)已被隐藏。

需要注意的是,这一策略没有像代码清单11-5那样在操作之间插入任何cusdaEventRecord(),部分原因是由于硬件的限制。在大多数的CUDA硬件上,试图在代码清单11-6的流式操作之间记录事件将打破并发性并降低性能。相反地,我们在所有操作之前和所有操作之后分别加入了一个cusdaEventRecord()。

代码清单11-6 stream3Streams.cu内核节选

for ( int iStream = 0; iStream < nStreams; iStream++) { CUDART_CHECK(udaMemcpyAsync{ dptrX+iStream\*streamStep, bptrX+iStream\*streamStep, streamStep\*sizeof(float),udaMemcpyHostToDevice, streams[iStream])}; CUDART_CHECK(udaMemcpyAsync{ dptrY+iStream\*streamStep, bptrY+iStream\*streamStep, streamStep\*sizeof(float),udaMemcpyHostToDevice, streams[iStream])};   
}   
for (int iStream  $= 0$  ;iStream<nStreams;iStream++){ saxpyGPU<<nBlocks,nThreads,0,streams[iStream]  $\gg >$  ( dptrOut+iStream\*streamStep, dptrX+iStream\*streamStep, dptrY+iStream\*streamStep, streamStep, alpha);   
}   
for (int iStream  $= 0$  ;iStream<nStreams;iStream++){ CUDART_CHECK(udaMemcpyAsync{ dptrOut+iStream\*streamStep, dptrOut+iStream\*streamStep, streamStep\*sizeof(float),udaMemcpyDeviceToHost, streams[iStream])};