Skip to content

第1章 架构总览

"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 到 Token 输出的完整旅程
  • 理解 V1 引擎的多进程架构及其设计动因
  • 掌握 vLLM 五大核心子系统的职责边界
  • 认识 V0 到 V1 的架构跃迁:哪些被保留,哪些被重写,为什么
  • 建立全书后续章节的认知地图

1.1 一个请求的完整旅程

让我们从最具体的场景开始。

一个用户向你的 LLM 服务发送了一条消息:"请用一句话解释什么是 PagedAttention。" 这条消息经过网络,抵达你部署的 vLLM 服务。从这一刻起,到用户收到完整回复的那一刻,中间发生了什么?

这趟旅程可以分为六个阶段:

阶段一:API Server 接收请求

请求首先抵达 API Server 进程(vllm/entrypoints/openai/api_server.py)。这是一个基于 FastAPI 的 HTTP 服务,提供 OpenAI 兼容的接口:/v1/chat/completions/v1/completions 等。

API Server 做三件事:

  1. 参数校验——检查模型名、温度、top_p 等参数是否合法
  2. 分词(Tokenization)——将用户的文本转换为 Token ID 序列。这一步在 API Server 进程中完成,而不是在 GPU Worker 中——这是 V1 架构的一个关键决策,我们稍后会解释为什么
  3. 提交请求——通过 ZMQ Socket 将请求发送给 EngineCore 进程

注意这里的进程分离:API Server 和 EngineCore 运行在不同的进程中,通过 ZMQ IPC(进程间通信)连接。这与 V0 架构有本质区别——V0 中所有逻辑都在同一个进程内。

阶段二:EngineCore 接收并排队

EngineCore(vllm/v1/engine/core.py)是 vLLM V1 的心脏。它运行在一个独立的进程中,有自己的事件循环,不受 HTTP 请求处理的干扰。

收到请求后,EngineCore 将其封装为内部的 Request 对象,加入等待队列。此时请求还没有分配任何 GPU 资源——它在排队等候调度器的宠幸。

阶段三:调度器做出决策

这是最精妙的环节。

每一步推理开始前,调度器vllm/v1/core/sched/scheduler.py)都要回答一个问题:这一步该让哪些请求往前走?每个请求走多少个 Token?

调度器的输出极其简洁——一个字典:

python
# {request_id: num_tokens_to_process}
scheduled = {
    "req-001": 128,   # 新请求,预填充 128 个 Token
    "req-002": 1,     # 老请求,解码 1 个 Token
    "req-003": 1,     # 老请求,解码 1 个 Token
    "req-004": 64,    # 新请求的部分预填充(分块)
}

这个看似简单的字典背后,是调度器在显存预算、吞吐量和延迟之间做的精密权衡。它需要考虑:当前有多少空闲的 KV Cache 块?新请求的预填充会不会挤掉正在解码的老请求?分块预填充应该切多大?

我们会在第 3 章详细拆解这个决策过程。

阶段四:KV Cache 分配

调度器决策完成后,KV Cache 管理器vllm/v1/core/kv_cache_manager.py)登场。它需要为调度器选中的请求分配物理显存块。

这里就是 PagedAttention 发挥作用的地方。KV Cache 管理器维护一个 BlockPool——一个由固定大小显存块组成的资源池。每个块能存储 16 个 Token 的 Key-Value 对。分配时不需要连续空间,就像操作系统的内存分页一样,通过一张"块表"(类似页表)记录逻辑块到物理块的映射。

这种设计消除了显存碎片——V0 时代,一个 2048 Token 的请求需要一整块连续显存,即使只生成了 100 个 Token 也要占着 2048 的坑位。PagedAttention 将浪费率从 60-80% 降到了 4% 以下。

阶段五:GPU 执行前向计算

一切准备就绪后,Executor 将调度结果发送给 Workervllm/v1/worker/gpu_worker.py)。Worker 是真正触碰 GPU 的角色——每张 GPU 卡对应一个 Worker 进程。

Worker 收到的不是完整的请求状态,而是一个差量更新(diff)。它本地缓存了所有活跃请求的状态,每一步只接收"哪些请求新增了、哪些变化了、哪些结束了"。这是 V1 的另一个关键优化——V0 每一步都要广播完整状态,通信开销随并发请求数线性增长。

Worker 调用 ModelRunner 执行模型的前向传播:

  1. 准备输入——从缓存的状态中组装 Token ID、位置编码、注意力掩码
  2. 执行注意力计算——调用 FlashAttention3 内核,利用块表访问分页的 KV Cache
  3. 计算 Logits——模型的最后一层输出每个词表位置的得分
  4. 采样——根据温度、top_p、top_k 等参数从 Logits 中采出下一个 Token

一个 Token 就这样诞生了。

阶段六:去分词与流式输出

新生成的 Token ID 被送回 API Server 进程。API Server 负责去分词——将 Token ID 转换回人类可读的文本片段。

如果用户请求了流式输出(stream=true),每个 Token 会立即通过 Server-Sent Events(SSE)推送给客户端。用户在浏览器中看到文字逐字蹦出来,就是这个过程的体现。

去分词之所以放在 API Server 而非 Worker 中,是 V1 架构的刻意选择。去分词是 CPU 密集型操作,放在 Worker 进程会与 GPU 计算争夺宝贵的执行时间。进程分离让 CPU 工作和 GPU 工作真正并行。

然后,旅程循环往复——EngineCore 启动下一步调度,Worker 生成下一个 Token,直到遇到终止条件(最大长度、EOS Token、停止词)。一个完整的回复就这样一个 Token 一个 Token 地拼出来了。

1.2 V1 多进程架构

上一节的旅程中,你可能已经注意到一个反复出现的关键词:进程分离。这是 V1 架构最重要的设计决策,值得我们停下来仔细审视。

为什么要多进程

V0 的架构是单进程的。LLMEngine 对象在一个 Python 进程中完成所有工作——接收 HTTP 请求、分词、调度、与 GPU Worker 通信、去分词、返回结果。这种设计简单直观,但有一个致命问题:Python 的 GIL(全局解释器锁)让 CPU 工作和 GPU 编排互相阻塞

具体来说:当 API Server 在做分词(CPU 密集)时,调度器无法同时做下一步的调度决策;当去分词在处理输出时,Worker 的结果无法及时被消费。在高并发场景下,CPU 成了瓶颈,GPU 利用率反而下降。

V1 的解法是把 vLLM 拆分为三类进程:

进程职责CPU/GPU通信方式
API ServerHTTP 处理、分词、去分词纯 CPUZMQ IPC → EngineCore
EngineCore调度、KV Cache 管理纯 CPU共享内存 MQ → Workers
Worker × N模型前向计算GPU共享内存 MQ → API Server

每个进程有自己的 Python 解释器和 GIL,互不干扰。API Server 可以在 Worker 执行 GPU 计算时同时做分词和去分词;EngineCore 可以在 Worker 执行当前步时提前做下一步的调度决策。

进程间通信:ZMQ 与共享内存

进程分离带来了通信开销的问题。V1 选择了两种机制:

  1. ZMQ IPC——用于 API Server 与 EngineCore 之间。ZMQ 是成熟的消息队列库,IPC 模式使用 Unix Domain Socket,延迟在微秒级。请求和响应的数据量不大(Token ID 序列),ZMQ 完全够用。

  2. 共享内存 MessageQueue——用于 EngineCore 与 Worker 之间。调度结果需要高效广播给所有 Worker,共享内存避免了数据拷贝。MultiprocExecutorvllm/v1/executor/multiproc_executor.py)使用 rpc_broadcast_mq 向所有 Worker 广播调度指令,Worker 通过 worker_response_mq 返回结果。

为什么不全用 ZMQ 或全用共享内存?因为它们各有优势:ZMQ 提供了更好的抽象(发布-订阅、请求-回复模式),适合 API Server 这种面向外部的组件;共享内存则提供了最低的延迟和零拷贝语义,适合引擎内部的高频通信。

V0 到 V1:一张对比表

维度V0V1为什么改
进程模型单进程多进程(API Server + EngineCore + Workers)GIL 瓶颈,CPU/GPU 无法并行
调度模型区分预填充/解码阶段统一 Token 调度简化逻辑,天然支持分块预填充
Worker 状态无状态(每步广播全量)有状态(只发差量)减少通信开销
前缀缓存可选,有性能开销默认启用,零开销优化数据结构后开销可忽略
CUDA 图标准 CUDA 图分段 CUDA 图(Piecewise)突破标准 CUDA 图的动态 shape 限制
请求抽象SequenceGroupRequest消除不必要的复杂性
去分词在引擎循环内在 API Server 进程异步执行CPU 工作不阻塞 GPU 编排

V1 发布后的基准测试显示,在不使用多步调度的情况下,吞吐量提升了 1.7 倍。对于视觉语言模型(VLM),提升更为显著,因为图像编码器的 CPU 预处理在 V0 中会严重阻塞 GPU。

1.3 五大子系统

从全局视角看,vLLM 的代码库可以映射为五个核心子系统。理解它们的职责边界和交互方式,就掌握了整个系统的骨架。

子系统一:入口层(Entrypoints)

vllm/entrypoints/ 目录是 vLLM 面向外部世界的窗口。它包含:

  • OpenAI 兼容 API Server——最常用的入口,支持 /v1/chat/completions/v1/completions/v1/embeddings 等端点
  • 离线推理接口——LLM 类(vllm/entrypoints/llm.py),用于批量推理场景,不启动 HTTP 服务
  • gRPC Server——高性能 RPC 接口
  • CLI 工具——vllm serve 命令

入口层的核心原则是——它只负责协议适配和序列化,不包含任何推理逻辑。所有的智慧都在 EngineCore 中。

子系统二:引擎核心(Engine Core)

vllm/v1/engine/vllm/v1/core/ 是 vLLM 的大脑。

  • EngineCorevllm/v1/engine/core.py)——引擎的主循环。接收请求,驱动调度器,分发执行指令,收集结果。它是唯一了解系统全局状态的组件。
  • Schedulervllm/v1/core/sched/scheduler.py)——调度器。决定每一步哪些请求参与计算,每个请求处理多少 Token。
  • KVCacheManagervllm/v1/core/kv_cache_manager.py)——KV Cache 块的分配、释放和共享。

引擎核心的设计哲学是集中决策,分布执行。调度器掌握全局信息(所有请求的状态、所有块的使用情况),做出集中的调度决策;Worker 只需要执行决策,不需要知道其他 Worker 在做什么。

子系统三:执行层(Executor & Worker)

vllm/v1/executor/vllm/v1/worker/ 是 vLLM 的肌肉。

  • Executor 是引擎核心与 Worker 之间的代理层。它屏蔽了 Worker 的部署拓扑——EngineCore 不需要知道 Worker 是单卡还是多卡,是本地进程还是远程 Ray Actor。
  • Worker 是真正操控 GPU 的角色。每个 Worker 负责一张 GPU 卡,执行模型的前向传播。

Executor 有三种实现,对应三种部署模式:

子系统四:模型层(Model Executor)

vllm/model_executor/ 包含了所有与具体模型相关的代码:

  • 模型实现——Llama、Qwen、Mistral、GPT-NeoX 等数百种模型的适配
  • 层实现——注意力层、MLP 层、嵌入层等基础组件
  • 量化支持——FP8、GPTQ、AWQ 等量化方案的实现
  • 模型加载——从 HuggingFace Hub 下载和加载模型权重

模型层的设计原则是可插拔。添加一个新模型只需要实现规定的接口,不需要修改引擎核心或调度器。这种解耦是 vLLM 能支持数百种模型的基础。

子系统五:内核层(Kernels)

vllm/kernels/vllm/vllm_flash_attn/ 包含了 vLLM 的性能秘密——用 CUDA 和 Triton 编写的定制内核。

最核心的是 PagedAttention 内核——它实现了在分页 KV Cache 上高效执行注意力计算。标准的 FlashAttention 假设 KV Cache 是连续的,PagedAttention 内核则能通过块表间接寻址,在非连续的内存块上完成同样的计算。

此外还有 FlashAttention3 的集成——vLLM V1 的主力注意力后端,支持预填充和解码在同一批次内混合执行。

1.4 源码目录导航

最后,让我们快速浏览 vllm/ 目录的组织结构,为后续的源码之旅做好准备:

vllm/
├── v1/                     # V1 引擎(当前默认)
│   ├── engine/             #   EngineCore、客户端
│   ├── core/               #   调度器、KV Cache 管理
│   ├── executor/           #   Executor 实现
│   ├── worker/             #   GPU Worker
│   ├── attention/          #   注意力后端
│   ├── sample/             #   采样逻辑
│   └── spec_decode/        #   投机解码

├── entrypoints/            # API 服务器、CLI
│   ├── openai/             #   OpenAI 兼容 API
│   └── llm.py              #   离线推理接口

├── model_executor/         # 模型实现与加载
│   ├── models/             #   各模型适配
│   ├── layers/             #   基础层(注意力、量化等)
│   └── model_loader/       #   权重加载

├── distributed/            # 分布式通信
│   ├── parallel_state.py   #   并行状态管理
│   └── kv_transfer/        #   KV Cache 传输

├── kernels/                # CUDA/Triton 内核
├── lora/                   # LoRA 适配器
├── multimodal/             # 多模态输入处理
├── config/                 # 配置类
└── sampling_params.py      # 采样参数

记住两个原则:

  1. V1 的代码在 v1/ 目录下——如果你在 vllm/engine/vllm/core/ 中看到类似的代码,那是 V0 遗留的,不要混淆
  2. v1/engine/core.py 开始读——这是整个系统的入口点,从这里出发你可以到达任何一个子系统

1.5 本章小结

本章建立了对 vLLM 的全局认知:

  • 一个推理请求经历六个阶段:HTTP 接收 → 分词入队 → 调度决策 → KV Cache 分配 → GPU 前向计算 → 去分词输出
  • V1 架构采用多进程设计,API Server、EngineCore、Worker 各司其职,通过 ZMQ 和共享内存通信,彻底消除 GIL 瓶颈
  • 五大子系统——入口层、引擎核心、执行层、模型层、内核层——构成了 vLLM 的骨架
  • V1 相比 V0 的核心改进:多进程、统一调度、有状态 Worker、零开销前缀缓存

这张地图将在后续章节中逐步展开。下一章,我们将走进 EngineCore——那个驱动一切运转的心脏。


延伸阅读

基于 VitePress 构建