混合精度训练与数值稳定性
前置知识点
一句话定义
混合精度让矩阵乘法跑在 16-bit 上、权重主副本与累积保持在 32-bit,用一半显存与近两倍速度换取「需要管理数值溢出」的新风险。
为什么重要
不开混合精度,训练显存翻倍、速度腰斩;用错精度(fp16 用在老卡或配置不当),loss 变 NaN、训练无声失败。fp16 与 bf16 的选择、loss scaling 的工作机制,是「训练突然崩了」类问题的高频病根。
前置知识
kp-004 的精度-字节数关系(fp32=4B、fp16/bf16=2B);浮点数由「符号/指数/尾数」三段组成。
核心概念
- fp16:5 位指数 + 10 位尾数——范围小(±65504),精度高;小梯度会下溢为 0,大值会溢出为 inf。
- bf16:8 位指数 + 7 位尾数——范围与 fp32 相同,精度低但训练稳定,无需 loss scaling。
- 主权重(master weights):优化器中维护的 fp32 参数副本,更新在 fp32 下进行以防累积误差。
- 动态 loss scaling:把损失放大 N 倍使 fp16 小梯度免于下溢,检测到溢出则跳过该步并缩小 N。
- 纯 BF16 混合精度:现代默认(Ampere/SM80+ 及之后架构),一劳永逸。
原理与机制
混合精度的分工:前向/反向的矩阵乘与卷积用 16-bit(Tensor Core 加速),参数更新、梯度累积、优化器动量保持 fp32。fp16 的数值陷阱来自范围:反向传播中梯度常常小至 1e-7 量级,直接用 fp16 表示会下溢成 0(权重从此收不到这些梯度信号);loss scaling 的解法是把损失乘以一个缩放因子 S(如 2^16),链式法则让所有梯度同倍放大到 fp16 可表示区间,优化器更新前再除回来。溢出检测:若某步出现 inf/NaN,跳过该次更新并把 S 减半,如此动态寻找最大安全缩放。
bf16 把指数位扩到 8 位,动态范围与 fp32 一致——下溢与溢出问题天然消失,代价是尾数只有 7 位(约两位十进制精度)。对随机梯度下降而言,梯度方向比精度更重要,因此 bf16 训练稳定且无需 scaling,成为 2020 年后硬件的默认选择。
图示
fp32: 1符号 8指数 23尾数 范围巨大, 4字节
fp16: 1符号 5指数 10尾数 范围±65504 → 需 loss scaling
bf16: 1符号 8指数 7尾数 范围≈fp32 → 训练即插即用直观类比
fp16 像一辆载重小但自重轻的货车(快,但装不下太重也不适合太轻的散件,需要「打包放大」再运);bf16 像一辆装载范围与卡车相同的轻型货车,直接开。主权重像账本正本保持高精度,草稿纸(16-bit 计算)算完誊回正本。
实例或案例
同一 LoRA 任务在 V100(无 BF16)上用 fp16 + 动态 scaling(初始 2^16):训练 300 步出现一次溢出跳步,loss 最终正常收敛;换到 4090 后改 bf16=True,无 scaling、无跳步、曲线更平滑。反过来,有人在 4090 上习惯性沿用 fp16 且 scaling 配置有误,loss 在第 40 步变 NaN 后全程不再恢复——精度配置属于「错了未必立刻暴露,暴露就是大事」的类型。
操作步骤
- 确认卡型:
nvidia-smi看架构,Ampere(A100/30 系)及以后 → 直接bf16=True。 - 旧卡(V100/T4)才用 fp16:
fp16=True,让 Trainer/accelerate 启用动态 loss scaling,不要手动定死 scaling 值。 - 全参微调确认主权重 fp32(框架默认),优化器状态不被意外降精度。
- 监控第一步的 grad norm(kp-019):出现 inf/NaN 立即停,先查精度与 lr 再谈别的。
- 模型加载与保存的 dtype 与训练精度保持一致,避免隐式转换。
排错清单
- loss 变 NaN/inf:按顺序查——lr 是否过大(先降一半)→ fp16 时 scaling 是否启用 → bf16 是否可用而未用 → 数据是否含异常大值(脏样本)。
- grad norm 长期为 0:fp16 下溢,启用 scaling 或换 bf16。
- 训练慢于预期:确认 bf16 真正生效(日志中的 dtype),不要隐式跑在 fp32。
- 保存后推理效果异常:保存 dtype 与原模型不一致(如 fp16 存成 fp32 截断),统一精度重存。
常见误区
- fp16 与 bf16 可随意互换:fp16 需要配套 scaling 且有溢出风险;bf16 需要硬件支持,两者是按卡型做的工程决策。
- 16-bit 训练=权重只存 16-bit:主权重与优化器状态仍是 fp32,这正是「混合」的含义。
- NaN 一定是精度问题:lr 过大、脏数据同样产生 NaN,排查时先确认精度配置正确,再降 lr、查数据。
与其他知识点的关系
显存账中精度项的换算见 kp-004;QLoRA 的 compute_dtype 配置见 kp-007;训练日志中 grad norm 的解读见 kp-019。
延伸阅读
本节不适用:混合精度的权威依据是 NVIDIA 文档与框架(PyTorch AMP/DeepSpeed)实现说明,而非单篇论文。
自测题
- bf16 为什么不需要 loss scaling?
答:它的指数位与 fp32 相同、动态范围一致,小梯度不会下溢、大值不会溢出,无需缩放技巧。
- 混合精度中哪些部分必须保持 fp32?
答:优化器中的主权重副本、参数更新、动量状态与梯度累积——保证低精度计算不累积成系统性误差。
- V100 上训练出现梯度全零,最可能的原因?
答:V100 不支持 bf16,而 fp16 未启用 loss scaling 导致梯度下溢;启用动态 scaling 或换精度方案。