跳转到内容

数值精度与格式

上一章说,提高算术强度的四种手段里有一种同时改善两侧——降低数值精度。这一章把它讲透。

降精度是 AI 系统中唯一一个同时缓解四个约束的手段:矩阵单元吞吐翻倍、显存占用减半、带宽需求减半、通信量减半。代价是精度损失和一堆工程细节。

一个浮点数分三部分:

x=(−1)s×2e−bias×1.mx = (-1)^{s} \times 2^{e - \text{bias}} \times 1.m
  • 符号位 ss:1 位。
  • 指数位 ee:决定动态范围——能表示多大和多小的数。
  • 尾数位 mm:决定精度——相邻两个可表示数之间的间隔。

关键在于这两者是独立的。给定固定的总位数,多分给指数就少分给尾数,反之亦然。不同的分配方式产生不同的格式,各有适用场景。

格式符号-指数-尾数动态范围十进制有效位相对吞吐主要用途
FP641-11-52~1e±308~161/2×科学计算
FP321-8-23~1e±38~71×主权重、优化器状态、归约
TF321-8-10~1e±38~3~8×无需改代码的加速
BF161-8-7~1e±38~2–3~16×训练主力
FP161-5-106e-5 ~ 65504~3–4~16×推理、旧式训练
FP8 E4M31-4-3~±448~1–2~32×前向的激活与权重
FP8 E5M21-5-2~±57344~1~32×反向的梯度
INT8定点 + 缩放由缩放因子定—~32×推理量化
INT4 / NF4定点 + 缩放由缩放因子定—~64×权重量化

这是个值得停下来想的问题。科学计算需要双精度,为什么神经网络用两三位有效数字就够?

三个原因:

  1. 梯度本身是噪声估计。 随机梯度下降用一个小批量估计全数据的梯度,这个估计的相对误差通常在 10% 量级。相比之下,BF16 引入的百分之零点几的舍入误差微不足道。
  2. 网络有冗余。 大量参数共同表达一个函数,个别参数的小扰动被其他参数吸收。这也是剪枝和量化能work的同一个原因。
  3. 训练过程有自我纠正。 每一步的误差不会累积成灾难,因为后续的梯度会朝着修正的方向走。

但推理时没有第三条——误差只会向前传播、逐层放大。所以推理量化需要更小心地验证,尽管它能用更激进的位宽。

这是最值得理解的一组对比。两者都是 16 位,分配方式不同:

指数位尾数位后果
FP16510精度更高,但范围窄(最小正规数 6e-5)
BF1687精度更低,但范围与 FP32 完全相同

训练中的梯度经常非常小。用 FP16 时,小于 6e-5 的梯度会下溢成 0,模型学不动。

解决办法是损失缩放(loss scaling):把损失乘一个大系数(比如 1024),反向传播后梯度也被放大,避开下溢区;更新前再除回来。还要动态调整这个系数——太小仍然下溢,太大会溢出成 Inf。这是一整套需要维护、需要调参、且会出问题的机制。

BF16 直接砍掉三位尾数换三位指数,范围与 FP32 一致,根本不需要损失缩放。代价是只有约 2–3 位十进制有效数字。

结论:新代码一律用 BF16 训练。FP16 只在两种情况下用——硬件不支持 BF16(Volta 及更早),或者推理(推理没有梯度下溢问题)。

"混合"指的是不同的量用不同的精度。标准配方:

量精度为什么
前向/反向的矩阵乘输入BF16吞吐
矩阵乘的累加器FP32长串累加,低精度会丢有效位
主权重FP32见下
优化器状态(动量、方差)FP32数值稳定
梯度(通信时)BF16 或 FP32权衡带宽与精度
LayerNorm / Softmax 内部FP32涉及归约与指数,对精度敏感
损失与全局归约FP32累加项多

因为参数更新量常常比参数本身小几个数量级。

假设权重 w=1.0w = 1.0,学习率与梯度的乘积是 10−610^{-6}。BF16 只有约 3 位有效数字,在 1.0 附近能表示的最小间隔约 0.00780.0078。于是:

1.0+10−6→BF16 舍入1.01.0 + 10^{-6} \xrightarrow{\text{BF16 舍入}} 1.0

更新完全消失,权重永远不动。 保留一份 FP32 主权重,让这些小更新能累积起来,才是训练能继续的前提。

这也是"每参数 16 字节"的来源:

项字节/参数
FP32 主权重4
FP32 一阶动量4
FP32 二阶动量4
BF16 计算副本2
BF16 梯度2
合计16

第十一章的显存估算会用到这个数。

FP8 只有 4 或 5 位指数,动态范围窄到不能直接用——一个张量里的值必须先被缩放到 FP8 能表示的区间。

两种变体分工明确:

  • E4M3(范围 ±448,精度较高):前向的激活与权重。
  • E5M2(范围 ±57344,精度较低):反向的梯度,因为梯度的动态范围更大。
粒度说明权衡
Per-tensor整个张量一个缩放因子最简单;一个离群值会毁掉整体
Per-channel每个通道一个更稳,开销略高
Per-block每 128 个元素一个最稳,近年主流做法
延迟缩放用历史最大值预测本次省一次遍历,需要维护历史

离群值是 FP8 最大的敌人。 大模型的激活里常有比中位数大几十倍的离群值,per-tensor 缩放为了容纳它们,会把其余 99.9% 的值压缩到只剩一两个有效位。per-block 缩放就是为了解决这个问题。

FP8 训练在 2024 年后逐步进入生产,但仍需谨慎:注意力附近、归一化附近容易溢出,通常保持更高精度;收敛性需要逐模型验证。详见混合精度与 FP8。

训练关心收敛,推理关心"输出别变差",所以推理能用激进得多的精度。

为什么量化对推理特别有效? 回到上一章:自回归解码是访存受限的,每生成一个 token 要把全部权重读一遍。

速度∝1权重字节数\text{速度} \propto \frac{1}{\text{权重字节数}}

权重从 BF16 量化到 INT4,字节数变成 1/4,速度直接提升接近 4 倍——而这与算力毫无关系。

三类量化:

类型记法量化什么收益难点
仅权重W4A16只量化权重显存 + 带宽离群通道
权重 + 激活W8A8都量化上面 + 用低精度矩阵单元激活离群值
中间状态量化—推理时缓存的中间张量更长上下文 / 更高并发长上下文下误差累积

代表方法:GPTQ、AWQ(权重)、SmoothQuant(把激活的离群值迁移到权重上)。

低精度带来的不只是"数值不准",还有一些反直觉的行为:

归约顺序影响结果。 浮点加法不满足结合律,(a+b)+c≠a+(b+c)(a+b)+c \ne a+(b+c)。不同的并行归约顺序会给出不同的结果。后果是:换一个并行度,训练轨迹就变了;同样的种子,不同的机器数得不到相同的结果。要严格可复现,必须固定归约顺序,代价是性能。

累加器精度比输入精度更重要。 一千个 BF16 数相加,如果用 BF16 累加,后面的小数会被完全吃掉;用 FP32 累加就没问题。所有正经的矩阵乘实现都用 FP32 累加器。

Softmax 需要减最大值。 exe^{x} 在 xx 稍大时就溢出。标准做法是先减去最大值再取指数,数学上等价、数值上安全。这是"稳定 softmax"的由来。

梯度裁剪与低精度交互。 裁剪阈值要在 FP32 下计算全局范数,否则范数本身就不准。

一张总结表——这是本章最实用的部分:

位宽减半带来幅度前提
矩阵单元吞吐×2硬件支持该格式
权重显存÷2—
激活显存÷2—
显存带宽需求÷2访存受限时直接转化为加速
集合通信量÷2以该精度传输时
算术强度×2见上一章
精度变差需要缩放策略与验证

这是唯一一个同时改善所有四个约束的杠杆(回顾第一章的四约束),所以精度选择往往是性价比最高的优化。也正因为它太好用,容易被滥用——先验证质量,再享受收益。

做法后果
用 FP16 训练不加损失缩放梯度下溢,损失不降
权重全用 BF16 存(不留 FP32 主权重)训到一定程度不再进步
量化后只测困惑度上线后发现代码和数学能力崩了
混用不同精度做归约数值不可复现,不同 rank 结果不一致
以为 FP8 就是改个 dtype缺缩放策略直接 NaN
用带稀疏的峰值算力估算结论乐观一倍(第三章)
  • 指数位决定范围,尾数位决定精度;BF16 与 FP16 的差别就在这个取舍上。
  • 训练一律用 BF16(范围同 FP32,不需损失缩放);FP16 只用于不支持 BF16 的硬件或推理。
  • 混合精度的关键是保留 FP32 主权重与优化器状态——否则小更新会被舍入吃掉;这是每参数 16 字节的来源。
  • FP8 需要缩放策略(per-block 是近年主流),离群值是主要敌人。
  • 推理量化对解码特别有效,因为它是访存受限的;量化后必须跑完整评测,困惑度不够。
  • 位宽减半同时改善算力、显存、带宽、通信四项——性价比最高的杠杆,也最容易被滥用。
  • 注意归约顺序、累加器精度、softmax 稳定性这三个数值坑。