跳转至

为何FP16训练时梯度容易下溢为0?

标签: 模型量化LLM

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;
  • 通过 损失缩放 把梯度整体抬高,再在更新前缩回原尺度,即可避免下溢,同时保持计算正确性。