模型量化 - vLLM 运行时量化架构
前四篇分别概述了 GPTQ、AWQ、SmoothQuant、FP8 四种量化算法。读完之后你应该理解:GPTQ 用 Hessian 逐列补偿误差,AWQ 用网格搜索做 scale 迁移,FP8 用浮点格式自身解决动态范围问题。
LDA 2026年5月14日 11:55
vLLM 运行时量化架构
一、引言
前四篇分别概述了 GPTQ、AWQ、SmoothQuant、FP8 四种量化算法。读完之后你应该理解:GPTQ 用 Hessian 逐列补偿误差,AWQ 用网格搜索做 scale 迁移,FP8 用浮点格式自身解决动态范围问题。
但这些算法是 llm-compressor 侧的——它们在模型导出阶段运行。当模型被保存为checkpoint之后, vLLM 侧 要做的事情完全不同:识别 checkpoint 的量化格式、为每一层选择正确的内核、正确加载打包的量化权重、在 forward 时执行高效推理。
这篇文章介绍vLLM的运行时量化架构,将前四篇的算法放到同一个系统框架中,展示它们如何在工程上落地。
我们会以一条命令为线索:--quantization gptq_marlin。追踪它经过的四个环节:
- 配置检测 vLLM 怎么知道 checkpoint 是 GPTQ 格式的?为什么 gptq_marlin 能自动替代 gptq?
- 逐层分发 模型有 100+ 个 Linear 层,每层怎么知道自己该用什么量化方法?
- 权重参数 INT4 权重打包在 INT32 里,加载时怎么正确做 TP 分片?
- 加载后处理 权重加载完成后,还需要做什么才能交给内核执行? 二、配置检测:从命令到 QuantizationConfig
2.1 检测入口
ModelConfig._verify_quantization()(config/model.py:944-1043)是整个量化配置检测的入口。它在ModelConfig.__init__()的验证阶段被调用(model.py:699),此时用户指定的 --quantization 已经作为字符串存入 self.quantization。
核心数据流:
●●●
1HF config.json 的 quantization_config 字段
2 → _verify_quantization()
3 → 遍历所有量化方法的 override_quantization_method()
4 → 首个返回非 None 的方法成为最终 self.quantization
5 → 如果用户未指定 → 自动赋值;如果已指定 → 校验匹配
方法的第一步是读取 HuggingFace 模型 config.json 中的 quantization_config 字段(model.py:950)。这个字段是 llm-compressor 在导出阶段写入的,包含了量化方法的所有参数——比如 GPTQ 的 bits、group_size、sym,或 FP8 的 activation_scheme。
2.2 覆盖机制:为什么 gptq_marlin 能自动替代 gptq
QuantizationConfig 基类定义了一个类方法 override_quantization_method()(base_config.py:112-130),默认返回 None。每个量化方法可以覆盖它来声明:「我能处理这种 checkpoint 格式,请用我来替代」。
以 GPTQ → Marlin 自动升级为例(gptq_marlin.py:221-245):
●●●
1@classmethod
2def override_quantization_method(cls, hf_quant_cfg, user_quant, hf_config=None):
3 if is_gptq_marlin_compatible(hf_quant_cfg) and user_quant != "gptq":
4 return "gptq_marlin" # ← 自动升级
5 return None
逻辑是:如果 checkpoint 的量化配置与 Marlin 兼容,且用户没有显式强制使用 legacy gptq,就自动返回 "gptq_marlin" 覆盖原始方法名。这意味着用户写 --quantization gptq 时,实际运行的是更快的 Marlin 内核——完全透明。
AWQ 有同样的模式(awq_marlin.py:234-262):检查 is_awq_marlin_compatible(),自动将 AWQ checkpoint 升级到 AWQ-Marlin。
优先级顺序 (config/model.py:957-984):检测循环并非简单遍历所有方法。实际逻辑是:先把 不在 内置 overrides 列表中的方法放在前面(包括用户通过 register_quantization_config 注册的外部方法),然后把内置 overrides 列表追加在后面。这意味着 外部注册的方法优先于内置覆盖方法 ——用户自定义量化方法有机会在内置检测之前接管匹配。在 overrides 列表内部,Marlin 变体(gptq_marlin、awq_marlin)排在各自的 legacy 版本之前,heavy 后端(如 mxfp4)排在最后——它们的导入会触发 Triton 等重依赖,放在末尾避免不必要的加载。
这个优先级列表是精心排列的,注释中明确说明 "ordering is important"。如果顺序反了,一个同时兼容 GPTQ 和 Marlin 的 checkpoint 可能被 legacy gptq 抢走,用户就享受不到 Marlin 的性能提升。
AWQ 的等价升级 (awq_marlin.py:234-262):
●●●
1@classmethod
2def override_quantization_method(cls, hf_quant_cfg, user_quant, hf_config=None):
3 if is_awq_marlin_compatible(hf_quant_cfg) and user_quant != "awq":
4 return "awq_marlin"
5 return None
架构含义: 量化格式(checkpoint 的存储格式)与推理内核解耦了 。同一个 GPTQ 格式的 checkpoint,可以由 legacy GPTQ 内核运行,也可以由 Marlin 内核运行。vLLM 自动选择最快的兼容内核,对用户和模型制作者都透明。
2.3 get_quant_config():从方法名到配置对象
检测到方法名(如"gptq_marlin")之后,需要构建对应的 QuantizationConfig 对象。get_quant_config()(weight_utils.py:259-433)负责这一步,它有一个 级联解析 逻辑:
●●●
1① HF config 内嵌的 quantization_config → quant_cls.from_config(hf_cfg)
2② 单独的 JSON 文件(如 quantize_config.json) → quant_cls.from_config_file()
3③ 用户传入的 config_dict JSON 字符串 → quant_cls.from_config_dict_json()
4④ 在线量化(用户传了 quantization_config 参数)→ OnlineQuantizationConfig()
5⑤ 磁盘搜索(下载目录中匹配的 JSON 文件) → quant_cls.from_config(json.load(f))
这个级联体现了 vLLM 的兼容性哲学。同一个量化方法(如 fp8)的配置可能以多种形式存在:
- 预量化 checkpoint(如 ModelOpt 导出的 FP8 模型)→ 路径 ① 或 ②
- 在线量化(用户想在加载时把 BF16 转为 FP8)→ 路径 ④
- GGUF 格式(量化元数据嵌在 GGUF 文件本身,不需要单独 JSON)→ 直接 return quant_cls() 空参数构造(line 267-268)
第五级的磁盘搜索是最灵活的 fallback——vLLM 下载模型目录后,搜索匹配 quant_cls.get_config_filenames() 返回的 JSON 文件名,读到的第一个就是配置。
2.4 量化方法注册表
quantization/__init__.py:12-46的QuantizationMethods类型列出了全部 30+ 种支持的量化方法字符串。get_quantization_config()(lines 109-188)构建方法名到配置类的映射,使用 懒导入 避免在启动时引入 Triton、CUTLASS 等重依赖。
register_quantization_config() 装饰器(lines 59-106)是扩展点——外部量化方法可以通过 @register_quantization_config("my_method") 注册到 vLLM 的量化系统中。
三、逐层分发:每个 Linear 层独立决策
3.1 LinearBase 中的分发点
LinearBase.__init__()(linear.py:245-278)是每层获得量化方法的地方:
●●●
1if quant_config is None:
2 self.quant_method = UnquantizedLinearMethod()
3else:
4 self.quant_method = quant_config.get_quant_method(self, prefix=prefix)
prefix 是该层的完整限定名——例如 "model.layers.0.self_attn.q_proj"。这个字符串传给 get_quant_method(),使配置对象能 按名称决定 该层的量化策略。
注意quant_config是 共享的 (一个模型只有一个 Fp8Config 或 GPTQMarlinConfig 实例),但 quant_method 是 每层独立的 。100 个 Linear 层有 100 个 quant_method 对象,虽然大部分会返回相同的类,但架构允许它们不同。
3.2 三种分发模式
不同的 QuantizationConfig 子类有不同的 get_quant_method() 实现,代表三种分发粒度:
统一量化 (Fp8Config):所有Linear层返回相同的 Fp8OnlineLinearMethod,通过ignored_layers跳过特定层(如lm_head)。Attention层走 Fp8KVCacheMethod。分发逻辑是按 层类型 (isinstance 检查)。
按 target 匹配 (CompressedTensorsConfig):维护一个 target_scheme_map,键是层名正则模式(如 "re:.self_attn."),值是该组的量化 scheme。同一个模型的 attention 层和 MLP 层可以有不同的 scheme(W4A16 vs W8A8)。分发逻辑是按 层名前缀 匹配。
按quant_algo查表 (ModelOptMixedPrecisionConfig):维护 quantized_layers dict,直接映射层名 → "FP8" 或 "NVFP4"。用于导出时就决定了混合精度策略的 checkpoint。分发逻辑是 字典查找 。
3.3 策略模式:三层生命周期
每个 QuantizeMethodBase 子类控制该层在三个生命周期阶段的全部行为:
●●●
1① create_weights() → 分配什么 dtype、什么 shape 的参数
2② weight_loader → 如何从 checkpoint 加载并正确 TP 分片
3③ apply() → forward 时用哪个内核
这意味着同一模型可以混合 GPTQMarlinLinearMethod(attention 用 W4A16 Marlin)和 Fp8LinearMethod(MLP 用 FP8 CUTLASS)——因为每层有自己独立的 quant_method 对象,各自控制自己的参数创建、权重加载和前向计算。
这种设计让 vLLM 能支持 mixed precision ——不同层用不同量化方案,无需在框架层做特殊处理。只需get_quant_method()根据层名返回不同的 QuantizeMethodBase 子类即可。
四、权重参数与加载后处理
4.1 参数子类体系
vLLM 的参数与普通的 torch.nn.Parameter不同。BasevLLMParameter(parameter.py:31-126)扩展了 PyTorch 的参数类,附加了一个 weight_loader 回调——当 safetensors 加载器把磁盘数据写入参数时,这个回调被触发,处理 TP 分片、列/行并行切分等逻辑。
重要子类:
| 参数类 | 行号 | 用途 |
|---|---|---|
| _ColumnvLLMParameter | 129 | 列并行 TP 分片基类——load_column_parallel_weight() 在此定义 |
| RowvLLMParameter | 204 | 行并行 TP 分片基类——load_row_parallel_weight() 在此定义 |
| ModelWeightParameter | 233 | 普通 FP16 权重,多继承列+行并行 |
| PackedvLLMParameter | 353 | 打包量化权重 + Marlin tile 对齐,继承 ModelWeightParameter |
| GroupQuantScaleParameter | 242 | Per-group scale/zp(如 group_size=128) |
| ChannelQuantScaleParameter | 251 | Per-channel scale(仅列并行) |
| PerTensorScaleParameter | 260 | Fused 层(QKV/gate_up)的标量 scale |
| BlockQuantScaleParameter | 397 | Block 量化 scale(FP8 per-block, NVFP4) |
文中提及的 load_column_parallel_weight() / load_row_parallel_weight() 方法定义在_ColumnvLLMParameter/RowvLLMParameter 上,ModelWeightParameter 和 PackedvLLMParameter 通过继承获得这些方法。
这些子类的共同设计原则: 封装 sharding 知识到参数本身 。加载器不需要知道量化格式——它只需调用param.load_column_parallel_weight(tensor),参数对象自己处理 TP 分片。
4.2 打包参数如何做 TP 分片
以 GPTQ Marlin 为例:INT4 权重以 8 个为一组打包进 INT32。当模型做 TP(张量并行)时,每个 GPU 只负责输出通道的一部分。
模块级函数 _adjust_shard_indexes_for_packing()(parameter.py:605-617,由 PackedvLLMParameter.adjust_shard_indexes_for_packing() 调用,line 389)将 TP 分片的 offset 和 size 除以 packed_factor:
●●●
1原始(FP16): shard_size = d_out / tp_size 个 FLOAT16 元素
2INT4 打包后: shard_size = d_out / tp_size / 8 个 INT32 元素
3 (每个 INT32 包含 8 个 INT4)
Marlin 内核还有 tile size 对齐要求——如果 d_out 不是 256 的倍数(Marlin tile size),参数创建时自动padding到tile边界。这在 PackedvLLMParameter 的构造中通过 marlin_tile_size 参数控制。
4.3 process_weights_after_loading():最后一公里
process_weights_after_loading()(utils.py:99-127)是 vLLM 量化模型加载的最后一步。它遍历 整个模型的所有 module (通过 named_modules()),对每个拥有 QuantizeMethodBase 类型 quant_method 属性的 module 调用其后处理:
●●●
1for _, module in model.named_modules():
2 quant_method = getattr(module, "quant_method", None)
3 if isinstance(quant_method, QuantizeMethodBase):
4 with device_loading_context(module, target_device):
5 quant_method.process_weights_after_loading(module)
这里用 isinstance(quant_method, QuantizeMethodBase) 而非 hasattr——因为非量化 module 也可能有同名的 quant_method 属性(如 Attention 模块的 quant_method 是其自身的量化配置,不继承 QuantizeMethodBase),必须做类型检查避免误匹配。
device_loading_context是一个上下文管理器,接受 (module, target_device) 两个参数,临时将 CPU-offloaded 的权重移到 GPU 上,处理完再移回去。
第二遍——Attention模块 (utils.py:115-122):第一遍处理完所有 QuantizeMethodBase之后,还有一个独立的循环遍历Attention、MLAAttention、MMEncoderAttention模块,调用它们自身的 process_weights_after_loading(model_config.dtype)。这是 KV-cache 量化(如 Fp8KVCacheMethod,§3.2 已提及)的入口——Attention 类用自己的方法处理 KV cache scale 的解压和格式转换,与 QuantizeMethodBase 的生命周期并行但独立。
不同量化方法的 process_weights_after_loading() 做的事差异很大:
| 量化方法 | process_weights_after_loading() 做了什么 |
|---|---|
| GPTQ Marlin | QKV 的 scale 与 zp 交错排列(Marlin 内核要求的格式),调用 Marlin repack 函数将权重从 GPTQ 格式转 Marlin 格式 |
| AWQ | 权重逆排列(恢复 activation ordering 之前的列顺序),转置,可选转换为 Marlin 格式 |
| FP8 Online | 调用 ops.scaled_fp8_quant() 将刚加载的 BF16 权重现场量化为 FP8,转置 |
| FP8 Offline | QKV/MLP 用 max scale 统一各分片的 scale(TP 下各 rank 的 scale 可能略有差异),转置权重适配内核格式 |
| Compressed-Tensors | 根据 scheme 类型做对应的 weight repack、scale format conversion |
一个值得注意的差异 :GPTQ 和 AWQ 的 Marlin 路径会在 Python 侧主动调用 Marlin 的 repack 函数,将权重从「checkpoint 中的存储格式」转为「Marlin 内核期望的格式」。而 FP8 的 Marlin 路径(用于无 FP8 硬件的 GPU)跳过 Python 侧的转置——Marlin 内核自己在其 process_weights_after_loading() 中完成格式转换。
4.4 三阶段生命周期的闭合
至此,量化模型在 vLLM 中的加载遵循统一的三阶段生命周期:
●●●
1① create_weights()
2 分配参数:确定 dtype(FP8/INT4/FP16)、shape、是否打包、是否需要 scale/zp
3 注册 weight_loader 回调
4
5② 权重加载(safetensors → parameter)
6 磁盘数据逐块写入参数
7 weight_loader 回调处理 TP 分片 + 打包偏移换算
8 加载器对此一无所知——它只调用 param.load_xxx_weight(tensor)
9
10③ process_weights_after_loading()
11 重排/转置/重量化/格式转换
12 内核初始化(设置 fp8_linear / marlin_linear 等内核实例)
FP8 在线量化的第三步是最重量级的——它在 process_weights_after_loading() 中现场执行 ops.scaled_fp8_quant(),等价于把 llm-compressor 侧的权重量化搬到了 vLLM 的加载阶段。
五、全链路串联:GPTQ Marlin 的完整旅程
用第二篇的 GPTQ 算法作为贯穿案例,串联本文的全部四个环节。
第 0 步——llm-compressor 导出 checkpoint (第二篇 §5-6):
llm-compressor 的 GPTQModifier 读取校准数据,逐层计算 Hessian、执行 Cholesky + 逐列补偿,最终将 INT4 权重、per-group scale/zp、可选的 g_idx(activation ordering)写入 safetensors。量化配置可以写入 HF config.json 的内嵌 quantization_config 字段(路径 ①),也可以写入单独的 quantize_config.json 文件(路径 ②)——两种格式 vLLM 都能识别(见 §2.3 的级联解析)。
① 配置检测 :
●●●
1用户:vllm serve meta-llama/Llama-3.1-8B-GPTQ --quantization gptq_marlin
2
3vLLM 启动:
4 → ModelConfig.__init__() 调用 _verify_quantization()(model.py:944)
5 → 读取 HF config.json 的 quantization_config:
6 {"bits": 4, "group_size": 128, "desc_act": false, "sym": true, "quant_method": "gptq"}
7 → 遍历覆盖方法列表:
8 gptq_marlin: override_quantization_method() 检查 is_gptq_marlin_compatible()
9 → desc_act=false, sym=true, group_size=128 → 兼容!
10 → 返回 "gptq_marlin"
11 → self.quantization = "gptq_marlin"
12
13 → get_quant_config()(weight_utils.py:259)
14 → get_quantization_config("gptq_marlin") → GPTQMarlinConfig 类
15 → 从 hf_quant_cfg 调 GPTQMarlinConfig.from_config(hf_cfg)
16 → 返回配置对象
② 逐层分发 :
●●●
1→ initialize_model()(utils.py:40)
2→ 构建模型,每个 Linear 层触发 LinearBase.__init__()(linear.py:274):
3 quant_config.get_quant_method(self, prefix="model.layers.0.self_attn.q_proj")
4→ GPTQMarlinConfig.get_quant_method() 返回 GPTQMarlinLinearMethod 对象
5→ 100+ 个 Linear 层各有自己的 GPTQMarlinLinearMethod 实例
6→ lm_head 层:prefix 匹配 ignored_layers → UnquantizedLinearMethod
③ 权重加载 :
●●●
1→ GPTQMarlinLinearMethod.create_weights():
2 - 创建 PackedvLLMParameter("weight", packed_factor=8, marlin_tile_size=256)
3 shape: [d_out, d_in/8] # 8 个 INT4 打包进 1 个 INT32
4 - 创建 GroupQuantScaleParameter("weight_scale", group_size=128)
5 shape: [d_out, d_in/128]
6 - 创建 GroupQuantZeroPointParameter("weight_zp", group_size=128)
7 - 分配 Marlin kernel 实例
8
9→ safetensors 逐层加载:
10 对每层调用 param.load_column_parallel_weight(loaded_tensor)
11 → PackedvLLMParameter._adjust_shard_indexes_for_packing():
12 如果 tp_size=4,d_out=4096 → 每 rank 负责 1024 个输出通道
13 shard_size = 1024 / 8 = 128 个 INT32
14 shard_offset = rank * 128
④ 后处理 :
●●●
1→ process_weights_after_loading()(utils.py:99)
2→ 遍历每个 module:
3 对每个 GPTQMarlinLinearMethod:
4 ① QKV 层:将 q_proj/k_proj/v_proj 的 scale 和 zp 交错排列
5 (Marlin 内核要求 interleaved format)
6 ② 调用 Marlin repack 函数:
7 将 GPTQ 存储格式的权重重排为 Marlin 内核期望的 tile 布局
8 ③ 非 Marlin 路径:转置权重(Python 侧 .t())
9 ④ 完成权重创建时的 padding 对齐
⑤ Forward :
●●●
1→ GPTQMarlinLinearMethod.apply(layer, x):
2 → Marlin kernel:INT4 权重 × FP16 激活 → FP16 输出
从 --quantization gptq_marlin 到 INT4 矩阵乘法执行,中间经过了配置检测、方法分发、参数创建、权重加载、格式转换五个阶段。每个阶段都是可插拔的——如果将 gptq_marlin 替换为 fp8,整个流程仍然适用,只是每个阶段的具体类不同。
六、小结
- vLLM 量化架构遵循三阶段生命周期 create_weights() 分配参数 → 权重加载(safetensors → parameter)→ process_weights_after_loading()重排/转置/格式转换。这个模式适用于所有量化方法。
- 覆盖机制实现格式兼容 override_quantization_method() 让 GPTQ checkpoint 自动被 Marlin 内核接管,AWQ checkpoint 自动升级到 AWQ-Marlin。用户无需知道底层内核的差异,vLLM 自动选择最快的兼容路径。
- 逐层分发实现混合精度 get_quant_method(layer, prefix) 使同一模型的不同层可以有不同的量化方案——attention 用 W4A16 Marlin,MLP 用 FP8,lm_head 保持 FP16。每层独立的 quant_method 对象控制自己的全部行为。
- 参数子类封装打包与分片 PackedvLLMParameter、PerTensorScaleParameter 等子类将 TP 分片和比特打包逻辑封装在参数自身——加载器完全格式无关。
- 这条架构不依赖任何特定算法 ——GPTQ、AWQ、SmoothQuant、FP8、NVFP4 全部复用同一套基础设施。添加新的量化方法只需实现QuantizationConfig+ QuantizeMethodBase 两个接口即可。
SGLang&VLLM · 目录
作者提示: 个人观点,仅供参考
继续滑动看下一个
LLM高性能计算
向上滑动看下一个