3、混合精度训练(AMP):FP16与FP32的区别、自动混合精度原理、GradScaler的使用、实际收益与陷阱
聊到显存优化,混合精度训练绝对是个绕不开的话题。我刚开始接触这玩意儿的时候,心里也犯嘀咕:把精度砍一半,模型还能收敛吗?后来踩过几次坑,才真正摸透了它的脾气。
说白了,混合精度训练就是让模型在训练时,该用 FP16 的地方用 FP16,该用 FP32 的地方用 FP32。不是一刀切,而是有策略地混着来。这样既能省显存、提速度,又能保住模型的收敛质量。
FP16 与 FP32 的区别
先看一张表,直观感受下两者的差异:
| 特性 | FP32(单精度) | FP16(半精度) |
|---|---|---|
| 位宽 | 32 位 | 16 位 |
| 指数位 | 8 位 | 5 位 |
| 尾数位 | 23 位 | 10 位 |
| 表示范围 | ~1.4e-45 到 3.4e38 | ~6e-8 到 65504 |
| 显存占用 | 4 字节 | 2 字节 |
| 计算速度 | 基准 | 约 2-8 倍(看硬件) |
FP16 的显存占用直接砍半,计算速度还能翻倍。但代价是什么?精度丢失和数值溢出。FP16 能表示的最小正数是 6e-8,最大是 65504。你想想看,如果梯度值小于 6e-8,直接变成 0——这就是所谓的「下溢出」。反过来,大于 65504 就变成无穷大——「上溢出」。
我在项目中遇到过一个大模型训练,loss 突然变成 NaN。排查了半天,发现就是某个层的梯度值超过了 FP16 的上限。嗯,这就是典型的溢出问题。
自动混合精度原理
自动混合精度(AMP)的核心思路,就是把 FP16 和 FP32 结合起来用。具体来说:
- 前向传播:用 FP16 计算,省显存、提速度
- 反向传播:用 FP16 计算梯度,同样省显存
- 权重更新:用 FP32 维护一份权重副本,保证精度
- 损失缩放:对 loss 进行缩放,防止梯度下溢出
为什么会这样设计?因为权重更新这一步对精度最敏感。你想想看,如果权重本身精度不够,模型参数就会慢慢漂移,最终收敛不了。所以 AMP 的做法是:计算用 FP16,存储用 FP32。
我个人习惯用 PyTorch 的 torch.cuda.amp 模块。它提供了两个核心组件:autocast 和 GradScaler。
核心逻辑:autocast 自动选择精度,GradScaler 防止梯度下溢出。两者配合,才能发挥 AMP 的最大威力。
下面这张图展示了 AMP 的整体流程:
GradScaler 的使用
GradScaler 是 AMP 中防止梯度下溢出的关键。它的工作原理很简单:在反向传播前,把 loss 放大一个倍数。这样梯度值也跟着放大,就不会掉到 FP16 的表示范围之外了。更新权重时,再把梯度缩回来。
看一段实际代码:
import torch
from torch.cuda.amp import autocast, GradScaler
# 初始化 GradScaler
scaler = GradScaler()
for data, target in dataloader:
optimizer.zero_grad()
# 使用 autocast 自动管理精度
with autocast():
output = model(data)
loss = loss_fn(output, target)
# 关键步骤:用 scaler 放大 loss
scaler.scale(loss).backward()
# 更新权重前,先缩放梯度
scaler.step(optimizer)
# 更新 scaler 的缩放因子
scaler.update()
这段代码里,scaler.scale(loss).backward() 做了两件事:放大 loss,然后反向传播。放大后的梯度就不会下溢出了。scaler.step(optimizer) 在更新权重前,会自动把梯度缩回来。scaler.update() 会根据梯度情况动态调整缩放因子。
小技巧:我习惯把 scaler.update() 放在每个 batch 的最后。如果某个 batch 的梯度出现 NaN,scaler 会自动增大缩放因子,保证后续训练稳定。
实际收益与陷阱
AMP 带来的收益是实打实的。我测试过一个 7B 参数的模型:
- 显存节省:从 24GB 降到 14GB,省了约 40%
- 训练速度:从 3.2 it/s 提升到 5.8 it/s,快了 80%
- 模型精度:验证集准确率从 72.3% 降到 72.1%,几乎没损失
但陷阱也不少。我曾经踩过几个坑,分享给你:
陷阱一:梯度爆炸
如果模型本身不稳定,AMP 会放大梯度爆炸的问题。因为 FP16 的表示范围小,一旦梯度值超过 65504,直接变成无穷大。我建议先跑几个 batch 看看梯度范数,如果太大,先调小学习率。
陷阱二:某些层不适合 FP16
BatchNorm、LayerNorm 这些层对精度很敏感。AMP 会自动把它们回退到 FP32,但如果你手动改了设置,可能会出问题。我曾经手贱把 BatchNorm 强制设为 FP16,结果 loss 直接飞了。
陷阱三:分布式训练时的同步问题
多卡训练时,梯度同步用的是 FP16。如果某个卡的梯度下溢出了,同步后所有卡的梯度都变成 0。我建议在 scaler.step 之前,检查一下梯度是否有效。
嗯,这里要注意一点:AMP 不是万能的。对于小模型(参数量小于 100M),AMP 的收益不明显,反而可能因为精度损失导致收敛变慢。我一般只在模型超过 1B 参数时才用 AMP。
最后说一句:AMP 的配置其实很简单,但调优需要经验。我建议你先在 1-2 个 epoch 上跑通,观察 loss 曲线和梯度范数。如果一切正常,再全量跑。这样能避免浪费算力。
想要更多资料,联系可可