vLLM 在线量化(Online Quantization)实战指南:加载期量化 Linear 与 MoE 权重
导读
vLLM 的在线量化(Online Quantization)允许直接加载一个 BF16/FP16 的原始模型,并在模型加载阶段将其 Linear 与 MoE(专家混合)层权重转换为 FP8 / MXFP4 等低精度格式,全程无需预先量化的 checkpoint,也无需校准数据集。整个过程分为两段:权重在加载时完成一次性转换,激活值则在每次前向传播时动态计算缩放因子。本文基于 在线量化官方文档 与 vLLM 仓库源码,完整讲解其支持的量化方案、quantization_config 高级配置语法、Dense 与 MoE 分层量化、逐层排除与逐层覆盖等能力,并深入到 vllm/config/quantization.py 与 vllm/model_executor/layers/quantization/online 的实现细节,让读者既能直接上手,也能理解背后的解析与派发机制。
在线量化是什么
传统量化流程通常是离线的:先用校准数据(calibration data)统计数值范围,产出预量化 checkpoint,再在推理时加载。在线量化把"量化"这一动作搬进了模型加载流程:
- 权重:BF16/FP16 权重在加载时被转换为更低精度(例如 FP8 的 e4m3),缩放因子基于权重自身的最大值一次性计算;
- 激活:不依赖校准统计,每个 forward pass 都依据实际输入张量的数值范围动态求取缩放因子。
该设计有两类典型收益:一是省去了 pre-quantized checkpoint 与校准管线;二是能够对已经发布但只有原始权重、且当前没有对应量化权重发布的模型立刻做低精度推理。
从源码看,在线量化是一整套独立的量化方法族。模型级入口是 OnlineQuantizationConfig,位于 vllm/model_executor/layers/quantization/online/base.py,其 get_name() 返回 "online",并且 get_config_filenames() 返回空列表——这正说明它不是从 checkpoint 的量化配置文件中恢复出来的,而是完全由运行期传入的 quantization 与 quantization_config 参数驱动。在线线性方法基类 OnlineLinearBase 还把权重放到 meta device 上延迟物化(uses_meta_device: bool = True,见 fp8.py),并在权重物化过程中完成在线量化,这也是"不需要预量化 checkpoint"能够成立的关键工程实现。
Quick Start:两种入口立即上手
在线量化最简单的用法是给 quantization 参数传入一个方案名(shorthand)。
Python API
from vllm import LLM
# Per-tensor FP8 量化(每个权重张量一个缩放因子)
llm = LLM("meta-llama/Llama-3.1-8B", quantization="fp8_per_tensor")
# Per-block FP8 量化(权重按 128x128 block 缩放,激活按 1x128 block 缩放)
llm = LLM("meta-llama/Llama-3.1-8B", quantization="fp8_per_block")
# 权重与激活均为 MXFP8
llm = LLM("meta-llama/Llama-3.1-8B", quantization="mxfp8")
# MXFP4 权重;激活是否量化取决于所选 linear_backend
llm = LLM("meta-llama/Llama-3.1-8B", quantization="mxfp4")
# MXFP4 仅作用于 MoE 层的权重与激活(Dense 层保持原 dtype)
llm = LLM(
"Qwen/Qwen3.5-35B-A3B",
quantization="mxfp4",
quantization_config={"linear": {"activation": None, "weight": None}}
)
CLI
vllm serve meta-llama/Llama-3.1-8B --quantization fp8_per_tensor
vllm serve meta-llama/Llama-3.1-8B --quantization fp8_per_block
vllm serve meta-llama/Llama-3.1-8B --quantization mxfp8
vllm serve meta-llama/Llama-3.1-8B --quantization mxfp4
vllm serve Qwen/Qwen3.5-35B-A3B --quantization mxfp4 \
--quantization-config '{"linear":{"activation":null,"weight":null}}'
最后一个例子展示了一个重要细节:对于 35B-A3B 这类 Dense+MoE 混合架构,mxfp4 shorthand 默认同时作用于 Linear 与 MoE;如果只想量化 MoE 专家(通常是推理与显存热点),就把 linear 的 weight/activation 都显式置为 null。
支持的量化方案总览
| 方案 | 权重配方 | 激活配方 | 备注 |
|---|---|---|---|
fp8_per_tensor |
fp8_e4m3 数据,fp32 每张量缩放 | fp8_e4m3 数据,fp32 每张量缩放 | 部分 GPU(Ada、Hopper)上线性层激活改用 per-token 缩放以提升性能 |
fp8_per_block |
fp8_e4m3 数据,fp32 按 128x128 block 缩放 | fp8_e4m3 数据,fp32 按 1x128 block 缩放 | |
mxfp8 |
fp8_e4m3 数据,e8m0 按 1x32 block 缩放 | fp8_e4m3 数据,e8m0 按 1x32 block 缩放 | w8a8 形态需要 SM 100+(Blackwell 或更新);其余 GPU 走 w8a16 回退 |
mxfp4 |
fp4_e2m1 数据,e8m0 按 1x32 block 缩放(遵循 OCP Microscaling 格式规范) | Linear:部分后端为 fp4_e2m1 + e8m0 按 1x32 block 缩放,其余为 BF16;MoE:fp4_e2m1 + e8m0 按 1x32 block 缩放 | Linear 的 MXFP4 后端按平台自动选择,并不强制激活 dtype,部分后端使用 BF16 激活;可通过 --linear-backend 固定,如 --linear-backend flashinfer |
需要说明两点:
mxfp8中两种格式皆为 OCP MX(Microscaling)格式。在仓库实现里,mxfp8对应动态量化键kMxfp8Dynamic(quant_utils.py),实现在 online/mxfp8.py;MXFP8 线性方法支持动态缩放激活、逐 block e8m0 缩放权重。mxfp4方案对应静态权重键kMxfp4Static(quant_utils.py),实现位于 online/mxfp4.py。MXFP4 是 4 bit 格式,权重以 FP4(e2m1)存储并配上块共享的 8 bit e8m0 缩放指数,相比纯 FP8 更进一步压缩权重体积。
除上述 shorthand 外,--quantization 还接受 fp8_per_channel、int8_per_channel_weight_only、nvfp4_per_token 以及字面量 online。见 vllm/config/quantization.py 中 ONLINE_QUANT_SHORTHAND_NAMES 的定义。
quantization_config:细粒度高级配置
要获得精确控制,应使用 quantization_config 字典。
Schema 结构
quantization_config:
linear:
weight: <name> # 取值见 QUANT_KEY_NAMES(vllm/config/quantization.py)
activation: <name>
moe:
weight: <name>
activation: <name>
ignore: [<layer-name-or-regex-or-fnmatch-pattern>, ...]
该结构的 Python 端定义是 QuantizationConfigArgs(vllm/config/quantization.py),其中:
linear:应用到所有LinearBase层(Dense GEMM);moe:应用到RoutedExperts(MoE 专家层,对应FusedMoEFactory体系);ignore:跳过量化的层名单;targets:按层精细指定不同方案(见下文)。
linear 与 moe 的每个字段都接受一个 QuantSpec(vllm/config/quantization.py),即 {weight, activation} 的完整字典,其中字段值既可以是对应权重方案的 QuantKey 名称,也允许直接置为 None(表示该侧不量化、回退到方法类自身默认值)。
权重方案名表(QUANT_KEY_NAMES)
文档中 weight/activation 可填的名字由 vllm/config/quantization.py 的 QUANT_KEY_NAMES 定义:
| 名称 | 含义 |
|---|---|
fp8_per_tensor_static |
FP8 e4m3,fp32 每张量静态缩放 |
fp8_per_tensor_dynamic |
FP8 e4m3,fp32 每张量动态缩放 |
fp8_per_token |
FP8 e4m3,fp32 每 token 动态缩放 |
fp8_per_channel_static |
FP8 e4m3,fp32 每输出通道静态缩放 |
fp8_per_block_static |
FP8 e4m3,fp32 按 128x128 block 静态缩放 |
fp8_per_block_dynamic |
FP8 e4m3,fp32 按 block 动态缩放 |
mxfp8 |
e8m0 缩放,MXFP8 动态 |
mxfp4 |
e8m0 缩放,MXFP4 动态 |
int8_per_channel_static |
INT8 每通道静态缩放 |
传入非法名称时,解析器会抛出错误并列出合法候选项(_coerce_quant_key,vllm/config/quantization.py)。
字符串简写解析规则
linear 和 moe 除了接受完整的 {weight, activation} 字典,也接受裸字符串。裸字符串的解析分两步(见 _coerce_spec,vllm/config/quantization.py):
- 先与
--quantization的 shorthand 对照——若命中(如"fp8_per_block"),取出该 shorthand 在当前层种类(linear 或 moe)槽位上的默认配置; - 若未命中 shorthand,再按
QUANT_KEY_NAMES解析为权重方案名(如"fp8_per_block_static")。
未显式设置的字段回退规则:优先取 --quantization shorthand 的默认值;对本身已量化的 checkpoint,则回退到 checkpoint 声明的配置。
CLI 等价写法:JSON 或点号路径
命令行既支持整段 JSON,也支持点号路径(dotted keys):
vllm serve <model> --quantization-config '{"moe":{"activation":"mxfp8"}}'
vllm serve <model> --quantization-config.moe.activation mxfp8
两种写法都会被 Pydantic 解析进同一个 QuantizationConfigArgs,再经 resolve_quantization_config 合并到 --quantization 的 base 配置上——凡是 quantization_config 显式设置的字段都优先于 shorthand 的默认值(vllm/config/quantization.py)。
--quantization 的完整 shorthand 清单
vllm/config/quantization.py 中 _ONLINE_SHORTHANDS 给出了所有可直接用于 --quantization 的方案及其解糖后的实际配置:
| shorthand | 解糖语义 |
|---|---|
fp8_per_tensor |
linear.weight = kFp8StaticTensorSym,moe.weight = kFp8StaticTensorSym |
fp8_per_block |
linear.weight = kFp8Static128BlockSym,moe.weight = kFp8Static128BlockSym |
fp8_per_channel |
每输出通道权重缩放 + 动态 per-token 激活;形状与 llmcompressor 的 FP8_DYNAMIC recipe 一致 |
mxfp8 |
linear.weight = kMxfp8Dynamic,moe.weight = kMxfp8Dynamic |
mxfp4 |
linear.weight = kMxfp4Static,moe.weight = kMxfp4Static |
int8_per_channel_weight_only |
仅 MoE 用 INT8 weight-only;Linear 不量化(无 linear 槽) |
nvfp4_per_token |
MoE 使用 NVFP4 + 每 token 动态激活缩放(仅 Blackwell + FlashInfer TRTLLM 后端);Linear 不量化 |
注意 mxfp4、mxfp8 同时是常见的 checkpoint 量化方法名。_DEFERRED_ONLINE_SHORTHANDS(vllm/config/quantization.py)说明:当检测到 checkpoint 自带量化元数据时,这两个名字会被"延迟",优先复用 checkpoint 的量化方式;只有 checkpoint 没有量化元数据时才解析为在线配置。
在已量化 checkpoint 上覆盖激活格式
对 checkpoint 本身已量化的模型,quantization_config 允许你独立于固化在权重里的格式,挑选激活的量化格式。可选覆盖集合因 checkpoint 而异。目前该能力已接通 MXFP4 MoE checkpoint(如 gpt-oss 系列),可为原生的 FP4 权重选择 FP8 激活:
vllm serve openai/gpt-oss-20b --quantization-config.moe.activation mxfp8
需要与特定 kernel 家族组合时,还可以配合 --moe-backend 固定 MoE 的 kernel 实现。
需要留意实现边界:在 base.py 的 _get_method_cls 中,各在线方法类内部自行决定激活格式,目前并非每个方法类都接通了显式的 activation 覆盖——对尚未支持显式覆盖的类传入非空 activation 会直接报错。因此 activation 覆盖应当严格按"checkpoint 特定、方法类已支持"的前提使用。
部分量化 checkpoint:对未量化层补充在线量化
在线量化还可以叠加在"只量化了一部分层"的已有 checkpoint 上,与其原始 quant_method(如 modelopt、compressed-tensors、quark 等)无关。其组合原则是:
- checkpoint 的
quant_method继续负责它已量化的那些层; - 原 checkpoint 中保持未量化的层,则采用本次请求的在线量化方式。
举例来说,下面的命令给一个 Quark 格式、仅量化了 MoE 专家的 checkpoint 的 Dense Linear 层补上 MXFP8 在线量化:
vllm serve amd/Qwen3.5-35B-A3B-MXFP4 \
--quantization-config.linear mxfp8
其组合逻辑在 resolve_quantization_config 中:当 --quantization 不是在线 shorthand(即它是 checkpoint 方法名)但给出了 quantization_config 时,vLLM 会把 base quant_method 的加载与在线层的补充量化衔接起来。
一个容易踩的坑:quantization_config.ignore 是仅在线量化生效的排除列表。原 quant_method 的忽略逻辑只依赖它自己的 ignore 实现与 config.json 里声明的忽略层,二者互不干扰。
Dense 与 MoE 分层使用不同方案
通过 linear 与 moe 两个字段,可以让 Dense Linear 层与 MoE 专家层使用完全不同的量化方案。每一侧都可给出完整 spec dict,或裸字符串(在线 shorthand 名,如 "fp8_per_block",或权重格式名,如 "fp8_per_block_static");未设置的字段回退到 shorthand 默认值。
from vllm import LLM
# Linear: per-block FP8;MoE: per-tensor FP8(从 shorthand 继承)
llm = LLM(
"ibm-granite/granite-3.0-1b-a400m-base",
quantization="fp8_per_tensor",
quantization_config={
"linear": "fp8_per_block",
},
)
或者反过来:
from vllm import LLM
# Linear: per-tensor FP8(继承);MoE: per-block FP8
llm = LLM(
"ibm-granite/granite-3.0-1b-a400m-base",
quantization="fp8_per_tensor",
quantization_config={
"moe": "fp8_per_block",
},
)
从源码看,linear/moe 之所以能够各自独立生效,是因为在线量化在运行时按层派发:resolve_quant_method_cls(base.py)根据层实例类型决定走哪张派发表与哪个 spec——LinearBase 层取 args.linear + _ONLINE_LINEAR_METHODS,RoutedExperts 层取 args.moe + _ONLINE_MOE_METHODS。两张派发表定义在 base.py:Linear 侧支持 FP8 per-tensor / per-block / per-channel、MXFP8、MXFP4;MoE 侧额外支持 INT8 与 NVFP4。
用 ignore 排除不想量化的层
ignore 参数用来跳过特定层,接受三类匹配形式:
- 精确层名:如
"model.layers.1.self_attn.o_proj"; - 正则表达式:以
re:为前缀; - fnmatch 模式:使用 Python
fnmatch.fnmatch的语义(*、?、[seq]等)。
from vllm import LLM
llm = LLM(
"ibm-granite/granite-3.0-1b-a400m-base",
quantization="fp8_per_tensor",
quantization_config={
"ignore": [
# 精确层名
"model.layers.1.self_attn.o_proj",
# 正则:跳过所有 QKV 投影
"re:.*[qkv]_proj",
# fnmatch:跳过所有 MoE 专家
"*mlp.experts*",
],
},
)
注意 fused 层(如 qkv_proj 融合了 q_proj/k_proj/v_proj)的特殊性:匹配模式既可以直接命中融合后的层名,也可以命中其全部未融合分片名。实现上,忽略判断经由 should_ignore_layer,并传入 packed_modules_mapping(融合映射)与 use_fnmatch=True,见 base.py。
targets:逐层精细量化方案
如果不想让同一方案作用于 linear/moe 两大类里的所有层,而是要给具体不同的层分配不同方案,就使用 targets 参数。其 key 支持精确层名、re: 正则、fnmatch 模式;value 是方案 shorthand 名(fp8_per_tensor、fp8_per_block、fp8_per_channel、mxfp8、int8_per_channel_weight_only、nvfp4_per_token)。
from vllm import LLM
llm = LLM(
"Qwen/Qwen3.5-35B-A3B",
quantization="online",
quantization_config={
"targets": {
# 精确层名
"model.layers.0.self_attn.o_proj": "fp8_per_tensor",
# 正则:量化所有 QKV 投影
r"re:.*self_attn\.qkv_proj.*": "mxfp8",
# fnmatch:量化所有 MoE 专家
"*mlp.experts*": "mxfp4",
},
},
)
或者使用 CLI:
vllm serve Qwen/Qwen3.5-35B-A3B \
--quantization online \
--quantization-config '{"targets":{"model.layers.0.self_attn.o_proj":"fp8_per_tensor","re:.*self_attn\\.qkv_proj.*":"mxfp8","*mlp.experts*":"mxfp4"}}'
使用 targets 时,--quantization 通常取字面量 "online",因为具体方案已由 targets 逐层指定。
targets 的约束与校验规则
在线量化的 targets 在解析期会做严格校验,这些约束在源码中均有对应实现(_validate_targets 与 _validate_targets_exclusivity,vllm/config/quantization.py):
targets与linear/moe互斥:只能设置其一。同时给出会在配置解析阶段直接抛出ValueError。- 未命中任何 pattern 的层保持不变:维持 checkpoint 原始 dtype,不做在线量化。
- 同一层不能同时命中
targets与ignore:会抛出错误(base.py)。 - 同一层不能命中多个
targetspattern:命中多个同样报错(base.py)。 - fused 层的分片约束:融合层(如 qkv_proj)的每个分片都必须命中相同方案,不同分片若各自命中了不同 target 会报错——这保证了 fused 权重不会被切分成互相矛盾的量化格式(base.py)。
- fnmatch 风格仅在线量化支持:Quark 或 compressed-tensors 的配置不会应用 fnmatch pattern。
- targets 的值必须是合法 shorthand 名:否则在
_validate_targets中即报错并给出可用清单;re:前缀的 pattern 还会先做一次正则编译验证。
平台与后端注意事项
XPU 上 Linear 的 W8A16/W8A8 选择
在 XPU 平台,非 block 的 FP8 scaled-mm Linear 层默认使用 W8A16;可通过后端开关改变:
--linear-backend xpu:强制 W8A8(使用 XPU 自定义 kernel);--linear-backend xpu_woq:显式选择 weight-only 量化(W8A16);--linear-backend torch:同样强制 W8A8,但 GEMM 通过torch._scaled_mm执行,而非自定义 XPU kernel。
Blackwell 世代与 MoE 后端的依赖
mxfp8的 w8a8 形态要求 SM 100+(Blackwell 或更新);其他 GPU 自动回退到 w8a16 方案。nvfp4_per_token在线 MoE 量化要求 Blackwell GPU 与 FlashInfer 的 TRTLLM 后端配合。- MoE 场景可用
--moe-backend固定特定的 kernel 家族,与--quantization-config.moe.activation覆盖搭配使用。
在线量化在源码里如何工作
逐层派发与摘要日志
加载模型时,vLLM 会遍历每个模块,用其完整前缀名(prefix,如 model.layers.0.self_attn.qkv_proj)调用 get_quant_method(layer, prefix)(base.py):
- 命中的层会被登记进
quantized_layers,并返回对应量化方法类实例(Linear 方法须继承OnlineLinearBase,MoE 方法须继承FusedMoEMethodBase); - 未命中的
LinearBase回落到UnquantizedLinearMethod,未命中的RoutedExperts回落到UnquantizedFusedMoEMethod,即"保持原 dtype"。
启动日志会通过 quantized_layer_summaries(base.py)输出形如 self_attn.o_proj: 24 (from targets: re:.*self_attn\.o_proj, mxfp4) 的汇总,方便核对哪些层被量化成了什么格式。
权重量化与并行一致性
在线权重量化发生在加载完成后的处理阶段(对应 process_weights_after_loading 等钩子)。对 tensor parallel 与 expert parallel 场景,缩放因子必须在各 rank 之间保持一致。仓库为此提供了一组 amax 归约辅助函数(quant_utils.py):
amax_for_tp_weight_quant:当权重沿 amax 归约维被张量切分时,在 TP group 上做 MAX all-reduce;amax_for_moe_weight_quant:在 EP 覆盖的 DP × PCP × TP 范围上归约(EP 下每个 rank 持有完整专家,无需归约);amax_for_moe_activation_quant:把 per-expert 激活缩放归约为所有专家共享的单一值——在开启 EPLB 时,缩放会折入 per-expert 反量化 alpha,因此必须保证跨 rank 完全一致。
动态激活缩放与 kernel 路由
激活侧不保存静态缩放,而是在每次前向时依据输入算 amax 再推导 fp32 scale(_fp8_scale 等,见 online/fp8.py)。可执行 kernel 的挑选也被封装进统一的 kernel 初始化入口 init_fp8_linear_kernel,配合平台能力自动选择 CUTLASS / Marlin 等 scaled-mm kernel 实现。
常见误区与限制小结
- 不是所有 activation 覆盖都能随意使用:激活覆盖目前只对已接通的 checkpoint/方法类生效(文档明确给出的是 MXFP4 MoE checkpoint 选择 FP8 激活的场景),对未支持的方法类显式传 activation 会报错。
ignore不是"全局忽略":对 checkpoint 自带 quant_method 的场景,ignore只影响在线叠加的部分。- 方案有硬件门槛:MXFP8 w8a8 需要 Blackwell+,NVFP4 per-token MoE 需要 Blackwell + FlashInfer TRTLLM,运行前应结合目标 GPU 能力选择,避免落到非预期的回退路径。
- fused 层必须整体一致:被融合进同一个权重张量的多个投影必须使用相同的 target 方案,否则加载直接报错,这是设计上对正确性的强制约束。
在线量化把"权重一次性转换 + 激活动态缩放"这套能力完整地内置到了 vLLM 加载管线中,配合 quantization_config 的分层、逐层覆盖能力,可以在不重新发布 checkpoint 的前提下,灵活地对不同模型结构探索低精度推理的最佳配置。更多背景可回到 在线量化官方文档 及其所在目录 docs/features/quantization 查阅其他量化方法的对比。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00