数值精度与格式
上一章说,提高算术强度的四种手段里有一种同时改善两侧——降低数值精度。这一章把它讲透。
降精度是 AI 系统中唯一一个同时缓解四个约束的手段:矩阵单元吞吐翻倍、显存占用减半、带宽需求减半、通信量减半。代价是精度损失和一堆工程细节。
浮点数由什么构成
Section titled “浮点数由什么构成”一个浮点数分三部分:
- 符号位 :1 位。
- 指数位 :决定动态范围——能表示多大和多小的数。
- 尾数位 :决定精度——相邻两个可表示数之间的间隔。
关键在于这两者是独立的。给定固定的总位数,多分给指数就少分给尾数,反之亦然。不同的分配方式产生不同的格式,各有适用场景。
| 格式 | 符号-指数-尾数 | 动态范围 | 十进制有效位 | 相对吞吐 | 主要用途 |
|---|---|---|---|---|---|
| FP64 | 1-11-52 | ~1e±308 | ~16 | 1/2× | 科学计算 |
| FP32 | 1-8-23 | ~1e±38 | ~7 | 1× | 主权重、优化器状态、归约 |
| TF32 | 1-8-10 | ~1e±38 | ~3 | ~8× | 无需改代码的加速 |
| BF16 | 1-8-7 | ~1e±38 | ~2–3 | ~16× | 训练主力 |
| FP16 | 1-5-10 | 6e-5 ~ 65504 | ~3–4 | ~16× | 推理、旧式训练 |
| FP8 E4M3 | 1-4-3 | ~±448 | ~1–2 | ~32× | 前向的激活与权重 |
| FP8 E5M2 | 1-5-2 | ~±57344 | ~1 | ~32× | 反向的梯度 |
| INT8 | 定点 + 缩放 | 由缩放因子定 | — | ~32× | 推理量化 |
| INT4 / NF4 | 定点 + 缩放 | 由缩放因子定 | — | ~64× | 权重量化 |
为什么深度学习能容忍低精度
Section titled “为什么深度学习能容忍低精度”这是个值得停下来想的问题。科学计算需要双精度,为什么神经网络用两三位有效数字就够?
三个原因:
- 梯度本身是噪声估计。 随机梯度下降用一个小批量估计全数据的梯度,这个估计的相对误差通常在 10% 量级。相比之下,BF16 引入的百分之零点几的舍入误差微不足道。
- 网络有冗余。 大量参数共同表达一个函数,个别参数的小扰动被其他参数吸收。这也是剪枝和量化能work的同一个原因。
- 训练过程有自我纠正。 每一步的误差不会累积成灾难,因为后续的梯度会朝着修正的方向走。
但推理时没有第三条——误差只会向前传播、逐层放大。所以推理量化需要更小心地验证,尽管它能用更激进的位宽。
BF16 与 FP16:为什么训练选 BF16
Section titled “BF16 与 FP16:为什么训练选 BF16”这是最值得理解的一组对比。两者都是 16 位,分配方式不同:
| 指数位 | 尾数位 | 后果 | |
|---|---|---|---|
| FP16 | 5 | 10 | 精度更高,但范围窄(最小正规数 6e-5) |
| BF16 | 8 | 7 | 精度更低,但范围与 FP32 完全相同 |
训练中的梯度经常非常小。用 FP16 时,小于 6e-5 的梯度会下溢成 0,模型学不动。
解决办法是损失缩放(loss scaling):把损失乘一个大系数(比如 1024),反向传播后梯度也被放大,避开下溢区;更新前再除回来。还要动态调整这个系数——太小仍然下溢,太大会溢出成 Inf。这是一整套需要维护、需要调参、且会出问题的机制。
BF16 直接砍掉三位尾数换三位指数,范围与 FP32 一致,根本不需要损失缩放。代价是只有约 2–3 位十进制有效数字。
结论:新代码一律用 BF16 训练。FP16 只在两种情况下用——硬件不支持 BF16(Volta 及更早),或者推理(推理没有梯度下溢问题)。
混合精度训练
Section titled “混合精度训练”"混合"指的是不同的量用不同的精度。标准配方:
| 量 | 精度 | 为什么 |
|---|---|---|
| 前向/反向的矩阵乘输入 | BF16 | 吞吐 |
| 矩阵乘的累加器 | FP32 | 长串累加,低精度会丢有效位 |
| 主权重 | FP32 | 见下 |
| 优化器状态(动量、方差) | FP32 | 数值稳定 |
| 梯度(通信时) | BF16 或 FP32 | 权衡带宽与精度 |
| LayerNorm / Softmax 内部 | FP32 | 涉及归约与指数,对精度敏感 |
| 损失与全局归约 | FP32 | 累加项多 |
为什么主权重必须是 FP32
Section titled “为什么主权重必须是 FP32”因为参数更新量常常比参数本身小几个数量级。
假设权重 ,学习率与梯度的乘积是 。BF16 只有约 3 位有效数字,在 1.0 附近能表示的最小间隔约 。于是:
更新完全消失,权重永远不动。 保留一份 FP32 主权重,让这些小更新能累积起来,才是训练能继续的前提。
这也是"每参数 16 字节"的来源:
| 项 | 字节/参数 |
|---|---|
| FP32 主权重 | 4 |
| FP32 一阶动量 | 4 |
| FP32 二阶动量 | 4 |
| BF16 计算副本 | 2 |
| BF16 梯度 | 2 |
| 合计 | 16 |
第十一章的显存估算会用到这个数。
FP8:需要缩放的低精度
Section titled “FP8:需要缩放的低精度”FP8 只有 4 或 5 位指数,动态范围窄到不能直接用——一个张量里的值必须先被缩放到 FP8 能表示的区间。
两种变体分工明确:
- E4M3(范围 ±448,精度较高):前向的激活与权重。
- E5M2(范围 ±57344,精度较低):反向的梯度,因为梯度的动态范围更大。
缩放策略是核心工程问题
Section titled “缩放策略是核心工程问题”| 粒度 | 说明 | 权衡 |
|---|---|---|
| Per-tensor | 整个张量一个缩放因子 | 最简单;一个离群值会毁掉整体 |
| Per-channel | 每个通道一个 | 更稳,开销略高 |
| Per-block | 每 128 个元素一个 | 最稳,近年主流做法 |
| 延迟缩放 | 用历史最大值预测本次 | 省一次遍历,需要维护历史 |
离群值是 FP8 最大的敌人。 大模型的激活里常有比中位数大几十倍的离群值,per-tensor 缩放为了容纳它们,会把其余 99.9% 的值压缩到只剩一两个有效位。per-block 缩放就是为了解决这个问题。
FP8 训练在 2024 年后逐步进入生产,但仍需谨慎:注意力附近、归一化附近容易溢出,通常保持更高精度;收敛性需要逐模型验证。详见混合精度与 FP8。
推理量化:另一套逻辑
Section titled “推理量化:另一套逻辑”训练关心收敛,推理关心"输出别变差",所以推理能用激进得多的精度。
为什么量化对推理特别有效? 回到上一章:自回归解码是访存受限的,每生成一个 token 要把全部权重读一遍。
权重从 BF16 量化到 INT4,字节数变成 1/4,速度直接提升接近 4 倍——而这与算力毫无关系。
三类量化:
| 类型 | 记法 | 量化什么 | 收益 | 难点 |
|---|---|---|---|---|
| 仅权重 | W4A16 | 只量化权重 | 显存 + 带宽 | 离群通道 |
| 权重 + 激活 | W8A8 | 都量化 | 上面 + 用低精度矩阵单元 | 激活离群值 |
| 中间状态量化 | — | 推理时缓存的中间张量 | 更长上下文 / 更高并发 | 长上下文下误差累积 |
代表方法:GPTQ、AWQ(权重)、SmoothQuant(把激活的离群值迁移到权重上)。
数值稳定性的几个坑
Section titled “数值稳定性的几个坑”低精度带来的不只是"数值不准",还有一些反直觉的行为:
归约顺序影响结果。 浮点加法不满足结合律,。不同的并行归约顺序会给出不同的结果。后果是:换一个并行度,训练轨迹就变了;同样的种子,不同的机器数得不到相同的结果。要严格可复现,必须固定归约顺序,代价是性能。
累加器精度比输入精度更重要。 一千个 BF16 数相加,如果用 BF16 累加,后面的小数会被完全吃掉;用 FP32 累加就没问题。所有正经的矩阵乘实现都用 FP32 累加器。
Softmax 需要减最大值。 在 稍大时就溢出。标准做法是先减去最大值再取指数,数学上等价、数值上安全。这是"稳定 softmax"的由来。
梯度裁剪与低精度交互。 裁剪阈值要在 FP32 下计算全局范数,否则范数本身就不准。
位宽如何影响四个约束
Section titled “位宽如何影响四个约束”一张总结表——这是本章最实用的部分:
| 位宽减半带来 | 幅度 | 前提 |
|---|---|---|
| 矩阵单元吞吐 | ×2 | 硬件支持该格式 |
| 权重显存 | ÷2 | — |
| 激活显存 | ÷2 | — |
| 显存带宽需求 | ÷2 | 访存受限时直接转化为加速 |
| 集合通信量 | ÷2 | 以该精度传输时 |
| 算术强度 | ×2 | 见上一章 |
| 精度 | 变差 | 需要缩放策略与验证 |
这是唯一一个同时改善所有四个约束的杠杆(回顾第一章的四约束),所以精度选择往往是性价比最高的优化。也正因为它太好用,容易被滥用——先验证质量,再享受收益。
六个常见的坑
Section titled “六个常见的坑”| 做法 | 后果 |
|---|---|
| 用 FP16 训练不加损失缩放 | 梯度下溢,损失不降 |
| 权重全用 BF16 存(不留 FP32 主权重) | 训到一定程度不再进步 |
| 量化后只测困惑度 | 上线后发现代码和数学能力崩了 |
| 混用不同精度做归约 | 数值不可复现,不同 rank 结果不一致 |
| 以为 FP8 就是改个 dtype | 缺缩放策略直接 NaN |
| 用带稀疏的峰值算力估算 | 结论乐观一倍(第三章) |
- 指数位决定范围,尾数位决定精度;BF16 与 FP16 的差别就在这个取舍上。
- 训练一律用 BF16(范围同 FP32,不需损失缩放);FP16 只用于不支持 BF16 的硬件或推理。
- 混合精度的关键是保留 FP32 主权重与优化器状态——否则小更新会被舍入吃掉;这是每参数 16 字节的来源。
- FP8 需要缩放策略(per-block 是近年主流),离群值是主要敌人。
- 推理量化对解码特别有效,因为它是访存受限的;量化后必须跑完整评测,困惑度不够。
- 位宽减半同时改善算力、显存、带宽、通信四项——性价比最高的杠杆,也最容易被滥用。
- 注意归约顺序、累加器精度、softmax 稳定性这三个数值坑。
- 算术强度与 Roofline — 上一章
- 集合通信原语与代价模型 — 下一章
- 量化:权重、激活与 KV cache — 第六卷的详细展开