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

CUDA自学笔记01—Reduction规约求和

目录

  • 备注
  • Reduction规约求和
  • Reduce朴素版本
  • Reduce1——使用共享内存
  • Reduce2——去除分支发散和除法操作
    • bank conflict计算
  • Reduce3——减少Bank Conflicts
  • Reduce4——减少空闲线程
  • Reduce5——warp展开
  • Reduce6——for循环进一步展开
  • Reduce7——网格步幅循环加载
  • Reduce8——warp shuffle
  • 参考文献

备注

这个笔记是在自学过程的一些记录,中间包含了很多个人的理解以及问AI的回答,因为找不到很系统的教程,官方文档有时候不会讲的特别细,比方说某个指标的具体计算过程网上也没找到什么资料,所以在这里记录下,后续自己有了新的理解或者发现写的不对的地方都会在这不断更新,如有不对的地方,还请指正。

Reduction规约求和

Reduction就是对一组数据经过一些操作变换成一个结果,这里以求和为例进行记录。


每个数加上它右边间隔s的数,开始s=1,每迭代一轮s*2,迭代log₂ⁿ轮,n是数据量,这里假设n是2的幂次。迭代完后第一个数就是求和结果。

测试过程数据是4000000个int的求和,然后默认blocksize=512,显卡是RTX 4070 Super。

Reduce朴素版本


kernel代码如下:

__global__voidreduce_naive(int*g_idata,int*g_odata){inttid=threadIdx.x;inti=blockIdx.x*blockDim.x+threadIdx.x;for(ints=1;s<blockDim.x;s*=2){if(tid%(2*s)==0){g_idata[i]+=g_idata[i+s];}__syncthreads();}if(tid==0)g_odata[blockIdx.x]=g_idata[blockIdx.x*blockDim.x];}

这段代码相当于在每个block的512个thread里面做规约,g_odata里面是每个block里512个数据的和,最后把g_odata求和就得到最终结果。

Reduce1——使用共享内存

我们都知道,共享内存能够帮助我们减少内存搬运耗时,因为它是片上内存,我们可以先把数据搬运到共享内存上再去计算。

kernel代码如下:

__global__voidreduce1(int*g_idata,int*g_odata){extern__shared__intsdata[];inttid=threadIdx.x;inti=blockIdx.x*blockDim.x+threadIdx.x;sdata[tid]=g_idata[i];__syncthreads();for(ints=1;s<blockDim.x;s*=2){if(tid%(2*s)==0){sdata[tid]+=sdata[tid+s];}__syncthreads();}if(tid==0)g_odata[blockIdx.x]=sdata[0];}

这里我用Nsight Compute分析对比上面两个kernel时发现耗时没有明显降低,然后上网搜索了一下说是Naive的全局显存数据请求到数据后会暂存在L2 cache中复用,所以相对于共享内存版本耗时差异不大。从下表中可以看到Naive版本的L2 cache throughput很高。(PS:这里其实没有完全理解,在这里Mark一下。我当前的猜测是因为大部分操作数据在内存上都是相邻的,一次事务可以取32个4字节的int数,所以内存事务复用率较高,所以和共享内存版本差异不大)

Reduce2——去除分支发散和除法操作

Reduce1有两个问题,一个是分支发散比较严重,另一个是’%'操作比较耗时,需要优化一下。

__global__voidreduce2(int*g_idata,int*g_odata){extern__shared__intsdata[];inttid=threadIdx.x;inti=blockIdx.x*blockDim.x+threadIdx.x;sdata[tid]=g_idata[i];__syncthreads();for(ints=1;s<blockDim.x;s*=2){intidx=2*s*tid;if(idx<blockDim.x){sdata[idx]+=sdata[idx+s];}__syncthreads();}if(tid==0)g_odata[blockIdx.x]=sdata[0];}

从下表可以看出我们实现了规约操作,且减少了分支发散和除法操作,但是这里带来了一个新的问题就是引入了Bank conflict。例如第一轮迭代tid=0的线程会读取idx=0的数据,tid=16的线程会读取idx=32的数据,而这两个地址的数据在同一Bank0(idx%32=0)里面,这就相当于不同线程在读取同一Bank的不同地址的数据,这就是Bank conflict,导致并行变成串行,增加耗时。不过相比于前面的kernel,还是能看到耗时明显降低,说明之前的kernel分支发散和除法操作比bank conflict更耗时。

bank conflict计算

先按一个512个int、blocksize=512的demo计算bank conflicts

指标定义参考Nvidia Nsight Compute官方文档


对于512个线程,每个线程处理一个int数据,可以分为512/32=16个warp

Share Store:
1、对于sdata[tid] = g_idata[i];这是在给共享内存写入,所以对应Share Store,总共16个warp,对应16个Instructions和Requests;这里由于都是连续写入,所以不存在bank conflicts,wavefronts=16

2、对于sdata[idx] += sdata[idx + s],第一次迭代也就是s=1的时候是只在idx——0~255的索引下写入,所以只有8个warp,总共9次迭代依次是8、4、2、1、1、1、1、1、1个warp在做共享内存写入,所以对应8+4+2+1+1+1+1+1+1=20个Instructions和Requests;

这里我举了s=1和s=16时的索引,可以看到s=1时,idx和idx+s都是间隔2的索引,就是一个2-way的bank conflict,一个warp就需要2次transactions,依次类推s=2时4-way的bank conflict需要4次transactions,而到了s=16时,注意tid=16时,idx已经到了512,索引已经超出了,超出的这部分是无效的,虽然这个warp是32-way的,但是我只需要发送16次transactions就满足了,后续迭代以此类推。最终得到1次load或1次store的wavefronts=16+16+16+16+16+8+4+2+1=95

综合下来Share Store Instructions和Requests = 16 + 20 = 36,wavefronts=95+16=111

Share Load:
1、对于sdata[idx] += sdata[idx + s],分析思路和Share Store一致,不过有个区别是这里每个线程读取是有两个地址idx和idx+s,所以Share Load Instructions和Requests = 20 + 20 = 40;wavefronts参考前面的Store分析wavefronts=95*2=190(乘2是因为做了两次读取)

2、if (tid == 0) g_odata[blockIdx.x] = sdata[0];这行语句也有一个读取共享内存操作,不过只做一次; sdata[0]相当于做了一次内存读取,也就相当于一次wavefront
综合下来Share Load Instructions和Requests = 40 + 1 = 41,wavefronts=190+1=191

而bank conflicts就相当于额外的wavefronts,结果也就等于总的wavefronts减去请求的requests
Share Load Bank Conflicts = 191 - 41 = 150
Share Store Bank Conflicts = 111 - 36 = 75
注意到这里Load的冲突数刚好是Store的两倍,这是因为发生冲突的语句sdata[idx] += sdata[idx + s]刚好是两次读取一次写入。

我后面又尝试了不同尺寸的输入,发现Share Load wavefronts不能通过单个block的wavefronts * 总的blocks计算(而且每次跑可能会不一样,虽然相比于总数不会差太多),其他几个都可以。问了下AI说是Wavefronts 是流水线层面的“物理动作”。在 GPU 的硬件流水线中,当某几个 Warp 的执行由于某些外部原因(例如抢占、上下文切换、或规约中的 __syncthreads() 同步导致暂停再恢复时),LSU 阶段可能为了确保数据的危害(Hazard)检测通过,在极个别周期发生了物理重复请求(Retry)。这种极低概率的硬件自发抖动,会被物理计数器记录下来。

Reduce3——减少Bank Conflicts

我们可以将求和的间隔由小到大改成由大到小,如下图所示,这样就可以避免绝大多数的Bank conflict

可以看到Bank Conflits明显减少

Reduce4——减少空闲线程

前面的Reduce方法有比较多的线程冗余,比如说我们每个block开了512个线程,但是后面256个线程只做了一次数据搬运就没有其他操作了,第一轮迭代只有前面256个线程在做加法运算运行,后面256个线程就都空置了,后续每次迭代空置的线程也就越来越多。
为了把线程利用起来,可以在把数据往共享内存搬运的时候就做一次加法,比方说我一个block还是512个线程,但是我用这512个线程处理1024个数据,在把数据从全局内存搬运到共享内存的时候,我读取第0个数据和第512个数据,把他们相加然后写入共享内存,然后一直到第511个数据和第1023个数据,把他们相加然后写入共享内存,这样就相当于每个线程都做了一次加法操作,相比上面的Reduce把空闲线程也做了利用。

__global__voidreduce4(int*g_idata,int*g_odata,intsize){extern__shared__intsdata[];inttid=threadIdx.x;inti=blockIdx.x*(blockDim.x*2)+threadIdx.x;if(i<size)sdata[tid]=g_idata[i]+g_idata[i+blockDim.x];elsesdata[tid]=0;__syncthreads();for(ints=blockDim.x/2;s>0;s>>=1){if(tid<s){sdata[tid]+=sdata[tid+s];}__syncthreads();}if(tid==0)g_odata[blockIdx.x]=sdata[0];}

这里需要注意,由于我们每个block是用512个线程处理1024个数据,所以总的block数相比于之前的Reduce要减半。

可以看到kernel耗时显著降低

Reduce5——warp展开

我们知道一个warp里面有32个线程,且是同步执行的,所以当间隔s从大间隔降到32的时候,我们可以不用 __syncthreads()命令,直接把内部规约展开,注意kenel中for循环中止条件从s>0变成了s>32

__device__voidwrapReduce5(volatileint*sdata,inttid){sdata[tid]+=sdata[tid+32];sdata[tid]+=sdata[tid+16];sdata[tid]+=sdata[tid+8];sdata[tid]+=sdata[tid+4];sdata[tid]+=sdata[tid+2];sdata[tid]+=sdata[tid+1];}__global__voidreduce5(int*g_idata,int*g_odata,intsize){extern__shared__intsdata[];inttid=threadIdx.x;inti=blockIdx.x*(blockDim.x*2)+threadIdx.x;if(i<size)sdata[tid]=g_idata[i]+g_idata[i+blockDim.x];elsesdata[tid]=0;__syncthreads();for(ints=blockDim.x/2;s>32;s>>=1){if(tid<s){sdata[tid]+=sdata[tid+s];}__syncthreads();}if(tid<32)wrapReduce5(sdata,tid);if(tid==0)g_odata[blockIdx.x]=sdata[0];}

可以看到kernel耗时进一步降低

Reduce6——for循环进一步展开

可以进一步展开kernel中的for循环

template<unsignedintblockSize>__device__voidwrapReduce(volatileint*sdata,inttid){if(blockSize>=64)sdata[tid]+=sdata[tid+32];if(blockSize>=32)sdata[tid]+=sdata[tid+16];if(blockSize>=16)sdata[tid]+=sdata[tid+8];if(blockSize>=8)sdata[tid]+=sdata[tid+4];if(blockSize>=4)sdata[tid]+=sdata[tid+2];if(blockSize>=2)sdata[tid]+=sdata[tid+1];}template<unsignedintblockSize>__global__voidreduce6(int*g_idata,int*g_odata,intsize){extern__shared__intsdata[];inttid=threadIdx.x;inti=blockIdx.x*(blockDim.x*2)+threadIdx.x;if(i<size)sdata[tid]=g_idata[i]+g_idata[i+blockDim.x];elsesdata[tid]=0;__syncthreads();// 展开循环if(blockSize>=1024){if(tid<512){sdata[tid]+=sdata[tid+512];}__syncthreads();}if(blockSize>=512){if(tid<256){sdata[tid]+=sdata[tid+256];}__syncthreads();}if(blockSize>=256){if(tid<128){sdata[tid]+=sdata[tid+128];}__syncthreads();}if(blockSize>=128){if(tid<64){sdata[tid]+=sdata[tid+64];}__syncthreads();}// Warp 级优化if(tid<32){// 强制转换为 volatile 指针传递,确保读写可见性wrapReduce<blockSize>((volatileint*)sdata,tid);}if(tid==0)g_odata[blockIdx.x]=sdata[0];}

不过耗时并没有太大变化,但是Mark Harris当时测试时有一定提升,猜测是这些年硬件有一定的迭代优化

Reduce7——网格步幅循环加载

Mark Harris文档里最后还提了一个网格步幅循环加载(Grid-Stride Loop)的规约,原理是考虑数据量很大,然后以有限的block有限的线程去做运算,就相当于每个线程处理ceil(size/gridsize)个数据

template<unsignedintblockSize>__global__voidreduce7(int*g_idata,int*g_odata,intsize){extern__shared__intsdata[];inttid=threadIdx.x;inti=blockIdx.x*(blockSize*2)+threadIdx.x;intgridSize=blockSize*2*gridDim.x;sdata[tid]=0;while(i<size){sdata[tid]+=g_idata[i];if(i+blockSize<size){// 防止越界sdata[tid]+=g_idata[i+blockSize];}i+=gridSize;}__syncthreads();// 展开循环if(blockSize>=1024){if(tid<512){sdata[tid]+=sdata[tid+512];}__syncthreads();}if(blockSize>=512){if(tid<256){sdata[tid]+=sdata[tid+256];}__syncthreads();}if(blockSize>=256){if(tid<128){sdata[tid]+=sdata[tid+128];}__syncthreads();}if(blockSize>=128){if(tid<64){sdata[tid]+=sdata[tid+64];}__syncthreads();}// Warp 级优化if(tid<32){// 强制转换为 volatile 指针传递,确保读写可见性wrapReduce<blockSize>((volatileint*)sdata,tid);}if(tid==0)g_odata[blockIdx.x]=sdata[0];}

这里我就没去调节block数了,直接和上面的kernel用的一样的block数

Reduce8——warp shuffle

现在的CUDA有个shuffle的功能支持warp间不同线程做一些数据处理,等价于把之前的取数相加赋值改成__shfl_down_sync函数调用

template<unsignedintblockSize>__global__voidreduce8(constint*__restrict__ g_idata,int*__restrict__ g_odata,intsize){extern__shared__intsdata[];unsignedinttid=threadIdx.x;unsignedintidx=blockIdx.x*(blockSize*2)+tid;intsum=0;// 处理不能整除情况if(idx<size)sum+=g_idata[idx];if(idx+blockSize<size)sum+=g_idata[idx+blockSize];sdata[tid]=sum;__syncthreads();//--------------------------------------------------// Block Reduction//--------------------------------------------------if(blockSize>=1024){if(tid<512)sdata[tid]+=sdata[tid+512];__syncthreads();}if(blockSize>=512){if(tid<256)sdata[tid]+=sdata[tid+256];__syncthreads();}if(blockSize>=256){if(tid<128)sdata[tid]+=sdata[tid+128];__syncthreads();}if(blockSize>=128){if(tid<64)sdata[tid]+=sdata[tid+64];__syncthreads();}//--------------------------------------------------// Warp Reduction using shuffle//--------------------------------------------------if(tid<32){intval=sdata[tid];if(blockSize>=64)val+=sdata[tid+32];for(intoffset=16;offset>0;offset>>=1){val+=__shfl_down_sync(0xffffffff,val,offset);}if(tid==0)g_odata[blockIdx.x]=val;}}

不过后面几种Reduce方法耗时都比较接近了

参考文献

Optimizing Parallel Reduction in CUDA

http://www.jsqmd.com/news/983544/

相关文章:

  • AI大模型时代已来!小白程序员收藏,抓住高薪新机遇
  • 深入解析ARM MCU外设时序:从I2C、SDHC到I2S的电气规格与工程实践
  • Qwen3.6-35B-A3B_最新代码模型vLLM高效部署
  • 如何用JPEXS Free Flash Decompiler轻松解密和编辑SWF文件:完整指南
  • 总结速度差14倍 从14分钟缩至1分钟2026实测分钟搞定知识视频总结这个实用技
  • ZXPInstaller:3分钟搞定Adobe插件安装的免费开源方案
  • 2026太原高二低分逆袭秘诀,高三全托冲刺提分攻略 - 信息热点
  • 服装货源+AI穿搭教学一站式攻略,这个车内穿搭博主藏了太多干货 - 信息热点
  • 收藏!CRUD程序员轻松转型AI大模型应用开发,高薪未来等你来
  • [ZJOI2019] 开关 (视觉和谐版)
  • 2026上海GEO(AI搜索优化)公司哪家好?
  • Java学生成绩管理系统实战项目:含可运行源码、MySQL建库脚本与完整功能界面
  • Bandcamp音乐收藏自动化备份方案:专业级批量下载工具深度解析
  • PUBG雷达系统:5分钟搭建战场信息可视化平台
  • 深入解析S12 BDM调试模式:硬件命令、固件命令与安全机制
  • 台州专业的漏水检测公司推荐2026年6月最新 - 信息热点
  • Cognition发布FrontierCode:突破现有局限,精准衡量AI代码“可合并性”
  • 2026 年鹤壁市优质财税公司实力排行榜 专业财税服务机构综合测评推荐 - GrowthUME
  • 嵌入式硬件设计:从数据手册电气特性到稳定可靠的MCU系统实战
  • 3分钟搞定Windows和Office激活:KMS_VL_ALL_AIO智能激活脚本完全指南
  • 如何快速集成Qt PDF查看器:QPDF Widget完整指南
  • [AI in Education] 上海高中语文作文评分 prompt
  • 明日方舟自动助手:告别重复操作,解放你的游戏时间
  • 无锡 2026 瓷砖空鼓翘边拱起原因及解决办法 免砸砖快速修复 - 苏易房屋修缮
  • 寄快递哪家便宜?寄半折寄件5折起 - 快递物流资讯
  • OnmyojiAutoScript:阴阳师自动化脚本,每天为你节省2小时游戏时间
  • 图论建模入门:把‘放黄油’问题变成最短路径,手把手教你解决信息学奥赛典型题
  • 2026年电脑蓝屏不再愁,河源启科安联教你轻松解决 - 信息热点
  • RBAC 详解(基于角色的访问控制)
  • 2026 年 6 月 Riverhawk/GMN/NHBB 轴承官方授权代理商 苏州德胜恩全国现货直供 - GrowthUME