内存层次结构: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图,把整个内存层次结构画清楚了。你可以看到:越往上,速度越快,容量越小;越往下,速度越慢,容量越大。
这张图我每次讲课都会放。你想想看,优化本质上就是让数据尽量往金字塔上层移动。把频繁访问的数据放到寄存器或共享内存里,把不常用的数据留在全局内存里。
六、实战建议:如何选择存储层次
我总结了一个简单的决策流程,你可以参考:
- 先看数据复用次数:如果每个数据只被一个线程用一次,那就直接从全局内存读,别折腾。
- 再看数据共享范围:如果数据需要在线程块内共享,用共享内存;如果只是线程内复用,用寄存器。
- 最后看容量限制:共享内存和寄存器都是稀缺资源。用之前算一下,别超了。
避坑指南:我曾经为了省事,把所有数据都塞进共享内存,结果每个SM只能跑一个线程块,occupancy掉到12.5%。后来改成“只缓存复用次数多的数据”,occupancy回到50%,性能反而更好。记住:并行度比单块速度更重要。
好了,关于内存层次结构,今天就聊这么多。记住一句话:数据在哪里,性能就在哪里。把数据放在正确的位置,你的算子自然就快了。
小练习:打开你的Nsight Compute,看看你的kernel里,全局内存访问次数和共享内存访问次数的比例。如果全局内存访问占比超过80%,那就有优化空间了。
想要更多资料,联系可可