vLLM 深度拆解:当大模型推理学会「分页注意力」——PagedAttention、连续批处理与 V1 引擎如何重写 GPU 推理服务的心智模型
当你用一块 A100 跑 7B 模型,却发现 GPU 利用率长期在 30% 以下;当你的 API 在流量高峰时 P99 延迟飙到十几秒;当你兴冲冲把
max_batch_size调大,显存却直接 OOM——你遇到的不是模型的问题,而是「推理服务」这件事本身的工程难题。vLLM 之所以在 2024–2026 年成为大模型 serving 的事实标准,不是因为它把模型跑得更快,而是因为它重新定义了「如何在一块有限的 GPU 上,同时伺候成千上万个请求」这件事。
一、背景介绍:大模型推理的真正瓶颈不在算力,在「显存与调度」
绝大多数工程师第一次部署开源大模型时,都会从 Hugging Face 的 transformers 起步:
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-8B", torch_dtype=torch.bfloat16)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
inputs = tokenizer("请解释一下 PagedAttention 的原理", return_tensors="pt").to("cuda")
out = model.generate(**inputs, max_new_tokens=256)
print(tokenizer.decode(out[0]))
这段代码能跑,但它在生产环境里有三个致命问题:
- 一次只服务一个请求(或简单静态批处理)。要么串行处理,要么必须等一个 batch 的所有请求都生成完同一长度的 token 才往下走——短请求会被长请求拖死。
- KV Cache 按最大长度预分配。自回归生成时,每一步都要缓存历史 token 的 Key/Value。朴素实现会为每个序列预留
max_length大小的连续显存块,哪怕它实际只生成了 20 个 token,剩下的显存也白白浪费且无法被别的请求借用。 - 显存碎片化严重。不同请求长度不一,连续显存被切成无法复用的碎片,GPU 显存看着还有 20GB,新的请求却因找不到连续块而进不来。
这三点叠加起来,结果就是开头那句:GPU 算力在空转,显存在浪费,吞吐上不去,延迟下不来。
核心矛盾:大模型推理的瓶颈从来不是「算不动」,而是「KV Cache 显存管不好 + 请求调度不智能」。vLLM 的全部设计,都是冲着这两个点去的。
1.1 先讲清楚:KV Cache 到底是什么
Transformer 的自注意力(self-attention)里,每个 token 在每一层都会算出 Query、Key、Value 三个向量。生成第 t 个 token 时,模型需要拿第 t 个 token 的 Query,去和前面所有 token(1..t-1)的 Key/Value 做点积注意力。
如果每次都重新算前面所有 token 的 K/V,复杂度是 O(n²),且完全浪费。于是工程上会把历史 token 的 K/V 缓存下来,这就是 KV Cache:
生成进度: [t1][t2][t3] ... [t_{k-1}] ← 已生成
KV Cache: K1V1 K2V2 K3V3 ... K_{k-1}V_{k-1} ← 每步追加,永不丢弃
下一步: 用 t_k 的 Query 去 attend 上面所有 KV
KV Cache 的大小 ≈ 2(K 和 V) × 层数 × 序列长度 × 隐藏维度 × 每元素字节数。对一个 7B 模型、序列 4K、bf16 来说,单个请求的 KV Cache 就能到几百 MB。当并发到几百个请求时,KV Cache 轻松吃掉几十 GB——它才是显存的真正大户,而不是模型权重本身(权重只占约 14GB bf16)。
vLLM 的第一个革命,就是把 KV Cache 的管理,从「粗暴预分配」变成了「像操作系统管理内存一样分页」。
二、核心概念:PagedAttention 与连续批处理
2.1 PagedAttention:把 KV Cache 当虚拟内存管
传统实现里,一个请求的 KV Cache 必须是一整块连续显存,且按最大长度预留。vLLM 借鉴操作系统的虚拟内存分页思想,提出 PagedAttention:
- 把 KV Cache 切成固定大小的 block(页),比如每页存 16 个 token 的 KV。
- 一个请求的 KV Cache 由一组不一定连续的 block 组成,通过一张「页表(block table)」映射逻辑位置 → 物理显存。
- 生成新 token 时,按需分配新 block;请求结束,block 立即归还到空闲池,可被任意其他请求复用。
逻辑序列: [tok0..15][tok16..31][tok32..47] ...
↓ 页表映射(逻辑块号 → 物理块号,可离散)
物理显存: 块#7 块#3 块#21 ...
这带来两个立竿见影的好处:
- 消除内部碎片:不再按
max_length预留,请求用多少占多少,最后一块没填满也只浪费不到一个 block。 - 消除外部碎片:因为 block 可以离散存放,空闲显存被切成小块也不怕,调度器总能拼出所需 block。
更妙的是,PagedAttention 还顺手解决了前缀共享(prefix sharing):多个请求如果共享同一段系统提示词(如同样的 RAG 检索上下文、同样的 function calling 模板),它们的 KV Cache 前缀 block 可以引用同一份物理 block(写时复制 Copy-on-Write)。这在多租户、少样本提示、Agent 多轮调用场景下,能把显存占用砍掉一大截。
直觉类比:传统 KV Cache 像「给每个进程预分配一整条连续内存且不能共享」;PagedAttention 像「分页 + 写时复制」,显存利用率从 20%~40% 直接拉到 90%+。这正是 vLLM 吞吐能比朴素实现高一个数量级的根本原因。
2.2 连续批处理(Continuous Batching / In-flight Batching)
第二个革命是调度层面。传统静态批处理(static batching)必须等 batch 内所有请求都生成完同等步数才整体前进一步;一个请求早结束,它的位置就空着浪费,或者拖着别人等。
vLLM 采用连续批处理:调度以「每一步(每个 decode step)」为单位,不关心请求属于哪个 batch。
时间 →
step 1: [Req A][Req B][Req C] ← 三个请求一起跑一步
step 2: [Req A][Req B] (C 结束,腾出) [Req D 新进]
step 3: [Req A][Req D][Req E 新进] ← 随时有请求结束、随时有新请求插入
- 任意请求生成完,立刻释放其 block,新请求可马上补位。
- 每个请求按自己的节奏走(有的长有的短),互不阻塞。
- GPU 上的矩阵乘法永远喂满,利用率从「锯齿状空闲」变成「持续高位」。
连续批处理 + PagedAttention 是绝配:正因为 KV Cache 是分页、可动态增删的,调度器才敢在每一步灵活地增删请求而不破坏显存布局。
2.3 两阶段:Prefill 与 Decode
理解 vLLM 性能,必须分清两个阶段:
- Prefill(预填充):处理用户输入的整个 prompt,一次性算出所有输入 token 的 KV Cache,并生成第一个输出 token。这是**计算密集(compute-bound)**的,因为要并行算很多 token。
- Decode(解码):在已有 KV Cache 基础上,一次只生成一个新 token。这是**显存带宽密集(memory-bound)**的,每次只算一个 token 却要读整份权重和 KV Cache。
这两个阶段的资源画像完全不同,所以 vLLM 的调度(尤其 V1 引擎)会针对性优化:prefill 要尽快出第一个 token(TTFT),decode 要稳定低延迟(TPOT, time-per-output-token)。后面会讲 chunked prefill 和分离式架构如何调和二者的矛盾。
三、架构分析:从 V0 到 V1 引擎
3.1 vLLM 的整体数据流
一次请求在 vLLM 里的生命周期:
HTTP/gRPC 请求
↓
API Server (FastAPI, OpenAI 兼容)
↓ 提交到请求队列
Scheduler(调度器)
↓ 每个 step:选出可运行的序列,分配/复用 KV block
GPU Executor(模型前向)
↓ 按 block table 做 PagedAttention 计算
Sampler(采样:贪婪/温度/top-p/beam)
↓ 生成 token,写回 KV Cache
流式返回客户端,循环直至 EOS 或达到 max_tokens
调度器是大脑:它决定「这一步让哪些请求跑」「给新请求分哪些 block」「哪些请求因为显存不够要排队」。
3.2 V0 → V1:为什么重写引擎
早期 vLLM(V0)是单进程、基于协程、各模块耦合的实现。当模型规模、特性(多模态、投机解码、分离式部署、长上下文)爆发式增长后,V0 的调度逻辑变得难维护、难扩展。
2025 年起,vLLM 推出 V1 引擎并逐渐成为默认,核心变化:
- 基于 asyncio 的事件循环 + 分层调度器:把「前端 API」「调度决策」「GPU 执行」解耦成清晰的层级,调度延迟更低、吞吐更高。
- 统一的 Attention 抽象(AttentionBackend):把注意力计算抽象成可插拔后端——FlashAttention、FlashInfer、Pallas(TPU)、甚至 CPU 后端。不同硬件/场景换后端即可,不必改动调度与模型代码。
- 解耦的 KV Cache 传输层(NIXL / 分布式):支持分离式 prefill(disaggregated prefill)——prefill 和 decode 跑在不同 GPU 组上,通过高速互联(如 NIXL over InfiniBand/RDMA)传输 KV Cache。这解决了「长 prompt 的 prefill 拖慢所有 decode」的问题。
- 原生支持 Chunked Prefill、Prefix Caching、Speculative Decoding、多模态、多 LoRA:这些在 V0 是补丁,在 V1 是一等公民。
一句话:V0 是「能用的原型级架构」,V1 是「面向生产、可水平演进的引擎」。2026 年部署 vLLM,默认就是 V1。
3.3 KV Cache 管理器:BlockSpaceManager
调度的显存核心是一个叫 BlockSpaceManager 的组件:
- 维护「空闲 block 池」和「每个请求的 block 表」。
- 请求来新 token →
append_slot():若当前 block 未满则写入,满则向空闲池申请新 block。 - 请求结束 →
free():归还所有 block 到空闲池。 - Copy-on-Write 共享:当两个请求的前缀 block 完全一致,新请求不复制,而是引用同一物理 block,并标记「共享」;只有当其中一个请求要修改共享 block(通常发生在它自己的新 token 写入时,实际是写新 block,不会改历史)时才复制——典型场景下前缀块只读不写,因此共享是安全的、零拷贝的。
这种机制让「相同的 system prompt + 不同的用户问题」这类流量,显存几乎只付一次前缀的代价。
3.4 并行策略
单卡装不下大模型,或者单卡吞吐不够时,vLLM 提供多层并行:
- Tensor Parallel(张量并行,TP):把每一层的权重按列/行切到多卡,forward 时用 all-reduce 合并。最常用,
--tensor-parallel-size 8表示用 8 卡张量并行。 - Pipeline Parallel(流水线并行,PP):把不同层放到不同卡,形成流水线。适合超深/超长模型。
- Data Parallel(数据并行,DP / V1 的 multiplexing):多份完整模型副本各自处理不同请求,V1 用共享调度实现高效 multiplexing。
- 专家并行(EP):针对 MoE 模型,把专家分布到不同卡。
# 8 卡跑一个 70B 级模型,张量并行
vllm serve Qwen/Qwen3-72B \
--tensor-parallel-size 8 \
--gpu-memory-utilization 0.92 \
--max-model-len 32768
四、代码实战:从一行命令到生产集成
4.1 最简启动:OpenAI 兼容服务
vLLM 自带与 OpenAI API 完全兼容的服务端,几乎是「零改造迁移」:
pip install vllm
vllm serve Qwen/Qwen3-8B \
--host 0.0.0.0 --port 8000 \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--enable-prefix-caching \
--enable-chunked-prefill \
--max-num-seqs 256
启动后,任何原本调用 OpenAI 的代码,把 base_url 指过来即可:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
# 流式对话
stream = client.chat.completions.create(
model="Qwen/Qwen3-8B",
messages=[
{"role": "system", "content": "你是一个严谨的中文技术助手。"},
{"role": "user", "content": "用一句话说清 PagedAttention 解决了什么问题?"},
],
temperature=0.7,
max_tokens=512,
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
注意:这里没有任何 vLLM 专属代码——这就是 OpenAI 兼容的价值:你的业务代码、LangChain/LlamaIndex 中间件、前端 SDK 全部原样复用。
4.2 离线批量推理:直接调 SDK
不需要起服务、不想走 HTTP 时,用进程内 LLM 类:
from vllm import LLM, SamplingParams
prompts = [
"解释一下连续批处理(continuous batching)的原理。",
"对比 PagedAttention 和操作系统虚拟内存分页的异同。",
"在什么场景下应该开启 prefix caching?",
]
sampling = SamplingParams(
temperature=0.8,
top_p=0.95,
max_tokens=512,
repetition_penalty=1.05,
)
llm = LLM(
model="Qwen/Qwen3-8B",
tensor_parallel_size=1,
gpu_memory_utilization=0.90,
max_model_len=8192,
enable_prefix_caching=True,
)
outputs = llm.generate(prompts, sampling)
for out in outputs:
print("=" * 40)
print("Prompt:", out.prompt[:40], "...")
print("Output:", out.outputs[0].text)
print("Tokens:", len(out.outputs[0].token_ids))
generate 内部就用了连续批处理:三个 prompt 长度不同,但会在一个动态 batch 里高效跑完,互不等候。
4.3 关键性能参数调优清单
这些参数直接决定你的吞吐和延迟,是实战里最该理解的旋钮:
| 参数 | 作用 | 调优直觉 |
|---|---|---|
--tensor-parallel-size | 张量并行卡数 | 模型放不下就加大;单卡够就设 1 省通信开销 |
--gpu-memory-utilization | KV Cache 可用显存比例 | 0.9 是常见起点;和别的进程共用 GPU 时调低 |
--max-model-len | 单请求最大序列长 | 设得越大,每个 block 预留越保守,能并发的请求越少;按真实需求设 |
--max-num-seqs | 并发序列上限 | 显存够就调大提升吞吐,但会拉长单请求延迟 |
--enable-prefix-caching | 前缀 KV 缓存复用 | 多轮对话/RAG/Agent 同前缀流量必开 |
--enable-chunked-prefill | 把长 prefill 切片 | 防长 prompt 阻塞 decode,降低 TPOT 抖动 |
--max-num-batched-tokens | 每步最多 token 数 | 配合 chunked prefill 控制 prefill 粒度 |
--speculative-model | 投机解码小模型 | 用草稿模型「猜」多个 token,主模型一次验证,提 decode 速度 |
--quantization | 权重量化 | GPTQ/AWQ/FP8 降低显存与带宽压力 |
# 生产推荐组合:RAG/多轮场景 + 长上下文 + 高并发
vllm serve Qwen/Qwen3-8B \
--tensor-parallel-size 2 \
--gpu-memory-utilization 0.90 \
--max-model-len 16384 \
--max-num-seqs 512 \
--enable-prefix-caching \
--enable-chunked-prefill \
--max-num-batched-tokens 8192 \
--quantization awq
4.4 结构化输出与函数调用
生产系统很少只要「一段自由文本」,更多要 JSON、要可解析的字段、要 function calling。vLLM 通过 guided decoding(引导解码) 在采样阶段约束输出格式,保证吐出来的一定是合法结构:
from vllm import LLM, SamplingParams
llm = LLM(model="Qwen/Qwen3-8B", gpu_memory_utilization=0.90)
# 用 JSON schema 约束输出
schema = {
"type": "object",
"properties": {
"intent": {"type": "string", "enum": ["query", "book", "complain"]},
"confidence": {"type": "number"},
"summary": {"type": "string"},
},
"required": ["intent", "confidence", "summary"],
}
params = SamplingParams(
temperature=0.3,
max_tokens=256,
guided_decoding={"json": schema}, # 引导解码:每一步只在合法 token 里采样
)
out = llm.generate(
["用户说:我想查一下上周的订单为什么还没发货"], params
)
print(out[0].outputs[0].text)
# 保证输出是严格符合 schema 的 JSON,无需后处理正则兜底
引导解码的原理是:把 schema 编译成有限状态机(或用 grammar),在每一步把「会导致非法结构的 token」概率置零。它不降低质量,却彻底消灭了「模型偶尔吐出半个 JSON 导致解析崩溃」的线上事故——这是生产落地的关键能力。
4.5 用基准说话:怎么测你的部署
vLLM 自带 benchmark_serving.py,用真实分布的压力验证配置:
# 模拟 1000 个请求、固定 QPS、ShareGPT 风格分布
python -m vllm.entrypoints.openai.run_batch \
--model Qwen/Qwen3-8B --input-file requests.jsonl --output-file out.jsonl
# 或用官方 benchmark 工具看 TTFT / TPOT / 吞吐
python benchmarks/benchmark_serving.py \
--backend vllm \
--model Qwen/Qwen3-8B \
--dataset-name sharegpt \
--num-prompts 1000 \
--request-rate 20
你会观察到三个核心指标:
- TTFT(Time To First Token):首 token 延迟,反映 prefill 和排队。
- TPOT(Time Per Output Token):每个输出 token 的间隔,反映 decode 阶段带宽。
- Throughput(tokens/s):总吞吐,部署优化的终极目标。
五、性能优化:把 GPU 喂满的艺术
5.1 吞吐 vs 延迟:永恒的跷跷板
连续批处理让「高吞吐」和「低延迟」第一次可以部分兼得,但二者仍有张力:
- 把
--max-num-seqs调大 → 更多请求并发 → 吞吐上升,但每个请求的 TPOT 会被摊薄(每步要算更多序列)→ 单请求延迟上升。 - 把并发调小 → 单请求延迟漂亮,但 GPU 可能喂不饱 → 吞吐下降。
经验法则:先按「延迟 SLA」反推并发上限(比如 TPOT < 50ms 允许的最大序列数),再在这个上限内尽量拉高吞吐。--enable-chunked-prefill 是关键平衡器:它把长 prompt 的 prefill 切成小块穿插在 decode 之间,避免一次长 prefill 占满 GPU 导致所有 decode 卡顿。
5.2 Prefix Caching:被低估的免费午餐
如果你的流量里有大量「相同系统提示 + 不同用户问题」(RAG、Agent 工具调用、客服机器人),务必开 --enable-prefix-caching。命中时,那些共享前缀的 KV Cache 直接复用,等于把这部分 prefill 计算成本降到接近零。在典型多轮对话场景,TTFT 能下降一个数量级,吞吐显著提升——而且它是「自动」的,无需改业务代码。
5.3 量化:用精度换显存与带宽
权重量化是另一把钥匙:
- AWQ / GPTQ(4-bit/8-bit 权重量化):把权重压小,显存占用和带宽读取都下降,decode 更快;质量损失在小模型上可控。
- FP8:在支持 FP8 的 GPU(如 H100)上几乎无损地提速。
vllm serve Qwen/Qwen3-8B --quantization awq # 需先用 AutoAWQ 等工具产出 AWQ 权重
量化释放的显存可以转去放大 --max-num-seqs 或 --max-model-len,间接提吞吐,是「花小钱办大事」的优化。
5.4 投机解码:让 decode 一次走多步
decode 是 memory-bound 的「一步一 token」。投机解码(speculative decoding)用一个小草稿模型先「猜测」接下来 N 个 token,主模型一次前向把它们全部验证:猜对了就白赚 N-1 步,猜错了退回分叉点。在输出「可预测」的场景(代码补全、模板生成)提速明显:
vllm serve Qwen/Qwen3-72B \
--tensor-parallel-size 8 \
--speculative-model Qwen/Qwen3-8B \
--num-speculative-tokens 5
5.5 分离式部署:把 prefill 和 decode 拆开
超大规模场景(长上下文、高并发),prefill 和 decode 的资源画像冲突会被放大。V1 引擎支持分离式 prefill:两组 GPU,一组专跑 prefill(算 KV),一组专跑 decode(消费 KV),通过 NIXL 高速传输 KV Cache。这样 decode 组永远不被长 prompt 打断,TPOT 稳定;prefill 组用满算力算 KV。这是 2026 年头部推理平台的标配架构。
六、总结与展望
回到开头那个问题:为什么你裸跑 transformers 时 GPU 在空转?因为缺了两样东西——像操作系统一样精细的 KV Cache 显存管理(PagedAttention),和以每一步为粒度的连续批处理调度。vLLM 把这两件事做成引擎的核心,才让「一块 GPU 伺候上千并发请求」从奢望变成日常。
它的工程哲学其实很朴素,却极具威力:不要和算力较劲,要去管理稀缺资源(显存、调度时机)。这套思路并不只属于大模型——它和操作系统的虚拟内存、数据库的连接池、Web 服务器的事件循环,是同一类智慧。
展望 2026 年往后,vLLM 的演进方向已经清晰:
- 长上下文常态化:随着 128K+ 上下文普及,KV Cache 管理(分页、分层存储、卸载到 CPU/NVMe)会成为主战场。
- 分离式 + 多模态:prefill/decode 解耦、文本/视觉/语音统一调度,推理平台走向「数据中心级」架构。
- 投机解码与稀疏化:用更小的草稿模型、更好的猜测策略,把 decode 的 memory-bound 瓶颈进一步压低。
- 标准化与生态:OpenAI 兼容只是起点,与评测、可观测(metrics/tracing)、自动扩缩容的集成会越来越深。
如果你今天要上线一个开源大模型服务,vLLM 几乎是不需要论证的默认选项。但真正用好它,靠的不是「装上就行」,而是理解 PagedAttention 为什么省显存、连续批处理为什么提吞吐、那十几个旋钮在每个指标上的此消彼长——本文拆的,就是这层「心智模型」。
工具会迭代,框架会重写,但「在有限资源上,用聪明的调度把利用率推向极限」这条工程主线不会变。理解了 vLLM,你就理解了这一类系统的灵魂。