14. 量化:让每一步搬更少的字节
第 6 篇得出结论:Decode 慢,是因为每一步都要把全部权重和 KV 从显存搬一遍。第三部分的调度技术让一次搬运服务更多请求;这一篇换一个方向:让要搬的数据本身变小。 把每个数从 16 位压到 8 位甚至 4 位,就是量化。读完你应该能看懂 W4A16、W8A8、FP8 KV 这些记号分别压缩了什么,知道 GPTQ、AWQ、SmoothQuant 各自解决什么问题,理解为什么“文件小了 4 倍”不等于“快了 4 倍”,并能设计一次可信的量化验收。
FP16、BF16、FP8 等格式的位结构和误差原理,在模型数值精度专题里有完整讲解;这里只讲它们用在推理里的取舍。
量化在做什么
量化是把连续的数映射到少数几个刻度上。最常见的做法是线性量化:先选一个缩放系数(scale),把原数除以它、四舍五入成整数存起来,用的时候再乘回去。
text
量化: q = round(x ÷ s)
反量化:x̂ = q × s例如 s = 0.1 时,0.26 被存成 3,读回来是 0.3,误差 0.04。刻度越粗,能表示的值越少,误差越大:INT8 有 256 个刻度,INT4 只有 16 个,INT4 的刻度间隔约是 INT8 的 17 倍。
缩放系数怎样选?一组数里最大的那个决定了刻度要覆盖多宽。对称量化常取 s = 最大绝对值 ÷ 127(INT8)。问题是,只要组里有一个特别大的数(离群值),整组的刻度就会被撑得很粗,其他正常的数都挤在少数几个刻度上,误差很大。
所以缩放系数的粒度很重要:
| 粒度 | 怎样分组 | 特点 |
|---|---|---|
| 每张量 | 整个矩阵一个 scale | 最省元数据,最容易被离群值拖累 |
| 每通道 | 每一列(或行)一个 scale | 权重量化的常用起点 |
| 每组 | 每 128 个数一个 scale | 4 位权重量化的主流做法 |
| 每 token | 激活的每一行一个 scale | 激活量化常用,运行时动态计算 |
scale 本身也要存储,这是量化的额外开销。4 位权重、每 128 个共用一个 2 字节的 scale,平均每个权重要占 0.5 + 2 ÷ 128 ≈ 0.516 字节,而不是正好 0.5 字节。
量化的三个对象
推理时有三类数据可以量化,它们的收益和风险不同:
| 对象 | 主要节省 | 主要受益的阶段 | 主要风险 |
|---|---|---|---|
| 权重 | 权重显存、每步搬运的字节 | Decode(访存受限) | 模型能力下降 |
| 激活 | 让矩阵乘法用低精度计算单元 | Prefill(计算受限) | 激活中的离群值难以量化 |
| KV Cache | KV 显存、每步读取的 KV | 长上下文、高并发 | 历史信息的误差累积 |
常见记号 WxAy 表示权重 x 位、激活 y 位:W4A16 是权重 4 位、激活保持 16 位;W8A8 是权重和激活都 8 位。这个记号不说明 KV Cache 用什么精度,KV 量化要另外看。
路线一:只量化权重(W4A16)
只压缩权重、计算仍用 16 位,是最常见的路线。它直接对准 Decode 的瓶颈。
代入我们的例子。实际的 4 位模型通常把输入的词嵌入层和输出的词表投影层保留为 BF16,它们和词表大小有关,在 Llama-3.1-8B 里合计约 10.5 亿参数。其余约 70 亿参数压到 4 位(每 128 个一组),约 3.6 GB。每一步 Decode 要读取的是这 3.6 GB,加上输出投影层的约 1.05 GB(词嵌入层每步只查几行,可以忽略)。按第 6 篇的方法估算单请求 Decode 的下界:
text
BF16:16.1 GB ÷ 3.35 TB/s ≈ 4.8 ms / token
W4A16:约 4.65 GB ÷ 3.35 TB/s ≈ 1.4 ms / token理论上权重搬运的时间缩短到约 30%。实际收益会再小一些:KV 的读取没有变少;权重读进来以后要先反量化成 16 位才能参与计算,这需要专门的内核把解包、反量化和矩阵乘法融合在一起高效完成(比如 Marlin 内核)。
权重量化的另一面是 Prefill。Prefill 是计算受限的,瓶颈不在搬运,而计算仍是 16 位,反量化还多了一道工序。所以 W4A16 对 Prefill 帮助不大,在批很大时甚至可能比不量化更慢。
把 4 位权重做好,难点在误差。两种主流方法:
- GPTQ:逐层量化,每量化一列权重,就根据少量校准数据计算出的二阶信息,调整还没量化的列,补偿刚才引入的误差。目标是让这一层的输出尽量接近原来,而不是让每个权重单独最接近原值。
- AWQ:观察到少数权重通道对应的激活特别大,这些通道的误差对输出影响最大。它用校准数据找出这些重要通道,先把它们放大再量化(激活相应缩小,数学上等价),让重要权重获得更细的刻度。
两者都只需要几百条校准样本,不需要重新训练,属于训练后量化(PTQ)。
路线二:权重和激活都量化(W8A8)
要让 Prefill 也变快,计算本身就得用低精度。H100 这一代 GPU 的 FP8 和 INT8 矩阵运算峰值大约是 BF16 的两倍。权重和激活都换成 8 位,矩阵乘法就能用上这部分算力。
难点在激活。大模型的激活里有少数通道的数值比其他通道大几十倍,按每 token 量化时,一个离群通道就会把整行的刻度撑粗。
SmoothQuant 的办法是“把难度从激活转移给权重”:对每个通道 j 选一个系数 s_j,激活除以 s_j,对应的权重乘以 s_j,矩阵乘法的结果不变:
text
Y = X · W = (X · diag(s)⁻¹) · (diag(s) · W)
s_j = max|X_j|^α ÷ max|W_j|^(1−α)激活里的离群通道被压小,权重相应变大一些,两边都变得容易量化。α 控制转移的力度,论文对多数模型取 0.5。这个缩放可以提前合并进前一层的归一化参数里,运行时没有额外开销。
在支持 FP8 的 GPU 上,还有一条更省事的路线:FP8 W8A8。FP8 是浮点格式,自带指数,对离群值的容忍度比 INT8 好。vLLM 可以在加载时把 BF16 模型在线转换成 FP8(--quantization fp8),不需要预先准备量化模型,权重用每张量 scale,激活在每次前向时动态计算 scale。这通常是在 H100 上尝试量化的第一步。
路线三:量化 KV Cache
第 6 篇说过,批处理摊不薄每人一份的 KV,长上下文的 Decode 越来越被 KV 的读取主导。量化 KV 同时带来两个好处:同样的显存装下更多 token,每一步读取的字节更少。
把 KV 从 BF16 换成 FP8,每个 token 从 128 KiB 降到 64 KiB,第 4 篇算出的 50 GiB KV 空间就能装下约 80 万个 token,并发能力翻倍。vLLM 里用 --kv-cache-dtype fp8 开启。
代价是误差会进入所有后续的注意力计算,而且 scale 需要合理设置。KV 量化也不一定让每个指标都变好。vLLM 0.32.0 的发布说明里有一个例子:某模型在新一代 GPU 上开启 FP8 KV 后,KV 显存减半、吞吐提高 2.3%—5.5%,但 TTFT 增加了 7%—11%。吞吐更高和首字更快,可能方向相反。
更激进的做法压到 2 位,比如 KIVI 方法:K 按通道分组量化,V 按 token 分组量化,最近的若干 token 保留高精度。这类方法省得更多,对质量的验证要求也更高。
新一代低位格式:FP8、FP4 与微缩放
除了整数,现在的 GPU 越来越多地原生支持低位浮点格式:
| 格式 | 位结构 | 特点 |
|---|---|---|
| FP8 E4M3 | 1 符号 + 4 指数 + 3 尾数 | 最大约 448,精度较好,推理最常用 |
| FP8 E5M2 | 1 符号 + 5 指数 + 2 尾数 | 范围大(最大 57344),精度粗 |
| FP4 E2M1 | 1 符号 + 2 指数 + 1 尾数 | 只能表示 0、0.5、1、1.5、2、3、4、6 及其负数 |
FP4 只有十几个可表示的值,单靠它几乎无法表示模型权重,必须配合非常细的分组缩放,这就是微缩放(microscaling)格式:
- MXFP4(OCP 开放规范):每 32 个 FP4 共享一个 8 位的 scale,scale 只能是 2 的幂(E8M0 格式)。平均每个数 4 + 8/32 = 4.25 位。
- NVFP4(NVIDIA):每 16 个 FP4 共享一个 FP8 E4M3 的 scale,scale 可以是任意值,再加一个整张量的 FP32 scale。平均每个数 4 + 8/16 = 4.5 位,分组更细、scale 更精确,误差通常更小。
Blackwell 这一代 GPU(如 B200)的 Tensor Core 原生支持 FP4 运算,这些格式在那里才能发挥计算上的优势。在不支持的硬件上,它们只能当作压缩存储格式使用。
方法、格式、文件和内核是四回事
量化相关的名词很多,混在一起会造成误判:
- 方法:怎样选 scale、怎样补偿误差,比如 GPTQ、AWQ、SmoothQuant。
- 数值格式:存成什么样的数,比如 INT4、FP8 E4M3、NVFP4。
- 文件格式:模型怎样保存,比如 safetensors、llama.cpp 用的 GGUF。
- 内核:GPU 上用哪段代码把这种格式算起来,比如 Marlin。
同样是“AWQ 量化的 4 位模型”,用的内核不同,速度可以差好几倍。vLLM 加载 AWQ 模型时会自动选择更快的 Marlin 内核;如果在命令行里显式写死 --quantization awq,反而会锁定在较慢的实现上,日志里能看到相应的提示。部署时要从启动日志确认实际使用的量化方法和内核。
怎样选
按瓶颈和硬件选择:
| 情况 | 优先考虑 |
|---|---|
| 模型放不进显存 | 4 位权重量化(AWQ、GPTQ) |
| 小并发、Decode 为主、要降 TPOT | 4 位或 8 位权重量化 |
| H100 等支持 FP8 的 GPU,高吞吐 | FP8 W8A8 |
| 长上下文、高并发、KV 是瓶颈 | FP8 KV Cache |
| Blackwell GPU | 评估 NVFP4 / MXFP4 |
| 对质量极其敏感 | 先用 FP8,或者换一个更小的原生模型做对照 |
如果一个更小的原生模型已经能满足业务质量,直接用它往往比把大模型压到 4 位更简单、更可控。但这要在同一个质量测试集上比较,不能只凭参数量判断。
怎样验收
量化一定会引入误差,问题只是业务能否接受。一次可信的量化验收:
- 先建基线。 同一个模型的 BF16 版本,固定对话模板、采样参数、最大长度和硬件,记录质量与性能。
- 一次只改一项。 权重量化、激活量化、KV 量化分别评估,不要同时打开再看结果。
- 用业务数据评质量。 通用基准分数相近,不代表你的业务没问题。量化最容易影响的是数学计算、代码、长上下文中的细节回忆和格式严格的输出,这些都要覆盖。
- 分别记录性能。 权重显存、KV 容量(看引擎启动日志里的 token 数,不要看
nvidia-smi的总占用)、TTFT、TPOT、吞吐,在低并发和高并发下各测一次。 - 检查输出长度。 量化后的模型可能变得更啰嗦或更简短,输出长度变了,速度对比就失去意义。
“模型能正常回答一句话”和“安装成功”,都不能作为上线的依据。
量化用更少的位数表示数值,用 scale 控制刻度范围,粒度越细误差越小、元数据越多。权重量化(W4A16)减少搬运,主要加速 Decode;W8A8 和 FP8 让计算也用低精度,能加速 Prefill;KV 量化扩大并发容量、减少长上下文的读取。GPTQ、AWQ 处理权重误差,SmoothQuant 处理激活离群值,MXFP4、NVFP4 用细粒度缩放让 4 位浮点可用。
量化让每一步变便宜。下一篇换一个思路:每一步的成本不变,但让一步推进多个 token:投机解码。
