定位:为没有数值计算背景的读者补充的前置讲,原课没有对应的课次。它支撑第 3 讲(精度、量化与稀疏)和第 11 讲(FlashAttention 的在线 softmax),也解释了第 1 讲里”FP8 是 BF16 的 2 倍算力""NVFP4 两级缩放”这些说法背后的东西。建议先读 P1。 依据:IEEE 754-2019;OCP 8-bit Floating Point Specification (OFP8) Rev 1.0;OCP Microscaling Formats (MX) Specification v1.0(2023);P. Micikevicius et al., FP8 Formats for Deep Learning(2022);NVIDIA 技术博客 Introducing NVFP4 for Efficient and Accurate Low-Precision Inference(2025-06);DeepSeek-AI, DeepSeek-V3 Technical Report §3.3(FP8 训练的细粒度量化与累加精度);M. Milakov and N. Gimelshein, Online normalizer calculation for softmax(2018);N. Higham, Accuracy and Stability of Numerical Algorithms(误差界);D. Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic(1991)。 配套代码 Code:
code/prereq3/(numpy;共享模块numfmt.py能逐位编码 / 解码任意小浮点格式,+ 5 个脚本,最慢的约 3 秒) 预计阅读时间:2.5 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 为什么推理课要讲数值表示
第 1 讲里有两个数字值得再看一眼:H100 的 FP8 算力是 BF16 的 2 倍;Blackwell Ultra 的 NVFP4 算力又是 FP8 的 3 倍。厂商宣传的”每代 N 倍”里,有一大半来自换了更窄的数值格式。decode 受限于带宽,每个参数少一个字节,就是少搬一个字节;KV cache 用 FP8 存,同样的显存就能多放一倍的上下文。
但”换一个格式”到底丢掉了什么?4 位的数只有 16 个编码,它怎么能表示一个模型的权重?为什么 FP8 有两种(E4M3 和 E5M2),NVFP4 为什么要”两级缩放”?为什么低精度矩阵乘法要用高精度累加?为什么 softmax 要先减最大值?这一讲从比特讲起,把这些问题一个一个拆开:
| 本讲的概念 | 后面在哪里用到 |
|---|---|
| 补码、溢出、累加器宽度 | 第 3 讲(INT8 量化);第 8 讲(Tensor Core 的输入 / 累加格式) |
| 浮点格式:指数管范围、尾数管精度 | 第 1 讲 §5(NVFP4);第 3 讲(FP8 / FP4 量化);第 7 讲(KV cache 量化) |
| 舍入、误差积累、Kahan / 两两求和 | 第 3 讲;第 10–11 讲(内核里的累加顺序);第 14 讲(为什么”结果对不上”不一定是 bug) |
| 数值稳定的 softmax、在线 softmax | 第 2 讲(注意力);第 11 讲(FlashAttention、flash-decoding) |
| 缩放因子、零点、粒度、块缩放 | 第 3 讲(LLM.int8、MX、NVFP4);第 9 讲(低精度与能量) |
0.2 学习目标 Learning goals
- 读懂一个整数的补码;知道溢出时”回绕”和”饱和”的区别;解释 INT8 矩阵乘法为什么要累加到 INT32。
- 读懂任何一种二进制浮点格式(符号、指数、尾数、偏置、次正规数、特殊值);对 FP32 / FP16 / BF16 / FP8 / FP6 / FP4 能说出它们的范围、精度和用途。
- 理解舍入误差的大小(单位舍入 )和方向(偏差),会用误差界比较顺序求和、两两求和、Kahan 求和;解释低精度累加器的问题和”提升到 FP32”的解法。
- 识别数值上不稳定的公式并改写:softmax 减最大值、log-sum-exp、在线 softmax 的合并、方差的两遍法 / Welford。
- 会做仿射量化(scale 和 zero point),理解截断的权衡、量化粒度的选择,以及 MX / NVFP4 这类块缩放格式为什么有效。
0.3 本讲地图 Map
§1 为什么数值格式是推理效率的核心:字节、算力、能量
§2 整数:补码;回绕与饱和;累加器宽度;定点数的局限 ← 代码 01
§3 浮点数:格式的解剖;ulp 与相对误差;FP32 / FP16 / BF16;FP8;FP6 / FP4;范围 vs 精度 ← 代码 02
§4 舍入与误差的积累:舍入方式;停滞;求和算法;低精度矩阵乘法的累加器 ← 代码 03
§5 数值稳定性:安全 softmax;log-sum-exp;在线 softmax;1/√d;方差 ← 代码 04
§6 量化入门:仿射量化;整数矩阵乘法;截断;粒度;块缩放格式;误差怎样传到输出 ← 代码 05
§7 硬件视角:每一代 Tensor Core 支持什么格式,为什么越窄越快
§8–12 小结、常见误区、练习(含答案)、延伸阅读、和正课的联系
0.4 关于数字 · About the numbers
- 本讲的全部实验数字都来自代码的实际输出,每次运行完全相同(固定随机种子,没有墙钟测量)。
- numpy 原生只有 FP64 / FP32 / FP16。其它格式(BF16、FP8、FP6、FP4)用
numfmt.py模拟:它把一个实数舍入到任意”指数 位、尾数 位”的格式。§3.9 会展示它和 numpy、PyTorch 的实现逐位一致,所以模拟出来的数就是真实硬件会得到的数。 - 数据都是合成的(高斯、Student-t、人为加入的离群通道),用来把机制看清楚;真实模型上的量化效果是第 3 讲的内容。
1. 为什么数值格式是推理效率的核心 · Why number formats matter
把一个数从 16 位换成 8 位,有三重收益:
- 字节:每个参数、每个 KV 元素少一半字节。decode 是 memory-bound 的(第 1 讲 §4),时间 ≈ 字节 ÷ 带宽,字节减半,时间几乎减半;容量也一样——同一块显存能放下两倍的 KV cache,批可以开得更大。
- 算力:乘法器的面积大致随尾数位数的平方增长(一个 位乘法要处理 个部分积)。同样的芯片面积,8 位乘法器能放的数量是 16 位的好几倍。这就是 H100 的 FP8 算力(1,979 TFLOP/s)是 BF16(989)2 倍的来历。
- 能量:第 1 讲 §2 的 Horowitz 能量表(45 nm):一次 FP32 乘法 3.7 pJ,FP16 乘法 1.1 pJ,INT8 乘法 0.2 pJ;8 位整数加法只要 0.03 pJ。更窄的数,每次运算更省电;而第 1 讲说过,功耗固定时,省电就是提速。
代价是精度(相邻两个能表示的数离得多近)和范围(能表示多大、多小的数)。一个 位的格式只有 个编码:8 位只有 256 个,4 位只有 16 个。全讲的核心问题就是:
有限的比特,怎样分配给范围和精度?剩下的误差,怎样不让它积累、不让它爆炸?
2. 整数 · Integers
2.1 补码
一个 位无符号整数的值是 ,范围 。负数怎么办?几乎所有硬件都用补码(two’s complement):
也就是说,最高位的”权重”是负的。8 位补码的范围是 。代码 01 演示 1 列出了几个例子: 是全 1(1 1111111), 是 1 0000000。
为什么是补码,而不是”一位符号 + 绝对值”?
- 同一个加法器:把两个补码当作无符号数相加、丢掉进位,结果正好是正确的补码。演示 1 里 :编码 253 + 5 = 258,丢掉第 9 位得 2。有符号、无符号整数共用一套电路。
- 0 只有一个编码(“符号 + 绝对值”有 +0 和 −0)。
- 取负数 = 按位取反再加 1,硬件很便宜。
一个怪现象: 的相反数 表示不了,取反加一后还是它自己。所以对称量化通常只用 ,OCP 的 MX 规范也明确允许空着 ,以保持正负对称、避免负偏差。
2.2 溢出:回绕与饱和
结果超出范围时,有两种处理方式(演示 2):
| 运算(8 位) | 数学结果 | 回绕(wrap) | 饱和(saturate) |
|---|---|---|---|
| 127 + 1 | 128 | −128 | 127 |
| 100 + 100 | 200 | −56 | 127 |
| −100 × 2 | −200 | 56 | −128 |
- 回绕是模 的运算,C 语言和 numpy 的整数类型都这样做,而且不报错:numpy 的 int8 数组
[127] + [1]静默地得到[-128],np.abs(int8(-128))返回 。 - 饱和把结果截到最大 / 最小值。它的结果离真实答案最近,所以量化时用饱和:超范围的权重截到 127,误差有限;回绕成 就是灾难。
2.3 累加器要多宽
INT8 矩阵乘法的每个输出是 个乘积之和。一个乘积最大 ,所以 int16 最多放下 2 个”最坏”的乘积,int32 能放下 133,144 个。演示 3 真的算了 1000 个长度 4096 的点积:
| 输入分布 | 结果的平均绝对值 | int16 回绕:错 | int16 饱和:错 | int32:错 |
|---|---|---|---|---|
| 均匀分布 | 278,979 | 91.8% | 100.0% | 0.0% |
| 量化过的高斯() | 50,447 | 59.3% | 98.3% | 0.0% |
输入可以省,累加器不能省:GPU Tensor Core 的 INT8 运算是 s8 × s8 → s32,TPU 的 MXU 也累加到 32 位。一个有趣的细节:回绕的 int16 比饱和的错得少。因为模运算有个性质——中间的部分和溢出了也没关系,只要最终结果落回范围内,回绕的答案就是对的;饱和一旦截断就回不来了。
2.4 定点数的局限
能不能用整数表示小数?可以:约定”小数点在第 8 位”,整数 代表 。这叫定点数(fixed point),例如 Q7.8:1 位符号、7 位整数、8 位小数,间隔 ,最大约 128。
定点数的绝对误差是常数,于是相对误差随数值大小剧烈变化。演示 4 比较了同样 16 位的 Q7.8、FP16、BF16 在每个数量级上的最大相对误差:
| 数量级 | 定点 Q7.8 | FP16 | BF16 |
|---|---|---|---|
| 1.0e+00 | 4.8e-04 | 3.8e-03 | |
| 1.9e-02 | 4.9e-04 | 3.9e-03 | |
| 1.9e-04 | 4.8e-04 | 3.9e-03 | |
| 9.9e-01 | 4.9e-04 | 3.9e-03 | |
| 1.0e+00 | inf | 3.8e-03 |
定点数只在 1–100 附近好用。神经网络的权重、激活、梯度跨越很多个数量级,需要一种相对精度处处相同的表示——让小数点跟着数值大小”浮动”。这就是浮点数。整数会在 §6 重新登场:先用一个缩放因子把数”挪”到已知范围,整数就又好用了。
3. 浮点数 · Floating point
3.1 二进制的科学计数法
十进制的科学计数法 有三部分:符号、有效数字、指数。二进制浮点数一样:
- :1 位符号;
- : 位指数字段,当作无符号整数,减去偏置(bias)才是真正的指数——这样指数字段不用补码,比较大小更简单;
- : 位尾数字段(mantissa / significand),前面有一个隐含的 1(不用存,因为非零数的二进制科学计数法第一位总是 1),所以有效数字一共 位。
一个格式记作 EM:FP32 是 E8M23,FP16 是 E5M10,FP8 有 E4M3 和 E5M2,FP4 是 E2M1。
3.2 拆开 FP32
代码 02 演示 1 把几个数编码成 FP32(符号 | 8 位指数 | 23 位尾数):
| 输入 | 比特 | 实际存下来的值 |
|---|---|---|
| 1.0 | 0 01111111 000…0 | 1 |
| −2.5 | 1 10000000 0100…0 | −2.5 |
| 0.1 | 0 01111011 10011001100110011001101 | 0.100000001490116119384765625 |
0 10010111 000…0 | 16777216 | |
0 11111111 000…0 | Inf |
- :指数字段是 127(就是偏置),尾数全 0。
- 0.1 在二进制里是无限循环小数(就像 1/3 在十进制里),存下来的只是最近的那个 FP32 数。所以浮点运算”几乎总是”有舍入误差,这不是 bug。
- 存不下:FP32 只有 24 位有效数字, 以上相邻两个数相差 2。
3.3 次正规数、Inf 和 NaN
指数字段的两个极端值被留作特殊用途:
- :次正规数(subnormal)。值是 ——没有隐含的 1。它们在 0 和最小正规数 之间等间隔地填满空隙,让数”逐渐”下溢到 0,而不是突然跳到 0。代价是精度逐渐降低(有效位数越来越少)。
- 全 1:Inf 和 NaN(IEEE 格式)。 是 ±Inf(溢出、除以 0), 是 NaN(0/0、Inf − Inf)。
3.4 ulp、机器精度和相对误差
在区间 里,能表示的数是等间隔的 个,间隔是
数越大,间隔越大,但间隔 / 数值大致不变。最重要的两个量:
- 机器精度 :1 和”比 1 大的下一个数”的距离;
- 单位舍入误差 :就近舍入时,任何(不溢出、不下溢的)实数 的舍入结果满足
这一行是整个数值分析的出发点:每次舍入引入一个不超过 的相对误差。FP32 的 ,FP16 是 (§2.4 表里 FP16 那一列正是这个数),BF16 是 。
3.5 格式一览
代码 02 演示 2 由定义算出每种格式的参数(和 IEEE 754、OCP 规范的表格一致):
| 格式 | 结构 | 最大值 | 最小正规数 | 最小正数 | 十进制位数 | 有限值个数 | |
|---|---|---|---|---|---|---|---|
| FP32 | E8M23 | 3.403e+38 | 1.175e-38 | 1.401e-45 | 1.19e-07 | 7.2 | 4,278,190,079 |
| TF32 | E8M10 | 3.401e+38 | 1.175e-38 | 1.148e-41 | 0.000977 | 3.3 | 522,239 |
| FP16 | E5M10 | 65,504 | 6.104e-05 | 5.96e-08 | 0.000977 | 3.3 | 63,487 |
| BF16 | E8M7 | 3.39e+38 | 1.175e-38 | 9.184e-41 | 0.00781 | 2.4 | 65,279 |
| FP8 E4M3 | E4M3 | 448 | 0.01562 | 0.001953 | 0.125 | 1.2 | 253 |
| FP8 E5M2 | E5M2 | 57,344 | 6.104e-05 | 1.526e-05 | 0.25 | 0.9 | 247 |
| FP6 E3M2 | E3M2 | 28 | 0.25 | 0.0625 | 0.25 | 0.9 | 63 |
| FP6 E2M3 | E2M3 | 7.5 | 1 | 0.125 | 0.125 | 1.2 | 63 |
| FP4 E2M1 | E2M1 | 6 | 1 | 0.5 | 0.5 | 0.6 | 15 |
几个要点:
- FP16 vs BF16:都是 16 位,分法相反。FP16 精度高(3.3 位十进制)、最大只有 65,504;BF16(“brain float”,Google Brain 为 TPU 设计)就是 FP32 的高 16 位,范围和 FP32 一样,精度只有 2.4 位十进制。训练从 FP16 转向 BF16,因为范围比精度更难对付(§3.8)。
- TF32 不是存储格式,而是 NVIDIA Tensor Core 内部用的输入格式:FP32 的 8 位指数 + FP16 的 10 位尾数,让”FP32 的矩阵乘法”也能跑在 Tensor Core 上。
- 越往下,格式越”可怜”:FP8 只有 253 / 247 个不同的有限值,FP4 只有 15 个。
3.6 FP8:E4M3 与 E5M2
FP8 有两种编码(Micikevicius et al. 2022,后来成为 OCP OFP8 规范):
- E4M3:精度高、范围小,建议用于权重和激活。它做了一个改动:不要 Inf,只保留一个 NaN 编码(S.1111.111),把指数全 1 的其余 7 个编码拿来表示 256、288、…、448。如果照搬 IEEE 的约定,最大值只有 240。
- E5M2:范围大、精度低,和 FP16 的指数一样(可以直接截断 FP16 得到),保留 IEEE 的 Inf / NaN,建议用于梯度。
演示 3 列出了 E4M3 每个”2 的幂区间”里的数:每个区间都是 8 个(3 位尾数),间隔从次正规区的 一路翻倍到最后一个区间的 32,但间隔 / 数值始终在 6.7%–12.5% 之间——这就是”相对精度均匀”。最后一个区间 只有 7 个数(第 8 个编码是 NaN),所以最大值是 448。
DeepSeek-V3 是一个反例:它在所有张量(包括梯度)上都用 E4M3,“尾数优先于指数”,靠细粒度的缩放因子(§6.5)弥补范围。
3.7 FP6 和 FP4:小到可以列出全部的数
OCP 的 MX 规范定义了两种 FP6(E3M2、E2M3)和一种 FP4(E2M1),它们没有 Inf 和 NaN,所有编码都是数,溢出时只能饱和。FP4 E2M1 的全部非负值是:
1 和 2 之间间隔 0.5,2 到 4 之间间隔 1,4 到 6 之间间隔 2。一个最大值只有 6、一共 15 个数的格式,不缩放根本没法用:权重通常在 量级,直接舍入几乎全变成 0。缩放因子的作用,就是把一组数整体乘以一个因子,让它们落在格式”最密”、又不溢出的那段数轴上(§6)。
3.8 范围 vs 精度:loss scaling 的故事
演示 5 把几个数放进不同的格式(不做任何缩放):
| 数 | FP16 | BF16 | FP8 E4M3 | FP8 E5M2 |
|---|---|---|---|---|
| 1e−9 | 0(下溢) | 9.96806e-10(0.32%) | 0(下溢) | 0(下溢) |
| 1 + 1/300 | 1.00293(0.04%) | 1(0.33%) | 1(0.33%) | 1(0.33%) |
| 300.5 | 精确 | 300(0.17%) | 288(4.2%) | 320(6.5%) |
| 70,000 | Inf(溢出) | 70144(0.21%) | NaN(溢出) | Inf(溢出) |
同一个数的命运完全不同。BF16 分辨不出 1 和 1 + 1/300,FP16 却装不下 70,000。
这就引出了混合精度训练早期的一个问题(Micikevicius et al. 2018):梯度通常非常小。演示 5 生成了 20 万个”像梯度”的数(绝对值的中位数 1e−6),在 FP16 里有 8.0% 下溢成 0、87.0% 掉进精度很差的次正规区。解决办法叫 loss scaling:把 loss 乘以 ,反向传播出来的梯度也都放大 倍,在 FP16 里就不会下溢;更新权重前再除以 。
| FP16 前乘以 | 下溢成 0 | 落在次正规区 | 上溢成 Inf |
|---|---|---|---|
| 8.0% | 87.0% | 0.00% | |
| 0.0% | 28.3% | 0.00% | |
| 0.0% | 0.3% | 0.00% | |
| 0.0% | 0.0% | 0.04% | |
| 0.0% | 0.0% | 13.77% |
缩放因子要”刚刚好”:太小救不了小数,太大让大数溢出。BF16 根本不需要这一步(范围和 FP32 一样),这是今天训练普遍用 BF16 的原因。
请记住这个故事:loss scaling 是”用一个缩放因子把数挪进格式的表示范围”的第一个大规模应用。之后的发展只是让缩放因子越来越细:每张量一个(FP8 训练)→ 每 128 个元素一个(DeepSeek-V3)→ 每 32 个(MX)→ 每 16 个(NVFP4)。
3.9 我们的模拟器可信吗
numfmt 用 numpy 实现了通用的舍入:找到 所在的 2 的幂区间(用 np.frexp,精确)、算出 ulp、把 舍入成整数、再处理溢出。演示 4 把它和独立的实现逐位比较:
| 格式 | 对照的独立实现 | 比较的数 | 不一致 |
|---|---|---|---|
| FP16 | numpy astype(float16) | 100,007 | 0 |
| FP32 | numpy astype(float32) | 100,000 | 0 |
| BF16 | 位运算 (u + 0x7FFF + lsb) >> 16 | 100,000 | 0 |
| FP8 E4M3 | PyTorch float8_e4m3fn | 100,006 | 0 |
| FP8 E5M2 | PyTorch float8_e5m2 | 100,006 | 0 |
一个细节:同样是”转成 FP8”,PyTorch 对 E4M3 饱和(超过 448 的变成 448),对 E5M2 却溢出成 Inf。OCP 规范两种方式(SAT / OVF)都允许。用低精度时,要查清楚溢出是怎么处理的。
4. 舍入与误差的积累 · Rounding and error accumulation
4.1 四种舍入方式
一个数落在两个可表示的数之间时,选哪一个?
- 就近,平局取偶(round to nearest, ties to even,RNE):IEEE 默认,numpy、GPU 的默认。平局时选尾数末位为 0 的那个。
- 截断(toward zero,RTZ):直接丢掉多余的位。硬件最便宜。
- 就近,平局远离 0(ties away):小学的”四舍五入”。
- 随机舍入(stochastic rounding,SR):以” 离下面那个数的距离 / ulp”为概率向上取,期望值正好是 。
演示 1 把 里 20 万个均匀分布的数舍入到 BF16(单位:ulp):
| 方式 | 平均误差(偏差) | 均方根误差 | 最大误差 |
|---|---|---|---|
| 就近取偶 | +0.0005 | 0.288 | 0.500 |
| 截断 | −0.4996 | 0.577 | 1.000 |
| 四舍五入 | +0.0005 | 0.288 | 0.500 |
| 随机舍入 | −0.0012 | 0.408 | 0.998 |
比误差大小更重要的是偏差。截断每次都偏小半个 ulp,这是系统性的,累加几千次就线性积累;无偏的误差有正有负,互相抵消,只按 增长。随机舍入单次误差更大,但无偏,而且对”比半个 ulp 还小”的变化也有反应(§4.3)。
为什么”平局取偶”而不是”四舍五入”?均匀分布的数几乎不会正好平局,两者没区别;但在低精度格式和整数里平局很常见(FP4 里 2.5 正好在 2 和 3 之间),“远离 0”会把所有平局都往外推,产生偏差;“取偶”一半向上、一半向下。
4.2 浮点加法不满足结合律
由 §3.4,每次加法 。所以 和 一般不相等——舍入发生在不同的地方。这有两个直接后果:
- 同一个求和,换个顺序,结果就可能不同。GPU 上的并行归约、多卡上的 all-reduce、不同的分块方式,都会改变求和顺序。“换了内核以后结果对不上最后几位”通常不是 bug(第 14 讲讨论 KernelBench 怎样设置容差)。
- 顺序的选择会影响误差(§4.4)。
4.3 停滞,以及随机舍入怎样救回来
在 BF16 里从 0 开始一直加 1:加到 256 时,ulp 变成 2, 正好是平局,取偶回到 256——加了等于没加。演示 2:
| 累加器 | 操作 | 精确答案 | 就近舍入 | 随机舍入(200 次的均值 ± 标准差) |
|---|---|---|---|---|
| BF16 | 1,000 次 + 1 | 1000 | 256 | 1000.1 ± 42.7 |
| BF16 | 10,000 次 + 0.01 | 100.098 | 4 | 100.3 ± 5.1 |
| FP16 | 10,000 次 + 1 | 10000 | 2048 | 9997.6 ± 177.5 |
| FP16 | 10,000 次 + 0.01 | 100.021 | 32 | 100.0 ± 1.8 |
这叫停滞(stagnation):累加器大到”增量不足半个 ulp”时,再怎么加都不变。它无处不在:点积的累加、求均值、softmax 的分母、优化器更新权重 。于是有混合精度的核心原则:
低精度存、低精度乘,高精度加。
随机舍入把停滞救了回来(平均值接近精确答案),代价是随机波动。这是一些 AI 芯片在低精度权重更新里使用随机舍入的原因(Gupta et al. 2015)。
4.4 求和算法:顺序、两两、Kahan
个数求和,三种算法的最坏情况误差界(Higham 第 4 章; 是单位舍入误差):
- 两两求和(pairwise):先两两相加,再把结果两两相加……每个数只参与 次加法,而且相加的两个数大小相近。GPU 上的并行归约(P2 §6)天然就是这个结构。
- Kahan 补偿求和:多用一个变量 记住每次被舍掉的低位,下一次补回来:
y = x_i − c # 先把上次丢掉的补回来
t = s + y # 加到和里(这里会丢掉 y 的低位)
c = (t − s) − y # (t − s) 是"实际加进去的",减去 y 得到"丢掉的"(取负)
s = t
演示 4 用 1 万个 里的正数,在 16 位累加器里比较:
| 累加器 | 算法 | 结果 | 精确答案 | 相对误差 |
|---|---|---|---|---|
| BF16 | 顺序 | 256 | 4994.17 | 9.49e-01 |
| BF16 | 两两 | 4992 | 4994.17 | 4.34e-04 |
| BF16 | Kahan | 4992 | 4994.17 | 4.34e-04 |
| FP16 | 顺序 | 2048 | 5058.1 | 5.95e-01 |
| FP16 | 两两 | 5056 | 5058.1 | 4.16e-04 |
| FP16 | Kahan | 5060 | 5058.1 | 3.75e-04 |
顺序求和停滞了;两两求和把误差降了三个数量级——BF16 的 4992 已经是 BF16 能表示的、离精确答案最近的数(5000 附近 BF16 的间隔是 32)。Kahan 在 FP16 里给出 5060,正好是”精确答案舍入到 FP16”。并行归约不仅更快,而且更准;Kahan 多花 3 次加减法,在 memory-bound 的内核里几乎免费。
4.5 低精度矩阵乘法的累加器
FP8 矩阵乘法的输入是 FP8,两个 FP8 的乘积在 FP32 里是精确的(两个 4 位有效数字相乘只有 8 位),所以误差几乎全部来自累加。累加器有多准?
DeepSeek-V3 技术报告(§3.3.2)有一个重要的观察:H800 上 FP8 GEMM 的累加”只保留约 14 位”,远低于 FP32;对 的随机矩阵,“最大相对误差接近 2%“。他们的解决办法是每 个元素提升一次:Tensor Core 在低精度里累加 128 个乘积后,把部分和拷到 CUDA core 的 FP32 寄存器里累加(这 128 个元素等于 4 次 WGMMA 指令;两个 warpgroup 交替,一个提升、一个继续做 MMA,掩盖开销)。
报告没有说明累加器具体怎样舍入,所以演示 3 用一个玩具模型:累加器保留 14 位有效数字(FP32 的指数 + 13 位尾数),分别试”就近舍入”和”截断”两种(每个 模拟 256 个输出;误差用”范数相对误差”):
| 累加器 | |||
|---|---|---|---|
| FP32 | 0% | 3.93e-06% | 1.05e-05% |
| 14 位,就近舍入 | 0.0259% | 0.0877% | 0.208% |
| 14 位,截断 | 0.317% | 4.01% | 17.8% |
| 14 位截断 + 每 128 提升到 FP32 | 0.0479% | 0.0464% | 0.0436% |
| 14 位就近 + 每 128 提升到 FP32 | 0.0107% | 0.0109% | 0.0113% |
| BF16 | 2.83% | 6.94% | 13.4% |
| FP16 | 0.331% | 0.903% | 1.98% |
读这张表:
- 截断的偏差随 积累得很快: 从 512 增加到 4096(8 倍),误差涨了约 13 倍;就近舍入的误差有正有负,只涨了约 3.4 倍(接近 )。DeepSeek 报告的”接近 2%“和两个玩具模型给出的量级范围一致。
- 每 128 个提升一次,误差几乎不再随 增长:低精度累加器里最多只有 128 个数,误差被定期”重置”到 FP32。
- 16 位累加器只适合很短的累加。这也是为什么”FP8 训练”真正难的部分不在 FP8 本身,而在累加和缩放的细节。
4.6 小结:混合精度的分工
| 环节 | 典型格式 | 原因 |
|---|---|---|
| 存储(权重、KV、激活) | BF16 / FP8 / FP4(加缩放因子) | 字节决定带宽和容量 |
| 乘法 | 和存储一样低 | 乘法器面积 ∝ 位数²,能量也低 |
| 累加 | FP32(或定期提升到 FP32) | 停滞、偏差、误差积累 |
| 归一化、softmax、方差 | FP32 | 指数、除法、相减对精度敏感(§5) |
5. 数值稳定性 · Numerical stability
一个公式在数学上正确,不代表它在浮点数里能算对。数值稳定的算法,不会把舍入误差、溢出、下溢放大成荒谬的结果。LLM 推理里最常见的两个例子是 softmax 和方差。
5.1 安全 softmax:先减最大值
两种写法数学上相等(分子分母同乘 ),数值上天差地别。 不溢出的最大 是 :FP32 / BF16 是 88.72,FP16 只有 11.09,FP8 E4M3 只有 6.10。演示 1 把一组 logits 整体放大:放大 5 倍(最大 15)时 FP16 的朴素 softmax 已经是 NaN(Inf / Inf),放大 30 倍时 FP32 也 NaN 了;减去最大值的版本在所有情况下都正常——所有指数 ,exp 的值在 里,最大那一项正好是 1,分母 。
5.2 log-sum-exp:在对数空间里算
对数概率 ,其中
如果先算 softmax 再取 log,概率下溢成 0 时就得到 。演示 2:logits 差距到 120 时, 小于 FP32 的最小正数,log(softmax) 给出 −inf,而 给出精确的 −120。推理引擎返回 logprobs、投机解码计算接受率(第 6 讲)、计算困惑度,都要这样算。
5.3 在线 softmax 与它的合并运算
安全 softmax 要读三遍输入:一遍求最大值,一遍求分母,一遍算输出。Milakov 和 Gimelshein(2018)发现前两遍可以合成一遍:维护”目前为止的最大值 “和”以 为基准的指数和 “,每来一个 :
旧的和要乘以 来”换基准”。例:, 依次是 、、。
更重要的是,两段的统计量可以合并:
满足结合律,所以一行可以切成很多段、交给不同的线程块各算一段,最后再合并。FlashAttention(按 KV 分块)和 flash-decoding(把很长的 KV 切成几段并行)都建立在这个运算上(第 11 讲)。
演示 3 在 32,000 个 FP32 logits 上比较(分母 对 FP64 的相对误差):
| 方法 | 的相对误差 |
|---|---|
| 两遍:先 max,再用 numpy 的 sum | 4.7e-08 |
| 两遍:先 max,再逐个累加 | 2.7e-05 |
| 在线一遍(逐个累加) | 2.8e-05 |
| 8 段各自在线 + 合并 | 1.0e-06 |
这张表有一个写代码时没想到的结果:在线算法的误差比”两遍法”大了几百倍——但原因不是”在线”,而是求和顺序。numpy 的 sum 内部用两两求和;把两遍法的第二遍也改成逐个累加,误差就和在线算法一样了(§4.4 的结论在这里重演)。分成 8 段再合并,误差降到 1e−6。真实的 GPU 内核里每个线程只累加几个元素,再用 warp shuffle 树形合并,所以误差很小。
访存上,在线 softmax 把每个元素的访存从 4 次(读、读、读、写)降到 3 次,对 memory-bound 的 softmax 是约 1.33 倍的理论加速;论文在 时实测最多约 1.3 倍。再把 softmax 和 top-k 融合,论文报告最多 5 倍——这是”算子融合”的预告(第 11–13 讲)。
5.4 注意力为什么要除以
的分量是方差为 1 的随机数时, 的方差是 。演示 4(,4,096 个 key):
| 分数 | 标准差 | 最大值 | 最大注意力权重 | 熵 / 均匀分布的熵 | FP16 朴素 softmax |
|---|---|---|---|---|---|
| 不缩放 | 10.90 | 46.1 | 0.975 | 0.12 / 8.32 | NaN |
| 除以 | 0.96 | 4.1 | 0.009 | 7.83 / 8.32 | 正常 |
不缩放时,softmax 几乎变成 one-hot(梯度很小,这是 Attention Is All You Need 给的理由),而且分数的绝对值大,低精度下更容易溢出。FlashAttention 等内核通常把 和 合成一个常数,乘到分数上再用 exp2 计算——exp2 在硬件上比 exp 便宜,而第 1 讲 §5.5 说过,exp 正在成为 Blackwell 上的瓶颈。
5.5 方差:灾难性抵消
方差有一个”一遍公式”,读一遍数据就能同时累加 和 。但当均值远大于标准差时,两个很大、很接近的数相减,有效数字全被抵消,剩下的主要是舍入误差——灾难性抵消(catastrophic cancellation)。演示 5(,标准差 1,FP32):
| 均值 | 精确方差(FP64) | 一遍公式 | 两遍法 | Welford |
|---|---|---|---|---|
| 0 | 0.995118 | 0.995118 | 0.995118 | 0.995117 |
| 1000 | 0.983014 | 0.937500 | 0.983014 | 0.983018 |
| 10000 | 0.982780 | −8.000000 | 0.982781 | 0.982971 |
均值 时,一遍公式算出的方差是负数。两遍法(先求均值,再求 )几乎和 FP64 一样,但要读两遍数据。Welford 算法一遍扫描、增量更新:
它的误差比两遍法略大(均值 时从小数点后第 4 位开始不同),但远好于一遍公式,而且像 一样可以分块合并。融合的 LayerNorm 内核想只读一遍激活,用的就是这类算法。RMSNorm 只需要 ,没有抵消问题——这也是它更受欢迎的原因之一。
6. 量化入门 · Quantization basics
量化(quantization)= 选一个缩放因子,把一组实数映射到低精度格式能表示的范围,再舍入。§3.8 的 loss scaling 是最早的例子;这一节把它系统化。
6.1 仿射量化:scale 和 zero point
把实数 映射到 位整数 :
- 对称(symmetric):,,。适合以 0 为中心的数据(权重)。
- 非对称(asymmetric):,,。适合偏向一侧的数据(ReLU、SiLU 之后的激活)。
演示 1 用 ReLU 之后的激活(一半是 0、一半是正数):对称 int8 只能用 0…127(实际用到 107 个编码),相对误差 1.077%;非对称 uint8 能用 0…255(实际用到 203 个),scale 减半,误差 0.535%——相当于多了 1 位。
6.2 只用整数的矩阵乘法
量化的真正好处是乘加可以全在整数里做。设激活非对称()、权重对称(每个输出通道 ):
方括号里全是整数:第一项是整数矩阵乘法(累加到 int32,§2.3),第二项只和权重有关,提前算好。最后乘一次浮点的 。演示 1 验证:整数流水线和”先反量化再用浮点乘”的最大差只有 7.11e-15(浮点舍入),和原始浮点结果的相对误差 0.943%。这就是 INT8 推理的标准做法。
6.3 截断:两种误差的拉锯
按 来定,意味着一个离群值就能把所有数的间隔撑大。另一个选择是截断(clipping):把范围缩到 ,超出的数截到边界。这时误差有两部分:舍入误差 (越小越好),截断误差( 越小,被截的数越多、截得越狠)。演示 2(INT4 对称):
| 分布 | max/σ | 不截断() | 最优 | 最优误差 |
|---|---|---|---|---|
| 高斯 | 4.7 | 19.5% | 0.5(≈ 2.4σ) | 11.5% |
| 拉普拉斯 | 8.1 | 32.4% | 0.4(≈ 3.3σ) | 16.8% |
| Student-t() | 29.3 | 84.6% | 0.1(≈ 2.9σ) | 21.4% |
尾巴越重,最大值离”大多数数”越远,截断的收益越大(Student-t 从 84.6% 降到 21.4%)。这条 U 形曲线是所有”选缩放因子”方法(MSE 搜索、百分位数校准、GPTQ / AWQ 的缩放搜索)背后的共同权衡。但截断扔掉的恰恰是最大的那些数——如果它们很重要(LLM 激活里的离群特征),截断就会出事。另一条路是缩小缩放的粒度。
6.4 粒度:一个缩放因子管多少个数
演示 3 构造了两个合成矩阵:权重 每一行(输出通道)有自己的尺度;激活 有 8 个”离群通道”,在每个 token 上都放大 60 倍(这是 LLM.int8 论文的观察,第 3 讲)。
| 矩阵 | 位数 | 整张量 | 每行 | 每 128 个一组 | 每 32 个一组 |
|---|---|---|---|---|---|
| :每行尺度不同 | INT4 | 52.61% | 15.72% | 11.72% | 9.70% |
| :8 个离群通道 | INT8 | 16.25% | 8.86% | 2.34% | 1.20% |
| :8 个离群通道 | INT4 | 38.00% | 36.28% | 15.25% | 8.34% |
- 权重按行(per-channel)缩放就解决了大部分问题,而且几乎没有开销(每行一个缩放因子),是权重量化的默认做法。
- 激活按 token(每行)缩放几乎没用(INT4 38% → 36%):离群通道在每个 token 上都在,每一行的最大值都被它决定。必须沿通道方向分组,离群通道才只”连累”同组的通道。
麻烦在于:矩阵乘法 恰恰沿通道方向(内维 )求和。缩放因子沿 变化时,不能在累加完以后统一乘一次缩放因子,必须在累加过程中处理。这正是 DeepSeek-V3”每 128 个提升一次”、MX / NVFP4 “硬件块缩放”要解决的问题;LLM.int8 走的是另一条路——把离群通道拆出去用 FP16 单独算(第 3 讲)。
组越小越准,但缩放因子的开销越大:每 32 个数一个 FP16 缩放因子,相当于每个数多 0.5 bit。
6.5 块缩放浮点格式:MX 与 NVFP4
把”每组一个缩放因子”标准化、并做进硬件,就是块缩放格式:
- MX(OCP Microscaling,2023,AMD、Arm、Intel、Meta、Microsoft、NVIDIA、Qualcomm 联合制定):每 个元素共享一个 E8M0 缩放因子(8 位,只能表示 2 的幂,)。元素可以是 FP8(E4M3 / E5M2)、FP6(E3M2 / E2M3)、FP4(E2M1)或 INT8。规范给出的转换算法:
对 FP4 来说,,块内最大值除完以后落在 ,超过 6 的会被截断。
- NVFP4(NVIDIA Blackwell):每 16 个元素一个 E4M3 缩放因子(有 3 位尾数,不必是 2 的幂),外加整个张量一个 FP32 缩放因子 ,把块缩放因子挪进 E4M3 的表示范围。平均每元素 bit。
演示 4 在第 1 讲同样的数据(100 万个 Student-t 随机数,同一个种子,所以 MXFP4 / NVFP4 的数字和第 1 讲完全一样)上比较:
| 格式 | 每元素比特 | 相对误差 | SQNR (dB) |
|---|---|---|---|
| FP8 E4M3,整张量缩放 | 8 | 2.65% | 31.5 |
| INT8,整张量(对称) | 8 | 6.68% | 23.5 |
| MXFP8(E4M3) | 8.25 | 3.16% | 30.0 |
| MXFP6(E2M3) | 6.25 | 3.05% | 30.3 |
| MXFP6(E3M2) | 6.25 | 5.49% | 25.2 |
| INT4,每 32 个一组(FP16 缩放) | 4.5 | 12.25% | 18.2 |
| MXFP4(E2M1) | 4.25 | 12.65% | 18.0 |
| NVFP4 | 4.5 | 9.23% | 20.7 |
| FP4 E2M1,整张量缩放 | 4 | 64.44% | 3.8 |
再把 NVFP4 和 MXFP4 的两个差别拆开(元素都是 FP4 E2M1):
| 块大小 | E8M0 缩放(2 的幂) | E4M3 缩放 | 精确缩放(理想) |
|---|---|---|---|
| 每 32 个 | 12.65% | 10.28% | 10.19% |
| 每 16 个 | 12.42% | 9.23% | 9.08% |
几点结论:
- 缩放因子的格式比块大小更重要:同样每 32 个,E8M0 → E4M3 把误差从 12.65% 降到 10.28%;同样用 E4M3,块从 32 缩到 16 只再降到 9.23%。2 的幂太粗:块内最大值除完以后可能落在 4–8 之间的任何地方,浪费刻度,还要截断超过 6 的数。而 E4M3 缩放已经非常接近”精确缩放”。
- 同样 4.5 bit,NVFP4 比 INT4 分组更准:重尾数据里,浮点元素”0 附近密、远处疏”的刻度比整数的均匀刻度更合适。
- 块缩放主要是为窄格式准备的:MXFP8(3.16%)反而比整张量缩放的 FP8(2.65%)略差——E4M3 自己的动态范围已经够用,而 2 的幂缩放会截断 0.788% 的元素(块内最大值除完以后落在 ,超过 448 的被截)。
- 块缩放之后,尾数比指数重要:MXFP6 的 E2M3(3.05%)比 E3M2(5.49%)准得多,甚至和 MXFP8 差不多。块内的数大小相近,用不上那么大的范围。这和 DeepSeek-V3”细粒度缩放后全部用 E4M3”是同一个道理。
6.6 误差怎样传到输出
第 1 讲 §5.4 留了一个问题:NVFP4 的逐元素误差约 9%,为什么模型精度能和 FP8 相差不到 1%?一个常见的猜想是”误差在点积里正负抵消了”。演示 5 直接检验:
| 权重格式 | 权重的相对误差 | 输出 的相对误差 |
|---|---|---|
| INT8 每行 | 2.01% | 2.00% |
| NVFP4 | 9.23% | 9.25% |
| MXFP4 | 12.63% | 12.65% |
| INT4 每 128 个一组 | 17.17% | 17.13% |
没有抵消。误差矩阵 和激活不相关时, 和 都按 增长,比值不变:逐元素误差 9%,每个矩阵乘法的输出也有约 9% 的噪声。答案在别处:模型对噪声有一定的容忍度(最终看的是 argmax 和排序)、不同层和不同张量的敏感度差别很大、量化方法会有针对性地保护重要的部分——这些是第 3 讲的内容。
最后一个经验法则:每多 1 位,SQNR 增加约 6 dB(间隔减半,误差减半,)。演示 5 的高斯数据从 INT5 到 INT8,每一位的增量是 +6.6、+6.3、+6.2、+6.1 dB;位数很少时(INT2–INT4)截断和”0 附近的刻度太粗”让增量偏离 6 dB。
7. 硬件视角 · The hardware view
把 §1 的三重收益和 Tensor Core 的发展对照起来看:
| 代 | 年份 | Tensor Core 新增的输入格式 |
|---|---|---|
| Volta(V100) | 2017 | FP16 |
| Turing(T4) | 2018 | INT8、INT4 |
| Ampere(A100) | 2020 | TF32、BF16 |
| Hopper(H100) | 2022 | FP8(E4M3 / E5M2) |
| Blackwell(B200 / B300) | 2024–25 | FP6、FP4、MX 块缩放、NVFP4 |
每一代都在往更窄的格式走,而累加始终至少是 FP32(或像 §4.5 那样定期提升)。还有两点值得注意:
- 格式越窄,“附加电路”越重要。FP4 本身很便宜,但块缩放要求硬件在点积里处理缩放因子(MX 规范把它写成 :先在元素上累加,再乘两个块的缩放因子)。NVFP4 的 E4M3 缩放因子比 E8M0 更准(§6.5),但乘一个 E4M3 比”加一个指数”(E8M0)贵一点——这是精度和硬件成本的又一个取舍,NVIDIA 博客也承认 E8M0 的优点是”简单”。
- 低精度改变了 roofline(第 1 讲 §3.7)。如果权重、激活和算力换成同一种更窄的格式,强度和屋脊点同比例变化;Blackwell Ultra 的 NVFP4 算力涨得比字节省得多,临界 batch 从 FP8 的 353 涨到 747。仅权重量化(W4A16)是另一种不对称:权重字节变少,计算仍是 16 位,decode 更快,prefill 不变。
8. 小结 · Summary
- 比特是有限的: 位只有 个编码。整数用补码,溢出时量化用饱和;INT8 乘积要累加到 INT32。
- 浮点数 = 符号 × 2^指数 × 1.尾数:指数位决定范围,尾数位决定精度。每次舍入引入不超过 的相对误差。FP16 / BF16、E4M3 / E5M2、E3M2 / E2M3 都是同一个取舍的不同答案。
- 误差会积累:舍入的偏差比大小更危险;累加器大到增量不足半个 ulp 就会停滞。低精度存、低精度乘、高精度加;两两求和和 Kahan 求和便宜而有效;FP8 GEMM 要定期把部分和提升到 FP32。
- 数值稳定:softmax 先减最大值;对数概率用 log-sum-exp;在线 softmax 的 可以分段合并,是 FlashAttention 的基础;方差不要用一遍公式。
- 量化:缩放因子把数挪进格式的范围;截断用”大数的误差”换”小数的精度”;粒度越细越准,但缩放因子要沿内维变化时需要硬件支持;块缩放格式(MX、NVFP4)把这一切标准化,缩放因子的精度比块大小更重要。
- 误差原样传到输出:矩阵乘法不会让量化噪声的相对大小变小。模型为什么能容忍它,是第 3 讲的问题。
9. 常见误区 · Common pitfalls
- “浮点运算有误差,说明代码有 bug。” 0.1 本身就存不准;换一个求和顺序结果就会变。判断对错要用合适的容差(第 14 讲)。
- “FP16 和 BF16 差不多,都是 16 位。” 最大值差了约 倍,精度差了 8 倍,适用场景完全不同。
- “FP8 就是 FP8。” E4M3 和 E5M2 是两种格式;转换时还要看是饱和还是溢出成 Inf / NaN。
- “低精度的问题主要在输入的舍入。” 对长的点积,累加器的精度同样重要(§4.5);FP8 GEMM 的误差几乎全部来自累加。
- “截断(四舍五入到零)和就近舍入差不多。” 单次误差差 2 倍,但截断有系统性偏差,累加 4096 次后误差差了 40 多倍(§4.5 表)。
- “在线 softmax 不如两遍法准。” 在线本身不损失精度;误差来自求和顺序(§5.3)。
- “按 token 缩放就能处理激活的离群值。” 离群值集中在固定的通道,要沿通道方向分组(§6.4)。
- “块越小越好。” 块小了缩放因子的开销变大;而且缩放因子的格式(2 的幂还是 E4M3)比块大小影响更大(§6.5)。
- “量化误差会在矩阵乘法里抵消。” 不会,相对误差原样传递(§6.6)。
10. 练习 · Exercises
带 💻 的题可以用
numfmt验证。参考答案在每题后面。
练习 1(补码)。写出 的 8 位补码。8 位编码 11110110 代表哪个有符号整数?
参考答案
,即 11111011(也可以由 5 = 00000101 取反得 11111010,再加 1)。11110110 = 246,作为有符号数是 。验证:twos_complement_encode(-5, 8) = 251,twos_complement_decode(0b11110110, 8) = −10。
练习 2(手工编码 FP16)💻。把 1.5 和 −0.375 编码成 FP16(1 | 5 | 10)。
参考答案
:符号 0,指数字段 = 01111,尾数 1000000000。:符号 1,指数字段 = 01101,尾数 1000000000。验证:bits_str(FP16.encode(-0.375), 16, (1, 5, 10)) 得 1 01101 1000000000。
练习 3(BF16 的舍入)💻。 和 舍入到 BF16 分别是多少?
参考答案
BF16 在 的 ulp 是 。 正好在 1 和 中间,平局取偶(1 的尾数末位是 0)得 1。,离 更近,得 1.0078125。
练习 4(设计一种格式)💻。一个 8 位格式 E3M4、偏置 3。按 IEEE 的约定(指数全 1 留给 Inf / NaN),最大值是多少?如果像 FP6 / FP4 那样不要特殊值呢?最小正数是多少?
参考答案
IEEE 约定:最大指数 ,最大值 。不要特殊值:最大指数 ,最大值 。最小正数 。用 FloatFormat("E3M4", 3, 4, 3, "ieee").max_value 验证。
练习 5(停滞)💻。在 FP16 累加器里从 0 开始反复加 0.5(就近舍入),会停在哪里?为什么?
参考答案
停在 1024。FP16 在 的 ulp 是 , 正好是平局,取偶(1024 的尾数是 0)回到 1024。验证:accumulate(np.full((1, 5000), 0.5), FP16) 得 1024。(对比:加 1 时停在 2048。)
练习 6(求和的误差界)。用 BF16 累加 个正数。顺序求和、两两求和的最坏情况相对误差界各是多少?这说明了 GPU 树形归约的什么好处?
参考答案
。顺序:(大于 1,界失去意义,§4.4 的表里顺序求和确实完全失败);两两:。树形归约的深度只有 ,每个数经历的舍入次数从 降到 。
练习 7(在线 softmax)。对 手算在线 softmax 每一步的 ,以及最终的 softmax。再把 和 两段分别算出 ,用 合并,验证结果相同。
参考答案
。softmax 。分段:,;合并 ,。
练习 8(仿射量化)。把 用 8 位对称量化,scale、编码分别是多少?反量化的结果呢?
参考答案
,,反量化正好是原数(这组数恰好都是 0.01 的倍数)。quantize_int(np.array([-0.5, 0.2, 1.27]), 8) 给出同样的结果。
练习 9(MX 的缩放因子)💻。一个 MXFP4 块的最大值是 5.0,其余元素包括 0.3 和 −1.2。按 OCP 规范的算法,缩放因子 是多少?这三个数量化成多少?NVFP4(假设这 16 个数单独成一个张量)呢?
参考答案
MXFP4:。5.0 正好在 E2M1 的 4 和 6 中间,平局取偶得 4(误差 20%);0.3 → 0.5;−1.2 → −1.0。NVFP4:张量级因子 ,块因子 (E4M3 正好能表示),实际缩放 。于是 5.0 → 6 × 5/6 = 5.0(精确),0.3 → 0.4167,−1.2 → −1.25。块最大值被精确表示,正是 E4M3 缩放的优势。
练习 10(显存账)。一个 700 亿参数的模型,用 BF16、FP8、MXFP4、NVFP4、INT4(每 128 个一组、FP16 的 scale 和 zero point)存权重,各要多少 GB?
参考答案
BF16:140 GB;FP8:70 GB;MXFP4: GB;NVFP4:4.5 bit → 39.4 GB;INT4 分组: bit → 37.2 GB。4 位格式把 70B 模型塞进一块 80 GB 的 H100 还剩约 40 GB 给 KV cache。
练习 11(INT32 够不够)。 的 INT8 矩阵乘法,最坏情况下 INT32 累加器会溢出吗? 最大到多少才保证不溢出?
参考答案
最坏情况 ,不会。保证不溢出的最大 是 (对称量化不用 −128 时;用 −128 则乘积最大 16,384)。
11. 延伸阅读 · Further reading
- D. Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic, ACM Computing Surveys, 1991:浮点数的经典入门。
- N. Higham, Accuracy and Stability of Numerical Algorithms(第 2 版, SIAM 2002):第 2 章(浮点运算)、第 4 章(求和)。
- OCP 8-bit Floating Point Specification (OFP8) 与 Microscaling Formats (MX) Specification v1.0:FP8 / FP6 / FP4 / E8M0 的权威定义,只有十几页。
- P. Micikevicius et al., Mixed Precision Training(ICLR 2018):loss scaling;FP8 Formats for Deep Learning(2022):E4M3 / E5M2 的设计。
- B. Darvish Rouhani et al., Microscaling Data Formats for Deep Learning(2023):MX 格式的实验。
- NVIDIA 技术博客 Introducing NVFP4 for Efficient and Accurate Low-Precision Inference(2025)。
- DeepSeek-AI, DeepSeek-V3 Technical Report §3.3:细粒度量化、累加精度提升、E4M3 全覆盖。
- M. Milakov and N. Gimelshein, Online normalizer calculation for softmax(2018):FlashAttention 的前身。
- 本仓库 CS349D 附录 A:从推理系统角度看量化方法(AWQ、GPTQ、SmoothQuant、KV 量化)。
12. 和正课的联系 · Where this shows up
- 第 1 讲:§5.4 的 NVFP4 实验在本讲 §6.5 被拆开(缩放因子格式 vs 块大小),§6.6 回答了”逐元素误差和任务精度”的第一步。
- 第 2 讲(Transformer):注意力里的 、安全 softmax、RMSNorm(§5)。
- 第 3 讲(精度、量化与稀疏):在真正的(小)Transformer 上做量化:离群特征、LLM.int8 的混合精度分解、仅权重量化 vs W8A8 vs FP8、MX 与 NVFP4。
- 第 7 讲(KV cache):KV cache 用 FP8 / INT4 存,容量翻倍;按什么粒度缩放(每 token 还是每通道)又是 §6.4 的问题。
- 第 8 讲(硬件引擎):Tensor Core 的输入 / 累加格式,脉动阵列里的累加器宽度。
- 第 11 讲(内核优化):FlashAttention 和 flash-decoding 就是 §5.3 的在线 softmax 加上分块。
- 第 14 讲(智能体生成内核):判断生成的内核”对不对”,需要理解浮点误差和合理的容差(§4.2)。