Mixed precision 怎么做?
FP16、BF16、FP8 有什么区别?
先弄清楚:每个概念是什么
- Exponent 与 mantissa(range vs precision)
- 浮点数 = 符号 × 1.mantissa × 2exponent。exponent 的 bit 决定尺子有多长(range),mantissa 的 bit 决定刻度有多密(precision)。例:1 附近相邻两个数的间距,FP16 是 \(2^{-10}\approx 0.001\),BF16 是 \(2^{-7}\approx 0.008\)。
- Underflow / overflow
- 比最小值还小就变成 0,比最大值还大就变成 inf。例:
np.float16(1e-8)= 0;np.float16(70000)= inf。 - Loss scaling
- backward 之前把 loss 乘 \(S\),所有 gradient 一起放大 \(S\) 倍,小 gradient 不会变成 0,optimizer 更新前再除回去。例:gradient \(10^{-8}\) 存成 FP16 是 0;乘 \(S = 2^{16}\) 得 \(6.55\times10^{-4}\),存得下;在 FP32 里除回来仍 ≈ \(10^{-8}\)。
- FP32 master weights
- weight 的正本用 FP32 保存和更新,forward/backward 用它转出的 16-bit 副本。FP16 只有 10 位 mantissa,weight 比 update 大超过 2048 倍时 \(w + \Delta w\) 舍入后还是 \(w\)。例:\(w = 1\)、update \(= 10^{-4}\):FP16、BF16 算出来都还是 1;FP16 连加 10000 次仍是 1,FP32 是 2.0002。
- Per-tensor scaling(FP8)
- FP8 的 range 够放下任意一个 tensor,但放不下所有 tensor。所以每个 tensor 各配一个 scale,把它的 amax(最大绝对值)对准格式的最大值。
看图:bit 怎么分,一个 step 怎么走
30 秒回答
Mixed precision runs matmuls in 16-bit or FP8 and keeps the sensitive parts in FP32: master weights, accumulation inside matmuls, and reductions like softmax and norms. FP16 has only 5 exponent bits, so small gradients underflow; dynamic loss scaling fixes that: multiply the loss by S, unscale before the step, and on inf or NaN skip the step and halve S. BF16 keeps FP32's 8 exponent bits, so it needs no loss scaling, but with only 7 mantissa bits it still needs FP32 master weights. FP8 uses E4M3 for weights and activations and E5M2 for gradients, with a scale per tensor.
关键公式
| 格式 | sign / exp / mantissa | 最大值 | 最小 normal | 1 附近间距 | 用在哪 |
|---|---|---|---|---|---|
| FP32 | 1 / 8 / 23 | 3.4e38 | 1.2e-38 | \(2^{-23}\) | master weight、optimizer state、accumulate |
| FP16 | 1 / 5 / 10 | 65504 | 6.1e-5 | \(2^{-10}\) | 要 loss scaling |
| BF16 | 1 / 8 / 7 | 3.4e38 | 1.2e-38 | \(2^{-7}\) | LLM 训练首选 |
| FP8 E4M3 | 1 / 4 / 3 | 448 | \(2^{-6}\) | \(2^{-3}\) | forward:weight、activation |
| FP8 E5M2 | 1 / 5 / 2 | 57344 | \(2^{-14}\) | \(2^{-2}\) | backward:gradient |
loss scaling:backward 是线性的
\[ \nabla_\theta (S\cdot L) = S\cdot \nabla_\theta L \]dynamic loss scaling(PyTorch GradScaler 默认值):
一个 training step
- FP32 master weight → 转出 BF16/FP16 副本
- forward:matmul 16-bit 输入、FP32 accumulate;softmax、norm 统计量用 FP32
- loss × \(S\) → backward,得到 16-bit gradient(BF16 时 \(S = 1\))
- gradient ÷ \(S\),检查 inf/NaN,再 clipping → 更新 FP32 master weight
model states 每个 parameter 占 16 bytes,见 training memory。
必答点
- 三件套:FP32 master weight + loss scaling + FP32 accumulation;matmul 低精度,reduction 和 update 用 FP32
- FP16:5 位 exponent,range 窄 → gradient underflow → 要 loss scaling
- BF16:8 位 exponent,range 同 FP32,不用 loss scaling;只有 7 位 mantissa,仍要 FP32 master weight
- dynamic loss scaling:溢出就跳过这步、\(S\) 减半;长时间不溢出再翻倍
- FP8:E4M3(±448)用于 forward,E5M2(±57344)用于 gradient,每个 tensor 一个 scale
追问
为什么大模型都用 BF16,不用 FP16?
range 和 FP32 一样,不用调 loss scale。PyTorch 文档提醒,多数 BF16 预训练的模型放进 FP16 时 gradient 会 overflow。
loss scale 选多大?太大会怎样?
只要不 overflow 越大越好,也就是 \(S\cdot\max|g| < 65504\)。太大会出 inf/NaN,所以实际用 dynamic loss scaling。\(S\) 取 2 的幂,乘除不引入误差。
FP8 溢出了怎么办?delayed scaling 是什么?
溢出时直接 saturate 到最大值,不跳过 step,因为 FP8 溢出太频繁。Transformer Engine 的 delayed scaling 用前几个 iteration 的 amax history 算 scale,不用现算 amax 多扫一遍数据。
MXFP8 / NVFP4 怎么缩放?在哪个维度?(智谱)
按 block 缩放,block 沿 matmul 的 reduction 维连续。MXFP8 每 32 个元素一个 E8M0 scale,NVFP4 每 16 个元素一个 E4M3 scale 再加一个 per-tensor FP32 scale(见 MXFP8、NVFP4)。NVFP4 的 weight 用 16×16 的 2D block,训练 recipe 见 Nemotron 3 Ultra。
哪些 op 必须留在 FP32?
大的 reduction 和容易溢出的 op(softmax、layer_norm、cross_entropy、exp/log、sum,PyTorch autocast 自动放 FP32)、matmul 的累加、optimizer update 和 Adam state。逐元素 op 受 memory bandwidth 限制,用 FP16 还是 FP32 速度一样。
来源Micikevicius et al. 2018, Mixed Precision Training §3.1(pp.2–3)、§3.2(pp.3–5)、§3.3(p.5)· Micikevicius et al. 2022, FP8 Formats §2(p.2)、§3 Table 1(p.3)· Kalamkar et al. 2019, BFLOAT16 abstract、§3 Table 1(pp.3–4)· PyTorch AMP(Gradient Scaling、GradScaler)· Transformer Engine: Using FP8 and FP4(delayed scaling、MXFP8、NVFP4)