vLLM 推理内核深度解析

第1章 架构总览:从一条请求读懂整个 vLLM

作者 杨艺韬 · 11,508 字

第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 做三件事:

  1. 参数校验:用 Pydantic schema 校验 modeltemperaturetop_pmessages 结构等。校验不通过直接返回 400。
  2. Chat Template 渲染 + Tokenize:messages 数组通过模型自带的 chat_template(Jinja2 格式,来自 tokenizer 的元数据)渲染成单一 prompt 字符串,再用 tokenizer 分词成 prompt_token_ids
  3. 构造 EngineCoreRequest:把 token IDs、sampling params、请求元数据打包成一个最小化的跨进程 DTO。

关键问题:分词为什么放在 API Server 而不是 Worker 或 EngineCore?

答案在 V0 的痛:V0 时代分词和引擎主循环在同一进程内,一个 8000-token 的长 prompt 分词要几十毫秒,期间 EngineCore 无法调度别的请求——所有 decode 流被卡住。V1 把分词推到独立的 API Server 进程,CPU 密集的文本处理和 GPU 调度真正并行

除此之外还有收益:

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() 的第一步是调用 Schedulervllm/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={...},        # 本拍完成的请求
    ...
)

这个看似轻描淡写的字典背后,是调度器做了数十个小决策:

调度逻辑的精妙之处在第 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 对应一个进程)收到后:

  1. _update_states:差量更新本地 InputBatch——新请求添加槽位、老请求追加 token
  2. _prepare_inputs:构造 GPU 输入张量(input_ids、positions、block_tables、slot_mapping、cu_seqlens)
  3. execute_model:调用 ModelRunner 跑前向
    • 若开了 CUDA Graph → pad 到最近的 capture size → replay
    • 否则 eager 模式
  4. sampler.forward:从 logits 采样出新 token
  5. 返回 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 后:

  1. detokenize:token_id → 字符片段(Rust tokenizer 快到纳秒级)
  2. SSE 封装data: {"choices": [{"delta": {"content": "Page"}}]}\n\n
  3. 通过 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 代码。后果:

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。三个进程物理并行

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 ↔ 内部 DTO),不含任何推理逻辑。

1.4.2 引擎核心 (Engine Core)

vllm/v1/engine/ + vllm/v1/core/ 是大脑:

哲学:集中决策,分布执行。Scheduler 看全局、做决策;Worker 只执行。

1.4.3 执行层 (Executor & Worker)

vllm/v1/executor/ + vllm/v1/worker/ 是肌肉:

哲学:同一套 collective_rpc 接口覆盖单卡 / 多卡 / 多机

1.4.4 模型层 (Model Executor)

vllm/model_executor/ 是具体模型的实现:

哲学:可插拔。新模型 = 实现接口 + PR 提交,不动核心引擎。

1.4.5 内核层 (Kernels)

性能的最后一公里:

这些是 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:41EngineCoreRequest 的真实定义:

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 EngineCoreEventtime.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 竞态注释

EngineCoreRequestcurrent_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                  # 版本号

读源码的起点

  1. 主循环看 v1/engine/core.py::EngineCoreProc.run_busy_loop
  2. 调度看 v1/core/sched/scheduler.py::Scheduler.schedule
  3. 执行看 v1/worker/gpu_model_runner.py::GPUModelRunner.execute_model
  4. 采样看 v1/sample/sampler.py::Sampler.forward
  5. 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% 运行时统计

三点非显然的观察——

  1. Worker 比 Engine 大 15%——直觉会认为"engine 是主循环、应该最重"——实际是 GPUModelRunner 把一次 execute_model 的所有细节都塞在 worker 里:InputBatch reshape、CUDA Graph 选择、sampling 调度、attention meta 构造、KV write 路径——全是 worker 的责任;engine 只是分派器
  2. attention 3115 行、比 sample 大近一倍——attention 的 backend 选择逻辑(FlashAttention 2 vs 3、FlashInfer、Triton、xFormers、CUDA 原生)每条路径各自维护一份 metadata 构造——多后端兼容的代价;如果只支持一种 backend 能砍掉 2/3
  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.pyoutputs.py ch17 API Server ch09 Sampling
② 引擎(V1) v1/enginev1/core ch02 EngineCore、ch03 Scheduler、ch05 KVCacheManager ch04 PagedAttention
② 引擎(V0 遗留) engine/core/ 本书不覆盖 ch18 设计哲学(演化脉络)
③ 执行与通信 v1/executorv1/workerdistributed/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 代码仍被保留的三个原因

本书的态度:全书 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

这张图上有几条容易被忽视的边、值得单独强调:

AttentionKV 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 章)。

ModelFP8/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。中间会经过:

  1. serving_chat.py 的协议适配(chat template 渲染)
  2. AsyncLLMgenerate()vllm/v1/engine/async_llm.py
  3. ZMQ client → EngineCoreProcvllm/v1/engine/core_client.py + core.py
  4. Scheduler.schedule()vllm/v1/core/sched/scheduler.py
  5. Executor 的 execute_modelvllm/v1/executor/multiproc_executor.pyray_executor.py
  6. Worker 的 execute_model + GPUModelRunner

追完一遍后你就知道"每一层在哪个文件"——这比读任何综述性文档都有效。

1.6C.2 姿势 B:追"一个特性"(想加 feature 或 fix bug 的人)

tests/v1/ 目录里找对应特性的测试用例、以测试为锚点反向定位源码。例如:

测试代码用最少的 setup 触发目标代码路径、是天然的"最小复现案例"。配合 pytest 的 --trace 可以直接单步调试。

1.6C.3 姿势 C:追"一次 benchmark"(做性能调优的人)

benchmarks/benchmark_serving.pybenchmarks/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 按角色阅读

1.7A 组件协作序列图:一拍里到底发生了什么

给"一拍(step)"画一张精细的 sequence diagram,粒度从 LLMEngine 进入主循环一直到 WorkerModelRunnerOutput 送回来。这张图比前面的 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 哪些步骤可以并行?

这张图是一拍的串行因果链、但几拍之间有流水线并行可能:

记住这个并行清单——性能调优的许多决策(要不要开 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 章介绍了一堆还没讲细节的东西、会不会记不住"——这正是本章的设计目的不求你现在就懂所有细节、只求你脑子里有一张"粗糙但完整"的地图。后续每一章都会:

  1. 回指本章的架构图、告诉你"我们正在深入这个框"
  2. 把该框内部展开、画同风格的子图
  3. 结束时把学到的内容填回本章的全景图

这样读完 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 的架构选择有了"为什么是这样"的答案——不是因为这些选择唯一正确、而是因为它们在 "通用开源框架" 这个定位下是最优解组合。

1.8 本章小结

本章通过"跟一条请求走完全程"的方式,建立了 vLLM V1 架构的全景认知:

1.9 下一步

第 2 章会钻进 EngineCoreProc.run_busy_loop——把本章反复提到的"一拍"拆成十几个微观动作、贴 v1/engine/core.py 的每一段关键代码,并讲清楚 ZMQ/共享内存两套 IPC 在源码层面到底长什么样。如果你读本章时对"三进程/两协议/差量更新"三个概念产生了"直觉上懂但想看代码"的冲动——第 2 章就是为这种冲动准备的。

翻开前、建议再看一眼 1.6B 的全景图——第 2 章的每一段代码、都会对应这张图里的一个位置。读完第 2 章、整张全景图的"中央大脑"部分(API Server + EngineCore 连接线)会从"概念"变成"代码"。


延伸阅读

源码起点

  • 主循环: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