CUDA分块2D卷积性能劣于不分块版本的问题排查与代码咨询
老兄,我仔细扒了你的分块卷积代码和测试场景,为啥分块版本反而干不过不分块的,这里面有几个核心问题,我给你拆解清楚,再给你点可落地的优化建议:
一、性能反超的核心原因
1. 大量线程摸鱼,硬件资源浪费严重
你设置的tile_width = BLOCK_WIDTH - mask_width + 1,比如mask宽9的时候,tile_width就变成24了。这意味着你每个32x32的block里,只有24x24的线程会真的参与输出计算,剩下的448个线程只干了加载共享内存Halo元素这一件事,之后就彻底闲置了。Tesla T4的SM可是有一堆CUDA核心的,这么多线程摸鱼,硬件利用率直接砍半,性能能好才怪,反而不如不分块版本全线程干活来得高效。
2. 共享内存的收益被额外开销抵消了
分块卷积本来是靠共享内存复用输入数据,减少全局内存访问来提速的,但你的实现里,每个线程只加载一个共享内存元素,然后要做mask_width²次共享内存访问。当mask比较小的时候(比如3x3),全局内存访问的减少量根本抵不上共享内存加载、__syncthreads()同步这些额外开销。再加上T4的L2缓存足足有16MB,不分块版本的全局内存访问大概率已经被L2高效命中了,分块带来的缓存收益微乎其微,甚至还不如不分块的直接访问快。
3. 共享内存访问有bank冲突,拖慢速度
CUDA的共享内存是按bank划分的,同一个bank的访问会被序列化,影响速度。你的tile是32x32的float数组,按行存储的话,tile[y][x]的内存地址是y*32 + x,x对应bank索引(32个bank,每个4字节)。在卷积循环里,当多个线程访问同一个x值的时候(比如warp里tx相同的线程),就会触发bank冲突,共享内存的访问效率直接下降。
4. 无效的边界加载开销
你的代码里,所有线程都会判断自己是否在矩阵边界内,然后加载数据或者0值。当block在矩阵边缘时,大量线程都在加载无用的0值,这也是一笔不小的额外开销。
二、可落地的优化方向
1. 调整分块策略,让所有线程都干活
别再用32x32的block了,把block尺寸改成tile_width x tile_width,然后共享内存的尺寸设为(tile_width + mask_width - 1) x (tile_width + mask_width - 1)。比如mask宽9时,tile_width=24,block就设为24x24,共享内存还是32x32(和原来一样大),但所有24x24的线程都参与输出计算,没有闲置的。这样线程利用率直接从56%拉到100%,性能肯定会有明显提升。
2. 优化共享内存加载,减少无效操作
可以让每个线程加载多个元素,比如一次加载2x2的输入数据,减少线程的加载次数。另外,边界处理可以更聪明点,只让需要加载Halo元素的线程去做,不用所有线程都判断边界。
3. 解决共享内存bank冲突
给共享内存加个padding就行,比如把tile[BLOCK_WIDTH][BLOCK_WIDTH+1],这样每个行的长度变成33,x索引从0到32,同一列的x值对应的bank索引(x%32)就不一样了,彻底避免bank冲突。
4. 利用CUDA的优化指令
比如用__ldg()加载只读的掩码数据,能提升全局内存的读取效率;卷积计算的时候尝试向量化,用float2或float4批量加载数据,提升吞吐量。
5. 调整block尺寸,找最优配置
不一定非要用正方形的block,T4的SM每个block最多支持1024线程,你可以试试16x16、32x16这些不同的尺寸,说不定能找到更适合的配置。比如16x16的block,每个block有256线程,T4的SM能同时跑更多block,利用率更高。
6. 优化内存初始化与传输
你现在用cudaMallocManaged虽然方便,但有时候不如显式的cudaMalloc+cudaMemcpy高效。可以试试在CPU上初始化矩阵,然后拷贝到GPU,避免在GPU上做循环初始化(GPU的计算单元不擅长干这个)。
三、测试验证建议
先从调整block尺寸开始,把block改成tile_width x tile_width,看看性能有没有提升;然后加上共享内存padding,解决bank冲突;再试试不同的block尺寸,比如16x16、24x24这些。另外,当mask尺寸更大的时候(比如15、21),分块版本的优势应该会更明显,因为这时候全局内存访问的减少量会超过共享内存的开销。
内容的提问来源于stack exchange,提问作者ctrlaltdel




