显存不够,几乎是每个微调项目的“入场仪式”
如果你做过大模型微调,那“显存不够”这四个字,你几乎不可能陌生。
第一次跑,直接 OOM。
换个 batch size,再 OOM。
开 bf16,还是不够。
关掉一些东西,终于能跑了,但速度慢得离谱。
很多人会在这个阶段得出一个结论:
“是我显卡不行。”
但当你真的开始拆解显存使用之后,你会发现一个非常反直觉的事实:
大多数显存,并不是被模型参数吃掉的。
而你之所以总感觉显存不够,往往是因为你根本不知道显存是怎么被花掉的。
一个必须先说清楚的事实:显存不是“模型大小 × 2”
这是新手最常见、也最危险的一个误解。
很多人心里都有一笔非常粗糙的账:
模型参数多少 GB,我有多少显存,差不多就能跑。
但在真实训练中,这个估算几乎一定是错的。
因为模型参数,只是显存账单里最小的一项。

显存构成的“账单拆解图”
显存第一大户:激活(Activation),而且它非常“隐蔽”
很多人第一次被问到“显存主要花在哪”,会下意识回答:
模型参数。
但在训练阶段,真正吃显存的,往往是 activation。
activation 是什么?
简单说,就是模型前向计算过程中,每一层产生的中间结果,用来在反向传播时算梯度。
关键在于两点:
第一,activation 和 batch size 强相关
batch size 一大,activation 几乎线性增长。
第二,activation 和模型深度强相关
层数越多,存的中间结果就越多。
所以你会看到一个非常典型的现象:
模型参数看起来不大,但一开训练就 OOM。
不是模型“太大”,而是 activation 在默默吃显存。

batch size 增加导致 activation 激增示意图
第二大头:优化器状态,尤其是 Adam
如果你用的是 Adam 或 AdamW,那你几乎一定低估了它的显存消耗。
Adam 至少要为每一个可训练参数,维护两份额外状态:
- 一份一阶动量
- 一份二阶动量
也就是说:
参数 × 3,才是 Adam 的真实显存账单。
在全参数微调里,这个成本是灾难性的;
在 LoRA 微调里,它看起来“还好”,但依然不可忽视。
第三个被忽略的消耗:梯度本身
很多人以为梯度“算完就没了”,但实际上,在反向传播过程中,梯度也要完整存储。
尤其是在没有梯度累积、没有清理缓存的情况下,梯度会和 activation 一起,占据一大块显存。
这也是为什么你会看到:
前向还好,
一到 backward 就直接炸显存。
显存杀手中的“隐形 Boss”:PyTorch 缓存与碎片化
这是很多人查了一天 nvidia-smi 都想不明白的问题。
你明明看到:
显存用了 20GB,卡有 24GB,
但就是分配不了一个 1GB 的 tensor。
原因很简单:
显存碎片化。
PyTorch 会缓存显存以加速后续分配,但这也意味着,显存并不是一整块连续空间。
你“看得到”的空闲,不等于“用得上”。
为什么你“已经开了 bf16”,显存还是不够
很多人会觉得:
“我已经用 bf16 / fp16 了,应该很省显存了。”
但半精度,只解决了一件事:
参数和部分激活的存储大小。
它并没有解决:
- activation 数量本身
- 优化器状态数量
- 缓存和碎片化
所以 bf16 是“必要条件”,但绝不是“充分条件”。
gradient checkpointing:显存的“以时间换空间”
这是最常见、也最有效的一种显存优化方式。
gradient checkpointing 的核心思想非常朴素:
我不保存所有中间激活,需要时再算一遍。
这会明显降低 activation 的显存占用,但代价是:
前向计算要重复做,训练时间会变长。
下面是一段非常典型的开启方式(示意):
model.gradient_checkpointing_enable()
这一行代码,往往能直接救活一个“差一点就 OOM”的训练。

checkpointing 前后显存 vs 时间对比图
梯度累积:你以为在调 batch,其实在拆账单
当 batch size 太大显存扛不住时,梯度累积是最常见的替代方案。
它的本质是:
把一个大 batch,拆成多个小 batch,梯度累加后再更新。
loss = loss / grad_accum_steps
loss.backward()
if step % grad_accum_steps == 0:
optimizer.step()
optimizer.zero_grad()
这样做的好处是:
activation 显存按“小 batch”算,
但优化效果近似“大 batch”。
坏处是:
- 训练逻辑更复杂
- 调试更容易出错

真实 batch vs 梯度累积 batch 示意图
Offload:显存省了,但系统开始“喘气”
当你开始把 optimizer state 或部分参数 offload 到 CPU,你确实能省下一大截显存。
但你也必须意识到:
你是在用 PCIe 带宽换显存。
一旦 offload 过多,训练速度可能直接腰斩,甚至不稳定。
这类优化,非常不适合新手“无脑打开”。
一个容易被忽略的问题:你可能根本不需要“这么大”
这是一个很多人不愿意面对的问题。
你显存不够,真的是因为模型必须这么大吗?
还是因为你默认选了一个“看起来更强”的模型?
在微调阶段,模型大小的边际收益往往非常低。
有时候,换一个小一点的基座模型,反而比死磕显存优化更理性。
一个现实建议:别一开始就把显存榨干
这是我见过最多人踩的坑。
刚好能跑 ≠ 稳定能跑
刚好不 OOM ≠ 可以反复试错
你永远需要给显存留余地,用来:
- 调试
- 评估
- 临时开 profiler
- 打印中间结果
显存问题,往往是“系统设计问题”,不是参数问题
当你已经打开 bf16、checkpointing、梯度累积,还是跑不动时,通常意味着一件事:
你该停下来重新审视整体方案了。
继续抠显存,只会让系统越来越脆。
一个健康的显存优化顺序(经验总结)
不是“能开什么开什么”,而是:
- bf16 / fp16
- 减 batch size
- 梯度累积
- gradient checkpointing
- 评估是否需要 offload
- 重新审视模型规模
在显存受限阶段,更重要的是“验证方向”
这点和前面几篇其实是一脉相承的。
当你显存很紧张时,你真正该做的,不是把训练堆到极限,而是尽快验证:
这个方向值不值得继续投入。
在显存和算力都受限的阶段,先用 LLaMA-Factory online 快速跑通微调流程、验证数据和目标是否有效,再决定是否投入重资源,会比一开始就死磕本地显存更理性。
总结:显存不够,往往是你“算错账”,而不是你“资源太少”
写到最后,其实可以把这篇文章压缩成一句话:
显存问题,本质上是一个系统认知问题。
当你真正搞清楚显存是怎么被吃掉的,你会发现:
很多 OOM,并不是不可避免的;很多显存优化,也不是必须的。
真正成熟的工程师,不是“把显存榨到 0”,而是知道哪些钱该省,哪些钱不该省。