梯度累积能模拟大Batch是因为loss.backward()默认累加梯度而非覆盖,通过控制zero_grad()时机和缩放loss,使多次小batch的梯度叠加等效于单次大batch更新。

梯度累积为什么能模拟大 Batch
因为 PyTorch 的 optimizer.step() 只根据当前 loss.backward() 累积的梯度更新参数,不关心这些梯度来自多少样本。只要在调用 optimizer.step() 前,让梯度在多次 backward() 中叠加(而非清零),就等效于用更大的 batch 计算了 loss 并求导。
关键点在于:每次 loss.backward() 默认会把梯度累加到 param.grad 上(不是覆盖),而 optimizer.zero_grad() 才真正清空它——所以控制 zero_grad() 的时机,就能控制累积步数。
手动实现梯度累积的 4 个关键步骤
假设目标是用 4 次小 batch(每个 size=8)模拟 total batch=32:
- 在每轮小 batch 前,**不**立即调用
optimizer.zero_grad();而是只在第 1 次前调用一次,或改用model.zero_grad(set_to_none=True)避免冗余内存(推荐) - 计算 loss 后,直接调用
loss.backward()—— 此时梯度被累加到已有grad上 - 每
accumulation_steps次后才调用optimizer.step()和optimizer.zero_grad() - 为保持梯度 scale 一致,需将 loss 除以
accumulation_steps:否则 4 次 backward 相当于把 loss 放大了 4 倍,梯度也放大 4 倍
示例核心逻辑:
accumulation_steps = 4
for i, (x, y) in enumerate(dataloader):
pred = model(x)
loss = criterion(pred, y) / accumulation_steps # 关键:缩放 loss
loss.backward()
<pre class="brush:php;toolbar:false;">if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad() # 或 model.zero_grad(set_to_none=True)容易忽略的三个坑
实际跑起来常因以下细节失败:
-
loss.backward()前没确保model.train()已启用 —— 尤其切换 eval/train 模式后,Dropout和BatchNorm行为不同,影响梯度一致性 - 用了
torch.cuda.amp混合精度时,必须用scaler.scale(loss).backward(),且scaler.step(optimizer)和scaler.update()要配对出现在 step 位置,不能只写optimizer.step() - 最后一个 batch 不够
accumulation_steps次时,仍执行optimizer.step()是安全的(PyTorch 允许),但若想严格对齐 total batch size,需跳过或补零,通常不必
和 torch.nn.DataParallel / DDP 的兼容性
梯度累积本身与分布式训练不冲突,但要注意:
- 在
DDP下,loss.backward()会自动跨卡归约梯度,因此累积的是「已归约后」的梯度 —— 这正是你想要的,无需额外处理 - 但
DataParallel已弃用,且其梯度累积需手动同步,容易出错,强烈建议只在 DDP 或单卡下使用梯度累积 - 如果你用
FSDP,注意其shard_grad_op模式可能影响grad的存储位置,建议设置use_orig_params=True并确认param.grad可写
最简验证方式:打印任意参数的 param.grad.norm(),观察是否在 step 前逐步增大、step 后归零 —— 这比看 loss 曲线更能确认累积是否生效。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











