Skip to content

量化与推理引擎差异性测试

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×。这个动作本身在数学上有三个不可避免的代价:

  1. 截断误差:分布的两端尾部会被强行截断到 [-7, 7] 区间,而 LLM 中正是少数极端权重承担着关键语义。
  2. 分辨率损失:本来连续的权重值现在只能落到 16 个离散点上,attention 中的细微数值差异被磨平。
  3. 累积误差:一个 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 = 0

Step 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_K2.623 GB-8 ~ -12%极度受限设备
Q3_K_M3.530 GB-3 ~ -5%低端边缘设备
Q4_K_M4.540 GB-1 ~ -2%主流推荐
Q5_K_M5.548 GB-0.4 ~ -0.8%精度敏感
Q6_K6.656 GB-0.1 ~ -0.3%近似 FP16
Q8_08.572 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。

bash
pip install autoawq==0.2.7 transformers==4.46.0 accelerate==1.0.1
pip install datasets==3.0.1
python
import 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 主流版本定位核心特性测试关注点
vLLM0.10.x通用、最常用PagedAttention、Continuous batching、Speculative、FP8、AWQ、GPTQ0.7→0.9→0.10 升级 breaking change 多
SGLang0.5.xRadixAttention 强场景RadixAttention、共享前缀缓存、structured output 一流chunked prefill 默认配置变化频繁
TensorRT-LLM0.18.xNVIDIA 极致性能FP8/INT4/W4A16/MoE 优化、In-flight batchingbuild 一次后产物固化,灵活性差
TGI3.1.xHuggingFace 官方Router 易用、Tool use 接口稳定3.x 重写后部分量化方案不再支持
Ollama0.6.x本地开发首选开箱即用、自动 GGUF 拉取、Modelfile 管理不是生产级吞吐方案
llama.cppb6xxxCPU/边缘端王者纯 C++、最小依赖、Metal/Vulkan/CUDA 多后端不同 GGUF 量化档位的精度差异
MLX-LM0.21.xApple Silicon 唯一选择Apple Metal 原生、统一内存零拷贝M4/M5 上 4-bit 性能/精度
LMDeploy0.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 推荐)

  1. baseline (FP16) 每次测试都重跑,不要拿历史数据当 baseline——上下文 / 评测 harness 版本会影响
  2. 每个量化产物都做 SHA256 校验,避免文件污染
  3. 评测时关 sampling,用 greedy 或 fixed seed temperature=0
  4. 每次测试至少 3 轮,看方差
  5. 每条样本同时记录 input tokens / output tokens / latency / 输出文本
  6. 对长输出任务(HumanEval、MATH)单独看长度分布——量化常见症状是输出变短或重复
  7. 用 Mann-Whitney U test 判定差异显著性(不要只看均值)
  8. 业务测试集做 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 次重跑取均值。

模型方案GSM8KHumanEvalMMLU-Pro显存
DeepSeek-V3.5 685BFP1695.889.078.41370 GB
DeepSeek-V3.5 685BFP8 (Dynamic)95.5 (-0.3)88.6 (-0.4)78.1 (-0.3)685 GB
DeepSeek-V3.5 685BAWQ-INT493.0 (-2.8)85.5 (-3.5)77.0 (-1.4)355 GB
Qwen3-Max 235BFP1692.685.274.3470 GB
Qwen3-Max 235BFP8 (Dynamic)92.3 (-0.3)84.9 (-0.3)74.0 (-0.3)235 GB
Qwen3-Max 235BAWQ-INT488.9 (-3.7)80.4 (-4.8)72.2 (-2.1)132 GB
Llama 4-Maverick 400BFP1690.482.772.8800 GB
Llama 4-Maverick 400BFP8 (Dynamic)90.1 (-0.3)82.3 (-0.4)72.5 (-0.3)400 GB
Llama 4-Maverick 400BGPTQ-INT487.0 (-3.4)78.2 (-4.5)71.0 (-1.8)220 GB
GLM-4.5 (300B)FP1691.583.073.5600 GB
GLM-4.5 (300B)AWQ-INT488.4 (-3.1)78.6 (-4.4)71.7 (-1.8)165 GB
Gemma 3-27BFP1687.276.569.354 GB
Gemma 3-27BFP886.9 (-0.3)76.1 (-0.4)69.0 (-0.3)27 GB
Gemma 3-27BAWQ-INT483.0 (-4.2)71.2 (-5.3)67.4 (-1.9)15 GB
Gemma 3-27BGGUF Q4_K_M83.6 (-3.6)71.8 (-4.7)67.6 (-1.7)16 GB (CPU)

从这张表至少能读出三件事:

  1. FP8 几乎是免费午餐——除非硬件不支持,否则选 FP8 而不是 FP16。
  2. INT4 在数学和代码上必然有显著退化,对这两类任务谨慎使用。
  3. AWQ-INT4 vs GGUF Q4_K_M 在 MMLU 上接近,差距主要在硬件后端而非算法本身。

6.4 业务测试集的构造方法论

"业务测试集"听起来简单,做起来却是测试团队最大的杠杆。一个 500 条的高质量业务测试集,能给量化决策带来比 5000 条公开 benchmark 更大的信号量。 构造步骤:

  1. 抽样源选择:优先从最近 30 天线上日志抽样,避免取太老的数据(用户行为漂移)。
  2. 分层抽样:按业务子场景(FAQ / 工单 / 复杂咨询 / 多轮对话)分层,每层抽 100~200 条。
  3. 长度覆盖:明确覆盖三档长度——短(<512 tok)、中(512~4K)、长(>8K),不要让短样本占 90%。
  4. 难度覆盖:人工标注难度等级(简单 / 中等 / 困难),确保困难样本占 ≥30%。
  5. 金标准:困难样本必须有专家标注的"参考输出"或"评分 rubric"。
  6. 反事实样本:参考第 46 篇偏见测试,构造少量"敏感属性反事实"对,可同时跑公平性回归。
  7. 定期更新:每季度替换 20% 旧样本为新数据,避免测试集本身被业务团队"刷过拟合"。

500 条业务测试集的最小构成模板。

FAQ 100 条 + 工单 100 条 + 复杂咨询 100 条 + 多轮对话 100 条 + 长上下文 RAG 50 条 + 工具调用 30 条 + 反事实对 20 条。这个构成在客服 / 助手 / 政务咨询场景下是经过验证的——既覆盖了主流分布,又有长尾。

第7章:引擎差异性测试矩阵设计

7.1 测试动作的本质

引擎差异性测试不是"评测引擎",而是"固定模型权重和量化方案,把引擎作为唯一变量"。这要求测试人员有能力在多个引擎之间消除其他变量。 消除非引擎变量的清单

  1. 统一权重:同一份 safetensors / GGUF 文件(用 SHA256 校验)
  2. 统一 tokenizer:用 HuggingFace AutoTokenizer,确保 chat template 一致
  3. 统一采样:temperature=0、top_p=1.0、top_k=-1、固定 max_tokens、关闭 repetition penalty
  4. 统一 KV cache dtype:所有引擎都设为 fp16(不开 fp8/int8)
  5. 统一 batch size:单流测试用 batch=1;吞吐测试用相同 batch size
  6. 统一上下文长度:固定 prompt 长度(用 padding 对齐)
  7. 预热:每个引擎先跑 50 条 warmup,再测正式 200 条
  8. 独立物理隔离:不要在同一 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 callbackP99 < 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 这种"答案可机械判别"的任务上,看是否得出相同答案。这是最有业务意义的对比维度。
python
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 / FP16vLLM / TGI业务对话 + BBQ + 多步推理
通用客服电商 / 售后AWQ-INT4 / FP8SGLang / vLLM业务对话 500+ + GSM8K
代码助手IDE 内补全 / 解释FP8 onlyTensorRT-LLM / vLLMHumanEval+ / MBPP+ / 业务真实代码
RAG 文档问答企业知识库AWQ-INT4 + FP16 KVSGLang业务问答 + RULER 长上下文
边缘端部署本地客户端 / 离线设备GGUF Q4_K_M / Q5_K_Mllama.cpp / Ollama / MLX-LM业务子集 + 关键短任务
推理 / 数学密集研究助手 / 数据分析FP16 / FP8vLLM / TensorRT-LLMMATH / 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 8000

9.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 2

9.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.2SGLang 0.4.4差异
TTFT (P50)340 ms295 msSGLang -13%
TTFT (P99)820 ms710 msSGLang -13%
TPOT (P50)21.5 ms19.8 msSGLang -8%
Output throughput1820 tok/s2470 tok/sSGLang +35%
GPU memory peak72 GB74 GBSGLang +3%
GSM8K (greedy)87.387.1-0.2
HumanEval (greedy)78.478.40
RAG benchmark (业务)0.7620.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"模式。完整流程是:

  1. 用 ModelOpt 做 INT4 量化(或直接用预量化的 AWQ checkpoint)
  2. trtllm-build 编译为 .engine 文件
  3. 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 4096

11.4 TensorRT-LLM 的注意事项

  • build 阶段的 max_input_lenmax_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。

引擎版本GSM8KHumanEvalMMLU-ProOutput (chars/s)GPU peak
vLLM0.9.283.872.069.1345072 GB
SGLang0.4.483.671.869.0428074 GB
TensorRT-LLM0.16.084.172.369.2472068 GB
TGI3.0.583.571.668.9298076 GB
LMDeploy0.7.283.771.969.0451070 GB
llama.cpp (Q4_K_M)b543082.970.568.482050 GB
Ollama (Q4_K_M)0.5.782.970.468.479050 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 上线流程

  1. 2026-01-12:算法团队完成 AWQ-INT4 量化,校准集用了 C4 + 部分财报文本,1024 条。
  2. 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%)。算法和测试都判定"在可接受范围",决定上灰度。
  3. 2026-01-22:灰度 5% 流量,观察一周,业务侧无显著反馈。
  4. 2026-02-01:全量上线。
  5. 2026-02-14:投研团队反馈"分析报告中的数字越来越乱",特别是涉及多步财务计算的场景。

13.3 复现与定位

测试团队用业务真实场景重新构造了一组 500 条"多步财务推理"测试集(不是 GSM8K,而是真实业务的"营收+毛利+ROE+三年趋势"复合计算)。结果令人吃惊:

测试集FP16 准确率AWQ-INT4 准确率退化
GSM8K (公开)95.592.6-2.9
MATH (公开)74.869.7-5.1
业务多步财务推理87.471.2-16.2
业务长上下文(>32K)82.062.8-19.2

核心结论:公开 benchmark 的退化幅度严重低估了业务退化幅度。原因有三:

  • 业务的多步推理深度(5~10 步)远超 GSM8K(2~3 步),量化误差按链式累积放大。
  • 业务的长上下文场景下 KV cache 累积误差进一步放大。
  • 校准集没有覆盖财务领域的 token 分布,导致部分行业术语的权重精度损失更大。

13.4 缓解方案与效果

测试团队和算法团队联合给出三个方案,按风险从低到高列出:

  1. 方案 A · 重新校准:用 1024 条真实业务对话替换 C4 校准集,重做 AWQ-INT4。耗时 6 小时。
  2. 方案 B · 切 FP8:放弃 INT4,改用 DeepSeek 官方提供的 FP8 权重,需要扩容到 16×H100。增加成本 ~30%。
  3. 方案 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.3vLLM 0.9.2影响
默认 EngineV0V1(默认)调度策略变化
chunked prefill需手动开默认开启长 prefill 被切分
prefix caching实验性,默认关稳定,默认开内存占用上升
num_scheduler_steps11不变
max-num-seqs 默认2561024显存压力增加
KV cache 默认 dtypefp16auto(部分量化场景被推 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章:课堂练习

  1. 量化方案选型练习:你的产品是"医疗影像报告生成助手",用 Qwen3-72B 作为底座,业务对精度要求极高,但只有 2×H100 80G 可用。请给出量化方案推荐并说明理由(提示:考虑 FP8 / AWQ-INT4 / 混合精度三选一,并算一下显存占用)。
  2. 校准集构造练习:你被分配做 AWQ-INT4 量化的校准集准备工作。请列出"业务校准集"和"通用校准集"的取舍清单——至少 5 条决策点。
  3. 引擎升级测试方案:SRE 通知你下周要把 vLLM 从 0.9.2 升级到下一个稳定版本。请设计一个完整的 pre-upgrade 测试方案,必须涵盖:(a) 精度回归;(b) 性能回归;(c) 长尾场景压测;(d) 灰度切换策略。
  4. 七引擎对比脚本扩展:基于第 12 章的脚本,添加对 MLX-LM 的支持(Apple Silicon 专属)。提示:MLX-LM 不天然提供 OpenAI 兼容接口,需要自己起一层适配。
  5. 显著性检验:你跑了 200 条业务样本,FP16 准确率 78.5%,INT4 准确率 76.0%。这个 2.5% 的差异在统计上显著吗?请用 Bootstrap 法或 McNemar's test 计算并给出 p-value 估算。
  6. SGLang RadixAttention 实战:设计一个测试用例,能够稳定复现 SGLang 在 RAG 场景下相对 vLLM 的 30%+ 吞吐优势。要点:构造合理的 prompt 共享前缀。
  7. 合规审计场景:监管要求你提交"模型量化前后能力一致性证明"。请列出报告需要包含的 8 项内容,至少要包括:评测集说明、统计显著性、长输出退化、长上下文退化、业务 slice 分析。

本章小结。

量化与推理引擎差异性测试是 2026 年大模型工程化的"隐性回归战场"——它的 bug 不会在 PR review 阶段被发现,不会在单元测试里被覆盖,往往要靠业务侧的"模型变笨了"才被发觉。测试人员的核心使命是把这个战场显性化:用三轴矩阵(模型 × 量化 × 引擎)结构化地覆盖,用统一的指标体系(精度 / 吞吐 / 显存 / 输出一致性)量化对比,用 CI 门控避免回归。技术上要懂量化数学,要懂引擎实现差异;管理上要把"引擎版本"和"模型权重"同等管控。本章给的代码和案例都是 2026 年线上能直接复用的——拿过去就能跑。

量化与推理引擎差异性测试 大模型测试体系教程 · 第 49 篇 · 内部培训资料