场景:在多卡 DDP、开启 AMP(混合精度)模式下,同时采用了梯度累积(gradient accumulation) 做大 batch。单卡/小 batch 能学;一上多卡 + 累积就容易发散、loss 爆涨、grad_norm 飙高。在本章中主要阐述bug可能发生的现象以及总结原因。
accum_steps=8
:几十个 step 内 loss 爆炸,grad_norm
经常 > 1e3;把 LR 降低 8× 似乎能“救回一点”,但仍然不稳。典型错误:没有对 loss 除以
accum_steps
;每个小步都在调用scheduler.step()
;clip_grad_norm_
在unscale_
之前或在每小步都裁剪。
import os, torch, torch.nn as nn, torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
def train_one_epoch(model, loader, optimizer, scheduler, scaler, accum_steps=8, max_norm=1.0):
model.train()
for step, (x, y) in enumerate(loader):
x, y = x.cuda(non_blocking=True), y.cuda(non_blocking=True)
with torch.cuda.amp.autocast(True):
logits = model(x)
loss = nn.functional.cross_entropy(logits, y) # ❌ 未缩放
optimizer.zero_grad(set_to_none=True) if step % accum_steps == 0 else None
scaler.scale(loss).backward() # ❌ 累积但没 /accum_steps
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm) # ❌ 未 unscale_ 就裁剪
if (step + 1) % accum_steps == 0:
scaler.step(optimizer)
scaler.update()
scheduler.step() # ❌ 每小更新都在 step 调度
per_gpu_bs × world_size × accum_steps
,但loss 没除以 accum_steps
,等价于把梯度放大 accum_steps
倍;scheduler.step()
,学习率在一个 epoch 内被“走太快”;1️⃣ 打印“有效学习率”与梯度范数
eff_lr = optimizer.param_groups[0]["lr"] * (accum_steps)
print(f"lr={optimizer.param_groups[0]['lr']:.2e}, eff_lr≈{eff_lr:.2e}")
lr
乘上 accum_steps
后,与发散时的感觉吻合(太大)。同时在关键层上挂grad hook:
def watch_grad(module, name):
def _hook(grad):
print(name, grad.norm().item())
return _hook
model.layer4.register_full_backward_hook(lambda m, gin, gout: print('g=', gout[0].norm()))
accum_steps
内的每个小步梯度范数都很高,最终一步 step()
前已严重超标。2️⃣ 分离“优化步”和“调度步”
scheduler.step()
改到优化器更新后,并且只在完成一次累积后才步进,震荡明显减弱。3️⃣ AMP 裁剪顺序验证
scaler.unscale_(optimizer)
之后再裁剪,grad_norm
日志与直觉一致。# fixed_train.py —— 可直接用
import os, torch, torch.nn as nn, torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
def train_one_epoch(model, loader, optimizer, scheduler, scaler,
accum_steps=8, max_norm=1.0):
model.train()
optimizer.zero_grad(set_to_none=True)
for step, (x, y) in enumerate(loader):
x, y = x.cuda(non_blocking=True), y.cuda(non_blocking=True)
with torch.cuda.amp.autocast(True):
logits = model(x)
loss = nn.functional.cross_entropy(logits, y)
loss = loss / accum_steps
scaler.scale(loss).backward()
if (step + 1) % accum_steps == 0:
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)
scaler.step(optimizer) # 自动跳过 inf/NaN
scaler.update()
optimizer.zero_grad(set_to_none=True)
if scheduler is not None:
scheduler.step()
loss /= accum_steps
;(step+1) % accum_steps == 0
时:unscale_ → clip → step → update → scheduler.step()
;scaled backward
,不要调度、不要裁剪、更不要 step()
;zero_grad
建议放在优化步后,且用 set_to_none=True
。step()
,与累积无关。示例(Cosine 每步调度):
total_updates = (len(train_loader) // accum_steps) * num_epochs
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=total_updates)
accum_steps=8
、Cosine 每步调度:grad_norm
稳定在 0.5–3 范围;梯度累积本质是把多次小反传合成一次优化步。只要记住两点:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。