1. CUDA显存体系全景

在CUDA性能优化中,绝大多数“程序跑不快”的问题,最终都会落到显存访问上。要判断一段kernel是算力受限还是带宽受限,第一步不是改代码,而是先建立一张清晰的显存层级地图:数据放在哪里、离SM有多远、访问一次要多少周期、能提供多大带宽。本章不涉及具体优化技巧,只做一件事——把这张地图画清楚。

一、为什么显存是分层的

GPU的算力增长远快于显存带宽增长,这个“剪刀差”是分层的根本原因。以典型的现代GPU为例,FP32算力可达数十TFLOPS,而HBM全局显存带宽通常在1~3TB/s量级。若每次运算都直接访问全局显存,SM会长期处于“等数据”状态。于是硬件在SM内部和附近布置了多级容量更小、但延迟更低、带宽更高的存储:寄存器、共享内存/L1、常量缓存、纹理缓存,以及容量大但慢的全局显存和本地内存。

理解分层的关键是三个维度:物理位置(在SM内、SM旁还是片外)、访问延迟(约多少个时钟周期)、有效带宽(单位时间能搬多少字节)。三者通常此消彼长:越靠近计算单元,越快越小越贵。

二、各层级存储的定位与差异

寄存器(Register)是速度最快的存储,位于SM的寄存器文件中,由编译器直接分配。访问延迟约1个周期,几乎不构成瓶颈。但每个线程可用寄存器数量有限(如每线程最多255个),寄存器用量过高会降低occupancy,甚至把变量“溢出”到本地内存。

共享内存(Shared Memory)位于SM内部,是片上可编程的高速暂存区,典型延迟约20~30周期。它按block分配,常用于线程间数据复用、矩阵分块、归约等。共享内存存在bank冲突问题:32个bank若被同一warp的多个线程同时访问同一bank的不同地址,访问会被串行化。

L1/L2缓存是硬件自动管理的缓存。L1与共享内存共享同一块物理SRAM(可配置划分),L2为全GPU共享,容量更大但延迟更高。它们对程序员透明,命中率取决于访问局部性。

常量内存(Constant Memory)位于片外,但有专用常量缓存。当同一warp内所有线程访问同一地址时,广播效率极高,接近寄存器速度;若线程访问不同地址,则退化为串行。适合存放kernel参数、系数表等只读且warp内一致的数据。

纹理内存(Texture Memory)同样位于片外,通过纹理缓存访问,针对二维空间局部性做了优化,并支持硬件插值、边界处理等。适合图像、网格等具有空间邻接关系的只读数据。

全局内存(Global Memory)是容量最大的显存,所有线程可见,延迟约400~800周期。它是带宽优化的主战场,访问是否合并(coalesced)直接决定有效带宽。

本地内存(Local Memory)名字有误导性:它并不在SM本地,而是全局显存的一部分,只是被编译器用来存放寄存器溢出的变量、动态索引数组等。因此本地内存访问同样慢,且容易造成隐蔽的性能损失。

CUDA 显存层级:位置 · 延迟 · 带宽 SM 内部(片上) 寄存器 ~1 周期 · 最快 每线程私有 共享内存 / L1 ~20–30 周期 block 内共享 常量缓存 warp 内同址广播 异址则串行 纹理缓存 二维空间局部性 只读数据 L2 缓存(全 GPU 共享) 容量较大 · 延迟高于 L1 · 硬件自动管理 片外显存(HBM / GDDR) 全局内存 ~400–800 周期 容量最大 · 带宽主战场 本地内存 寄存器溢出 / 动态索引 物理上仍在显存 常量 / 纹理数据 片外存储本体 经专用缓存访问 延迟增大 容量增大

三、延迟与带宽的数量级直觉

把上面的层级换算成“人走一步”的直觉:如果访问寄存器相当于从桌上拿笔(1秒),访问共享内存相当于走到隔壁房间(约半分钟),访问全局显存则相当于去另一个城市取件(约十几分钟)。这个比喻虽不精确,但足以说明:优化的本质是让数据尽量在近处被反复使用,而不是频繁往返远处。

带宽方面,全局显存带宽是硬上限。以一块假设带宽为1.5TB/s的GPU为例,若一个kernel需要读取4GB数据且只读一次,理论最短时间约为4GB / 1.5TB/s ≈ 2.7ms。若实测耗时远高于此,说明要么访问未合并导致有效带宽下降,要么存在大量重复读取。共享内存和寄存器的作用,正是把“重复读取”从全局显存转移到片上。

四、一张表看清各层级

存储类型物理位置典型延迟可见范围典型用途
寄存器SM 内~1 周期线程私有标量运算、循环变量
共享内存SM 内~20–30 周期block 内共享分块复用、归约
L1 / L2 缓存SM 内 / 全 GPU数十~百周期硬件管理自动缓存全局访问
常量内存片外 + 常量缓存命中时接近寄存器全局只读kernel 参数、系数表
纹理内存片外 + 纹理缓存低于全局全局只读图像、空间邻接数据
全局内存片外 HBM~400–800 周期全局可读写主数据存储
本地内存片外(实为全局)同全局线程私有寄存器溢出、动态数组
判断口诀:先问“数据放在哪一层”,再问“访问是否合并/是否冲突”,最后才问“算力够不够”。多数kernel的性能瓶颈在第二问就已暴露。

五、从这张地图出发