张量显存占用分析:模型参数、梯度、优化器状态、中间激活的内存计算方法

做深度学习训练,你最先撞上的瓶颈是什么?十有八九是显存不够。
我遇到过不少同学,模型明明能装下,一跑训练就OOM。问题出在哪?
说白了,训练时占显存的不止模型参数。还有梯度、优化器状态、中间激活——这四大家族才是显存大户。

咱们今天就把每个家族的身家算个清楚。以后你再碰到OOM,直接公式一套,心里就有底了。

模型参数:最直观的占位

每个参数占多少内存?就两个因子:参数量 × 数据类型字节数。

参数量 = 所有可学习张量的元素个数之和
字节数:float32 → 4 字节,float16/bfloat16 → 2 字节,int8 → 1 字节
模型参数显存 = 参数量 × 字节数

举个例子:LLaMA-7B,参数量约 7×10⁹。
如果用fp16,参数显存 = 7e9 × 2 = 14 GB。如果用fp32,就是28 GB。
你看,一张A100也就80GB,纯参数就快吃一半了。

小提示: 我习惯用 model.parameters()torch.numel() 算总参数量,再手动乘字节数。模型加载前就能估算。

梯度:训练时隐形的兄弟

梯度跟参数等大。前向算完,反向传播必须存一份梯度,才能更新参数。

梯度显存 = 参数量 × 字节数(通常与参数精度一致)

所以训练时,参数 + 梯度就已经是 2倍的参数显存了。
如果你用fp16参数 + fp16梯度,那就是2×14=28 GB。

这里有个坑:混合精度训练时,梯度可能是fp16,但Loss Scaling之后会转成fp32再更新。你想想看,这块内存有临时分配,也可能造成峰值上升。我之前就踩过——以为自己算得很准,结果跑起来还是炸了,一查发现是梯度累加时没清临时buffer。

优化器状态:Adam这个吃内存大户

SGD的话,优化器只记录学习率,几乎不占额外内存。
但Adam就厉害了。它需要维护 一阶矩(动量) 和 二阶矩(自适应学习率)。每个参数对应两个额外变量。

纯fp32训练时:
参数: 4B/参数
梯度: 4B/参数
一阶矩: 4B/参数
二阶矩: 4B/参数
总计 = 16B/参数 → 即 4倍参数量

混合精度(常见做法):
参数(fp16): 2B/参数
梯度(fp16): 2B/参数
fp32副本: 4B/参数
一阶矩(fp32): 4B/参数
二阶矩(fp32): 4B/参数
总计 = 16B/参数 → 还是 4倍?不对,因为参数本身是fp16 + fp32副本,实际是6倍?

嗯,这里要注意。混合精度下,参数保存两份:一份fp16用于前向/反向,一份fp32用于累积更新。所以内存分到优化器状态里的“参数副本”算作优化器开销。通常我们简化计算:
训练时总显存(不含激活) ≈ 4×模型参数量 × 字节数(以fp32为基准)

优化器 参数占内存 梯度占内存 优化器状态占内存 合计(以每个参数计,fp32单位)
SGD(无动量) 4B 4B 0 8B
SGD(动量 0.9) 4B 4B 4B(动量) 12B
Adam(fp32) 4B 4B 8B(一阶+二阶) 16B
AdamW(混合精度) 2B(fp16参数) 2B(fp16梯度) 12B(fp32副本+一阶+二阶) 16B(与fp32 Adam一致)
核心公式(快速估算): 训练时 参数+梯度+优化器状态 ≈ 4倍参数量(以fp32每个参数4B为基准)。例如7B模型用Adam训练:4×7e9×4B = 112 GB!哪怕A100 80GB也装不下。所以必须用混合精度+ZeRO/FSDP。

中间激活:最大的隐藏炸弹

这部分最容易被忽视。前向传播时,每一层的输入、输出、中间值都要保留,供反向使用。
我见过一个项目,模型参数只占30GB,激活却吃掉了50GB,一训练就OOM。

以Transformer为例,每层激活内存 ≈ batch_size × seq_len × hidden_dim × 字节数 × 层数 × 系数

系数包括:
- 注意力:Q/K/V + 注意力矩阵 + dropout mask ≈ 3~4倍hidden
- FFN:两层线性,中间4倍隐藏维度
- LayerNorm:统计量

粗略估算:batch_size=4, seq_len=1024, hidden=4096, layers=32, fp16
单层激活 ≈ 4×1024×4096×2B ≈ 32 MB
总共32层 ≈ 1 GB(注意力+FFN系数2~3)≈ 2~3 GB

如果batch_size=32呢?直接变成16~24 GB。激活会随batch线性增长。

实战中,激活占比经常超过参数。比如LLaMA-7B(参数14GB fp16),把batch从4调到8,激活从2GB跳到4GB,总显存就从20GB涨到22GB,而峰值可能更高(因为内存碎片)。

避坑提醒: 我曾经用 PyTorch Profile 排查一个 OOM 问题,发现激活分配了 40GB,而参数只有 10GB。当时 batch_size=64 对一个 1.3B 模型。后来把 batch 减半,配合 gradient checkpointing(激活重计算),瞬间降到 8GB。
所以:激活优化是显存优化的大头。

总显存公式与实战计算

训练总显存 ≈ 参数显存 + 梯度显存 + 优化器状态显存 + 中间激活显存 + 额外开销(~5%)

或者用更通用的公式(以fp32为基准):
总显存 = 参数量 × 4 × 4(Adam) + 激活显存
      = 16×参数量(GB, 当参数量以10^9计) + 激活(gb)

举个例子:7B模型,fp16 Adam训练,batch=4,seq=1024,hidden=4096,layers=32。

参数+梯度+优化器(混合精度)≈ 16B/参数 × 7e9 = 112 GB ?等等,这里16B是按fp32每个参数4B算的。
实际混合精度下参数+梯度+优化器 = 14GB(fp16参数) + 14GB(fp16梯度) + 28GB(fp32副本+动量+方差) = 56 GB。
激活 ≈ 3 GB(上面估算)。
额外开销 ≈ 3 GB。
总计 ≈ 62 GB。一张A100 80G勉强能跑。

你看,简单一算就知道为什么大模型必须用ZeRO分片了。

可视化:显存占用的四大块

下面这张图帮你快速理解它们之间的关系。

显存占用的四大家族 模型参数 参数量 × 字节 梯度 与参数等大 优化器状态 Adam: 2倍参数 中间激活 batch×层 总显存 ≈ 参数 + 梯度 + 优化器状态 + 激活 额外开销 5% ~ 10%(临时buffer、框架缓存) 💡 想降低哪个块? 参数 → 剪枝/量化 梯度 → 高效通信 优化器 → 分片(ZeRO) 激活 → 重计算/梯度检查点

实战代码:快速查看各块显存

PyTorch 里有个很好用的 API:torch.cuda.memory_summary()。但更细粒度的分析建议用 torch.cuda.memory.mem_get_info() 和 torch.cuda.memory_allocated()。

import torch
from transformers import AutoModel

model = AutoModel.from_pretrained("meta-llama/Llama-2-7b-hf", torch_dtype=torch.float16)
model.cuda()

# 只算模型参数
param_mem = sum(p.numel() * p.element_size() for p in model.parameters())
print(f"模型参数显存: {param_mem / 1024**3:.2f} GB")

# 模拟一次前向(生成激活)
batch_size, seq_len = 4, 1024
input_ids = torch.randint(0, 32000, (batch_size, seq_len)).cuda()
out = model(input_ids, output_hidden_states=False)

# 总分配显存
allocated = torch.cuda.memory_allocated()
print(f"当前总显存分配: {allocated / 1024**3:.2f} GB")

# 激活显存 ≈ 总分配 - 参数
act_mem = allocated - param_mem
print(f"中间激活(估计): {act_mem / 1024**3:.2f} GB")
个人小技巧: 我经常在训练循环里每100步打一次 torch.cuda.max_memory_allocated(),看峰值。如果峰值比总分配高很多,说明有临时buffer没及时释放。这时候可以用 torch.cuda.empty_cache() 手动释放,但治标不治本,还是得优化代码。

避坑与调优建议

最后总结几个我踩过的坑:

  • 只算参数不算激活 – 相当于只带衣服没带行李,一跑就炸。
  • 优化器状态不注意精度 – 混合精度下,fp32副本 + 动量 + 二阶矩 = 12B/参数,比参数本身还多。用bitsandbytes的8-bit Adam能省一半。
  • 梯度累加时显存翻倍 – 如果你手动迭代累加,每个step都会保留梯度buffer。用PyTorch的accumulation_steps参数,或者用no_grad包裹。
  • 大batch激活炸掉 – 这时候 gradient checkpointing 是你的好朋友。代价是慢30%左右,但显存立减60%。
我曾经 在调一个15B模型时,天真地以为用fp16参数+梯度就能塞进40GB显卡。一算:参数30G、梯度30G、优化器(Adam)算下来要60G,还没加激活呢!后来换了ZeRO-2分片优化器和检查点才跑起来。现在每回训练前,我都先拿上面的公式算一遍再开卡。