大模型推理引擎实战:从 PagedAttention、Continuous Batching 到投机解码与量化部署,把 GPU 利用率榨到极限(vLLM/SGLang 2026 完全指南)
本文面向已经能跑通训练、却在「把模型卖出去」这一步被延迟和成本卡住的工程师。我们不谈模型结构,只谈一件事:如何用最少的卡,把最大的模型,以最低的延迟,喂给最多的用户。 文中所有结论都来自 vLLM / SGLang 的真实工程实践,并配有可运行的代码。
一、背景介绍:为什么推理优化是 2026 年最值钱的一行代码
如果你在 2026 年还停留在「model.generate() 一把梭」的阶段,那么恭喜你——你的 GPU 里有 60%~80% 的算力正在被白白烧掉。
一个残酷的事实:在大模型落地的成本结构里,推理(inference / serving)占整个生命周期总成本的 80% 以上,而训练只是一次性投入。模型一旦上线,每一毫秒的延迟、每一个被浪费的 token、每一张空转的卡,都会变成账单上的真金白银。这也是为什么 2025~2026 年,「推理引擎」成了 AI Infra 赛道里融资最猛、迭代最快的方向:vLLM 星标破 75k,SGLang 团队带着 RadixAttention 拿了 1 亿美元种子轮,Google、字节、腾讯、阿里纷纷下场做自己的高性能 runtime。
但绝大多数团队对推理优化的理解,还停留在「量化一下、显存不够就上更大卡」的层面。这远远不够。要真正把 GPU 榨干,你得先搞清楚推理到底在算什么、卡在哪里。
1.1 推理的两个阶段:Prefill 与 Decode
自回归大模型生成文本,本质上是一个不断「猜下一个字」的过程。把这个过程拆开,它由两个阶段组成,而且这两个阶段的性能瓶颈完全不同:
- Prefill(预填充):用户把整段 prompt 丢进来,模型一次性并行计算所有 token 的 K/V(键值)缓存,并生成第一个输出 token。这一步是 compute-bound(计算密集)——它要做的是一批大矩阵乘法(GEMM),GPU 算得越满越好。
- Decode(解码):从第二个 token 开始,模型每次只基于「上一步的输出 + 全部历史 KV 缓存」算出一个新 token。这一步是 memory-bound(访存密集)——每步的矩阵很小,但要从显存里反复读取庞大的 KV 缓存和权重,算力根本用不满。
理解这一点是后面一切优化的根基:Prefill 怕的是算力没喂饱,Decode 怕的是显存带宽不够 + 调度空隙。 一套好的推理引擎,必须同时伺候好这两种脾气。
1.2 你必须盯死的四个指标
别再只看「QPS」了,那是个容易被平均误导的指标。生产环境里,真正决定用户体验和成本的,是这四个:
| 指标 | 含义 | 受什么影响 |
|---|---|---|
| TTFT (Time To First Token) | 从发请求到吐出第一个字的延迟 | Prefill 速度、排队时长 |
| TPOT (Time Per Output Token) / ITL | 相邻两个输出 token 的间隔 | Decode 速度、batch 内是否被打断 |
| 吞吐量 (tokens/s) | 单位时间总产出 token 数 | batch 大小、GPU 利用率 |
| MFU (Model FLOPs Utilization) | 模型算力实际利用率 | 调度是否连续、kernel 效率 |
一个典型的翻车现场:你为了高吞吐把 batch 开到很大,结果 TPOT 从 20ms 飙到 200ms,用户感觉「卡死了」;你为了低延迟把 batch 限得很小,结果 MFU 掉到 10%,一张 H100 的租金等于打水漂。
1.3 朴素实现的三个坑
为什么 transformers 的 pipeline 不适合生产?因为它有三个原罪:
- 静态批处理(Static Batching):必须等一个 batch 里所有序列都生成完,才能换下一批。只要有一个序列生成了 2048 个 token,同批里只生成了 8 个 token 的请求也得陪跑——这些「陪跑 token」叫 zombie tokens,纯属浪费算力。
- KV 缓存碎片:为了容纳最长可能的序列,通常会给每个请求预分配
max_length的连续显存。结果就是内部碎片(短请求用不满)+ 外部碎片(连续大块凑不齐),显存利用率可能不到 40%。 - 无法共享前缀:多轮对话里,每一轮的 system prompt、few-shot 示例几乎一模一样,但朴素实现每次都重新算一遍 KV,重复劳动。
后面的章节,就是看 vLLM 和 SGLang 如何逐个填掉这些坑。
二、核心概念:推理引擎到底在优化什么
2.1 KV Cache:既是银弹,也是显存黑洞
要理解一切优化,先理解 KV Cache。Transformer 的注意力机制里,第 i 个 token 在计算时需要看到之前所有 token 的 Key 和 Value。如果每次生成都从头算,复杂度是 O(n²);聪明的做法是把算过的 K、V 存起来复用,这就是 KV Cache。
单个 token 的 KV Cache 大小可以精确算出来:
每 token KV 字节数 = 2 (K 和 V)
× num_layers
× num_kv_heads
× head_dim
× bytes_per_element
以 LLaMA-3-70B(80 层、8 个 KV head、head_dim 128、FP16=2 字节)为例:
2 × 80 × 8 × 128 × 2 ≈ 327,680 字节 ≈ 320 KB / token
看起来不大?但当上下文拉长到 32K,单请求的 KV Cache 就到了 10 GB——比很多 7B 模型的权重还大。而且它是随序列长度线性增长的(注意:不是平方,平方是注意力计算的 FLOPs,KV 存储是线性)。这就是为什么「长上下文」首先卡的是显存,而不是算力。
2.2 PagedAttention:把操作系统的内存分页搬进注意力
vLLM 在 2023 年的 OSMI 论文里提出的核心思想,灵感直接来自操作系统:既然 OS 用虚拟内存分页解决了内存碎片,为什么不用同样的方式管理 KV Cache?
做法很简单也很优雅:
- 把 KV Cache 切成固定大小的 block(块),比如每 block 存 16 个 token 的 KV。
- 每个序列维护一张 block table(页表),记录「逻辑 block」到「物理 block」的映射。
- 物理 block 不需要连续,像内存页一样散落在显存里。
序列 A 的逻辑块: [0] [1] [2] [3]
↓ ↓ ↓ ↓
物理块: [P7][P2][P9][P1] <- 不需要连续
带来的好处是颠覆性的:
- 外部碎片几乎归零:只会有「最后一个 block 没填满」这种不到 16 token 的内部碎片,浪费上限是常数。
- 显存利用率从 ~40% 提升到 ~95%,意味着同样一张卡能同时服务的并发数直接翻倍甚至更多。
- 天然支持共享:并行采样(一个 prompt 出 N 个结果)、beam search,可以让多个序列共享 prompt 部分的 KV block,用写时复制(Copy-On-Write)保证互不干扰。
一句话记住:PagedAttention = 用页表管理 KV,让显存「按需分配、随用随取、可共享」。
2.3 Continuous Batching:请求级动态调度
这是解决「zombie tokens」问题的关键,源自 Orca 论文的 iteration-level scheduling(迭代级调度)。
静态批处理要等整批结束;连续批处理则把调度粒度降到每个解码步:
- 每一步解码前,调度器先看 GPU 还有没有空位(由剩余 KV block 决定)。
- 有新请求到达?只要还有空位,立刻塞进去,不用等当前 batch 凑满。
- 有请求生成完了?立刻释放它的 KV block,空出来的位置马上给排队的人。
step 1: [A, B, C] <- 三个请求在解
step 2: [A, B, C, D] <- D 中途加入,不用等
step 3: [A, B, D] <- C 生成完了,立刻腾出位置
step 4: [A, B, D, E] <- E 立刻补位
效果有多猛?在真实流量(请求长度差异大)下,连续批处理能把吞吐量提升 数倍到数十倍,同时把排队的 P99 延迟砍掉一大截。今天 vLLM、SGLang、TGI 全都默认开启它。
2.4 Chunked Prefill:别让长 prompt 饿死解码
连续批处理有个副作用:如果一个请求带了个 8K 的超级长 prompt,它的 Prefill 这一步会占住 GPU 很久,导致同批正在 Decode 的请求 TPOT 突然飙高(被「饿」了一下)。
Chunked Prefill(分块预填充,vLLM 0.4+ 引入)的解法:把长 Prefill 切成多个 chunk(比如每块 512 token),每算完一个 chunk,就穿插执行一批 Decode 步。这样 Prefill 和 Decode 在同一个 GPU 上交替进行,Decode 的 TPOT 不再被长 prompt 钉死。
代价是 Prefill 本身会稍慢一点(多了调度开销),但在「多并发、长上下文」场景里,换来的是更平稳的 TPOT,对用户体验是净赚。
2.5 RadixAttention:把前缀缓存做成一棵树
SGLang 的杀手锏。多轮对话里,第 2 轮、第 3 轮的 prompt = 「相同的 system prompt + 历史对话 + 新提问」。传统引擎每轮都重算一遍前面的 KV,纯浪费。
RadixAttention 用一棵 radix tree(基数树) 把「相同前缀」的 KV 缓存组织起来:
[system prompt KV]
│
┌─────────────┴─────────────┐
[第1轮对话 KV] [另一个用户会话 KV]
│
[第2轮提问 KV]
当新请求到来,引擎沿着树匹配最长公共前缀,命中部分直接复用 KV,只算增量部分。在 Agent 场景(同一个 system prompt 驱动成百上千次工具调用)和多轮对话里,前缀命中率能提升 3~5 倍,等效吞吐暴涨。
vLLM 后来也通过 --enable-prefix-caching 支持了类似能力(基于哈希的前缀缓存),思路一致。
三、架构分析:vLLM 与 SGLang 是怎么搭起来的
3.1 vLLM 的运行时骨架
vLLM 的架构可以抽象成一条流水线:
HTTP/gRPC 请求
↓
[ Tokenizer ] → 把文本变成 token
↓
[ Scheduler ] → 核心调度器(FCFS + 优先级 + 连续批处理策略)
↓
[ KV Block Manager ] → 用 PagedAttention 管理显存块,LRU 淘汰
↓
[ Model Runner / GPU Workers ] → 真正跑模型前向(支持 TP/PP)
↓
[ Sampler ] → 采样出下一个 token
↓
[ Detokenizer ] → token 变回文本,流式返回
几个工程要点:
- Scheduler 是大脑:它决定哪些请求进 batch、分配哪些 block、何时抢占(显存不够时把低优先级序列的 KV 换出或丢弃)。vLLM 默认 FCFS(先来先服务),也支持优先级。
- AsyncLLMEngine:用 asyncio 把「调度循环」和「API 服务」解耦,单进程就能扛高并发,不会因某个请求阻塞整个服务。
- Executor 抽象:同一套调度逻辑,底下可以跑单卡、Tensor Parallel(TP,模型切到多卡)、Pipeline Parallel(PP,层切到多卡),甚至跨节点的专家并行(MoE)。
3.2 SGLang 的「前端 + 后端」双层设计
SGLang(Structured Generation Language)的定位更偏「为 Agent 和多轮而生」:
- 前端:一套 Python DSL(也可以直接走 OpenAI 兼容 API),让你用
async def的方式描述「先调模型、再调工具、再判断」的控制流,而不是手写一堆拼字符串的 prompt。 - 后端:带 RadixAttention 的高性能 runtime,配上 FlashInfer / Triton 定制 kernel、chunked prefill、张量并行,以及「约束解码(constrained decoding)」——保证输出严格符合 JSON Schema / 正则。
两者的取舍很清晰:
| 维度 | vLLM | SGLang |
|---|---|---|
| 显存管理 | PagedAttention 标杆 | 同样支持 paging |
| 多轮/Agent 前缀复用 | prefix caching(哈希) | RadixAttention(树,命中率更高) |
| 结构化输出 | 支持 JSON schema | 原生强约束解码 |
| 生态成熟度 | 最广泛,插件多 | Agent 场景更优 |
| 单轮高并发 | 极强 | 强 |
实战建议:单轮高并发 API 选 vLLM,多轮对话 / Agent 编排选 SGLang。 两者 API 都兼容 OpenAI,业务代码几乎不用改。
3.3 量化:推理优化里最便宜的「免费午餐」
在不改模型结构的前提下,量化是降本最猛的手段。核心思想:用更低的数值精度(FP16 → FP8/INT8 → INT4)存权重和激活,显存占用和访存带宽需求随之下降,Decode 这种 memory-bound 的阶段直接受益。
常见方案对比:
| 方案 | 精度 | 原理 | 适用场景 |
|---|---|---|---|
| FP8 (W8A8) | 权+激 FP8 | Hopper/Blackwell 原生支持 | H100/B200,几乎无损 |
| INT8 (W8A8) | 权+激 INT8 | SmoothQuant 抑制激活异常值 | 老卡(A100)提速 |
| AWQ | 权 INT4 | 保护 1% 显著权重,激活感知 | 低延迟、保精度首选 |
| GPTQ | 权 INT4 | 基于 Hessian 的二阶补偿 | 通用 INT4 量化 |
| GGUF (k-quants) | 权 2~6bit | llama.cpp 端侧方案 | 笔记本/手机本地跑 |
| NF4 | 权 4bit | bitsandbytes 正态量化 | QLoRA 微调常用 |
关键认知:Decode 是 memory-bound,所以「权重更省显存带宽」比「算力更省」更重要。 INT4 量化让权重读取量降到 1/4,Decode 速度常常能翻倍,而精度损失通常可接受——这就是它成生产标配的原因。
四、代码实战:从零把引擎跑起来
下面所有示例基于 2026 年主流版本(vLLM ≥ 0.6、SGLang ≥ 0.4、Transformers 生态),可直接复制运行。
4.1 用 vLLM 起一个 OpenAI 兼容服务
最快的部署方式,一条命令:
# 需要一张以上 GPU,模型从 HuggingFace / 镜像源拉取
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen3-32B \
--tensor-parallel-size 2 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--enable-prefix-caching \
--enable-chunked-prefill \
--port 8000
起好后,它就是一个标准的 OpenAI 端点,原来的业务代码零改动:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
# 流式对话
stream = client.chat.completions.create(
model="Qwen/Qwen3-32B",
messages=[{"role": "user", "content": "用一句话解释 PagedAttention"}],
stream=True,
temperature=0.7,
max_tokens=256,
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
生产环境建议用官方 Docker 镜像,避免环境冲突:
docker run --gpus all -p 8000:8000 \
vllm/vllm-openai:latest \
--model Qwen/Qwen3-32B --tensor-parallel-size 2 \
--enable-prefix-caching --enable-chunked-prefill
4.2 离线批量生成(LLM 同步接口)
不需要起服务、直接在本进程里高吞吐跑批量任务:
from vllm import LLM, SamplingParams
# 初始化引擎:前缀缓存 + 高显存利用率
llm = LLM(
model="Qwen/Qwen2.5-7B-Instruct",
tensor_parallel_size=1,
gpu_memory_utilization=0.90,
max_model_len=4096,
enable_prefix_caching=True,
)
# 采样参数:温度、top_p、最大长度、重复惩罚
params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=256,
repetition_penalty=1.05,
)
prompts = [
"解释一下 Continuous Batching 解决了什么问题",
"为什么 Decode 阶段是 memory-bound 的",
"给出一个用 RadixAttention 提升命中率的例子",
]
outputs = llm.generate(prompts, params)
for out in outputs:
print(f"=== prompt: {out.prompt[:30]}...")
print(out.outputs[0].text)
print(f"[生成 {len(out.outputs[0].token_ids)} 个 token]\n")
llm.generate 内部已经帮你做了连续批处理——你传进去 1000 条 prompt,它会自动按 GPU 容量分批、动态调度,比你自己写 for 循环串行生成快几十倍。
4.3 异步引擎 + 流式(扛高并发)
线上服务要的是「来一个请求处理一个,别阻塞」:
import asyncio
from vllm import AsyncLLMEngine, AsyncEngineArgs, SamplingParams
async def main():
engine_args = AsyncEngineArgs(
model="Qwen/Qwen2.5-7B-Instruct",
tensor_parallel_size=1,
gpu_memory_utilization=0.90,
)
engine = AsyncLLMEngine.from_engine_args(engine_args)
params = SamplingParams(temperature=0.8, max_tokens=128)
results = []
async for out in engine.generate("讲个关于 GPU 的冷笑话", params, request_id="req-1"):
# out 是逐步累积的结果,可做流式回传
results.append(out.outputs[0].text)
print("".join(results))
asyncio.run(main())
4.4 投机解码(Speculative Decoding):用小模型替大模型「打草稿」
当你的服务是 Decode-bound(卡在逐 token 生成、batch 不大)时,投机解码是性价比最高的加速。原理:
- 用一个小 draft 模型(比如 7B)快速「猜」出接下来 N 个 token;
- 让大 target 模型(比如 70B)一次前向并行验证这 N 个 token 对不对;
- 接受所有连续正确的前缀,从第一个错误处重新猜。
只要小模型的「命中率」过得去,大模型每个 step 就能一次吐出多个 token,等价 Decode 速度成倍提升,且输出分布与「只用大模型」严格一致(无精度损失)。
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
# 让 8B 当草稿模型,每次猜 5 个 token
speculative_model="meta-llama/Llama-3.1-8B-Instruct",
num_speculative_tokens=5,
tensor_parallel_size=4,
gpu_memory_utilization=0.90,
)
out = llm.generate(
"用 Python 写一个快速排序",
SamplingParams(max_tokens=512, temperature=0.3),
)
print(out[0].outputs[0].text)
除了「小模型当草稿」,vLLM 还支持 EAGLE(用目标模型自己的特征头预测)、ngram 投机(从已生成文本里找重复模式,适合代码/模板类内容,零额外显存)——后者在代码补全场景经常白捡 2~3 倍加速。
# ngram 投机:无需草稿模型,零显存开销
llm = LLM(
model="Qwen/Qwen2.5-Coder-32B",
speculative_config={"method": "ngram", "prompt_lookup_max": 5},
tensor_parallel_size=2,
)
4.5 量化实战:用 AWQ 把 7B 模型压到 INT4
下面演示如何用 autoawq 对一个模型做 INT4 权重量化,再让 vLLM 加载:
# 1) 量化阶段(离线做一次即可)
from awq import AutoAWQForCausalLM
model_id = "Qwen/Qwen2.5-7B-Instruct"
quant_config = {
"w_bit": 4, # 权重量化到 4 bit
"q_group_size": 128, # 每组 128 个权重共享缩放因子
"zero_point": False,
"version": "GEMM", # 用 GEMM 内核,推理更快
}
model = AutoAWQForCausalLM.from_pretrained(model_id)
model.quantize(
"awq_calib/", # 一小批校准数据(128~256 条文本即可)
quant_config=quant_config,
)
model.save_quantized("qwen2.5-7b-awq") # 权重体积约降到 1/4
# 2) 推理阶段:vLLM 直接加载量化模型
from vllm import LLM, SamplingParams
llm = LLM(
model="qwen2.5-7b-awq",
quantization="awq", # 告诉引擎走 AWQ 反量化路径
gpu_memory_utilization=0.95,
max_model_len=4096,
)
print(llm.generate("你好", SamplingParams(max_tokens=64))[0].outputs[0].text)
如果是 H100 / B200 这类支持 FP8 的卡,更推荐直接用 FP8(几乎无损、实现简单),vLLM 在加载时指定 quantization="fp8" 即可,无需离线量化步骤。决策优先级:有 FP8 硬件 → FP8;要极致省显存/低延迟 → AWQ/GPTQ INT4;端侧 → GGUF。
4.6 结构化输出:让模型「只能」吐合法 JSON
RAG、工具调用、数据抽取场景里,最怕模型输出格式飘。用约束解码把输出钉死在 schema 上:
from vllm import LLM, SamplingParams
llm = LLM(model="Qwen/Qwen2.5-7B-Instruct", gpu_memory_utilization=0.90)
schema = {
"type": "json_schema",
"json_schema": {
"name": "user_profile",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"skills": {"type": "array", "items": {"type": "string"}},
},
"required": ["name", "age", "skills"],
},
},
}
params = SamplingParams(
temperature=0,
max_tokens=256,
response_format=schema,
)
out = llm.generate("从这句话抽取信息:张三,28岁,会 Go 和 Rust", params)
print(out[0].outputs[0].text)
# 保证输出是合法 JSON,且字段齐全,无需再写正则去兜底
SGLang 里则用 @function 装饰器声明返回结构,后端用 xgrammar/outlines 做同样的约束解码。
4.7 压测:把 TTFT / TPOT 测出来
优化不能靠感觉,要能复现数字。用 vLLM 自带的压测工具:
# 模拟 100 个并发、共 1000 个请求,测吞吐与延迟
vllm bench serve \
--model Qwen/Qwen2.5-7B-Instruct \
--backend openai \
--host localhost --port 8000 \
--request-rate 50 \
--num-prompts 1000 \
--sharegpt \
--metric-percentiles 50 90 99
关注输出里的 TTFT (ms) 和 TPOT (ms) 的 P50/P99,以及 Output token throughput (tok/s)。每次改一个参数(比如开关 prefix caching),重测一次对比——这才是工程化的优化闭环。
五、性能优化:把旋钮拧到对的位置
光会跑还不够,下面是生产环境最常调、也最容易出效果的几个旋钮。
5.1 显存与并发的三角博弈
三个参数互相牵制,记牢这组关系:
gpu_memory_utilization(默认 0.9):引擎能用多少显存比例。调高 → 能开更多 KV block → 更高并发;但留太少会给激活值/碎片腾不出空间,反而 OOM。经验值 0.90~0.95。max_model_len:单请求最大上下文。它直接和 KV 显存预算挂钩——设成 32K 还是 8K,显存占用差 4 倍。按业务真实上限设,别拍脑袋设最大。max_num_seqs(默认 256):同时解码的最大序列数,即并发天花板。显存不够但想扛高并发,就靠量化 + 调高 utilization 来「换」出更多 block。
调参口诀:先按真实流量定 max_model_len,再调 gpu_memory_utilization 到不 OOM 的最大值,最后用 max_num_seqs 兜住并发上限。
5.2 量化选型决策树
有 H100/B200 吗?
├─ 是 → 直接用 FP8(近乎无损,最省事)
└─ 否 → 服务是低延迟 / 显存紧张?
├─ 是 → AWQ 或 GPTQ INT4(Decode 提速 ~2x)
└─ 否 → 保持 FP16,靠连续批处理提吞吐
端侧 / 本地?→ GGUF k-quants,llama.cpp 跑
5.3 投机解码什么时候开?
只在 Decode-bound 且 draft 模型和目标模型同源/强相关 时开。典型甜区:
- 大模型(≥30B)做通用对话,配一个同系列小模型当草稿 → 1.5~2.5x 加速;
- 代码补全 / 模板生成 → ngram 投机几乎零成本拿 2~3x;
- 已经高并发到 MFU 拉满 → 投机解码帮助有限(瓶颈在算力不在 decode 等待),别强开。
5.4 长上下文必开的两件套
- Prefix Caching(vLLM)/ Radix Cache(SGLang):多轮对话、Agent、带固定 system prompt 的场景,命中率直接决定成本。默认就该开。
- Chunked Prefill:长 prompt 多的流量里,开着能让 TPOT 更平稳,避免「长请求一来,短请求就卡顿」。
5.5 并行策略:单节点 vs 跨节点
- Tensor Parallel(TP):把每一层的权重切到多卡,通信频繁但延迟低,单节点多卡首选(比如 8 卡 H100 上跑 70B,TP=8)。
- Pipeline Parallel(PP):把不同层分到不同节点,通信少但会有「气泡」,适合跨节点部署超大模型(如 405B)。
- Expert Parallel(EP):MoE 模型专属,把不同专家分到不同卡。
- PD 分离(Disaggregated Prefill/Decode):2025~2026 最热架构——把 Prefill 实例和 Decode 实例物理分开,各自按自己的瓶颈(算力 vs 带宽)独立扩缩容,中间用 KV 传输层(如 Mooncake、DistServe)衔接。大厂高流量服务的标配。
5.6 把推理指标接进监控
延迟和吞吐必须可观测。vLLM 自带 Prometheus 指标(--enable-metrics),关键看这几个:
vllm:num_requests_running/vllm:num_requests_waiting:排队深度,持续 > 0 说明该扩容或降max_model_len;vllm:gpu_cache_usage:KV 块使用率,长期逼近 100% 且等待多 → 显存是瓶颈;vllm:time_to_first_token_seconds、vllm:time_per_output_token_seconds:直接对应 TTFT / TPOT 的 SLO。
配一条告警:num_requests_waiting 持续上涨 或 time_per_output_token 超阈值就报警,比用户投诉早一步。
5.7 排障清单:线上最常见的五个雷
优化不是改完参数就完事,真正吃经验的是出问题时的定位。下面这五个是生产环境最高频的雷区:
- CUDA out of memory / KV block 分配失败:典型原因是
gpu_memory_utilization调太高,或max_model_len设得超出业务真实需要。先降max_model_len到真实上限,再把 utilization 从 0.95 降到 0.88 观察;若仍 OOM,说明并发已经逼近max_num_seqs允许的 KV 容量天花板,此时最有效的手段是量化(AWQ/FP8)释放权重显存。 - TTFT 突然变长:先看
num_requests_waiting有没有堆积——有堆积说明吞吐到顶,该加副本或上 PD 分离;若等待不多但 TTFT 仍高,检查是不是关了 chunked prefill,导致某个长 prompt 正在独占 GPU 做整段 Prefill。 - TPOT 毛刺严重:多半是长 prompt 与短请求混跑、又没开 chunked prefill,或者某个请求触发了抢占(preemption)——显存不够时引擎会把低优先级序列的 KV 换出,等空位再换入,这一进一出就造成延迟尖刺。开 chunked prefill、适当调小
max_num_seqs通常能压平。 - 前缀缓存命中率低:先确认
enable-prefix-caching/enable-radix-cache真的开了;再检查多轮对话是否共享完全同一段 system prompt——拼接顺序不同、多了个空格或换行,都会让哈希对不上,缓存直接失效。 - 量化后精度肉眼可见地掉:优先换更稳的量化方案(AWQ 通常比朴素 GPTQ 稳),或把
q_group_size从 128 调到 64 提升局部精度;还不行就退到 FP8 / FP16,只对激活做量化。
5.8 一个真实压测对比(同硬件、同模型)
在单卡 A100 80G、Qwen2.5-7B-Instruct、100 并发、ShareGPT 混合长度流量下,逐步开启关键特性的典型变化(数字为示意区间,请务必用你自己的 vllm bench serve 复测):
- 朴素静态批处理:吞吐约 1.x k tok/s,P99 TPOT 偏高,排队明显;
- 加 PagedAttention + 连续批处理:吞吐约 3~4 倍,排队基本消失;
- 再加 prefix caching(多轮):命中后 TTFT 显著下降,等效成本再降一截;
- 量化到 INT4(AWQ):相同显存下
max_num_seqs可翻倍,吞吐再上一个台阶。
记住一条铁律:所有「N 倍提升」都必须用你自己的流量分布复测。离线基准用的是均匀长度,线上真实流量是长尾分布,两者往往差得很远——别人测出的 24 倍,到你业务上可能是 3 倍,这很正常,也可能是你还有大把可优化空间。
六、总结与展望:2026 年推理引擎格局与选型速查
6.1 主流引擎一句话定位
- vLLM:开源事实标准,生态最全,显存管理标杆,单轮高并发首选。
- SGLang:Agent / 多轮之王,RadixAttention 前缀复用 + 强约束解码,复杂控制流场景更顺手。
- TensorRT-LLM:NVIDIA 全家桶,极致 kernel 优化,愿意绑死的场景下延迟最低。
- llama.cpp / Ollama:端侧与本地,GGUF 量化,笔记本、手机、边缘设备跑模型的唯一答案。
- LMDeploy / TGI / DeepSpeed-FastGen:各有侧重(LMDeploy 的 TurboMind 很快,TGI 是 HuggingFace 官方,FastGen 用拆分式投机解码)。
6.2 2026 年的四个明显趋势
- PD 分离成为大规模服务标配:Prefill 和 Decode 解耦,按瓶颈独立扩缩容。
- 投机解码走向默认:从「实验特性」变成高 Decode 延迟服务的标准配置,EAGLE / ngram / 小模型三路并行。
- INT4/FP8 量化常态化:新模型发布即带官方量化版本,AWQ/GPTQ/FP8 一键加载。
- KV Cache 分层卸载:热点放显存、温数据放 CPU 内存、冷数据放 SSD(如文章开头提到的 tiered KV cache 思路),把「上下文长度」和「成本」进一步解耦。
6.3 选型速查表(直接抄)
| 你的场景 | 推荐引擎 | 必开选项 |
|---|---|---|
| 单轮高并发 API | vLLM | prefix caching + chunked prefill |
| 多轮对话 / Agent | SGLang | radix cache |
| 有 H100/B200 | 任意 + FP8 | quantization="fp8" |
| 显存紧张 / 低延迟 | vLLM + AWQ | w_bit=4, q_group_size=128 |
| Decode 慢、同源小模型 | vLLM | speculative decoding |
| 本地 / 端侧 | llama.cpp | GGUF k-quants |
| 超大模型跨节点 | vLLM + PP/TP | pipeline/tensor parallel |
6.4 写在最后
推理优化的本质,是在「算力、显存、延迟、成本」四个互相打架的目标里找平衡点。PagedAttention 解决了显存碎片,Continuous Batching 解决了调度空隙,RadixAttention 解决了前缀浪费,投机解码和量化解决了 Decode 的速度与成本——它们不是孤立的 trick,而是一套「把每一分 GPU 算力都用在刀刃上」的系统工程。
2026 年,模型能力的差距在被快速抹平,真正的护城河正在从「谁的模型更强」转向「谁能更便宜地把模型跑出来」。把这篇文章里的旋钮摸熟,你的账单会替你谢谢自己。
参考资料与延伸:vLLM 论文《Efficient Memory Management for Large Language Model Serving with PagedAttention》(OSDI 2023)、Orca《Orca: A Distributed Serving System for Transformer-Based Generative Models》(OSDI 2022)、SGLang《SGLang: A Language for Structuring LLM Applications》(ICLR 2024)、以及 vLLM / SGLang 官方文档与源码。文中配置参数以你实际使用的版本为准,建议每次改动后用 vllm bench serve 复测确认收益。