FP8 是 DeepSeek V3 的三大贡献之一,FP4 权重已经出现在更新的模型上。 所有人都在往下压精度。
但「压精度」不是一个开关,它是一整套决策:
目标:看到一个量化方案,能说出它在哪里省、风险在哪、 以及怎么验证它没把模型搞坏。
两个收益,性质不同(这一条要讲透,否则后面分不清训练和推理):
所以量化在推理侧的收益比训练侧直接得多: decode 读一遍权重就是全部开销,字节减半近似就是快一倍。
要立的一条主线:范围(指数位)比精度(尾数位)重要。
E8M0(范围极大、精度极差),NVFP4 选了 E4M3
(有尾数、范围只到 448)—— 同一个取舍,换了个位置又演一遍。
见 §3。这一节是整讲的核心。从一个问题往下长:四个 bit 只能表示八个数, 这怎么可能算得准?
先把这一段的问题问出来:E2M1 一共四个 bit,
能表示的绝对值只有八个 —— 这怎么可能算得准?
答案不在那四个 bit 上,在外面套的那层 scale。 四个 bit 只负责「形状」,量级由 scale 给。 所以真正决定它能不能用的,是那个 scale 有多密。
密到什么程度算够?拿一个真尺寸走一遍 —— 设某层权重是 7,168 × 18,432。
⚠️ 「一个 tensor」是整块矩阵,不是它的某一条边 —— per-tensor 不等于「按 7,168 量化」,后者是 per-channel。
上一代 FP8 就是最粗的那一档:整张量一个 scale, 而且还得靠软件盯着历史极值去猜它。 一个离群值就把整张量的 scale 拽跑,剩下那些小数全被压进同一个格子。 FP8 有 256 个可表示值,还忍得住;FP4 只有八个,这么干直接崩。
所以 Blackwell 干的事只有一件:把共用 scale 的范围从「整张量」缩到「十六个数」。 这一小撮就叫一个 micro-tensor。
⚠️ 这件事有四个名字,别以为是四件事: NVIDIA 叫 micro-tensor scaling,OCP 标准叫 microscaling(MX) (MXFP4 的 MX 就是它),写 kernel 的人叫 block scaling, 本课叫块量化。
⚠️ 这里有个真的命名碰撞,中文尤其容易踩: 「块」在日常语感里像是二维方块, 于是很自然会以为块量化是「横着几个、竖着几个」拿一小块。
但在这套术语里,「块」不蕴含二维。 NVIDIA 自己给 block quantization 的定义就是沿某一个轴按固定长度分块 —— 文档里连公式带例子:取一个轴当 blocking axis、给一个 block size, 另一个轴不分。所以一维的那 16 个,厂商就管它叫 block。
同一个形状还有别的名字:sub-channel、group-wise —— GPTQ/AWQ 那边的「group size 128」说的就是它; Google 讲 Ironwood 调优的公开文章里也写作 subchannel activation quantization。 它们和 block scaling 指的是同一件事,只是出身不同的社区。
真正的二维块量化是另一回事,而且一般会把维度写出来 —— 比如 DeepSeek V3 的 FP8 权重是 128×128 的二维块。 看到「块量化」而没写维度,默认它是一维的。
两个格式的数值部分完全一样,差别全在 scale 上:
E8M0 的 scale ——
纯指数没有尾数,所以倍数只能是 2 的整数次幂。E4M3(就是 FP8)——
有尾数,不再卡在 2 的幂上;外面再套一个整张量共用的 FP32,两级。代价是平均位宽:NVFP4 是 4 + 8/16 = 4.5 bit, MXFP4 是 4 + 8/32 = 4.25 bit。
⚠️ 那两级 scale 各是什么类型?—— 差别很大,而且是算出来的,不是随便定的。
UE4M3(不带符号位,4 指数 3 尾数);
MXFP4 用 E8M0(纯指数,连尾数都没有)。
都不是 FP32,也不是 BF16。为什么块内那级只能给 8 bit —— 因为它要摊到每个值头上。
NVFP4 每 16 个值配一个 scale,8 ÷ 16 = 每个值多摊 0.5 bit。
换成 BF16 就是 16 ÷ 16 = 每值多摊 1 bit,4 bit 的格式变成 5 bit;
换成 FP32 就是多摊 2 bit,直接变 6 bit ——
省下来的那一半全吐回去了,还不如直接用 FP8。
而 per-tensor 那一级不用摊,一整张量就一个数, 占多少位都约等于零 —— 所以它可以大方地用 FP32。
⭐ 这就是这套格式的设计逻辑:精度砸在不用摊的那一级上。
⚠️ 那内层外层到底怎么分工?—— 外层管范围,内层管局部量级。 两级不是「更高级」,是为了补一个坑。
坑在这里:NVIDIA 想让块内 scale 带尾数(比 2 的幂更准),
于是挑了 E4M3。
但 E4M3 自己能表示的最大值只到 448 ——
而一个真实张量里,不同块之间的量级可以差出好几个数量级,
448 那点范围根本盖不住。
所以才有外层那一级:先用一个 FP32 的全局 scale 把整张量搬进内层能表示的区间,剩下的局部差异再交给每 16 个数的 E4M3。 NVIDIA 自己对这一级的定性是「防溢出」,不是「提精度」。
范围能算出来:一个值最终 =
全局 FP32 × 块内 E4M3 × 那个 E2M1。
后两级叠满就是 448 × 6 = 2,688 ——
所以全局那一级大致取「整张量里最大那个块的 amax ÷ 2,688」,
正好把最大的块顶到不溢出的位置。
⭐ 反过来就能看懂 MXFP4 为什么只有一级:
E8M0 是纯指数、8 位全给幂次,范围大得离谱
—— 根本不存在「盖不住」的问题。
它不需要外层,代价是那个 scale 只能是 2 的整数次幂,粗。
一句话:NVFP4 拿「多一级」换「scale 更准」,MXFP4 拿「scale 更粗」换「结构更简单」。
⚠️ 那机器怎么知道自己拿到的是哪一种?—— 写在指令里,三处限定符。
kind:NVFP4 是 kind::mxf4nvf4,
MXFP4 是 kind::mxf4。scale_vec:NVFP4 是 4X,MXFP4 是 2X
—— 这个倍数不用查也能对上:一条指令 K=64,
64 ÷ 16 = 4 个 scale,64 ÷ 32 = 2 个。stype(scale 的类型):NVFP4 是 ue4m3,
MXFP4 是 ue8m0。⚠️ 外层那个 per-tensor FP32 不在指令里 —— 它是软件在量化和收尾时自己乘进去的,硬件只认块内那一级。
⚠️ 那 16 个数是怎么排的?—— 是一条线,不是一个方块。
很自然会猜它是「横 4 竖 4」的小方块。不是。 它是一行里沿收缩维连续的 16 个数,完全不跨行。
不用信转述,直接读那条指令的分片形状:
NVFP4 那条 mma.sync 吃进的 A 片是 16×64(M×K),
而它的 scale 片 SFA 是 16×4。
64 ÷ 16 = 4 —— 一行 64 个 K 切成 4 段,每段一个 scale。
B 那边同理:B 片 64×8,SFB 是 4×8。
所以落到激活上,一个 block = 同一个 token 的连续 16 个特征。 它一个 token 都不跨。
那为什么非得沿收缩维?
因为 scale 必须能从求和号里提出来。
沿 K 切成一段一段,每段内 sA、sB
都是常数,那一段的点积就是
sA·sB·Σ(a·b) ——
一段只要一次额外乘法。
换成「每个通道一个 scale」就提不出来了
—— 每一项的系数都不同,求和号里再也拆不开。
所以「沿 K 分段」是「仍然能提出来」的前提下能做到的最细粒度。
⭐ 最要紧的一点:它不需要「相邻的数值相近」这个假设。
NVIDIA 给的理由从头到尾只有一条 ——
更多次机会去贴合局部的动态范围。
要的只是「这 16 个数的量级差得别太远」,不是「它们彼此相似」。
而这是个统计性质,跟相不相关无关:
十六个数里出一个离群值的概率,远低于一亿三千万个数里出一个;
真出了,被拖累的也只有那十五个邻居。
⚠️ 但要小心,别把「硬件是一维」读成「二维块缩放没人用」。
NVIDIA 自己的 NVFP4 训练 recipe,权重那一侧就是 16×16 的二维块 —— 官方文档原话是「一个 scaling factor 由一个 16×16 的二维块共享」, 并说这跟 DeepSeek V3 训练里的权重量化是同一路子,只是粒度细得多。
这两件事不矛盾,它们在不同层: 硬件那一层永远是一维 16;二维是叠在它上面的一层约束 —— 算出一个 16×16 块的 scale 之后,让那 16 行各自的 1×16 共用这一个值。 (这一句不是推测:论文原文就是「2D block scales are replicated for each of the 1×16 blocks when being passed into Tensor Cores」。)
先回到专题四那张 16 字节的表,从精度角度重看一遍:
fixed 固定缩放会毁掉收敛,不是「精度略降」,是训坏⭐ 那量化那一步,具体是怎么算的?
拿一个 16 个数的块走一遍(推理侧、NVFP4):
α(FP32)。
取法大致是「最大那个块的绝对值最大值 ÷ 2,688」
—— 2,688 = 448 × 6,就是后两级叠满能表示的上限。amax。Δ = amax ÷ 6 ÷ α,
再把它round 成 UE4M3 存起来。
(除以 6 是因为 E2M1 最大只能表示 6;除以 α 是把它挪进 E4M3 的量程。)q = round( x ÷ (α · Δ) ),
结果落进那八个可表示值里,存成 4 bit。
权重和激活用就近偶数舍入,梯度用随机舍入。反量化就是把这条路倒着走:x ≈ α · Δ · q。
其中 Δ · q 那一步是 Tensor Core 在 MMA 里顺手做的,
α 那一步是软件在 FP32 输出上乘回去的。
(所以硬件只认两级里的一级 —— 这一条前面说过,这里对上了。)
⭐ 再补一个图景,不然下一段听不懂:一个线性层其实是三个矩阵乘。
y = w · x∂x = wᵀ · ∂y
—— 算给上一层的梯度。∂w = ∂y · xᵀ
—— 算这一层权重自己的梯度。三个都在 FP4 上算,输出是 BF16 或 FP32。
注意第二个:它用的是 wᵀ —— 同一个权重,被转置着又用了一次。
下一段整段就是在处理这一件事。
⭐ 那 16×16 到底怎么回事?—— 这是训练侧的事,而且理由不是「更准」。
先把配方摆出来(出自 NVIDIA 那篇 NVFP4 预训练论文):
权重用 16×16 的二维块,
激活和梯度仍然用 1×16 的一维块。
(那 16×16 是「16 个输入通道 × 16 个输出通道」。)
为什么权重要特殊照顾?—— 因为反向传播会把它转置。
缩放必须沿点积那一维,这条前面说过。
可前向算的是 y = w·x,反向算的是 ∂x = wᵀ·∂y
—— 点积那一维换了。
于是同一个权重,在前向被沿行切块量化,在反向被沿列切块量化,
得到两份不一样的量化结果。
⛔ 这不是「精度差一点」,这是链式法则被打破。
论文说得很直白:反向传播算的梯度,对应的已经不是前向那个函数了
—— wfprop ≠ wbprop,
你在给另一个函数求导。
16×16 就是来堵这个的:一个正方形的块,转置之后还是同一个块。 前向反向拿到的是同一份量化权重,链式法则重新成立。
⭐ 最反直觉的一点:16×16 是 256 个数共用一个 scale, 比 1×16 粗十六倍。 它更粗,却更好 —— 论文的原话就是「尽管块粒度更大, 2D 仍然改善了 loss」。说明这里赢的不是精度,是一致性。
那硬件在这里干了什么?—— 什么都没多干。
Tensor Core 只认沿点积维的一维 scale,这是硬性约束
—— 论文自己也这么写。
所以 16×16 是这么落地的:软件按 16×16 算出一个 scale,
然后把它复制十六份,当作那 16 行各自的 1×16 scale 喂进 Tensor Core。
硬件从头到尾不知道有「二维」这回事,它看到的还是十六个一维块 ——
只不过那十六个 scale 的值恰好一样。
⚠️ 所以二维不省存储:喂给硬件的 scale 张量大小跟一维时一模一样, 省的不是空间,买的是「转置前后是同一份」。外层那个 per-tensor FP32 照旧。
⚠️ 论文里确实出现过 4,但那是另一个东西。 它比较的 d = 4 / 16 / 128 是随机 Hadamard 变换的矩阵尺寸 —— 用来把离群值打散的那一步,跟块大小不是一回事。 结论是 d = 16 比 4 收敛好、跟 128 差不多,所以选了 16。 两个 16 同值不同物,最容易混。
配方里还有两条跟量化方式直接相关的: 梯度用随机舍入(去掉量化偏置), 权重和激活用就近偶数舍入; 数值敏感的层整层留在高精度。 (另外论文发现:训练后段把前向切回 BF16 能把 12B 模型的相对误差从 1.5% 拉回 0.5%,而切反向没有同样的收益。)
同一路子的还有 DeepSeek V3 的 FP8 权重 —— 128×128 的二维块。 所以判据要改一句:看到「块量化」而没写维度,说的多半是硬件那层的一维; 但训练侧的 recipe 很可能在上面又叠了二维,得看具体 recipe。
这一节接专题二。 「硬件支持 FP4」这句话,落到硅上到底是什么?
所以 HBM 里放的是 FP4,不是 FP8 —— 打包好的 E2M1 加那一串 scale,省下的一半就是这么省的。
那「硬件支持 FP4」到底加了什么?—— 没有协处理器。 加的是三样,分布在三个不同的地方:
有一个反例能把 ① 和 ③ 是两块独立硬件钉死: 桌面那颗 GB10 有 FP4 的 Tensor Core 指令,却缺那条转换指令 —— 能拿 FP4 算矩阵乘,却没法把数转成 FP4。 同一颗芯片上,一个有一个没有。 (出自 NVIDIA 开发者论坛的实测报告,不是官方规格书。)
于是分工很清楚:硬件负责「用」scale,软件负责「算」scale。 权重是离线量化好、直接以 FP4 存进去的;激活是运行时由 kernel 在上一步收尾里算的。
硬件那条乘法长这样:指令是 tcgen05.mma 的 block-scale 变体
(B200 走这条)和 warp 级的 mma.sync…block_scale
(消费级那颗 die 走这条)—— 这正是上面那道门槛的来处。
A、B 是 FP4,两串 scale 叫 SFA/SFB,
Tensor Core 一边读 E2M1 一边把 scale 乘进去,累加仍是 FP32。
⚠️ scale 不是「放进 Tensor Core 里」,是放进 TMEM。
TMEM 是 SM 上的一块内存 —— 每 SM 256 KiB,
128 lane × 512 column、每格 32 bit,路径是 显存 → 共享内存 → TMEM。
(⭐ lane = TMEM 的「行」,一条 lane 对应一个线程的落点 —— 所以下面那句「一个 warp 只够得着 32 条 lane」说的就是 warp 的 32 个线程。)
它装的是累加器和 scale,不是 FP4 操作数本身(那些从共享内存流进来)。
⭐ 「乘 scale 不额外消耗算力」这句对 —— 那一乘是流水线里的一级,不占发射槽,也不吃 FP4 的峰值。 但「不花钱」是另一回事,代价有三笔,只是都不算在 FLOPs 头上:
连起来,一层的来回是这样一圈:
① 上一层的结果在 TMEM 里是 FP32 累加值 →
② 收尾时就地量化成 FP4 + scale,写出去 →
③ 下一层把 FP4 搬进共享内存、scale 搬进 TMEM →
④ Tensor Core 边读边乘 scale,累加又回到 FP32 → 回到 ①。
⭐ 一句话:存的是 4 bit,算的是 32 bit。 进来那一下的换算焊在 MMA 流水线里,出去那一下 是普通核跑一条新指令。
kind 分两组:f8f6f4 /
mxf8f6f4.block_scale)—— 2×,四种布局都收mxf4 / mxf4nvf4)
—— 4×,但只支持 TN 布局
下面四块都是把上面那三样往下再追一层, 主线上跳过不影响听懂。第 ② 块最实用 —— 它能解释「为什么我换了 FP4 却没快一倍」。
| 操作数格式 | 一个 scale 盖多少个元素 | scale 的类型 |
|---|---|---|
mxf8(E5M2 / E4M3) | 32 | UE8M0 |
mxf6(E3M2 / E2M3) | 32 | UE8M0 |
mxf4(E2M1)= MXFP4 | 32 | UE8M0 |
nvf4(E2M1)= NVFP4 | 16 | UE4M3 |
两种 scale 类型都是无符号八位、都支持 NaN 但不支持无穷。
UE8M0 把八位全部给指数(取值只能是 2x,
−127 ≤ x ≤ 127);UE4M3 是四位指数三位尾数。
mxf4nvf4 这一档,块长 16 时 E8M0 和 E4M3 都收,
块长 32 则必须配 E8M0。
也就是说「块 16 + 纯指数 scale」这个组合硬件是支持的,
只是没有哪个标准格式叫这个名字。
kind
分档标速度的(倍率相对 Hopper 的 FP8 Tensor Core):
| 指令 kind | 能吃什么 | 速度档 | 矩阵布局 |
|---|---|---|---|
kind::f8f6f4 | f4 / f6 / f8 混着来 | 2× | 四种都行 |
kind::mxf8f6f4.block_scale | mxf4 / mxf6 / mxf8 | 2× | 四种都行 |
kind::mxf4.block_scale | 只吃 mxf4 | 4× | 只有 TN |
kind::mxf4nvf4.block_scale | mxf4 或 nvf4 | 4× | 只有 TN |
TN = A 行优先、B 列优先。4× 那两条只认这一种排法。
⭐ 所以「B200 的 FP4 是 FP8 的两倍」这句话有两个隐含前提: 你走的是专用那条 kind,而且矩阵摆的是 TN。cta_group::2)
—— 只有累加器住在公共内存里才做得到。量化出问题往往不报错。已经攒下三类,一类比一类隐蔽:
wfprop ≠ wbprop)
—— 它连「开关没生效」都算不上:一切正常,只是你在给另一个函数求导。
见 §4。所以:每一次精度改动都必须有对照实验,而且要跑足够长才看得出差别。
按「结论 ← 材料」排,不按书目排 —— 要回查的人手里拿的是结论,不是书单。 凡是推的都单独标了。
⭐ 出处台账 —— 上面每一条结论是从哪儿来的
mxf4nvf4 块 16 可配两种 scale、TMEM 按列分配(2 的幂、≥32 列)、
一个 warp 只够得着 32 条 lanekind 的速度档与布局限制(2× 对 4×、
4× 只支持 TN)tcgen05.mma 指令表⚠️ 本段里属于「推的」只有一处,已在原处标明: 「集合通信为什么也适合交给 SparseCore」那一句 —— 那是 3.5 另一半的事,跟 FP4 无关,写在这里只是为了台账完整。 FP4 这一段的每一条都能对上上面某一份材料。
⚠️ 最后澄清一个名字:「Transformer Engine」不是物理部件 —— 它是「支持块缩放的 Tensor Core + 决定怎么选 scale 的软件库」的合称, 芯片版图上找不到它。所谓第二代,差别就是上面那条: 第一代整张量、软件猜;第二代每十六个一个、硬件乘。
← 回 课程总纲 · 这一讲的硬件背景在 专题二 · TPU 与 GPU · 📝 讲义