CUDA学习笔记(八)动态并行

Dynamic Parallelism(动态并行)

到目前为止,所有kernel都是在host端调用,GPU的工作完全在CPU的控制下。CUDA Dynamic Parallelism允许GPU kernel在device端创建调用。Dynamic Parallelism使递归更容易实现和理解,由于启动的配置可以由device上的thread在运行时决定,这也减少了host和device之间传递数据和执行控制。我们接下来会分析理解使用Dynamic Parallelism。

Nested Execution(嵌套执行)

在host调用kernel和在device调用kernel的语法完全一样。kernel的执行则被分为两种类型:parent和child。一个parent thread,parent block或者parent grid可以启动一个新的grid,即child gridchild grid必须在parent 之前完成,也就是说,parent必须等待所有child完成。

当parent启动一个child grid时,在parent显式调用synchronize之前,child不保证会开始执行parent和child共享同一个global和constant memory,但是有不同的shared 和local memory。不难理解的是,只有两个时刻可以保证child和parent见到的global memory完全一致:child刚开始和child完成所有parent对global memory的操作对child都是可见的,而child对global memory的操作只有在parent进行synchronize操作后对parent才是可见的

 

Nested Hello World on the GPU

为了更清晰的讲解Dynamic Parallelism,我们改编最开始写的hello world程序。下图显示了使用Dynamic Parallelism的执行过程,host调用parent grid(每个block八个thread)。thread 0调用一个child grid(每个block四个thread),thread 0 的第一个thread又调用一个child grid(每个block两个thread),依次类推。

 

下面是具体的代码,每个thread会先打印出Hello World;然后,每个thread再检查自己是否该停止。

 

 
  1. __global__ void nestedHelloWorld(int const iSize,int iDepth) {

  2. int tid = threadIdx.x;

  3. printf("Recursion=%d: Hello World from thread %d block %d\n",iDepth,tid,blockIdx.x);

  4. // condition to stop recursive execution

  5. if (iSize == 1) return;

  6. // reduce block size to half

  7. int nthreads = iSize>>1;

  8. // thread 0 launches child grid recursively

  9. if(tid == 0 && nthreads > 0) {

  10. nestedHelloWorld<<<1, nthreads>>>(nthreads,++iDepth);

  11. printf("-------> nested execution depth: %d\n",iDepth);

  12. }

  13. }


编译:

 

$ nvcc -arch=sm_35 -rdc=true nestedHelloWorld.cu -o nestedHelloWorld -lcudadevrt


-lcudadevrt是用来连接runtime库的,跟gcc连接库一样。-rdc=true使device代码可重入,这是DynamicParallelism所必须的,至于原因则将是一个比较大的话题,以后探讨。

代码的输出为:

 

 
  1. ./nestedHelloWorld Execution Configuration: grid 1 block 8

  2. Recursion=0: Hello World from thread 0 block 0

  3. Recursion=0: Hello World from thread 1 block 0

  4. Recursion=0: Hello World from thread 2 block 0

  5. Recursion=0: Hello World from thread 3 block 0

  6. Recursion=0: Hello World from thread 4 block 0

  7. Recursion=0: Hello World from thread 5 block 0

  8. Recursion=0: Hello World from thread 6 block 0

  9. Recursion=0: Hello World from thread 7 block 0

  10. -------> nested execution depth: 1

  11. Recursion=1: Hello World from thread 0 block 0

  12. Recursion=1: Hello World from thread 1 block 0

  13. Recursion=1: Hello World from thread 2 block 0

  14. Recursion=1: Hello World from thread 3 block 0

  15. -------> nested execution depth: 2

  16. Recursion=2: Hello World from thread 0 block 0

  17. Recursion=2: Hello World from thread 1 block 0

  18. -------> nested execution depth: 3

  19. Recursion=3: Hello World from thread 0 block 0


这里的01234….输出顺序挺诡异的,太规整了,我们暂且认为CUDA对printf做过修改吧。还有就是,按照CPU递归程序的经验,这里的输出顺序就更怪了,当然,肯定不是编译器错误或者CUDA的bug,大家可以在调用kernel后边加上cudaDeviceSynchronize,就可以看到“正常”的顺序了,原因也就清楚了。

使用nvvp可以查看执行情况,空白说明parent在等待child执行结束

 

$nvvp ./nesttedHelloWorld


接着,我们尝试使用两个block而不是一个:

 

$ ./nestedHelloWorld 2


输出是:

 

 
  1. ./nestedHelloWorld 2Execution Configuration: grid 2 block 8

  2. Recursion=0: Hello World from thread 0 block 1

  3. Recursion=0: Hello World from thread 1 block 1

  4. Recursion=0: Hello World from thread 2 block 1

  5. Recursion=0: Hello World from thread 3 block 1

  6. Recursion=0: Hello World from thread 4 block 1

  7. Recursion=0: Hello World from thread 5 block 1

  8. Recursion=0: Hello World from thread 6 block 1

  9. Recursion=0: Hello World from thread 7 block 1

  10. Recursion=0: Hello World from thread 0 block 0

  11. Recursion=0: Hello World from thread 1 block 0

  12. Recursion=0: Hello World from thread 2 block 0

  13. Recursion=0: Hello World from thread 3 block 0

  14. Recursion=0: Hello World from thread 4 block 0

  15. Recursion=0: Hello World from thread 5 block 0

  16. Recursion=0: Hello World from thread 6 block 0

  17. Recursion=0: Hello World from thread 7 block 0

  18. -------> nested execution depth: 1

  19. -------> nested execution depth: 1

  20. Recursion=1: Hello World from thread 0 block 0

  21. Recursion=1: Hello World from thread 1 block 0

  22. Recursion=1: Hello World from thread 2 block 0

  23. Recursion=1: Hello World from thread 3 block 0

  24. Recursion=1: Hello World from thread 0 block 0

  25. Recursion=1: Hello World from thread 1 block 0

  26. Recursion=1: Hello World from thread 2 block 0

  27. Recursion=1: Hello World from thread 3 block 0

  28. -------> nested execution depth: 2

  29. -------> nested execution depth: 2

  30. Recursion=2: Hello World from thread 0 block 0

  31. Recursion=2: Hello World from thread 1 block 0

  32. Recursion=2: Hello World from thread 0 block 0

  33. Recursion=2: Hello World from thread 1 block 0

  34. -------> nested execution depth: 3

  35. -------> nested execution depth: 3

  36. Recursion=3: Hello World from thread 0 block 0

  37. Recursion=3: Hello World from thread 0 block 0


从上面结果来看,首先应该注意到,所有child的block的id都是0。下图是调用过程,parent有两个block了,但是所有child都只有一个blcok:

nestedHelloWorld<<<1, nthreads>>>(nthreads, ++iDepth);

 

注意:Dynamic Parallelism只有在CC3.5以上才被支持。通过Dynamic Parallelism调用的kernel不能执行于不同的device(物理上实际存在的)上。调用的最大深度是24,但实际情况是,kernel要受限于memory资源,其中包括为了同步parent和child而需要的额外的memory资源。

Nested Reduction

学过算法导论之类的算法书应该知道,因为递归比较消耗资源的(在CUDA编程中也不推荐使用递归),所以如果可以的话最好是展开,而这里要讲的恰恰相反,我们要实现递归,这部分主要就是再次证明DynamicParallelism的好处,有了它就可以实现像C那样写递归代码了。

下面的代码就是一份实现,和之前一样,每个child的有一个block,block中第一个thread调用kernel,不同的是,parent的grid有很多的block。第一步还是讲global memory的地址g_idata转化为每个block本地地址。然后,if判断是否该退出,退出的话,就将结果拷贝回global memory。如果不该退出,就进行本地reduction,一般的线程执行in-place(就地)reduction,然后,同步block来保证所有部分和的计算。thread0再次产生一个只有一个block和当前一半数量thread的child grid。

 

 
  1. __global__ void gpuRecursiveReduce (int *g_idata, int *g_odata,

  2. unsigned int isize) {

  3. // set thread ID

  4. unsigned int tid = threadIdx.x;

  5. // convert global data pointer to the local pointer of this block

  6. int *idata = g_idata + blockIdx.x*blockDim.x;

  7. int *odata = &g_odata[blockIdx.x];

  8. // stop condition

  9. if (isize == 2 && tid == 0) {

  10. g_odata[blockIdx.x] = idata[0]+idata[1];

  11. return;

  12. }

  13. // nested invocation

  14. int istride = isize>>1;

  15. if(istride > 1 && tid < istride) {

  16. // in place reduction

  17. idata[tid] += idata[tid + istride];

  18. }

  19. // sync at block level

  20. __syncthreads();

  21. // nested invocation to generate child grids

  22. if(tid==0) {

  23. gpuRecursiveReduce <<<1, istride>>>(idata,odata,istride);

  24. // sync all child grids launched in this block

  25. cudaDeviceSynchronize();

  26. }

  27. // sync at block level again

  28. __syncthreads();

  29. }


编译运行,下面结果是运行在Kepler K40上面:

 

 
  1. $ nvcc -arch=sm_35 -rdc=true nestedReduce.cu -o nestedReduce -lcudadevrt

  2. ./nestedReduce starting reduction at device 0: Tesla K40c

  3. array 1048576 grid 2048 block 512

  4. cpu reduce elapsed 0.000689 sec cpu_sum: 1048576

  5. gpu Neighbored elapsed 0.000532 sec gpu_sum: 1048576<<<grid 2048 block 512>>>

  6. gpu nested elapsed 0.172036 sec gpu_sum: 1048576<<<grid 2048 block 512>>>


相较于neighbored,nested的结果是非常差的。

从上面结果看,2048个block被初始化了。每个block执行了8个recursion,16384个child block被创建,__syncthreads也被调用了16384次。这都是导致效率很低的原因。

当一个child grid被调用后,他看到的memory是和parent完全一样的,因为child只需要parent的一部分数据,block在每个child grid的启动前的同步操作是不必要的,修改后:

 

 
  1. __global__ void gpuRecursiveReduceNosync (int *g_idata, int *g_odata,unsigned int isize) {

  2. // set thread ID

  3. unsigned int tid = threadIdx.x;

  4. // convert global data pointer to the local pointer of this block

  5. int *idata = g_idata + blockIdx.x * blockDim.x;

  6. int *odata = &g_odata[blockIdx.x];

  7. // stop condition

  8. if (isize == 2 && tid == 0) {

  9. g_odata[blockIdx.x] = idata[0] + idata[1];

  10. return;

  11. }

  12. // nested invoke

  13. int istride = isize>>1;

  14. if(istride > 1 && tid < istride) {

  15. idata[tid] += idata[tid + istride];

  16. if(tid==0) {

  17. gpuRecursiveReduceNosync<<<1, istride>>>(idata,odata,istride);

  18. }

  19. }

  20. }

  21. 将同步取消了,操作加到了判断里面


运行输出,时间减少到原来的三分之一:

 

 
  1. ./nestedReduceNoSync starting reduction at device 0: Tesla K40c

  2. array 1048576 grid 2048 block 512

  3. cpu reduce elapsed 0.000689 sec cpu_sum: 1048576

  4. gpu Neighbored elapsed 0.000532 sec gpu_sum: 1048576<<<grid 2048 block 512>>>

  5. gpu nested elapsed 0.172036 sec gpu_sum: 1048576<<<grid 2048 block 512>>>

  6. gpu nestedNosyn elapsed 0.059125 sec gpu_sum: 1048576<<<grid 2048 block 512>>>


不过,性能还是比neighbour-paired要慢。接下来在做点改动,主要想法如下图所示,kernel的调用增加了一个参数iDim,这是因为每次递归调用,child block的大小就减半,parent 的blockDim必须传递给child grid,从而使每个thread都能计算正确的global memory偏移地址。注意,所有空闲的thread都被移除了。相较于之前的实现,每次都会有一半的thread空闲下来而被移除,也就释放了一半的计算资源。

 

 

 
  1. __global__ void gpuRecursiveReduce2(int *g_idata, int *g_odata, int iStride,int const iDim) {

  2. // convert global data pointer to the local pointer of this block

  3. int *idata = g_idata + blockIdx.x*iDim;

  4. // stop condition

  5. if (iStride == 1 && threadIdx.x == 0) {

  6. g_odata[blockIdx.x] = idata[0]+idata[1];

  7. return;

  8. }

  9. // in place reduction

  10. idata[threadIdx.x] += idata[threadIdx.x + iStride];

  11. // nested invocation to generate child grids

  12. if(threadIdx.x == 0 && blockIdx.x == 0) {

  13. gpuRecursiveReduce2 <<<gridDim.x,iStride/2>>>(

  14. g_idata,g_odata,iStride/2,iDim);

  15. }

  16. }


编译运行:

 

 
  1. ./nestedReduce2 starting reduction at device 0: Tesla K40c

  2. array 1048576 grid 2048 block 512

  3. cpu reduce elapsed 0.000689 sec cpu_sum: 1048576

  4. gpu Neighbored elapsed 0.000532 sec gpu_sum: 1048576<<<grid 2048 block 512>>>

  5. gpu nested elapsed 0.172036 sec gpu_sum: 1048576<<<grid 2048 block 512>>>

  6. gpu nestedNosyn elapsed 0.059125 sec gpu_sum: 1048576<<<grid 2048 block 512>>>

  7. gpu nested2 elapsed 0.000797 sec gpu_sum: 1048576<<<grid 2048 block 512>>>


从这个结果看,数据又好看了不少,可以猜测,大约是由于调用了较少的child grid,我们可以用nvprof来验证下:

 

$ nvprof ./nestedReduce2


部分输出结果如下,第二列上显示了dievice kernel 的调用次数,第一个和第二个创建了16384个child grid。gpuRecursiveReduce2八层nested Parallelism只创建了8个child。

 

 
  1. Calls (host) Calls (device) Avg Min Max Name

  2. 1 16384 441.48us 2.3360us 171.34ms gpuRecursiveReduce

  3. 1 16384 51.140us 2.2080us 57.906ms gpuRecursiveReduceNosync

  4. 1 8 56.195us 22.048us 100.74us gpuRecursiveReduce2

  5. 1 0 352.67us 352.67us 352.67us reduceNeighbored


对于一个给定的算法,我们可以有很多种实现方式,避免大量的nested 调用可以提升很多性能。同步对算法的正确性至关重要,但也是一个消耗比较大的操作,block内部的同步操作倒是可以去掉。因为在device上运行nested程序需要额外的资源,nested调用是有限的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值