能不要 loss scaling 吗?
本课一问:上一课的 FP16 靠 loss scaling 才活下来,可缩放倍数要一直调,溢出了还得跳过一步。能不能换一种 16 位格式,干脆不缩放?
这一课先不碰:8 位格式(第 4 课)、激活(第 5 课)。
从已有出发
同一步训练,把 FP16 换成 BF16:格式标签只改了一个字母,loss 那一格的「× S」没了。
- 解决了范围到 ±3.4 × 10³⁸,1 附近的间距 2⁻²³ ≈ 1.2 × 10⁻⁷,数值上几乎不用操心。卡在每个参数 4 + 4 + 4 + 4 = 16 字节(权重、梯度、Adam 的 m、v):锚点 27,781,427,952 个参数就是 444.5 GB,是 80 GB 单卡的 5.6 倍,还没算激活。
- 解决了矩阵乘和激活用 16 位,带宽与激活显存减半;FP32 主权重接住小更新,loss 乘 S 把小梯度推出下溢区,更新前再除回来。卡在指数只有 5 位:fp16(10⁻⁸) = 0,fp16(1 + 10⁻⁴) = 1,fp16(70,000) = inf;缩放倍数要一直调,溢出就得跳过这一步。
- 解决了8 位指数,范围与 FP32 相同:不再下溢,不需要 loss scaling。2026 年的默认训练精度。卡在1 附近间距粗到 1/128:bf16(257) = 256,bf16(1 + 10⁻³) = 1——主权重和优化器状态仍要 FP32,账本还是 16 字节/参数。
- 解决了激活每 1 × 128、权重每 128 × 128 一个缩放:张量 [1000, 0.01, 0.02, 0.5] 里,0.01 的误差从整张量缩放的 12.8% 降到 0.45%;DeepSeek-V3 相对 BF16 的 loss 相对误差 < 0.25%(官方自报)。卡在只有 256 个位型,E4M3 最大 448;块的形状要跟着离群值走——激活梯度改按 128 × 128 块量化,16B MoE 训到约 300B token 发散(附录 B.2);主权重和梯度仍是 FP32。
- 解决了MXFP4 每 32 个数共用一个 2 的幂(4.25 位/参数),NVFP4 每 16 个数一个 E4M3 缩放;K3、DeepSeek-V4 在后训练阶段对 MoE 专家权重做 QAT。卡在E2M1 只有 16 个码点:块 [0.3, −1.2, 2.7, 0.05] 用 MX 缩放后,0.05 → 0;全 FP4 预训练仍是前沿。
- 解决了不把 s × s 的注意力分数存下来:锚点一条 4,096 token 的序列,激活从 162.5 GiB 降到 42.5 GiB(选择性重计算 / FlashAttention);全量重计算每层只存输入,只剩 2.5 GiB(标准块公式近似)。卡在重计算用多算一遍前向换显存;可 444.5 GB 的模型状态一张 80 GB 卡仍然放不下 → C10 并行。
这一课继续追踪上一课那三个数(w = 1、Δw = 10⁻⁴、g = 10⁻⁸),原因同上一课:格式讨论的是单个数,贯穿例句不进到这一层。
先自己设计:留在 FP16,把缩放调好
最省事的想法:FP16 的问题既然有解,就把动态 loss scaling 调好,继续用 FP16。它卡在一个缩放改变不了的数上:
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
口径:浏览器现算。FP32 用 DataView 取位;其余格式先存成 FP32,再按 RNE 舍入(与 PyTorch 的 dtype 转换逐值一致,开发时用 torch 2.6 对照过)。E4M3 是 OCP 规范的 FN 变体(无 inf,最大 448);E5M2 有 inf(最大 57,344)。
前两行是 BF16 解决的:10⁻⁸ 不下溢(1.0012 × 10⁻⁸,正规数),70,000 不上溢(70,144)。后三行是它付出的代价,下一节讲。
窗口宽度这件事,原论文的数字也在说同一件事:不同网络需要的缩放倍数从 8 到 32K 不等,很多网络根本不需要缩放 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条原文口径;S 取多大取决于模型的梯度分布,没有通用值。 出处Micikevicius et al. 2017 §3.2(需联网) ↗需联网 。一个要按模型、按训练阶段去找的超参,本身就是负担。公平地说,在我们的小模型上它运行得很好:fp16 + 动态缩放一组一步都没跳(0 步),缩放倍数从 65,536 翻到了 2¹⁸——对这个规模,FP16 + 缩放完全够用。换成 BF16 的理由不是「这次失败了」,而是上面那点余量:BF16 把缩放这套机关和余量问题一起拿掉。
改进:BF16——把尾数挪给指数
BF16(brain floating point)由 Google Brain 为 TPU 设计:还是 16 位,但分法变了——1 位符号、8 位指数、7 位尾数。8 位指数和 FP32 一模一样,所以它能表示的范围和 FP32 一样宽:
「就是 FP32 的高 16 位」可以直接在位上核对:
口径:浏览器现算。FP32 用 DataView 取位;其余格式先存成 FP32,再按 RNE 舍入(与 PyTorch 的 dtype 转换逐值一致,开发时用 torch 2.6 对照过)。E4M3 是 OCP 规范的 FN 变体(无 inf,最大 448);E5M2 有 inf(最大 57,344)。
试三件事:① 在 BF16 与 FP32 之间切换 0.1:FP32 的位型是 0x3DCCCCCD,BF16 是 0x3DCD——前 16 位 0x3DCC,再按舍去的低 16 位 0xCCCD(大于一半)进一,就是 0x3DCD;② 点 1×10⁻⁸,切 FP16 与 BF16:一个是零,一个是正规数;③ 点 257,看 BF16 的「相邻可表示值」:256 的下一个数是 258,257 正好在中间,平局取偶数,落回 256。
BF16 的格式出自 Google Brain(Google Cloud 博客 2019-08-23,Shibo Wang 与 Pankaj Kanwar)。同年 Kalamkar 等人的研究在多个领域的模型上检验它:和 FP32 范围相同,不改超参就能达到 FP32 的效果 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条原文自称首个全面的 BF16 实证研究;注意其中的 BF16 是模拟的。 出处Kalamkar et al. 2019 摘要(需联网) ↗需联网 。今天它是大模型训练的默认格式:锚点 Qwen3.8 的权重就以 BF16 发布。
新的失败:精度粗了 8 倍
范围换来了,代价是尾数。上面表格后三行就是:
- bf16(257) = 256:在 256 到 512 之间,BF16 的间距是 2,奇数一个都存不下;
- bf16(1 + 10⁻³) = 1:同一个数在 FP16 里是 1.0009766(加上了),在 BF16 里被吞;
- bf16(0.1) = 0.10009765625:相对误差约 0.1%。
更新被吞的问题在 BF16 里更严重。还是那个 w = 1,这次把 BF16 也放进去累加:
口径:浏览器现算。FP32 用 DataView 取位;其余格式先存成 FP32,再按 RNE 舍入(与 PyTorch 的 dtype 转换逐值一致,开发时用 torch 2.6 对照过)。E4M3 是 OCP 规范的 FN 变体(无 inf,最大 448);E5M2 有 inf(最大 57,344)。
试两件事:① 默认输入下 FP16 和 BF16 都一步没加上,只有 FP32 主权重在走;② 点预设「1 + 10⁻³ × 1000」:FP16 走到了 1.9765625,BF16 还停在 1——BF16 的门槛是 w 的 2⁻⁸ ≈ 1/256,比 FP16 的 2⁻¹¹ 高 8 倍。
所以换成 BF16 之后,FP32 主权重、FP32 的 m 和 v 一样也不能少,上一课的账本不变,还是 16 字节/参数:
BF16 省掉的是 loss scaling 这套机关,不是显存。
回扣 C8:logit 漂到上千时,BF16 还分得清吗?
C8 讲 z-loss、QK-Norm 时留了一个问题:logit 漂到上千,BF16 还分得清吗?现在可以算了。在 512 到 1,024 之间,BF16 的相邻两个数相差 2⁹⁻⁷ = 4:
口径:浏览器现算。FP32 用 DataView 取位;其余格式先存成 FP32,再按 RNE 舍入(与 PyTorch 的 dtype 转换逐值一致,开发时用 torch 2.6 对照过)。E4M3 是 OCP 规范的 FN 变体(无 inf,最大 448);E5M2 有 inf(最大 57,344)。
1,000、1,001、1,002 在 BF16 里是同一个数 1,000,1,003 变成 1,004。两个 logit 1,001 与 1,003,真实的 softmax 概率之比是 e² ≈ 7.39;存成 BF16 以后变成 e⁴ ≈ 54.6——差了 7 倍多。FP16 在这里反而分得清(间距 0.5),但它的最大值只有 65,504。这就是 C8 要把 logit 按住的另一个理由:logit 越大,BF16 能分辨的差别越粗;实际实现也会把 softmax 这类敏感算子放在更高的精度里算(下面的 Dig)。
🔍 BF16 也不是处处都行:Switch Transformer 的路由器
Switch Transformer 说自己「首次」用 bfloat16 这样的低精度训练大型稀疏模型;但它同时报告,纯 bfloat16 训练会发散——路由器里的 softmax 对精度敏感。解法是只把路由器的输入转成 FP32,其余照旧用 bfloat16:训练稳定,速度与纯 bfloat16 相同(§2.4「Selective precision」、表 2)。这就是后来各家的共同做法:大部分算子用低精度,少数敏感的(softmax、归一化、路由)留在高精度。DeepSeek-V3 也写明,归一化、注意力、MoE 门控等算子保持 BF16 或 FP32(第 4 课)。
回扣 C4:ε = 10⁻⁶ 在 16 位里
C4 第 3 课讲 RMSNorm 时留了一句:ε 在低精度下会遇到问题。锚点的 rms_norm_eps = 0.000001。把它存进两种 16 位:
口径:浏览器现算。FP32 用 DataView 取位;其余格式先存成 FP32,再按 RNE 舍入(与 PyTorch 的 dtype 转换逐值一致,开发时用 torch 2.6 对照过)。E4M3 是 OCP 规范的 FN 变体(无 inf,最大 448);E5M2 有 inf(最大 57,344)。
在 FP16 里,10⁻⁶ 小于最小正规数 6.1 × 10⁻⁵,是次正规数,只剩 5 位有效二进制位,相对误差 1.3%;在 BF16 里它是正规数。更重要的是,真实实现通常把归一化整个放在高精度里算(DeepSeek-V3 就把归一化算子保留在 BF16 / FP32 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条原文列出保持原精度的组件:embedding、输出头、MoE 门控、归一化算子、注意力算子。 出处DeepSeek-V3 §3.3.1(本地 PDF) ↗ ),ε 不会以 16 位出现在分母里。
我们自己的模型:bf16 和 fp32 差多少
常见误解一:「BF16 比 FP16 更精确。」
正相反。BF16 的尾数只有 7 位,1 附近间距 2⁻⁷,比 FP16 粗 8 倍;它多出来的是范围。训练选它,是因为梯度与激活跨越的数量级比 FP16 的 40 个 2 的幂还宽,而舍入误差可以交给 FP32 主权重兜底。
常见误解二:「用了 BF16,就不需要 FP32 了。」
更新被吞在 BF16 里更严重(门槛从 FP16 的 1/2048 提到 1/256),所以主权重和优化器状态仍然是 FP32,账本还是 16 字节/参数;矩阵乘的累加也仍在 FP32 里做。
常见误解三:「BF16 是 IEEE 754 的标准格式。」
IEEE 754 定义了 16 位的 binary16(就是 FP16),没有 bfloat16。bfloat16 是 Google Brain 为 TPU 定的格式,后来被 GPU 等硬件广泛支持。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 不要 loss scaling:让 16 位格式的范围和 FP32 一样宽,梯度不再下溢 |
| ② 数学上为什么可能有效? | 8 位指数与 FP32 相同,范围算术上就和 FP32 一致 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条正规数 2⁻¹²⁶ 到约 3.39 × 10³⁸;与 FP32 互转只动尾数。 出处待补 ;精度的损失由 FP32 主权重接住(上一课的累加实验) |
| ③ 实验上为什么被选中? | 多领域不改超参就能匹配 FP32 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条模拟 BF16 的研究。 出处Kalamkar et al. 2019(需联网) ↗需联网 ;大模型训练普遍采用,Switch Transformer 用它训大型稀疏模型时需要把路由器留在 FP32 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条纯 bfloat16 发散,路由器输入转 FP32 后稳定;原文自报。 出处Switch Transformer §2.4、表 2(需联网) ↗需联网 |
一句话
BF16 把 FP16 的 3 位尾数挪给指数,范围和 FP32 一样宽,于是不下溢、不用 loss scaling;代价是 1 附近间距粗到 1/128,主权重和优化器状态仍要 FP32。
先停在这里:记住「FP16 缺范围、BF16 缺精度,两者都要 FP32 主权重;BF16 不用缩放」就可以进下一课。
再深一层:展开 Switch Transformer 的 Dig,想一想为什么偏偏是 softmax 和路由对精度敏感。
下一个缺口
16 位已经稳了。矩阵乘能不能再砍一半,用 8 位算? → 第 4 课:8 位还能训吗?
你可能会问
- 「那 TF32 是什么?」 NVIDIA 张量核里的一种计算模式:用 FP32 的 8 位指数、只取 10 位尾数做矩阵乘,输入输出仍是 FP32。它加速的是 FP32 训练里的矩阵乘,不改变显存账。
- 「BF16 的权重能直接拿去推理吗?」 可以,锚点 Qwen3.8 就是以 BF16 发布的;推理没有更新,也就没有「更新被吞」的问题。
- 「既然 BF16 这么粗,为什么训练结果和 FP32 差不多?」 一个直观的解释(不是证明):一次舍入的误差是随机方向的小扰动,SGD 本身就带着比它大得多的噪声;真正致命的是系统性地丢掉信息(下溢成 0、更新被吞),而这两件事分别被「范围够宽」和「FP32 主权重」解决了。