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
vllm/entrypoints/openai/api_server.py

关键路径是:

1
2
3
4
run_server_worker()
-> build_async_engine_client()
-> build_async_engine_client_from_engine_args()
-> AsyncLLM.from_vllm_config()

build_async_engine_client_from_engine_args() 会先创建 VllmConfig

1
vllm_config = engine_args.create_engine_config(usage_context=usage_context)

然后构造 AsyncLLM

1
2
3
4
5
6
7
8
9
10
async_llm = AsyncLLM.from_vllm_config(
vllm_config=vllm_config,
usage_context=usage_context,
enable_log_requests=engine_args.enable_log_requests,
aggregate_engine_logging=engine_args.aggregate_engine_logging,
disable_log_stats=engine_args.disable_log_stats,
client_addresses=client_config,
client_count=client_count,
client_index=client_index,
)

这一步很关键:EngineCore 不是第一个请求来了才启动,而是在 API Server 初始化 backend 的时候就启动。

AsyncLLM 初始化做什么

AsyncLLM 是 API Server 侧的核心对象。它的初始化大致做四件事:

1
2
3
4
1. 创建 renderer
2. 创建 InputProcessor
3. 创建 OutputProcessor
4. 创建 EngineCoreClient,也就是 AsyncMPClient

对应的代码结构可以理解为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
self.renderer = renderer_from_config(self.vllm_config)

self.input_processor = InputProcessor(self.vllm_config, renderer)

self.output_processor = OutputProcessor(
renderer.tokenizer,
log_stats=self.log_stats,
stream_interval=self.vllm_config.scheduler_config.stream_interval,
tracing_enabled=tracing_endpoint is not None,
)

self.engine_core = EngineCoreClient.make_async_mp_client(
vllm_config=vllm_config,
executor_class=executor_class,
log_stats=self.log_stats,
client_addresses=client_addresses,
client_count=client_count,
client_index=client_index,
)

这几个对象的分工非常清楚:

1
2
3
4
5
6
7
8
9
10
11
renderer:
负责 tokenizer / chat template / renderer mode

InputProcessor:
把 EngineInput 变成 EngineCoreRequest

OutputProcessor:
把 EngineCoreOutputs 变成 RequestOutput

AsyncMPClient:
负责和 EngineCore 后台进程通信

AsyncLLM 本身不是 scheduler,也不是 executor。它是 API Server 侧的引擎门面。

EngineCoreClient 如何选择

vLLM 有多种 EngineCoreClient:

1
2
3
4
5
InprocClient
SyncMPClient
AsyncMPClient
DPAsyncMPClient
DPLBAsyncMPClient

这里主要关注 AsyncMPClient,因为 OpenAI API Server 是 asyncio/FastAPI 风格,需要异步地收发请求和输出。

选择逻辑可以简化成:

1
2
3
4
5
if parallel_config.data_parallel_size > 1:
if parallel_config.data_parallel_external_lb:
return DPAsyncMPClient(...)
return DPLBAsyncMPClient(...)
return AsyncMPClient(...)

DP=1 的普通场景下,返回的就是 AsyncMPClient

AsyncMPClient 和 MPClient 的关系

AsyncMPClient 继承自 MPClient

MPClient 是跨进程 EngineCore 的基础客户端,注释里已经写得很直接:

1
2
3
4
5
EngineCore runs in a background process busy loop,
getting new EngineCoreRequests and returning EngineCoreOutputs.

* pushes EngineCoreRequests via input_socket
* pulls EngineCoreOutputs via output_socket

AsyncMPClient 在此基础上做 asyncio 包装:

1
2
3
4
5
6
7
MPClient:
管 socket、进程、序列化、ready handshake

AsyncMPClient:
用 asyncio.Queue 承接 EngineCoreOutputs
用 asyncio task 后台读取 output socket
提供 add_request_async / get_output_async

EngineCore 是什么时候被拉起的

真正拉起 EngineCore 的地方在 MPClient.__init__()

逻辑可以简化成:

1
2
3
4
5
6
7
8
MPClient.__init__()
-> 创建 ZMQ context
-> 创建 input_socket: ROUTER
-> 创建 output_socket: PULL
-> launch_core_engines(...)
-> CoreEngineProcManager(...)
-> multiprocessing.Process(target=EngineCoreProc.run_engine_core)
-> proc.start()

画成 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
2
3
4
5
6
7
max_model_len
num_gpu_blocks
block_size
kv_cache_size_tokens
kv_cache_max_concurrency
world_size
data_parallel_size

这也解释了为什么 API Server 启动时需要等模型加载和 KV cache profiling 完成。因为 EngineCore 要初始化完,前端才能知道后端真实可用的能力。

两层 multiprocessing

读到这里很容易混淆一个点:vLLM 这里至少有两层进程。

第一层是:

1
2
API Server process
-> EngineCore process

这一层由 AsyncMPClient / MPClient / CoreEngineProcManager 管理。

第二层是:

1
2
EngineCore process
-> GPU Worker processes

这一层由 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
2
3
1 API Server process
1 EngineCore process
16 GPU worker processes / actors

如果使用多机 mp + headless,非 API 节点会运行 headless vLLM 进程,用来加入分布式 worker 体系,而不是启动 HTTP API。

API Server 和 EngineCore 通信

API Server 和 EngineCore 之间用 ZMQ。

输入方向:

1
2
3
4
API Server AsyncMPClient
ROUTER socket
-> EngineCoreProc
DEALER socket

输出方向:

1
2
3
4
EngineCoreProc
PUSH socket
-> API Server AsyncMPClient
PULL socket

输入消息大致是:

1
2
3
4
frame 0: engine identity
frame 1: request type
frame 2: msgpack payload
frame 3..N: optional tensor / ndarray buffers

request type 主要包括:

1
2
3
4
5
ADD
ABORT
UTILITY
WAKEUP
EXECUTOR_FAILED

ADD 用于新增请求,ABORT 用于取消请求,UTILITY 用于执行一些控制类调用,比如获取 supported tasks、reset prefix cache、add/remove LoRA 等。

一个 Chat 请求怎么穿过这套结构

/v1/chat/completions 为例,请求进入 API Server 后先到:

1
OpenAIServingChat._create_chat_completion()

这一步做 OpenAI Chat 协议适配:

1
2
3
4
messages
-> chat template
-> EngineInput
-> SamplingParams

随后调用:

1
engine_client.generate(...)

进入 AsyncLLM

1
2
3
4
5
6
AsyncLLM.generate()
-> AsyncLLM.add_request()
-> InputProcessor.process_inputs()
-> EngineCoreRequest
-> AsyncMPClient.add_request_async()
-> ZMQ ADD

EngineCore 收到后:

1
2
3
4
5
6
7
8
EngineCoreProc input thread
-> decode EngineCoreRequest
-> preprocess_add_request()
-> Request.from_engine_core_request()
-> input_queue
-> EngineCore._handle_client_request()
-> EngineCore.add_request()
-> Scheduler.add_request()

后续调度和执行:

1
2
3
4
5
6
Scheduler.schedule()
-> KVCacheManager allocate slots
-> Executor.execute_model()
-> GPU workers forward
-> sampler
-> EngineCoreOutputs

输出回到 API Server:

1
2
3
4
5
6
7
EngineCore output thread
-> ZMQ PUSH EngineCoreOutputs
-> AsyncMPClient output task
-> OutputProcessor.process_outputs()
-> RequestOutputCollector
-> async generator
-> Chat stream/full response

OutputProcessor 在这里的位置

OutputProcessor 是 API Server 进程里的对象,但它处理的是 EngineCore 返回的结果。

它的职责是:

1
2
3
4
5
6
7
EngineCoreOutput
-> detokenize
-> stop string check
-> logprobs processing
-> n>1 child request aggregation
-> RequestOutput
-> per-request queue

它不是 scheduler,也不决定哪个 token 该算。它决定的是“算出来的 token 怎么变成用户看到的输出”。

这解释了为什么 AsyncLLM._add_request() 会先把 request 登记到 OutputProcessor,再发送给 EngineCore:

1
2
1. OutputProcessor.add_request(...)
2. engine_core.add_request_async(...)

如果不先登记,EngineCore 很快返回 token 时,API Server 侧就不知道该把结果放进哪个请求的 queue。

多机部署时 API Server 是否对称

如果是 Ray 后端:

1
2
3
4
5
6
7
8
9
10
node0:
vllm serve
API Server
AsyncLLM
EngineCore / Ray control

node1:
Ray worker runtime
GPU actors
no OpenAI API server

如果是 mp + headless

1
2
3
4
5
6
7
8
9
10
node0:
vllm serve ... --node-rank 0
API Server
EngineCore
local workers

node1:
vllm serve ... --node-rank 1 --headless
no API routes
headless multiproc executor / workers

所以多机情况下 API Server 并不是天然对称的。通常只有入口节点暴露 HTTP API,其他节点作为执行资源加入模型并行体系。

架构小结

vLLM 的服务启动链路可以浓缩成三句话。

第一,API Server 启动时构造 AsyncLLMAsyncLLM 构造时就启动了 EngineCore 后台进程。

第二,API Server 和 EngineCore 通过 ZMQ + msgpack 通信,API Server 发 EngineCoreRequest,EngineCore 回 EngineCoreOutputs

第三,EngineCore 不是 GPU worker 本身。EngineCore 管 scheduler、KV cache manager 和 executor;executor 再负责拉起或管理真正的 GPU worker。

最终的层次关系是:

1
2
3
4
5
OpenAI protocol layer
-> AsyncLLM frontend layer
-> AsyncMPClient IPC layer
-> EngineCore scheduling layer
-> Executor / Worker execution layer

这个分层是 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。


vLLM Code Reading (Section 1): vLLM API Server 如何启动 EngineCore
https://jeremyguo.space/2026/06/24/vllm-code-reading-section-1-api-server-enginecore-startup/
作者
郭俊毅 / JeremyGuo
发布于
2026年6月24日
许可协议