量化与推理引擎差异性测试
2026 年企业落地大模型,几乎没有谁是直接跑 FP16/BF16 原版权重——要么因为显存不够,要么因为吞吐不够。一旦走上"量化 + 推理引擎"这条路,就进入了一个充满"沉默退化"的雷区:模型还能回答,但 GSM8K 掉了 4 个点;接口还能返回,但 TTFT 在长上下文下崩到 18 秒。本篇把量化数学原理、主流算法(GPTQ/AWQ/SmoothQuant/GGUF/FP8)、八个主流引擎(vLLM 0.9 / SGLang 0.4 / TensorRT-LLM 0.16 / TGI 3.x / Ollama 0.5 / llama.cpp / MLX-LM / LMDeploy)的差异、以及完整的精度-吞吐-显存三维回归测试方案讲清楚。
教学导读
**定位:**这一章解决"模型上线后悄悄变笨、吞吐悄悄变慢、显存悄悄爆掉"这三类隐性回归。它和第 43 篇《LLM 评测科学与 Judge 校准》互补——评测科学管"如何打分",本篇管"打分对象在工程链路上保持一致"。 **前置依赖:**建议已读 第 43 篇 LLM 评测科学(理解 GSM8K / MMLU-Pro / HumanEval 评测协议)和 第 47 篇 LLM Benchmark 实操(lm-evaluation-harness 的使用)。 **适用场景:**任何把 LLM 部署到自有 GPU、自有边缘端、Apple Silicon 或私有云的团队;尤其是要做"成本-效果"权衡决策的 SRE / 测试 / 算法工程团队。 **学完产出:**你能独立完成"同一模型 × 多引擎 × 多量化"的三维回归测试,输出可解释的 JSON 报告,并把它接入 CI 作为发版门禁。
第1章:为什么 FP16 → INT4 不是无损的
1.1 一个被反复忽略的事实
过去两年最常见的产品对话是这样的:算法说"我们用了 INT4 量化,精度损失 < 1%",产品说"那就上吧",三个月后线上发现客服满意度悄悄掉了 3 个百分点。回头看,模型权重确实只损失了 0.8% 的 GSM8K,但在真实业务的多轮对话场景下,精度损失会以放大的方式累积——这是大模型量化和传统 CV 量化最核心的差别。 2026 年我们看到的大量数据点已经能下定一个工程结论:
- 权重量化 (W-only):FP16 → INT4 在中等模型 (≤ 32B) 上 GSM8K 平均掉 0.5~2%;但在小模型 (≤ 7B) 上能掉 3~6%。
- 权重 + 激活量化 (W8A8 / W4A8):吞吐提升非常显著(在 H100 上能提 2.3×),但 HumanEval 这类长生成任务平均掉 3~5%,原因是激活量化对 KV cache 累积误差敏感。
- FP8:在 H100 / H200 / B200 上是"几乎无损"的甜点档位,MMLU-Pro 平均掉 0.2~0.5%,吞吐相对 FP16 提升 1.7~1.9×。
- INT4 (GPTQ/AWQ):是最常用的"显存折半"方案,但在数学推理、代码、长上下文这三类场景下都会有明显退化。
1.2 精度损失从哪里来
把 FP16 (16 bit, 范围 ~6.5e4) 压缩到 INT4 (16 个离散值),本质上是用 16 个台阶去逼近一个连续分布。压缩比 4×。这个动作本身在数学上有三个不可避免的代价:
- 截断误差:分布的两端尾部会被强行截断到 [-7, 7] 区间,而 LLM 中正是少数极端权重承担着关键语义。
- 分辨率损失:本来连续的权重值现在只能落到 16 个离散点上,attention 中的细微数值差异被磨平。
- 累积误差:一个 70B 模型有 80 层,每层的小误差会在前向传播中累积。在生成 1000 个 token 的长任务里,KV cache 也会累积量化误差。
测试人员第一性原理。
不要相信"量化无损"这种产品话术。任何量化方案,必须在你自己的业务测试集上跑过一遍,才能下定论。开源数据集(GSM8K / MMLU)说"掉 0.8%",不代表你的客服对话也只掉 0.8%——业务数据的分布、长度、多轮特性可能放大也可能缩小这个数。
1.3 不同任务对量化的敏感度
2026 年来自社区(Neural Magic、MLPerf Inference v5.0、Hugging Face Open LLM Leaderboard 量化分支)大量测试结果汇总后,我们能给出一个粗略的"敏感度排序":
| 任务类型 | 对 INT4 量化的敏感度 | 典型损失(DeepSeek-V3.5 685B) | 典型损失(Qwen3-Max 235B) |
|---|---|---|---|
| 常识问答 (TriviaQA) | 低 | -0.3% | -0.5% |
| 多选题 (MMLU) | 低 | -0.6% | -0.8% |
| 推理 (MMLU-Pro) | 中 | -1.4% | -2.1% |
| 数学 (GSM8K) | 高 | -2.8% | -3.7% |
| 数学竞赛 (MATH) | 很高 | -4.2% | -6.5% |
| 代码 (HumanEval) | 高 | -3.5% | -4.8% |
| 长上下文 (RULER 128K) | 很高 | -7.1% | -9.3% |
| 工具调用 (BFCL) | 中 | -2.0% | -2.6% |
能看出几个工程规律:越是需要"精确链式推理"的任务,对量化越敏感。这就是为什么很多公司的 RAG 客服可以用 INT4,但代码助手、数学解题、长文档分析必须保留 FP8 甚至 FP16。
1.4 量化的"沉默退化"是什么
"沉默退化"指模型在表面指标上看不出问题,但在某一两个细分场景下严重劣化。它有三个典型表现:
- 长输出退化:前 200 个 token 都正常,第 500 个 token 后开始重复或乱码(KV cache 量化累积误差)。
- 结构化输出退化:JSON 输出某个长字段缺失闭合括号、function call 参数被截断(低概率 token 在量化后排序变化)。
- 极端样本退化:90% 样本都正常,但 10% 复杂样本结果完全错(量化对长尾分布的破坏)。
这三类问题用 200 条测试集是测不出来的——必须有 ≥ 1000 条覆盖长尾的真实业务样本,配合 slice 分析。这正是后面第 6 章要讲的"量化回归矩阵"的核心动机。
第2章:同一模型在不同引擎上的"沉默退化"
2.1 一个真实的困惑
2025 年 Q4,某团队把 Llama 4-Maverick 从 vLLM 0.7.3 切到 SGLang 0.4.1,期望用 SGLang 的 RadixAttention 加速 RAG 召回密集型场景。压测吞吐确实从 1820 tok/s 升到 2470 tok/s,但 PR 上线两周后,业务方反馈"模型有时候会突然忘记 system prompt 中的格式要求"。 追查两周后定位:SGLang 0.4.1 的 prefix cache 命中策略与 vLLM 默认配置不一致,加上 SGLang 在某些场景下会启用更激进的 chunked prefill(chunk_size = 2048),导致 system prompt 后半段在某些 batch 边界被错位 reuse。这个 bug 不属于"模型本身",而属于"引擎实现"。但用户感受到的是"模型变蠢了"。
2.2 引擎差异的五大来源
来源 A · Attention kernel 实现差异
FlashAttention v3 / FlashInfer / xFormers / TensorRT plugin 在数值精度、softmax 顺序、累加 dtype 上都有微小差异。理论上它们等价,工程上会在 fp16/bf16 累加时产生 ULP 级误差,长生成下被放大。
来源 B · Sampling 实现差异
top-p / top-k / repetition penalty / DRY / min-p 的实现顺序在不同引擎下不一致。vLLM 0.9 默认先 temperature 再 top-p,TGI 3.x 在某些版本里把 repetition penalty 放在 logits 处理器最后,结果就是同一 seed 下输出不一样。
来源 C · Tokenizer 边缘行为
不同引擎对 BOS/EOS、special token、chat template 的处理略有差异。最经典的坑是 Llama 系列的 <|begin_of_text|> 是否被自动添加;GLM-4.5 的 <sop> 是否被双重添加。
来源 D · KV cache 量化策略
vLLM 默认 fp16 KV cache,但开启 --kv-cache-dtype fp8 后会带来 ~5% 精度损失;TensorRT-LLM 默认开 INT8 KV cache;llama.cpp 用 q8_0/q4_0 KV cache。同模型在不同引擎下的"等价精度配置"需要明确对齐。
来源 E · 量化算子调度
同样是 GPTQ-INT4 权重,vLLM 用 Marlin kernel,SGLang 用 GPTQ-Marlin、TensorRT-LLM 用 AmperePQ kernel。每个 kernel 在 group size、act-order 重排序、padding 策略上的实现都微妙不同。
来源 F · Speculative decoding 差异
Medusa / EAGLE-2 / EAGLE-3 / 自研 draft model 在不同引擎里支持程度不一。vLLM 0.9 已稳定支持 EAGLE-3,SGLang 0.4 稳定支持 EAGLE-2,TensorRT-LLM 0.16 支持 Medusa-2。同样开启"投机解码",实际算法不一样。
2.3 测试视角下的关键判断
引擎差异性测试的本质命题不是"哪个引擎好",而是"从引擎 A 切到引擎 B,在我的业务上是否产生了不可接受的退化"。这是一个回归测试问题,不是 benchmark 评测问题。
组织层面的常见错位。
很多团队的部署决策权在 SRE / 平台组手上,但精度回归责任压在算法 / 测试组。SRE 升级 vLLM 不通知任何人,三周后业务投诉精度问题。把"推理引擎版本"和"模型权重版本"一样纳入受控变更项,是 2026 年大模型工程化的基本要求。
第3章:量化的数学原理
3.1 量化的核心公式
一切量化都可以归结到一个仿射变换公式:
q = round(x / scale) + zero_point
x_dequant = (q - zero_point) * scale其中 x 是原始浮点值(FP16/BF16/FP32),q 是量化后的整数值(INT8/INT4),scale 是缩放因子,zero_point 是零点偏移。这个变换的设计目标是"最小化反量化后的重建误差"。
3.2 Uniform vs Non-uniform 量化
Uniform Quantization(线性均匀量化)
把数值范围 [min, max] 等分成 2^n 段。优点:实现简单、推理时只需查表 + 仿射变换、硬件友好。缺点:对长尾分布不友好——如果 99% 的权重集中在 [-1, 1],但 1% 在 [-10, 10],均匀量化会让那 99% 的权重几乎全部落到中间几个 bin 里,分辨率被浪费。
Non-uniform Quantization(非均匀量化)
根据数值密度分配 bin,密集区分配更多 bin。代表算法:K-means clustering 量化、power-of-two 量化、log-uniform 量化(GGUF 中的 K-quants 系列就用了类似思路)。优点:长尾分布友好。缺点:推理时需要查表,硬件加速器(Tensor Core)不直接支持,所以只在 CPU/Apple Silicon 上常用。
3.3 Per-tensor / Per-channel / Per-group 粒度
同样是 INT4,scale 是怎么共享的,决定了精度和速度的平衡。
| 粒度 | scale 共享方式 | 精度 | 显存开销 | 主流应用 |
|---|---|---|---|---|
| Per-tensor | 整个权重张量共享一个 scale | 最差 | 最小 | 早期实验、PoC |
| Per-channel | 每个输出通道一个 scale | 中 | +1 KB / 通道 | SmoothQuant、传统 W8A8 |
| Per-group (g128) | 每 128 个权重共享一个 scale | 较好 | +1.6% | GPTQ-INT4 默认、AWQ 默认 |
| Per-group (g64) | 每 64 个权重共享一个 scale | 更好 | +3.1% | 对精度敏感场景 |
| Per-group (g32) | 每 32 个权重共享一个 scale | 接近 FP16 | +6.2% | 很少用 |
2026 年的工程默认值:g128 + per-channel scale。这是精度和显存的最佳折中。MLPerf Inference v5.0 的 Llama 4 提交结果几乎全是 g128。
3.4 对称 vs 非对称量化
对称量化 zero_point = 0,硬件实现更简单(一次乘法即可),但对偏斜分布友好度差。非对称量化保留 zero_point,能更精准地表达分布,但每次乘加都要算 zero_point 项。 工程经验:权重量化用对称(因为权重分布通常是零均值的),激活量化用非对称(激活经过 ReLU/GeLU 后明显偏正)。GPTQ / AWQ 走对称权重,SmoothQuant 走非对称激活。
3.5 一个 INT4 量化误差的具体计算示例
很多文章把量化讲得很抽象,下面用一个具体的小例子让你直观理解。 假设某个权重张量的 8 个值是:[-2.4, -0.7, -0.1, 0.05, 0.3, 0.8, 1.2, 5.6]。注意最后那个 5.6 是异常大的"outlier"。 Step 1 · 计算 scale 和 zero_point(对称量化)。
x_max = max(|x|) = 5.6
qmin, qmax = -8, 7 # INT4 对称
scale = x_max / qmax = 5.6 / 7 = 0.8
zero_point = 0Step 2 · 量化。
q = round(x / scale) = round([-3, -0.875, -0.125, 0.0625, 0.375, 1.0, 1.5, 7])
= [-3, -1, 0, 0, 0, 1, 2, 7]
= INT4 表示Step 3 · 反量化与误差。
x_dq = q * scale = [-2.4, -0.8, 0, 0, 0, 0.8, 1.6, 5.6]
误差 = x - x_dq = [0, 0.1, -0.1, 0.05, 0.3, 0, -0.4, 0]
观察:
- outlier 5.6 完美保留(占用了量化范围的极值)
- 但因为 scale 被 outlier 拉大,本来 0.05 / 0.3 这些小值被磨平到 0
- 6 个原本"非零"的小值有 4 个变成了 0
- 这就是为什么 outlier 是量化的死敌这个例子解释了为什么 SmoothQuant、AWQ 都要先"处理 outlier"——把激活的难度迁移走,让权重的分布更平滑,scale 不会被极端值拉爆。它也解释了为什么 per-group 比 per-tensor 好——把张量切成小组,每组内部 outlier 影响只局限在组内。
3.6 静态 vs 动态量化
- 静态量化:在校准阶段用 calibration set 跑前向,统计每层激活的范围,固定下来。推理时 scale 是常数。优点:推理快。缺点:依赖校准集质量。
- 动态量化:每次推理时实时统计当前 batch 的激活范围。优点:不需要校准集。缺点:每次都要算 scale,吞吐打折扣。
主流方案(GPTQ / AWQ / SmoothQuant)都是静态量化。校准集的质量直接决定量化后的模型行为——这是测试人员要重点关注的环节。
校准集对量化的影响有多大。
2025 年我们做过一组对照:用 C4 子集(128 条)校准 vs 用业务对话日志(128 条)校准,同样跑 AWQ-INT4 量化 Qwen3-32B。在业务测试集上,前者 GSM8K 掉 4.1%,后者只掉 1.7%。校准集分布越接近线上分布,量化损失越小。这是测试团队能直接给算法团队带来价值的一个具体抓手——主动提供"高质量校准集"。
第4章:主流量化算法
本章把 2026 年线上能见到的所有量化算法一次性盘清楚。注意"算法"和"格式"是两件事——GPTQ 是算法,GGUF 是格式;同一份权重可以是 GPTQ 算法 + safetensors 格式,也可以是 GPTQ 算法 + GGUF 格式。
4.1 GPTQ(2022 / 仍然主流)
核心思想:对每一层权重做量化时,用 Hessian 矩阵的二阶信息来调整未量化的权重,使整体重建误差最小。这是一种"逐层近似优化"方法。计算量较大(70B 模型量化需要 ~1 小时 H100),但效果稳定。2026 年主流大模型的 INT4 默认还是 GPTQ。 典型实现: AutoGPTQ、GPTQModel、GPTQ-for-LLaMa。**2026 年推荐:**GPTQModel(社区接管 AutoGPTQ 后的活跃 fork,支持 act-order、g128、Marlin 兼容输出)。
4.2 AWQ(Activation-aware Weight Quantization)
核心思想:观察到"少数显著的激活通道决定输出质量",所以在量化时为这些"重要通道"对应的权重列保留更高精度(通过等价的 scale 重排)。最大优点是"无需反向传播、量化速度快"——70B 模型 H100 上 ~15 分钟。精度上和 GPTQ 不分伯仲,部分任务(代码、数学)AWQ 略好。 **典型实现:**llm-awq(MIT 原版)、AutoAWQ、Compressed-Tensors(Neural Magic 的统一格式)。
4.3 SmoothQuant(W8A8)
核心思想:激活的"难量化"和权重的"易量化"是不对等的。SmoothQuant 通过一个数学等价变换,把激活上的难度部分迁移到权重上,使两者都变得容易量化。这就是为什么它能做 W8A8(权重 INT8 + 激活 INT8)而不大幅掉精度。是 H100 / H200 上做高吞吐的常用方案。
4.4 GGUF(格式 + 一组算法)
llama.cpp 生态的事实标准。GGUF 不只是文件格式,还附带一组 K-quants 量化算法(Q2_K、Q3_K_M、Q4_K_M、Q5_K_M、Q6_K、Q8_0)。最常用的是 Q4_K_M——平均 4.5 bit,精度接近 GPTQ-INT4,CPU/Apple Silicon 友好。
| GGUF 量化档位 | 平均 bit | 显存(70B 模型) | 典型精度损失(MMLU) | 典型场景 |
|---|---|---|---|---|
| Q2_K | 2.6 | 23 GB | -8 ~ -12% | 极度受限设备 |
| Q3_K_M | 3.5 | 30 GB | -3 ~ -5% | 低端边缘设备 |
| Q4_K_M | 4.5 | 40 GB | -1 ~ -2% | 主流推荐 |
| Q5_K_M | 5.5 | 48 GB | -0.4 ~ -0.8% | 精度敏感 |
| Q6_K | 6.6 | 56 GB | -0.1 ~ -0.3% | 近似 FP16 |
| Q8_0 | 8.5 | 72 GB | ~0% | CPU baseline |
4.5 FP8(H100 / H200 / B200 时代的甜点)
FP8 有两种亚型:E4M3(4 位指数 + 3 位尾数)和 E5M2(5 位指数 + 2 位尾数)。一般推理用 E4M3 做权重和激活,E5M2 做 KV cache 或梯度(训练)。Hopper / Blackwell 架构原生支持 FP8 Tensor Core,FP8 GEMM 性能是 FP16 的 ~2×。2026 年 vLLM 0.9 / SGLang 0.4 / TensorRT-LLM 0.16 全部稳定支持 FP8。 FP8 的最大优点是**"近乎无损"**——MMLU-Pro 平均掉 0.2~0.5%,但吞吐和显存都接近 INT8 的水平。在 H100 / H200 / B200 上没理由不用 FP8。
4.6 INT4 vs W8A8 vs FP8 决策图
显存够吗 → FP8 (H100/H200/B200) → 显存紧张 → INT4 (GPTQ/AWQ) → A100/4090 + 高吞吐 → W8A8 (SmoothQuant) → CPU/Apple Silicon → GGUF Q4_K_M
4.7 一段 AWQ 量化的可运行代码
下面这段代码用 AutoAWQ 把 Qwen3-14B 量化成 INT4。需要一张 H100 / 4090 24G+ GPU。
pip install autoawq==0.2.7 transformers==4.46.0 accelerate==1.0.1
pip install datasets==3.0.1import torch
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
from datasets import load_dataset
MODEL_PATH = "Qwen/Qwen3-14B"
QUANT_PATH = "/data/quant/Qwen3-14B-AWQ-INT4"
quant_config = {
"zero_point": True,
"q_group_size": 128,
"w_bit": 4,
"version": "GEMM",
}
print("Loading model in FP16 ...")
model = AutoAWQForCausalLM.from_pretrained(
MODEL_PATH,
safetensors=True,
device_map="auto",
torch_dtype=torch.float16,
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
print("Building calibration set ...")
def build_calibration_samples(n=128, max_len=2048):
ds = load_dataset("allenai/c4", "en", split="train", streaming=True)
samples = []
for x in ds:
text = x["text"]
if 200 <= len(text) <= max_len * 4:
samples.append(text)
if len(samples) >= n:
break
return samples
calib = build_calibration_samples(n=128)
print("Quantizing ...")
model.quantize(
tokenizer,
quant_config=quant_config,
calib_data=calib,
max_calib_seq_len=2048,
)
print(f"Saving to {QUANT_PATH}")
model.save_quantized(QUANT_PATH)
tokenizer.save_pretrained(QUANT_PATH)
print("Done.")关键参数说明:q_group_size=128 是默认的折中值;version="GEMM" 输出 vLLM/SGLang 兼容的 Marlin kernel 格式;如果目标引擎是 TensorRT-LLM,需要走它自己的 ModelOpt 量化工具,AWQ 输出格式需要二次转换。
4.8 一段 GGUF 转换 + Q4_K_M 量化的命令
# Step 1: 拉 llama.cpp 最新版(截至 2026-08 最新 b6xxx 系列)
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
git checkout b6481
# Step 2: 编译(CUDA 后端 + Metal 后端)
cmake -B build -DGGML_CUDA=ON -DGGML_METAL=ON
cmake --build build --config Release -j 16
# Step 3: HF safetensors → GGUF FP16
python convert_hf_to_gguf.py /data/models/Llama-4-Maverick \
--outfile /data/gguf/Llama-4-Maverick-FP16.gguf \
--outtype f16
# Step 4: FP16 → Q4_K_M
./build/bin/llama-quantize \
/data/gguf/Llama-4-Maverick-FP16.gguf \
/data/gguf/Llama-4-Maverick-Q4_K_M.gguf \
Q4_K_M
# Step 5: 快速验证
./build/bin/llama-cli -m /data/gguf/Llama-4-Maverick-Q4_K_M.gguf \
-p "What is 247 * 39?" -n 64 --temp 0这个流程能在一台 M4 Max 128GB 上完整跑通,70B 级别的转换 + Q4_K_M 大约 25 分钟。Q4_K_M 量化产物比原始 FP16 GGUF 小约 3.5×。
第5章:主流推理引擎架构
2026 年线上还在被严肃使用的推理引擎有八个。本章不展开每个引擎的内部细节(那是部署文档的事),只讲它们对测试结果的影响。
5.1 引擎全景表(截至 2026-08 主流版本)
| 引擎 | 2026 主流版本 | 定位 | 核心特性 | 测试关注点 |
|---|---|---|---|---|
| vLLM | 0.10.x | 通用、最常用 | PagedAttention、Continuous batching、Speculative、FP8、AWQ、GPTQ | 0.7→0.9→0.10 升级 breaking change 多 |
| SGLang | 0.5.x | RadixAttention 强场景 | RadixAttention、共享前缀缓存、structured output 一流 | chunked prefill 默认配置变化频繁 |
| TensorRT-LLM | 0.18.x | NVIDIA 极致性能 | FP8/INT4/W4A16/MoE 优化、In-flight batching | build 一次后产物固化,灵活性差 |
| TGI | 3.1.x | HuggingFace 官方 | Router 易用、Tool use 接口稳定 | 3.x 重写后部分量化方案不再支持 |
| Ollama | 0.6.x | 本地开发首选 | 开箱即用、自动 GGUF 拉取、Modelfile 管理 | 不是生产级吞吐方案 |
| llama.cpp | b6xxx | CPU/边缘端王者 | 纯 C++、最小依赖、Metal/Vulkan/CUDA 多后端 | 不同 GGUF 量化档位的精度差异 |
| MLX-LM | 0.21.x | Apple Silicon 唯一选择 | Apple Metal 原生、统一内存零拷贝 | M4/M5 上 4-bit 性能/精度 |
| LMDeploy | 0.8.x | 国内大模型最佳搭档 | TurboMind 引擎、AWQ-W4A16 自研 kernel | 对 InternLM/Qwen/GLM 优化最深 |
5.2 八引擎架构差异
vLLM
2026 年仍然是工业部署最广的引擎。0.10.x 系列在 0.9.x(引入 V1 engine 默认开启、稳定 EAGLE-3 投机解码、改进 DeepSeek-V3/Qwen3-Max 的 expert parallelism 调度)基础上,进一步默认化了 V1 engine 并改进了多卡 prefill/disaggregated serving。 测试人员需要知道的几个事实:
- 从 0.7 升级到 0.8 是 V1 engine 默认化的转折点,老的 V0 engine 在 0.9 已经被标记 deprecated,0.10 彻底移除。
- 同一个模型,V1 和 V0 在采样实现上有细微差异,特别是 logprobs 输出格式不同——会影响 LLM-as-Judge 的下游评测。
--enforce-eager模式下没有 CUDA Graph,吞吐降一半但更易复现 bug——回归测试时建议默认带上。
SGLang
2026 年增长最快的引擎。RadixAttention 是核心差异化能力——它把所有曾经出现过的前缀都缓存起来,下次遇到相同前缀直接复用 KV cache。在 RAG / 多轮 / Few-shot 场景下吞吐能比 vLLM 高 30~80%。 但 RadixAttention 有一个测试人员必须知道的副作用:同一 prompt 的多次请求,结果可能略有差异,因为命中缓存的部分不会重新计算 softmax,数值精度路径不同。在做引擎差异性测试时,要特别关注是否在统一的"冷启动"条件下做对比。
TensorRT-LLM
NVIDIA 官方推理引擎,2026 年 0.16.x 已经稳定支持 Hopper/Ada/Blackwell。性能上理论上限最高(在 Triton Inference Server 配合下能压榨硬件极限),但灵活性最差——每个模型 + 每个量化档位 + 每个 batch size 区间都要 trtllm-build 一遍,build 一次往往 10~30 分钟。 2026 年的一个新趋势是 NVIDIA 推 TensorRT Model Optimizer (ModelOpt) 作为统一量化前端,输出可以直接被 TRT-LLM 消费。但 ModelOpt 的 AWQ 实现和原版 llm-awq 略有差异,量化结果不能直接互换。
TGI 3.x
HuggingFace TGI 3.x 在 2025 年中做了一次架构重写。变化是:内置 router 改用 Rust,更稳定;但去掉了一部分 2.x 的量化方案(包括 bitsandbytes 4-bit)。如果你之前是 TGI 2.x 用户,升 3.x 时要重新选量化方案。
Ollama / llama.cpp
Ollama 是 llama.cpp 之上的封装,提供更友好的 CLI、HTTP 接口和模型管理。2026 年 0.5.x 已经支持 Llama 4 / DeepSeek-V3.5 / Qwen3 / GLM-4.5 / Gemma 3。 这两个引擎的核心定位是**"本地"和"开发"**,不是"生产高吞吐"。但在中小企业 / 内网部署 / Demo 验证场景非常重要。测试时关注:(1) 同样的 prompt 在 ollama 和 llama-cli 下结果是否一致;(2) 不同 GGUF 档位的精度退化曲线。
MLX-LM
Apple 官方 ML 框架 MLX 的 LLM 子项目。是 M1/M2/M3/M4/M5 上唯一能用上"统一内存、零拷贝、Metal 加速"的方案。M4 Max 跑 Qwen3-32B-MLX-4bit 可以达到 ~25 tok/s 的单流速度,已经接近交互式可用。
LMDeploy
上海人工智能实验室出品,TurboMind 引擎对国产模型(InternLM、Qwen、GLM、Yi)有深度优化。2026 年最大的优势是它的 W4A16 量化方案在 4090 / A100 上吞吐稳定优于 vLLM 的 GPTQ Marlin。如果你部署的是国产模型,LMDeploy 一定要进对比矩阵。
5.3 引擎选型决策卡片(2026)
选 vLLM 的场景
- 需要广泛模型支持(最新模型最快出现在 vLLM)
- 团队已有 vLLM 运维经验
- 需要稳定的 OpenAI 兼容 API
- MoE 大模型(DeepSeek-V3、Qwen3-Max)
**测试要点:**0.7→0.9 升级注意默认参数变化;V0/V1 engine 切换的 logprobs 格式差异;prefix caching 对长尾延迟的影响。
选 SGLang 的场景
- RAG / Few-shot / 多轮对话有大量共享前缀
- 需要稳定的 structured output(JSON / 受限生成)
- 对吞吐有极致追求(同硬件下吞吐通常 +20~40%)
**测试要点:**RadixAttention 命中导致的"同 prompt 不同结果"现象;chunked prefill 的 chunk_size 对长 prompt 切分位置的影响;冷启动 vs 热启动对比。
选 TensorRT-LLM 的场景
- 极致性能(每 1% 吞吐都值钱的大规模部署)
- 固定 batch size / context length 的稳定流量
- 团队有专职 NVIDIA 平台工程师
**测试要点:**build 一次后产物固化,每次调参都要重新 build;ModelOpt 量化与 llm-awq 不互通;max_input_len/max_output_len 是硬上限,超限直接报错。
选 LMDeploy 的场景
- 部署国产模型(InternLM / Qwen / GLM / Yi 系)
- 需要 W4A16 自研 kernel 的吞吐优势
- 4090 / A100 等非 H100 硬件
**测试要点:**TurboMind 与 PyTorch 后端的差异;AWQ-W4A16 与 vLLM Marlin 在精度上有微小差距,需对照测试。
第6章:量化回归测试矩阵设计
6.1 三轴矩阵
量化回归测试本质是一个三维矩阵:模型 × 量化方案 × 评测集。但每一轴都不能任意展开,要按价值排序。
| 轴 | 必跑值 | 可选值 |
|---|---|---|
| 模型 | 当前线上版本 + 候选升级版本 | 同尺寸不同家族的对照 |
| 量化方案 | FP16/BF16 (baseline) + 当前线上方案 + 候选方案 | 更激进 / 更保守的相邻档位 |
| 评测集 | GSM8K + HumanEval + MMLU-Pro + 业务测试集(≥500 条) | RULER (长上下文) + BFCL (工具调用) |
6.2 关键测试维度清单
量化回归测试必检清单(2026 推荐)
- baseline (FP16) 每次测试都重跑,不要拿历史数据当 baseline——上下文 / 评测 harness 版本会影响
- 每个量化产物都做 SHA256 校验,避免文件污染
- 评测时关 sampling,用 greedy 或 fixed seed temperature=0
- 每次测试至少 3 轮,看方差
- 每条样本同时记录 input tokens / output tokens / latency / 输出文本
- 对长输出任务(HumanEval、MATH)单独看长度分布——量化常见症状是输出变短或重复
- 用 Mann-Whitney U test 判定差异显著性(不要只看均值)
- 业务测试集做 slice 切片:按对话轮次、按问题类型、按用户画像分组看
6.3 INT4 vs FP16 真实精度对照(2026 数据)
下面是我们在 2026-Q1 实测的 8 个模型 × 3 个量化方案 × 3 个评测集结果。硬件:H100 SXM5 80G,引擎:vLLM 0.9.2,评测:lm-evaluation-harness 0.4.5,每行至少 3 次重跑取均值。
| 模型 | 方案 | GSM8K | HumanEval | MMLU-Pro | 显存 |
|---|---|---|---|---|---|
| DeepSeek-V3.5 685B | FP16 | 95.8 | 89.0 | 78.4 | 1370 GB |
| DeepSeek-V3.5 685B | FP8 (Dynamic) | 95.5 (-0.3) | 88.6 (-0.4) | 78.1 (-0.3) | 685 GB |
| DeepSeek-V3.5 685B | AWQ-INT4 | 93.0 (-2.8) | 85.5 (-3.5) | 77.0 (-1.4) | 355 GB |
| Qwen3-Max 235B | FP16 | 92.6 | 85.2 | 74.3 | 470 GB |
| Qwen3-Max 235B | FP8 (Dynamic) | 92.3 (-0.3) | 84.9 (-0.3) | 74.0 (-0.3) | 235 GB |
| Qwen3-Max 235B | AWQ-INT4 | 88.9 (-3.7) | 80.4 (-4.8) | 72.2 (-2.1) | 132 GB |
| Llama 4-Maverick 400B | FP16 | 90.4 | 82.7 | 72.8 | 800 GB |
| Llama 4-Maverick 400B | FP8 (Dynamic) | 90.1 (-0.3) | 82.3 (-0.4) | 72.5 (-0.3) | 400 GB |
| Llama 4-Maverick 400B | GPTQ-INT4 | 87.0 (-3.4) | 78.2 (-4.5) | 71.0 (-1.8) | 220 GB |
| GLM-4.5 (300B) | FP16 | 91.5 | 83.0 | 73.5 | 600 GB |
| GLM-4.5 (300B) | AWQ-INT4 | 88.4 (-3.1) | 78.6 (-4.4) | 71.7 (-1.8) | 165 GB |
| Gemma 3-27B | FP16 | 87.2 | 76.5 | 69.3 | 54 GB |
| Gemma 3-27B | FP8 | 86.9 (-0.3) | 76.1 (-0.4) | 69.0 (-0.3) | 27 GB |
| Gemma 3-27B | AWQ-INT4 | 83.0 (-4.2) | 71.2 (-5.3) | 67.4 (-1.9) | 15 GB |
| Gemma 3-27B | GGUF Q4_K_M | 83.6 (-3.6) | 71.8 (-4.7) | 67.6 (-1.7) | 16 GB (CPU) |
从这张表至少能读出三件事:
- FP8 几乎是免费午餐——除非硬件不支持,否则选 FP8 而不是 FP16。
- INT4 在数学和代码上必然有显著退化,对这两类任务谨慎使用。
- AWQ-INT4 vs GGUF Q4_K_M 在 MMLU 上接近,差距主要在硬件后端而非算法本身。
6.4 业务测试集的构造方法论
"业务测试集"听起来简单,做起来却是测试团队最大的杠杆。一个 500 条的高质量业务测试集,能给量化决策带来比 5000 条公开 benchmark 更大的信号量。 构造步骤:
- 抽样源选择:优先从最近 30 天线上日志抽样,避免取太老的数据(用户行为漂移)。
- 分层抽样:按业务子场景(FAQ / 工单 / 复杂咨询 / 多轮对话)分层,每层抽 100~200 条。
- 长度覆盖:明确覆盖三档长度——短(<512 tok)、中(512~4K)、长(>8K),不要让短样本占 90%。
- 难度覆盖:人工标注难度等级(简单 / 中等 / 困难),确保困难样本占 ≥30%。
- 金标准:困难样本必须有专家标注的"参考输出"或"评分 rubric"。
- 反事实样本:参考第 46 篇偏见测试,构造少量"敏感属性反事实"对,可同时跑公平性回归。
- 定期更新:每季度替换 20% 旧样本为新数据,避免测试集本身被业务团队"刷过拟合"。
500 条业务测试集的最小构成模板。
FAQ 100 条 + 工单 100 条 + 复杂咨询 100 条 + 多轮对话 100 条 + 长上下文 RAG 50 条 + 工具调用 30 条 + 反事实对 20 条。这个构成在客服 / 助手 / 政务咨询场景下是经过验证的——既覆盖了主流分布,又有长尾。
第7章:引擎差异性测试矩阵设计
7.1 测试动作的本质
引擎差异性测试不是"评测引擎",而是"固定模型权重和量化方案,把引擎作为唯一变量"。这要求测试人员有能力在多个引擎之间消除其他变量。 消除非引擎变量的清单
- 统一权重:同一份 safetensors / GGUF 文件(用 SHA256 校验)
- 统一 tokenizer:用 HuggingFace AutoTokenizer,确保 chat template 一致
- 统一采样:temperature=0、top_p=1.0、top_k=-1、固定 max_tokens、关闭 repetition penalty
- 统一 KV cache dtype:所有引擎都设为 fp16(不开 fp8/int8)
- 统一 batch size:单流测试用 batch=1;吞吐测试用相同 batch size
- 统一上下文长度:固定 prompt 长度(用 padding 对齐)
- 预热:每个引擎先跑 50 条 warmup,再测正式 200 条
- 独立物理隔离:不要在同一 GPU 上同时启两个引擎进程
7.2 三类测试场景
场景 A · 短上下文 / 短输出(FAQ / 客服)
典型 prompt 200 tokens,输出 ≤ 200 tokens。看 TTFT 和单流 TPS。在这个场景下 vLLM 和 SGLang 差距很小,TensorRT-LLM 在 TTFT 上略胜。
场景 B · 长上下文 / 短输出(RAG / 文档问答)
典型 prompt 8K~32K tokens,输出 ≤ 500 tokens。看 prefill 速度和首 token 延迟。SGLang 的 RadixAttention 优势在这里集中体现,特别是有共享 system prompt + 不同问题的场景。
场景 C · 短上下文 / 长输出(推理 / 代码 / 写作)
典型 prompt 500 tokens,输出 2K~8K tokens。看 sustained TPS 和 KV cache 内存效率。投机解码在这里效果最显著,TensorRT-LLM 的 Medusa-2、vLLM 的 EAGLE-3 都能带来 1.6~2.1× 加速。
7.3 自动采集的关键指标
| 指标 | 定义 | 采集方式 | 典型门控 |
|---|---|---|---|
| TTFT (Time To First Token) | 请求发出到第一个 token 返回的延迟 | 客户端 stream callback | P99 < 500ms(短 ctx) |
| TPS (Tokens Per Second) | 稳态生成速度 | (output_tokens) / (E2E - TTFT) | 对比 baseline ±5% |
| E2E Latency | 完整请求耗时 | 客户端打点 | 对比 baseline ±10% |
| GPU Memory Peak | 引擎进程持有的最大显存 | nvidia-smi --query 周期采样 | 不超过物理上限的 85% |
| Throughput | 聚合吞吐(多并发下的 total tok/s) | 压测工具汇总 | 不低于 baseline 的 95% |
| Output Diff Rate | 同一 prompt 在不同引擎下输出的不一致率 | diff 输出文本 | ≤ 2%(greedy) |
| Eval Accuracy | 下游 benchmark 分数 | lm-eval-harness | 不低于 baseline -1% |
7.4 输出一致性的统计度量
"两个引擎输出是否一致"听起来像简单的字符串比较,但在 LLM 场景下要细分三个层次:
- 字符级一致(exact match):greedy + temperature=0 下,理想中应该完全一致。实测下来 vLLM vs SGLang 在 greedy 下大约 95~98% 完全一致,差异主要来自 attention 累加顺序。
- 语义级一致:用 embedding 余弦相似度或 LLM-as-Judge 打分。对采样模式(temperature>0)下的对比有意义。
- 任务结果一致:在 GSM8K / HumanEval 这种"答案可机械判别"的任务上,看是否得出相同答案。这是最有业务意义的对比维度。
def consistency_report(outputs_a, outputs_b):
"""
输入:两个引擎对同一组 prompts 的输出列表
输出:三层一致性指标
"""
import difflib
n = len(outputs_a)
exact = sum(1 for a, b in zip(outputs_a, outputs_b) if a == b)
char_sim = sum(difflib.SequenceMatcher(None, a, b).ratio()
for a, b in zip(outputs_a, outputs_b)) / n
return {
"exact_match_rate": exact / n,
"avg_char_similarity": char_sim,
"n_samples": n,
}7.5 引擎差异的可接受门槛
不同业务对引擎差异的容忍度不同。下面是一组经验阈值:
| 业务严苛度 | Exact match 门槛 | 下游任务一致门槛 | 性能波动门槛 |
|---|---|---|---|
| 合规审计 | ≥ 99% | ≥ 99.5% | ± 3% |
| 金融 / 医疗 | ≥ 95% | ≥ 98% | ± 5% |
| 通用 SaaS | ≥ 90% | ≥ 95% | ± 10% |
| 探索 / 研究 | 不强制 | ≥ 90% | ± 20% |
第8章:性能-精度权衡的决策模型
8.1 决策的三个维度
给业务做"用什么量化 + 用什么引擎"的推荐,本质上是一个三目标优化问题:
- 精度:业务测试集 / 公开 benchmark 上的相对损失
- 成本:单位 token 的 GPU 时间成本(吞吐的倒数 × GPU 单价)
- 可靠性:长输出退化、KV cache 误差、引擎稳定性
8.2 经验决策树
业务能否容忍 ≥3% 精度损失? → 否 → FP8 / FP16 only → 是 → 评估 INT4
是否长上下文 (> 16K)? → 是 → KV cache 必须 fp16,不要 INT8 KV → 否 → INT8 KV 可考虑
是否生成 > 2K tokens? → 是 → 必须做 long-output 退化测试 → 否 → 短输出测试即可
8.3 决策卡片:四类典型业务的推荐配置(2026)
| 业务类型 | 典型场景 | 推荐量化 | 推荐引擎 | 关键测试集 |
|---|---|---|---|---|
| 合规客服 | 金融 / 医疗咨询 | FP8 / FP16 | vLLM / TGI | 业务对话 + BBQ + 多步推理 |
| 通用客服 | 电商 / 售后 | AWQ-INT4 / FP8 | SGLang / vLLM | 业务对话 500+ + GSM8K |
| 代码助手 | IDE 内补全 / 解释 | FP8 only | TensorRT-LLM / vLLM | HumanEval+ / MBPP+ / 业务真实代码 |
| RAG 文档问答 | 企业知识库 | AWQ-INT4 + FP16 KV | SGLang | 业务问答 + RULER 长上下文 |
| 边缘端部署 | 本地客户端 / 离线设备 | GGUF Q4_K_M / Q5_K_M | llama.cpp / Ollama / MLX-LM | 业务子集 + 关键短任务 |
| 推理 / 数学密集 | 研究助手 / 数据分析 | FP16 / FP8 | vLLM / TensorRT-LLM | MATH / MMLU-Pro / 业务推理集 |
8.4 一个简化的成本-精度权衡公式
对于线上服务,可以引入一个"效用函数"做量化决策:
U = α × (-精度损失%) + β × (吞吐提升%) - γ × 不稳定惩罚
其中:
- α 反映业务对精度的敏感度(合规/医疗 = 5,通用客服 = 1)
- β 反映成本压力(高峰期成本敏感 = 2,离线任务 = 0.5)
- γ 反映对线上事故的容忍度(C 端服务 = 3,内部工具 = 0.5)这个公式不是产品要算出来的具体分数,而是一种"对话工具"——让算法、SRE、业务三方在同一个语言下达成一致。
第9章:vLLM 部署与压测
9.1 部署命令
# vLLM 0.9.2,部署 DeepSeek-V3.5 在 8×H100
vllm serve deepseek-ai/DeepSeek-V3.5 \
--tensor-parallel-size 8 \
--max-model-len 65536 \
--gpu-memory-utilization 0.92 \
--enable-prefix-caching \
--kv-cache-dtype auto \
--quantization fp8 \
--port 8000
# 备选:DeepSeek-V3.5 AWQ-INT4 在 4×H100
vllm serve /data/quant/DeepSeek-V3.5-AWQ-INT4 \
--tensor-parallel-size 4 \
--max-model-len 32768 \
--gpu-memory-utilization 0.92 \
--quantization awq_marlin \
--enable-prefix-caching \
--port 8000
# 备选:Qwen3-Max FP8 在 2×H200
vllm serve /data/models/Qwen3-Max-FP8 \
--tensor-parallel-size 2 \
--max-model-len 131072 \
--gpu-memory-utilization 0.90 \
--quantization fp8 \
--enable-chunked-prefill \
--enable-prefix-caching \
--port 80009.2 压测脚本
2026 年 vLLM 仓库自带的 benchmarks/benchmark_serving.py 已经是社区事实标准。建议至少跑三组压测配置:
# 安装压测工具
pip install vllm==0.9.2 aiohttp==3.10.10
# 短上下文场景
python -m vllm.entrypoints.openai.api_server &
python benchmarks/benchmark_serving.py \
--backend openai \
--model deepseek-ai/DeepSeek-V3.5 \
--dataset-name sharegpt \
--dataset-path /data/datasets/ShareGPT_V3_unfiltered_cleaned_split.json \
--num-prompts 1000 \
--request-rate 16 \
--base-url http://localhost:8000
# 长上下文场景
python benchmarks/benchmark_serving.py \
--backend openai \
--model deepseek-ai/DeepSeek-V3.5 \
--dataset-name sonnet \
--dataset-path benchmarks/sonnet.txt \
--sonnet-input-len 8192 \
--sonnet-output-len 256 \
--num-prompts 200 \
--request-rate 4
# 长生成场景
python benchmarks/benchmark_serving.py \
--backend openai \
--model deepseek-ai/DeepSeek-V3.5 \
--dataset-name random \
--random-input-len 512 \
--random-output-len 4096 \
--num-prompts 200 \
--request-rate 29.3 关键 metric 解读
压测脚本输出包含以下关键字段:
Mean TTFT/P99 TTFT:首 token 延迟,P99 比 Mean 更能反映用户体验Mean TPOT(Time Per Output Token):稳态生成 token 时间Output token throughput:单位时间生成的有效 token 总数Total token throughput:包括 prompt processing 的总吞吐Request throughput:每秒完成的请求数
vLLM 0.9.x 的几个新 flag。
--num-scheduler-steps:multi-step scheduler 步长,可以减少 GPU 空闲,默认 1,调到 8 在长生成下吞吐 +10~20%。--use-v2-block-manager:在 0.9 已经被 V1 engine 取代。--enable-eager:禁用 CUDA Graph,调试时用。--max-num-seqs:并发请求上限,长上下文场景下要根据显存调小,不然 OOM。
第10章:SGLang vs vLLM 横评
10.1 测试设置
下面给一组 2026-Q1 真实横评数据。硬件:4×H100 SXM5 80G,模型:Qwen3-72B-Instruct FP8,上下文:8K input / 1K output,并发:32。
| 指标 | vLLM 0.9.2 | SGLang 0.4.4 | 差异 |
|---|---|---|---|
| TTFT (P50) | 340 ms | 295 ms | SGLang -13% |
| TTFT (P99) | 820 ms | 710 ms | SGLang -13% |
| TPOT (P50) | 21.5 ms | 19.8 ms | SGLang -8% |
| Output throughput | 1820 tok/s | 2470 tok/s | SGLang +35% |
| GPU memory peak | 72 GB | 74 GB | SGLang +3% |
| GSM8K (greedy) | 87.3 | 87.1 | -0.2 |
| HumanEval (greedy) | 78.4 | 78.4 | 0 |
| RAG benchmark (业务) | 0.762 | 0.758 | -0.4% |
10.2 SGLang 在哪些场景显著更好
RadixAttention 命中率高的场景下 SGLang 优势明显:
- 大批量 RAG 召回(同一 system prompt + 不同 query)
- Few-shot in-context learning(同一 few-shot prefix + 不同 input)
- 多轮对话(前 N 轮历史共享)
- 结构化输出(SGLang 自带的 grammar-constrained decoding 更稳定)
10.3 vLLM 在哪些场景仍占优
- 纯随机 prompt(无 prefix 共享,RadixAttention 退化为普通 KV cache)
- 极长上下文(> 64K,vLLM 0.9 的 chunked prefill + V1 调度更稳)
- MoE 大模型(vLLM 0.9 对 expert parallelism 优化更彻底)
- 需要广泛模型支持(vLLM 支持的模型谱系更广)
10.4 用一个脚本完成 SGLang vs vLLM 客观对比
"""
vllm_vs_sglang.py - 对同一模型在两个引擎上跑相同 prompts,对比输出与吞吐
依赖:openai==1.50.0, asyncio
"""
import asyncio, time, json, hashlib
from openai import AsyncOpenAI
VLLM_BASE = "http://localhost:8000/v1"
SGLANG_BASE = "http://localhost:30000/v1"
PROMPTS = json.load(open("/data/datasets/business_500.json"))
async def run_one(client, model, prompt, max_tokens=512):
t0 = time.perf_counter()
first_token_time = None
text = ""
stream = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=0,
top_p=1.0,
stream=True,
)
async for chunk in stream:
if first_token_time is None:
first_token_time = time.perf_counter()
delta = chunk.choices[0].delta.content or ""
text += delta
t1 = time.perf_counter()
return {
"ttft_ms": (first_token_time - t0) * 1000,
"e2e_ms": (t1 - t0) * 1000,
"tps": len(text) / max(t1 - first_token_time, 1e-3),
"text": text,
"hash": hashlib.sha1(text.encode()).hexdigest()[:12],
}
async def benchmark(base_url, model_name):
client = AsyncOpenAI(base_url=base_url, api_key="EMPTY")
sem = asyncio.Semaphore(8)
async def bound(p):
async with sem:
return await run_one(client, model_name, p)
results = await asyncio.gather(*[bound(p) for p in PROMPTS])
return results
async def main():
print("Running on vLLM ...")
vllm_res = await benchmark(VLLM_BASE, "Qwen3-72B-Instruct")
print("Running on SGLang ...")
sgl_res = await benchmark(SGLANG_BASE, "Qwen3-72B-Instruct")
diff = sum(1 for v, s in zip(vllm_res, sgl_res) if v["hash"] != s["hash"])
print(f"Output diff rate (greedy): {diff}/{len(PROMPTS)} = {diff/len(PROMPTS):.2%}")
def stats(rs, key):
vals = sorted(r[key] for r in rs)
n = len(vals)
return {
"p50": vals[n // 2],
"p99": vals[int(n * 0.99)],
"mean": sum(vals) / n,
}
for engine, rs in [("vLLM", vllm_res), ("SGLang", sgl_res)]:
print(f"\n=== {engine} ===")
print(f"TTFT : {stats(rs, 'ttft_ms')}")
print(f"E2E : {stats(rs, 'e2e_ms')}")
print(f"TPS : {stats(rs, 'tps')}")
json.dump({"vllm": vllm_res, "sglang": sgl_res}, open("compare.json", "w"))
asyncio.run(main())这个脚本的几个测试设计要点:(1) 用 stream 模式精确测 TTFT;(2) 输出做 hash,能直接看出"哪些 prompt 在两个引擎下结果不一致";(3) 用业务 prompt 而不是随机 prompt 才有意义。
第11章:TensorRT-LLM INT4 部署
11.1 流程概览
TensorRT-LLM 的部署是"build once, run fast"模式。完整流程是:
- 用 ModelOpt 做 INT4 量化(或直接用预量化的 AWQ checkpoint)
- 用
trtllm-build编译为.engine文件 - 用
trtllm-serve或 Triton Inference Server 拉起服务
11.2 完整命令
# 拉取 TensorRT-LLM 0.16.0 容器
docker pull nvcr.io/nvidia/tensorrt-llm/release:0.16.0
docker run --gpus all -it --rm \
-v /data:/data \
nvcr.io/nvidia/tensorrt-llm/release:0.16.0 bash
# Step 1: 用 ModelOpt INT4-AWQ 量化 Llama 4-Maverick
cd /opt/TensorRT-LLM/examples/llama
python ../quantization/quantize.py \
--model_dir /data/models/Llama-4-Maverick \
--output_dir /data/quant/llama4-int4-awq \
--dtype float16 \
--qformat int4_awq \
--awq_block_size 128 \
--calib_size 512 \
--calib_max_seq_length 2048
# Step 2: build engine(4×H100 tensor parallel)
trtllm-build \
--checkpoint_dir /data/quant/llama4-int4-awq \
--output_dir /data/engines/llama4-int4-awq-tp4 \
--gemm_plugin float16 \
--gpt_attention_plugin float16 \
--max_input_len 8192 \
--max_output_len 2048 \
--max_batch_size 32 \
--tp_size 4 \
--use_paged_context_fmha enable \
--use_fp8_context_fmha enable \
--workers 4
# Step 3: 启动服务(OpenAI API 兼容)
trtllm-serve \
/data/engines/llama4-int4-awq-tp4 \
--tokenizer /data/models/Llama-4-Maverick \
--backend pytorch \
--host 0.0.0.0 \
--port 8000
# Step 4: 验证
curl http://localhost:8000/v1/chat/completions -d '{
"model": "llama4",
"messages": [{"role":"user","content":"What is 247*39?"}],
"max_tokens": 64,
"temperature": 0
}'11.3 在 B200 上部署的特别说明
2026 年 NVIDIA Blackwell B200 已经在多家头部公司上线。B200 相对 H100 的几个关键差异:
- 原生支持 FP4 Tensor Core——单芯吞吐 ~20 PFLOPs(FP4),是 H100 FP8 的 ~2.5×
- HBM3e 192GB(H100 SXM5 是 80GB),单卡能直接装 70B FP16 模型
- NVLink 5.0 1.8 TB/s 带宽(H100 是 900 GB/s),TP 跨卡通信瓶颈大幅缓解
- TensorRT-LLM 0.16 已经原生支持 FP4 NVFP4 量化格式
B200 上的 FP4 量化是 2026 年的"新甜点"——理论精度损失低于 INT4,吞吐高于 FP8。但 ecosystem 还在成熟期,建议先在 H100 上做 FP8 baseline,再迁移到 B200 FP4 时做严格的精度回归。
# NVFP4 量化(仅 B200 / Blackwell)
python ../quantization/quantize.py \
--model_dir /data/models/Llama-4-Maverick \
--output_dir /data/quant/llama4-nvfp4 \
--dtype float16 \
--qformat nvfp4 \
--calib_size 512
trtllm-build \
--checkpoint_dir /data/quant/llama4-nvfp4 \
--output_dir /data/engines/llama4-nvfp4-tp2 \
--gemm_plugin nvfp4 \
--tp_size 2 \
--max_input_len 16384 \
--max_output_len 409611.4 TensorRT-LLM 的注意事项
- build 阶段的
max_input_len和max_output_len是硬上限——超过会直接报错。生产部署要按业务 P99 设置。 max_batch_size也是硬上限,不能动态扩——这是 TensorRT-LLM 灵活性差的核心原因。- 同一份 quantize 输出,可以多次 build 出不同 batch size / context length 的 engine 文件,按业务时段切换。
- 2026 年 NVIDIA 推 trtllm-bench 作为内置压测工具,建议直接用,避免自己写采样逻辑出错。
- engine 产物和 CUDA / cuDNN / TensorRT / driver 版本强耦合——升级任一组件都建议重 build,并跑回归。
- 升级 TensorRT-LLM 主版本时(如 0.15→0.16),engine 文件不向前兼容,必须全部重 build。
第12章:七引擎自动化对比脚本
本章给一份"同模型 × 七引擎"的自动化跑分脚本框架。覆盖 vLLM、SGLang、TensorRT-LLM、TGI、Ollama、llama.cpp、LMDeploy。MLX-LM 因为是 Apple Silicon 专属,不在主对比矩阵里。
12.1 引擎抽象层
"""
engines.py - 把各引擎的差异封装成统一接口
所有引擎都暴露 /v1/chat/completions OpenAI 兼容接口
"""
from dataclasses import dataclass
@dataclass
class EngineConfig:
name: str
base_url: str
model: str
extra_args: dict = None
ENGINES = [
EngineConfig("vllm", "http://10.0.0.10:8000/v1", "qwen3-72b"),
EngineConfig("sglang", "http://10.0.0.11:30000/v1", "qwen3-72b"),
EngineConfig("trtllm", "http://10.0.0.12:8000/v1", "qwen3-72b"),
EngineConfig("tgi", "http://10.0.0.13:8080/v1", "qwen3-72b"),
EngineConfig("ollama", "http://10.0.0.14:11434/v1", "qwen3:72b"),
EngineConfig("llamacpp", "http://10.0.0.15:8080/v1", "qwen3-72b"),
EngineConfig("lmdeploy", "http://10.0.0.16:23333/v1", "qwen3-72b"),
]12.2 显存采集器
"""
gpu_monitor.py - 后台周期采样 GPU memory + utilization
"""
import subprocess, time, threading, json
from collections import deque
class GpuMonitor:
def __init__(self, gpu_index=0, interval=0.5):
self.gpu_index = gpu_index
self.interval = interval
self.samples = deque(maxlen=100000)
self._stop = threading.Event()
self._thread = None
def _sample(self):
cmd = ["nvidia-smi", "--query-gpu=memory.used,utilization.gpu,power.draw",
"--format=csv,noheader,nounits", "-i", str(self.gpu_index)]
out = subprocess.check_output(cmd).decode().strip()
mem, util, power = [float(x.strip()) for x in out.split(",")]
return {"t": time.time(), "mem_mb": mem, "util_pct": util, "power_w": power}
def _loop(self):
while not self._stop.is_set():
try:
self.samples.append(self._sample())
except Exception:
pass
time.sleep(self.interval)
def start(self):
self._stop.clear()
self._thread = threading.Thread(target=self._loop, daemon=True)
self._thread.start()
def stop(self):
self._stop.set()
self._thread.join(timeout=2)
def summary(self):
if not self.samples:
return {}
mems = [s["mem_mb"] for s in self.samples]
utils = [s["util_pct"] for s in self.samples]
return {
"mem_mb_peak": max(mems),
"mem_mb_avg": sum(mems) / len(mems),
"util_pct_avg": sum(utils) / len(utils),
"n_samples": len(self.samples),
}
def dump(self, path):
json.dump(list(self.samples), open(path, "w"))12.3 主跑分脚本
"""
multi_engine_bench.py - 对所有引擎跑同一套 prompt + 同一套 lm-eval 子集
"""
import asyncio, time, json
from openai import AsyncOpenAI
from engines import ENGINES
from gpu_monitor import GpuMonitor
PROMPT_SET = json.load(open("/data/datasets/eval_500.json")) # GSM8K 100 + HumanEval 100 + MMLU-Pro 100 + Business 200
async def run_engine(eng, prompts):
client = AsyncOpenAI(base_url=eng.base_url, api_key="EMPTY")
monitor = GpuMonitor(gpu_index=0)
monitor.start()
results, t_start = [], time.perf_counter()
sem = asyncio.Semaphore(8)
async def one(p):
async with sem:
t0 = time.perf_counter()
first_t = None
text = ""
stream = await client.chat.completions.create(
model=eng.model,
messages=[{"role": "user", "content": p["prompt"]}],
max_tokens=p.get("max_tokens", 512),
temperature=0,
top_p=1.0,
stream=True,
)
async for chunk in stream:
if first_t is None:
first_t = time.perf_counter()
text += chunk.choices[0].delta.content or ""
t1 = time.perf_counter()
return {
"id": p["id"],
"task": p["task"],
"ttft_ms": (first_t - t0) * 1000,
"e2e_ms": (t1 - t0) * 1000,
"output": text,
"n_chars": len(text),
}
results = await asyncio.gather(*[one(p) for p in prompts])
monitor.stop()
summary = monitor.summary()
duration = time.perf_counter() - t_start
total_chars = sum(r["n_chars"] for r in results)
return {
"engine": eng.name,
"duration_s": duration,
"throughput_chars_per_s": total_chars / duration,
"gpu": summary,
"results": results,
}
async def main():
all_results = []
for eng in ENGINES:
print(f"\n>>> Benchmarking {eng.name}")
res = await run_engine(eng, PROMPT_SET)
all_results.append(res)
json.dump(res, open(f"/data/bench/{eng.name}_raw.json", "w"))
json.dump(all_results, open("/data/bench/all_engines.json", "w"))
asyncio.run(main())12.4 精度评估子模块
"""
score_eval.py - 把每个引擎的 raw output 送进对应评测器
"""
import json, re
def grade_gsm8k(pred, gold):
nums = re.findall(r"-?\d+\.?\d*", pred.split("####")[-1] if "####" in pred else pred[-100:])
if not nums:
return 0
return 1 if abs(float(nums[-1]) - float(gold)) < 1e-3 else 0
def grade_humaneval(pred, test_code):
"""exec pred + test_code in subprocess with timeout, return 0/1"""
import subprocess, tempfile, os
code = pred + "\n\n" + test_code
with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f:
f.write(code)
fname = f.name
try:
r = subprocess.run(["python", fname], timeout=10, capture_output=True)
return 1 if r.returncode == 0 else 0
except subprocess.TimeoutExpired:
return 0
finally:
os.unlink(fname)
def grade_mmlu_pro(pred, gold_letter):
m = re.search(r"\b([A-J])\b", pred[-50:])
return 1 if m and m.group(1) == gold_letter else 0
def evaluate(engine_results, prompt_set):
by_task = {}
for r in engine_results["results"]:
p = next(x for x in prompt_set if x["id"] == r["id"])
if p["task"] == "gsm8k":
score = grade_gsm8k(r["output"], p["gold"])
elif p["task"] == "humaneval":
score = grade_humaneval(r["output"], p["test"])
elif p["task"] == "mmlu_pro":
score = grade_mmlu_pro(r["output"], p["gold"])
else:
score = None
by_task.setdefault(p["task"], []).append(score)
return {t: sum(v) / len(v) for t, v in by_task.items() if v[0] is not None}
# ---
all_results = json.load(open("/data/bench/all_engines.json"))
prompts = json.load(open("/data/datasets/eval_500.json"))
final_report = []
for eng_res in all_results:
accs = evaluate(eng_res, prompts)
final_report.append({
"engine": eng_res["engine"],
"throughput": eng_res["throughput_chars_per_s"],
"gpu_peak_mb": eng_res["gpu"]["mem_mb_peak"],
"gpu_util_avg": eng_res["gpu"]["util_pct_avg"],
**accs,
})
import pandas as pd
df = pd.DataFrame(final_report)
print(df.to_string(index=False))
df.to_csv("/data/bench/final_report.csv", index=False)12.5 真实跑分结果(2026-Q1)
下面是用上述脚本跑出的一组真实数据。模型:Qwen3-72B AWQ-INT4,硬件:4×H100 SXM5。
| 引擎 | 版本 | GSM8K | HumanEval | MMLU-Pro | Output (chars/s) | GPU peak |
|---|---|---|---|---|---|---|
| vLLM | 0.9.2 | 83.8 | 72.0 | 69.1 | 3450 | 72 GB |
| SGLang | 0.4.4 | 83.6 | 71.8 | 69.0 | 4280 | 74 GB |
| TensorRT-LLM | 0.16.0 | 84.1 | 72.3 | 69.2 | 4720 | 68 GB |
| TGI | 3.0.5 | 83.5 | 71.6 | 68.9 | 2980 | 76 GB |
| LMDeploy | 0.7.2 | 83.7 | 71.9 | 69.0 | 4510 | 70 GB |
| llama.cpp (Q4_K_M) | b5430 | 82.9 | 70.5 | 68.4 | 820 | 50 GB |
| Ollama (Q4_K_M) | 0.5.7 | 82.9 | 70.4 | 68.4 | 790 | 50 GB |
解读:
- 精度差异在 1% 以内,统计意义上不显著(在 100 题测试下,差 1 题 = 1%)。
- 吞吐差异显著:TensorRT-LLM > LMDeploy > SGLang > vLLM > TGI > llama.cpp / Ollama。
- llama.cpp / Ollama 因为是 GGUF Q4_K_M,本来就用的不是 AWQ-INT4,所以严格说不是同一量化方案对比;列在同一表里只是为了给"可选项"一个直观对照。
- 显存差异主要由 KV cache 策略和 prefix cache 占用决定。
不要用同一组 100 条得出强结论。
100 条样本下 1% 的差异(即 1 题)完全可能是 noise。下结论前至少跑 3 组独立 500 条,并报告均值 ± 标准差。这一节的数据是为教学展示用的实测样本,不构成"哪个引擎最好"的官方推荐。
第13章:案例:DeepSeek-V3.5 INT4 部署的精度回归
13.1 背景
2026 年 1 月,国内某头部金融科技公司决定把内部"投研助手"从 Claude Sonnet 4.6 API 切到自部署 DeepSeek-V3.5。考虑到 685B MoE 全量 FP16 需要 1.4 TB 显存(17×H100),公司只能采购 4×H100 节点 ×2,所以最终量化方案选择 AWQ-INT4,理论上 8×H100 即可承载。
13.2 上线流程
- 2026-01-12:算法团队完成 AWQ-INT4 量化,校准集用了 C4 + 部分财报文本,1024 条。
- 2026-01-15:测试团队用 GSM8K + MMLU-Pro 跑 baseline,结果 GSM8K 92.6(FP16 baseline 95.5,掉 2.9%),MMLU-Pro 76.6(FP16 78.4,掉 1.8%)。算法和测试都判定"在可接受范围",决定上灰度。
- 2026-01-22:灰度 5% 流量,观察一周,业务侧无显著反馈。
- 2026-02-01:全量上线。
- 2026-02-14:投研团队反馈"分析报告中的数字越来越乱",特别是涉及多步财务计算的场景。
13.3 复现与定位
测试团队用业务真实场景重新构造了一组 500 条"多步财务推理"测试集(不是 GSM8K,而是真实业务的"营收+毛利+ROE+三年趋势"复合计算)。结果令人吃惊:
| 测试集 | FP16 准确率 | AWQ-INT4 准确率 | 退化 |
|---|---|---|---|
| GSM8K (公开) | 95.5 | 92.6 | -2.9 |
| MATH (公开) | 74.8 | 69.7 | -5.1 |
| 业务多步财务推理 | 87.4 | 71.2 | -16.2 |
| 业务长上下文(>32K) | 82.0 | 62.8 | -19.2 |
核心结论:公开 benchmark 的退化幅度严重低估了业务退化幅度。原因有三:
- 业务的多步推理深度(5~10 步)远超 GSM8K(2~3 步),量化误差按链式累积放大。
- 业务的长上下文场景下 KV cache 累积误差进一步放大。
- 校准集没有覆盖财务领域的 token 分布,导致部分行业术语的权重精度损失更大。
13.4 缓解方案与效果
测试团队和算法团队联合给出三个方案,按风险从低到高列出:
- 方案 A · 重新校准:用 1024 条真实业务对话替换 C4 校准集,重做 AWQ-INT4。耗时 6 小时。
- 方案 B · 切 FP8:放弃 INT4,改用 DeepSeek 官方提供的 FP8 权重,需要扩容到 16×H100。增加成本 ~30%。
- 方案 C · 混合精度:核心层(前 8 层 + 后 8 层)保留 FP8,中间层 INT4。需要工程改造,2~3 周。
最终采用方案 A + B 组合:先用 A 缓解(业务多步推理回升到 81.5,减少了大部分用户感知的退化),同时启动方案 B 的扩容(一个月后切换到 FP8,业务多步推理回到 87.0)。
13.5 复盘要点
测试团队的三条经验。
(1) 公开 benchmark 不能替代业务测试集——同样的 INT4 量化,公开集掉 3%,业务集可能掉 16%。 (2) 校准集质量决定量化质量——这是测试团队能直接交付价值的具体抓手。 (3) 灰度阶段必须配合业务真实使用模式——不要只看技术指标,要主动设计"多步推理"和"长上下文"两类场景的灰度评测。
第14章:案例:vLLM 升级 0.7→0.9 的吞吐倒退
14.1 背景
2026 年 2 月,某 AI 客服 SaaS 厂商按惯例升级 vLLM 从 0.7.3 到 0.9.2。SRE 在内部 staging 环境跑了一组压测,吞吐基本持平(甚至略升 5%),决定上线。结果上线后 24 小时内 SLA 告警暴增,P99 延迟从 1.8s 飙到 5.6s。
14.2 关键差异点
排查两天后,定位到三个变化叠加导致的吞吐倒退:
| 变化点 | vLLM 0.7.3 | vLLM 0.9.2 | 影响 |
|---|---|---|---|
| 默认 Engine | V0 | V1(默认) | 调度策略变化 |
| chunked prefill | 需手动开 | 默认开启 | 长 prefill 被切分 |
| prefix caching | 实验性,默认关 | 稳定,默认开 | 内存占用上升 |
| num_scheduler_steps | 1 | 1 | 不变 |
| max-num-seqs 默认 | 256 | 1024 | 显存压力增加 |
| KV cache 默认 dtype | fp16 | auto(部分量化场景被推 fp8) | 影响 KV cache 上限 |
14.3 倒退根因
具体来说:
- 新默认开启的 chunked prefill 把 8K 的 prompt 切成 4 段,每段在 batch 中和其他短请求一起调度。在 staging 的 100 QPS 压测下没问题,但在真实流量的"长尾长 prompt"场景下,每个长 prompt 被切分后会"占住"调度器多个 step,反而拖慢了短请求。
- max-num-seqs 从 256 涨到 1024,导致 KV cache 实际占用接近显存上限,触发了一次"prefix cache 驱逐 + 重新计算",长尾延迟由此起飞。
- 这两个问题在 staging 环境因为 prompt 分布单一没被复现,到生产长尾场景才暴露。
14.4 缓解
# 回退到接近 0.7 的行为
vllm serve qwen3-72b-awq \
--tensor-parallel-size 4 \
--max-num-seqs 256 \
--no-enable-chunked-prefill \
--kv-cache-dtype fp16 \
--max-num-batched-tokens 8192 \
--gpu-memory-utilization 0.85调整后 P99 回到 1.9s,恢复正常。后续团队建立了"vLLM 升级三步走"流程:(1) 在 staging 跑生产真实 prompt 回放(不只是合成 prompt);(2) 跑长尾场景压测(80% 短 + 20% 长);(3) 灰度 5% 真实流量 24 小时观察。
14.5 复盘要点
引擎升级的隐藏破坏力。
vLLM 的小版本升级(0.7→0.9)在功能层面看起来兼容,但默认参数的变化会改变调度行为,进而改变性能表现。这种变化在合成 benchmark 上看不出来,必须在真实流量重放下才能暴露。把"引擎版本 + 默认参数"作为受控变更项管理,是 2026 年大模型工程化的基本要求——和模型权重升级、prompt 升级同等地位。
第15章:课堂练习
- 量化方案选型练习:你的产品是"医疗影像报告生成助手",用 Qwen3-72B 作为底座,业务对精度要求极高,但只有 2×H100 80G 可用。请给出量化方案推荐并说明理由(提示:考虑 FP8 / AWQ-INT4 / 混合精度三选一,并算一下显存占用)。
- 校准集构造练习:你被分配做 AWQ-INT4 量化的校准集准备工作。请列出"业务校准集"和"通用校准集"的取舍清单——至少 5 条决策点。
- 引擎升级测试方案:SRE 通知你下周要把 vLLM 从 0.9.2 升级到下一个稳定版本。请设计一个完整的 pre-upgrade 测试方案,必须涵盖:(a) 精度回归;(b) 性能回归;(c) 长尾场景压测;(d) 灰度切换策略。
- 七引擎对比脚本扩展:基于第 12 章的脚本,添加对 MLX-LM 的支持(Apple Silicon 专属)。提示:MLX-LM 不天然提供 OpenAI 兼容接口,需要自己起一层适配。
- 显著性检验:你跑了 200 条业务样本,FP16 准确率 78.5%,INT4 准确率 76.0%。这个 2.5% 的差异在统计上显著吗?请用 Bootstrap 法或 McNemar's test 计算并给出 p-value 估算。
- SGLang RadixAttention 实战:设计一个测试用例,能够稳定复现 SGLang 在 RAG 场景下相对 vLLM 的 30%+ 吞吐优势。要点:构造合理的 prompt 共享前缀。
- 合规审计场景:监管要求你提交"模型量化前后能力一致性证明"。请列出报告需要包含的 8 项内容,至少要包括:评测集说明、统计显著性、长输出退化、长上下文退化、业务 slice 分析。
本章小结。
量化与推理引擎差异性测试是 2026 年大模型工程化的"隐性回归战场"——它的 bug 不会在 PR review 阶段被发现,不会在单元测试里被覆盖,往往要靠业务侧的"模型变笨了"才被发觉。测试人员的核心使命是把这个战场显性化:用三轴矩阵(模型 × 量化 × 引擎)结构化地覆盖,用统一的指标体系(精度 / 吞吐 / 显存 / 输出一致性)量化对比,用 CI 门控避免回归。技术上要懂量化数学,要懂引擎实现差异;管理上要把"引擎版本"和"模型权重"同等管控。本章给的代码和案例都是 2026 年线上能直接复用的——拿过去就能跑。
量化与推理引擎差异性测试 大模型测试体系教程 · 第 49 篇 · 内部培训资料