从符号位、指数、尾数的结构出发,解释为何 bf16 通过削减尾数保指数解决了梯度下溢问题,以及 fp8 两种规格的设计取舍。
每一种格式都把它的位拆分为符号位、指数位和尾数位。指数决定范围——即一个数能有多大和多小,尾数决定精度。bf16 出现的原因是梯度在 fp16 中下溢了,它通过从尾数取出 3 位交给指数来解决了这个问题。
value = (-1)^S x 2^(E - bias) x 1.M
S 一个符号位
E 指数域,一个无符号整数
bias 一个常数,从 E 中减去,使其可以为负
1.M 尾数,带一个隐含的前导 1
bias = 2^(exponent_bits - 1) - 1
隐含的前导 1 是白嫖一位的技巧:任何规格化二进制数都以 1 开头,所以没必要存储它。因此 10 位的尾数实际上提供了 11 位的有效数字。
1.5 = 1.1 in binary = 1.1000000000 x 2^0
S = 0
E = 0 + bias = 0 + 15 = 15 = 01111
M = 1000000000
0 01111 1000000000
读回:(-1)^0 * 2^(15-15) * 1.5 = 1.5
fp32 S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMM
1 8 23 = 32 bits, bias 127
fp16 S EEEEE MMMMMMMMMM
1 5 10 = 16 bits, bias 15
bf16 S EEEEEEEE MMMMMMMM
1 8 7 = 16 bits, bias 127
fp8 E4M3 S EEEE MMM
1 4 3 = 8 bits, bias 7
fp8 E5M2 S EEEEE MM
1 5 2 = 8 bits, bias 15
int4 S MMM
1 3 = 4 bits, no exponent
把三种 16 位格式与 fp32 排成一排,设计意图一目了然。fp16 保留了大的尾数,把指数从 8 位砍到 5 位。bf16 完全保留了 fp32 的指数——同样的 8 位,同样的 bias 127——把尾数从 23 位砍到 7 位。bf16 简直就是 fp32 数字的高 16 位,这就是为什么两者之间的转换只是截断,零成本。
两个属性都源于域宽的算术。范围来自指数:最大有限值为 2^(E_max - bias) * (2 - 2^-mantissa_bits)。精度来自尾数:相邻值之间最小的相对间隔为 2^-(mantissa_bits + 1)。
"可精确表示的整数上限"这一列是让人惊讶的。bf16 无法表示 257。它只有 8 位有效数字,257 需要 9 位,所以会四舍五入到 256。同样它也无法精确表示 1000——最接近的 bf16 值是 1008 和 992。这对权重无所谓,因为权重都是小分数;但对任何计数类的东西就非常重要了。
同一数值用每种格式表示,精度损失清晰可见:
1/3 = 0.3333333333333...
fp32 0.33333334 (24 significand bits)
fp16 0.33325195 (11 significand bits)
bf16 0.33398438 ( 8 significand bits)
Absolute errors:
fp32 0.00000001
fp16 0.00008138
bf16 0.00065105
bf16 比 fp16 粗糙 8 倍,正好是它让出的 3 个尾数位的 2^3。
用 fp16 训练会崩,而且崩在底部而不是顶部。训练后期的梯度很小——量级在 1e-7 及以下是家常便饭——而 fp16 的最小规格化值是 6.1e-5。
每种格式中 1e-8 的梯度:
fp16 1e-8 小于最小次规格化数 5.96e-8
-> 被 flush 到 0.0
权重得不到任何更新。永远不会。
bf16 最小规格化值是 1.175e-38
-> 表示良好,有 2 到 3 位有效数字
fp32 -> 表示良好
一个变成恰好零的梯度不是小误差,是权重停止学习,静默地,没有任何日志警告。fp16 的变通方案是 loss scaling:在反向传播前把 loss 乘以一个大常数,使每个梯度都被放大到可表示的范围,然后在下一次优化器步骤前把梯度除以同一个常数。
scale = 1024 = 2^10
gradient 1e-8 x 1024 = 1.024e-5 在 fp16 中可表示
optimiser step: 再除以 1024
动态 loss scaling:不断提高 scale 直到出现 inf 或 nan,
然后减半并跳过那一步。这是一个运行在训练循环内部的控制循环,
纯粹因为 fp16 的指数只有 5 位宽而存在。
bf16 删掉了整个机制。它有 fp32 的指数,所以任何在 fp32 中不溢出的值在 bf16 中也不会下溢,所以不需要 scaling,也不可能有失控的控制循环。代价是 3 个尾数位,而代价可以接受的原因是梯度下降本来就在平均噪声估计——每个梯度两三位有效数字,用大批量平均,足够了。
范围比精度更重要。范围太小的失败模式是静默的零;精度太低的失败模式是梯度噪声稍大。前者是可以恢复的。
转换是零成本的。fp32 到 bf16 是丢弃低 16 位;bf16 到 fp32 是追加 16 个零。不需要指数重新偏置,没有特殊情况。
推理是另一个问题。推理时没有梯度,激活值落在一个舒适的范围内,fp16 额外的尾数位是真的有用。很多推理框架正因为这个原因偏好 fp16,而那些确实超过 65,504 的异常激活值是通过对特定层做 scaling 来处理的,而不是改变格式。
两个指数模式被保留了,所有特殊值都住在这里。公式 2^(E - bias) * 1.M 只在指数域既不是全零也不是全一时适用。
次规格化数是真正有性能影响的部分。没有它们,最小的 fp16 值将是 6.104e-5,以下都会是零。有了它们,范围下延到 2^-24 = 5.96e-8:
fp16 subnormals: E = 00000, M = 1..1023
value = M * 2^-24
smallest: 1 * 2^-24 = 5.9605e-8
largest: 1023 * 2^-24 = 6.0976e-5
smallest normal = 6.1035e-5
在空隙中有 1023 个额外可表示的值,
有效数字位逐渐减少——最大的次规格化数有 10 位精度,
最小的只有 1 位。
很多加速器运行在 flush-to-zero 模式,次规格化结果被设为零而不是正常处理,因为在某些硬件上次规格化算术很慢,历史上会被 trap 到软件处理。对训练的影响是直接的:一个 1e-7 的梯度原则上可以作为 fp16 次规格化数表示,但实践中会变成零,所以实际下界是最小规格化值而不是最小次规格化数。这是 bf16 胜出的又一个原因。
关于 NaN 唯一要记住的性质:它是唯一一个不等于自身的值。x != x 当且仅当 x 是 NaN 时为真,这就是每种语言中 NaN 检查的实现方式。这也意味着对一个包含 NaN 的数组排序是未定义行为,用朴素比较循环计算最大值可能静默返回错误的元素。
没有指数域。int4 存储一个范围在 -8 到 +7 的有符号整数——十六个级别——然后用一组权重共享的 scale 来恢复实数:
w ~= scale * (q - zero_point)
q 4 位整数,-8..7(或 0..15 无符号)
scale 每组一个 fp16 值
zero_point 可选偏移量,用于非对称范围
group 通常是 32、64 或 128 个连续权重
Example, group of 4 real weights:
[0.031, -0.052, 0.018, 0.044]
absmax = 0.052, symmetric int4 range is 7 levels each way
scale = 0.052 / 7 = 0.0074286
q = round(w / scale) = [4, -7, 2, 6]
dequantised = [0.029714, -0.052, 0.014857, 0.044571]
errors = [0.0013, 0.0000, 0.0031, 0.0006]
误差以半个 scale 步长为界,这就是为什么 group 大小才是真正重要的旋钮:更小的 group 意味着 scale 能更精确地追踪局部量级,代价是存储更多的 scale。128 个权重一组加一个 fp16 scale 相当于每个权重增加 16 / 128 = 0.125 位,所以"4 位"实际上是 4.125 位,加上 zero point 就是 4.25 位。
这也解释了为什么量化质量对异常值如此敏感。一组中单个大权重会迫使一个大 scale,而该组中所有其他权重的分辨率都会下降。区分一种 4 位方法与另一种的技术在很大程度上是对这一个问题给出的不同答案。
file size ~= params * bytes_per_weight
For a 7B model (6.97e9 parameters, using 1 GB = 1e9 bytes):
fp32 4 bytes -> 27.9 GB
fp16/bf16 2 bytes -> 13.9 GB
fp8 1 byte -> 7.0 GB
int4 0.53 bytes -> 3.7 GB (4.25 bits with scales)
For a 70B model:
fp16 140 GB
fp8 70 GB
int4 37 GB
这行算是本地推理中最常用的算术:它告诉你一个模型能否装下,其余的 VRAM 预算——KV cache、激活值、框架开销——都是在这基础上叠加的。
它也能预测解码速度,因为生成一个 token 需要读取每一次权重。在 3.3 TB/s 的内存带宽下,13.9 GB 权重每个 token 需要 4.2 ms,3.7 GB 需要 1.1 ms。量化完全不减少 FLOP 计数;它减少的是移动的字节数,而移动的字节数才是限制单流生成的实际因素。
提供商以不同精度提供同一个模型名称,量化方式并不总是写在模型卡上。Multigrid 列出了每个提供商暴露的每个模型的精度,这是当同一个模型名称在两条路线上给出明显不同的输出质量时首先要检查的。
矩阵乘法就是全部工作