vLLM Code Reading (Section 1): vLLM API Server 如何启动 EngineCore
引子
理解 vLLM 的在线服务,需要先把注意力从模型本身移到服务进程的组织方式上。
如果只从 /v1/chat/completions 或 /v1/responses 看,很容易觉得 vLLM 就是一个 FastAPI server 加一层模型调用。但真正读进去以后会发现,vLLM 把 HTTP 协议适配、请求状态管理、核心调度、GPU 执行拆成了多个层级。API Server 进程不直接跑 scheduler,也不直接跑 model forward。它更像一个前端控制面,负责把 OpenAI 兼容请求变成 vLLM 内部请求,再通过 IPC 交给后面的 EngineCore。
这条启动链路里最重要的结论是:
vllm serve启动 API Server 时,会在构造AsyncLLM的过程中拉起独立的EngineCore后台进程;API Server 和 EngineCore 之间通过 ZMQ + msgpack 通信。
总体分层
先给出一个完整的 mental model:
flowchart TB
subgraph Client["Client"]
U["OpenAI-compatible request"]
end
subgraph API["API Server process"]
F["FastAPI / Uvicorn"]
R["OpenAI routers"]
C["OpenAIServingChat / OpenAIServingResponses"]
A["AsyncLLM"]
IP["InputProcessor"]
OP["OutputProcessor"]
M["AsyncMPClient"]
end
subgraph Core["EngineCore process"]
ECP["EngineCoreProc"]
EC["EngineCore"]
S["Scheduler"]
KV["KVCacheManager"]
EX["Executor"]
end
subgraph GPU["GPU worker processes / actors"]
W0["Worker rank 0"]
W1["Worker rank 1"]
WN["Worker rank N"]
MR["GPUModelRunner"]
end
U --> F --> R --> C --> A
A --> IP
A --> M
M -- "ZMQ ADD / ABORT / UTILITY" --> ECP
ECP --> EC --> S --> KV --> EX
EX --> W0
EX --> W1
EX --> WN
W0 --> MR
W1 --> MR
WN --> MR
ECP -- "ZMQ EngineCoreOutputs" --> M
M --> OP --> A --> C --> F --> U
这张图里的几个边界很重要:
- API Server 是协议层和异步请求层。
- EngineCore 是调度层和执行控制层。
- GPU Worker 是真正执行模型分片的地方。
OutputProcessor虽然在 API Server 进程里,但它处理的是来自 EngineCore 的输出,把EngineCoreOutput变成上层能消费的RequestOutput。
启动入口
vllm serve ... 的 OpenAI server 入口最终会进入:
1 | |
关键路径是:
1 | |
build_async_engine_client_from_engine_args() 会先创建 VllmConfig:
1 | |
然后构造 AsyncLLM:
1 | |
这一步很关键:EngineCore 不是第一个请求来了才启动,而是在 API Server 初始化 backend 的时候就启动。
AsyncLLM 初始化做什么
AsyncLLM 是 API Server 侧的核心对象。它的初始化大致做四件事:
1 | |
对应的代码结构可以理解为:
1 | |
这几个对象的分工非常清楚:
1 | |
AsyncLLM 本身不是 scheduler,也不是 executor。它是 API Server 侧的引擎门面。
EngineCoreClient 如何选择
vLLM 有多种 EngineCoreClient:
1 | |
这里主要关注 AsyncMPClient,因为 OpenAI API Server 是 asyncio/FastAPI 风格,需要异步地收发请求和输出。
选择逻辑可以简化成:
1 | |
在 DP=1 的普通场景下,返回的就是 AsyncMPClient。
AsyncMPClient 和 MPClient 的关系
AsyncMPClient 继承自 MPClient。
MPClient 是跨进程 EngineCore 的基础客户端,注释里已经写得很直接:
1 | |
AsyncMPClient 在此基础上做 asyncio 包装:
1 | |
EngineCore 是什么时候被拉起的
真正拉起 EngineCore 的地方在 MPClient.__init__()。
逻辑可以简化成:
1 | |
画成 sequence diagram 更直观:
sequenceDiagram
participant API as API Server process
participant ALLM as AsyncLLM
participant AMC as AsyncMPClient
participant MPC as MPClient
participant LCE as launch_core_engines
participant MGR as CoreEngineProcManager
participant ECP as EngineCoreProc process
participant EX as Executor
API->>ALLM: AsyncLLM.from_vllm_config()
ALLM->>AMC: EngineCoreClient.make_async_mp_client()
AMC->>MPC: MPClient.__init__(asyncio_mode=True)
MPC->>MPC: bind input ROUTER / output PULL
MPC->>LCE: launch_core_engines(...)
LCE->>MGR: CoreEngineProcManager(...)
MGR->>ECP: multiprocessing.Process(...).start()
ECP->>ECP: EngineCoreProc.__init__()
ECP->>EX: executor_class(vllm_config)
EX->>EX: initialize model / workers / KV cache
ECP-->>MPC: READY response through input socket
MPC-->>ALLM: AsyncMPClient ready
这里的 READY 很关键。MPClient 会等待每个 EngineCore 发回 ready message,才认为引擎启动完成。
ready response 里会带回一些 EngineCore 初始化后的信息,例如:
1 | |
这也解释了为什么 API Server 启动时需要等模型加载和 KV cache profiling 完成。因为 EngineCore 要初始化完,前端才能知道后端真实可用的能力。
两层 multiprocessing
读到这里很容易混淆一个点:vLLM 这里至少有两层进程。
第一层是:
1 | |
这一层由 AsyncMPClient / MPClient / CoreEngineProcManager 管理。
第二层是:
1 | |
这一层由 Executor 管理。如果是 distributed_executor_backend=mp,就是 MultiprocExecutor 在本地或多节点拉起 worker。如果是 ray,就是 Ray actor。
对于 DP=1, PP=2, TP=8,模型执行的 world size 是:
$$
\text{world_size} = \text{DP} \times \text{PP} \times \text{TP}
$$
代入:
$$
\text{world_size} = 1 \times 2 \times 8 = 16
$$
但这 16 个 rank 是 GPU worker 层面的 rank,不是 16 个 API Server,也不是 16 个 EngineCore。
常见结构是:
1 | |
如果使用多机 mp + headless,非 API 节点会运行 headless vLLM 进程,用来加入分布式 worker 体系,而不是启动 HTTP API。
API Server 和 EngineCore 通信
API Server 和 EngineCore 之间用 ZMQ。
输入方向:
1 | |
输出方向:
1 | |
输入消息大致是:
1 | |
request type 主要包括:
1 | |
ADD 用于新增请求,ABORT 用于取消请求,UTILITY 用于执行一些控制类调用,比如获取 supported tasks、reset prefix cache、add/remove LoRA 等。
一个 Chat 请求怎么穿过这套结构
以 /v1/chat/completions 为例,请求进入 API Server 后先到:
1 | |
这一步做 OpenAI Chat 协议适配:
1 | |
随后调用:
1 | |
进入 AsyncLLM:
1 | |
EngineCore 收到后:
1 | |
后续调度和执行:
1 | |
输出回到 API Server:
1 | |
OutputProcessor 在这里的位置
OutputProcessor 是 API Server 进程里的对象,但它处理的是 EngineCore 返回的结果。
它的职责是:
1 | |
它不是 scheduler,也不决定哪个 token 该算。它决定的是“算出来的 token 怎么变成用户看到的输出”。
这解释了为什么 AsyncLLM._add_request() 会先把 request 登记到 OutputProcessor,再发送给 EngineCore:
1 | |
如果不先登记,EngineCore 很快返回 token 时,API Server 侧就不知道该把结果放进哪个请求的 queue。
多机部署时 API Server 是否对称
如果是 Ray 后端:
1 | |
如果是 mp + headless:
1 | |
所以多机情况下 API Server 并不是天然对称的。通常只有入口节点暴露 HTTP API,其他节点作为执行资源加入模型并行体系。
架构小结
vLLM 的服务启动链路可以浓缩成三句话。
第一,API Server 启动时构造 AsyncLLM,AsyncLLM 构造时就启动了 EngineCore 后台进程。
第二,API Server 和 EngineCore 通过 ZMQ + msgpack 通信,API Server 发 EngineCoreRequest,EngineCore 回 EngineCoreOutputs。
第三,EngineCore 不是 GPU worker 本身。EngineCore 管 scheduler、KV cache manager 和 executor;executor 再负责拉起或管理真正的 GPU worker。
最终的层次关系是:
1 | |
这个分层是 vLLM 后面所有复杂能力的基础,包括:
- streaming response
- continuous batching
- prefix cache
- tensor parallelism
- pipeline parallelism
- data parallel load balancing
- multi-node serving
- disaggregated prefill/decode
这个边界也是理解 scheduler 和 KV cache 的前提:API Server 只是请求的入口和输出的出口,真正控制 token 计算顺序的是 EngineCore 里的 scheduler。