unitstride
从推理加速视角,看量化 · 第 1 篇 · unitstride

量化在推理里省的是什么:
容量、带宽和算力

发布于 2026-10-21

拿到一个量化模型,我们最先要搞清楚的通常是这几件事:它省了什么,什么场景下会变快、什么场景下不会,config.json 里该看哪几个字段,以及如何衡量量化精度。量化算法内部怎么做,这篇基本不涉及。

1为什么推理要量化

量化就是用更少的比特来存数和算数。推理时显存里放着权重和 KV cache,每一步 decode 都要把权重搬到 Tensor Core 上做矩阵乘,所以量化能省三种资源:显存容量、显存带宽和算力。以 BF16 换成 8 位为例,每个数的字节数减半,三种资源受益的场景各不相同:

BF16:每个权重 2 字节显存 HBM · 容量权重KV cache带宽每步搬 8 块,每块 2 单位大小芯片 · 算力Tensor Core989TFLOPS输出FP8:每个权重 1 字节显存 HBM · 容量权重KV cache带宽同样 8 块,每块减半,搬得更快芯片 · 算力Tensor Core1979TFLOPS输出权重腾空间给 KV cache承载更高并发 → 吞吐更高decode 原本受带宽限制→ 现在搬运数据量大幅减少带宽瓶颈缓解 → decode 更快prefill 原本受算力限制→ 现在 FP8 是 BF16 计算速度两倍→ prefill 更快
示意图 · 显存占用和数据块大小的比例是示意;带宽(通道宽度)两行相同(假设其余条件不变,只有权重字节减半)。算力数字来自 NVIDIA H100 SXM 规格页:BF16 989 / FP8 1979 TFLOPS(dense,不含稀疏加速)

2数字在显存里怎么存

符号 / 指数 / 尾数

浮点数由三部分组成:

FP321 + 8 + 23范围 ±3.4e38BF161 + 8 + 7范围同 FP32FP161 + 5 + 10范围 ±65504FP8 E4M31 + 4 + 3范围 ±448符号指数(管范围)尾数(管精度)
示意图 · 每个小格是 1 个比特,格数按各格式规定的真实位数画

FP8 E4M3 只有 4 位指数、3 位尾数,范围和精度两头都很紧:

位数从 16 降到 8 省下了容量和带宽,付出的代价是每个数只剩 3 位尾数,一共 254 个有限值。下一章看把高精度的数转成这些格式时会发生什么。

3从高精度到低精度,再回到高精度

直觉做法:直接舍入

从高精度到低精度,最先想到的办法是直接舍入。以 FP32 转 BF16 为例:BF16 的指数和 FP32 一样是 8 位,范围相同,只是尾数从 23 位砍到 7 位。所以 FP32 转 BF16 直接舍入就行,丢的只是精度,不会越界。下面拿 0.0137 走一遍。

FP32 存 0.0137:1 位符号 + 8 位指数 + 23 位尾数FP32符号指数 8 位尾数 23 位00111100011000000111010111110111尾数后 16 位:砍掉这部分不到保留下来最后一位的一半,舍入不进位FP32 = 0.01370000001
示意图 · FP32 / BF16 的真实比特;转换按就近舍入(round-to-nearest-even)。0.0137 被砍掉的低 16 位不到一半,舍入结果和直接截断相同

再换一个数,3.14159 转成 BF16 是 3.140625,相对误差约 0.03%。BF16 有 7 位尾数,舍入的最大相对误差在 2-8 ≈ 0.39% 以内,对推理来说通常可以接受。

FP16 要小心一些:它只有 5 位指数,最大 65504,最小正规数约 6.1 × 10-5。权重一般落在这个范围里,可以直接转;但激活或中间结果偶尔会超出去,大的溢出成 inf,小的下溢。这常被认为是大模型训练更多用 BF16 的原因之一。

这个直觉放到 FP8 上为什么行不通

大模型里的权重和激活,数值大多集中在 0 附近,量级很小。下面用一组量级很小的示意数据来看:10 万个数,服从均值 0、标准差 0.002 的正态分布。直接转 BF16 的误差上面已经看过,问题出在直接转 FP8。

直接转 FP8:这组数都挤在尺子正中间,两侧的刻度全部浪费① 不 scale:直接转 FP8浪费:这一侧的刻度一根都用不上浪费:这一侧的刻度一根都用不上−448−2560256448放大 ±0.0095:这一段全在次正规区(红),只有 9 根刻度−0.008−0.00400.0040.008中位相对误差 45.1%,37.5% 变成 0,只剩 9 个值② 先 ÷ scale(scale = absmax ÷ 448)再转−448−2560256448中位相对误差 2.2%,变成 0 的 0.001%,用上 231 个值
示意图 · 示意分布 N(0, 0.002²),10 万个数、随机种子 0;动画里的点是从中均匀抽取的 120 个;误差、变成 0 的比例和不同值的个数用全部 10 万个数统计;FP8 舍入一律按 E4M3 刻度就近舍入

图里标红的次正规区,是 FP8 最小正规数 2-6 ≈ 0.0156 以下的那一段。到这里指数位已经是全 0,不再补隐含的 1,刻度没法继续按 2 倍区间往 0 加密,只能等间距地排下去,间距固定是 2-9 ≈ 0.00195。数越小,这个间距相对它自己就越大,所以越靠近 0 相对精度越差。

直接转的问题在于这组数挤在 ±0.0085 以内,用不到 FP8 大部分的刻度。先用一个比例尺把这组数整体放大,让最大的那个数正好落在 448,再转 FP8,就能用上整把尺子。这个比例尺就是 scale:先把高精度的数按这个比例映射到 FP8 的刻度上,用的时候再映射回去。这时自然会有两个问题:

具体怎么映射:量化后的数存的是什么

先用 scale 缩放,量化后的数就不再是原来那个数了。比如一组权重里绝对值最大的是 0.05,那么其中的 0.02 量化之后是 176(0.02 ÷ (0.05 ÷ 448) ≈ 179.2,再舍入到最近的刻度 176)。单独看 176 这个数没有意义,和真实值 0.02 差了近 9000 倍。

真实值00.05(absmax)0.02读数0176448存下来的176读数×0.05 ÷ 448scale≈ 0.0196
示意图 · 尺子上的刻度是 E4M3 的真实刻度(只画正数,负数对称),舍入按就近舍入

量化后的数记录的是这个数相对于这组里最大的那个数有多大。可以把 scale 理解成尺子上一个单位代表的真实长度,量化后的数是用这把尺子量出来的读数,两者相乘(x′ = 读数 × scale),才回到真实的单位。所以一组量化后的数由两部分组成:

量化的步骤:跟着 4 个数走一遍

假设一个张量里有这样 4 个数:12、7.25、−8.95、1.71。跟着它们走一遍:

  1. 找出绝对值最大的数(absmax):这里是 12。用它定出 scale = 12 ÷ 448 ≈ 0.0268。
  2. 每个数除以 scale,换到 FP8 的尺子上:12 → 448,7.25 → 270.7,−8.95 → −334.1,1.71 → 63.8。
  3. 舍入到最近的刻度,得到量化后的数:FP8 只能表示那些离散的刻度值,不在刻度上的数必须舍入到离它最近的那一个。
    • 448 本身就是刻度,不变。
    • 270.7 和 −334.1 所在的区段,相邻刻度相差 32,分别被舍入到 256 和 −320,偏离了一段明显的距离。
    • 63.8 离刻度 64 很近,几乎不变。
  4. 用的时候乘回 scale,得到 x′:12、6.857、−8.571、1.714。和原值比,误差分别是 0%、5.4%、4.2%、0.25%。

下图把这四步画了出来,最后一步的虚线把原值投到输出尺上,前后差多少可以直接看出来。

初始:4 个数都在原值尺上,接下来依次经过每一步。1x = 122x = 7.253x = 1.714x = -8.95① 原值(线性数轴)-12-60612absmax = 12(绝对值最大)② ÷ scale  scale = absmax ÷ 448 = 12 ÷ 448 ≈ 0.02679③ FP8 刻度尺(线性数轴 −448…448,E4M3 真实刻度,正负对称)-448-2560256448④ 舍入到最近的刻度⑤ × scale  x′ = 读数 × scale(换回原来的单位)⑥ 输出(与 ① 同一尺度,空心圈 = 原值位置)-12-60612
示意图 · 4 个玩具数;FP8 尺上画的是真实的 E4M3 刻度(正负对称),舍入按就近舍入

误差来自舍入这一步:舍入时偏离刻度的距离乘回 scale,就是最终的偏差。刻度稀的区段(绝对值大的数)偏离得多,刻度密的区段偏离得少。

absmax ÷ 448 其实是把原值和 FP8 刻度映射起来的一种方法,也是最简单的一种:保证最大的数不越界,其余的数按比例落下去。还有一些更复杂的映射策略,目标都是让量化带来的误差尽量小,大致分两类:

对 FP8 来说,absmax 通常已经够用,因为它的相对精度处处差不多;复杂的映射策略在 INT4 这类刻度很少的格式上更重要。

为什么用 FP8,不用 INT8

INT8 量化的流程也是上面这几步,只是 scale = absmax ÷ 127,舍入到等距的整数刻度(−127…127)。两者都是 8 位、256 个编码,区别只在刻度怎么排。下图把两种刻度都换算回原始数值,画在同一条横轴上,最下面一行是一个真实权重矩阵里的数。

FP8INT8权重-0.4-0.200.20.4x = 0.403FP8:绝对误差 0.016,相对 4.0%INT8:绝对误差 0.0014,相对 0.35%FP8INT8权重-0.01-0.00500.0050.01x = 0.00533FP8:绝对误差 0.00021,相对 3.9%INT8:绝对误差 0.0018,相对 33.3%放大 ±0.0124:INT8 在这一段只有 7 根刻度,矩阵里 54.7% 的数落在这里
示意图 · 真实张量:Qwen/Qwen2.5-0.5B 的 model.layers.12.mlp.down_proj.weight(896 × 4864,共 4,358,144 个数,BF16),absmax = 0.451。per-tensor scale:FP8 = absmax ÷ 448,INT8 = absmax ÷ 127;点是随机抽取的 3000 个数,纵向位置只为错开。误差统计用全部元素:FP8 按 E4M3 刻度就近舍入,INT8 按四舍五入到整数并截到 ±127,相对误差 = |量化再还原的值 − 原值| ÷ |原值|,只统计非零元素

两种刻度的误差规律正好相反:

真实张量里的数大多挤在 0 附近,所以 FP8 更合适。按这个矩阵的全部元素统计,相对误差的中位数 INT8 是 7.2%,FP8 是 2.2%;INT8 有 9.1% 的数直接变成 0。

反过来,如果一组数跨度很窄、也没有离群值,INT8 的等距刻度(7 位幅值)反而比 FP8(3 位尾数)分得更细。所以 FP8 的优势在动态范围大;数据跨度窄的时候,它并不比 INT8 准。

4粒度:一个 scale 管多大一组

前面一直说「一组数配一个 scale」,这一组可以划得很粗,也可以划得很细。常见的有三种:

一个实际的例子是 DeepSeek-V4.1-Flash:按公开的 config 和参考代码,权重每 32×32 一块配一个 scale,激活每 32 个数配一个 scale。

粒度放细的意义在于隔离离群值,也就是一组数里个别特别大的数。scale 由一组里绝对值最大的数决定,组越大,越容易混进一个离群值,把其他数挤向 0 附近;切成小块以后,一个离群值只会拖累它所在的那一块。下图在三种粒度里各放了同一个离群值:

per-tensor整个张量一个 scale被拖累:全部 64 个数per-channel每行一个 scale被拖累:这一行 8 个数per-block每个 4×4 块一个 scale被拖累:这一块 16 个数
示意图 · 8×8 的小矩阵,粗框圈出共用一个 scale 的一组数;红格是离群值,浅红是被它决定 scale 的那一组

代价有两个:

5量化谁:W / A / KV,以及 WxAy 记法

WxAy 这种记法说的是进入矩阵乘的两个输入(权重 W、激活 A)各是几位,和输出无关。下面两种方案的输出都是 BF16,层与层之间传的也都是 BF16。

W8A8激活 XBF16在线量化→ FP8 + scaleFP8矩阵乘 FP8 × FP8FP32 累加× scaleFP32 → BF16输出 YBF16权重 WFP8 + scaleFP8W8A16激活 XBF16不量化 · BF16矩阵乘 BF16 × BF16FP32 累加scale 已在反量化时乘过输出 YBF16量化后的权重FP8 / INT4 + scale反量化读数 × scale → BF16BF16
示意图 · W8A8 与 W8A16 的数据流;数据类型依据 DeepSeek 公开参考代码 linear() 与通用 GEMM 约定

两者的累加器都是 FP32,精度上的差别来自激活:W8A16 的误差只来自权重,W8A8 还多了一份激活的量化误差。

另外,W8A8 的激活要在推理时现场量化,这带来两点麻烦:

三种方案各省什么

量化谁省什么哪边有效
只量权重(W8A16 / W4A16,weight-only)容量 + 带宽,不省算力decode(带宽型)
权重 + 激活(W8A8)再加上 算力prefill(算力型)也有效
KV cache容量 + attention 的带宽;attention 是否用 FP8 算看 kernel长上下文、高并发

6AWQ / GPTQ 是什么,和 absmax ÷ 448 的关系

前面「absmax ÷ 量程 + 就近舍入」这套做法叫 RTN(round-to-nearest)。8 位的刻度够多,RTN 通常够用;INT4 只有 16 个刻度,再用 RTN 误差就太大了。

AWQ 和 GPTQ 基本都是为 4 位权重(W4A16)设计的。它们没有引入新的数据格式,改的是每个权重最后取哪个值。产物形态和 RTN 一样,还是低比特权重加 scale;各家的打包格式、zero point 约定有差别,框架按 quant_method 选对应的 kernel,推理时做的都是 W4A16 的反量化加 GEMM。

方法最小化什么怎么做
RTN‖W − W′‖(权重本身的误差)absmax ÷ 量程,就近舍入
AWQ(arXiv 2306.00978)‖WX − W′X‖(输出的误差,需要校准数据拿到 X)和大激活相乘的权重 channel 更重要:量化前放大它们,降低相对误差
GPTQ(arXiv 2210.17323)逐列量化,每量化一列就调整剩余未量化的权重来补偿误差(用校准数据算二阶信息)

对 infra 来说,这些都发生在离线阶段,推理时只会在 config 里看到一个 quant_method 字段。AWQ / GPTQ 的作用是让 4 位的时候精度别掉太多。

7读懂量化模型的 config

拿公开的 DeepSeek-V4.1-Flash 来看,量化相关的字段都在 config.json 的 quantization_config 里:

"quantization_config": { "quant_method": "fp8", "activation_scheme": "dynamic", "weight_block_size": [32, 32], "scale_fmt": "ue8m0", "expert_dtype": "fp4" }
字段含义
quant_method量化方案名,框架据此选 kernel
activation_schemedynamic = 推理时现场算激活 scale;static = 校准好的固定 scale
weight_block_size权重分块粒度,[32, 32] = 每 32×32 一个 scale
scale_fmt = ue8m0scale 只有 8 位指数、无尾数 → 只能是 2 的整数次幂
expert_dtype = fp4routed expert(路由专家)权重用 FP4 E2M1

参考代码(公开 inference/model.py 的 linear())注释原文:

“Quantized weights need a quantized activation, and both fp4 and fp8 weights take an fp8 one -- for fp4 the kernel handles the mixed precision.”
译:量化权重需要配量化激活,fp4 与 fp8 权重都配 fp8 激活,fp4 的混合精度由 kernel 处理。

按 linear() 的分派规则:

具体哪些层保留 BF16,要去看 safetensors。

别的量化模型还会出现一些常见字段:fmt(如 e4m3)、bits / group_size(INT4 类),以及被排除在量化之外的模块(如 lm_head)。safetensors 里也会有对应的 scale 张量(如 weight_scale_inv)。

8如何衡量量化精度

量化省下来的资源,前提是输出没错。精度验证可以分三层,越往下成本越高、覆盖的范围也越大;上一层通过了,不代表下一层也没问题。

层级怎么测能发现什么算子级单张量与 BF16 参考对比:相对误差、余弦相似度kernel 写错、scale 用错分布级top-1 / top-5 token 一致率、KL 散度、NLL误差累积到输出分布任务级GSM8K、MMLU 等评测集用户实际感知到的效果
示意图 · 精度验证三层;指标定义为通用做法

分布级里的 NLL(negative log-likelihood,负对数似然)在这个场景下和交叉熵是同一个量,perplexity = exp(NLL)。

另外,每一组推理配置都要测精度,包括高并发。不要只测了串行精度正常,就直接去测高并发下的性能。

汇总:拿到量化模型看哪些字段

把前面的内容按「看哪个字段、省哪种资源、哪边有收益」整理成一张表:

拿到量化模型,看哪个字段它告诉你对应省的资源哪边有收益
权重位宽(quant_method / dtype)权重字节数容量 + 带宽decode(带宽型)
activation_scheme、是否量化激活(A8 / A16)是否走低精度 Tensor Core算力prefill(算力型)
KV cache dtypeKV 字节数容量(+ attention 带宽)长上下文、高并发
weight_block_size、scale_fmtscale 粒度与格式精度 / kernel 路径能否真的快、会不会掉点