《vLLM 内核探秘》完整目录
- 前言
- 第1章 架构总览
- 第2章 EngineCore:引擎的心脏(当前)
- 第3章 调度器:Token 的交通指挥
- 第4章 PagedAttention:虚拟内存的启示
- 第5章 KV Cache 管理:寸土寸金的显存
- 第6章 Worker 与 Executor:GPU 军团
- 第7章 模型加载与权重管理
- 第8章 前向计算与 CUDA Graph
- 第9章 采样与输出处理
- 第10章 前缀缓存:零开销的加速
- 第11章 分块预填充与混合批处理
- 第12章 投机解码:以小博大
- 第13章 量化引擎:精度与速度的平衡
- 第14章 张量并行与流水线并行
- 第15章 多模态推理
- 第16章 LoRA 适配器热切换
- 第17章 API 服务器与生产部署
- 第18章 设计模式与架构哲学
第2章 EngineCore:引擎的心脏
"A conductor does not make a sound. He depends on his ability to make other people powerful." -- Benjamin Zander
:::tip 本章要点
- 理解 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_outputs
scheduler.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 间接访问。这一层抽象的存在是为了支持不同的部署模式。
同步调用"] Client --> |接口相同| Async["AsyncClient
异步调用"] Client --> |接口相同| MP["MultiprocClient
多进程 + ZMQ"] style API fill:#8b5cf6,color:#fff,stroke:none style Client fill:#f59e0b,color:#fff,stroke:none style Sync fill:#10b981,color:#fff,stroke:none style Async fill:#10b981,color:#fff,stroke:none style MP fill:#10b981,color:#fff,stroke:none
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 利用了这个特性:
markdown
时间线:
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