vLLM 推理内核深度解析
第1章 架构总览:从一条请求读懂整个 vLLM
第1章 架构总览:从一条请求读懂整个 vLLM
"The best way to understand a city's traffic system is not to study the route map, but to ride a bus from terminal to terminal."
本章要点
- 以"一次 HTTP 请求的完整旅程"为线索,建立对 vLLM V1 架构的直觉
- 理解 V1 的三进程拓扑(API Server / EngineCore / Worker):为什么这样拆分、为什么每一对之间的协议不同
- 掌握五大子系统的职责边界:入口层、引擎核心、执行层、模型层、内核层
- 读懂从 SchedulerOutput 到 ModelRunnerOutput 的四类 DTO 数据契约
- 对照 V0 → V1 七个结构性变革:多进程化、统一调度、有状态 Worker、零开销前缀缓存、分段 CUDA Graph、异步去分词、Executor 抽象
- 认识 vLLM 源码目录的组织逻辑:V1 vs V0 代码在哪里、读源码从哪里开始
- 拿到一张"章节地图":后续 17 章每一章讲什么、映射到源码哪个位置
1.1 一个推理请求的完整旅程
让我们从最具体的场景出发。
一个用户通过 HTTP POST 向你的 vLLM 服务发送:
POST /v1/chat/completions
{
"model": "Llama-3-70B-Instruct",
"messages": [
{"role": "system", "content": "你是一个专业的 AI 助手。"},
{"role": "user", "content": "请用一句话解释什么是 PagedAttention。"}
],
"stream": true,
"temperature": 0.7
}
从这个请求到达你的服务器,到用户的浏览器里逐字蹦出完整回复——中间发生了什么?
我们把旅程切成六个阶段,一步步看:
graph LR
A["① HTTP 到达<br/>FastAPI"] --> B["② API Server<br/>tokenize + 请求构造"]
B --> C["③ EngineCore<br/>排队 + 状态机"]
C --> D["④ Scheduler<br/>调度决策 + KV 分配"]
D --> E["⑤ Worker<br/>GPU 前向 + 采样"]
E --> F["⑥ API Server<br/>detokenize + SSE 输出"]
style A fill:#3b82f6,color:#fff,stroke:none
style B fill:#8b5cf6,color:#fff,stroke:none
style C fill:#ec4899,color:#fff,stroke:none
style D fill:#f59e0b,color:#fff,stroke:none
style E fill:#10b981,color:#fff,stroke:none
style F fill:#6366f1,color:#fff,stroke:none
这六个阶段横跨三个独立进程,用两套 IPC 协议连接。我们逐个展开。
1.1.1 阶段 ① ②:API Server 接收请求
请求首先抵达 API Server 进程(vllm/entrypoints/openai/api_server.py)。这是一个基于 FastAPI 的 HTTP 服务,对外暴露 OpenAI 兼容的接口。
API Server 做三件事:
- 参数校验:用 Pydantic schema 校验
model、temperature、top_p、messages结构等。校验不通过直接返回 400。 - Chat Template 渲染 + Tokenize:messages 数组通过模型自带的
chat_template(Jinja2 格式,来自 tokenizer 的元数据)渲染成单一 prompt 字符串,再用 tokenizer 分词成prompt_token_ids。 - 构造 EngineCoreRequest:把 token IDs、sampling params、请求元数据打包成一个最小化的跨进程 DTO。
关键问题:分词为什么放在 API Server 而不是 Worker 或 EngineCore?
答案在 V0 的痛:V0 时代分词和引擎主循环在同一进程内,一个 8000-token 的长 prompt 分词要几十毫秒,期间 EngineCore 无法调度别的请求——所有 decode 流被卡住。V1 把分词推到独立的 API Server 进程,CPU 密集的文本处理和 GPU 调度真正并行。
除此之外还有收益:
- API Server 可以横向扩容(多个 API Server 副本共享一个 EngineCore)
- 换 tokenizer 不需要重启引擎
- Engine Core 里的请求对象永远只含 token IDs,内存紧凑
1.1.2 阶段 ③:EngineCore 接收请求
API Server 把 EngineCoreRequest 通过 ZMQ PUSH/PULL socket 发给独立的 EngineCore 进程(vllm/v1/engine/core.py)。
EngineCore 内部是一个纯异步主循环 run_busy_loop:
while running:
process_input_queue() # 拉取 ZMQ 新消息,入 waiting 队列
if has_requests():
outputs = step() # 调度 + 执行 + 收集 = 一拍
send_outputs(outputs) # ZMQ PUSH 给 API Server
else:
wait_for_work() # 阻塞等新请求
第 2 章会详细讲这个循环的每一拍。此时新请求进入WAITING 状态,等待调度器选中。
1.1.3 阶段 ④:Scheduler 决定这一拍做什么
step() 的第一步是调用 Scheduler(vllm/v1/core/sched/scheduler.py)。Scheduler 的输出是一个简洁的字典:
SchedulerOutput(
num_scheduled_tokens={
"req-A": 128, # 新请求,prefill 前 128 token
"req-B": 1, # 老请求,decode 1 token
"req-C": 64, # 新请求 chunked prefill 后半段
"req-D": 1, # 老请求 decode
},
scheduled_new_reqs=[...], # 新进 RUNNING 的请求元数据
scheduled_cached_reqs=[...], # 继续 RUNNING 的请求
finished_req_ids={...}, # 本拍完成的请求
...
)
这个看似轻描淡写的字典背后,是调度器做了数十个小决策:
- 还剩多少空闲 KV 块?需要抢占谁来腾空间?
- Chunked prefill 切多大块合适?
- 前缀缓存命中了多少 block?命中的 block 不计入 token_budget
- 有没有 LoRA 限制?max_loras=4 的情况下是否要放弃某些 LoRA 请求?
- Spec decode 这一拍要产多少个 token?
调度逻辑的精妙之处在第 3 章(Scheduler)和第 10、11、12 章(前缀缓存、Chunked Prefill、Spec Decode)详细展开。
1.1.4 阶段 ⑤:Worker 执行 GPU 计算
Scheduler 产出 SchedulerOutput 后,EngineCore 通过 Executor 抽象(第 6 章)把它广播给所有 Worker。单机多卡场景下用 MultiprocExecutor,走共享内存 MessageQueue 零拷贝广播。
每个 Worker(vllm/v1/worker/gpu_worker.py,每张 GPU 对应一个进程)收到后:
_update_states:差量更新本地InputBatch——新请求添加槽位、老请求追加 token_prepare_inputs:构造 GPU 输入张量(input_ids、positions、block_tables、slot_mapping、cu_seqlens)execute_model:调用 ModelRunner 跑前向- 若开了 CUDA Graph → pad 到最近的 capture size → replay
- 否则 eager 模式
sampler.forward:从 logits 采样出新 token- 返回
ModelRunnerOutput(含 sampled_token_ids、logprobs、spec_token_ids)
这是整个系统里唯一真正吃 GPU 算力的环节。第 6 章(Worker/Executor)、第 8 章(ModelRunner/CUDA Graph)、第 9 章(Sampling)详讲。
1.1.5 阶段 ⑥:API Server 去分词 + 流式输出
Worker 结果通过 Executor 汇总回 EngineCore,再通过 ZMQ output socket 发回 API Server。
API Server 里每个 request 有一个 asyncio.Queue,消息到达后按 request_id 分发。对应的 FastAPI handler 在 async for 循环里等消息,拿到新 token 后:
- detokenize:token_id → 字符片段(Rust tokenizer 快到纳秒级)
- SSE 封装:
data: {"choices": [{"delta": {"content": "Page"}}]}\n\n - 通过 HTTP 长连接 push 给客户端
用户浏览器里就看到一个 token 一个 token 蹦出来。
1.1.6 这六个阶段的时间构成
假设 Llama-70B 单请求,prompt 1000 token,生成 200 token:
阶段 ①② HTTP + tokenize: ~15 ms (一次性)
阶段 ③ ZMQ → EngineCore: ~0.2 ms(一次性)
阶段 ④ Scheduler schedule: ~0.5 ms/step
阶段 ⑤ GPU forward + sample: ~20 ms/step
阶段 ⑥ detokenize + SSE: ~0.1 ms/token
Prefill: 1 step × ~60 ms (第一个 token 出)
Decode: 200 step × ~20 ms = ~4 s
总耗时 ≈ 4.08 s
其中 GPU 时间 ≈ 4060 ms
非 GPU 时间 ≈ 20 ms (占 0.5%)
系统内部开销只占 0.5%——说明 V1 的工程打磨让 GPU 几乎满负荷工作。
1.2 三进程拓扑:为什么要这样拆?
上一节你应该已经注意到一个反复出现的关键词:进程分离。这是 V1 架构最关键的设计决策。
1.2.1 V0 单进程的痛
V0 时代,API Server + Scheduler + KV 管理 + 向 Worker 发指令 全部挤在一个 Python 进程里。CPython 的 GIL 让这个进程在任何时刻只有一个线程能运行 Python 代码。后果:
- Tokenize 一个长 prompt(几十 ms CPU 时间)→ GIL 被占 → Scheduler 无法做下一拍决策
- Detokenize 一个新 token(虽然快但频繁)→ 主循环被打断
- 图像预处理(VLM 场景)→ GPU 调度完全暂停几十毫秒
CPU 和 GPU 无法并行,GPU 利用率卡在 50-70%。
1.2.2 V1 的三进程分工
graph TB
subgraph "API Server 进程 (CPU 密集)"
Web[HTTP Handler<br/>FastAPI]
Tok[Tokenizer<br/>Rust impl]
Detok[Detokenizer]
SSE[SSE 流式输出]
end
subgraph "EngineCore 进程 (CPU 密集,主循环)"
Sched[Scheduler]
KVMgr[KVCacheManager]
Loop[run_busy_loop]
end
subgraph "Worker 进程 × N (GPU 密集)"
W1[Worker 0 / GPU 0]
W2[Worker 1 / GPU 1]
WN[Worker N / GPU N]
end
Web <-->|ZMQ PULL/PUSH| Loop
Loop <-->|共享内存 MQ| W1
Loop <-->|共享内存 MQ| W2
Loop <-->|共享内存 MQ| WN
style Web fill:#8b5cf6,color:#fff,stroke:none
style Tok fill:#8b5cf6,color:#fff,stroke:none
style Sched fill:#ec4899,color:#fff,stroke:none
style W1 fill:#10b981,color:#fff,stroke:none
style W2 fill:#10b981,color:#fff,stroke:none
每个进程一个独立 Python interpreter + 独立 GIL。三个进程物理并行:
- API Server 做 tokenize / detokenize 时,EngineCore 可以做 schedule
- EngineCore 做 schedule 时,Worker 可以跑上一拍的 GPU forward
- 三者通过 ZMQ / shared memory 通信,延迟 < 100 μs
1.2.3 为什么用两套 IPC 协议
V1 在 API Server ↔ EngineCore 之间用 ZMQ,在 EngineCore ↔ Worker 之间用共享内存 MessageQueue。两套协议各司其职:
| 协议 | 位置 | 消息频率 | 消息大小 | 延迟要求 | 选型理由 |
|---|---|---|---|---|---|
| ZMQ PUSH/PULL | API Server ↔ EngineCore | 低(per request) | 小(token IDs + params) | 毫秒级够用 | 抽象好、跨 Linux/Mac、支持未来跨机 |
| Shared Memory MQ | EngineCore ↔ Worker | 高(per step) | 大(block_tables、input_ids flat) | 微秒级 | msgpack + 零拷贝;zero-mp-queue 开销 |
在 step 频率(每 20-30 ms 一拍)下,Worker 通信必须微秒级——如果用 ZMQ 或 multiprocessing.Queue,每 step 要 1-2 ms,占去 5-10% GPU 时间。共享内存把这个开销压到 < 100 μs。
1.3 V0 → V1 的七个结构性变革
V1 不是 V0 的 patch,是整体重写。总结下来有七个结构性变革:
graph TB
V0[V0 旧架构] -.变革 1.-> MP[多进程化]
V0 -.变革 2.-> UT[统一 Token 调度]
V0 -.变革 3.-> SW[有状态 Worker]
V0 -.变革 4.-> PC[零开销前缀缓存]
V0 -.变革 5.-> PCG[分段 CUDA Graph]
V0 -.变革 6.-> AD[异步 detokenize]
V0 -.变革 7.-> EA[Executor 抽象]
MP --> V1[V1 新架构]
UT --> V1
SW --> V1
PC --> V1
PCG --> V1
AD --> V1
EA --> V1
style V0 fill:#ef4444,color:#fff,stroke:none
style V1 fill:#10b981,color:#fff,stroke:none
| # | 变革 | V0 | V1 | 相关章节 |
|---|---|---|---|---|
| 1 | 多进程化 | 单进程 + GIL 瓶颈 | 三进程物理并行 | 第 2 章 |
| 2 | 统一 Token 调度 | Prefill / Decode 分离代码路径 | {req_id: num_tokens} 统一 |
第 3 章 |
| 3 | 有状态 Worker | 无状态,每 step 广播全量 | 有状态,发 diff | 第 6 章 |
| 4 | 零开销前缀缓存 | 5-10% 吞吐损耗,opt-in | < 1% 损耗,默认开 | 第 10 章 |
| 5 | 分段 CUDA Graph | 整图 capture,128 张图 | Piecewise,13 张图 | 第 8 章 |
| 6 | 异步 detokenize | 在引擎主循环里串行做 | API Server 进程异步做 | 第 2、17 章 |
| 7 | Executor 抽象层 | EngineCore 直接管 Worker 拓扑分支 | 统一 collective_rpc 接口 |
第 6 章 |
基准测试结果:
| 负载类型 | V0 吞吐 | V1 吞吐 | 提升 |
|---|---|---|---|
| 纯文本 chat | baseline | 1.7× | +70% |
| VLM(图文混合) | baseline | 1.7× | +70% |
| 长 context RAG | baseline | 1.5× | +50% |
不是一个算法改进一个 kernel 替换加出来的——是全栈重构的结果。
1.4 五大子系统:代码库骨架
从全局视角看,vLLM 的代码库可以映射为五个子系统。理解它们的边界和交互,就掌握了整个系统骨架。
graph TB
USER[👤 用户]
USER --> ENT[① 入口层<br/>Entrypoints]
ENT --> ENG[② 引擎核心<br/>Engine Core]
ENG --> EXE[③ 执行层<br/>Executor + Worker]
EXE --> MOD[④ 模型层<br/>Model Executor]
MOD --> KER[⑤ 内核层<br/>CUDA + Triton Kernels]
KER --> GPU[🔥 GPU]
style ENT fill:#8b5cf6,color:#fff,stroke:none
style ENG fill:#ec4899,color:#fff,stroke:none
style EXE fill:#f59e0b,color:#fff,stroke:none
style MOD fill:#3b82f6,color:#fff,stroke:none
style KER fill:#10b981,color:#fff,stroke:none
1.4.1 入口层 (Entrypoints)
vllm/entrypoints/ 是 vLLM 对外的窗口:
- OpenAI API Server(
entrypoints/openai/api_server.py)——最常用的入口,提供/v1/chat/completions等 - LLM 类(
entrypoints/llm.py)——离线批处理,llm = LLM(model="...")直接跑 - CLI 工具(
entrypoints/cli/)——vllm serve命令
核心原则:薄。入口层只做协议适配(OpenAI ↔ 内部 DTO),不含任何推理逻辑。
1.4.2 引擎核心 (Engine Core)
vllm/v1/engine/ + vllm/v1/core/ 是大脑:
- EngineCore(
v1/engine/core.py)——主循环,系统唯一的全局状态持有者 - Scheduler(
v1/core/sched/scheduler.py)——每一拍做什么的决策 - KVCacheManager(
v1/core/kv_cache_manager.py)——KV 块的分配、释放、共享 - BlockPool(
v1/core/block_pool.py)——物理块的 malloc 实现
哲学:集中决策,分布执行。Scheduler 看全局、做决策;Worker 只执行。
1.4.3 执行层 (Executor & Worker)
vllm/v1/executor/ + vllm/v1/worker/ 是肌肉:
- Executor 接口(
v1/executor/abstract.py)——屏蔽部署拓扑的抽象层 - UniProc / Multiproc / Ray 三种 Executor 实现
- GPU Worker(
v1/worker/gpu_worker.py)——单张 GPU 的控制者 - GPUModelRunner(
v1/worker/gpu_model_runner.py)——每 step 前向计算的协调者
哲学:同一套 collective_rpc 接口覆盖单卡 / 多卡 / 多机。
1.4.4 模型层 (Model Executor)
vllm/model_executor/ 是具体模型的实现:
- 模型家族(
model_executor/models/)——Llama / Qwen / Mistral / DeepSeek / Gemma 等 134 个源文件、registry.py共注册 155 种模型 ID(80 文本生成 + 26 embedding + 39 多模态 + 6 投机 + 4 交叉编码器) - 层库(
model_executor/layers/)——Attention、MLP、LayerNorm、Embedding 等基础组件 - 量化(
model_executor/layers/quantization/)——FP8 / GPTQ / AWQ / BnB 等 20+ 种 - 权重加载器(
model_executor/model_loader/)——从 HF / 本地 / S3 加载
哲学:可插拔。新模型 = 实现接口 + PR 提交,不动核心引擎。
1.4.5 内核层 (Kernels)
性能的最后一公里:
- PagedAttention kernels(
vllm/attention/ops/+vllm_flash_attn/) - Triton kernels(
vllm/v1/sample/ops/+ 各种 fused 操作) - Punica LoRA kernels(
vllm/lora/punica_wrapper/) - 量化 kernels(Marlin / Machete / CUTLASS FP8)
- 集合通信 kernels(NCCL wrapper
vllm/distributed/device_communicators/)
这些是 CUDA / Triton 代码,提供 C 接口给上层 PyTorch 调用。
1.5 数据契约:四类跨边界 DTO
理解 vLLM 架构还有一个线索——跨进程/跨层边界的四类 DTO:
graph LR
U[👤 用户请求] --> R1[ClientRequest<br/>API Server 内部]
R1 --> R2[EngineCoreRequest<br/>跨进程最小化]
R2 --> R3[Request<br/>EngineCore 内部有状态]
R3 --> SO[SchedulerOutput<br/>Scheduler → Worker]
SO --> MO[ModelRunnerOutput<br/>Worker → Scheduler]
MO --> ECO[EngineCoreOutput<br/>跨进程回程]
ECO --> U
style R2 fill:#8b5cf6,color:#fff,stroke:none
style SO fill:#f59e0b,color:#fff,stroke:none
style MO fill:#f59e0b,color:#fff,stroke:none
style ECO fill:#8b5cf6,color:#fff,stroke:none
| DTO | 边界 | 字段 |
|---|---|---|
EngineCoreRequest |
API Server → EngineCore | 最小化:token IDs、sampling params、elastic metadata |
Request |
EngineCore 内部 | 含状态:KV block IDs、num_computed_tokens、status |
SchedulerOutput |
EngineCore → Worker | 每 step 指令:num_scheduled_tokens dict、new/cached reqs、finished/preempted |
ModelRunnerOutput |
Worker → EngineCore | 执行结果:sampled_token_ids、logprobs、spec_token_ids |
EngineCoreOutput |
EngineCore → API Server | 用户结果:new_tokens、finish_reason |
数据契约最小化(第 18 章的设计哲学)——每个 DTO 只带下游真正需要的字段。这让每一层都能独立演化:换 API Server 不动 EngineCore、换 Executor 不动 Scheduler。
1.5.1 msgspec.Struct 的四个参数:为什么不用 dataclass
打开 vllm/v1/engine/__init__.py:41 看 EngineCoreRequest 的真实定义:
class EngineCoreRequest(
msgspec.Struct,
array_like=True, # ← ①
omit_defaults=True, # ← ②
gc=False, # ← ③
):
request_id: str
prompt_token_ids: list[int]
mm_inputs: Optional[...]
# ...
不是 Python dataclass、不是 Pydantic BaseModel——用 msgspec.Struct。三个参数组合是 vLLM 跨进程通信性能的核心。
① msgspec.Struct 本身(vs dataclass / Pydantic):
msgspec 是专门做序列化性能的库——其 JSON / MessagePack 编解码是 pure C 实现、比 Pydantic 的 JSON 序列化快 5-10 倍、比 stdlib json 快 3-5 倍。vLLM 用 ZMQ 在 API Server 和 EngineCore 之间传递 EngineCoreRequest 百万次/秒级别——这个选型直接决定了 P99 延迟。
dataclass 没有内置序列化、Pydantic 的验证开销大(每次构造都做类型检查)。msgspec.Struct 介于两者之间:构造时不验证(类型错误会在访问字段时才暴露)、序列化极快。vLLM 选它是因为 request 构造后很快就序列化传输、验证逻辑放在更上层(API Server 入口)完成。
② array_like=True:
默认 msgspec 序列化成 JSON 对象 {"request_id": "abc", "prompt_token_ids": [...], ...}——每个字段带 key 名。array_like=True 切换成按位置的数组:["abc", [...], ...]——省掉所有 key 字符串。
对 EngineCoreRequest 这种有 9 个字段的结构、JSON object 形式的 payload 有 ~100 字节的 key 字符串 overhead;array 形式只有值本身。在一个 vLLM 每秒几百个请求的场景、这是生态级别的带宽节省。代价是编码/解码两端必须严格按相同字段顺序——一端加字段另一端没同步就数据错位。msgspec 通过 schema 强制同版本、让这个代价可控。
③ omit_defaults=True:
字段值等于默认值时不序列化。current_wave: int = 0 这种字段、绝大多数请求都是 0——不发省字节。接收端发现字段缺失时用 Struct 定义里的默认值填。这是一个 "只传变化量" 的经典优化。
④ gc=False:
最精妙的参数——告诉 Python GC "别追踪这个对象的引用环"。Python 默认对每个 container 对象(list/dict/class instance)加 GC tracking——为了检测循环引用。但 EngineCoreRequest 是叶子 DTO、不会产生循环引用(它只持有 primitive 和 list of primitives)。
GC tracking 的代价:每个对象 +16 字节头部、每次分配加入 gen-0 链表、gc.collect() 遍历时被扫到。对一个每秒百万级分配销毁的 DTO、这个 overhead 累积到可见的 CPU 占用。gc=False 让 msgspec 生成的 struct 不进 GC 链表——少一次 list append/remove、少一次 scan、少 16 字节头部。
四个参数合起来让 EngineCoreRequest 的序列化成本接近"裸的二进制"——没有不必要的 key 字符串、没有默认值、没有 GC 开销。这是第 17 章讲 API Server 生产延迟稳定性的物理基础。
1.5.2 EngineCoreEvent 的 time.monotonic 明示限制
对照 EngineCoreEvent(line 74-89):
class EngineCoreEvent(msgspec.Struct):
"""The timestamp is a monotonic timestamps and is used for by the engine
frontend to calculate intervals between engine core events. These
timestamps should not be compared with timestamps from other processes."""
type: EngineCoreEventType
timestamp: float
docstring 明确警告:"timestamps should not be compared with timestamps from other processes"——因为 time.monotonic() 返回的值在每个进程里都是独立的时钟(起点是该进程启动时刻)。把 API Server 进程的 monotonic 时间和 EngineCore 进程的 monotonic 时间直接比较是错的——会得到无意义的"负延迟"或"十小时前"。
正确做法:各进程内部用 monotonic 时间算 interval、跨进程要对齐时间必须用 time.time()(墙钟、UTC)。源码里每一处 EngineCoreEvent 的 timestamp 字段都严格限定在进程内部比较——文档明示、约束显式。这种"文档化的约束"比隐含的"大家别这么用"约定强得多——新贡献者读到就不会犯错。
1.5.3 current_wave 的 DP 竞态注释
EngineCoreRequest 的 current_wave: int = 0 字段(line 64)有个容易忽略的详细注释:
# Used in DP case to indicate which wave of requests this is expected to
# belong to, to cover a race condition where the request is sent before
# a wave finished notification is received.
场景:数据并行(DP)模式下、多 rank 的 EngineCore 实例需要同步"第 N 轮请求何时结束"——rank A 发完 wave-N 的最后一个请求后广播"N 结束"、其他 rank 收到后进入 wave-N+1。但如果 rank B 在还没收到"N 结束"广播时就把下一批请求(本该属于 wave-N+1)发了出去、rank A 会以为它们还属于 wave-N——统计错乱。
current_wave 字段让发送方显式标注请求属于哪一 wave——接收方对照自己当前 wave、不一致就缓冲等同步。消除了一个具体的分布式竞态。
字段值 0 默认代表 "单机场景、没有 wave 概念"——omit_defaults=True 让绝大多数非 DP 请求不需要传这个字段、不额外增加带宽开销。msgspec 三个参数在这里和业务字段设计精确配合——不是一个装饰性选择、是性能和正确性缠在一起的工程系统。
1.6 目录结构快速导航
vllm/
├── v1/ # V1 引擎(默认,本书主线)
│ ├── engine/ # EngineCore, EngineCoreProc, Client
│ ├── core/ # Scheduler, KVCacheManager, BlockPool
│ ├── executor/ # UniProc / Multiproc / Ray executor
│ ├── worker/ # Worker, GPUModelRunner, InputBatch
│ ├── attention/backends/ # FlashAttention / FlashInfer 等后端
│ ├── sample/ # Sampler, logits processor, topk/topp
│ ├── spec_decode/ # 投机解码(Draft / EAGLE / MTP / ngram)
│ ├── structured_output/ # guided decoding (outlines / XGrammar)
│ ├── metrics/ # Prometheus 指标
│ └── kv_cache_interface.py # KV cache 接口定义
│
├── engine/ # V0 遗留(本书不涉及)
├── core/ # V0 遗留
│
├── entrypoints/ # 入口层
│ ├── openai/ # OpenAI 兼容 API
│ │ ├── api_server.py # FastAPI app
│ │ ├── serving_chat.py # Chat 协议
│ │ ├── serving_completion.py
│ │ ├── serving_embedding.py
│ │ └── protocol.py # OpenAI Pydantic models
│ ├── cli/ # vllm serve CLI
│ └── llm.py # 离线推理入口
│
├── model_executor/ # 模型层
│ ├── models/ # 134 .py 文件、registry.py 注册 155 种模型 ID
│ ├── layers/ # attention, mlp, norm, etc.
│ │ └── quantization/ # FP8 / GPTQ / AWQ / ...
│ └── model_loader/ # safetensors / HF / S3 加载
│
├── distributed/ # 分布式
│ ├── parallel_state.py # TP / PP / EP / DP 进程组
│ ├── communication_op.py # all_reduce / all_gather 等
│ ├── device_communicators/ # NCCL / shm_broadcast / XLA / HPU
│ └── kv_transfer/ # KV 跨机传输(Disagg Serving)
│
├── multimodal/ # 多模态:图、视频、音频
├── lora/ # LoRA 适配器 + punica kernels
├── attention/ # 共享 attention 层
├── config/ # 配置类(SchedulerConfig, ModelConfig 等)
├── sampling_params.py # SamplingParams 定义
├── inputs/ # 输入预处理
├── outputs.py # 输出结构
└── version.py # 版本号
读源码的起点:
- 主循环看
v1/engine/core.py::EngineCoreProc.run_busy_loop - 调度看
v1/core/sched/scheduler.py::Scheduler.schedule - 执行看
v1/worker/gpu_model_runner.py::GPUModelRunner.execute_model - 采样看
v1/sample/sampler.py::Sampler.forward - Attention 看
v1/attention/backends/flash_attn.py
从这 5 个文件出发,可以到达系统的任何角落。
1.6.1 vllm/v1/ 21050 行的十个子目录分布
把 v1 整个目录按子模块按行数统计一次——最大的不是 engine,是 worker——
| 子目录 | 行 | 份额 | 内容 |
|---|---|---|---|
worker/ |
4851 | 23% | GPUModelRunner(含 CUDA Graph、InputBatch 维护)——单个最重的子系统 |
engine/ |
4121 | 20% | EngineCore + EngineCoreProc + Client + IPC |
attention/ |
3115 | 15% | FlashAttention / FlashInfer / backend selector |
core/ |
2843 | 14% | Scheduler + KVCacheManager + BlockPool |
sample/ |
1627 | 8% | Sampler + logits_processor + topk/topp kernel 调用 |
structured_output/ |
981 | 5% | outlines / xgrammar 适配 |
metrics/ |
736 | 4% | Prometheus + stats_logger |
spec_decode/ |
692 | 3% | draft model / EAGLE / MTP / ngram 四种路径 |
executor/ |
653 | 3% | UniProc / MultiProc / Ray 三种 executor |
stats/ |
453 | 2% | 运行时统计 |
三点非显然的观察——
- Worker 比 Engine 大 15%——直觉会认为"engine 是主循环、应该最重"——实际是 GPUModelRunner 把一次
execute_model的所有细节都塞在 worker 里:InputBatch reshape、CUDA Graph 选择、sampling 调度、attention meta 构造、KV write 路径——全是 worker 的责任;engine 只是分派器 - attention 3115 行、比 sample 大近一倍——attention 的 backend 选择逻辑(FlashAttention 2 vs 3、FlashInfer、Triton、xFormers、CUDA 原生)每条路径各自维护一份 metadata 构造——多后端兼容的代价;如果只支持一种 backend 能砍掉 2/3
- executor/ 仅 653 行——跨进程执行架构本质上很薄——因为重活(worker 创建、RPC、NCCL 初始化)都委托给 Ray 或
multiprocessing——这条规律在 §14.8.4 kv_transfer 和 §14.6.5 device_communicators 也成立:vLLM 的设计纪律是"重通信语义用成熟库、自己只维护接口契约"
1.6A 顶层模块账本:vllm/ 根目录的 29 个子系统
上节讲完 v1/ 内部十个子目录的行数分布,这一节把视野拉回到 vllm/ 根——整个 vLLM 仓库的子系统拓扑。截至 main 分支 2026-04 的快照,vllm/ 下一共有 29 个顶层子目录加若干核心 .py 文件。按"谁依赖谁"的方向整理一遍、能看清 V1 在整个工程里的定位。
1.6A.1 按职能分成的六个圈层
graph TB
subgraph "圈层 ① 对外协议"
ENT[entrypoints/<br/>OpenAI API/CLI/LLM]
PROT[protocol<br/>serving_* 定义]
end
subgraph "圈层 ② 引擎(新旧并存)"
V1D[v1/<br/>21050 行 · 默认引擎]
V0D[engine/<br/>V0 遗留主循环]
V0C[core/<br/>V0 遗留 scheduler]
end
subgraph "圈层 ③ 执行与通信"
DIST[distributed/<br/>TP/PP/EP/DP + KV 传输]
RAY[ray/<br/>Ray 适配]
end
subgraph "圈层 ④ 模型与权重"
ME[model_executor/<br/>134 files · 155 注册模型]
MM[multimodal/<br/>图/视频/音频]
LORA[lora/<br/>punica + 加载]
TRANS[transformers_utils/<br/>HF 桥接]
TOK[tokenizers/<br/>MistralTokenizer 等]
end
subgraph "圈层 ⑤ 性能底座"
KER[kernels/<br/>自研 CUDA/Triton]
FA[vllm_flash_attn/<br/>FA2/FA3 wrapper]
COMP[compilation/<br/>torch.compile cache]
TU[triton_utils/]
DA[device_allocator/<br/>caching allocator]
end
subgraph "圈层 ⑥ 观测与扩展"
PROF[profiler/]
TRACE[tracing/<br/>OTel 链路]
LOG[logging_utils/]
USAGE[usage/]
PLUG[plugins/<br/>OOT 模型注册]
TP[tool_parsers/]
REASON[reasoning/]
end
ENT --> V1D
V1D --> DIST
V1D --> ME
ME --> KER
ME --> FA
ME --> LORA
V1D --> COMP
V1D --> MM
style V1D fill:#10b981,color:#fff,stroke:none
style V0D fill:#ef4444,color:#fff,stroke:none
style V0C fill:#ef4444,color:#fff,stroke:none
style ENT fill:#8b5cf6,color:#fff,stroke:none
1.6A.2 圈层与章节对应表
| 圈层 | 主要子目录 | 主责章节 | 辅助章节 |
|---|---|---|---|
| ① 对外协议 | entrypoints/、sampling_params.py、outputs.py |
ch17 API Server | ch09 Sampling |
| ② 引擎(V1) | v1/engine、v1/core |
ch02 EngineCore、ch03 Scheduler、ch05 KVCacheManager | ch04 PagedAttention |
| ② 引擎(V0 遗留) | engine/、core/ |
本书不覆盖 | ch18 设计哲学(演化脉络) |
| ③ 执行与通信 | v1/executor、v1/worker、distributed/、ray/ |
ch06 Worker & Executor、ch14 TP/PP/EP/DP | ch17 生产部署 |
| ④ 模型与权重 | model_executor/、multimodal/、lora/、transformers_utils/、tokenizers/ |
ch07 模型加载、ch15 多模态、ch16 LoRA | ch08 ModelRunner |
| ⑤ 性能底座 | kernels/、vllm_flash_attn/、compilation/、triton_utils/、device_allocator/ |
ch04 PagedAttention、ch08 CUDA Graph、ch13 量化 | ch18 哲学(分层复用) |
| ⑥ 观测与扩展 | profiler/、tracing/、logging_utils/、usage/、plugins/、tool_parsers/、reasoning/ |
ch17 生产部署 | ch18 可扩展性 |
这张表是阅读本书时最常回翻的一张——当你定位到源码某个文件、忘记它属于哪一章时、循"文件 → 子目录 → 圈层 → 章节"的四跳路径就能回到正确的位置。
1.6A.3 "V1 与 V0 并存"的工程含义
读者往往第一眼就注意到一个"反常"的现象:v1/ 是默认引擎、但 engine/ 和 core/ 两个 V0 遗留目录并没有被删除。这不是疏忽、而是 vLLM 社区的演化纪律。
V0 代码仍被保留的三个原因:
- 少量旧路径没迁完——截至 2026-04,某些实验性后端(部分 Neuron/TPU 硬件适配、特定 spec decode 分支)仍在用 V0 接口;直接删除会让硬件厂商的 out-of-tree 插件一夜崩掉。
- 回归对照——当 V1 调度器出现诡异性能回退时、社区维护者可以在相同硬件上切回 V0 对照复现,这是工程 debugging 的"备胎"。
- 教学与考古价值——V0 相对"朴素"的单进程架构对新 contributor 更好读,对照 V1 能把"为什么这样重构"讲清楚。第 18 章会用这条对照线索贯穿始终。
本书的态度:全书 17 章(除第 18 章个别对照段落)都以 V1 为主线。当你翻源码看到 vllm/engine/llm_engine.py(V0)和 vllm/v1/engine/core.py(V1)名字相近、不要混淆——V1 路径永远在 vllm/v1/ 下。一个实用判定:import 语句里出现 vllm.v1.xxx 就是 V1 代码,其他路径大概率是 V0 或跨版本共用。
1.6B 架构全景大图:从 LLMEngine 到 Kernels 的一次性视图
前面几节把系统切成"进程/阶段/子系统/DTO"四个维度各讲一次——每次都强调某一面、但缺一张**"一次性把所有东西串到同一张图上"的全景图**。这一节补上这张图、并用它作为本章最重要的"离场帽子"。
graph TB
USER([👤 外部调用者<br/>HTTP / SDK])
subgraph "进程 P1 · API Server(CPU)"
API[AsyncLLM<br/>协议入口]
CLI_TOK[tokenizer / detokenizer]
CLI_SSE[SSE / 长连接]
end
subgraph "进程 P2 · EngineCore(CPU 主循环)"
LLME[LLMEngine / EngineCoreProc]
SCHED[Scheduler<br/>调度决策]
KVM[KVCacheManager<br/>块分配]
BP[BlockPool<br/>物理块]
SG[Request + SequenceGroup<br/>状态机 + 迭代状态]
EXEC[Executor<br/>抽象层]
end
subgraph "进程 P3..PN · Worker × N(GPU)"
W[Worker<br/>单卡总控]
MR[GPUModelRunner<br/>InputBatch · CUDA Graph]
MOD[Model<br/>Llama / Qwen / ...]
ATT[Attention Backend<br/>FlashAttention / FlashInfer]
SAMP[Sampler]
KVT[KV Cache Tensors<br/>on GPU HBM]
end
subgraph "底层 · Kernels(C++/Triton)"
PAK[PagedAttention Kernel]
QUANT[FP8 / GPTQ / Marlin Kernel]
NCCL[NCCL / shm_broadcast]
end
USER -->|HTTP POST| API
API --> CLI_TOK
API -->|ZMQ PUSH<br/>EngineCoreRequest| LLME
LLME --> SG
SG --> SCHED
SCHED --> KVM
KVM --> BP
SCHED -->|SchedulerOutput| EXEC
EXEC -->|共享内存 MQ<br/>广播 diff| W
W --> MR
MR -->|input_ids/block_tables| MOD
MOD --> ATT
ATT --> PAK
MOD --> QUANT
ATT -.读写.-> KVT
MR --> SAMP
SAMP -->|ModelRunnerOutput| W
W -->|shm MQ 汇总| EXEC
EXEC -->|回程| LLME
LLME -->|EngineCoreOutput<br/>ZMQ PUSH| API
API --> CLI_SSE
CLI_SSE -->|SSE chunks| USER
W <-->|AllReduce / AllGather| NCCL
style API fill:#8b5cf6,color:#fff,stroke:none
style LLME fill:#ec4899,color:#fff,stroke:none
style SCHED fill:#f59e0b,color:#fff,stroke:none
style KVM fill:#f59e0b,color:#fff,stroke:none
style W fill:#10b981,color:#fff,stroke:none
style MR fill:#10b981,color:#fff,stroke:none
style PAK fill:#3b82f6,color:#fff,stroke:none
style NCCL fill:#3b82f6,color:#fff,stroke:none
这张图上有几条容易被忽视的边、值得单独强调:
① Attention 到 KV Cache Tensors 是双向虚线——KV 的读写发生在同一张前向图里、不是"先读后写"两个独立阶段。FlashAttention kernel 在计算当前 step 的 attention 时、同时把新生成的 K/V 写回缓存——这是 V1 把 KV write 融合进 attention 的关键优化(第 4 章深入)。如果把它画成"读→计算→写"三段、会误导读者以为有三次内存往返。
② Executor 既下行(广播 SchedulerOutput)、也上行(汇总 ModelRunnerOutput)——这是 collective_rpc 接口的本质:一次 RPC 既是广播、也是 gather;Executor 抽象层把"请求所有 worker 执行某函数、收集所有返回值"封装成一个调用。第 6 章会贴 abstract.py 的完整接口签名。
③ NCCL 出现在 Worker 之间而不是 Worker 与 EngineCore 之间——这是 TP/PP/EP 的通信物理定位:所有跨 GPU 集合通信都发生在 Worker 进程群内部、EngineCore 永远不参与 NCCL。这解释了为什么 distributed/parallel_state.py 的进程组初始化发生在 Worker 启动阶段(第 14 章)。
④ Model 到 FP8/GPTQ Kernel 没有经过 Attention——量化只在 Linear 层(QKV projection、O projection、MLP up/down/gate)上发生、Attention 本身的 softmax/matmul 保持高精度。第 13 章会讲这个"量化边界"的设计取舍。
把这张图钉在读源码时的显示器角落——看到任何一个模块都能回到它在全景里的位置。
1.6C 读源码的三个"切入姿势"
理解了架构、下一步是真正打开 IDE 读 vLLM 源码。不同目的下的切入姿势不同——走错路会花几小时读无关的代码。给出三个最常用的入口:
1.6C.1 姿势 A:追"一条请求"(推荐第一次读源码的人)
从 vllm/entrypoints/openai/api_server.py 的 /v1/chat/completions handler 入手、用 IDE 的 Go to Definition 一路跟进去、直到进入 vllm/v1/worker/gpu_model_runner.py::execute_model。中间会经过:
serving_chat.py的协议适配(chat template 渲染)AsyncLLM的generate()(vllm/v1/engine/async_llm.py)- ZMQ client →
EngineCoreProc(vllm/v1/engine/core_client.py+core.py) Scheduler.schedule()(vllm/v1/core/sched/scheduler.py)- Executor 的
execute_model(vllm/v1/executor/multiproc_executor.py或ray_executor.py) - Worker 的
execute_model+GPUModelRunner
追完一遍后你就知道"每一层在哪个文件"——这比读任何综述性文档都有效。
1.6C.2 姿势 B:追"一个特性"(想加 feature 或 fix bug 的人)
从 tests/v1/ 目录里找对应特性的测试用例、以测试为锚点反向定位源码。例如:
- 想懂前缀缓存 → 读
tests/v1/core/test_prefix_caching.py→ 追到v1/core/kv_cache_manager.py - 想懂 Chunked Prefill → 读
tests/v1/core/test_scheduler.py的相关测试 → 追到v1/core/sched/scheduler.py的_schedule_prefills - 想懂 Spec Decode → 读
tests/v1/spec_decode/→ 追到v1/spec_decode/
测试代码用最少的 setup 触发目标代码路径、是天然的"最小复现案例"。配合 pytest 的 --trace 可以直接单步调试。
1.6C.3 姿势 C:追"一次 benchmark"(做性能调优的人)
从 benchmarks/benchmark_serving.py 或 benchmarks/benchmark_throughput.py 入手、用 PyTorch Profiler / NVIDIA Nsight 录一段 trace、再回源码定位。这个姿势回答"时间到底花在哪"——光靠读代码猜不到真实瓶颈。第 17 章的生产部署章会展开这种"trace-driven tuning"的方法论。
1.6C.4 三种姿势的共同底层:查找入口
无论哪种姿势、最关键的能力是快速定位入口文件。记住这五个"根入口":
| 入口 | 路径 | 用途 |
|---|---|---|
| HTTP 根 | vllm/entrypoints/openai/api_server.py |
所有 OpenAI 协议请求起点 |
| 离线根 | vllm/entrypoints/llm.py::LLM.generate |
llm.generate(prompts) 起点 |
| 主循环根 | vllm/v1/engine/core.py::EngineCoreProc.run_busy_loop |
整个系统的心跳 |
| 调度根 | vllm/v1/core/sched/scheduler.py::Scheduler.schedule |
每 step 决策的起点 |
| 前向根 | vllm/v1/worker/gpu_model_runner.py::GPUModelRunner.execute_model |
GPU 计算的起点 |
把这五个路径抄写下来贴在显示器边——比任何"vLLM 入门 PPT"都实用。
1.7 章节地图:本书每一章对应什么
用这张表作为全书的导航:
graph LR
subgraph "第一篇 · 全景"
C1["ch01 架构总览<br/>当前章节,全书地图"]
end
subgraph "第二篇 · 引擎核心"
C2["ch02 EngineCore"]
C3["ch03 Scheduler"]
C4["ch04 PagedAttention"]
C5["ch05 KVCacheManager"]
end
subgraph "第三篇 · 执行层"
C6["ch06 Worker & Executor"]
C7["ch07 模型加载"]
C8["ch08 ModelRunner & CUDA Graph"]
C9["ch09 Sampling"]
end
subgraph "第四篇 · 性能优化"
C10["ch10 前缀缓存"]
C11["ch11 Chunked Prefill"]
C12["ch12 投机解码"]
C13["ch13 量化"]
end
subgraph "第五篇 · 分布式 + 外延"
C14["ch14 TP/PP/EP/DP"]
C15["ch15 多模态"]
C16["ch16 LoRA"]
C17["ch17 API Server"]
C18["ch18 设计哲学"]
end
C1 --> C2 --> C3 --> C4 --> C5
C5 --> C6 --> C7 --> C8 --> C9
C9 --> C10 --> C11 --> C12 --> C13
C13 --> C14 --> C15 --> C16 --> C17 --> C18
style C1 fill:#3b82f6,color:#fff,stroke:none
style C18 fill:#10b981,color:#fff,stroke:none
1.7.1 按问题驱动阅读
如果你不想全书读一遍,这里是常见问题的章节映射:
| 你的问题 | 主读 | 辅读 |
|---|---|---|
| 服务 p99 TTFT 不稳定 | ch03 Scheduler, ch11 Chunked Prefill | ch10 前缀缓存 |
| KV OOM 频繁 | ch04 PagedAttention, ch05 KVCacheManager | ch10 前缀缓存 |
| 想让 decode 更快 | ch12 投机解码, ch13 量化 | ch08 CUDA Graph |
| 想加新模型 | ch07 模型加载, ch08 ModelRunner | 模型家族源码 model_executor/models/ |
| 想上多机 | ch06 Executor, ch14 TP/PP/EP/DP | ch17 生产部署 |
| 想 host 多个微调 | ch16 LoRA | ch13 量化 |
| 接图片/视频输入 | ch15 多模态 | ch07 模型加载 |
| 为什么 V1 比 V0 快 1.7× | ch02、ch03、ch06、ch08 交叉 | ch18 哲学 |
| 部署到 K8s | ch17 API Server & 生产部署 | ch06 Executor |
1.7.2 按角色阅读
- AI 基础设施工程师:ch01 → ch02 → ch03 → ch06 → ch14 → ch17
- 推理算法研究者:ch01 → ch04 → ch10 → ch11 → ch12 → ch13
- 系统架构师:ch01 → ch02 → ch06 → ch14 → ch18(强推荐,架构原则全在这)
- 好奇的开发者:ch01 → ch04 → ch18,三章看完能在饭桌上聊 vLLM
1.7A 组件协作序列图:一拍里到底发生了什么
给"一拍(step)"画一张精细的 sequence diagram,粒度从 LLMEngine 进入主循环一直到 Worker 把 ModelRunnerOutput 送回来。这张图比前面的 block diagram 多一个维度:时间轴——谁在等谁、谁和谁可以并行。
sequenceDiagram
autonumber
participant API as API Server<br/>AsyncLLM
participant CORE as EngineCoreProc<br/>run_busy_loop
participant SCHED as Scheduler
participant KVM as KVCacheManager
participant BP as BlockPool
participant RS as Request + State
participant EXEC as Executor
participant W as Worker × N
participant MR as GPUModelRunner
participant M as Model + Attention
participant S as Sampler
API ->>+ CORE: EngineCoreRequest<br/>(ZMQ PUSH)
CORE ->> RS: 构造 Request 对象<br/>status=WAITING
CORE ->>+ SCHED: schedule()
SCHED ->> RS: 枚举 waiting / running 队列
SCHED ->>+ KVM: allocate_slots(req, n_tokens)
KVM ->>+ BP: get_free_blocks(n)
BP -->>- KVM: block_ids / 失败
KVM -->>- SCHED: alloc 结果
alt 无空闲块
SCHED ->> KVM: preempt(lowest_priority)
KVM ->> BP: free_blocks(victim)
end
SCHED -->>- CORE: SchedulerOutput<br/>(num_scheduled_tokens)
CORE ->>+ EXEC: execute_model(SchedulerOutput)
EXEC ->>+ W: 共享内存广播 diff
W ->> W: _update_states<br/>(差量更新 InputBatch)
W ->>+ MR: _prepare_inputs
MR ->> MR: 构造 input_ids / block_tables / slot_mapping
MR ->>+ M: forward(CUDA Graph replay 或 eager)
M ->> M: Attention (读/写 KV cache)
M -->>- MR: hidden_states / logits
MR ->>+ S: sampler.forward(logits)
S -->>- MR: sampled_token_ids
MR -->>- W: ModelRunnerOutput
W -->>- EXEC: 汇总
EXEC -->>- CORE: ModelRunnerOutput
CORE ->> RS: update_from_output<br/>(推进 num_computed_tokens)
CORE ->>- API: EngineCoreOutput<br/>(ZMQ PUSH)
API ->> API: detokenize + SSE 下发
1.7A.1 五个值得品一遍的细节
细节 ①:allocate_slots 可能返回"失败"——第 14 步。不是每一拍都能 alloc 成功;空闲块不够时会走 preempt 分支(第 16 步)把优先级最低的 Request 踢出 running、它的 KV 块归还 BlockPool。被抢占的 Request 的 status 会回到 PREEMPTED、下一拍可能被重新调度。这是第 3 章讲调度公平性时的核心机制。
细节 ②:_update_states 是"差量"不是"全量"——第 21 步。V1 的 Worker 是有状态的、自己维护 InputBatch(所有 RUNNING 请求的 token IDs、positions、block tables);每一拍只发"变化量"(新增请求、追加 token、完成请求)。V0 每 step 要广播全部 RUNNING 请求的完整状态——1000 并发下广播开销能占 step 时间的 10%。这是七大变革里 "有状态 Worker" 的具体落地。
细节 ③:CUDA Graph replay 或 eager——第 24 步。是否走 graph 取决于本拍的 batch 尺寸是否在 capture size 集合里。V1 的分段 CUDA Graph 只对"后半段"(Attention 之后的 MLP/LayerNorm)capture;Attention 本身因为 KV 长度可变、用 eager 跑。第 8 章会把 13 张 graph 的切分规则完整列出来。
细节 ④:Attention (读/写 KV cache)——第 26 步。这一步的 "读写" 是同一个 kernel 内部完成的、没有独立的 write_kv_cache 调用。这就是前面 1.6B 图强调的"双向虚线"——FlashAttention 直接把本 step 生成的 K/V 用 store_kv_cache PTX 指令写回 paged 内存。
细节 ⑤:update_from_output 的副作用——第 33 步。这一步不只是"记账更新 token 数"——它还会判断 Request 是否完成(命中 stop_token、达到 max_tokens、触发 stop_str)、完成的 Request 的 KV 块在下一拍开头归还 BlockPool、对应的 Request 从 RUNNING 移到 FINISHED 队列。这里也是 Prometheus 指标(第 17 章)打点的地方:每 step 完成几个 req、每 step 消耗多少 token,都在这里上报。
1.7A.2 哪些步骤可以并行?
这张图是一拍的串行因果链、但几拍之间有流水线并行可能:
- 拍 N 的 CPU 调度 ∥ 拍 N-1 的 GPU 前向:默认不并行(EngineCore 单线程)。但开启
enable_chunked_prefill和某些异步配置后、Scheduler 下一拍的调度决策可以和当前拍的 GPU 执行重叠——第 11 章详细讨论。 - API Server 的 detokenize ∥ EngineCore 的下一拍:天然并行。detokenize 发生在独立进程、不影响 EngineCore。这是 V1 架构相比 V0 最直观的吞吐收益来源之一。
- Worker 的 NCCL 通信 ∥ 下一层计算:TP 场景下、All-Reduce 可以和 MLP 的下一个 Linear 启动重叠(第 14 章讲通信-计算 overlap)。
记住这个并行清单——性能调优的许多决策(要不要开 chunked prefill、TP 度设多大、API Server 实例数)都围绕这三组并行可能性展开。
1.7B 17 章与架构的精确映射
前面 1.7 节给了按问题和按角色的阅读路径、这一节补上最细粒度的一张映射表——本书每一章 depths into 架构图的哪一个具体组件。适合"想研究某一组件时精确定位章节"的场景。
| 章节 | 架构组件 | 源码主路径 | 本章要解决的核心问题 |
|---|---|---|---|
| ch01(当前) | 全景 | vllm/ 根 |
读一次,知道"哪一章讲哪部分" |
| ch02 EngineCore | EngineCore + IPC |
v1/engine/core.py v1/engine/async_llm.py v1/engine/core_client.py |
run_busy_loop 每一拍做什么、ZMQ 怎么收发、如何处理 backpressure |
| ch03 Scheduler | Scheduler |
v1/core/sched/scheduler.py |
为什么一个字典能替代 V0 的两条代码路径、抢占/连续 batching/chunked 怎么做决策 |
| ch04 PagedAttention | Attention Backend + KV Cache Tensors |
v1/attention/backends/flash_attn.py vllm_flash_attn/ vllm/attention/ |
为什么分块能把碎片从 60% 降到 4%、block_tables 的物理布局 |
| ch05 KVCacheManager | KVCacheManager + BlockPool |
v1/core/kv_cache_manager.py v1/core/block_pool.py |
allocate/free/preempt 的数据结构(free_list、ref_count、hash table) |
| ch06 Worker & Executor | Executor + Worker |
v1/executor/abstract.py v1/executor/multiproc_executor.py v1/worker/gpu_worker.py |
collective_rpc 统一接口、三种 Executor 各自适合什么场景 |
| ch07 模型加载 | Model Loader |
model_executor/model_loader/ model_executor/models/registry.py |
HF / S3 / safetensors 怎么加载、qkv_proj 怎么融合 |
| ch08 ModelRunner & CUDA Graph | GPUModelRunner + compilation/ |
v1/worker/gpu_model_runner.py compilation/ |
InputBatch 怎么维护、为什么分段 capture 比整图 capture 快 |
| ch09 Sampling | Sampler |
v1/sample/sampler.py v1/sample/ops/ |
top-k/top-p/temperature/penalty 怎么在一个 kernel 里融合 |
| ch10 前缀缓存 | KVCacheManager.hash_to_block |
v1/core/kv_cache_manager.py 的 hash 路径 |
零开销怎么做到(<1% 损耗)、hash 碰撞怎么处理 |
| ch11 Chunked Prefill | Scheduler._schedule_prefills |
v1/core/sched/scheduler.py |
怎么切、切多大、和 decode 共存的 token budget 模型 |
| ch12 投机解码 | v1/spec_decode/ |
v1/spec_decode/ |
draft / EAGLE / MTP / ngram 四条路径在调度上的差异 |
| ch13 量化 | model_executor/layers/quantization/ |
model_executor/layers/quantization/ |
FP8 / GPTQ / AWQ / Marlin 的 kernel 选择矩阵 |
| ch14 TP/PP/EP/DP | distributed/ + Worker 进程组 |
distributed/parallel_state.py distributed/communication_op.py |
并行策略的通信模式、何时混合使用 |
| ch15 多模态 | multimodal/ + model_executor/models/ VLM 家族 |
multimodal/ model_executor/models/qwen2_vl.py 等 |
图像预处理何时发生、vision encoder 的 placement |
| ch16 LoRA | lora/ + Punica kernels |
lora/ lora/punica_wrapper/ |
多 LoRA 共存的 batching、Punica 的 kernel 融合思路 |
| ch17 API Server | entrypoints/ + 可观测 |
entrypoints/openai/ v1/metrics/ tracing/ |
生产环境的 p99 调优、SSE 长连接稳定性 |
| ch18 设计哲学 | 全栈回顾 | —— | 把前 17 章的设计决策提炼成 10 条可迁移的工程原则 |
怎么用这张表:把本章学到的架构图放左侧、这张表放右侧——随时可以定位"我想深入的那个框对应哪一章、源码在哪个文件"。第 2 章开始、每一章的第一张图都会高亮当前章在全景图里的位置、维持这种"大地图 + 当前位置"的认知框架。
1.7B.0 三条"贯穿 17 章的主线"
除了每章独立的映射、还有三条跨章节的主线、理解它们能帮你把分散的知识点串成脉络。
主线 A · "数据流":Request 从构造到销毁一共跨越 ch02/ch03/ch05/ch06/ch08/ch09 六章。每一章讲这个对象在它那一层的新字段和状态变化——读完就能完整理解"一个请求在系统里的生命周期"。
主线 B · "内存:KV 内存管理散落在 ch04(物理布局)、ch05(块池数据结构)、ch10(前缀共享)、ch11(chunked 对 KV 压力的缓解)、ch14(跨机 KV 传输)五章。这条线是理解 vLLM 性能天花板的关键——GPU 显存永远是最稀缺的资源、整本书一半的性能优化都绕着它转。
主线 C · "并行:并行不是只有 ch14 讲——ch02(进程并行)、ch06(Executor 抽象)、ch08(CUDA Graph 内部并行)、ch14(TP/PP/EP/DP)、ch17(副本并行)五章各自讨论不同粒度的并行。读懂这条线你就能回答"在任何时刻、vLLM 里有多少个层次的并行在同时发生"这个看似简单实则刁钻的问题。
看到后续章节里出现"本章是主线 A/B/C 的第 N 站"类提示时——把它串回这条线、比孤立地读单章收获大得多。
1.7B.1 "先看一遍全景再深入"的阅读节奏
很多读者担心"第 1 章介绍了一堆还没讲细节的东西、会不会记不住"——这正是本章的设计目的:不求你现在就懂所有细节、只求你脑子里有一张"粗糙但完整"的地图。后续每一章都会:
- 回指本章的架构图、告诉你"我们正在深入这个框"
- 把该框内部展开、画同风格的子图
- 结束时把学到的内容填回本章的全景图
这样读完 18 章、你不仅知道每个组件怎么实现、还知道它在整个系统里处于什么位置、和相邻组件怎么协作。这就是本书追求的"体系化"——不是"讲完所有细节"、是"让读者自己能把细节拼回整体"。
1.7C 与同类推理框架的架构对比
vLLM 不是唯一的 LLM 推理框架——TGI(Hugging Face Text Generation Inference)、TensorRT-LLM(NVIDIA)、SGLang、LMDeploy 都在同一竞技场上。读懂 vLLM V1 的架构、回头对照这些同类、能理解为什么 vLLM 在开源生态里跑赢——不是某一个点强、是整体架构选型的一致性。
| 维度 | vLLM V1 | TGI | TensorRT-LLM | SGLang |
|---|---|---|---|---|
| 进程模型 | API / Engine / Worker 三进程 | Router(Rust)+ Shard(Python)双进程 | 单 executor 进程、多 CUDA stream | Router + Scheduler + Worker 三进程 |
| KV 管理 | PagedAttention + 块池 | Paged(继承 vLLM 思想)+ 定长 slot | Paged KV Cache(C++ 实现) | RadixAttention(Radix 树前缀共享) |
| 调度 | 统一 token-level 调度、chunked 默认开 | continuous batching + chunked prefill | Inflight batching(近似 continuous) | Radix 树驱动调度 |
| 投机解码 | Draft / EAGLE / MTP / ngram 四种 | 仅 medusa 和 n-gram | Medusa / EAGLE / draft | EAGLE-2 / ngram |
| 可扩展性 | Ray / Multiproc / UniProc Executor | 单机多卡为主、多机依赖外部编排 | 静态并行配置、启动时固定 | 类似 vLLM,Python 主导 |
| 语言栈 | Python 为主 + CUDA kernels | Rust(router)+ Python(model) | C++ 为主、Python 仅前端 | Python 为主 |
| 生态包容性 | HF 任意模型即插即用 | 聚焦少量 HF 模型、有深度优化 | 需要导出 + 引擎构建(门槛高) | HF 模型 + 特色前端 RadixLang |
三个非显然的观察:
- "三进程"不是 vLLM 独有、但"调度 + 块池 + 统一 token 调度"组合最成熟——TGI 和 SGLang 都是三进程、但各自在调度粒度和 KV 管理上有不同倾向;vLLM 在"通用性 × 性能 × 生态"三维度上取得最平衡的点。
- TensorRT-LLM 走的是"静态最优"路线——编译期把并行度、batch size、kernel 选择全部固定、换配置要重建引擎;跑对了场景极快、但失去了 vLLM 的动态调度弹性。第 18 章会把这种"静态 vs 动态"的取舍拓展成一条工程哲学。
- SGLang 的 RadixAttention 和 vLLM 的前缀缓存是"同一个思想的两种实现"——都是把 KV 按 token 前缀去重;vLLM 用块级 hash 表、SGLang 用 trie。不是谁对谁错,是数据结构的不同选择——vLLM 的选择换来更简单的块分配代码、SGLang 的选择换来更自然的多分支共享(tree-of-thought 推理)。
读完本章、对 vLLM 的架构选择有了"为什么是这样"的答案——不是因为这些选择唯一正确、而是因为它们在 "通用开源框架" 这个定位下是最优解组合。
1.8 本章小结
本章通过"跟一条请求走完全程"的方式,建立了 vLLM V1 架构的全景认知:
- 六阶段旅程:HTTP → tokenize → 排队 → 调度 → GPU 前向 → detokenize → SSE 输出
- 三进程拓扑:API Server(CPU 密集)+ EngineCore(CPU 密集)+ Worker(GPU 密集),物理并行、GIL 隔离
- 两套 IPC 协议:ZMQ(API↔EngineCore,低频大粒度)+ 共享内存 MQ(EngineCore↔Worker,高频零拷贝)
- 七个 V0→V1 变革:多进程化、统一调度、有状态 Worker、零开销前缀缓存、分段 CUDA Graph、异步 detokenize、Executor 抽象
- 五大子系统:Entrypoints / Engine Core / Executor+Worker / Model Executor / Kernels
- 四类 DTO 数据契约:EngineCoreRequest / Request / SchedulerOutput / ModelRunnerOutput
- 源码目录导航:V1 代码在
v1/下,从v1/engine/core.py起步 - 章节地图:17 章按"全景 → 核心 → 执行 → 优化 → 外延 → 哲学"组织
- 物理骨架:v1/ 21050 行、worker 23% 是最大子系统、executor 3% 是最薄(重活委托给 Ray / multiprocessing)
- 六圈层拓扑:从"对外协议 → 引擎 → 执行与通信 → 模型与权重 → 性能底座 → 观测与扩展",29 个顶层子目录全部对号入座
- 全景大图:
AsyncLLM → EngineCore → Scheduler → KVCacheManager → Executor → Worker → ModelRunner → Attention ↔ KV Cache → Sampler一张图串全栈 - 一拍 sequence:33 个步骤粒度的组件协作图,标出"差量更新 InputBatch"、"CUDA Graph replay vs eager"、"Attention 内部融合 KV write" 等五个非显然细节
- 17 章精确映射:每章对应全景图的某个具体组件、主路径源码、要解决的核心问题——阅读时随时定位
1.9 下一步
第 2 章会钻进 EngineCoreProc.run_busy_loop——把本章反复提到的"一拍"拆成十几个微观动作、贴 v1/engine/core.py 的每一段关键代码,并讲清楚 ZMQ/共享内存两套 IPC 在源码层面到底长什么样。如果你读本章时对"三进程/两协议/差量更新"三个概念产生了"直觉上懂但想看代码"的冲动——第 2 章就是为这种冲动准备的。
翻开前、建议再看一眼 1.6B 的全景图——第 2 章的每一段代码、都会对应这张图里的一个位置。读完第 2 章、整张全景图的"中央大脑"部分(API Server + EngineCore 连接线)会从"概念"变成"代码"。
延伸阅读
- vLLM V1 Alpha 发布博文:https://blog.vllm.ai/2025/01/27/v1-alpha-release.html
- V1 设计 RFC:GitHub Issue #8779
- PagedAttention 原论文:Kwon et al., "Efficient Memory Management for Large Language Model Serving with PagedAttention", SOSP 2023 (arXiv:2309.06180)
- FlashAttention 系列:Dao et al., NeurIPS 2022/2023
源码起点
- 主循环:
vllm/v1/engine/core.py- Scheduler:
vllm/v1/core/sched/scheduler.py- Worker:
vllm/v1/worker/gpu_worker.py- ModelRunner:
vllm/v1/worker/gpu_model_runner.py- API Server:
vllm/entrypoints/openai/api_server.py