看过模型配置的人大概都注意过一个现象:训练脚本里写 bf16,
推理部署时换成 fp16。两个都是 16 位,为什么不能通用?
答案藏在位布局里。理解了这三个格式各自牺牲了什么,选择就不再是靠猜。
三种格式的位布局
浮点数由三部分组成:1 位符号、若干位指数、若干位尾数。
- FP16:1 + 5 + 10,共 16 位。指数 5 位,偏置 15
- BF16:1 + 8 + 7,共 16 位。指数 8 位,偏置 127
- FP32:1 + 8 + 23,共 32 位。指数 8 位,偏置 127
注意 BF16 和 FP32 的指数位完全一样(都是 8 位、偏置 127), 只有尾数从 23 位砍到 7 位。所以:
BF16 是「砍掉尾数的 FP32」——范围不变,精度变差。
而 FP16 是指数从 8 位砍到 5 位,尾数保留了 10 位。所以:
FP16 是「砍掉一半指数的格式」——精度还行,范围大幅缩小。
范围差多少
具体数字最能说明问题:
- FP16 最大能表示 65504,超过就变成无穷
- BF16 和 FP32 最大约 3.4 × 10³⁸
差了 33 个数量级。深度学习里梯度、激活值、中间结果很容易出现大于 65504 的数, 一旦溢出成 Inf,反向传播就全毁了。这就是训练优先用 BF16 的原因—— 训练怕溢出,不怕精度差。
而推理阶段数值范围相对可控,FP16 的 10 位尾数能换来更高的精度, 同样的模型量化后效果更好。所以推理优先 FP16。
0.1 存不下
0.1 在二进制里是无限循环小数(0.0001100110011…),任何二进制浮点格式都存不下精确值。
同一个 0.1,在三种格式里分别是:
- FP16:
0.0999755859375,位模式0x2E66 - BF16:
0.10009765625,位模式0x3DCD - FP32:
0.10000000149011612,位模式0x3DCCCCCD
可以看到 FP32 的误差已经很小(1.5e-9),FP16 的误差是 2.4e-5, BF16 的误差最大(9.8e-5)——因为它的尾数只有 7 位。
这类误差在累加时会被放大。做几百万次浮点累加,用 FP16 和用 FP32 的结果可能差出可观的距离。 这也是为什么有些计算(比如损失函数的求和)会强制用 FP32 累加,只在中间存储时降精度。
为什么要有非规格数
如果指数位全 0 就直接等于 0,那么最小的规格数(FP16 是 6.1e-5)到 0 之间 会有一段巨大的空隙。两个很小的数相减,结果可能直接掉进这个空隙,变成 0。
解决方案是非规格数(subnormal):指数全 0 但尾数非 0 时,
放弃隐含的前导 1,改用 0.尾数 × 2^(1-bias) 的方式表示。
这样能表示到 FP16 的 5.96e-8,精度随数值变小而逐渐降低,但下溢变成了渐进的。
这个设计叫渐进下溢(gradual underflow)。代价是硬件要额外处理这种特例, 收益是数值计算不会在临界点突然断崖。
几个特殊值的位模式
所有特殊值都靠指数位全 1 表示,再用尾数区分:
- 尾数全 0 → 无穷(正负看符号位)
- 尾数非 0 → NaN。再看尾数最高位:为 1 是静默 NaN(qNaN),为 0 是信号 NaN(sNaN)
- 指数和尾数全 0 → 零,正负同样看符号位
在波形或者寄存器 dump 里看到 0x7C00 就知道是 FP16 的 +Inf,
看到 0x7E00 是 qNaN。这类对照在调试硬件时用得很多。
怎么选
- 训练大模型:BF16。范围够大,不怕梯度爆炸,混合精度(BF16 存储 + FP32 累加)是标准做法
- 推理部署:FP16。精度更好,同样 2 字节,效果通常优于 BF16
- 传统科学计算:FP32 或 FP64,别为了省内存牺牲精度
- 嵌入式 / DSP:可能根本没有硬件浮点,要用定点数(Q 格式),那是另一套东西
一句话总结:BF16 保范围,FP16 保精度,FP32 两者都要但占地方。