显存基础概念:显存与内存的区别、显存带宽与容量、显存分配机制

大家好,我是你们的讲师。今天咱们聊聊显存。很多同学跑模型一遇到OOM(Out Of Memory)就慌了,其实说白了,很多问题都出在对显存的理解不够深。我个人习惯是,先把底层概念吃透,再动手调优。这一节,我们就来把显存这层窗户纸捅破。

一、显存 vs 内存:它们到底有啥不一样?

先问个问题:你的电脑有32GB内存,为什么跑个13B的模型还报显存不足?

嗯,这里要注意:内存是CPU用的,显存是GPU用的。两者虽然都是存储数据,但分工完全不同。

  • 内存(RAM):负责存放操作系统、应用程序、文件缓存等。CPU通过内存总线访问它,延迟相对较高(几十纳秒级别)。
  • 显存(VRAM):专门为GPU设计的存储空间。它紧挨着GPU核心,通过超高位宽的总线连接,延迟低得多(几纳秒级别)。

我在项目中遇到过一位同学,把模型参数全塞在内存里,然后指望GPU直接读取——这根本行不通。GPU只能访问自己的显存,数据必须先从内存拷贝到显存,才能开始计算。这个拷贝过程,就是数据传输瓶颈的常见来源。

核心区别总结:

  • 内存:容量大(常见32GB~128GB),带宽小(几十GB/s),延迟高
  • 显存:容量小(常见8GB~80GB),带宽大(几百GB/s到TB/s),延迟低
  • GPU不能直接读内存,必须通过PCIe总线拷贝数据

你想想看,训练一个LLaMA-13B模型,光参数就要26GB(半精度)。如果显存只有24GB,那就得用CPU offloading,把部分参数临时挪到内存里。但每次挪动都要经过PCIe 4.0 x16(约32GB/s),比显存带宽慢了一个数量级。这就是为什么offloading会显著拖慢训练速度。

二、显存带宽与容量:两个关键指标

显存有两个核心参数:容量和带宽。很多人只盯着容量看,忽略了带宽,其实两者同样重要。

2.1 显存容量

容量决定了你能装下多大的模型。举个例子:

  • 一个7B参数的模型,用FP16存储,需要 7B × 2 bytes = 14GB 显存
  • 加上优化器状态(AdamW需要8 bytes/参数),总共需要 7B × (2+8) = 70GB
  • 再加上激活值、梯度等,实际需求更大

所以,为什么一张A100(80GB)才能勉强跑7B模型的全参数微调?因为光优化器状态就占了56GB。我曾经帮一个团队排查OOM,他们用RTX 3090(24GB)跑7B模型,结果发现优化器状态没开混合精度,直接爆了。嗯,这就是典型的容量估算失误。

我的经验:估算显存需求时,记得把优化器状态、梯度、激活值都算进去。一个简单的公式:总显存 ≈ 参数 × (参数精度 + 梯度精度 + 优化器状态精度) + 激活值。激活值通常占20%~50%的总显存,取决于batch size和序列长度。

2.2 显存带宽

带宽决定了数据搬运的速度。GPU计算单元再快,如果数据喂不进去,也是白搭。这就是所谓的“内存墙”问题。

常见的显存类型和带宽:

GPU型号 显存类型 显存带宽 典型容量
RTX 3090 GDDR6X 936 GB/s 24 GB
A100 80GB HBM2e 2,039 GB/s 80 GB
H100 HBM3 3,350 GB/s 80 GB

你看,H100的带宽是RTX 3090的3.5倍。这意味着同样大小的模型,H100在数据加载阶段能快3倍以上。我在做大规模分布式训练时,经常遇到带宽瓶颈——比如用8张3090做数据并行,每张卡都要从内存拉数据,结果PCIe带宽成了短板,GPU利用率只有30%。后来换成A100,带宽翻倍,利用率直接拉到80%。

避坑指南:我曾经以为只要显存够大就行,结果用RTX 4090(24GB)跑一个中等模型,发现计算单元经常空闲。一查才发现,数据从内存到显存的传输速度跟不上计算速度。说白了,带宽不够,容量再大也是白搭。所以选卡时,带宽和容量要一起看。

三、显存分配机制:GPU是怎么管显存的?

显存不是无限用的,GPU有一套自己的分配机制。理解这个,你才能精准定位OOM。

3.1 显存池与预分配

PyTorch和TensorFlow默认会预分配一大块显存,叫做“显存池”。比如你刚import torch,可能就看到显存被占了2GB。这不是bug,是框架为了减少频繁分配释放的开销。

具体来说:

  • 当你创建一个tensor时,框架从显存池里划一块出来
  • 当你删除tensor时,显存不会立刻还给系统,而是放回池子里复用
  • 如果池子不够,框架会向系统申请更多显存

这种机制的好处是速度快,坏处是显存碎片化。我在项目中遇到过,明明总显存还有5GB空闲,但申请一个4GB的tensor却报OOM。为什么?因为显存被分割成了很多小块,没有连续的大块空间了。

解决方法:设置环境变量 PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128,可以控制显存块的最大尺寸,减少碎片。或者用 torch.cuda.empty_cache() 手动清理缓存,但注意这会影响性能。

3.2 显存分配的生命周期

一个典型的训练循环中,显存分配是这样的:

  1. 模型加载:参数、梯度、优化器状态全部进显存
  2. 前向传播:每一层的激活值都会暂存,用于反向传播
  3. 反向传播:计算梯度,同时释放前向的激活值(如果没被保留)
  4. 优化器更新:更新参数,释放梯度
  5. 循环:重复步骤2-4

这里有个关键点:激活值是显存消耗的大头。比如一个序列长度为2048的7B模型,单层激活值可能就占几百MB。如果你堆了32层,那就是几十GB。我见过有人把batch size设得特别大,结果激活值直接撑爆显存——其实只要用gradient checkpointing,就能用计算换显存,把激活值从存所有层变成只存几层。

3.3 显存分配的可视化

为了让你更直观地理解,我画了一张图:

显存分配生命周期示意图 GPU 显存池 (VRAM Pool) 模型参数 权重 + 偏置 梯度 反向传播计算 优化器状态 动量 / 方差 (AdamW) 激活值 (Activations) 前向传播中间结果,占用最大 临时缓冲区 通信、算子中间结果 显存碎片 (Fragmentation) 小块的、不连续的空闲空间,导致OOM 图例: 参数 梯度 优化器 激活值 临时 碎片

从这张图你能看到,显存里不是只有模型参数。梯度、优化器状态、激活值、临时缓冲区,还有最烦人的碎片,都在抢空间。我曾经调试一个OOM问题,发现激活值占了60%的显存,而模型参数只占20%。后来用了gradient checkpointing,激活值降到15%,问题就解决了。

四、总结与避坑

好了,这一节的内容就这些。我帮你理一下重点:

  • 显存和内存:GPU只能访问显存,数据拷贝是瓶颈
  • 容量和带宽:两者都重要,别只盯着容量
  • 分配机制:显存池、碎片化、激活值是OOM的三大元凶

最后分享一个我自己的习惯:每次跑新模型前,先用 torch.cuda.memory_summary() 看一眼显存分配情况。这能帮你快速定位是哪个环节在吃显存。嗯,下次遇到OOM,别急着调batch size,先看看显存里到底装了啥。

想要更多资料,联系可可