权重量化
主线位置:一个请求的一生里的前向。量化方式在启动时、模型加载阶段定下来,之后每一步前向里的每个线性层都按它选好的 kernel 算。两个引擎这一段都是 Python 编排加 CUDA / Triton kernel,没有 Rust。
版本:vLLM v0.32.0、SGLang v0.5.22
结论先说:
- 量化方式通常不用手写。两边都先读 checkpoint
config.json里的quantization_config.quant_method,再逐个问已注册的量化配置类「要不要改写成你」,把awq改写成更快的实现;--quantization默认是None,写了就要和 checkpoint 对得上。 - 定下量化配置类之后,先查一次最低算力(compute capability),不够直接报错;然后每个线性层在构造时调
get_quant_method拿到自己的 linear method,再从一张按优先级排好的 kernel 列表里取第一个「这块卡能跑、这个形状能算」的。 - 同一个 checkpoint 在不同的卡上会落到不同的 kernel。FP8 在 Ampere(8.x,8.9 除外)上没有 FP8 张量核,两边都退到 Marlin 做「FP8 权重、16 位激活」;在 Ada / Hopper 上才是 FP8 × FP8 的 CUTLASS。AWQ / GPTQ 4 bit 在两边都主要走 Marlin,vLLM 在 Hopper(9.0)上优先 Machete。
- 两边的门槛不一样:vLLM 的 FP8、AWQ 和 Marlin 要 7.5 起,SGLang 的 FP8 和 Marlin 要 8.0 起;SGLang 在 CUDA 上不再注册普通
gptq,GPTQ checkpoint 必须能转成gptq_marlin。 - 权重量化和 KV 量化是两件事,开关、存法、影响的显存都不同。FP8 checkpoint 可以同时带 KV scale,见 KV 量化。
- 8B 模型的权重:bf16 约 16.06 GB,FP8 约 9.08 GB,4 bit(g128)约 5.7 GB(估算)。吞吐、精度、开关前后的输出一致性都待实测(C3-5)。
flowchart TB
ckpt["checkpoint config.json<br/>quantization_config.quant_method"] --> detect["逐个调用 override_quantization_method<br/>例如 awq 改写成 Marlin 实现"]
cli["--quantization(默认 None)"] --> detect
detect --> match{"和 --quantization 一致吗"}
match -->|"不一致"| err1["报错"]
match -->|"一致或没写"| cfg["量化配置类<br/>Fp8Config / AutoAWQConfig ..."]
cfg --> cap{"算力 ≥ get_min_capability"}
cap -->|"否"| err2["报错:not supported for the current GPU"]
cap -->|"是"| layer["每个线性层构造时<br/>get_quant_method(layer, prefix)"]
layer --> pick["按优先级列表挑 kernel<br/>is_supported 与 can_implement 都过的第一个"]
pick --> fwd["前向:layer.quant_method.apply"]
能选什么
两边注册的量化方法都有二十个上下,常见的几类:
| 类 | vLLM v0.32.0 | SGLang v0.5.22 | 权重 / 激活 |
|---|---|---|---|
| FP8 | fp8(checkpoint 已量化);在线:fp8_per_tensor、fp8_per_block、fp8_per_channel | fp8(checkpoint 已量化或在线)、w8a8_fp8 | 8 bit 浮点权重;激活视 kernel 而定,见下文 |
| AWQ | awq、awq_marlin、auto_awq 都映射到同一个 AutoAWQConfig | awq、awq_marlin 是两个类 | 4 bit 整数权重,按组带 scale 和 zero point,16 位激活 |
| GPTQ | gptq、gptq_marlin、auto_gptq 都映射到 AutoGPTQConfig | gptq_marlin(CUDA 上没有普通 gptq) | 4 / 8 bit 整数权重,对称,16 位激活 |
| 通用格式 | compressed-tensors、modelopt、quark、torchao 等 | compressed-tensors、modelopt、quark、gguf、bitsandbytes 等 | 由 checkpoint 内的方案决定 |
| 4 bit 浮点 | modelopt_fp4(NVFP4)、mxfp4 | modelopt_fp4、mxfp4 | 多数要 Blackwell |
出处:vLLM 的 QuantizationMethods 与 get_quantization_config("awq": AutoAWQConfig 在 #L157);SGLang 的 BASE_QUANTIZATION_METHODS。
--quantization 两边默认都是 None(vLLM model.py#L260,SGLang model.py#L171)。用量化好的 checkpoint 时一般不写;写它主要有两个用途:在 BF16 checkpoint 上做在线量化(加载时把权重转成 FP8),或者强制选某个实现。
vLLM:从 checkpoint 到 kernel
认出量化方法
ModelConfig._verify_quantization 读 checkpoint 的 quant_method,然后按固定顺序逐个调用量化配置类的 override_quantization_method,第一个返回非空的胜出:
overrides = [
"auto_gptq",
"gptq",
"gptq_marlin",
"auto_awq",
"awq",
"awq_marlin",
# ...
]
# ...
for name in quantization_methods:
method = me_quant.get_quantization_config(name)
quantization_override = method.override_quantization_method(
quant_cfg, self.quantization, hf_config=self.hf_config
)
if quantization_override is not None:
# ...
quant_method = quantization_override
self.quantization = quantization_override
break
AWQ checkpoint 的 quant_method 是 awq,用户没写或写了 awq / awq_marlin / auto_awq / marlin,都会被改写成 auto_awq(auto_awq.py#L263)。GPTQ 同理改写成 auto_gptq。之后如果用户写的 --quantization 和最后定下来的方法不一样,直接报错。
在 BF16 checkpoint 上写 --quantization fp8 仍然可用,但会打一条 deprecated 警告,改走 fp8_per_tensor 的在线量化(weight_utils.py#L353)。Fp8Config 自己已经不接受「checkpoint 不是 FP8」的配置(fp8.py#L108)。在线量化的几个简写定义在 _ONLINE_SHORTHANDS。
查一次最低算力
建好量化配置之后,VllmConfig 拿当前卡的算力和配置类声明的最低值比:
if capability < quant_config.get_min_capability():
raise ValueError(
f"The quantization method {model_config.quantization} "
"is not supported for the current GPU. Minimum "
f"capability: {quant_config.get_min_capability()}. "
f"Current capability: {capability}."
)
几个常见类的下限:Fp8Config 75(fp8.py#L146),AutoAWQConfig 75(auto_awq.py#L233),AutoGPTQConfig 60(auto_gptq.py#L182)。这一关只挡最旧的卡,真正决定用哪个 kernel 的在后面。
每层拿 linear method,再挑 kernel
LinearBase 构造时调 resolve_quant_method,它再调配置类的 get_quant_method(layer, prefix)(linear.py#L290)。返回的 linear method 在 create_weights 里挑 kernel。kernel 分几张表,表里按「性能优先」排好,挑第一个 is_supported(看平台和算力)与 can_implement(看形状、组大小、数据类型)都通过的:
vllm/model_executor/kernels/linear/__init__.py
# in priority/performance order (when available)
_POSSIBLE_FP8_KERNELS: dict[PlatformEnum, list[type[FP8ScaledMMLinearKernel]]] = {
PlatformEnum.CUDA: [
FlashInferFP8ScaledMMLinearKernel,
CutlassFP8ScaledMMLinearKernel,
B12xTensorFP8ScaledMMLinearKernel,
PerTensorTorchFP8ScaledMMLinearKernel,
ChannelWiseTorchFP8ScaledMMLinearKernel,
MarlinFP8ScaledMMLinearKernel,
HummingFP8ScaledMMLinearKernel,
],
# ...
}
vllm/model_executor/kernels/linear/__init__.py
_POSSIBLE_KERNELS: dict[PlatformEnum, list[type[MPLinearKernel]]] = {
PlatformEnum.CUDA: [
CutlassW4A8LinearKernel,
MacheteLinearKernel,
MarlinLinearKernel,
ConchLinearKernel,
ExllamaLinearKernel,
TritonW4A16LinearKernel,
HummingLinearKernel,
],
# ...
}
前一张给 FP8(init_fp8_linear_kernel → choose_scaled_mm_linear_kernel),带块 scale 的 FP8 另有一张 _POSSIBLE_FP8_BLOCK_KERNELS;后一张给「低 bit 整数权重 + 16 位激活」的混合精度(AWQ / GPTQ 走 choose_mp_linear_kernel)。
两个开关可以改变挑选结果:环境变量 VLLM_DISABLED_KERNELS 按类名排除 kernel(__init__.py#L633),--linear-backend 只保留指定的后端,默认 auto(kernel.py#L350)。实际选中谁,日志里有一行 Selected ... for ... 或 Using ... for ...。
架构检查在 kernel 里
决定一块卡能不能用某个 kernel 的检查,散在各 kernel 的 is_supported / can_implement 和几个工具函数里:
| kernel | 条件 | 出处 |
|---|---|---|
| FlashInfer FP8 | 算力 ≥ 100,装了 FlashInfer,权重和激活都是 per-tensor scale | flashinfer.py#L51 |
| CUTLASS FP8 | 算力 ≥ 90 且 CUDA ≥ 12.0,或算力 8.9 且 CUDA ≥ 12.4 | scaled_mm_entry.cu#L145 |
torch _scaled_mm FP8 | 平台支持 FP8,CUDA 上即算力 ≥ 89 | cuda.py#L586 |
| Marlin FP8(只量化权重) | 算力 ≥ 75 | marlin_utils_fp8.py#L31 |
| CUTLASS W4A8 | 算力恰好 90,激活是 FP8 e4m3,无 zero point,组大小 128 | cutlass.py#L37 |
| Machete | 算力恰好 90,组大小 −1 / 64 / 128(16 位激活时) | machete.py#L34 |
| Marlin(整数权重) | 算力 ≥ 75 | marlin.py#L33、marlin_utils.py#L66 |
FP8 的走法
FP8 checkpoint 的线性层统一由 Fp8LinearMethod 调 init_fp8_linear_kernel 挑 kernel(fp8.py#L347)。按上面两张表推,per-tensor scale 的 FP8 checkpoint:
- 7.5 / 8.0 / 8.6(Turing、Ampere):前五个都不满足,落到 Marlin FP8。权重以 FP8 存、计算前在 kernel 里展开,激活保持 16 位,相当于 W8A16:省显存,矩阵乘本身不用 FP8 张量核。
- 8.9 / 9.0(Ada、Hopper):FlashInfer 要 100 以上,CUTLASS FP8 满足,W8A8。
- 10.0 及以上(Blackwell):装了 FlashInfer 时它排第一,否则 CUTLASS。
AWQ / GPTQ 的走法
AWQ 在 CUDA 上先看 Marlin 能不能用(不开 VLLM_BATCH_INVARIANT、量化类型和组大小 Marlin 支持),能用就走 AutoAWQMarlinLinearMethod,否则退到 Triton 写的 AutoAWQLinearMethod:
vllm/model_executor/layers/quantization/auto_awq.py
use_marlin = (
not envs.VLLM_BATCH_INVARIANT
and current_platform.is_cuda()
and check_marlin_supported(
self.quant_type, self.group_size, self.zero_point
)
)
名字里的 Marlin 只是入口,真正的 kernel 由 choose_mp_linear_kernel 从混合精度表里挑(auto_awq.py#L448)。16 位激活时 CUTLASS W4A8 不满足,Hopper 上落到 Machete,其他 7.5 以上的卡落到 Marlin。GPTQ 一样(auto_gptq.py#L349),另外 AutoGPTQConfig 只收对称量化(auto_gptq.py#L99),sym=false 的 checkpoint 会报 Unsupported quantization config。
SGLang:同样的骨架,门槛更高
认出量化方法
ModelConfig._verify_quantization 的骨架和 vLLM 一样:读 checkpoint,按注册表顺序调 override_quantization_method(model_config.py#L1985)。区别:
awq能转就改写成awq_marlin;用户显式写了--quantization awq则保留awq并提示「用 awq_marlin 更快」(awq.py#L333)。能不能转,最后要过check_marlin_supported,而 SGLang 的 Marlin 要算力 ≥ 80(marlin_utils.py#L78)。gptq同理改写成gptq_marlin(gptq.py#L414)。但 CUDA 上没有注册普通gptq(注释说那个 kernel 已经删了,__init__.py#L118),所以转不成 Marlin 的 GPTQ checkpoint(比如在 8.0 以下的卡上)会在后面报Unknown quantization method。- CLI 和 checkpoint 不一致时,有一张「兼容表」放行几种组合(比如
modelopt_fp4对modelopt),以及若干「在线再量化」方法;其余报错。 - 不在「已优化」列表里的方法会打警告
quantization is not fully optimized yet(model_config.py#L1908),普通awq就在此列。
在 BF16 checkpoint 上写 --quantization fp8:Fp8Config 不需要配置文件,加载器直接构造一个默认实例(weight_utils.py#L375),默认 is_checkpoint_fp8_serialized=False(fp8.py#L260),即在线量化,不打 deprecated 警告。
最低算力
同样在加载时比一次(loader.py#L255),下限比 vLLM 高:
python/sglang/srt/layers/quantization/fp8.py
def get_min_capability(self) -> int:
# ...
return 100 if self.use_mxfp8 else 80
AWQMarlinConfig、GPTQMarlinConfig 都是 80(awq.py#L307),普通 AWQConfig 是 75(awq.py#L109),而且只接受 fp16 激活(awq.py#L106)。
FP8 的走法
SGLang 的 Fp8LinearMethod 没有 kernel 列表,在构造时用两个布尔值决定:
python/sglang/srt/layers/quantization/fp8.py
self.cutlass_fp8_supported = cutlass_fp8_supported()
# For GPUs that lack FP8 hardware support, we can leverage the Marlin
# kernel for fast weight-only FP8 quantization
self.use_marlin = False
if _is_cuda:
force_marlin = get_bool_env_var("SGLANG_FORCE_FP8_MARLIN")
auto_enable = can_auto_enable_marlin_fp8()
self.use_marlin = force_marlin or auto_enable
can_auto_enable_marlin_fp8 就是 80 <= sm < 89(fp8_utils.py#L2544);cutlass_fp8_supported 的条件和 vLLM 那条一样(≥ 9.0 且 CUDA ≥ 12.0,或 8.9 且 CUDA ≥ 12.4,fp8_utils.py#L307)。apply 时 use_marlin 走 apply_fp8_marlin_linear,否则走 apply_fp8_linear。SGLANG_FORCE_FP8_MARLIN=1 可以在新卡上也强制用 Marlin。
按卡型对照
把上面的条件合起来,常见 checkpoint 在单卡 CUDA 上落到哪里。这张表是按源码条件推出来的,没有在这些卡上跑过;实际选中谁以启动日志为准。
| 卡型(算力) | FP8 per-tensor:vLLM | FP8:SGLang | AWQ / GPTQ 4 bit g128:vLLM | AWQ:SGLang | GPTQ:SGLang |
|---|---|---|---|---|---|
| Turing(7.5) | Marlin FP8,W8A16 | 报错(下限 80) | Marlin | 普通 awq,仅 fp16 | 报错(转不成 Marlin,又没有普通 gptq) |
| Ampere(8.0 / 8.6) | Marlin FP8,W8A16 | Marlin FP8,W8A16 | Marlin | awq_marlin | gptq_marlin |
| Ada(8.9) | CUTLASS FP8,W8A8 | apply_fp8_linear,W8A8 | Marlin | awq_marlin | gptq_marlin |
| Hopper(9.0) | CUTLASS FP8,W8A8 | apply_fp8_linear,W8A8 | Machete | awq_marlin | gptq_marlin |
| Blackwell(10.0) | FlashInfer(已安装时)或 CUTLASS | apply_fp8_linear,W8A8 | Marlin | awq_marlin | gptq_marlin |
一张 24 GB 的卡常见的是 Ampere(8.6)和 Ada(8.9)。在 8.6 上 FP8 checkpoint 只省显存:权重按 1 字节存,矩阵乘仍是 16 位。是否比 bf16 快、快多少,要看解码时少读的那一半权重能不能盖过展开的开销,待实测(C3-5)。
flowchart LR
fp8["FP8 checkpoint"] --> q1{"算力"}
q1 -->|"7.5"| a1["vLLM:Marlin W8A16<br/>SGLang:报错"]
q1 -->|"8.0 到 8.8"| a2["两边:Marlin W8A16"]
q1 -->|"8.9 到 9.x"| a3["两边:CUTLASS 等 W8A8"]
q1 -->|"10.0 起"| a4["vLLM:FlashInfer 优先<br/>SGLang:apply_fp8_linear"]
int4["AWQ / GPTQ 4 bit"] --> q2{"算力"}
q2 -->|"7.5"| b1["vLLM:Marlin<br/>SGLang:AWQ 退普通 kernel,GPTQ 报错"]
q2 -->|"8.0 起,9.0 除外"| b2["两边:Marlin"]
q2 -->|"9.0"| b3["vLLM:Machete<br/>SGLang:Marlin"]
和 KV 量化的区别
| 权重量化 | KV 量化 | |
|---|---|---|
| 开关 | checkpoint 的 quant_method,或 --quantization | --kv-cache-dtype(默认 auto) |
| 量化的是什么 | 线性层的权重(有的方案连激活一起) | 注意力层写进缓存的 K、V |
| 省的显存 | 固定的一块:权重 | 随并发和上下文长度增长的一块 |
| scale 在哪 | 线性层的参数,随权重一起加载 | 每层一对 K、V scale 挂在注意力层上,或嵌在页里 |
| 选 kernel 的地方 | 线性层的 linear method | 注意力后端 |
两者独立:FP8 权重可以配 bf16 KV,bf16 权重也可以配 FP8 KV。交叉点有一个:vLLM 的 Fp8Config.get_quant_method 遇到注意力层时返回 Fp8KVCacheMethod(fp8.py#L207),负责从 checkpoint 读 KV scale;--kv-cache-dtype auto 时如果 checkpoint 的量化配置声明了 FP8 KV,KV 也会跟着用 FP8。细节见 KV 量化。
显存账:一张 24 GiB 的卡上的 8B 模型
按 Llama-3.1-8B 的公开形状算(估算,不是实测)。线性层约 69.8 亿参数,embedding 和 lm_head 合计约 10.5 亿、保持 bf16(这几种格式默认都不量化它们);每个线性权重的字节数里算进了组 scale 和 zero point。KV 每 token 128 KiB、引擎可用 24 GiB × 0.92 = 22.08 GiB 的算法同 为什么服务大模型难。
linear params=6,979,321,856 other params=1,050,939,392 total=8,030,261,248
KV per token = 128 KiB
bf16 weights= 16.06 GB = 14.96 GiB left= 7.12 GiB kv_tokens<= 58,347
fp8 per-tensor (W8A8 or W8A16 Marlin) weights= 9.08 GB = 8.46 GiB left=13.62 GiB kv_tokens<= 111,595
fp8 block 128x128 weights= 9.08 GB = 8.46 GiB left=13.62 GiB kv_tokens<= 111,582
int4 g128 sym (GPTQ) weights= 5.70 GB = 5.31 GiB left=16.77 GiB kv_tokens<= 137,387
int4 g128 asym (AWQ, 4-bit zero) weights= 5.73 GB = 5.33 GiB left=16.75 GiB kv_tokens<= 137,179
脚本 estimate.py 只用 Python 标准库,约 40 行。怎么读:
- FP8 把权重从 14.96 GiB 降到 8.46 GiB,KV 的上界从约 5.8 万 token 翻到约 11.2 万;4 bit 再降 3 GiB 左右。
- 4 bit 没有降到 bf16 的四分之一,因为 embedding 和 lm_head 的 1.96 GiB 没量化,组 scale 也要占地方。
left还要扣掉激活峰值、CUDA graph 等开销,实际 KV 块数待实测(C3-1);KV 再用 FP8 存,每 token 降到 64 KiB,上界再翻一倍。
待实测
- 同一个 8B 模型的 bf16、FP8、AWQ、GPTQ checkpoint 在两个引擎上的吞吐、TPOT、显存占用:待实测(C3-5)。
- 24 GB 卡(8.6 或 8.9)上 FP8 走 Marlin 与走 CUTLASS 的速度差:待实测(C3-5,取决于租到的卡型)。
- 量化前后在固定提示集上的输出一致性与精度差:待实测(C3-5)。
- 启动日志里实际选中的 kernel 名,回填上面的对照表:待实测(C3-5)。
小结
- 两边都以 checkpoint 的
quant_method为准,用override_quantization_method把它改写成更快的实现;--quantization默认None,主要用来做在线 FP8。 - 选 kernel 分三层:配置类的最低算力(vLLM 的 FP8、AWQ 是 75,SGLang 的 FP8、Marlin 是 80)→ 每层的 linear method → kernel 自己的
is_supported/can_implement。vLLM 用按优先级排好的 kernel 列表,SGLang 的 FP8 用两个布尔值。 - FP8 在 8.9 以下没有 FP8 张量核,两边都退到 Marlin 做 W8A16;8.9 起才是 W8A8。AWQ / GPTQ 主要走 Marlin,vLLM 在 9.0 上用 Machete。
- SGLang 在 CUDA 上没有普通
gptq,GPTQ 必须能转成gptq_marlin;vLLM 的 GPTQ 只收对称量化。 - 权重量化省的是固定的权重显存,KV 量化省的是随并发增长的那部分;两者独立配置。8B 模型 FP8 权重约 9.08 GB、4 bit 约 5.7 GB(估算),速度和精度待实测(C3-5)。