梯度累加能缓解显存压力,因为它将大batch拆分为多个小batch分步计算,每次仅存储单个小batch的激活值和梯度,显存峰值由小batch决定;其基础是pytorch默认梯度累加行为(loss.backward()不覆盖而累加到.grad),配合loss除以accumulation_steps、延迟optimizer.step()与精准zero_grad()实现等效大batch训练。

梯度累加为什么能缓解显存压力
梯度累加(Gradient Accumulation)本质是用时间换空间:不一次性把大 batch 拆成小 batch 后直接 backward,而是让小 batch 多次 forward + backward,但只在第 N 次才真正更新参数。这样每次 forward 的中间激活值不会叠加,显存峰值只取决于单个小 batch,而非原始大 batch。
关键点在于:loss.backward() 默认会把梯度累加到 model.parameters() 的 .grad 上,而不是覆盖——这是 PyTorch 的默认行为,也是梯度累加能工作的基础。
手动实现梯度累加的 4 个必要步骤
PyTorch 没有开箱即用的“梯度累加开关”,必须手写控制逻辑。漏掉任意一步都可能引发梯度错误或训练失效:
- 每次小 batch 前调用
optimizer.zero_grad()—— 但注意:**不是每个小 batch 都要清零**,只在第一次小 batch 前清零,或更稳妥地:在真正 update 前清零(即每 N 次小 batch 后清零) - forward 得到
loss后,立刻调用loss.backward()—— 此时梯度已累加进.grad - 用计数器判断是否达到累积步数(如
accumulation_steps = 4),仅在第 4 次才调用optimizer.step() - 每次
step()后必须再调用一次optimizer.zero_grad(),否则下一轮累加会带上旧梯度
容易踩的坑:loss 缩放与梯度爆炸风险
如果原始 batch size 是 64,拆成 4 个小 batch(每个 16),那么总 loss 是 4 次 loss 的和。但 PyTorch 的 nn.CrossEntropyLoss 默认对每个样本取均值,所以小 batch 的 loss 已经是均值;直接累加 4 次再 step,相当于把 loss 放大了 4 倍,等效 learning rate 变成原来的 4 倍——这会导致训练不稳定甚至发散。
正确做法是:在 loss.backward() 前,对 loss 做缩放:loss = loss / accumulation_steps。或者,在 step() 前对梯度做缩放(效果等价,但前者更直观)。
另一个隐藏问题:混合精度训练(torch.cuda.amp)下,scaler.scale(loss).backward() 会自动处理缩放,但你仍需保证 scaler.step(optimizer) 和 scaler.update() 只在累积完成时调用。
验证梯度是否真正在累加
最直接的办法是在循环中打印某个参数的梯度范数:
param = next(model.parameters())
print(f"Step {i}, grad norm: {param.grad.norm().item():.3f}")
你会看到:前几次小 batch 的 grad norm 逐渐增大,最后一次 step 后突降为 0(因 zero_grad)。如果每次都是相似大小,说明没累加成功——大概率是误在每个小 batch 都调用了 zero_grad()。
注意:model.zero_grad() 和 optimizer.zero_grad() 效果相同,但后者更推荐,因为它还兼容某些自定义 optimizer 的状态清理逻辑。
显存不足时,梯度累加是有效手段,但它延长了单个 epoch 时间,且对 loss 曲线平滑性有轻微影响;真正难处理的是 activation 内存(比如长序列 Transformer),这时候得配合 checkpointing 或序列分块,不能只靠累加。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











