数据截至 (上游 commit f25c580af159)
vLLM — 架构与原理
30 秒导读: vLLM 是开源界事实标准的 LLM 推理服务引擎(UC Berkeley Sky Computing Lab 发起)。它解决的问题是:把一个大模型放到 GPU 上对外服务时,吞吐被 KV cache 的显存浪费和「一 批请求必须同进同出」的批处理方式拖死。vLLM 的两个成名答案——PagedAttention(像操作系统管内存页一样管 KV cache)和 continuous batching(每个解码步都重新决定谁在这一批里)——让它在同样硬件上把吞吐抬高一个数量级。本文档按由浅入深的顺序讲清这套系统的原理与真实实现。
1. 这是什么(零基础也能懂)
一句话定义
vLLM 是一个高吞吐的 LLM 推理与服务引擎:你给它一个 Hugging Face 模型,它给你一条命令就能起量的 OpenAI 兼容 API 服务,或者一个离线批量推理的 Python 库。
它要解决谁的什么问题
设想你买了一个 70B 模型想给全公司当 API 用。naive 做法是:来一条请求,跑完,再跑下一条。这会撞两堵墙:
- 显存墙。 每生成一个 token,注意力都要读它前面所有 token 的 Key/Value 向量(即 KV cache)。这些向量占的显存常常比模型权重还大,而传统实现为每条请求预留一段连续显存,请求真实长度又不可预知——大量显 存被「占着没用」。
- 吞吐墙。 传统 batching 是「凑一批、一起跑完、再放下一批」。同一批里有 10 个 token 的回答也有 2000 个 token 的回答,短的早就结束了,GPU 却陪长的空跑。
vLLM 就是把这两堵墙拆掉的那一层:显存按固定大小的块(block)随用随分,批组成每个 step 都可变。
它能做什么
| 能力 | 具体支持 |
|---|---|
| 服务接口 | OpenAI 兼容 API(chat/completion/responses)、Anthropic Messages、gRPC、离线 LLM 类 |
| 核心优化 | PagedAttention、continuous batching、chunked prefill、prefix caching(前缀缓存) |
| 执行优化 | piecewise / full CUDA graph、torch.compile、FlashAttention / FlashInfer / MLA 等注意力后端 |
| 解码算法 | 贪心/采样、parallel sampling(n>1)、beam search、投机解码(EAGLE、n-gram、Medusa 等,vllm/v1/spec_decode/) |
| 量化 | FP8、NVFP4、INT8/INT4、GPTQ/AWQ、GGUF 等 |
| 并行 | 张量并行(TP)、流水并行(PP)、数据并行(DP)、专家并行(EP)、上下文并行 |
| 模型面 | 200+ 架构:稠密 LLM、MoE、Mamba 混合、多模态、embedding/奖励模型 |
| 硬件 | NVIDIA/AMD/Intel GPU、CPU,及 TPU 等插件后端 |
用起来什么样
两种形态。命令行起服务(一条命令就是一个 OpenAI 服务):
# 摘自官方 quickstart 的典型用法
vllm serve Qwen/Qwen3-8B --tensor-parallel-size 2
# 然后就能用 openai SDK 打 http://localhost:8000/v1/chat/completions
Python 里离线批量跑(LLM 类,定义在 vllm/entrypoints/llm.py:67):
from vllm import LLM, SamplingParams
llm = LLM(model="Qwen/Qwen3-8B") # 加载模型、建 KV cache、编译/捕获 CUDA graph
outs = llm.generate(["写一首关于秋天的诗"] * 1000, SamplingParams(max_tokens=128))
# 1000 条请求会自动 continuous batching,不用你管批怎么组
一句话直觉
把 vLLM 想成一台「token 流水线工厂」的操作系统。 KV cache 显存是物理内存,PagedAttention 是它的分页内存管理器(MMU);每条请求是一个进程,调度器每个时钟周期(step)都重新决定哪些进程上 CPU——不占满 token 预算不罢休;进程结束不立刻清场,留下的「内存页」还带着哈希,下一个带相同前缀的请求来了直接认领。
2. 顶层全景(它大概怎么转)
2.1 进程与部件拓扑
V1 引擎(本 commit 的唯一主线,V0 已移除)是一套多进程流水线:
OpenAI HTTP 请求
│
▼
┌──────────────────┐ zmq/共享内存 ┌───────────────────────┐
│ 前端进程 │ ─────────────► │ EngineCore 进程 │
│ AsyncLLM │ 请求 │ run_busy_loop 死循环: │
│ · InputProcessor │ │ ① Scheduler.schedule │
│ · OutputProcessor│ ◄───────────── │ ② Executor 执行 │
│ · 增量 Detokenize│ token 输出 │ ③ update_from_output │
└──────────────────┘ └──────────┬────────────┘
│ collective_rpc 广播
┌────────────▼───────────┐
│ Worker 进程 × N(每卡1个)│
│ GPUModelRunner: │
│ 备输入→forward→采样 │
└────────────────────────┘
怎么读这张图: 左边是用户看得到的部分(收请求、把 token 流转成文本流);中间是引擎的「大脑」,每转一圈生产一个 step 的 token;右边是真正碰 GPU 的部分,一个进程对应一张卡。三个框在默认部署里是三个(或更多)进程,靠消息队列解耦,谁都不会因为别人慢而空等。
2.2 部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
AsyncLLM | 异步前端:收请求、校验、把输出推给调用方 | vllm/v1/engine/async_llm.py:72 |
InputProcessor | 文本→token id、多模态预处理、算前缀哈希 | vllm/v1/engine/input_processor.py:38 |
EngineCoreProc | 引擎核心进程外壳:busy loop + 进程间握手 | vllm/v1/engine/core.py:1027 |
Scheduler | 每个 step 决定哪些请求跑、各跑几个 token、分到哪些 KV 块 | vllm/v1/core/sched/scheduler.py:74 |
KVCacheManager / BlockPool | KV cache 块的分配、引用计数、前缀缓存哈希表 | vllm/v1/core/kv_cache_manager.py:118、vllm/v1/core/block_pool.py:143 |
Executor(UniProc/MultiProc/Ray) | 把一步执行铺到 1..N 个 Worker | vllm/v1/executor/abstract.py:38 |
GPUModelRunner | 每卡上的执行体:组 batch 输入、跑模型、采样 | vllm/v1/worker/gpu_model_runner.py:501 |
CudagraphDispatcher | 运行时决定这一步用哪种 CUDA graph 模式 | vllm/v1/cudagraph_dispatcher.py:13 |
Sampler | logits → 采样 token(温度、top-k/top-p、惩罚项) | vllm/v1/sample/sampler.py:21 |
OutputProcessor + 增量 detokenizer | token → 文本增量、按 stop 条件收尾、推流 | vllm/v1/engine/output_processor.py:132、vllm/v1/engine/detokenizer.py:31 |
2.3 主线走一遍(一个请求的完整旅程)
不进代码,先建立全局直觉:
- 入队。 前端把 prompt 变成 token id 序列,按固定块大小预计算好每块的链式哈希(供前缀缓存命中用),包装成
EngineCoreRequest发给 EngineCore。 - 等待调度。 请求进 Scheduler 的
waiting队列。每个 step,调度器先保住running里的老请求,再用剩余 token 预算从waiting里拉新请求——这就是 continuous batching:批的成员每个 step 都重新决定。 - 分块。 调度器通过
KVCacheManager问:这个 prompt 的前缀有多少块已经在缓存里?命中的直接引用计数 +1 复用;没命中的从空闲块池拿新块。显存不够就把优先级最低的请求抢占回等待队列。 - 执行。
SchedulerOutput(谁、几个 token、用哪些块)广播给所有 Worker;每个GPUModelRunner把 batch 展平成一维 token 序列跑 forward,从各自最后一位置取 logits 采样出本步 token。 - 回流。 采样结果回 EngineCore,
update_from_output更新每条的进度、检查停止条件(EOS、长度上限、stop 字符串);活着的下一步继续,结束的释放块回池。 - 出字。 token 增量经 detokenizer 变成文本增量,前端按 SSE 流式推给用户。
3. 阅读地图
建议按编号顺序读;每章开头都有「这章讲什么」。
| 章节 | 你会学到 | 前置 |
|---|---|---|
| 01 PagedAttention 与 KV cache 块管理 | 为什么 KV cache 是显存杀手;分页 + 引用计数 + LRU 驱逐;前缀缓存的链式哈希 | 无 |
| 02 continuous batching 调度器 | token 预算、RUNNING/WAITING 双队列、chunked prefill、抢占与重算 | 01 |
| 03 V1 引擎架构 | 为什么拆成多进程;busy loop;async scheduling;输出处理链 | 02 |
| 04 模型执行与 CUDA graph | GPU 上一步的全貌;块表;CUDA graph 三模式与运行时派发;显存 profiling | 02 |
| 05 分布式与 API 服务 | Executor 抽象与多卡;vllm serve 到引擎的调用链 | 03、04 |
| 06 巧妙之处·边界·对比·代码地图 | 可抄走的设计、已知局限、和兄弟引擎的取舍 | 全部 |
只想快速建立心智模型:读本章 + 01 + 02 即可。要改调度策略:重点 02 + 03。要接新硬件/新注意力后端:重点 01 + 04。
4. 巧妙之处(预告)
以下是读完全文后值得带走的精华,细节和出处见 06:
- 分页显存 + 链式哈希:KV cache 块既能按 LRU 驱逐,又能靠「父块哈希 + 本块 token」的链式哈希秒级命中前缀缓存——同一份数据结构服务两个目的。
- 调度与执行的解耦:调度器只产出一个纯数据的
SchedulerOutput,Worker 缓存请求状态、每步只收 diff,通信成本压到最低。 - CUDA graph 运 行时派发:预捕获一组 batch 尺寸的图,运行时把任意 batch 「向上取整」到最近的已捕获尺寸,小批量解码几乎零 CPU 开销。
5. 边界与局限(速览)
- 它是推理引擎,不是训练框架——没有反向传播;训练侧请看兄弟库(如 verl 把 vLLM 当 rollout 后端用)。
- 性能特性随小版本剧烈变化;硬件相关调优(块大小、CUDA graph 尺寸集、后端选择)比配置项表面看起来更影响结果。
- 多进程 + 多后端意味着启动要经历「加载 → profiling → torch.compile → CUDA graph 捕获」的完整热身,冷启动以十秒到分钟计。
详细的坑与横向对比见 06。
6. 横向对比(一句话版)
| 项目 | 与 vLLM 的不同取舍 |
|---|---|
| SGLang | 同样做高性能 serving,但另有 RadixAttention(基数树前缀缓存)与结构化生成前端语言;vLLM 走块池 + 链式哈希路线 |
| llama.cpp | 面向本地/边缘、CPU 与量化优先;vLLM 面向数据中心 GPU 吞吐优先 |
| transformers | 研究与易用优先的模型库,generate() 朴素批处理;vLLM 是生产 serving 引擎 |
| verl | RL 训练框架,把 vLLM 作为推理后端嵌入训练循环 |
7. 代码地图(全局导航)
完整版(含更多符号)在 06;这里是「从哪儿跳进源码」的最小集合:
| 主题 | 文件路径 | 关键符号 |
|---|---|---|
| 引擎核心主循环 | vllm/v1/engine/core.py | EngineCore.step、EngineCoreProc.run_busy_loop |
| 调度决策 | vllm/v1/core/sched/scheduler.py | Scheduler.schedule、Scheduler.update_from_output |
| KV 块池 | vllm/v1/core/block_pool.py | BlockPool.get_new_blocks、BlockPool.free_blocks |
| 前缀缓存命中 | vllm/v1/core/single_type_kv_cache_manager.py | FullAttentionManager.find_longest_cache_hit |
| 块哈希 | vllm/v1/core/kv_cache_utils.py | hash_block_tokens、KVCacheBlock |
| GPU 一步执行 | vllm/v1/worker/gpu_model_runner.py | GPUModelRunner.execute_model |
| CUDA graph 派发 | vllm/v1/cudagraph_dispatcher.py | CudagraphDispatcher.dispatch |
| 采样 | vllm/v1/sample/sampler.py | Sampler.forward |
| 多卡执行器 | vllm/v1/executor/multiproc_executor.py | MultiprocExecutor.collective_rpc、WorkerProc |
| 服务入口 | vllm/entrypoints/cli/serve.py | ServeSubcommand |