想象您是高档餐厅的后厨主厨。灶台上的盐罐、糖罐放在手边(寄存器),切好的葱姜蒜搁在操作台上(L1),冰鲜食材放在后厨公用冰箱(L2),大宗米面则要从楼下冷库搬上来(HBM)。您不会每做一道菜都跑一趟冷库,而是把最常用的物料放在最趁手的位置。GPU 的存储体系也是一张“后厨备餐地图”:从寄存器到 L1、L2 再到 HBM,容量越来越大、速度越来越慢。理解这条取菜路线,是写出高效 CUDA 程序的第一步。本文借这个后厨比喻,逐层拆解 GPU 存储层级、访存优化与数据驻留策略,让存储不再是黑盒参数。

岗位关联

对 AI 从业者,掌握存储层次是 GPU 性能调优的基石:它能帮你定位访存瓶颈、设计更优 Kernel,是成为 CUDA/HPC 工程师的关键能力。

目录
  1. 可视化演示
  2. 存储层级总览
  3. 寄存器与L1缓存
  4. L2缓存机制
  5. HBM高带宽显存
  6. 访存模式优化
  7. 数据驻留策略
  8. 带宽计算与调优
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

存储层级总览

后厨高峰到来前,主厨会把盐罐放在灶台边,切好的配料放在操作台,半成品放进冰箱,大宗食材留在冷库。每一层“取货距离”不同,出菜速度也因此不同。GPU 的存储体系正是这样一层层“按距离定价”:离计算单元越近,速度越快、容量越小、造价越贵。

这里的“离得近”指的是物理上更靠近 SM(Streaming Multiprocessor,可以想象成一组灶台)。寄存器在 SM 内部,L1 缓存也在 SM 内;L2 缓存是多个 SM 共用的“后厨冰箱”;HBM 则是封装在 GPU 旁的片外显存。为了让数据访问尽量“近水楼台”,设计者把最常访问的数据放在最快的层,这就是局部性原理:程序倾向于反复访问同一批数据和时间上邻近的数据。

可以用平均内存访问时间(AMAT)来统一衡量“取一趟食材到底要多久”:命中时的时间,加上未命中时额外付出的代价。

主厨会把最常用的调料放手边,GPU 也把最常访问的数据放 L1。一旦放错位置,每次做菜都要跑一趟冷库,AMAT 就会飙升,整条流水线都慢下来。

AMAT=HitTime+MissRate×MissPenalty\displaystyle \text{AMAT} = \text{HitTime} + \text{MissRate} \times \text{MissPenalty}
AMAT 的意思是:一次访存的平均耗时 = 命中耗时 + 未命中概率 × 未命中的额外惩罚。它把“快与慢”变成了一个可计算的数字。
  1. 命中时间 HitTime当数据在上一层缓存中直接找到时,所花的时间。
    HitTime\text{HitTime}
  2. 未命中率 MissRate数据不在当前缓存层、需要去下一层找的概率。
    MissRate\text{MissRate}
  3. 未命中惩罚 MissPenalty数据未命中后,从更慢的下一层取数据多付出的时间。
    MissPenalty\text{MissPenalty}
  4. 平均访问时间 AMAT把命中与未命中的代价按概率加权,得到“平均取货时长”。
    AMAT=HitTime+MissRate×MissPenalty\text{AMAT} = \text{HitTime} + \text{MissRate} \times \text{MissPenalty}

寄存器与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,所以即便拥有高带宽,程序员依然要在意“去冷库的次数”,否则再宽的传送带也会排队。

Bandwidth=Clock×BusWidth8\displaystyle \text{Bandwidth} = \frac{\text{Clock} \times \text{BusWidth}}{8}
带宽就好比冷库传送带的运货能力:每秒钟能送多少字节到后厨。位宽越宽、频率越高,传送带越能装货。
  1. 时钟频率 Clock显存工作的基本频率,单位 Hz,表示每秒钟的传输周期数。
    Clock\text{Clock}
  2. 总线位宽 BusWidth一次能并行传输的数据位数,单位 bit。HBM 的位宽往往达数千 bit。
    BusWidth\text{BusWidth}
  3. 除以 8把 bit 换算成字节 Byte,因为 1 Byte = 8 bit。
    /8/8
  4. 带宽 Bandwidth理论上每秒能搬运的最大字节数,单位 B/s。
    Bandwidth=Clock×BusWidth8\text{Bandwidth} = \frac{\text{Clock} \times \text{BusWidth}}{8}

访存模式优化

备菜师傅会把同一道菜需要的配料码在一个托盘里,一次端到灶台;而不是每取一根葱就跑一趟。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%,说明传送带已经满载,这时应该用合并访存、共享内存复用、数据驻留等手段减少搬运量;如果计算单元空闲等待,则要增加并行度或减少分支。这就像先看清到底是货梯不够宽还是后厨人手不够,再对症下药。

AI=FLOPsBytes\displaystyle \text{AI} = \frac{\text{FLOPs}}{\text{Bytes}}
算术强度表示“每搬运一个字节,能做多少次浮点运算”。AI 高,说明数据复用充分,瓶颈可能在计算;AI 低,说明大部分时间在等数据,瓶颈在访存。
  1. FLOPs该 Kernel 完成的浮点运算次数。
    FLOPs\text{FLOPs}
  2. Bytes该 Kernel 从全局内存/显存搬运的字节数。
    Bytes\text{Bytes}
  3. 算术强度 AI二者之比,单位通常是 FLOPs/Byte。
    AI=FLOPsBytes\text{AI} = \frac{\text{FLOPs}}{\text{Bytes}}

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 演示

python33 行
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()
目的:模拟 32 个线程在不同访存模式下的缓存事务数,直观展示合并访存带来的带宽效益。
  1. def count_transactions(addresses, transaction_size=32):用集合记录不同事务块,块数越少越好。
  2. blocks.add(addr // transaction_size)把地址映射到所在的 32 字节事务块,同一块内的请求可以合并。
  3. addr_coal = [coalesced_access(t) for t in range(nthreads)]构造连续地址序列,模拟合并访存。
  4. addr_strided = [strided_access(t) for t in range(nthreads)]构造跨步地址序列,模拟糟糕的非合并访存。
  5. co = count_transactions(addr_coal)统计连续访问事务数,结果应为 4。
  6. st = count_transactions(addr_strided)统计跨步访问事务数,结果应为 32。
  7. 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 存储层次中,容量最大、延迟最高的是哪一层?

单选题 · easy

  • A.寄存器
  • B.一级缓存
  • C.二级缓存
  • D.HBM显存← 正确答案

答案:D。HBM 是片外大容量显存,容量达几十 GB,但延迟远高于片上 L1/L2。寄存器、L1、L2 都在片上,容量小、速度快;HBM 对应“冷库”,取一趟最慢。

Q2. 线程私有且访问速度最快的存储是?

单选题 · easy

  • A.寄存器← 正确答案
  • B.共享内存
  • C.全局内存
  • D.常量缓存

答案:A。寄存器是每个线程私有的存储,编译器分配变量后,线程直接在其中读写,通常一个周期即可访问。共享内存是线程块内共享,全局内存和常量缓存都需要经过更深层次。

Q3. 关于 GPU 中 L2 缓存的描述,正确的是?

单选题 · mid

  • A.单个线程专用
  • B.单个SM独占
  • C.所有SM共享← 正确答案
  • D.独立显卡卡槽

答案:C。L2 缓存是 GPU 中所有 SM 共享的片上缓存,充当全局内存访问的一致性点。它不像 L1 那样每个 SM 一份,因此能跨 SM 协调数据。

Q4. 合并访存的核心思想是?

单选题 · mid

  • A.相邻线程访问相邻地址← 正确答案
  • B.所有线程访问同一地址
  • C.每个线程跳着访问
  • D.只使用32位访存

答案:A。合并访存让线程束的访问落在尽量少的缓存事务内。相邻线程访问相邻、对齐的地址,可把 32 个请求合并成少数事务,避免带宽浪费。

Q5. HBM 相比 L2 缓存最大的优势是?

单选题 · hard

  • A.访问延迟更低
  • B.存储容量更大← 正确答案
  • C.线程私有访问
  • D.片上实现

答案:B。HBM 作为片外 3D 堆叠 DRAM,容量可达几十 GB,远超 L2 的几十 MB,能容纳大模型权重。但速度更慢,需要缓存和优化来掩盖延迟。

Q6. 某 Kernel 的算术强度很高,意味着它更可能是?

单选题 · mid

  • A.访存密集
  • B.计算密集← 正确答案
  • C.延迟敏感
  • D.带宽受限

答案:B。算术强度 = FLOPs / Bytes。数值高表示每搬运一个字节做了大量计算,通常瓶颈在计算单元;数值低则说明访存主导,需要优化访存模式。

Q7. 下列哪些手段能减少对 HBM 的重复访问?

多选题 · mid

  • A.使用共享内存复用数据← 正确答案
  • B.让线程束合并访存← 正确答案
  • C.增加循环次数
  • D.禁用L2缓存

答案:AB。共享内存复用把数据反复使用但只从 HBM 读一次;合并访存提高每次事务效率。增加循环次数并不等于减少访问,禁用 L2 反而会让 HBM 请求更多。

Q8. AMAT 公式中 MissPenalty 表示?

单选题 · mid

  • A.命中所需时间
  • B.未命中额外开销← 正确答案
  • C.缓存总容量
  • D.总线工作频率

答案:B。MissPenalty 是数据未命中时要到下一层取数据付出的额外时间。它乘上 MissRate 再加 HitTime,得到平均访存时间。

Q9. 为什么寄存器访问速度极快?

单选题 · hard

  • A.线程私有且单周期访问← 正确答案
  • B.容量非常大
  • C.位于片外内存
  • D.可以动态增长

答案:A。寄存器在 SM 内部、线程独占,硬件以最快路径访问。它容量极小且由编译器静态分配,不能动态增长,但速度优势无可替代。

Q10. 关于 GPU L2 缓存的一致性机制,正确的有?

多选题 · extreme

  • A.充当全局一致性点← 正确答案
  • B.每个SM各自独立
  • C.由硬件协议维护← 正确答案
  • D.完全取代HBM

答案:AC。L2 为所有 SM 提供统一的全局视图,通过一致性协议(如 ATOMS)保证读写次序。它不独立于 SM,也无法替代 HBM 的大容量。