3、混合精度训练原理:自动混合精度(AMP)、损失缩放(Loss Scaling)、梯度裁剪与参数更新流程
混合精度训练并不是简单地把模型权重从 FP32 换成 FP16,而是围绕“哪些计算用低精度、哪些状态必须保留高精度、梯度如何安全回传”建立的一整套协同机制。理解 AMP、Loss Scaling、梯度裁剪与参数更新之间的先后关系,是后续排查精度异常和做参数校准的基础。
一、核心原理:AMP 的“双精度分工”
自动混合精度(AMP)的核心思想是:前向与反向的矩阵乘、卷积等计算密集型算子使用 FP16/BF16 加速,而参数主副本、优化器状态、归约与敏感算子保留 FP32。这样既获得 Tensor Core 的吞吐收益,又避免小梯度在低精度下下溢为零。
以 PyTorch AMP 为例,典型结构如下:
scaler = torch.cuda.amp.GradScaler()
for x, y in loader:
optimizer.zero_grad()
with torch.cuda.amp.autocast(dtype=torch.float16):
out = model(x)
loss = criterion(out, y)
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
scaler.step(optimizer)
scaler.update()
这段代码里,autocast 负责算子级精度选择,GradScaler 负责损失缩放,unscale_ 负责把缩放后的梯度还原,clip_grad_norm_ 负责梯度裁剪,step 与 update 负责参数更新和缩放因子调整。四者顺序不能随意调换。
二、Loss Scaling:让梯度“浮”起来
FP16 的最小正规数约为 6.1e-5,很多层的梯度值远小于这个量级,直接反向会下溢为 0,导致参数不更新。Loss Scaling 的做法是:在反向传播前把 loss 乘以一个缩放因子 S,使梯度整体放大 2^S 倍,反向结束后再除以 S 还原。
动态损失缩放(Dynamic Loss Scaling)会根据梯度是否出现 inf/NaN 自动调整 S:
- 连续 N 步没有溢出:S 翻倍,提升数值分辨率;
- 出现 inf/NaN:跳过该步参数更新,S 减半,避免污染权重;
- 缩放因子有上下界,防止无限增长或缩到 1 以下。
三、梯度裁剪与参数更新的正确顺序
梯度裁剪必须在 unscale_ 之后、scaler.step 之前执行。如果先裁剪再还原,裁剪阈值会被缩放因子放大,实际裁剪强度失真;如果先 step 再裁剪,则已经用未裁剪梯度更新了参数,裁剪失去意义。
| 步骤 | 操作 | 精度 | 关键点 |
|---|---|---|---|
| 1 | autocast 前向 | FP16/BF16 | 算子自动选择低精度 |
| 2 | loss 计算 | FP32 | 避免累加误差 |
| 3 | scaler.scale(loss) | FP32 | 放大 2^S 倍 |
| 4 | backward | FP16 | 梯度放大后回传 |
| 5 | unscale_ | FP32 | 还原真实梯度 |
| 6 | clip_grad_norm_ | FP32 | 按真实梯度裁剪 |
| 7 | scaler.step | FP32 | 更新主权重 |
| 8 | scaler.update | — | 调整 S |
四、案例场景:一次典型的 AMP 精度异常
某视觉模型在开启 AMP 后,前 200 步 loss 正常下降,随后突然变为 NaN。排查日志发现:
[step 198] loss=0.421 scale=65536.0
[step 199] loss=0.418 scale=65536.0
[step 200] loss=nan scale=32768.0 skipped=True
[step 201] loss=nan scale=16384.0 skipped=True
根因是梯度裁剪写在 unscale_ 之前,导致裁剪阈值被放大 65536 倍,实际几乎没有裁剪,异常梯度直接进入优化器。调整顺序后,NaN 消失,训练恢复稳定。
clip_grad_norm_ 放在 scaler.scale(loss).backward() 之后、unscale_ 之前;或忘记调用 unscale_ 就直接裁剪。这两种写法都会让裁剪失效。五、复盘:参数校准的检查清单
- 确认
autocast只包裹前向,不要包裹 loss 计算和优化器步骤; - 确认
unscale_在裁剪之前调用,且每个 optimizer 只调用一次; - 确认
clip_grad_norm_的max_norm与 FP32 训练时一致,不要因缩放而调大; - 监控
scale值变化,若持续下降说明梯度频繁溢出,需检查数据或模型数值稳定性; - 若使用 BF16,可关闭 Loss Scaling,但仍需关注梯度裁剪与学习率匹配。