Appearance
第2章 EngineCore:引擎的心脏
"A conductor does not make a sound. He depends on his ability to make other people powerful." -- Benjamin Zander
本章要点
- 理解 EngineCore 的主循环:从输入处理到输出收集的完整周期
- 掌握 EngineCore 与 API Server 之间的 ZMQ 通信协议
- 深入
EngineCoreClient的三种实现:同步、异步、多进程 - 理解请求生命周期管理:从
add_request到abort_request - 认识 EngineCore 的"指挥者模式"——为什么它什么都不做,却又什么都离不开它
2.1 指挥者模式
在一个交响乐团中,指挥不演奏任何乐器。他不吹长笛,不拉小提琴,不敲定音鼓。但如果指挥离开,乐团会在几小节内陷入混乱——节奏失序,声部冲突,音乐瓦解。
EngineCore 就是 vLLM 的指挥。
它不做分词——那是 API Server 的工作。它不做 GPU 计算——那是 Worker 的工作。它甚至不做具体的调度决策——那是 Scheduler 的工作。但它协调所有这些组件的节奏,确保它们在正确的时刻做正确的事。
让我们打开 vllm/v1/engine/core.py,看看这位指挥是怎么工作的。
2.2 EngineCore 的主循环
EngineCore 的核心是一个无限循环。每一次迭代,它执行以下步骤:
这个循环看似简单,但每一步都暗藏玄机。
步骤一:接收新请求
EngineCore 维护一个请求队列。每次循环开始时,它会检查是否有新请求到达。新请求通过 add_request() 方法进入系统:
python
# vllm/v1/engine/core.py(简化)
class EngineCore:
def add_request(self, request: EngineCoreRequest):
"""将新请求加入引擎。"""
req = Request.from_engine_core_request(request)
self.scheduler.add_request(req)注意这里的 Request 类型转换。API Server 发来的是 EngineCoreRequest(一个轻量级的传输对象,只包含 Token IDs 和采样参数),EngineCore 将其转换为内部的 Request 对象(vllm/v1/request.py),后者携带了完整的生命周期状态。
这种分层设计是刻意的:API Server 不需要知道引擎内部的状态管理细节,EngineCore 也不需要关心 HTTP 协议。两者通过一个简洁的数据契约(EngineCoreRequest)解耦。
步骤二:执行调度
这是 EngineCore 每次循环中最关键的一步:
python
# vllm/v1/engine/core.py(简化)
def step(self) -> list[EngineCoreOutput]:
# 1. 调度器决定本步要处理哪些请求
scheduler_output = self.scheduler.schedule()
# 2. 将调度结果交给 Executor 执行
executor_output = self.executor.execute(scheduler_output)
# 3. 处理执行结果,更新请求状态
engine_core_outputs = self.process_outputs(
scheduler_output, executor_output
)
return engine_core_outputsscheduler.schedule() 返回的 SchedulerOutput 包含本步要处理的所有请求及其 Token 数量。这个调度结果被原封不动地传递给 Executor——EngineCore 不修改调度决策,它只是传话。
但"只是传话"并不意味着 EngineCore 无足轻重。恰恰相反——它是唯一知道调度结果和执行结果都长什么样的组件。Scheduler 不知道 GPU 执行了什么,Worker 不知道哪些请求被调度了,只有 EngineCore 掌握全局画面。
步骤三:处理输出
GPU 计算完成后,Worker 返回每个请求新生成的 Token。EngineCore 需要判断每个请求的状态:
- 还在继续——新 Token 不是终止符,请求继续参与后续调度
- 正常完成——遇到 EOS Token 或达到最大长度
- 被中断——用户主动取消(
abort_request)
对于完成的请求,EngineCore 通知 Scheduler 释放其占用的 KV Cache 块。对于继续的请求,新 Token 被追加到请求的上下文中,等待下一步调度。
2.3 EngineCoreClient:三种面孔
API Server 不直接与 EngineCore 交互——它通过 EngineCoreClient 间接访问。这一层抽象的存在是为了支持不同的部署模式。
SyncClient——EngineCore 在同一进程内同步调用。用于离线推理(LLM 类),不需要 HTTP 服务时最简单直接。
AsyncClient——EngineCore 在同一进程内异步调用。用于需要异步 IO 但不需要进程分离的场景。
MultiprocClient——EngineCore 运行在独立进程中,通过 ZMQ 通信。这是生产环境的推荐模式。API Server 调用 add_request() 时,请求被序列化后通过 ZMQ Socket 发送到 EngineCore 进程;get_output() 则从 ZMQ Socket 接收新产生的 Token。
python
# vllm/v1/engine/core_client.py(简化)
class MultiprocClient(EngineCoreClient):
def __init__(self, ...):
# 启动 EngineCore 子进程
self.proc = Process(target=run_engine_core, ...)
self.proc.start()
# 建立 ZMQ 连接
self.ctx = zmq.Context()
self.input_socket = self.ctx.socket(zmq.PUSH)
self.output_socket = self.ctx.socket(zmq.PULL)
async def add_request_async(self, request):
# 序列化并发送
msg = pickle.dumps(request)
await self.input_socket.send(msg)
async def get_output_async(self):
# 接收结果
msg = await self.output_socket.recv()
return pickle.loads(msg)为什么要三种实现?因为 vLLM 需要服务不同场景:
- 研究者做实验时,SyncClient 最方便——一行代码就能跑推理
- Web 服务需要异步处理并发请求,AsyncClient 或 MultiprocClient 是必需的
- 高吞吐生产环境,MultiprocClient 的进程分离是性能保证
通过 Client 接口抽象,上层代码不需要关心底层是哪种模式。切换部署方式只需要改一个配置,不需要修改任何业务逻辑。
2.4 请求的一生
让我们跟踪一个请求从出生到死亡的完整生命周期,看看它在 EngineCore 中经历了什么。
WAITING——请求刚进入系统,还没有分配 KV Cache 块。它在调度器的等待队列中,按到达顺序排列(FCFS)。
RUNNING——请求被调度器选中,已分配 KV Cache 块,正在参与 GPU 计算。每一步生成一个(或多个,如投机解码)Token。
PREEMPTED——显存紧张时,调度器可能"抢占"一些低优先级的正在运行的请求,释放它们的 KV Cache 块给更紧急的请求。被抢占的请求回到 WAITING 状态,之后需要重新预填充。V1 中抢占的实现比 V0 更轻量——因为统一 Token 调度天然支持部分预填充,被抢占的请求可以分块恢复而不是全量重来。
FINISHED——请求正常完成。完成的原因可能是:生成了 EOS Token、达到了 max_tokens 限制、匹配了用户指定的停止词。
ABORTED——请求被外部取消。常见场景:用户关闭了浏览器(流式请求断开),或者应用层主动调用 abort_request()。
无论以哪种方式结束,EngineCore 都会通知 KV Cache Manager 释放该请求占用的所有块。在有前缀缓存的情况下,这些块不会被立即回收,而是进入缓存池供后续请求复用——这个机制我们在第 10 章会详细讨论。
2.5 异步的艺术
EngineCore 的实现中有一个精妙之处值得单独拿出来说:它如何在不阻塞的情况下同时处理输入和输出。
在 MultiprocClient 模式下,EngineCore 进程内部运行着一个事件循环。这个循环需要同时做两件事:
- 监听新请求——API Server 随时可能发来新请求或取消指令
- 驱动推理步骤——每一步调度→执行→输出是一个完整的周期
这两件事不能互相阻塞。如果 EngineCore 在等待 GPU 计算完成时不能接收新请求,那新请求就会在 ZMQ 缓冲区中堆积,增加响应延迟。
V1 的解法是将推理步骤实现为非阻塞操作。GPU 计算本身是异步的——调用 CUDA 内核后,CPU 可以立即返回去做其他事情(比如接收新请求),等 GPU 完成后再取结果。EngineCore 利用了这个特性:
时间线:
EngineCore CPU | GPU
──────────────────────────────
t0 调度第 N 步 |
t1 发送给 Worker |
t2 接收新请求 | 执行第 N 步
t3 处理取消请求 | 执行第 N 步
t4 取回第 N 步结果 | 完成
t5 调度第 N+1 步 |
... | ...在 t2-t3 这段时间里,CPU 和 GPU 在并行工作。CPU 在处理 IO(接收/取消请求),GPU 在计算(前向传播)。这种流水线化的设计让 EngineCore 能在不牺牲吞吐量的前提下保持低延迟响应。
2.6 容错与优雅退出
生产环境中,引擎不能随便崩溃。EngineCore 实现了几层容错机制:
Worker 崩溃恢复——如果一个 Worker 进程意外退出(比如 GPU OOM),MultiprocExecutor 会检测到子进程终止,并尝试重启。重启期间,正在进行的请求会超时并被标记为失败。
请求超时——每个请求都有一个可配置的超时时间。如果一个请求长时间没有进展(比如被无限期抢占),EngineCore 会将其标记为失败并释放资源。
优雅退出——收到 SIGTERM 信号时,EngineCore 不会立即退出。它会:
- 停止接收新请求
- 等待当前正在执行的步骤完成
- 通知所有 Worker 释放 GPU 资源
- 关闭 ZMQ Socket
- 退出进程
这个顺序很重要——如果直接强杀进程,GPU 资源可能不会被正确释放,导致后续进程无法使用该 GPU。
2.7 本章小结
EngineCore 是 vLLM 的心脏,但它的力量不在于"做什么",而在于"让正确的组件在正确的时刻做正确的事":
- 指挥者模式——EngineCore 协调 Scheduler、KV Cache Manager、Executor 的节奏,自身不参与具体计算
- 主循环——接收请求 → 调度 → GPU 执行 → 处理输出 → 发送结果,周而复始
- 三种 Client——Sync/Async/Multiproc,适配不同部署场景,上层代码无感知
- 请求生命周期——WAITING → RUNNING → FINISHED/ABORTED,支持抢占和恢复
- 异步流水线——CPU 和 GPU 并行工作,IO 不阻塞计算
下一章,我们将深入调度器——那个决定"谁先谁后"的裁判。它的每一个决策都直接影响吞吐量和延迟,是 vLLM 性能优化的核心战场。
源码导航
- EngineCore 主类:
vllm/v1/engine/core.py- Client 实现:
vllm/v1/engine/core_client.py- Request 数据结构:
vllm/v1/request.py- MultiprocExecutor:
vllm/v1/executor/multiproc_executor.py