vLLM INT8 W8A8 量化实战:用 llm-compressor 制作 8 位权重与 8 位激活的推理模型
本文以 vLLM 官方量化文档(int8_w8a8.md)为主线,完整讲解如何用 llm-compressor 将 LLM 量化为 INT8 W8A8(8 位权重 + 8 位激活)checkpoint 并在 vLLM 中加载评估,同时结合 vLLM 源码(compressed_tensors_w8a8_int8.py)解析 vLLM 如何识别、加载并执行这类 checkpoint。读完后,你将能够独立完成"量化制作—vLLM 加载—lm-eval 精度验证"的全流程,并理解 vLLM 内部为 W8A8 模型选择的 kernel 与参数布局。
一、INT8 W8A8 是什么,支持哪些硬件
vLLM 支持将权重和激活同时量化到 INT8,用于降低显存占用并加速推理。W8A8 与"仅量化权重"(W8A16)的关键区别在于:激活值也在 8 位整数域内完成矩阵乘法(INT8 GEMM),因此在内存带宽受限的负载下往往能获得更高的吞吐,代价是需要校准数据来估计激活的量化范围。
文档明确了硬件适用前提,部署前必须确认:
- 支持:计算能力 > 7.5 的 NVIDIA GPU,即 Turing、Ampere、Ada Lovelace、Hopper 架构;
- 不支持(Blackwell 限制):计算能力 >= 10.0 的 GPU(如 RTX 6000 Blackwell)不支持 INT8 量化,应改用 FP8 量化,或在 Hopper/Ada/Ampere 架构上运行。
这一点在 vLLM 源码中可以直接印证。W8A8 INT8 的量化方案类 CompressedTensorsW8A8Int8 通过 get_min_capability() 声明了最低硬件要求:
@classmethod
def get_min_capability(cls) -> int:
# turing and up
return 75
返回的 75 即 Compute Capability 7.5(Turing),与文档中"compute capability > 7.5"的支持说明一致。在加载 checkpoint 时,vLLM 会在 compressed_tensors.py 的 get_scheme 末尾调用 _check_scheme_supported(scheme.get_min_capability()),若当前设备不满足方案要求会直接报错——这就是在 Blackwell 上加载 W8A8 模型会失败/回退的机制来源之一(Blackwell 的 CC 为 10.x,INT8 方案不被选择,故文档建议直接使用 FP8)。
二、环境准备:vLLM 与 llm-compressor 分环境部署
量化制作依赖 llm-compressor 库,该库提供 GPTQ、AWQ、AutoRound 等量化算法以及 SmoothQuant 之类的迁移变换,输出 compressed-tensors 格式供 vLLM 直接加载。文档建议安装两个互相独立的环境:
# 量化制作环境
(venv-llm-compressor) pip install llmcompressor
# vLLM 推理与评估环境
(venv-vllm) pip install vllm "lm-eval[api]>=0.4.12"
文档特别强调:请为 vLLM 和 llm-compressor 使用独立的 Python 环境,两者直接混装在同一个环境中可能无法协同工作(依赖版本冲突)。这个建议在实操中非常关键:量化是一次性离线流程,推理是线上常驻流程,两者解耦后各自升级互不影响。
三、量化流程四步走
文档将量化流程拆分为四个主要步骤:加载模型、准备校准数据、应用量化算法、在 vLLM 中评估精度。下面按顺序完整展开(以 Llama-3-8B-Instruct 为例,代码与文档一致,可直接复制运行)。
3.1 第一步:加载模型
使用标准的 transformers AutoModel 类加载模型与分词器:
from transformers import AutoTokenizer, AutoModelForCausalLM
MODEL_ID = "meta-llama/Meta-Llama-3-8B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
device_map="auto",
dtype="auto",
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
device_map="auto" 会把大模型自动切分到多卡;dtype="auto" 让 transformers 从模型配置中读取原始精度(通常为 bfloat16),保证量化前的参考状态与发布模型一致。
3.2 第二步:准备校准数据
激活量化到 INT8 时,必须用样本数据估计激活的量化尺度(scale)。文档的准则是:校准数据应尽可能贴近你的实际部署数据。对于通用指令微调模型,可以使用 ultrachat 数据集:
from datasets import load_dataset
NUM_CALIBRATION_SAMPLES = 512
MAX_SEQUENCE_LENGTH = 2048
# Load and preprocess the dataset
ds = load_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft")
ds = ds.shuffle(seed=42).select(range(NUM_CALIBRATION_SAMPLES))
def preprocess(example):
return {"text": tokenizer.apply_chat_template(example["messages"], tokenize=False)}
ds = ds.map(preprocess)
def tokenize(sample):
return tokenizer(sample["text"], padding=False, max_length=MAX_SEQUENCE_LENGTH,
truncation=True, add_special_tokens=False)
ds = ds.map(tokenize, remove_columns=ds.column_names)
参数要点:
NUM_CALIBRATION_SAMPLES = 512:文档最佳实践建议的起步校准样本数,若精度下降可继续增加;MAX_SEQUENCE_LENGTH = 2048:起步序列长度,同样可按场景调整;apply_chat_template:使用模型训练所用的 chat template 组装文本,避免校准分布偏离真实推理输入(文档最佳实践之一);truncation=True, add_special_tokens=False:截断超长样本且不重复添加特殊 token。
如果模型经过领域微调,文档建议改用一份训练数据的采样作为校准集,让激活统计更贴合目标分布。
3.3 第三步:应用量化算法(SmoothQuant + GPTQ)
llm-compressor 通过 oneshot 一次性执行 recipe 中的多个 Modifier。W8A8 的推荐组合是:
from llmcompressor import oneshot
from llmcompressor.modifiers.quantization import GPTQModifier
from llmcompressor.modifiers.smoothquant import SmoothQuantModifier
# Configure the quantization algorithms
recipe = [
SmoothQuantModifier(smoothing_strength=0.8),
GPTQModifier(targets="Linear", scheme="W8A8", ignore=["lm_head"]),
]
# Apply quantization
oneshot(
model=model,
dataset=ds,
recipe=recipe,
max_seq_length=MAX_SEQUENCE_LENGTH,
num_calibration_samples=NUM_CALIBRATION_SAMPLES,
)
# Save the compressed model: Meta-Llama-3-8B-Instruct-W8A8-Dynamic-Per-Token
SAVE_DIR = MODEL_ID.split("/")[1] + "-W8A8-Dynamic-Per-Token"
model.save_pretrained(SAVE_DIR, save_compressed=True)
tokenizer.save_pretrained(SAVE_DIR)
各配置项的含义:
SmoothQuantModifier(smoothing_strength=0.8):SmoothQuant 迁移变换,把激活中难以量化的"离群通道"难度部分转移给权重,使两者都更容易用 8 位表示。smoothing_strength控制迁移强度;GPTQModifier(targets="Linear", scheme="W8A8", ignore=["lm_head"]):以 GPTQ 算法对全部Linear层做 W8A8 量化;ignore=["lm_head"]保持词表投影层不量化(输出层对生成质量敏感,通常保留高精度);scheme="W8A8"产出的激活量化方式是动态逐 token(dynamic per-token),因此文档将保存目录命名为...-W8A8-Dynamic-Per-Token;save_compressed=True:以 compressed-tensors 格式序列化,这是 vLLM 能够自动识别并选择 W8A8 方案的 checkpoint 格式。
量化完成后,得到权重与激活均为 8 位整数的 W8A8 模型,体积约为 BF16 原始模型的一半。
3.4 第四步:在 vLLM 中加载并评估精度
量化后的模型可以直接用 LLM 类加载,无需指定量化参数——vLLM 会从 checkpoint 的量化配置中自动推导方案:
from vllm import LLM
llm = LLM("./Meta-Llama-3-8B-Instruct-W8A8-Dynamic-Per-Token")
随后用 lm_eval 做精度评估(以 GSM8K 五样例为文档示例):
lm_eval --model vllm \
--model_args pretrained="./Meta-Llama-3-8B-Instruct-W8A8-Dynamic-Per-Token",add_bos_token=true \
--tasks gsm8k \
--num_fewshot 5 \
--limit 250 \
--batch_size 'auto'
!!! 注意:add_bos_token 参数 !!!
文档特别提醒:量化模型对 bos token 的有无可能敏感,运行评估时务必带上 add_bos_token=true(即 add_bos_token=True),否则分数可能异常波动。这是 W8A8 评估中一个容易被忽略的坑。
四、源码级解析:vLLM 如何识别和执行 W8A8 checkpoint
vLLM 通过 compressed-tensors 量化框架处理 llm-compressor 的产物。理解加载链路有助于排查"为什么我的模型走了 W8A8 方案"或"为什么加载失败"。
4.1 方案判定:静态 per-tensor 还是动态 per-token
checkpoint 的 quantization_config 中记录了权重与激活各自的位宽、策略(tensor/channel/token)、动态性等信息。vLLM 在 compressed_tensors.py 中按两类条件分派到 W8A8 INT8 方案:
if self._is_static_tensor_w8a8(weight_quant, input_quant):
return CompressedTensorsW8A8Int8(
strategy=weight_quant.strategy,
is_static_input_scheme=True,
input_symmetric=input_quant.symmetric,
)
if self._is_dynamic_token_w8a8(weight_quant, input_quant):
return CompressedTensorsW8A8Int8(
strategy=weight_quant.strategy,
is_static_input_scheme=False,
input_symmetric=input_quant.symmetric,
)
两个判定函数给出了精确的方案边界(见 _is_static_tensor_w8a8 / _is_dynamic_token_w8a8):
| 判定条件 | 静态 per-tensor 变体 | 动态 per-token 变体 |
|---|---|---|
| 位宽 | 权重 8 位、激活 8 位 | 权重 8 位、激活 8 位 |
| 权重策略 | tensor 或 channel | tensor 或 channel |
| 激活策略 | tensor(静态全局 scale) | token(逐 token 动态) |
| 动态性 | 权重与激活均静态 | 权重静态、激活动态 |
| 对称性 | 权重必须对称;激活对称/非对称均可 | 权重必须对称;激活对称/非对称均可 |
本文 3.3 节由 GPTQModifier(scheme="W8A8") 产出的 W8A8-Dynamic-Per-Token 模型,走的正是第二行"动态 per-token"分支,且权重策略通常为 channel。
4.2 权重布局与 kernel 选择
CompressedTensorsW8A8Int8.create_weights 为每个被量化的线性层注册三类参数,这也是 compressed-tensors checkpoint 中张量组织的对应关系:
weight:torch.int8张量,形状[out_features, in_features],按 TP 分片维度为 output 维;weight_scale:float32。channel 策略下形状为[out_features, 1](每行权重一个尺度,用ChannelQuantScaleParameter承载);tensor 策略下是逐层单值(PerTensorScaleParameter);input_scale/input_zero_point:仅在静态输入方案(is_static_input_scheme=True)时注册。若激活量化非对称,zero point 以 int8 存储(加载后按 int32 使用,即azp)。
kernel 的选择在同一步完成:
self.kernel = init_int8_linear_kernel(
is_channelwise=(self.strategy == QuantizationStrategy.CHANNEL),
is_static_input_scheme=self.is_static_input_scheme,
input_symmetric=self.input_symmetric,
module_name=self.__class__.__name__,
)
从源码结构看,vLLM 依据"channel 还是 tensor 权重尺度 × 静态还是动态激活 × 激活是否对称"三个二元组合,在加载完成后由 self.kernel.process_weights_after_loading(layer) 把 checkpoint 的序列化形态重排(repack)成 kernel 期望的格式,前向时统一走 self.kernel.apply_weights(layer, x, bias) 完成 INT8 反量化矩阵乘。这意味着量化侧(llm-compressor 产物)与推理侧(kernel)之间由 compressed-tensors 配置契约解耦:只要配置落在上述判定边界内,vLLM 即可自动选路。
4.3 未匹配时的行为
若某层的量化参数组合落不到任何受支持的方案上,get_scheme_from_parts 会抛出 NotImplementedError,并打印该层的 layer_name、权重/激活/输出量化参数与 format,方便定位是哪一层、哪种配置不被支持;若 checkpoint 某层没有任何权重量化配置,则回退到未量化的线性方法(UnquantizedLinearMethod),与 ignore=["lm_head"] 的行为一致。
五、最佳实践
文档给出的最佳实践汇总如下,建议在制作自己的 W8A8 checkpoint 时逐条对照:
- 校准数据从 512 条样本起步,若精度下降则增加样本量;
- 校准序列长度从 2048 起步,按目标上下文长度调整;
- 使用模型训练时所采用的 chat/instruction template 构造校准文本;
- 若模型经过微调,考虑取训练数据的一个采样作为校准集,使激活统计与部署分布对齐。
此外,结合本文源码部分可以补充两条工程实践:量化时固定忽略 lm_head 等对精度敏感的输出层;评估时始终显式指定 add_bos_token=true 以消除 bos 敏感性带来的分数偏差。
六、故障排查
遇到问题时可按以下路径排查:
- Blackwell 上无法使用 INT8:CC >= 10.0 的 GPU 不支持该方案,改用 FP8 量化(见 fp8.md)或换用 Hopper/Ada/Ampere;
- 加载时报 "No compressed-tensors compatible scheme":用报错中打印的
weight_quant/input_quant字段对照 4.1 节的判定边界,检查位宽、策略与动态性是否落在受支持组合内; - 量化环境与推理环境冲突:回到 3.1 节前提,确认 llm-compressor 与 vLLM 分环境部署;
- 更复杂的量化问题:文档指引到 vllm-project 的 llm-compressor 仓库 issue 区提交问题或功能请求。
参考文件
- 原始文档:docs/features/quantization/llm_compressor/int8_w8a8.md
- llm-compressor 系列文档:README、FP8、INT4、INT8 W4A8
- W8A8 INT8 方案实现:vllm/model_executor/layers/quantization/compressed_tensors/schemes/compressed_tensors_w8a8_int8.py
- 方案判定与分派:vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors.py
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 StartedRust0624
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