当前位置: 首页 > news >正文

【CUDA 】第3章 CUDA执行模型——3.5循环展开(3)

CUDA C编程笔记

  • 第三章 CUDA执行模型
    • 3.5 循环展开
      • 3.5.2 展开 线程的归约(最后一个warp内32个线程展开)
        • 最后一个warp手动规约+一个线程块手动处理8个数据块
      • 3.5.3 (交错归约)完全展开 的归约
        • 手动展开交错归约+最后一个warp手动规约+一个线程块手动处理8个数据块
      • 3.5.4 模板函数 的归约
        • 模板函数+手动展开交错归约+最后一个warp手动规约+一个线程块手动处理8个数据块

待解决的问题:

第三章 CUDA执行模型

3.5 循环展开

3.5.2 展开 线程的归约(最后一个warp内32个线程展开)

__syncthreads()是用于块内同步的,在归约核函数中,用来保证线程进入下一轮之前,每一轮的所有线程都把局部结果写入全局内存。

只剩下一个线程束时(线程<=32),因为线程束执行是SIMT单指令多线程,每条指令之后有隐式的warp内同步过程。归约循环的最后6个迭代(最后一个warp内)可以用语句展开。

if(tid < 32){volatile int *vmem = idata;vmem[tid] += vmem[tid + 32];vmem[tid] += vmem[tid + 16];vmem[tid] += vmem[tid + 8];vmem[tid] += vmem[tid + 4];vmem[tid] += vmem[tid + 2];vmem[tid] += vmem[tid + 1];
}

这个线程束的展开避免了执行循环控制和线程同步逻辑

volatile关键字:表明编译器对访问该变量的代码就不再进行优化,告诉编译器每次赋值的时候把vmem[tid]的值存回全局内存中。如果省略volatile关键字,就不能正常工作,因为编译器或缓存会对全局/共享内存优化读写
如果全局/共享内存中的变量有volatile修饰,编译器假定它的值可以被其它线程任何时间修改使用,所以volatile修饰的变量强制读/写内存,而不是读写缓存或寄存器

代码与reduceUnrolling8相比的区别:在写回最后的结果之前多了上面展开的代码,对最后一个warp块内进行手动线程规约,对应上面的就地归约中,限制条件也需要改成stride>32

最后一个warp手动规约+一个线程块手动处理8个数据块
//展开线程规约4:手动展开八个块block的处理+最后一个块内手动规约
__global__ void reduceUnrollWarps8(int *g_idata, int *g_odata, unsigned int n){//设置线程idunsigned int tid = threadIdx.x;unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;//全局数据指针转换为当前块内数据指针int *idata = g_idata + blockIdx.x * blockDim.x * 8;//展开八个数据块,八个数据块加到第一个位置if(idx + 7 * blockDim.x < n){int a1 = g_idata[idx];int a2 = g_idata[idx + blockDim.x];int a3 = g_idata[idx + blockDim.x * 2];int a4 = g_idata[idx + blockDim.x * 3];int a5 = g_idata[idx + blockDim.x * 4];int a6 = g_idata[idx + blockDim.x * 5];int a7 = g_idata[idx + blockDim.x * 6];int a8 = g_idata[idx + blockDim.x * 7];g_idata[idx] = a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8;}__syncthreads();//就地归约,一个数据块内for(int stride = blockDim.x/2; stride > 32; stride >>= 1){if(tid < stride){idata[tid] += idata[tid + stride];}__syncthreads();}//【新增!!!】展开warp,当归约到最后一个warp时,手动展开if(tid < 32){volatile int *vmem = idata;//volatile关键字说明编译器不对该变量优化vmem[tid] += vmem[tid + 32];vmem[tid] += vmem[tid + 16];vmem[tid] += vmem[tid + 8];vmem[tid] += vmem[tid + 4];vmem[tid] += vmem[tid + 2];vmem[tid] += vmem[tid + 1];}//把这个块的结果写回全局内容if(tid == 0) g_odata[blockIdx.x] = idata[0];
}

对应的main函数代码也需要修改:

//kernel7:reduceUnrollWarps8 展开规约5:一个线程块处理八个数据块cudaMemcpy(d_idata, h_idata, bytes, cudaMemcpyHostToDevice);cudaDeviceSynchronize();iStart = seconds();//因为现在每个线程块处理两个数据块,需要调整内核的执行配置,网格大小减为原来的一半reduceUnrollWarps8<<<grid.x/8, block>>>(d_idata, d_odata, size);//这里写错成了bytescudaDeviceSynchronize();iElaps = seconds() - iStart;cudaMemcpy(h_odata, d_odata, grid.x/8*sizeof(int), cudaMemcpyDeviceToHost);//需要除2gpu_sum = 0;for(int i = 0; i<grid.x/8; i++){//需要除gpu_sum += h_odata[i];}printf("gpu reduceUnrollWarps8  elapsed %f ms gpu_sum: %d <<<grid %d block %d>>>\n", iElaps, gpu_sum, grid.x/8, block.x);//需要除2

执行结果:

./3-3reduceInteger2 starting reduction atdevice 0: NVIDIA GeForce RTX 3090 with array size 16777216 grid 32768 block 512
cpu reduce         elapsed 0.041231 ms cpu_sum: 2139353471
gpu Warmup          elapsed 0.031330 ms gpu_sum: 0 <<<grid 32768 block 512>>>
gpu Neighbored          elapsed 0.000483 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu NeighboredLess      elapsed 0.000276 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceInterleaved   elapsed 0.000239 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceUnrolling2    elapsed 0.000156 ms gpu_sum: 2139353471 <<<grid 16384 block 512>>>
gpu reduceUnrolling4    elapsed 0.000125 ms gpu_sum: 2139353471 <<<grid 8192 block 512>>>
gpu reduceUnrolling8    elapsed 0.000118 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceUnrollWarps8  elapsed 0.000119 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>

存在的问题:书上说比reduceUnrolling8快,但是实际上并没有,和书上结论不一致

reduceUnrollWarps8更快的原因是由于__syncthreads()的同步,更少线程束阻塞。
说明:__syncthreads()能减少核函数中的阻塞。

3.5.3 (交错归约)完全展开 的归约

如果编译时已知一个循环中的迭代次数,可以把循环完全展开。
因为Fermi或Kep1er架构中,每个块的最大线程数是1024,并且归约核函数中循环迭代次数是基于一个块block的维度。

把原来交错归约的步长自动迭代for循环具体手写。

    //【新增!!!】把就地归约步长迭代,完全展开if(blockDim.x >= 1024 && tid < 512) idata[tid] += idata[tid + 512];//线程块大小>1024时,前512个线程把1024个元素归约为512个元素__syncthreads();if(blockDim.x >= 512 && tid < 256) idata[tid] += idata[tid + 256];//线程块大小>512时,前256个线程把512个元素归约为256个元素__syncthreads();if(blockDim.x >= 256 && tid < 128) idata[tid] += idata[tid + 128];//线程块大小>256时,前128个线程把256个元素归约为128个元素__syncthreads();if(blockDim.x >= 128 && tid < 64) idata[tid] += idata[tid + 64];//线程块大小>128时,前64个线程把128个元素归约为64个元素__syncthreads();//tid此时已经<64了,<32的情况下面处理
手动展开交错归约+最后一个warp手动规约+一个线程块手动处理8个数据块
//完全展开的规约5:手动展开八个块block的处理+最后一个块内手动规约
__global__ void reduceCompleteUnrollWrap8(int *g_idata, int *g_odata, unsigned int n){//设置线程idunsigned int tid = threadIdx.x;unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;//全局数据指针转换为当前块内数据指针int *idata = g_idata + blockIdx.x * blockDim.x * 8;//展开八个数据块,八个数据块加到第一个位置if(idx + blockDim.x * 7 < n){int a1 = g_idata[idx];int a2 = g_idata[idx + blockDim.x];int a3 = g_idata[idx + blockDim.x * 2];int a4 = g_idata[idx + blockDim.x * 3];int a5 = g_idata[idx + blockDim.x * 4];int a6 = g_idata[idx + blockDim.x * 5];int a7 = g_idata[idx + blockDim.x * 6];int a8 = g_idata[idx + blockDim.x * 7];g_idata[idx] = a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8;}__syncthreads();//【新增!!!】把就地归约步长迭代,完全展开if(blockDim.x >= 1024 && tid < 512) idata[tid] += idata[tid + 512];//线程块大小>1024时,前512个线程把1024个元素归约为512个元素__syncthreads();if(blockDim.x >= 512 && tid < 256) idata[tid] += idata[tid + 256];//线程块大小>512时,前256个线程把512个元素归约为256个元素__syncthreads();if(blockDim.x >= 256 && tid < 128) idata[tid] += idata[tid + 128];//线程块大小>256时,前128个线程把256个元素归约为128个元素__syncthreads();if(blockDim.x >= 128 && tid < 64) idata[tid] += idata[tid + 64];//线程块大小>128时,前64个线程把128个元素归约为64个元素__syncthreads();//tid此时已经<64了,<32的情况下面处理//展开warp,当归约到最后一个warp时,手动展开if(tid < 32){volatile int *vmem = idata;vmem[tid] += vmem[tid + 32];vmem[tid] += vmem[tid + 16];vmem[tid] += vmem[tid + 8];vmem[tid] += vmem[tid + 4];vmem[tid] += vmem[tid + 2];vmem[tid] += vmem[tid + 1];}//把这个块的结果写回全局内容if(tid == 0) g_odata[blockIdx.x] = idata[0];
}

执行结果:

./3-3reduceInteger2 starting reduction atdevice 0: NVIDIA GeForce RTX 3090 with array size 16777216 grid 32768 block 512
cpu reduce         elapsed 0.039004 ms cpu_sum: 2139353471
gpu Warmup          elapsed 0.038595 ms gpu_sum: 0 <<<grid 32768 block 512>>>
gpu Neighbored          elapsed 0.001485 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu NeighboredLess      elapsed 0.001385 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceInterleaved   elapsed 0.001355 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceUnrolling2    elapsed 0.000496 ms gpu_sum: 2139353471 <<<grid 16384 block 512>>>
gpu reduceUnrolling4    elapsed 0.000456 ms gpu_sum: 2139353471 <<<grid 8192 block 512>>>
gpu reduceUnrolling8    elapsed 0.000908 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceUnrollWarps8  elapsed 0.000190 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceCompleteUnrollWrap8  elapsed 0.000469 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>

书上说比reduceUnrollWarps8又有了小加速,实际运行并没有

3.5.4 模板函数 的归约

在手动展开交错归约的基础上,用模板函数进一步减少分支消耗,CUDA支持模板参数,指定块大小作为模板函数的参数。

与上一个代码的区别
参数模板iBlockSize代替原来的块大小blockDim.x。检查块大小的if语句在编译的时候会被评估,如果该条件为false,编译时就删除,循环更有效率。
例如:块大小永远是256,下面语句永远是false,直接不进入该归约步骤,编译器自动从执行内核中移除它。
②调用的时候用switch-case语句,编译器可以为不同大小的代码块自动优化代码。

iBlockSize >= 1024 && tid < 512

关键代码修改:

    //【新增改变!!!】把就地归约步长迭代,完全展开+iBlockSize代替原来if(iBlockSize >= 1024 && tid < 512) idata[tid] += idata[tid + 512];//线程块大小>1024时,前512个线程把1024个元素归约为512个元素__syncthreads();if(iBlockSize >= 512 && tid < 256) idata[tid] += idata[tid + 256];//线程块大小>512时,前256个线程把512个元素归约为256个元素__syncthreads();if(iBlockSize >= 256 && tid < 128) idata[tid] += idata[tid + 128];//线程块大小>256时,前128个线程把256个元素归约为128个元素__syncthreads();if(iBlockSize >= 128 && tid < 64) idata[tid] += idata[tid + 64];//线程块大小>128时,前64个线程把128个元素归约为64个元素__syncthreads();//tid此时已经<64了,<32的情况下面处理
模板函数+手动展开交错归约+最后一个warp手动规约+一个线程块手动处理8个数据块
//完全展开的规约6:模板参数替换块大小+手动展开八个块block的处理+最后一个块内手动规约
template <unsigned int iBlockSize>
__global__ void reduceCompleteUnroll(int *g_idata, int *g_odata, unsigned int n){//设置线程idunsigned int tid = threadIdx.x;unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;//全局数据指针转换为当前块内数据指针int *idata = g_idata + blockIdx.x * blockDim.x * 8;//展开八个数据块,八个数据块加到第一个位置if(idx + blockDim.x * 7 < n){int a1 = g_idata[idx];int a2 = g_idata[idx + blockDim.x];int a3 = g_idata[idx + blockDim.x * 2];int a4 = g_idata[idx + blockDim.x * 3];int a5 = g_idata[idx + blockDim.x * 4];int a6 = g_idata[idx + blockDim.x * 5];int a7 = g_idata[idx + blockDim.x * 6];int a8 = g_idata[idx + blockDim.x * 7];g_idata[idx] = a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8;}__syncthreads();//【新增改变!!!】把就地归约步长迭代,完全展开+iBlockSize代替原来if(iBlockSize >= 1024 && tid < 512) idata[tid] += idata[tid + 512];//线程块大小>1024时,前512个线程把1024个元素归约为512个元素__syncthreads();if(iBlockSize >= 512 && tid < 256) idata[tid] += idata[tid + 256];//线程块大小>512时,前256个线程把512个元素归约为256个元素__syncthreads();if(iBlockSize >= 256 && tid < 128) idata[tid] += idata[tid + 128];//线程块大小>256时,前128个线程把256个元素归约为128个元素__syncthreads();if(iBlockSize >= 128 && tid < 64) idata[tid] += idata[tid + 64];//线程块大小>128时,前64个线程把128个元素归约为64个元素__syncthreads();//tid此时已经<64了,<32的情况下面处理//展开warp,当归约到最后一个warp时,手动展开if(tid < 32){volatile int *vmem = idata;vmem[tid] += vmem[tid + 32];vmem[tid] += vmem[tid + 16];vmem[tid] += vmem[tid + 8];vmem[tid] += vmem[tid + 4];vmem[tid] += vmem[tid + 2];vmem[tid] += vmem[tid + 1];}//把这个块的结果写回全局内容if(tid == 0) g_odata[blockIdx.x] = idata[0];
}

对应main函数调用也修改:

    //kernel9:reduceCompleteUnroll 展开规约5:一个线程块处理八个数据块//调用方式修改为在switch case结构cudaMemcpy(d_idata, h_idata, bytes, cudaMemcpyHostToDevice);cudaDeviceSynchronize();iStart = seconds();//改变【新增!!!】允许编译器为特定线程块大小自动优化代码switch(blocksize){case 1024:reduceCompleteUnroll<1024><<<grid.x/8, block>>>(d_idata, d_odata, size);break;case 512:reduceCompleteUnroll<512><<<grid.x/8, block>>>(d_idata, d_odata, size);break;case 256:reduceCompleteUnroll<256><<<grid.x/8, block>>>(d_idata, d_odata, size);break;case 128:reduceCompleteUnroll<128><<<grid.x/8, block>>>(d_idata, d_odata, size);break;case 64:reduceCompleteUnroll<64><<<grid.x/8, block>>>(d_idata, d_odata, size);break;}//因为现在每个线程块处理两个数据块,需要调整内核的执行配置,网格大小减为原来的一半//reduceCompleteUnroll<<<grid.x/8, block>>>(d_idata, d_odata, size);//这里写错成了bytescudaDeviceSynchronize();iElaps = seconds() - iStart;cudaMemcpy(h_odata, d_odata, grid.x/8*sizeof(int), cudaMemcpyDeviceToHost);//需要除2gpu_sum = 0;for(int i = 0; i<grid.x/8; i++){//需要除gpu_sum += h_odata[i];}printf("gpu reduceCompleteUnroll  elapsed %f ms gpu_sum: %d <<<grid %d block %d>>>\n", iElaps, gpu_sum, grid.x/8, block.x);//需要除2

执行结果:

./3-3reduceInteger2 starting reduction atdevice 0: NVIDIA GeForce RTX 3090 with array size 16777216 grid 32768 block 512
cpu reduce         elapsed 0.040380 ms cpu_sum: 2139353471
gpu Warmup          elapsed 0.053873 ms gpu_sum: 0 <<<grid 32768 block 512>>>
gpu Neighbored          elapsed 0.000478 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu NeighboredLess      elapsed 0.000273 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceInterleaved   elapsed 0.000235 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceUnrolling2    elapsed 0.000157 ms gpu_sum: 2139353471 <<<grid 16384 block 512>>>
gpu reduceUnrolling4    elapsed 0.000124 ms gpu_sum: 2139353471 <<<grid 8192 block 512>>>
gpu reduceUnrolling8    elapsed 0.000117 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceUnrollWarps8  elapsed 0.000120 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceCompleteUnrollWrap8  elapsed 0.000119 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceCompleteUnroll  elapsed 0.000119 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>

在这里插入图片描述
最大的性能增益是reduceUnrolling8核函数获得的,每个线程规约前处理8个数据块,有8个独立的内存访问,更好让内存带宽饱和,并且还能隐藏加载/存储延迟。


http://www.mrgr.cn/news/98518.html

相关文章:

  • 音视频小白系统入门笔记-0
  • 【强化学习漫谈】3.RLHF:从Reward Model到DPO
  • 代码随想录算法训练营Day30
  • C#中async await异步关键字用法和异步的底层原理
  • 怎么看英文论文 pdf沉浸式翻译
  • (二)Graspnet在mujoco的仿真复现(操作记录)
  • linux多线(进)程编程——(7)消息队列
  • Leetcode 2814. 避免淹死并到达目的地的最短时间【Plus题】
  • 第IV部分有效应用程序的设计模式
  • STM32F103_HAL库+寄存器学习笔记15 - 梳理CAN发送失败时,涉及哪些寄存器
  • 实战指南:封装Whisper为FastAPI接口并实现高并发处理-附整合包
  • 欧拉服务器操作系统安装MySQL
  • 单片机 + 图像处理芯片 + TFT彩屏 触摸开关控件 v1.2
  • Transformer-PyTorch实战项目——文本分类
  • (劳特巴赫调试器学习笔记)四、Practice脚本.cmm文件编写
  • C++第三方库【JSON】nlohman/json
  • Cribl 数据脱敏 -02 (附 测试数据)
  • 如何评估cpu的理论FLOPS能力
  • Windows 下 MongoDB ZIP 版本安装指南
  • libaom 码率控制实验:从理论到实践的深度探索