3. 显存层次结构:GPU显存体系

聊GPU显存,我习惯把它想象成一个「金字塔」。

塔尖最快,但容量最小。塔基最慢,但容量最大。你写的每一行CUDA代码,本质上都在跟这个金字塔打交道。搞不懂它,吞吐优化就是空中楼阁。

3.1 金字塔的四层结构

先看这张图,我手绘的,帮你建立整体认知:

GPU显存层次结构金字塔 寄存器 ~20 周期 L1 缓存 / Shared Memory ~30 周期 | 几十KB~上百KB L2 缓存 ~200 周期 | 几MB~几十MB HBM / GDDR(全局显存) ~400~800 周期 | 几十GB 速度:快 速度:慢 容量:小 容量:大

说白了,这就是一个「快小贵」vs「慢大便宜」的博弈。你想想看,如果所有数据都能塞进寄存器,那GPU早就飞起来了。但现实是——寄存器就那么一丁点。

3.2 各层级详解

寄存器(Register)

速度:~20个时钟周期。这是GPU里最快的地方,没有之一。

容量:每个线程最多255个32位寄存器(以NVIDIA为例)。注意,是「每个线程」。一个SM里有几千个线程在跑,寄存器总量是固定的,你用得越多,能同时跑的线程就越少。

我踩过的坑:有一次我写一个矩阵乘法的kernel,为了省事把所有中间变量都声明成float,结果寄存器溢出(spill)到了local memory。性能直接掉了40%。后来我手动复用寄存器,把一些中间结果算完就扔,才把性能拉回来。

关键点:寄存器溢出是性能杀手。编译器有时候会帮你做spill,但代价极高——因为spill出去的数据实际上走的是全局显存路径。

L1缓存 / Shared Memory

速度:~30个时钟周期。比寄存器慢一点点,但比L2快一个数量级。

容量:每个SM独立拥有。以A100为例,每个SM有192KB的L1+Shared Memory总和,你可以通过配置来分配多少给L1、多少给Shared Memory。

我个人习惯:如果数据需要在线程块内共享,我会优先用Shared Memory。如果只是单纯的数据重用,交给L1缓存自动管理就好。

小技巧:在NVIDIA GPU上,你可以用 cudaFuncSetAttribute 来设置每个线程块能用的Shared Memory上限。别设太大,否则SM能同时跑的线程块数会减少。

L2缓存

速度:~200个时钟周期。比L1慢,但比HBM快得多。

容量:所有SM共享。A100有40MB,H100有50MB。嗯,这个容量其实挺大的,很多模型参数都能塞进去。

避坑指南:我曾经以为L2缓存是「透明的」,不用管它。后来发现,如果多个SM频繁访问同一块显存地址,L2的命中率会很高。但如果每个SM都访问不同的地址,L2就变成了一个昂贵的「搬运工」。所以,数据访问模式要尽量让L2命中。

HBM / 全局显存

速度:~400~800个时钟周期。这是最慢的一层,但容量最大。

容量:几十GB。A100有40GB/80GB,H100有80GB。大模型训练全靠它撑着。

说白了,全局显存就是你的「仓库」。所有数据最终都存在这里。但每次从仓库取货,都要花几百个周期。所以,尽量减少对全局显存的直接访问,这是吞吐优化的核心原则之一。

3.3 速度与容量的量化对比

我整理了一张表,方便你直观对比:

层级 典型延迟(周期) 典型容量(A100) 作用域
寄存器 ~20 每个线程 255 个 线程私有
L1 / Shared Memory ~30 每个 SM 192KB 线程块内共享
L2 缓存 ~200 40 MB 全局共享
HBM 全局显存 ~400~800 40 GB / 80 GB 全局共享

你看这个延迟差距:寄存器到HBM,差了40倍。这意味着,如果你能从HBM里少读一次数据,换成从寄存器里读,性能就能提升40倍。当然,这是理想情况,但方向是对的。

3.4 实战中的层次协同

光知道层级没用,你得会用。我分享一个实际案例:

之前优化一个Attention kernel。原始版本直接读HBM里的Q、K、V矩阵,每次计算都从全局显存取数。性能惨不忍睹。

我的优化思路:

  1. 把Q矩阵的一整块数据加载到Shared Memory(L1层),让线程块内的所有线程共享。
  2. 把K矩阵分块加载到寄存器,每个线程只负责自己需要的部分。
  3. 计算过程中,中间结果全部留在寄存器里,绝不写回HBM。
  4. 最终结果才写回HBM。

结果呢?吞吐提升了3倍。说白了,就是让数据尽量待在金字塔的上层,少去底层「旅游」。

注意:不要盲目把所有数据都塞进Shared Memory。Shared Memory容量有限,塞满了会导致线程块并发数下降,反而拖慢性能。我见过有人为了省事,把整个矩阵都加载到Shared Memory,结果SM只能跑1个线程块,利用率直接腰斩。

3.5 总结一下

显存层次结构,说白了就是一套「缓存体系」。你写的代码,决定了数据在这个金字塔里怎么流动。

  • 寄存器:最快,但极小。别让变量溢出。
  • L1/Shared Memory:次快,线程块内共享。手动管理能带来巨大收益。
  • L2:全局共享,自动管理。注意访问模式。
  • HBM:最慢,但容量大。尽量减少直接访问。

嗯,理解了这个层次结构,你再看后面的「吞吐优化」和「显存复用」,就会觉得顺理成章了。因为所有优化技巧,本质上都是在跟这个金字塔做博弈。

一句话记住:让数据待在它该待的地方,别让它在金字塔里乱跑。

想要更多资料,联系可可