想象您是高档餐厅的后厨主厨。灶台上的盐罐、糖罐放在手边(寄存器),切好的葱姜蒜搁在操作台上(L1),冰鲜食材放在后厨公用冰箱(L2),大宗米面则要从楼下冷库搬上来(HBM)。您不会每做一道菜都跑一趟冷库,而是把最常用的物料放在最趁手的位置。GPU 的存储体系也是一张“后厨备餐地图”:从寄存器到 L1、L2 再到 HBM,容量越来越大、速度越来越慢。理解这条取菜路线,是写出高效 CUDA 程序的第一步。本文借这个后厨比喻,逐层拆解 GPU 存储层级、访存优化与数据驻留策略,让存储不再是黑盒参数。
对 AI 从业者,掌握存储层次是 GPU 性能调优的基石:它能帮你定位访存瓶颈、设计更优 Kernel,是成为 CUDA/HPC 工程师的关键能力。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
存储层级总览
后厨高峰到来前,主厨会把盐罐放在灶台边,切好的配料放在操作台,半成品放进冰箱,大宗食材留在冷库。每一层“取货距离”不同,出菜速度也因此不同。GPU 的存储体系正是这样一层层“按距离定价”:离计算单元越近,速度越快、容量越小、造价越贵。
这里的“离得近”指的是物理上更靠近 SM(Streaming Multiprocessor,可以想象成一组灶台)。寄存器在 SM 内部,L1 缓存也在 SM 内;L2 缓存是多个 SM 共用的“后厨冰箱”;HBM 则是封装在 GPU 旁的片外显存。为了让数据访问尽量“近水楼台”,设计者把最常访问的数据放在最快的层,这就是局部性原理:程序倾向于反复访问同一批数据和时间上邻近的数据。
可以用平均内存访问时间(AMAT)来统一衡量“取一趟食材到底要多久”:命中时的时间,加上未命中时额外付出的代价。
主厨会把最常用的调料放手边,GPU 也把最常访问的数据放 L1。一旦放错位置,每次做菜都要跑一趟冷库,AMAT 就会飙升,整条流水线都慢下来。
- 命中时间 HitTime当数据在上一层缓存中直接找到时,所花的时间。
- 未命中率 MissRate数据不在当前缓存层、需要去下一层找的概率。
- 未命中惩罚 MissPenalty数据未命中后,从更慢的下一层取数据多付出的时间。
- 平均访问时间 AMAT把命中与未命中的代价按概率加权,得到“平均取货时长”。
寄存器与L1缓存
盐罐是主厨的“私人领地”,别人不会顺手拿走;切好的葱姜蒜放在操作台上,虽然也近,但属于本灶台共享的备料。寄存器就是线程的盐罐,L1 缓存就是操作台。寄存器是每个线程私有的,只有当前线程能读写,因此延迟极低、带宽极大,通常一个时钟周期就能取到。
L1 缓存是 SM 内部的高速缓存,缓存该 SM 最近访问过的全局内存数据。当线程需要某个地址的数据时,先查 L1;命中则快速返回,未命中才去 L2 和 HBM。L1 容量通常是几十 KB,以 128 字节的缓存行为单位加载。编译器会将局部变量优先分配到寄存器,但如果变量太多或生命周期太长,就会“溢出”到局部内存,速度骤降。
寄存器的数量非常有限,典型一个线程最多 255 个;L1 要与共享内存共享同一块片上 SRAM,配置需要权衡。这就像操作台就那么大,摆满了盐罐就放不下备菜。正因如此,写 CUDA 时要注意减少每个线程的寄存器占用,同时提高 L1 命中率,让数据尽量“留在手边”。
L2缓存机制
后厨里有一台公用冰箱,几位灶台师傅都会从里面取腌肉,也会往里放做好的半成品。由于大家都用它,必须有个规则保证“不会拿错”。GPU 的 L2 缓存就是这台公用冰箱,它是多个 SM 共享的片上缓存,也是全局内存访问的一致性点。
L1 未命中后,请求会来到 L2。L2 容量比 L1 大得多,常见几 MB 到几十 MB,带宽也非常可观。更重要的是,L2 要对所有 SM 的读写请求做协调:当一个 SM 修改了某地址,其他 SM 必须看到最新值,否则就像两个厨师拿着同一块肉,一个腌了一个没腌,菜品就不一致了。GPU 用硬件一致性协议维护这份“台账”,让每个 SM 看到的全局内存视图保持一致。
L2 的存在让跨 SM 的数据共享更高效。比如两个线程块都要读同一份权重,第一次从 HBM 取走后,第二次就能从 L2 命中,省掉一次“跑冷库”。不过 L2 也有延迟代价,约为 L1 的好几倍,所以不能指望它替代 L1。设计 Kernel 时,应尽量让数据在 L1 和寄存器中完成复用,把 L2 当作第二道缓冲网。
HBM高带宽显存
冷库是后厨最大的仓库,猪牛羊肉、整袋面粉都放在里面,但每次取货都要坐货梯,来回一趟耗时远高于操作台取菜。HBM(High Bandwidth Memory)就是 GPU 的冷库:容量最大,通常几十 GB,用来存放模型权重、激活值、中间结果等大块数据。
HBM 是堆叠式 DRAM,利用 3D 堆叠和 TSV(硅通孔)把多颗 DRAM 芯片垂直连在一起,再通过硅中介层与 GPU 封装在一块基板上。相比传统 GDDR 显存,HBM 用更小的面积和功耗换来了极高的带宽,例如 HBM2e 单颗即可提供超过 1 TB/s 的带宽。之所以能这么高,除了堆叠带来的短距离连线,还因为它的总线位宽非常宽。
理论带宽可以由频率和位宽估算:
冷库传送带再宽,也不可能比操作台取菜更快。HBM 延迟远高于 L1/L2,所以即便拥有高带宽,程序员依然要在意“去冷库的次数”,否则再宽的传送带也会排队。
- 时钟频率 Clock显存工作的基本频率,单位 Hz,表示每秒钟的传输周期数。
- 总线位宽 BusWidth一次能并行传输的数据位数,单位 bit。HBM 的位宽往往达数千 bit。
- 除以 8把 bit 换算成字节 Byte,因为 1 Byte = 8 bit。
- 带宽 Bandwidth理论上每秒能搬运的最大字节数,单位 B/s。
访存模式优化
备菜师傅会把同一道菜需要的配料码在一个托盘里,一次端到灶台;而不是每取一根葱就跑一趟。GPU 的访存也一样:一个线程束(32 个线程)如果访问连续地址,就可以合并成少数几个内存事务,这就是合并访存(coalesced access)。
具体来说,GPU 以 32 字节为最小内存事务单位。32 个线程各读一个 4 字节整数,如果地址连续,这 128 字节只落在 4 个事务内;如果地址跳着访问,比如每个线程间隔 64 字节,就可能触发 32 个事务,带宽浪费 8 倍。因此,写内核时第一原则是“相邻线程访问相邻地址”,同时尽量做到 16 字节对齐。
除了合并访存,还要注意数据复用:一个数据被多次使用时,先把它从 HBM 请进共享内存,再反复读。另一个常见坑是共享内存的 bank 冲突——共享内存被分成多个 bank,若同一波线程访问同一 bank 的不同地址,就会串行化,等于操作台排队取菜。通过填充或改变映射方式,可以让访问均匀散开。
数据驻留策略
开业前,主厨会把最受欢迎的食材从冷库搬到后厨冰箱,并安排专人负责补货。数据驻留策略的本质,就是人工决定“什么数据放在哪一层、何时搬、何时清”,从而减少对慢速层的访问。
CUDA 中,这类策略通常由程序员显式控制。例如,用 __ldg 指令走只读数据缓存,用 cudaMemcpyAsync 在后台异步把数据从 HBM 拷贝到共享内存,或者使用 cp.async 指令实现异步预取。更进阶的还有 L2 驻留控制 API,允许把某些数据“钉”在 L2 中,避免被其他访问挤出去。
一个经典案例是分块矩阵乘法:把 A 和 B 的小块先搬进共享内存,让同一线程块的多次计算反复复用,从而把访存量从 O(N³) 降到 O(N³/B),B 为分块边长。这相当于把一整袋土豆搬上操作台,而不是每次炒菜都去冷库挖一个土豆。数据驻留策略的核心就是提高复用率,让每次 HBM 访问都“物有所值”。
带宽计算与调优
冷库货梯的运力是固定的,订单再多也只能一趟一趟送。判断一个 Kernel 是“被计算拖累”还是“被访存拖累”,常用算术强度(Arithmetic Intensity)这个指标:
调优时,先用 Profiler(如 NVIDIA Nsight Compute)查看内存吞吐利用率、L2 命中率、HBM 带宽利用率等指标。如果 HBM 带宽利用率接近 100%,说明传送带已经满载,这时应该用合并访存、共享内存复用、数据驻留等手段减少搬运量;如果计算单元空闲等待,则要增加并行度或减少分支。这就像先看清到底是货梯不够宽还是后厨人手不够,再对症下药。
- FLOPs该 Kernel 完成的浮点运算次数。
- Bytes该 Kernel 从全局内存/显存搬运的字节数。
- 算术强度 AI二者之比,单位通常是 FLOPs/Byte。
AI 应用场景
大模型训练显存规划
阶段:模型训练与资源规划
解决的问题:大模型权重动辄几十 GB,HBM 容量有限,传统做法只能盲目调小 batch size,导致训练效率低。借助存储层次思想,可将权重驻留在 HBM,把频繁访问的优化器状态和中间激活按热度分层,冷热数据分开放置,减少 OOM。
具体案例:训练 70B 参数模型时,将权重用低精度格式驻留 HBM,激活值通过重计算和异步拷贝按需往返,同时利用 L2 缓存常驻热点 embedding 表,避免每个 step 都去 HBM 重复读取。
LLM推理KV Cache访存优化
阶段:推理部署与延迟优化
解决的问题:生成式推理中 KV Cache 反复读写,若地址布局零散,会产生大量非合并访存,拖慢解码。利用合并访存与数据驻留策略,将 KV Cache 按连续块组织,让同一线程束读取相邻 key/value,可显著降低 HBM 访问次数。
具体案例:在 vLLM 等推理引擎中,将 KV Cache 分块并保证块内地址连续,配合 PagedAttention,既减少显存碎片,又让访存事务尽量合并,吞吐量提升可达 2 倍以上。
科学计算矩阵乘 Kernel 优化
阶段:Kernel 开发与 HPC 优化
解决的问题:矩阵乘是典型访存密集型算子,naive 实现会反复读全局内存。通过分块(tiling)把子块搬进共享内存,再配合寄存器复用,可以大幅降低对 HBM 的依赖。
具体案例:在 GPU 上实现 SGEMM,用 16×16 分块,每个线程负责多个输出元素,共享内存加载后复用 16 次,最终 HBM 访问量减少一个数量级,性能逼近理论峰值。
Python 演示
import math def count_transactions(addresses, transaction_size=32): """统计一组访存地址触发的缓存事务数""" blocks = set() for addr in addresses: blocks.add(addr // transaction_size) return len(blocks) def coalesced_access(thread_id): """相邻线程访问连续4字节地址""" return thread_id * 4 def strided_access(thread_id): """相邻线程访问间隔64字节地址""" return thread_id * 64 def main(): nthreads = 32 addr_coal = [coalesced_access(t) for t in range(nthreads)] addr_strided = [strided_access(t) for t in range(nthreads)] co = count_transactions(addr_coal) st = count_transactions(addr_strided) print(f"Coalesced : {co} transactions") print(f"Strided : {st} transactions") print(f"Amplification: {st / co:.1f}x") if __name__ == "__main__": main()
def count_transactions(addresses, transaction_size=32):用集合记录不同事务块,块数越少越好。blocks.add(addr // transaction_size)把地址映射到所在的 32 字节事务块,同一块内的请求可以合并。addr_coal = [coalesced_access(t) for t in range(nthreads)]构造连续地址序列,模拟合并访存。addr_strided = [strided_access(t) for t in range(nthreads)]构造跨步地址序列,模拟糟糕的非合并访存。co = count_transactions(addr_coal)统计连续访问事务数,结果应为 4。st = count_transactions(addr_strided)统计跨步访问事务数,结果应为 32。print(f"Amplification: {st / co:.1f}x")输出事务放大倍数,说明跨步访存浪费了多少带宽。
常见误区
误区我一直以为 HBM 和 L1/L2 一样都在 GPU 芯片内部,只是离得远一点。
正解HBM 是片外 DRAM,通过堆叠和硅中介层与 GPU 封装在一起,物理上和计算核心隔了不止一层。它的延迟远高于 L2,属于“仓库”而非“后厨冰箱”。把它当作容量极大的缓存层会导致延迟预估错误。
为何常见:因为 HBM 和 GPU 封装在一起,容易被误认为片上资源;实际上只有 L1/L2 等 SRAM 才在片上。
误区既然 L2 能减少 HBM 访问,那把 L2 配到最大,性能就一定最好。
正解缓存越大,访问延迟和功耗也越高,而且芯片面积有限。配置 L2 或共享内存需要匹配工作集大小和局部性特征,盲目调大反而可能让命中时间变长,得不偿失。
为何常见:直觉上“大缓存=少 miss”,但忽略了更大缓存需要更长的寻址和比较时间。
误区只要线程访问的是相邻的地址,不管数据类型和起始地址,都算合并访存。
正解合并访存的核心是同一线程束尽量落在同一个 32 字节事务块内。比如 32 个线程各读 8 字节 double,连续地址共 256 字节,需要 8 个事务,仍然合理;但若每个线程访问双精度数组的偶数下标,事务数就会翻倍。此外,起始地址没对齐也会破坏合并。
为何常见:初学时常把“相邻”理解成“挨着”,忽略了硬件事务粒度和对齐要求。
误区共享内存比 HBM 快,所以把能放的数据都放进去,访存瓶颈就解决了。
正解共享内存容量有限(几十 KB),放入过多数据会溢出或导致 occupancy 下降;同时 bank 冲突会让共享内存访问串行化。数据是否值得驻留,要比较它的复用次数和驻留成本。
为何常见:只看到共享内存快,却忽略它是稀缺且有冲突代价的资源。
误区HBM 带宽已经超过 1TB/s,怎么访存都够用,不用费心优化。
正解带宽高不代表延迟低,GPU 需要大量并行线程才能掩盖 HBM 延迟;而且很多 kernel 的算术强度很低,实际带宽利用率远低于峰值。非合并访存还会让有效带宽暴跌。优化访存模式永远有收益。
为何常见:被纸面峰值带宽迷惑,忽略了延迟、事务放大和访问模式对实际吞吐的影响。
面试题
Q1. GPU 存储层次中,容量最大、延迟最高的是哪一层?
- A.寄存器
- B.一级缓存
- C.二级缓存
- D.HBM显存← 正确答案
答案:D。HBM 是片外大容量显存,容量达几十 GB,但延迟远高于片上 L1/L2。寄存器、L1、L2 都在片上,容量小、速度快;HBM 对应“冷库”,取一趟最慢。
Q2. 线程私有且访问速度最快的存储是?
- A.寄存器← 正确答案
- B.共享内存
- C.全局内存
- D.常量缓存
答案:A。寄存器是每个线程私有的存储,编译器分配变量后,线程直接在其中读写,通常一个周期即可访问。共享内存是线程块内共享,全局内存和常量缓存都需要经过更深层次。
Q3. 关于 GPU 中 L2 缓存的描述,正确的是?
- A.单个线程专用
- B.单个SM独占
- C.所有SM共享← 正确答案
- D.独立显卡卡槽
答案:C。L2 缓存是 GPU 中所有 SM 共享的片上缓存,充当全局内存访问的一致性点。它不像 L1 那样每个 SM 一份,因此能跨 SM 协调数据。
Q4. 合并访存的核心思想是?
- A.相邻线程访问相邻地址← 正确答案
- B.所有线程访问同一地址
- C.每个线程跳着访问
- D.只使用32位访存
答案:A。合并访存让线程束的访问落在尽量少的缓存事务内。相邻线程访问相邻、对齐的地址,可把 32 个请求合并成少数事务,避免带宽浪费。
Q5. HBM 相比 L2 缓存最大的优势是?
- A.访问延迟更低
- B.存储容量更大← 正确答案
- C.线程私有访问
- D.片上实现
答案:B。HBM 作为片外 3D 堆叠 DRAM,容量可达几十 GB,远超 L2 的几十 MB,能容纳大模型权重。但速度更慢,需要缓存和优化来掩盖延迟。
Q6. 某 Kernel 的算术强度很高,意味着它更可能是?
- A.访存密集
- B.计算密集← 正确答案
- C.延迟敏感
- D.带宽受限
答案:B。算术强度 = FLOPs / Bytes。数值高表示每搬运一个字节做了大量计算,通常瓶颈在计算单元;数值低则说明访存主导,需要优化访存模式。
Q7. 下列哪些手段能减少对 HBM 的重复访问?
- A.使用共享内存复用数据← 正确答案
- B.让线程束合并访存← 正确答案
- C.增加循环次数
- D.禁用L2缓存
答案:AB。共享内存复用把数据反复使用但只从 HBM 读一次;合并访存提高每次事务效率。增加循环次数并不等于减少访问,禁用 L2 反而会让 HBM 请求更多。
Q8. AMAT 公式中 MissPenalty 表示?
- A.命中所需时间
- B.未命中额外开销← 正确答案
- C.缓存总容量
- D.总线工作频率
答案:B。MissPenalty 是数据未命中时要到下一层取数据付出的额外时间。它乘上 MissRate 再加 HitTime,得到平均访存时间。
Q9. 为什么寄存器访问速度极快?
- A.线程私有且单周期访问← 正确答案
- B.容量非常大
- C.位于片外内存
- D.可以动态增长
答案:A。寄存器在 SM 内部、线程独占,硬件以最快路径访问。它容量极小且由编译器静态分配,不能动态增长,但速度优势无可替代。
Q10. 关于 GPU L2 缓存的一致性机制,正确的有?
- A.充当全局一致性点← 正确答案
- B.每个SM各自独立
- C.由硬件协议维护← 正确答案
- D.完全取代HBM
答案:AC。L2 为所有 SM 提供统一的全局视图,通过一致性协议(如 ATOMS)保证读写次序。它不独立于 SM,也无法替代 HBM 的大容量。