为何FP16训练时梯度容易下溢为0?
1. 梯度天然就很小¶
深度网络的梯度由链式法则决定
\[\nabla_w L =\prod_{l=1}^{n}\frac{\partial h_l}{\partial h_{l-1}}\]
- 激活函数(ReLU 例外)、归一化、Softmax 等算子的导数绝对值普遍 小于 1。
- 网络越深或序列越长,这些导数相乘就越接近 0,梯度呈指数级衰减(梯度消失)。
实际大型 Transformer 中,某些层的权重梯度常落到\(10^{-9}\sim10^{-7}\)级别。
2. FP16 能表示的最小正数远大于这些梯度¶
- 在 硬件(GPU Tensor Core)上,亚正规数(subnormal)通常会被 flush-to-zero 以提升吞吐;这把可用下界从\(5.96\times10^{-8}\)再抬到\(6.10\times10^{-5}\)。
- 于是任何数值\(|g| < 6.1\times10^{-5}\)在乘法累加或写入内存时直接被截断为 0。
指标 | float32 (FP32) | float16 (FP16) |
---|---|---|
最小正 正规 数\(f_{\min}\) | \(2^{-126}\approx1.18\times10^{-38}\) | \(2^{-14}\approx6.10\times10^{-5}\) |
最小正 非正规 数\(f_{\text{sub}}\) | \(2^{-149}\approx1.40\times10^{-45}\) | \(2^{-24}\approx5.96\times10^{-8}\) |
动态范围\(\dfrac{f_{\max}}{f_{\text{sub}}}\) | \(\sim10^{38}\) | \(\sim10^{12}\) |
关键差异: FP16 只有 5 位指数,动态范围约是 FP32 的百万分之一,最小可表示正数也大得多。
举例
\(g = 3.2\times10^{-6}\quad\Longrightarrow\quad \text{FP32 表示: }3.2\times10^{-6}\bigl(\text{正常}\bigr)\quad \text{FP16 表示: }0\)
梯度被强行置零后,该权重就得不到更新,训练效果受损。
3. 为什么损失缩放能解决?¶
将损失放大\(S\)倍:
\[\tilde{L} = S\,L,\quad \nabla_w \tilde{L}=S\,\nabla_w L\]
只要挑选\(S\)使得
\[S \,\nabla_w L > 6.1\times10^{-5},\]
梯度就能 逃离下溢区,在 FP16 范围内安全表示;随后再除回\(S\)保证数值等价。
这正是 GradScaler 的核心思想:动态寻找“最大但不溢出”的\(S\),在性能与数值稳定性之间取得平衡。
总结:
- 梯度本身极小 + FP16 动态范围窄且硬件易将 subnormal 归零
⇒ 很多梯度被截断为 0;- 通过 损失缩放 把梯度整体抬高,再在更新前缩回原尺度,即可避免下溢,同时保持计算正确性。