解码阶段每生成一个 token,都要把权重从显存搬到计算单元;瓶颈常常是带宽,不是 FLOPs。 weight-only 只压权重,W8A8 连激活一起压,FP8 用指数位在 8 bit 内保留比 INT8 更大的动态范围。 压到几位不是关键,粒度、scale 和校准集才决定压完还能不能答对。
weight-only(W4A16)只量化权重,激活仍是 FP16。解码是 memory-bound,所以它能直接减少每 token 的权重读取量; 但 GEMM 仍是 FP16,KV cache 也不受益。AWQ 和 GPTQ 都属于这一类。
W8A8把激活也压到 INT8 或 FP8,才能用整数/FP8 tensor core。难点是激活离群值:SmoothQuant 用 per-channel 缩放把离群值从激活迁移到权重; FP8 用 E4M3 的 3 位尾数和 448 的最大值,再配 per-tensor、per-channel 或 block scale。
AWQ按激活幅度挑关键通道,做等价缩放;GPTQ用校准输入的 Hessian 做逐层误差补偿。 两者都不训练,但都需要校准数据,且默认只改权重。
KV cache 量化是另一笔账:FP16 到 FP8 直接把每 token 的 KV 字节减半,长上下文和高并发 batch 的收益比权重更直接; 代价是 scale 校准与 attention kernel 支持。
先分清两类瓶颈:权重读取和 KV cache 读取;量化动的是字节数,不是 FLOPs。
自回归解码每次只生成一个 token,batch=1 时矩阵乘法的右侧只有一行向量。AWQ 在 RTX 4090 上做的 roofline 分析指出, 这个阶段的算术强度约为 1 FLOP/byte,而 4090 的峰值算力与带宽之比是 165 FLOP/byte,因此生成阶段是内存带宽受限, 权重访问量比激活访问量高几个数量级(AWQ §4.1,Figure 3)。
以 70B 参数、FP16 权重为例:70 × 109 × 2 B = 140 GB(十进制)。若显存带宽为 1 TB/s, 单次读取全部权重的理论时间下界是 140 GB ÷ 1000 GB/s = 140 ms;换成 INT4 权重则是 35 GB,下界 35 ms。 这还没算 KV cache、计算和 kernel 开销,但它说明了为什么量化首先是一种显存与带宽手段。
KV cache 是第二本账。GPTQ 论文给过一个具体数字:OPT-175B 在 3-bit 权重下约 63 GB(含保持 FP16 的 embedding 与输出层), 若把 2048 个 token 的全部 K/V 历史存下来,还要再加约 9 GB(GPTQ §5)。长上下文或大 batch 时, KV cache 会从“附带开销”变成主要占用。
第三个问题是量化误差。均匀量化的步长 Δ 由张量(或通道/组)的最大绝对值决定; 激活里只要少数通道的离群值达到普通值的约 100 倍,Δ 就会被它们撑大,其余通道只剩很少的有效量化级 (SmoothQuant §3,Figure 2)。SmoothQuant 在 OPT-175B 上报告:在 WinoGrande、HellaSwag、PIQA、LAMBADA 四个零样本任务上, FP16 的平均准确率是 71.6%,而朴素的 INT8 per-tensor / per-token 激活量化分别只有 32.3% / 31.7% (SmoothQuant Table 1;§06 表 6 的 7 任务平均是另一组数字)。 SmoothQuant 也指出权重分布更平坦,激活才是量化难点(SmoothQuant §3)。
所以问题不是“压到 8 bit 还是 4 bit”,而是:压哪些张量、用多细的粒度、scale 怎么定、在哪里反量化。 这四件事决定了精度、显存和吞吐的三角关系。
三条路线的字节数、kernel 和精度风险不同,不能只看 bit 数。
AWQ / GPTQ
权重 4-bit(也有 3-bit),激活保持 FP16/BF16。checkpoint 里存 packed 整数、每组的 scale 和可选的 zero-point; kernel 在 GEMM 主循环里把权重反量化成 FP16/BF16,再与同精度激活相乘。
vLLM 的 AutoAWQConfig 暴露 weight_bits、group_size、zero_point
(auto_awq.py:188-202);AutoGPTQConfig 暴露 weight_bits、group_size、
desc_act、is_sym(auto_gptq.py:112-156)。两者都可走 Marlin 等 WNA16 kernel。
收益:权重显存约降为 1/4;解码的权重读取量同步下降。不改变:激活与 KV cache 的字节数;GEMM 仍按 FP16/BF16 计算(权重本身已有量化误差)。
SmoothQuant 等
权重和激活都量化到 INT8,才能用整数 GEMM。激活的难点是离群值:per-tensor 和 per-token 的 scale 都会被少数大通道支配,而非离群通道的有效量化级可能只剩 2–3 个(SmoothQuant §3)。
SmoothQuant 的观察是:离群值固定在少数通道上,且同一通道在不同 token 间幅度相近。它用一个 per-channel
缩放 s 把激活除以 s、权重乘以 s,数学上等价,却把量化难度从激活迁移到权重。
收益:权重和激活都减半;在支持 INT8 GEMM 的硬件上,整数 GEMM 吞吐更高。代价:静态激活 scale 需要校准集,动态 per-token 要在运行时统计;per-channel 激活量化本身与 INT8 GEMM 不兼容。
E4M3 / E5M2
FP8 用 8 位浮点表示,权重和激活都可以是 FP8。E4M3 有 4 位指数、3 位尾数,最大正规数 448; E5M2 有 5 位指数、2 位尾数,最大 57344,动态范围更大但精度更粗(FP8 Formats Table 1)。
vLLM 文档推荐的 FP8 W8A8 配方是权重 static per-channel、激活 dynamic per-token,
简单的 RTN 即可,不需要校准数据(docs/features/quantization/llm_compressor/fp8.md:79-84)。
其线性层目前只支持 float8_e4m3fn(fp8.py:226-228)。
收益:权重显存减半,FP8 tensor core 可用;vLLM 文档称在 H100/MI300x 上可让显存需求约减半,并带来最高 1.6× 吞吐提升(docs/features/quantization/llm_compressor/fp8.md:3-6)。代价:需要 Ada/Hopper 及以上。
三条路线的“8 bit”不是同一件事:W4A16 的 4 bit 只作用于权重;INT8 W8A8 的 8 bit 同时作用于权重和激活; FP8 的 8 bit 是浮点编码,scale 只负责把张量搬进可表示区间,尾数位数由格式决定。比较时先看量化对象,再看 bit 数。
per-tensor 最省,group-wise 最准;AWQ 调缩放,GPTQ 补误差,FP8 选指数位。
对称均匀量化的基本式是 q = round(w / Δ),反量化是 ŵ = q · Δ;
非对称量化再加 zero-point z。步长 Δ 由量化范围决定:
Δ。元数据最少,kernel 最简单;一个离群值会抬高所有通道的误差。Δ。权重上的 per-channel 很常见;激活上的 per-channel 精度好,但需要在内层维度上缩放,整数 GEMM 的 kernel 通常不支持(SmoothQuant §2)。G 的组,每组一个 Δ(以及可选 zero-point)。G=128 是 AWQ/GPTQ 的常用设置;组越小误差越低,但 scale/zero 的元数据和反量化开销越大。
粒度不是免费的。以 INT4、G=128、FP16 scale、4-bit zero 为例,每组 128 个权重额外带 16 + 4 = 20 bit,
平均每权重多 0.156 bit;若 zero-point 用 FP16 存,则多 0.25 bit。组再小,元数据占比继续上升。
这部分在 §04 的显存表里没有计入,后文会单独说明。
AWQ 的起点是一个实验观察:LLM 的权重不是同等重要,少量关键权重对输出影响很大; 而“关键”应由激活分布判断,不是权重范数。AWQ Table 1 在 OPT-6.7B、INT3-g128 上比较了保留 1% 通道为 FP16 的效果: 按激活选,PPL 从 RTN 的 23.54 降到 11.39;按权重选是 22.37,随机选是 24.23。
直接保留 FP16 通道会破坏硬件友好的规则矩阵,AWQ 改用等价缩放:对输入通道乘 s,
同时把激活除以 s:
缩放因子不是任意搜的。AWQ 把搜索空间压成一个参数 α:s = sXα,
其中 sX 是校准集上每个输入通道的平均激活幅度;然后最小化量化后的输出误差:
AWQ 的缩放因子只依赖激活的平均幅度,不做回归或反向传播,因此校准集可以很小;代价是它仍然需要一小批能代表部署分布的校准样本。
vLLM 的 AWQ checkpoint 把 qweight、qzeros、scales 分开存,
pack_factor = 32 / weight_bits,加载后先转成 GPTQ-like 标准布局,再交给 Marlin 等 kernel
(auto_awq.py:198,424-461,512-525)。
GPTQ 把问题写成逐层的输出重建:给定一层权重 W 和校准输入 X,找量化权重 Ŵ 使
‖WX − ŴX‖22 最小(GPTQ Eq.1)。它沿用 OBQ 的思路:一次量化一个权重,
然后更新所有尚未量化的权重,以补偿当前舍入造成的误差。
原始 OBQ 的复杂度是 O(drow · dcol3),无法扩展到十亿参数。
GPTQ 做了三处工程化:所有权重行使用同一固定顺序,把逆 Hessian 的更新从每权重一次降到每列一次;
以 B=128 列为一组做lazy batch-update;用 Cholesky 分解提高数值稳定性(GPTQ §4)。
最终它把 175B 模型的量化时间压到约 4 GPU 小时(GPTQ §1)。
和 AWQ 的区别:GPTQ 不改变激活,也不显式保护关键通道;它用校准输入的 Hessian 做误差补偿。 AWQ 则用激活幅度做等价缩放,让敏感通道在量化网格上更不容易被舍入。两者可以叠加,AWQ 论文 Table 9 报告了 AWQ 与 GPTQ 组合在更低 bit 下的结果。
FP8 不是整数网格,它由符号位、指数位、尾数位组成。E4M3 的指数偏置是 7,最大正规数 448,最小正规数 2−6; E5M2 的偏置是 15,最大 57344,最小正规数 2−14。E5M2 的最大正规数是 E4M3 的 128 倍(57,344 ÷ 448 = 128),即高 7 个 binade,总 binade 数也更多(论文称 E5M2 有 32 个,E4M3 调整后 18 个), 但尾数只有 2 位,表示同样数量级时的相对误差更大(FP8 Formats Table 1 与 §3.1)。
| 属性 | E4M3 | E5M2 |
|---|---|---|
| 符号 / 指数 / 尾数 | 1 / 4 / 3 | 1 / 5 / 2 |
| 指数偏置 | 7 | 15 |
| 最大正规数 | 448 | 57,344 |
| 最小正规数 | 2−6 | 2−14 |
| 无穷 / NaN | 无无穷,单一 NaN 编码 | 有 ±inf 与多个 NaN 编码 |
把一个 FP16/BF16 张量搬进 FP8,需要一个 scale:scale = amax / maxFP8(amax = max|w|),
然后 wFP8 = clamp(w / scale, −maxFP8, maxFP8)。
E4M3 的 maxFP8 是 448,E5M2 是 57344。scale 可以是 per-tensor、per-channel,
也可以是 per-block;粒度越细,越能适配局部动态范围,但元数据越多。
vLLM 的 Fp8Config 支持 static/dynamic 两种激活方案,以及可选的 weight_block_size
(fp8.py:92-135)。非 block 路径下,权重用 static per-tensor scale;动态激活在 CUTLASS 上走
per-token scale,否则走 per-tensor scale(fp8.py:266-274)。FP8 的线性层只支持
float8_e4m3fn,这是 torch._scaled_mm 的限制(fp8.py:226-228)。
E5M2 的动态范围更大,但 vLLM 的 FP8 线性层不走 E5M2;在 vLLM 里它主要出现在 KV cache 的 dtype 选项里
(docs/features/quantization/quantized_kvcache.md:39-41)。
70B 权重 FP16 是 140 GB;KV cache 每 token 只占几百 KiB,但会随上下文和 batch 线性增长。
权重显存的公式很直接:bytes = P × bw / 8,P 是参数量,bw 是每权重位数。
表 2 按 70B 参数、十进制 GB 计算,不计 scale、zero-point、embedding 和 lm_head。
| 权重格式 | bits / param | 70B 权重显存 | 说明 |
|---|---|---|---|
| FP16 / BF16 | 16 | 140 GB | 未量化基线 |
| FP8 E4M3 | 8 | 70 GB | W8A8 或 W8A16 |
| INT4 | 4 | 35 GB | weight-only,group scale |
| INT3 | 3 | 26.25 GB | AWQ 论文的极端设置;packing 与 kernel 支持更窄 |
KV cache 的公式是 bytes/token = 2 × L × Hkv × dhead × bkv:
2 表示 K 和 V,L 是层数,Hkv 是 KV head 数,dhead 是每头维度,
bkv 是每个元素的字节数。Llama-2-70B 的公开配置是 80 层、8 个 KV head、dhead=128
(config.json;GQA 让 KV head 数远小于 64 个 query head)。
| 上下文长度(batch=1) | FP16 KV | FP8 KV | 节省 |
|---|---|---|---|
| 4,096 token | 1.34 GB | 0.67 GB | 0.67 GB |
| 32,768 token | 10.74 GB | 5.37 GB | 5.37 GB |
| 131,072 token | 42.95 GB | 21.47 GB | 21.47 GB |
长上下文时,KV 量化的绝对节省更大:4k 上下文下,FP16 KV 只有 1.34 GB,相比 140 GB 权重是零头; 到 128k,FP16 KV 已经到 42.95 GB,FP8 把它压到 21.47 GB。若 batch=16,所有 KV 数字再乘 16。
权重固定按 70B 参数计算;KV 固定按 Llama-2-70B 的 80 层、8 个 KV head、head_dim=128 计算。 两个按钮组独立:weight-only 的 INT4 权重通常仍配 FP16 KV,FP8 W8A8 才可能同时用 FP8 KV。
组 A:70B 权重,FP16 / FP8 / INT4
70 × 109 × 2 B = 140 × 109 B = 140 GB。70 × 109 × 1 B = 70 GB。70 × 109 × 0.5 B = 35 GB;不计 group scale 与 zero-point。组 B:Llama-2-70B 的 KV cache,4096 token,batch=1
2 × 80 × 8 × 128 = 163,840 个元素(K 和 V 各一半)。163,840 × 2 B = 327,680 B = 320 KiB。327,680 × 4096 = 1,342,177,280 B ≈ 1.34 GB;JS 也显示 1.34 GB。163,840 × 1 B = 163,840 B = 160 KiB;4096 token 为 671,088,640 B ≈ 0.67 GB,JS 也显示 0.67 GB。140 + 1.34217728 ≈ 141.34 GB;
FP8 权重 + FP8 KV = 70 + 0.67108864 ≈ 70.67 GB;JS 分别显示 141.34 GB / 70.67 GB。
页面里的 JS 只做基本算术和 toFixed(2),没有随机数;上面显示的 1.34、0.67、141.34、70.67 GB 都能在模拟里逐位复现。
表 3 的 32k / 128k 行只是把 4096 换成对应上下文长度。
weight-only 在 GEMM 主循环反量化权重;W8A8 先量化激活再进 tensor core;KV 在写入和读取 cache 时转换。
三条路径的共同点是:反量化尽量不把高精度权重/激活写回 DRAM。
AWQ 的 TinyChat 把反量化与矩阵乘法融合,避免中间结果来回搬(AWQ §4.1);GPTQ 也实现了一个
“量化矩阵 × 全精度向量”的 kernel,在需要时动态反量化权重(GPTQ §5)。vLLM 则按 checkpoint 和硬件选择
Marlin、CUTLASS、DeepGEMM 等后端(docs/features/quantization/llm_compressor/fp8.md:19-25)。
vLLM 的两个入口参数值得对照:vllm/config/model.py:271-275 的 quantization 默认是
None,此时先读模型 config 里的 quantization_config,没有再按 dtype 加载;
vllm/config/cache.py:238-250 的 cache_dtype 默认是 "auto",也就是 KV cache
跟随模型 dtype。要量化 KV,必须在 kv_cache_dtype 里显式指定 fp8_e4m3 或 fp8_e5m2
(docs/features/quantization/quantized_kvcache.md:39-41)。
KV 路径的粒度限制更明显:kv_cache.py 的 BaseKVCacheMethod 在 checkpoint 加载路径上只接受 per-tensor 的
k/v scale(kv_cache.py:135-138);per-attention-head 的 scale 需要 Flash Attention 后端和
llm-compressor 的校准流程(docs/features/quantization/quantized_kvcache.md:15-19)。默认不校准时,vLLM 把 scale 设为 1.0
(docs/features/quantization/quantized_kvcache.md:25-30),并在 FP8 E4M3 的 scale 仍为 1.0 时给出警告
(kv_cache.py:157-162)。
三组实验的模型、数据集、基线和指标不同;只在自己的表内比较,不把不同论文的数字拼成一条曲线。
先看 weight-only。表 4 汇总了 AWQ 在 Llama-2-7B / 70B 上的 WikiText2 perplexity(PPL,越低越好), 同一张表里同时跑 FP16、RTN、GPTQ、GPTQ-R 和 AWQ;INT3/INT4 的 group size 都是 128。
| 模型 | 精度 | FP16 | RTN | GPTQ | GPTQ-R | AWQ |
|---|---|---|---|---|---|---|
| Llama-2-7B | INT3-g128 | 5.47 | 6.66 | 6.43 | 6.42 | 6.24 |
| Llama-2-7B | INT4-g128 | 5.47 | 5.73 | 5.69 | 5.63 | 5.60 |
| Llama-2-70B | INT3-g128 | 3.32 | 3.98 | 3.88 | 3.86 | 3.74 |
| Llama-2-70B | INT4-g128 | 3.32 | 3.46 | 3.42 | 3.43 | 3.41 |
表 4 里,Llama-2-7B INT3 的 AWQ 是 6.24,比 FP16 的 5.47 高 0.77;Llama-2-70B INT4 的 AWQ 是 3.41, 比 FP16 的 3.32 高 0.09。这里的 0.77 和 0.09 都是 PPL 绝对差。
再看 GPTQ。它在 OPT 系列上报告 WikiText2 PPL,表 5 取 13B 和 175B 两档,保留 FP16、RTN、GPTQ 的 4-bit 与 3-bit 结果。
| 模型 | 方法 | bits | WikiText2 PPL |
|---|---|---|---|
| OPT-13B | FP16 | 16 | 10.13 |
| OPT-13B | RTN | 4 | 11.32 |
| OPT-13B | GPTQ | 4 | 10.31 |
| OPT-13B | RTN | 3 | 3,400 |
| OPT-13B | GPTQ | 3 | 11.61 |
| OPT-175B | FP16 | 16 | 8.34 |
| OPT-175B | RTN | 4 | 10.54 |
| OPT-175B | GPTQ | 4 | 8.37 |
| OPT-175B | RTN | 3 | 7,300 |
| OPT-175B | GPTQ | 3 | 8.68 |
GPTQ 在 4-bit 上把 OPT-175B 从 8.34 拉到 8.37,只差 0.03;3-bit RTN 直接崩到 7,300, 而 GPTQ 3-bit 仍是 8.68。论文还指出一个反直觉趋势:更大的模型通常更容易量化, 但 OPT-66B 是例外,作者把原因归于该模型早期层存在大量 dead units(GPTQ §5 与脚注 2)。
最后看 W8A8。表 6 汇总了 SmoothQuant 在 OPT-175B 上的 7 个零样本任务平均准确率和 WikiText2 PPL; 朴素的 W8A8、ZeroQuant、LLM.int8() 与 SmoothQuant-O3 都在同一张表里。
| 方法 | 平均准确率(7 任务) | WikiText2 PPL |
|---|---|---|
| FP16 | 66.9% | 10.99 |
| W8A8(naive) | 35.5% | 93,080 |
| ZeroQuant | 35.8% | 84,648 |
| LLM.int8() | 66.7% | 11.10 |
| SmoothQuant-O3 | 66.8% | 11.17 |
表 6 说明 W8A8 的难点不在权重,而在激活:朴素 W8A8 的平均准确率从 66.9% 掉到 35.5%, SmoothQuant-O3 回到 66.8%,差 0.1 个百分点;PPL 从 10.99 变成 11.17,差 0.18。 LLM.int8() 也能保持精度,但它用 FP16 表示离群值,混合精度路径的开销另算。
吞吐方面,三篇论文/文档的口径不同,不能把数字直接横向比较:
docs/features/quantization/llm_compressor/fp8.md:3-6)。weight-only 的收益前提是解码阶段 memory-bound。如果 batch 很大、prefill 很长,GEMM 变成 compute-bound, weight-only 的收益会被摊薄;W8A8/FP8 则还能利用 tensor core 的计算吞吐。精度损失也不是一个固定值: 它取决于模型、数据集、bit 数、group size 和校准集,表 4–6 只代表各自的实验设置。
量化失败通常不是 bit 数不够,而是粒度、校准分布或 kernel 不支持。
激活离群值固定在少数通道,per-tensor/per-token 会被它们支配;per-channel 激活量化虽然准,
但与 INT8 GEMM 不兼容。SmoothQuant 的迁移强度 α 需要按模型调:论文在 OPT/BLOOM 上用 0.5,
GLM-130B 因为离群值更多而用 0.75(SmoothQuant §5.1)。
AWQ 只需要统计激活幅度,校准集可以小;GPTQ 需要校准输入来估 Hessian。两者都对校准分布敏感,
但 AWQ 明显更鲁棒:分布偏移时 AWQ 的 PPL 仅升 0.5–0.6,GPTQ 升 2.3–4.9(AWQ Figure 8)。
校准集与部署分布偏离时,量化误差会上升。vLLM 的 KV cache 在不校准的时候把 scale 设为 1.0,
FP8 E4M3 下可能出问题(docs/features/quantization/quantized_kvcache.md:25-30;kv_cache.py:157-162)。
FP8 计算需要 NVIDIA compute capability ≥ 8.9(Ada/Hopper/Blackwell);Turing/Ampere 只能走
weight-only W8A16 的 FP8 Marlin(docs/features/quantization/llm_compressor/fp8.md:15-17,24-25)。INT8 W8A8 在 Blackwell(≥10.0)上不支持,
文档建议改用 FP8(docs/features/quantization/llm_compressor/int8_w8a8.md:11-13)。
KV 的 per-head scale 只在 Flash Attention 后端可用,且需要 llm-compressor 校准;
BaseKVCacheMethod 的 checkpoint 加载路径只接受 per-tensor 的 k/v scale(docs/features/quantization/quantized_kvcache.md:15-19;
kv_cache.py:135-138)。group size、bit 数、packing 顺序也都受 kernel 限制。
KV 量化不减少权重显存,只减少 cache;它也不会让 GEMM 变快。它的收益在长上下文、大 batch 或高并发时最明显,
因为 KV 随 token 数和 batch 线性增长。FP8 KV 的 E4M3 只有 3 位尾数,K/V 的分布、scale 粒度和
attention 后端都会影响误差;per-tensor 是每个 K/V 张量各用一个 scale;per-head 粒度则走另一条路径(docs/features/quantization/quantized_kvcache.md:15-19)。
反过来,weight-only 不减少 KV cache;KV 占满时需要单独处理 KV。 两类量化解决的是不同的账。
需要严格可复现的数值、或模型对激活离群值特别敏感时,先做小规模评测再上线;不要假设某个 bit 数“通用”。 推断计算密集的 prefill、很大的 batch、或 kernel 不支持的 group size/bit 数下,量化的吞吐收益可能消失, 甚至因为反量化开销变慢。
表 4–6 都是特定模型、数据集和基线下的 PPL/准确率;换模型、换数据或换 kernel,数字可能不同,不能当成普遍保证。不同论文的基线和指标也不同,不能拼在一起比较。
一句话总结:weight-only 用 4-bit 权重换显存和带宽,W8A8/FP8 用 8-bit 权重加激活换 tensor core 吞吐, KV 量化用 8-bit cache 换长上下文容量。三者的收益来源不同,失败模式也不同;先确认瓶颈在哪一本账,再选量化对象。