图 1:本文封面,FP16 数轴上的下溢区与缩放逃生路线。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。「混合精度训练为什么要做 loss scaling」这道题,表面上问的是一个技巧,实际考你能不能把 FP16 的数值范围、反向传播的链式放大、动态调档策略三件事串成一条逻辑链。
一句话结论:FP16 的最小正规格化数约为 6.1×10⁻⁵,而训练后期的梯度大量落在 10⁻⁶ 到 10⁻⁸ 量级,不缩放会被舍入成 0;把 loss 乘一个大系数 S,梯度沿链式法则同步放大逃出下溢区,更新权重前再除以 S 还原。
问题出在 FP16 的表示范围。
IEEE 754 半精度浮点(FP16)用 1 位符号、5 位指数、10 位尾数,能表示的最小正规格化数约 6.1×10⁻⁵,最大 65504;作为对照,FP32 的最小正数约 1.2×10⁻³⁸,差了 33 个数量级。神经网络里激活值大多在可表示区内,但梯度不一样——训练到中后期,大量梯度落在 10⁻⁶ 到 10⁻⁸ 量级,直接塞进 FP16 会被舍入成 0,对应参数这一轮的更新信息就丢了。这就是梯度下溢(underflow)。2017 年发表的混合精度训练论文(arXiv:1710.03740)提出两招应对:一是保留一份 FP32 的权重主副本,每轮把 FP16 梯度累加到主副本上;二就是损失缩放。
为什么缩放 loss 就能缩放全部梯度?
反向传播是链式法则:每个参数的梯度都是 loss 对输出求导后一路乘回来的结果。loss 乘 S,链路上的每一个梯度都精确地乘上同一个 S——缩放源头一处,全链路同步放大。这比分层逐点缩放梯度干净得多:只改一个标量,不碰任何中间张量。放大后的梯度逃出下溢区完成反向传播,优化器更新前统一除以 S 还原真实量级,再把增量累加到 FP32 主副本。整套流程:FP16 前向算 loss → loss 乘 S → 反向传播(梯度同乘 S)→ 梯度除 S 还原 → FP32 主副本更新权重。
用代码把下溢与还原算一遍(Node.js 22.15.0,手写 IEEE 754 半精度舍入模拟,node fp16.js):
const g = 1e-8, S = 2 ** 10;
const scaled = f16(g * S); // 放大后反传(FP16)
const restored = scaled / S; // 更新前 unscale(FP32)
// f16(v) 表示「把 v 按 FP16 精度舍入」
实测输出:
FP16 最小正规格化数 2^-14 = 0.00006103515625
FP16 最大可表示数 = 65504
FP16 表示 65520(超出) = Infinity
梯度 1e-8 直接进 FP16 = 0
梯度 1e-7 直接进 FP16 = 1.1920928955078125e-7
梯度 1e-8 ×2^10 进 FP16 = 0.000010251998901367188
unscale 还原 = 1.0011717677116394e-8(保住了,误差 0.12%)
不缩放:41.2% 的梯度变 0;×2^10 后:0.0% 变 0
最后一行是 10 万个对数均匀分布在 10⁻⁴ 到 10⁻¹⁰ 的模拟梯度:不缩放时 41.2% 直接变 0,乘 2¹⁰ 后无一丢失。注意 65520 变成 Infinity——放大过头会撞另一堵墙。
S 不能拍死:动态调档在两面墙之间找平衡。
S 太小,梯度仍下溢;S 太大,梯度超过 65504 溢出成 inf,比下溢更糟——inf 反向传播会污染整个梯度图。而且不同模型、不同训练阶段的最优 S 不同,所以 S 必须动态调整,规则很简单:每步更新前检查梯度是否出现 inf 或 NaN,出现了就跳过本次权重更新并把 S 减半(降档);连续 N 步没有溢出,就把 S 翻倍(升档),尽量用大 scale 保住精度。PyTorch 的 GradScaler 就是这套策略的实现,默认初始 scale 65536、升档倍率 2、降档倍率 0.5、升档窗口 2000 步。另外提一句:BF16 的指数位与 FP32 相同(8 位),表示范围不吃亏,所以 BF16 混合精度通常不需要 GradScaler——loss scaling 是 FP16 路线的配套技术。
图 2:FP16 下溢区、五步缩放链路与动态调档规则。
面试现场的答题结构。
先给结论「防梯度下溢」,然后摆数值事实:FP16 最小正规格化数 6.1×10⁻⁵ 对比典型梯度 10⁻⁶~10⁻⁸,不缩放就清零。接着讲机制:链式法则让缩放 loss 等价于缩放全部梯度,反向在放大后的数值上进行,更新前 unscale 还原,FP32 主副本保证累加不亏精度。追问「S 取多少」时答动态调档:溢出减半、连续平稳翻倍,并指出两面墙——小了下溢、大了溢出成 inf。最后补一句 BF16 不需要缩放,展示你清楚这套机制是 FP16 专属的。你在实际训练里遇到过梯度 scale 频繁降档的情况吗?最后是怎么定位溢出源的?欢迎聊聊。