内存层次结构:GPU的全局内存、共享内存、寄存器、L1/L2缓存

好,咱们今天聊聊GPU的内存层次。说白了,这就是一张“存储金字塔”。

我刚开始做CUDA优化那会儿,总觉得显存带宽够大,随便读写就行。结果一跑大模型,直接卡成PPT。后来才明白——你写的每一行代码,都在跟这个内存层次打交道。不理解它,优化就无从谈起。

一、全局内存:最慢,但容量最大

全局内存,就是GPU板载的显存。HBM2、GDDR6这些,都属于它。

  • 容量:通常8GB~80GB,取决于显卡
  • 带宽:HBM2e约1.6TB/s,GDDR6约900GB/s
  • 延迟:400~800个时钟周期

嗯,这里要注意:全局内存的延迟是寄存器的几百倍。你想想看,如果每个数据都去全局内存里拿,那你的kernel基本就在“等数据”中度过。

核心原则:尽量减少全局内存访问次数。能合并访问就合并,能缓存就缓存。

我在项目中遇到过一个大矩阵转置的算子,原始版本每次读写都是随机地址,带宽利用率不到10%。后来改成合并访问(coalesced access),性能直接翻了8倍。说白了,就是让相邻线程访问相邻地址,这样硬件可以一次搬一大块数据。

二、共享内存:片上SRAM,程序员可控

共享内存是GPU上的一块片上SRAM,每个线程块独享一份。延迟只有几十个周期,比全局内存快一个数量级。

特性 值
容量 每个SM通常48KB~164KB(可配置)
延迟 约20~30个时钟周期
带宽 极高(每个SM内部)
作用域 线程块内所有线程共享

共享内存最大的价值在于:数据复用。比如做矩阵乘法时,每个数据会被多个线程用到。如果每次都从全局内存读,那就太浪费了。正确的做法是:先把数据搬进共享内存,然后所有线程从共享内存里读。

我的习惯:写kernel之前,先画一张数据流图。标清楚哪些数据是“一次读多次用”的,这些就是共享内存的候选者。

我曾经踩过一个坑:共享内存用太多,导致每个SM能同时运行的线程块数量减少,反而降低了并行度。嗯,这里要平衡——共享内存不是越多越好,够用就行。

三、寄存器:最快,但最稀缺

寄存器是GPU里最快的存储单元,没有之一。延迟只有1个时钟周期。但每个线程能用的寄存器数量有限——通常每个SM有65536个寄存器,但分到每个线程头上,也就几十个。

为什么会这样?因为GPU要同时运行成千上万个线程。如果每个线程用太多寄存器,那能同时运行的线程数就会减少,导致occupancy(占用率)下降。

注意:编译器有时会“溢出”寄存器到本地内存(local memory),而本地内存其实是在全局内存里的!这会导致性能骤降。我曾经在编译日志里看到“spill”字样,一查发现寄存器用了128个,远超限制,结果性能掉了40%。

我的建议是:写kernel时,尽量控制局部变量的数量。能用int就别用double,能用数组索引就别用临时变量。编译器不是万能的,它有时候会帮你“优化”出很多寄存器变量。

四、L1/L2缓存:硬件自动管理

L1和L2缓存是硬件自动管理的,程序员不需要显式操作。但理解它们的行为,对优化很有帮助。

  • L1缓存:每个SM独享,通常128KB(可配置为共享内存或L1)
  • L2缓存:所有SM共享,通常几MB到几十MB

L1缓存和共享内存其实是共用同一块片上存储的。你可以通过配置来调整它们的比例。比如,如果你的kernel需要大量共享内存,就把L1调小;如果数据局部性好,就把L1调大。

我记得有一次优化一个卷积算子,发现L1命中率只有30%。后来把数据访问模式改成空间局部性更好的方式,L1命中率提升到80%,性能直接翻倍。嗯,这就是硬件缓存的好处——你不需要手动管理,但你要“迎合”它的工作方式。

五、一张图看懂内存层次

下面这张SVG图,把整个内存层次结构画清楚了。你可以看到:越往上,速度越快,容量越小;越往下,速度越慢,容量越大。

寄存器 ~1周期延迟 共享内存 / L1 ~20-30周期延迟 L2 缓存 ~100-200周期延迟 全局内存 (HBM/GDDR) ~400-800周期延迟 速度更快 容量更大

这张图我每次讲课都会放。你想想看,优化本质上就是让数据尽量往金字塔上层移动。把频繁访问的数据放到寄存器或共享内存里,把不常用的数据留在全局内存里。

六、实战建议:如何选择存储层次

我总结了一个简单的决策流程,你可以参考:

  1. 先看数据复用次数:如果每个数据只被一个线程用一次,那就直接从全局内存读,别折腾。
  2. 再看数据共享范围:如果数据需要在线程块内共享,用共享内存;如果只是线程内复用,用寄存器。
  3. 最后看容量限制:共享内存和寄存器都是稀缺资源。用之前算一下,别超了。

避坑指南:我曾经为了省事,把所有数据都塞进共享内存,结果每个SM只能跑一个线程块,occupancy掉到12.5%。后来改成“只缓存复用次数多的数据”,occupancy回到50%,性能反而更好。记住:并行度比单块速度更重要。

好了,关于内存层次结构,今天就聊这么多。记住一句话:数据在哪里,性能就在哪里。把数据放在正确的位置,你的算子自然就快了。

小练习:打开你的Nsight Compute,看看你的kernel里,全局内存访问次数和共享内存访问次数的比例。如果全局内存访问占比超过80%,那就有优化空间了。

想要更多资料,联系可可