2. GPU显存架构:从HBM到L1的层级世界

做GPU优化这么多年,我最大的感触就是——很多人把显存当成一个黑盒子。往里面塞数据,塞爆了就开始骂娘。其实你想想看,显存内部是有层级的,就像CPU的缓存一样。搞懂这个层级结构,很多溢出问题你就能提前预判。

2.1 显存层级结构:HBM、L2、L1

现代GPU的显存,说白了就是三层结构:

  • HBM(高带宽显存):最底层的大仓库,容量最大,但速度相对最慢
  • L2缓存:中间层,所有SM共享,用来缓解HBM的压力
  • L1缓存:每个SM私有的,速度最快,但容量极小

我习惯把这三层比作「中央仓库→区域分拨中心→家门口便利店」。数据从HBM搬到L2,再从L2搬到L1,最后喂给计算单元。每一层都在做一件事——用空间换时间。

核心要点:显存溢出不只是HBM满了的问题。L1/L2的分配策略不当,同样会导致OOM。我在项目中遇到过好几次,明明HBM还有余量,但L1缓存分配失败直接崩了。

为什么会这样?因为GPU的内存分配是分层的。你申请一块显存,驱动层会先看L2能不能满足,不行再去找HBM。如果L1的缓存行被占满,新的请求就会阻塞,严重时直接报错。

2.2 显存带宽与容量:两个不同的瓶颈

很多人把「显存容量」和「显存带宽」混为一谈。我告诉你,这是两个完全不同的概念:

  • 容量:能装多少数据,单位是GB
  • 带宽:每秒能读写多少数据,单位是GB/s

你想想看,容量不够是「装不下」,带宽不够是「跑不动」。我见过一个团队,模型明明能塞进24GB显存,但训练速度慢得像蜗牛。一查,带宽利用率只有30%。说白了,数据在HBM和计算单元之间来回搬运,但路太窄了。

我的经验:判断瓶颈是容量还是带宽,有个简单方法——看GPU利用率。利用率高但显存没满,大概率是带宽瓶颈;利用率低但显存快满了,那就是容量问题。

2.3 不同GPU型号的显存规格对比

做工程选卡,不能只看显存大小。我整理了一份常用GPU的显存规格,你直接拿去用:

GPU型号 显存类型 容量 带宽 L2缓存
NVIDIA A100 HBM2e 40GB / 80GB 1.6 TB/s (80GB版) 40 MB
NVIDIA H100 HBM3 80GB 3.35 TB/s 50 MB
NVIDIA V100 HBM2 16GB / 32GB 900 GB/s 6 MB
NVIDIA RTX 4090 GDDR6X 24GB 1.01 TB/s 6 MB
NVIDIA RTX 3090 GDDR6X 24GB 936 GB/s 6 MB
AMD MI250 HBM2e 128GB 1.6 TB/s 8 MB

注意看H100的带宽,3.35 TB/s,是V100的将近4倍。这意味着什么?同样大小的模型,H100的数据搬运速度是V100的4倍。我当年用V100跑一个175B的模型,光数据加载就占了30%的时间。换成H100后,这个比例降到了8%。

避坑指南:我曾经在选型时只看容量,选了RTX 3090(24GB)而不是A100(40GB)。结果模型虽然能装下,但带宽不够,训练周期从3天拖到了7天。后来我学乖了——带宽和容量要一起看,尤其是大模型训练场景。

2.4 显存分配机制:谁在管这些内存?

显存分配不是你想像的「申请一块,给一块」那么简单。GPU内部有一套复杂的分配机制:

  1. 驱动层分配:CUDA驱动维护一个显存池,你调用cudaMalloc时,驱动从池子里划一块给你
  2. 缓存行对齐:分配的最小单位是缓存行(通常128字节或256字节),不是按字节来的
  3. 碎片化问题:频繁申请释放会导致显存碎片,就像硬盘碎片一样
  4. 虚拟地址映射:GPU有独立的虚拟地址空间,物理显存通过页表映射

我举个例子你就明白了。假设你有一个模型,需要申请3块显存:权重(10GB)、梯度(10GB)、优化器状态(10GB)。如果你按顺序申请,驱动会尽量分配连续地址。但如果中间有人插了一脚(比如数据加载器也申请了显存),地址就不连续了,碎片就产生了。

关键点:显存碎片是OOM的隐形杀手。你明明看到还有空闲显存,但就是分配失败。为什么?因为空闲空间不连续,凑不出一块大的连续区域。

怎么解决?我建议你用cudaMallocAsync(CUDA 11.2+),它支持流式分配,能减少碎片。或者干脆用PyTorch的缓存分配器,它内部做了内存池管理,比裸调cudaMalloc靠谱得多。

2.5 一张图看懂显存层级与分配流程

下面这张SVG图,我画了显存层级结构和数据流动路径。你看完应该能理解数据是怎么从HBM一步步走到计算单元的:

GPU显存层级结构与数据流动 HBM(高带宽显存) 容量最大(16GB~128GB),带宽较高(900GB/s~3.35TB/s) L2缓存(所有SM共享) 容量中等(6MB~50MB),速度比HBM快3~5倍 L1缓存(每个SM私有) 容量极小(128KB~256KB),速度最快 计算单元(SM/CUDA核心) 数据加载(慢) 缓存命中(中) 直接读取(快) 分配流程:cudaMalloc → 驱动层内存池 → 虚拟地址映射 → 物理页分配 → 缓存行对齐

这张图里,数据从HBM往上走,经过L2、L1,最后到计算单元。每一层都在做缓存和预取。你想想看,如果L1频繁miss,数据就得从L2甚至HBM重新加载,带宽就浪费了。这就是为什么有些模型明明显存够,但跑得慢——缓存命中率太低。

2.6 实际项目中的显存分配策略

我总结了几条实战经验,你直接抄作业:

  • 预分配大块内存:不要频繁cudaMalloc/cudaFree,一次申请一大块,自己管理子分配
  • 使用内存池:PyTorch的CUDACachingAllocator就是干这个的,默认开启
  • 注意对齐:申请大小最好是256字节的倍数,避免内部碎片
  • 监控碎片率:用nvidia-smi看显存使用,如果used + free < total,说明有碎片

小技巧:我习惯在训练脚本开头加一行 torch.cuda.empty_cache(),清空缓存池。虽然会慢一点点,但能避免很多奇怪的OOM。尤其是你调试时反复重启训练,显存池里可能残留了上一轮的碎片。

嗯,关于显存架构就讲这么多。记住一句话:显存溢出不只是容量问题,带宽、缓存、碎片、分配策略,每一个都可能成为瓶颈。搞懂了这些,你再去定位OOM,思路就清晰多了。