2. 显存架构基础:GPU显存层级结构
做GPU优化,说白了就是在跟显存打交道。
我刚开始接触CUDA时,总觉得显存就是显存,一个池子往里扔数据就行。结果第一次跑大模型训练,直接OOM(Out Of Memory)了。后来才明白,GPU里的显存不是一块铁板,而是一个多层级的金字塔结构。每一层都有自己的脾气——速度、容量、访问方式都不一样。
今天我们就来拆开看看,这个金字塔到底长什么样。
2.1 GPU显存金字塔
先看一张我手绘的结构图,帮你建立整体印象:
这张图我每次培训都会放。你看,从顶层的寄存器到底层的主机内存,速度差了上千倍。你想想看,如果程序频繁访问全局显存,而不知道利用共享显存,那性能损失有多大?
2.2 寄存器:最快但最稀缺
寄存器是GPU里离计算单元最近的地方。每个线程都有自己的私有寄存器,没有延迟,一个周期就能读写。
关键参数:
- 每个SM(流多处理器)通常有64K~128K个32位寄存器
- 每个线程最多可用255个寄存器(不同架构有差异)
- 寄存器是线程私有的,线程之间不能互相访问
有一次我写了一个kernel,每个线程用了200多个寄存器。结果发现占用率只有25%——因为寄存器不够,SM能同时运行的线程数被限制了。后来我把一些局部变量拆成共享显存来存,占用率直接翻倍。
记住:寄存器不是越多越好。用多了会降低并行度。
2.3 共享显存与L1缓存:可编程的加速利器
共享显存(Shared Memory)是我个人最喜欢的特性。它位于芯片上,延迟只有几十个周期,比全局显存快了一个数量级。
两者的关系:
| 特性 | 共享显存 | L1缓存 |
|---|---|---|
| 控制方式 | 程序员手动管理 | 硬件自动管理 |
| 延迟 | ~5周期 | ~30周期 |
| 容量 | 可配置(0~128KB) | 可配置(0~128KB) |
| 典型用途 | 线程间数据共享、归约 | 全局显存访问加速 |
在大多数现代GPU上,共享显存和L1缓存共享同一块物理SRAM。你可以通过编译选项调整它们的比例。比如:
// 配置48KB共享显存 + 16KB L1缓存
cudaDeviceSetCacheConfig(cudaFuncCachePreferShared);
// 或者反过来,16KB共享显存 + 48KB L1缓存
cudaDeviceSetCacheConfig(cudaFuncCachePreferL1);
如果你做的是矩阵乘法、卷积这类有规律的数据复用,果断用共享显存。如果是随机访问模式,交给L1缓存更省心。
2.4 L2缓存:全局显存的缓冲层
L2缓存是所有SM共享的。它的作用很简单——减少全局显存的访问次数。
我记得有一次优化一个推荐系统模型,发现L2缓存的命中率只有30%。这意味着70%的访问都要去全局显存里拿数据,带宽根本不够用。后来通过调整数据布局,把命中率提到了70%,性能直接翻倍。
L2缓存的关键点:
- 所有SM共享,容量通常在几MB到几十MB
- 按128字节的缓存行(cache line)加载数据
- 对全局显存的读操作会自动缓存
- 写操作可以通过写回(write-back)或写穿透(write-through)策略
2.5 全局显存:容量最大,但最慢
全局显存就是大家常说的「显存」。HBM2e、GDDR6这些技术都用在这里。
带宽与延迟:
| 显存类型 | 典型带宽 | 延迟 | 典型容量 |
|---|---|---|---|
| HBM2e | ~2 TB/s | ~400周期 | 16~80 GB |
| GDDR6 | ~1 TB/s | ~600周期 | 8~24 GB |
| GDDR5 | ~500 GB/s | ~800周期 | 4~16 GB |
你想想看,一个全局显存访问要等400个周期。这期间GPU能执行多少条指令?所以优化全局显存访问模式,是所有CUDA优化的核心。
合并访问(Coalesced Access)——让同一个warp里的线程访问连续的地址。这样一次内存事务就能加载所有需要的数据。否则,一次事务只加载一个线程的数据,带宽利用率惨不忍睹。
2.6 带宽与延迟:两个不同的概念
很多人把带宽和延迟混为一谈。其实它们是两码事。
- 带宽:单位时间内能传输多少数据。好比高速公路的车道数。
- 延迟:一次访问需要等多久。好比从入口到出口的行驶时间。
GPU的全局显存带宽很高(比如A100有2TB/s),但延迟也很高(400~800周期)。这意味着:
- 如果你一次传输大量连续数据,带宽利用率很高
- 如果你频繁做小规模随机访问,延迟会成为瓶颈
我在项目中遇到过这样的情况:一个kernel的全局显存利用率只有20%,但带宽明明还有余量。后来发现是访问模式太随机,每次只读4字节,但一次内存事务要传输128字节。浪费了96%的带宽。
2.7 避坑指南:显存层级使用建议
- 过度使用寄存器:导致占用率下降,性能反而不如用共享显存
- 忽略共享显存bank冲突:共享显存分32个bank,如果多个线程访问同一个bank,会串行化
- L2缓存当成无限用:L2只有几MB,大模型的数据根本放不下
- 全局显存随机访问:带宽利用率极低,能合并就合并
写kernel之前,先画一张数据流图。标清楚哪些数据放在寄存器,哪些放共享显存,哪些必须走全局显存。这样写出来的代码,性能不会差到哪里去。
好了,显存层级结构就讲到这里。下一节我们会深入全局显存的访问模式优化,到时候我会拿一个实际案例来拆解——怎么把带宽利用率从20%拉到90%。