• CUDA编程入门系列(十)并行规约


    一、什么是规约?

            规约,用白话来说,就是将多个值变成一个值的操作,如向量求和,向量内积操作。

            以向量求和为例,如果使用串行规约的话,那么就是依靠for循环来进行操作

                    for(int i = 0; i < nums.size(); i++)   sum += nums[i];

            但串行规约的话,此时算法的复杂度为O(N).

            在并行计算中,其中有一种方法叫做二叉树算法,即先将元素两两求和,再进行计算。如下图所示。此时算法的复杂度为O(LogN).

             在GPU计算,存在多种策略进行计算。

            (1)使用单个block进行计算:不足以充分利用资源

            (2)使用多个block进行计算:每个块负责向量的部分计算,再把每个块的计算结果进行处理

              

            如果使用多个块进行并行规约,那么就可能存在同步问题,不同块的处理规约时间不同。所以,这里的关键就是需要一个全局的同步。

            然而,CUDA并不支持全局同步。第一,如果使用全局同步的话,就会使得一些已执行完程序的块处于空闲状态,这会导致资源的浪费。 第二,如果使用的块比较多的话,就有可能导致死锁。这里也比较好理解,即大部分的块都申请了SM资源,但不够,所以继续阻塞申请SM资源,也都不释放SM,这就会导致死锁。

            解决方法:分多个kernel进行处理。因为在不同的kenel之间存在隐式的同步,即第一个kernel执行完,才能执行第二个kernel。

    二、并行规约算法 --- 二叉树算法

            使用并行规约就是为了最大化GPU的性能,指标包含:浮点运算数GFLOPS和有效带宽GB/s.

            二叉树算法,即每两个数相加得到部分和,依次进行该操作,直到最后一个数。步骤如下:

            ① 将数据从全局内存加载到共享内存当中

            ② 进行规约操作,每个线程负责规约两个数,每一次规约之后都有一半的线程处于空闲状态。不断进行上述操作,直到只剩下一个线程。

            ③ 将结果返回到全局内存当中

             该算法的伪代码如下图所示:

            其中 sdata[]代表所申请的共享内存,蓝色方框用以把数据从全局内存拷贝到共享内存。

            黄色方框负责进行规约操作,这里只有偶数号线程会进行操作。奇数好线程不操作。这里的__syncthreads()是为了在每一次规约后同步所有线程,直到所有的线程都执行完毕再进行下一次规约。

            橘色方框负责将规约结果从共享内存加载到全局内存当中。

              消耗时间及带宽如下图所示 

            因为一个warp里面的32线程执行相同的指令,如果指令不同(即指令分化)的话,可能会导致32个线程按照串行的方式进行执行,这与我们并行的思想是相悖的。所以,这里存在一个问题,因为此处的线程要进行if的分支结构判断,所以就有可能导致不同的线程有不同的指令,导致指令分化,影响执行速度。

            如下图所示,绿色方块代表线程处于活跃状态,灰白色方块代表线程处于空闲状态。在同一个warp之内,产生了指令分化。

    三、并行规约算法改进 warp divergence

            由于上述算法存在指令分化问题,会影响执行的速度,所以需要进行改进。改进的思路就是要让一个warp里面的线程执行相同的指令。

            改进的方式如黄色框的代码所示,只需要修改三行代码,即可缓解指令分化问题。

            通过引入临时变量index,假如s = 1,blockdim=128,那么此时前64个线程都执行相同的指令,后64个线程也执行相同的指令,就避免了指令分化的问题。 但是随着计算的进行,s的增大,越来越少的线程处于活跃状态,当活跃的线程小于一个warp的数量的时候,还是会产生指令分化的问题。

            改进后的算法,其warp内线程的状态如下图所示:

            时间和带宽如下所示: 

            但这里又存在一个问题,会产生bank conflict。通过参考其他博客的内容,bank conflict的含义为:同一个warp中的不同线程,访问相同的bank,就会产生conflict。 在这个例子中,其实我没太理解是如何产生冲突的。有一种可能就是 比如说线程1 是将第0个数和第1个数进行相加,也就是说线程1要访问第0个数和第1个数,而线程2要访问第1个数和第2个数,这就导致了warp中的不同线程访问了相同的地址。不同的线程访问同一资源,要进行临界操作,由原来的并行操作变成串行操作,影响了执行速度。

    四、 并行规约算法改进 消除bank conflict

            在三中,通过不同的编码方式,消除了warp内指令分化的情况。但是也引进了bank conflict问题。bank conflict 会把访问相同bank的线程由并行执行 改为串行执行,严重的影响了执行速度。

            故而,我们对算法进行如下的改进,我们不再使用相邻数相加的方式,而是采用跳跃的方式进行相加,以”折半“的形式进行相加。每次规约都把后半部分的内容加到前半部分的中。

            其伪代码如下图所示:

            代码的执行速度和带宽如下图所示: 

    五、并行规约算法改进 改进全局内存访问

            下图中,橘色方框所示代码为将数据从全局内存加载到共享内存中的方式,每一个线程只加载一个元素。而黄色方框所示代码进行了改进,每一个线程加载两个元素,并且执行了第一步的规约。这就提高了程序的执行速度。 但这里?黄色方框写的代码,不会导致g_idata访问越界吗? 这里没太懂啊。。。

            

    速度和带宽:

     五、并行规约算法改进 warp内循环展开 

            四种的规约算法代码如下:在每一个步计算中都要使用同步语句来进行同步。

      但根据warp的特性,warp内中32个线程会执行相同的指令。那么当活跃的线程数小于32个时, 所有的活跃线程都在一个warp内执行相同的指令,因为执行相同的指令,所以这个时候就不需要__syncthreads() 同步点,因此减少开销。改进完的代码如下图所示:在黄色方框所示的代码当中,由于编译器会对代码进行优化,然而,共享内存中的数据是不断进行更新的。所以我们不希望编译器对代码进行优化,所以我们要对共享内存添加关键字volatile。

            速度和带宽: 

            

     

    六、并行规约算法改进  完全循环展开

            在五中,我们对最后几个循环进行展开,减少了同步的次数,以此提高的执行效率。那么,能否将所有的循环全部展开?答案是可以的,但是存在以下前提: 每个块中最多只有512个线程,且必须为2的n次方

            

            速度和带宽: 

    七、规约算法应用:向量内积 

                 

    1. /* dot product of two vectors: d = */
    2. #include "aux.h"
    3. #include
    4. typedef double FLOAT;
    5. /* host, add */
    6. FLOAT dot_host(FLOAT *x, FLOAT *y, int N)
    7. {
    8. int i;
    9. FLOAT t = 0;
    10. assert(x != NULL);
    11. assert(y != NULL);
    12. for (i = 0; i < N; i++) t += x[i] * y[i];
    13. return t;
    14. }
    15. __device__ void warpReduce(volatile FLOAT *sdata, int tid)
    16. {
    17. sdata[tid] += sdata[tid + 32];
    18. sdata[tid] += sdata[tid + 16];
    19. sdata[tid] += sdata[tid + 8];
    20. sdata[tid] += sdata[tid + 4];
    21. sdata[tid] += sdata[tid + 2];
    22. sdata[tid] += sdata[tid + 1];
    23. }
    24. /* partial dot product */
    25. __global__ void dot_stg_1(const FLOAT *x, FLOAT *y, FLOAT *z, int N)
    26. {
    27. __shared__ FLOAT sdata[256];
    28. int idx = get_tid();
    29. int tid = threadIdx.x;
    30. int bid = get_bid();
    31. /* load data to shared mem */
    32. if (idx < N) {
    33. sdata[tid] = x[idx] * y[idx];
    34. }
    35. else {
    36. sdata[tid] = 0;
    37. }
    38. __syncthreads();
    39. /* reduction using shared mem */
    40. if (tid < 128) sdata[tid] += sdata[tid + 128];
    41. __syncthreads();
    42. if (tid < 64) sdata[tid] += sdata[tid + 64];
    43. __syncthreads();
    44. if (tid < 32) warpReduce(sdata, tid);
    45. if (tid == 0) z[bid] = sdata[0];
    46. }
    47. /* sum all entries in x and asign to y
    48. * block dim must be 256 */
    49. __global__ void dot_stg_2(const FLOAT *x, FLOAT *y, int N)
    50. {
    51. __shared__ FLOAT sdata[256];
    52. int idx = get_tid();
    53. int tid = threadIdx.x;
    54. int bid = get_bid();
    55. /* load data to shared mem */
    56. if (idx < N) {
    57. sdata[tid] = x[idx];
    58. }
    59. else {
    60. sdata[tid] = 0;
    61. }
    62. __syncthreads();
    63. /* reduction using shared mem */
    64. if (tid < 128) sdata[tid] += sdata[tid + 128];
    65. __syncthreads();
    66. if (tid < 64) sdata[tid] += sdata[tid + 64];
    67. __syncthreads();
    68. if (tid < 32) warpReduce(sdata, tid);
    69. if (tid == 0) y[bid] = sdata[0];
    70. }
    71. __global__ void dot_stg_3(FLOAT *x, int N)
    72. {
    73. __shared__ FLOAT sdata[128];
    74. int tid = threadIdx.x;
    75. int i;
    76. sdata[tid] = 0;
    77. /* load data to shared mem */
    78. for (i = 0; i < N; i += 128) {
    79. if (tid + i < N) sdata[tid] += x[i + tid];
    80. }
    81. __syncthreads();
    82. /* reduction using shared mem */
    83. if (tid < 64) sdata[tid] = sdata[tid] + sdata[tid + 64];
    84. __syncthreads();
    85. if (tid < 32) warpReduce(sdata, tid);
    86. if (tid == 0) x[0] = sdata[0];
    87. }
    88. /* dz and d serve as cache: result stores in d[0] */
    89. void dot_device(FLOAT *dx, FLOAT *dy, FLOAT *dz, FLOAT *d, int N)
    90. {
    91. /* 1D block */
    92. int bs = 256;
    93. /* 2D grid */
    94. int s = ceil(sqrt((N + bs - 1.) / bs));
    95. dim3 grid = dim3(s, s);
    96. int gs = 0;
    97. /* stage 1 */
    98. dot_stg_1<<>>(dx, dy, dz, N);
    99. /* stage 2 */
    100. {
    101. /* 1D grid */
    102. int N2 = (N + bs - 1) / bs;
    103. int s2 = ceil(sqrt((N2 + bs - 1.) / bs));
    104. dim3 grid2 = dim3(s2, s2);
    105. dot_stg_2<<>>(dz, d, N2);
    106. /* record gs */
    107. gs = (N2 + bs - 1.) / bs;
    108. }
    109. /* stage 3 */
    110. dot_stg_3<<<1, 128>>>(d, gs);
    111. }
    112. int main(int argc, char **argv)
    113. {
    114. int N = 10000070;
    115. int nbytes = N * sizeof(FLOAT);
    116. FLOAT *hx = NULL, *hy = NULL;
    117. FLOAT *dx = NULL, *dy = NULL, *dz = NULL, *d = NULL;
    118. int i, itr = 20;
    119. FLOAT asd = 0, ash;
    120. double td, th;
    121. if (argc == 2) {
    122. int an;
    123. an = atoi(argv[1]);
    124. if (an > 0) N = an;
    125. }
    126. /* allocate GPU mem */
    127. cudaMalloc((void **)&dx, nbytes);
    128. cudaMalloc((void **)&dy, nbytes);
    129. cudaMalloc((void **)&dz, sizeof(FLOAT) * ((N + 255) / 256));
    130. cudaMalloc((void **)&d, sizeof(FLOAT) * ((N + 255) / 256));
    131. if (dx == NULL || dy == NULL || dz == NULL || d == NULL) {
    132. printf("couldn't allocate GPU memory\n");
    133. return -1;
    134. }
    135. printf("allocated %e MB on GPU\n", nbytes / (1024.f * 1024.f));
    136. /* alllocate CPU mem */
    137. hx = (FLOAT *) malloc(nbytes);
    138. hy = (FLOAT *) malloc(nbytes);
    139. if (hx == NULL || hy == NULL) {
    140. printf("couldn't allocate CPU memory\n");
    141. return -2;
    142. }
    143. printf("allocated %e MB on CPU\n", nbytes / (1024.f * 1024.f));
    144. /* init */
    145. for (i = 0; i < N; i++) {
    146. hx[i] = 1;
    147. hy[i] = 2;
    148. }
    149. /* copy data to GPU */
    150. cudaMemcpy(dx, hx, nbytes, cudaMemcpyHostToDevice);
    151. cudaMemcpy(dy, hy, nbytes, cudaMemcpyHostToDevice);
    152. /* let dust fall */
    153. cudaDeviceSynchronize();
    154. td = get_time();
    155. /* call GPU */
    156. for (i = 0; i < itr; i++) dot_device(dx, dy, dz, d, N);
    157. /* let GPU finish */
    158. cudaDeviceSynchronize();
    159. td = get_time() - td;
    160. th = get_time();
    161. for (i = 0; i < itr; i++) ash = dot_host(hx, hy, N);
    162. th = get_time() - th;
    163. /* copy data from GPU */
    164. cudaMemcpy(&asd, d, sizeof(FLOAT), cudaMemcpyDeviceToHost);
    165. printf("dot, answer: %d, calculated by GPU:%f, calculated by CPU:%f\n", 2 * N, asd, ash);
    166. printf("GPU time: %e, CPU time: %e, speedup: %g\n", td, th, th / td);
    167. cudaFree(dx);
    168. cudaFree(dy);
    169. cudaFree(dz);
    170. cudaFree(d);
    171. free(hx);
    172. free(hy);
    173. return 0;
    174. }

  • 相关阅读:
    框架外的PHP读取.env文件(php5.6、7.3可用版)
    shell脚本使用(宿主机windows-服务器-centos)--用于使用shell脚本方式控制docker容器
    jdk 下载 ,开发工具下载 [jdk1.8.0_251.zip]
    [附源码]java毕业设计篮球装备商城系统
    【鸿蒙(HarmonyOS)】UI开发的两种范式:ArkTS、JS(以登录界面开发为例进行对比)
    【Linux】VirtualBox安装Centos7
    Spring-Cloud-Alibaba-SEATA源码解析(三)(客户端)
    使用 Python 和机器学习掌握爬虫和情感分析
    Java 顺序控制、分支控制、循环控制详解
    Python基础:【习题系列】列表、元组、字典和集合
  • 原文地址:https://blog.csdn.net/qq_45788429/article/details/133950846