编程 大模型推理引擎实战:从 PagedAttention、Continuous Batching 到投机解码与量化部署,把 GPU 利用率榨到极限(vLLM/SGLang 2026 完全指南)

2026-07-09 09:17:24 +0800 CST views 445

大模型推理引擎实战:从 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 朴素实现的三个坑

为什么 transformerspipeline 不适合生产?因为它有三个原罪:

  1. 静态批处理(Static Batching):必须等一个 batch 里所有序列都生成完,才能换下一批。只要有一个序列生成了 2048 个 token,同批里只生成了 8 个 token 的请求也得陪跑——这些「陪跑 token」叫 zombie tokens,纯属浪费算力。
  2. KV 缓存碎片:为了容纳最长可能的序列,通常会给每个请求预分配 max_length 的连续显存。结果就是内部碎片(短请求用不满)+ 外部碎片(连续大块凑不齐),显存利用率可能不到 40%。
  3. 无法共享前缀:多轮对话里,每一轮的 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]   <- 不需要连续

带来的好处是颠覆性的:

  1. 外部碎片几乎归零:只会有「最后一个 block 没填满」这种不到 16 token 的内部碎片,浪费上限是常数。
  2. 显存利用率从 ~40% 提升到 ~95%,意味着同样一张卡能同时服务的并发数直接翻倍甚至更多。
  3. 天然支持共享:并行采样(一个 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 / 正则。

两者的取舍很清晰:

维度vLLMSGLang
显存管理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)权+激 FP8Hopper/Blackwell 原生支持H100/B200,几乎无损
INT8 (W8A8)权+激 INT8SmoothQuant 抑制激活异常值老卡(A100)提速
AWQ权 INT4保护 1% 显著权重,激活感知低延迟、保精度首选
GPTQ权 INT4基于 Hessian 的二阶补偿通用 INT4 量化
GGUF (k-quants)权 2~6bitllama.cpp 端侧方案笔记本/手机本地跑
NF4权 4bitbitsandbytes 正态量化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 不大)时,投机解码是性价比最高的加速。原理:

  1. 用一个小 draft 模型(比如 7B)快速「猜」出接下来 N 个 token;
  2. 大 target 模型(比如 70B)一次前向并行验证这 N 个 token 对不对;
  3. 接受所有连续正确的前缀,从第一个错误处重新猜。

只要小模型的「命中率」过得去,大模型每个 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_secondsvllm:time_per_output_token_seconds:直接对应 TTFT / TPOT 的 SLO。

配一条告警:num_requests_waiting 持续上涨 或 time_per_output_token 超阈值就报警,比用户投诉早一步。


5.7 排障清单:线上最常见的五个雷

优化不是改完参数就完事,真正吃经验的是出问题时的定位。下面这五个是生产环境最高频的雷区:

  1. 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)释放权重显存。
  2. TTFT 突然变长:先看 num_requests_waiting 有没有堆积——有堆积说明吞吐到顶,该加副本或上 PD 分离;若等待不多但 TTFT 仍高,检查是不是关了 chunked prefill,导致某个长 prompt 正在独占 GPU 做整段 Prefill。
  3. TPOT 毛刺严重:多半是长 prompt 与短请求混跑、又没开 chunked prefill,或者某个请求触发了抢占(preemption)——显存不够时引擎会把低优先级序列的 KV 换出,等空位再换入,这一进一出就造成延迟尖刺。开 chunked prefill、适当调小 max_num_seqs 通常能压平。
  4. 前缀缓存命中率低:先确认 enable-prefix-caching / enable-radix-cache 真的开了;再检查多轮对话是否共享完全同一段 system prompt——拼接顺序不同、多了个空格或换行,都会让哈希对不上,缓存直接失效。
  5. 量化后精度肉眼可见地掉:优先换更稳的量化方案(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 年的四个明显趋势

  1. PD 分离成为大规模服务标配:Prefill 和 Decode 解耦,按瓶颈独立扩缩容。
  2. 投机解码走向默认:从「实验特性」变成高 Decode 延迟服务的标准配置,EAGLE / ngram / 小模型三路并行。
  3. INT4/FP8 量化常态化:新模型发布即带官方量化版本,AWQ/GPTQ/FP8 一键加载。
  4. KV Cache 分层卸载:热点放显存、温数据放 CPU 内存、冷数据放 SSD(如文章开头提到的 tiered KV cache 思路),把「上下文长度」和「成本」进一步解耦。

6.3 选型速查表(直接抄)

你的场景推荐引擎必开选项
单轮高并发 APIvLLMprefix caching + chunked prefill
多轮对话 / AgentSGLangradix cache
有 H100/B200任意 + FP8quantization="fp8"
显存紧张 / 低延迟vLLM + AWQw_bit=4, q_group_size=128
Decode 慢、同源小模型vLLMspeculative decoding
本地 / 端侧llama.cppGGUF k-quants
超大模型跨节点vLLM + PP/TPpipeline/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 复测确认收益。

推荐文章

2024年公司官方网站建设费用解析
2024-11-18 20:21:19 +0800 CST
使用 `nohup` 命令的概述及案例
2024-11-18 08:18:36 +0800 CST
Vue3中如何实现状态管理?
2024-11-19 09:40:30 +0800 CST
php腾讯云发送短信
2024-11-18 13:50:11 +0800 CST
程序员茄子在线接单