加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

CUDA之流与异步、并行数据传输

(2017-11-23 16:20:22)
标签:

gpu

分类: 并行编程:GPU/MPI/OPEN***
一些计算能力为2.x或更高的设备可以将锁页内存到设备内存的数据传输和设备内存到锁页内存的数据传输并行执行。应用程序可检查设备属性中的asyncEngineCount项来确定这一功能的支持程度,等于2时表示支持。

流(Streams)

        应用程序通过流来管理并行。一个流是一个顺次执行的命令序列。不同的流之间并行执行,没有固定的执行顺序。

1、流的创建与销毁

        定义一个流的过程通常包括:创建一个流对象,然后指定它为内核启动或者主机设备间数据传输的流参数。下面的一段代码创建了两个流并且在锁页内存中分配了一块float类型的数组hostPtr:

[cpp] view plain copy
  1. cudaStream_t stream[2];  
  2. for (int 0; 2; ++i)  
  3.     cudaStreamCreate(&stream[i]);  
  4. float *hostPtr;  
  5. cudaMallocHost(&hostPtr, size);  
  6. //如果使用cudaHostAlloc,则需要使用cudaFreeHost函数释放这一块内存
下面的代码定义了每一个流的行为:从主机端拷贝数据到设备端,内核启动,从设备端拷贝数据到主机端:
[cpp] view plain copy
  1. for (int 0; 2; ++i)  
  2.     cudaMemcpyAsync(inputDevPtr size, hostPtr size, size, cudaMemcpyHostToDevice, stream[i]);  
  3.     MyKernel<<<100, 512, 0, stream[i]>>>(outputDevPtr size, inputDevPtr size, size);  
  4.     cudaMemcpyAsync(hostPtr size, outputDevPtr size, size, cudaMemcpyDeviceToHost, stream[i]);  
  5.  

这部分代码中有一点需要注意:为了并行化数据拷贝和内核执行,主机端内存必须分配为锁页(page-locked)内存。

要销毁一个流需要调用函数cudaStreamDestroy()

[cpp] view plain copy
  1. for (int 0; 2; ++i)  
  2.     cudaStreamDestroy(stream[i]);  
cudaStreamDestroy()函数等待之前流中的指令序列运行完成,然后销毁指定流,将控制权返还给主机端。

  1. int *host_a, *host_b, *host_c;  
  2. int *dev_a, *dev_b, *dev_c;  
  3.   
  4. cudaError_t cudaStatus;  
  5. cudaStatus cudaMalloc((void **)&dev_a, sizeof(int));  
  6. if (cudaStatus != cudaSuccess)  
  7.  
  8.     printf("cudaMalloc dev_a failed!\n");  
  9.  
  10.   
  11. cudaStatus cudaHostAlloc((void **)&host_a, FULL_DATA_SIZE sizeof(int), cudaHostAllocDefault);  
  12. if (cudaStatus != cudaSuccess)  
  13.  
  14.     printf("cudaHostAlloc host_a failed!\n");  
  15. }  
  16. for (int 0; FULL_DATA_SIZE; += N)  
  17.  
  18.     cudaStatus cudaMemcpyAsync(dev_a, host_a i, sizeof(int), cudaMemcpyHostToDevice, stream);  
  19.     if (cudaStatus != cudaSuccess)  
  20.      
  21.         printf("cudaMemcpyAsync failed!\n");  
  22.      
  23.   
  24.     cudaStatus cudaMemcpyAsync(dev_b, host_b i, sizeof(int), cudaMemcpyHostToDevice, stream);  
  25.     if (cudaStatus != cudaSuccess)  
  26.      
  27.         printf("cudaMemcpyAsync failed!\n");  
  28.      
  29.   
  30.     kernel << >(dev_a, dev_b, dev_c);  
  31.   
  32.     cudaStatus cudaMemcpyAsync(dev_b, host_b i, sizeof(int), cudaMemcpyHostToDevice, stream);  
  33.     if (cudaStatus != cudaSuccess)  
  34.      
  35.         printf("cudaMemcpyAsync failed!\n");  
  36.      
  37. }  
  38. 这段代码中并没有使用cudaMemcpy(),而是通过cudaMemcpyAsync()在GPU与主机之间复制数据。函数差异虽小,但却很重要。cudaMemcpy()的行为类似于C库函数memcpy()。尤其是,这个函数将以同步方式执行,也就是说,当函数返回时,复制操作已经完成。
    异步函数的行为与同步函数相反,在调用cudaMemcpyAsync()时,只是放置一个请求,表示在流中执行一次内存复制操作,这个流是通过参数stream来指定的。当函数返回时,我们无法确保复制操作是否已经启动或完成。我们能够保证的是复制操作肯定会在下一个被放入流中的操作启动之前执行。任何传递给cudaMemcpyAsync()的主机内存指针都必须已经通过cudaHostAlloc()分配好内存,也就是,只能以异步方式对页锁定内存进行复制操作。
    注意,在核函数调用的尖括号中有一个流参数stream,此时核函数调用将是异步的。从技术上来说,当循环迭代完一次时,有可能不会启动任何内存复制或核函数执行。但能够确保的是,第一次放入流中的复制操作将在第二次复制操作之前执行,第二个复制操作将在核函数启动之前执行完成。这意味着,代码中for循环的完成不保证流的完成,每个流中的任务都可能处于等待状态。
    5) 当for循环结束时,队列中应该包含了许多等待GPU执行的工作。如果想要确保GPU执行完了计算与内存复制等操作,那么就需要将GPU与主机同步。也就是说,主机在继续执行之前,要首先等待GPU执行完成。可以调用cudaStreamSynchronize()并指定想要等待的流:
    [cpp] view plain copy
    1. cudaStatus cudaStreamSynchronize(stream);  
    6) 当程序执行到stream与主机同步之后的代码时,所用计算与复制操作都已完成。此时需要释放缓冲区,并销毁对GPU操作进行排队的流:
    [cpp] view plain copy
    1. cudaFreeHost(host_a);  
    2. cudaFreeHost(host_b);  
    3. cudaFreeHost(host_c);  
    4. cudaFree(dev_a);  
    5. cudaFree(dev_b);  
    6. cudaFree(dev_c);  
    7. cudaStreamDestroy(stream);  
    至此,单个流的使用已经讲完。

多个流的使用

1) GPU的工作调度机制
程序员可以将流视为有序的操作序列,其中即包含内存复制操作,又包含核函数调用。然而,在硬件中没有流的概念,而是包含一个或多个引擎来执行内存复制操作,以及一个引擎来执行核函数。这些引擎彼此独立地对操作进行排队,因此将导致如下图所示的任务调度情形。
                           http://img.blog.csdn.net/20160906220853528

因此,在某种程度上,用户与硬件关于GPU工作的排队方式有着完全不同的理解,而CUDA驱动程序则负责对用户和硬件进行协调。首先,在操作被添加到流的顺序中包含了重要的依赖性。例如上图,第0个流对A的内存复制需要在对B的内存复制之前完成。然而,一旦这些操作放入到硬件的内存复制引擎和核函数执行引擎的队列中时,这些依赖性将丢失,因此CUDA驱动程序需要确保硬件的执行单元不破坏流内部的依赖性。也就是说,CUDA驱动程序负责安装这些操作的顺序把它们调度到硬件上执行,这就维持了流内部的依赖性。下图说明了这些依赖性。
理解了GPU的工作调度原理之后,我们可以得到关于这些操作在硬件上执行的时间线,如下图所示。
记住,硬件在处理内存复制和核函数执行时分别采用了不同的引擎。因此,将操作放入流中队列中的顺序将影响着CUDA驱动程序调用这些操作以及执行的方式。
2) 高效的运用多个CUDA流
将操作放入流的队列时应采用宽度优先方式而非深度优先。也就是说,不是首先添加第0个流的所有四个操作,然后再添加第1个流的所有四个操作,而是将两个流交叉添加。实际代码如下:
[cpp] view plain copy
  1. for (int 0; FULL_DATA_SIZE; += 2)  
  2.  
  3.     cudaStatus cudaMemcpyAsync(dev0_a, host_a i, sizeof(int), cudaMemcpyHostToDevice, stream0);  
  4.     if (cudaStatus != cudaSuccess)  
  5.      
  6.         printf("cudaMemcpyAsync0 failed!\n");  
  7.      
  8.   
  9.     cudaStatus cudaMemcpyAsync(dev1_a, host_a i, sizeof(int), cudaMemcpyHostToDevice, stream1);  
  10.     if (cudaStatus != cudaSuccess)  
  11.      
  12.         printf("cudaMemcpyAsync1 failed!\n");  
  13.      
  14.   
  15.     cudaStatus cudaMemcpyAsync(dev0_b, host_b i, sizeof(int), cudaMemcpyHostToDevice, stream0);  
  16.     if (cudaStatus != cudaSuccess)  
  17.      
  18.         printf("cudaMemcpyAsync0 failed!\n");  
  19.      
  20.   
  21.     cudaStatus cudaMemcpyAsync(dev1_b, host_b i, sizeof(int), cudaMemcpyHostToDevice, stream1);  
  22.     if (cudaStatus != cudaSuccess)  
  23.      
  24.         printf("cudaMemcpyAsync1 failed!\n");  
  25.      
  26.       
  27.     kernel << >(dev0_a, dev0_b, dev0_c);  
  28.   
  29.     kernel << >(dev1_a, dev1_b, dev1_c);  
  30.   
  31.     cudaStatus cudaMemcpyAsync(host_c i, dev0_c, sizeof(int), cudaMemcpyDeviceToHost, stream0);  
  32.     if (cudaStatus != cudaSuccess)  
  33.      
  34.         printf("cudaMemcpyAsync0 failed!\n");  
  35.      
  36.   
  37.     cudaStatus cudaMemcpyAsync(host_c i, dev1_c, sizeof(int), cudaMemcpyDeviceToHost, stream1);  
  38.     if (cudaStatus != cudaSuccess)  
  39.      
  40.         printf("cudaMemcpyAsync1 failed!\n");  
  41.      
  42.  
此时,如果内存复制操作的时间与核函数执行的时间大致相当,那么新的执行时间线如下图所示。
http://img.blog.csdn.net/20160906222933274
假设复制操作需要时间a,核函数执行需要时间b,则有
当a ≈ b时,时间线长度约为6a。
当a > b时,时间线为6a。
当a < b时,时间线长度为4a + 2b。此时,修改放入流的任务的顺序,将获得更好的时间效率5a + 1b。



0

阅读 收藏 喜欢 打印举报/Report
前一篇:CUDA锁页内存
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有