vLLM系统拆解-01-架构设计:为什么推理引擎不能是一个进程
vLLM系统拆解-01-架构设计:为什么推理引擎不能是一个进程
model.generate() 很好用,但它解决的是单请求问题——一个输入进去,一个输出出来,跑完拉倒。
真实的 LLM serving 场景不是这样:并发请求是常态,每个请求 prompt 长度不同、生成进度不一,有的刚开始 Prefill,有的已经 Decode 到一半,还有的在等 KV Cache 空间释放。在这种状态下,GPU 利用效率、请求排队延迟和整体吞吐,都不是"把模型跑起来"能解决的问题。
vLLM 的定位是高吞吐、显存高效的大模型推理与服务引擎。它不靠某一个 attention kernel 快多少取胜,而是在并发请求的上下文里提供一套完整的调度机制、KV Cache 管理和分层执行架构。
这篇文章从 vLLM 的整体架构出发,解释它为什么采用多进程设计,每个进程负责什么,以及一次请求是怎样在这套架构里流转的。理解这张"总地图",是读懂后续 scheduler、KV Cache、PagedAttention 等机制的前提。
LLM serving 到底难在哪
单请求场景关注的是:模型能不能装进显存、forward 够不够快、结果是否正确。
服务型推理面对的是另一组问题:
- 大量请求动态到达,prompt 长度、生成长度各不相同
- 有的请求处于 Prefill 阶段(计算初始 KV Cache),有的处于 Decode 阶段(逐 token 生成)
- KV Cache 随 Decode 持续增长,且不同请求的 KV Cache 之间无法直接共享显存空间
- 需要流式返回,不能等全部生成完再输出
- 既要控制首 token 延迟(Time To First Token,TTFT),也要维持较高的整体吞吐
这些特征加在一起,让问题的核心变成:如何把大量处于不同状态的请求,持续、高效地编排进 GPU 的执行流程。
有一个角度可以帮助理解 vLLM 在做什么:把它当成"大模型推理操作系统"。和操作系统类似,它接收外部任务(请求)、管理资源(GPU 显存、KV Cache)、做调度(决定谁先运行、谁暂停)、做隔离(不同请求彼此独立)、做复用(prefix caching)、做回收(请求结束后释放 KV Cache),还要支持并行扩展(多 GPU、多副本)。这不是官方定义,但作为理解模型它很准确。
两种使用入口
vLLM 同时支持两类场景,对应两种入口:
入口一:Python 离线调用
1 | from vllm import LLM, SamplingParams |
这种模式更像库调用,适合离线推理、模型测试、写 benchmark、嵌入到自有程序里。对应的核心类是 LLM 和 LLMEngine。
入口二:OpenAI-compatible HTTP server
1 | vllm serve Qwen/Qwen2.5-1.5B-Instruct |
启动后,外部服务通过 OpenAI 兼容接口(/v1/chat/completions、/v1/completions 等)访问。适合工程/平台团队把 vLLM 部署成一个模型服务,对接已有上层应用。
两种入口存在的原因:研究/开发者需要在 Python 里直接用,工程/平台团队需要一个 HTTP 服务。vLLM 的做法是把核心推理引擎独立出来,再在上面承接不同入口——这个思路贯穿整个架构设计。
为什么必须是多进程
理论上用一个进程做完所有事(接 HTTP、tokenization、调度、调 GPU、流式返回)是可以的。但这条路很快遇到工程天花板。
CPU 任务与 GPU 任务相互干扰
API server 处理 HTTP 请求、序列化/反序列化、tokenization,是典型的 CPU-heavy 工作。调度逻辑是高频小循环,对延迟极其敏感——每一轮 Decode 都要重新决定哪些请求继续执行、KV Cache 够不够用。两类工作放在同一进程,网络请求的抖动会直接拖慢调度频率,进而拉低 GPU 利用率、增加 token 间延迟(Inter-Token Latency,ITL)。
服务接口和核心引擎不该耦合
vLLM 同时支持 Python 离线调用和 HTTP server 两种入口。如果 HTTP 层和调度逻辑写死在一起,换协议、加新接口、单独优化 API server 都会很麻烦。核心引擎需要是一个独立能力层,不同入口挂在上面。
多 GPU 场景天然要求多进程
每张 GPU 有独立的显存、CUDA context 和执行队列。Tensor Parallel、Pipeline Parallel 等并行策略,最终都要落到不同 GPU 上执行。用一个进程管理多张 GPU 的运行状态,会引入不必要的上下文切换复杂度、错误隔离问题和调试难度。
vLLM V1 的解法:把服务入口、调度核心、GPU 执行三层彻底拆开,每层只做自己最擅长的事。官方明确将这次重构的目标定为:代码简洁模块化、CPU 额外开销近乎为零、关键优化统一到一套架构里,以及尽量零配置默认启用合理优化。
整体架构:四个进程角色
vLLM V1 的进程架构最多包含四个角色(DP Coordinator 仅在 data parallel 场景出现):
flowchart TD
Client["用户 / 应用\nHTTP 请求 / Python 调用"]
subgraph API_Proc["API Server Process"]
direction TB
APIServer["API Server\n- 解析请求 / chat template\n- Tokenization\n- 多模态输入处理\n- 流式返回结果"]
end
subgraph Core_Proc["Engine Core Process"]
direction TB
Scheduler["Scheduler\n调度决策 / token budget"]
KVMgr["KV Cache Manager\nblock 分配 / 回收"]
end
subgraph Worker_Procs["GPU Worker Process(es)"]
direction LR
W0["GPU Worker 0\nModel Runner\nForward / KV Cache 写入"]
W1["GPU Worker 1\nModel Runner\nForward / KV Cache 写入"]
Wn["..."]
end
subgraph DP_Proc["DP Coordinator Process\n仅 data parallel 场景"]
DPCoord["DP Coordinator\n负载均衡 / MoE 同步协调"]
end
Client -- 请求进入 --> API_Proc
API_Proc -- 内部 request 对象 --> Core_Proc
Core_Proc -- 下发可执行 batch --> Worker_Procs
Worker_Procs -- 输出 token / 状态 --> Core_Proc
Core_Proc -- 结果 --> API_Proc
API_Proc -- 流式返回 --> Client
DP_Proc -. 跨副本协调 .-> Core_Proc
四个角色的职责和源码位置:
| 进程角色 | 核心职责 | 关键源码路径 |
|---|---|---|
| API Server | 接收 HTTP 请求、tokenization、多模态输入处理、结果流式返回 | vllm/entrypoints/openai/api_server.py |
| Engine Core | 运行 scheduler、管理 KV Cache、协调 GPU workers、busy loop 持续调度 | vllm/v1/engine/core.py |
| GPU Worker | 加载模型权重、管理本 GPU 显存、执行 forward、写入更新 KV Cache | vllm/v1/worker/gpu_worker.py |
| DP Coordinator | DP ranks 之间负载均衡、MoE 模型同步 forward 协调(条件性存在) | vllm/v1/engine/coordinator.py |
API Server:前台,不是大脑
API Server 的工作是"把用户能理解的输入,转成推理引擎能执行的内部表示,并把结果原路送回"。具体包括:解析 HTTP 请求和参数、应用 chat template、tokenization、多模态数据加载(如果有),然后把生成好的内部 request 对象发给 Engine Core,最后把生成结果流式回传给客户端。
把 API Server 单独拆出来有三个直接收益:
- 服务层和核心引擎解耦:HTTP 层如何变(换协议、加新接口),不影响调度和执行逻辑。
- 横向扩展更自然:data parallel 开启时,API Server 可以随副本数一起扩,与 Engine Core 的扩展逻辑解耦。
- CPU 资源规划更清晰:tokenization 和请求处理是明显的 CPU 密集操作,单独进程便于规划线程数和核数,不和调度侧竞争 CPU 时间。
Engine Core:系统的调度中枢
Engine Core 是整个系统的控制中枢,官方的描述是:运行 scheduler、管理 KV Cache、协调 GPU workers,以 busy loop 方式不断循环执行。
"busy loop"是关键词。Engine Core 不是被动等待请求触发,而是持续运行一个调度循环,每一轮都做:
- 决定哪些请求进入本轮 batch
- 为每个请求分配 token budget
- 查询和分配 KV Cache block
- 把请求组成可执行 batch
- 将 batch 下发给 GPU workers
为了完成这些决策,Engine Core 要持续维护大量动态状态,包括每个请求的当前阶段、已生成 token 数、prompt 长度、是否已完成 Prefill、当前占用的 KV Cache block、batch budget 剩余情况。
Engine Core 为什么必须独立成进程,有三个原因:
- 调度是高频操作:每一步 Decode 都要重新决策(哪些请求继续、是否需要抢占/回收/推迟),频率极高。
- 状态维护量大:请求数量越多,状态管理越复杂,与其他进程共享状态会引入不必要的同步开销。
- 对 CPU 延迟敏感:调度层 CPU 一旦被其他工作拖慢,GPU 就会空转,ITL 直接上升。
GPU Worker:执行者,不是决策者
每张 GPU 对应一个独立的 GPU Worker 进程。职责直接:加载模型权重、管理本 GPU 显存、接受 Engine Core 下发的 batch、执行 forward、把新生成的 KV Cache 写入显存、产生 logits。
“Engine Core 决定做什么,GPU Worker 负责把它做出来”——这是两者的分工边界,Worker 不参与全局请求决策。
一张 GPU 对应一个 worker 是最自然的设计:
- 每张 GPU 有独立的显存、CUDA context、执行队列,一对一对应职责最清晰
- 独立进程带来更好的错误隔离——某个 worker 出问题不直接影响其他 worker
- Tensor Parallel 和 Pipeline Parallel 都需要把计算映射到不同 GPU,"一 GPU 一 worker"天然适配这类映射
DP Coordinator:多副本场景的协调层
DP Coordinator 只在启用 data parallel 时出现。
data parallel 是把一整套 Engine Core + GPU Workers 复制多份,同时服务更多请求。但"多复制几份"不是终点——还需要解决:请求路由到哪个 DP rank、各 rank 负载是否均衡、MoE 模型的专家路由是否需要跨 rank 同步(对 MoE 模型,各 rank 之间需要协调 forward,否则结果可能不一致)。
DP Coordinator 处理的就是这一层的问题:比 Engine Core 更高层的跨副本编排。
LLMEngine / AsyncLLMEngine:逻辑组件 vs 进程角色
这里有一个容易混淆的地方。前面说的是进程角色(API Server Process、Engine Core Process 等),而 LLMEngine 和 AsyncLLMEngine 是代码层面的逻辑组件,不是进程,两个维度不在同一层次。
graph LR
subgraph logical["逻辑组件(代码层)"]
LLM["LLM\n用户调用接口"]
LLMEng["LLMEngine\n同步引擎抽象"]
AsyncEng["AsyncLLMEngine\n异步引擎抽象"]
Sched["Scheduler"]
KVM["KV Cache Manager"]
end
subgraph proc["进程角色(运行时)"]
AProc["API Server Process"]
CProc["Engine Core Process"]
WProc["GPU Worker Process"]
end
LLM -->|依赖| LLMEng
AsyncEng -->|被| AProc
LLMEng -.->|概念上对应| CProc
Sched -->|运行在| CProc
KVM -->|运行在| CProc
LLM:面向用户的顶层接口,LLM.generate()就是从这里进入LLMEngine:核心引擎抽象,负责输入处理、调度、模型执行组织、输出处理,适合 Python 离线场景AsyncLLMEngine:异步版本,适合服务端并发请求、流式输出、异步 request lifecycle 管理;OpenAI-compatible API server 使用的是这个,因为服务端不能因为一个请求阻塞其他请求
Engine Core(进程)和 LLMEngine(类)有对应关系,但不是同义词。前者是运行时的进程角色,后者是代码层面的抽象类。
一次请求的完整流转
把前面的模块串起来,看一次请求的完整路径:
sequenceDiagram
participant C as 用户/客户端
participant A as API Server
participant E as Engine Core
participant W as GPU Worker
participant S as Sampler
C->>A: 发起请求(HTTP POST 或 LLM.generate)
A->>A: 解析参数 / chat template / tokenization / 多模态处理
A->>E: 提交内部 request 对象
loop 调度循环(每轮 Decode)
E->>E: 调度决策:选本轮进入 batch 的请求
E->>E: 分配 token budget / 分配 KV Cache block
E->>W: 下发可执行 batch
W->>W: 准备输入张量 / 调用 Model Runner
W->>W: 执行 forward / 写入更新 KV Cache
W->>S: 传递 logits
S->>S: 采样(greedy / temperature / top-p / top-k 等)
S->>E: 返回本轮输出 token
E->>E: 更新请求状态
E->>A: 传递输出 token
A->>C: 流式返回 token(如启用)
end
E->>E: 检测到结束条件(EOS / max_tokens)
E->>E: 释放 KV Cache block / 清理请求状态
A->>C: 返回完整结果
每一步的要点:
第 1 步:输入处理(API Server)
把用户输入转为推理引擎的内部表示——解析请求参数、应用 chat template(对话模式下)、tokenization、多模态数据加载,生成内部 request 对象。
第 2 步:进入引擎(Engine Core)
请求进入 LLMEngine / AsyncLLMEngine 管理的流程,系统开始追踪它的状态:是新请求还是正在 Decode、当前需要多少 KV Cache、batch 能不能塞下它。
第 3 步:调度决策(Engine Core,持续循环)
在 busy loop 里,每轮决定:哪些请求进入本轮 batch、分配多少 token budget、KV Cache block 怎么分配,然后把可执行 batch 下发给 GPU workers。
第 4 步:GPU 执行(GPU Worker)
准备输入张量,调用 Model Runner,执行模型 forward,把新生成的 KV Cache 写入显存,产生 logits。
第 5 步:采样(Sampler)
从 logits 中根据采样策略选出下一个 token。支持 greedy、temperature、top-p、top-k 等策略,以及部分场景下的 beam search。
第 6 步:输出与状态更新
把 token 转为文本、流式回传给客户端(如果启用)、更新请求状态、检查是否满足结束条件。若请求结束,释放其占用的 KV Cache block 和请求状态;若未结束,继续参与下一轮调度。
最后一步有个细节值得注意:一个请求不是"进来执行一次就结束",而是在多个调度轮次中逐步完成 Decode。这就是为什么 vLLM 需要请求生命周期管理,而不是一次函数调用完事。
为什么要持续调度,而不是静态 batch
"静态 batch"的思路是:凑满一批请求,统一跑完,再接下一批。在离线 benchmark 里没问题,在在线服务场景里会遇到几个硬伤:
| 问题 | 静态 batch | 持续调度(continuous batching) |
|---|---|---|
| 新请求何时能进入 | 必须等当前 batch 全部完成 | 任意 Decode 轮次都可以插入 |
| batch 内请求长度不一 | 需要填充对齐,短请求浪费算力 | 按实际状态动态分配 token budget |
| 某个请求提前结束 | 对应槽位空转到整个 batch 完成 | 立即释放资源,下一轮可接新请求 |
| GPU 利用率 | 取决于 batch 内请求状态是否整齐 | 通过动态拼 batch 维持较高利用率 |
| KV Cache 释放粒度 | batch 结束后统一释放 | 每轮可做细粒度分配和回收 |
持续调度的核心思想:系统持续运行调度循环,每一轮根据所有活跃请求的当前状态,重新决定这轮 GPU 应该执行什么。这让 vLLM 能够接纳新请求、继续旧请求、动态控制 batch 内容、更好利用 GPU 空隙时间、更精细地管理 KV Cache。
这也是为什么 vLLM 的价值主要体现在 serving 场景,而不是单条离线推理。
V1 重构:为什么需要系统性改造
vLLM V1 不是小修小补,而是对核心架构的一次系统性重构。
V0 阶段,vLLM 快速扩展了对大量模型和硬件的支持,但不同特性是相对独立演进的——prefix caching、chunked prefill、speculative decoding、多模态等功能各自叠加,导致系统复杂度积累,技术债增加,核心调度路径越来越难以优化。
V1 想解决这些问题,目标明确:
- 代码更简单、模块化、易于 hack 和扩展:不同特性的实现要能统一在同一套架构里
- 近乎零 CPU 额外开销:scheduler 和 sampler 的 CPU 开销在 V1 里被显著压缩
- 关键优化统一到一套架构:PagedAttention、prefix caching、chunked prefill 等机制协同工作,而不是各自独立维护
- 尽量零配置:默认启用合理优化,减少用户手动调参的负担
这意味着本系列后面讲到的 scheduler、KV Cache 管理、PagedAttention,都是 V1 统一架构下的组成部分,不是独立的优化 trick。
源码路径速查
| 模块 | 源码路径 |
|---|---|
| Python 离线入口(CLI) | vllm/entrypoints/cli/main.py |
| OpenAI-compatible HTTP 服务 | vllm/entrypoints/openai/api_server.py |
| 同步引擎抽象 | vllm/engine/llm_engine.py |
| 异步引擎抽象 | vllm/engine/async_llm_engine.py |
| V1 Engine Core(调度中枢) | vllm/v1/engine/core.py |
| V1 Engine 工具函数 | vllm/v1/engine/utils.py |
| 多进程执行器 | vllm/v1/executor/multiproc_executor.py |
| GPU Worker | vllm/v1/worker/gpu_worker.py |
| DP Coordinator | vllm/v1/engine/coordinator.py |
几个常见混淆点
LLM / LLMEngine / AsyncLLMEngine 的区别
三者是不同层次的抽象:LLM 是面向用户的顶层接口,底层依赖 LLMEngine;AsyncLLMEngine 是服务端场景的异步版本,OpenAI API server 使用它,不会因单个请求阻塞其他请求。
Engine Core 和 LLMEngine 不是同一个概念
LLMEngine 是代码层面的类抽象,Engine Core 是 V1 进程架构里负责调度协调的进程角色。两者有对应关系,但不是同义词——一个是逻辑组件,另一个是运行时进程。
API Server 不做推理
它做的是请求接入、输入处理(tokenization 等)和结果回传。调度逻辑在 Engine Core,GPU 计算在 Worker。
GPU Worker 不是调度器
Worker 只负责执行 Engine Core 下发的任务,不参与全局请求决策。决策权在 Engine Core,Worker 专注执行。
这套架构的设计逻辑
把前面的内容串成一条逻辑链:
- LLM serving 是动态多请求问题,不是单次 forward 问题
- 动态多请求要求有请求生命周期管理
- 生命周期管理需要专门的调度中枢(Engine Core),持续运行调度循环
- 调度中枢是高频 CPU 循环,必须和 HTTP 服务层解耦,否则互相干扰
- GPU 执行天然适合封装成独立 worker,与 GPU 资源一一对应
- 多副本 data parallel 场景需要更上层的跨副本协调(DP Coordinator)
- 最终形成:API Server → Engine Core → GPU Workers 的分层多进程架构
这条逻辑链,是后续理解 scheduler、KV Cache 管理、PagedAttention、prefix caching 的基础——这些机制运行在这套架构的特定层次里,脱开架构单独看,很容易把它们当成相互独立的优化 trick,而看不到它们之间的系统联系。


