1. 显存基础:GPU显存架构、显存带宽与容量、显存与主存的数据传输机制

大家好,我是你们的老朋友。今天咱们聊聊显存——这个深度学习炼丹炉里最金贵的“燃料罐”。

说实话,我见过太多同学模型写得很漂亮,一跑就OOM(显存溢出)。为什么?说白了,就是没搞懂显存到底是怎么工作的。你想想看,连“油箱”的构造都不清楚,怎么控制油耗?

1.1 GPU显存架构:从物理到逻辑

先看一张图,这是我个人习惯用的显存架构示意。它帮你把整个知识体系串起来。

主存 (CPU DRAM) 容量大(GB~TB) | 延迟高(~100ns) PCIe/NVLink ~16GB/s ~ 900GB/s GPU 显存 (HBM/GDDR) 全局显存 参数/梯度/优化器状态 ~80% 显存占用 L2 缓存 ~40MB (A100) SM (流式多处理器) 内部 寄存器 ~256KB/SM 共享内存 ~164KB/SM L1 ~128KB 数据流动:主存 → 全局显存 → L2 → L1/共享内存 → 寄存器

这张图我画了好几个版本才定稿。它清晰地展示了数据从主存到GPU核心的完整路径。

GPU显存不是一块“大平层”。它分好几层:

  • 全局显存:就是咱们常说的“显存容量”,比如24GB、80GB。模型参数、梯度、优化器状态全堆在这儿。我刚开始做训练时,以为只要模型能放下就行,结果一跑就炸——因为中间激活值也要占地方。
  • L2缓存:所有SM共享的一块“中转站”。A100有40MB,H100有50MB。别看它小,命中率高了能省不少带宽。
  • SM内部:寄存器、共享内存、L1缓存。这些是“一线战场”,数据在这儿被真正计算。每个SM的寄存器总量有限,线程多了就分得少,容易“spill”到全局显存——性能直接腰斩。

核心要点:显存不是“一块”内存,而是一个多级存储体系。优化显存,本质上是优化数据在这几层之间的流动效率。

1.2 显存带宽与容量:两个不同的“水龙头”

很多人把显存容量和带宽混为一谈。我打个比方:

  • 容量 = 水桶能装多少水。对应模型能不能放得下。
  • 带宽 = 水龙头单位时间流多少水。对应训练/推理速度。

举个例子,RTX 3090有24GB容量,带宽约936GB/s。A100 80GB版容量更大,带宽约2TB/s。你想想看,如果带宽不够,就算容量再大,数据搬进搬出也慢得像蜗牛。

GPU型号 显存类型 容量 带宽 总线位宽
RTX 4090 GDDR6X 24 GB ~1008 GB/s 384-bit
A100 HBM2e 40/80 GB ~2039 GB/s 5120-bit
H100 HBM3 80 GB ~3352 GB/s 5120-bit
V100 HBM2 16/32 GB ~900 GB/s 4096-bit

看到没?H100的带宽是V100的近4倍。这意味着同样一次数据搬运,H100快4倍。我在做大规模分布式训练时,带宽瓶颈比容量瓶颈更致命——因为你可以用梯度累积来省容量,但带宽不够就只能干等。

避坑指南:我曾经在V100上跑一个BERT-Large模型,显存刚好够,但训练慢得离谱。后来发现是频繁的显存分配和释放导致带宽被浪费。改用内存池后,速度提升了30%。

1.3 显存与主存的数据传输机制

数据怎么从CPU内存搬到GPU显存?嗯,这里要注意,不是直接“飞”过去的。

标准流程是这样的:

  1. CPU发起请求:调用 cudaMemcpy() 或 torch.Tensor.to(device)。
  2. DMA引擎接管:数据通过PCIe或NVLink总线,由DMA(直接内存访问)引擎搬运。CPU不参与实际传输,只负责发指令。
  3. 到达显存控制器:数据先进入显存控制器的缓冲区,再写入全局显存。
  4. GPU核心访问:SM通过L2缓存、L1缓存逐级读取。

这里有个关键点:PCIe是半双工的。什么意思?同一时间只能上传或下载,不能同时进行。NVLink是全双工,可以双向同时传。所以多卡通信时,NVLink比PCIe快得多。

注意:CPU→GPU的数据传输延迟大约在10~100微秒级别。如果你在训练循环里频繁做 .cpu() 和 .cuda(),那性能基本就交代了。我见过有人每个step都打印loss到CPU——训练速度直接掉到原来的1/10。

再聊一个实际案例。我在做混合精度训练时,发现 torch.cuda.Stream 可以重叠数据传输和计算。说白了,就是让GPU一边算,一边从CPU搬下一批数据。这样带宽被充分利用,训练吞吐能提升20%~50%。

# 伪代码示例:使用CUDA Stream实现传输与计算重叠
stream = torch.cuda.Stream()
with torch.cuda.stream(stream):
    # 异步拷贝数据到GPU
    data_gpu = data_cpu.to('cuda', non_blocking=True)
    
# 主stream继续计算
output = model(data_gpu)
# 此时传输和计算在硬件层面是并行的

这段代码看着简单,但实际调优时坑不少。比如 non_blocking=True 要求CPU数据是页锁定内存(pinned memory),否则会退化成同步拷贝。我刚开始不知道,用了普通内存,结果重叠了个寂寞。

1.4 小结:显存优化的三个层次

讲到这里,你应该明白了:显存优化不是单纯“省容量”。它分三个层次:

  • 容量层:模型能不能放下?用梯度检查点、混合精度、模型并行来省。
  • 带宽层:数据搬得快不快?用Stream重叠、减少CPU-GPU传输、优化数据布局。
  • 延迟层:每次访问快不快?利用共享内存、寄存器、缓存局部性。

我个人习惯是:先看容量够不够,再看带宽有没有跑满,最后才抠延迟。顺序搞反了,容易事倍功半。

好了,第一章就到这里。记住这张显存架构图,后面所有优化技巧都围绕它展开。

一句话总结:显存是分层体系,容量决定能不能跑,带宽决定跑多快,传输机制决定效率上限。

想要更多资料,联系可可