---
title: "第 1 课:vLLM V1 源码地图:AsyncLLM、EngineCore 与 Worker"
jupyter: python3
---
## 本课目标与通过标准
本课产出:能从在线请求入口沿真实调用链追到一次模型执行和输出回传,并准确说明 frontend、EngineCore、Scheduler、Executor/Worker 的状态所有权。
通过要求:唯一代码填空题通过全部断言;Q1~Q3 都能沿源码对象给出因果链;能指出一个正确性不变量、一个性能边界和一个需要 benchmark 才能确认的结论;总分至少 8/10。
## 源码版本与阅读方法
本课程使用固定 commit 的永久链接保证行号可复现,同时链接 current docs 供核对最新变化。阅读时先画调用图和状态所有权,再进入分支;不要从大文件第一行机械顺读。
本课按下面顺序阅读,不要从 2500 行的 `core.py` 第一行顺读:
| 顺序 | 文件与入口 | 带着什么问题读 |
|---:|---|---|
| 1 | `vllm/v1/engine/async_llm.py`:`AsyncLLM.generate`、`_add_request`、`_run_output_handler` | 谁负责 tokenization、请求流和客户端取消? |
| 2 | `vllm/v1/engine/core.py`:`EngineCore.step` | 一次迭代为何是 schedule → execute → update? |
| 3 | `core.py`:`EngineCoreProc.run_busy_loop`、`_process_engine_step` | frontend 与 engine core 为什么分进程? |
| 4 | `vllm/v1/core/sched/interface.py`:`SchedulerInterface` | scheduler 的输入输出合同是什么? |
| 5 | `vllm/v1/worker/gpu_model_runner.py` | 哪些状态必须在 GPU worker 内,哪些不能由 frontend 猜? |
源码快照:vLLM `8e958902eee5`。方法会继续演进,面试回答应先声明所读版本。
## 核心心智模型
### 1. 它是什么,解决什么问题
**vLLM V1 架构**是大语言模型高并发推理领域最具代表性的重构工程范式。在早期的 vLLM V0 架构中,Python 全局解释器锁(GIL)瓶颈明显、调度逻辑与异步 HTTP 事件循环深层交织,导致在单步耗时仅需数毫秒的轻量 Decode 场景中,CPU 侧的调度开销往往超过了 GPU 的计算耗时,引发严重的“GPU 算力饥饿等待 CPU 调度”的瓶颈。
为了攻克这一系统瓶颈,vLLM V1 确立了严格的三层解耦架构(图 1-2):
```{=html}
<div class="fd fd-flow" role="img"><div class="fd-stack"><div class="fd-comp fd-td"><div class="fd-rank"><div class="fd-sg" data-nid="Frontend" data-depth="0"><div class="fd-sg-title">API Server / Frontend</div><div class="fd-stack"><div class="fd-comp fd-td"><div class="fd-rank fd-few"><div class="fd-node fd-box" data-nid="AsyncLLM">AsyncLLM 异步服务接口</div><div class="fd-node fd-box" data-nid="ReqQueue">输入请求队列 (Input Queue)</div></div></div></div></div></div><div class="fd-gap" style="--gl:153px;--gt:53px"></div><div class="fd-rank"><div class="fd-sg" data-nid="Core" data-depth="0"><div class="fd-sg-title">EngineCore (单事件循环单线程)</div><div class="fd-stack"><div class="fd-comp fd-td"><div class="fd-rank fd-few"><div class="fd-node fd-box" data-nid="Scheduler">Unified Scheduler (统一调度器)</div><div class="fd-node fd-box" data-nid="KVManager">KVCacheManager (显存块分配)</div></div></div></div></div></div><div class="fd-gap" style="--gl:118px;--gt:53px"></div><div class="fd-rank"><div class="fd-sg" data-nid="Backend" data-depth="0"><div class="fd-sg-title">分布式 Worker 集群 (WorkerPool)</div><div class="fd-stack"><div class="fd-comp fd-td"><div class="fd-rank fd-few"><div class="fd-node fd-box" data-nid="W0">ModelRunner (GPU 0 - TP Rank 0)</div><div class="fd-node fd-box" data-nid="W1">ModelRunner (GPU 1 - TP Rank 1)</div></div></div></div></div></div></div></div><i class="fd-e" data-s="AsyncLLM" data-t="Core" data-k="solid" data-h="1" data-b="0">ZMQ / IPC 无锁通道</i><i class="fd-e" data-s="Scheduler" data-t="Backend" data-k="solid" data-h="1" data-b="0">发送执行指令</i><i class="fd-e" data-s="KVManager" data-t="Backend" data-k="solid" data-h="1" data-b="0">发送执行指令</i></div>
```
<p class="caption" align="center"><em>图 1-2:vLLM V1 三层解耦核心架构 (Frontend → EngineCore → Workers)</em></p>
- **`AsyncLLM`(前端服务层)**:面向客户端的异步 HTTP/gRPC/SSE 接入网关,负责请求接入、Tokenization 分词、流式输出解包与取消信号监听;
- **`EngineCore` / `EngineCoreProc`(调度核心内核层)**:运行在完全物理隔离的独立进程中,由单事件循环主导,独占全局请求生命周期调度器(`Scheduler`)与显存逻辑账本(`KVCacheManager`);
- **`Worker` / `GPUModelRunner`(设备执行层)**:由分布式并行(TP/PP)驱动的执行引擎,独占物理 GPU 张量、物理 KV Block 内存池与 CUDA Graph 捕获环境。
它解决的核心工程问题是:**通过清晰的单写者状态所有权(Single-Writer State Ownership)与轻量化 IPC 进程解耦,将 CPU 控制侧的每步调度耗时压制在微秒级,实现调度步频与 GPU 硬件峰值吞吐的完全匹配**。
### 2. 它如何工作
端到端请求流转与核心控制循环如图 1-1 至图 1-3 所示:
:::: {.vfig}
::: {.vfig-title}
Prefill 受算力限制,Decode 受带宽限制
:::
::::: {.vfig-grid style="--cols:2"}
::: {.vfig-card .c-blue}
**Prefill(处理 prompt)**
- 一次计算全部 $S$ 个 token:GEMM
- 算术强度约 $B \cdot S$ FLOP/Byte(FP16,$B \cdot S \ll d$ 时)
- $B \cdot S$ 到几百即越过 H100 拐点(约 295)
:::
::: {.vfig-card .c-red}
**Decode(逐 token 生成)**
- 每个请求每步只算 1 个 token:GEMV
- 算术强度约 $B$ FLOP/Byte
- 每步都要读完整权重和全部 KV Cache
:::
:::::
::: {.vfig-key}
对策:Continuous Batching 增大 $B$;PagedAttention 减少 KV 碎片以容纳更多请求;投机解码把多步 decode 合成一次验证前向
:::
::::
<p class="caption">图 1-1:Prefill 与 Decode 的算术强度对比</p>
1. **三段式步进迭代循环(Schedule $\to$ Execute $\to$ Update)**:
在 `EngineCore.step()` 内部,系统严格遵循严谨的时序管道:
- **调度阶段(Schedule)**:`Scheduler` 消费输入队列,根据全局 Token 预算(Token Budget)生成本次批次的执行计划 `plan = scheduler.schedule()`;
- **异步发射与计算重叠(Async Execute & Overlap)**:通过 `model_executor.execute_model(plan, non_block=True)` 非阻塞地向 GPU Worker 广播执行指令。在 GPU 执行 Heavy GEMM 与 Attention 的微小间隙,CPU 主线程并发执行结构化输出的 Grammar 状态机掩码提取;
- **等待同步与取消处理(Wait & Abort Drain)**:同步等待 Worker 的 `future()` 返回本次推理结果;在应用输出前,优先消费清空执行期间积累的取消请求队列 `process_aborts()`;
- **状态递推更新(Update)**:调用 `scheduler.update_from_output(plan, model_output)`,更新每个请求的已计算 Token 计数并回收已完结请求的逻辑 KV 块。
2. **端到端流水线推进(图 1-3)**:
```{=html}
<div class="fd fd-flow" role="img"><div class="fd-stack"><div class="fd-comp fd-lr" data-minw="1265"><div class="fd-rank"><div class="fd-node fd-box" data-nid="Req">HTTP 请求到达</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="Tokenize">分词器编码 Tokens</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="PutReq">压入 EngineCore 请求队列</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="Step">EngineCore::step() 统一调度决策</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="Forward">GPU ModelRunner 并行前向推理 (TP AllReduce)</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="Detokenize">采样器输出 Token 解码并流式回传 SSE</div></div></div></div><i class="fd-e" data-s="Req" data-t="Tokenize" data-k="solid" data-h="1" data-b="0"></i><i class="fd-e" data-s="Tokenize" data-t="PutReq" data-k="solid" data-h="1" data-b="0"></i><i class="fd-e" data-s="PutReq" data-t="Step" data-k="solid" data-h="1" data-b="0"></i><i class="fd-e" data-s="Step" data-t="Forward" data-k="solid" data-h="1" data-b="0"></i><i class="fd-e" data-s="Forward" data-t="Detokenize" data-k="solid" data-h="1" data-b="0"></i></div>
```
<p class="caption" align="center"><em>图 1-3:vLLM V1 端到端请求流转与异构协同执行流水线</em></p>
HTTP 请求到达后,`AsyncLLM` 完成异步分词,将 `EngineCoreRequest` 经 msgspec 序列化后通过 ZMQ socket 发送给 `EngineCore` 进程;`EngineCore` 批量消费并驱动 GPU ModelRunner 并发前向传播;采样完成后的 Token 结果以流式事件(SSE)的形式逆向实时推送回前端。
### 3. 正确性条件与常见误区
- **严格的单写者状态所有权(Single Writer Discipline)**:
整个系统中每个核心对象的状态拥有唯一的权威写入者:
- `AsyncLLM` 独占流式传输通道与输入请求缓冲,严禁越权修改内部 KV 账本;
- `EngineCore` 与 `Scheduler` 独占跨步请求生命周期(WAITING、RUNNING、PREEMPTED、FINISHED_*;V1 没有 SWAPPED 状态,抢占后的请求释放 KV 块并在恢复时重算),严禁 Worker 擅自更改请求状态;
- `GPUModelRunner` 独占物理显存与算子 Kernel 执行。若跨进程试图共享可变全局对象,会导致灾难性的数据竞争与状态错位。
- **Abort 取消处理的原子时序边界**:
若请求在 GPU 执行前向传播期间被客户端主动取消(如用户关闭网页),`EngineCore` 必须在**调用 `scheduler.update_from_output` 之前**消费并处理取消信号。若顺序颠倒(先更新再取消),模型刚生成的 Token 会被误推送给已断开的客户端,甚至引起已被释放的物理 Block 发生二次读写踩内存。
- **计划与执行结果的版本一致性不变量**:
`SchedulerOutput` 与 `ModelRunnerOutput` 必须严格属于同一个迭代步骤。严禁在 Worker 执行失败时返回伪造的静默空输出,必须通过强类型异常驱动全局容错与降级。
### 4. 性能与工程取舍
- **进程解耦带来的 IPC 序列化开销 vs GIL 消除红利**:
将 EngineCore 独立为子进程释放了 Python GIL 的并发阻塞,但引入了进程间通信开销。vLLM V1 在 Frontend 与 EngineCore 之间使用 ZMQ socket 加 msgspec 序列化传输请求与输出(大张量可走零拷贝的附加缓冲区);共享内存消息队列(`MessageQueue`)用于多卡场景下 Executor 向各 Worker 广播调度结果。通信开销被控制在较小比例,使架构收益超过通信开销。
- **Grammar 准备与 GPU 计算的精妙重叠**:
在 JSON 等强约束解码(Structured Outputs)中,计算下一个合法 Token 的 Bitmask 极其消耗 CPU 算力。通过在 `execute_model(non_block=True)` 发射后立即开始计算 Grammar,系统完美地将 CPU 纯逻辑与 GPU 算力执行掩盖在同一时序窗口内,实现了近乎零气泡的控制流水线。
## 具体演示:单请求执行取消与控制流跟踪演练
考虑一个典型的并发取消场景:
1. 请求 $R_0$ 被调度进入当前批次,调度计划为 `{"scheduled_tokens": 4}`;
2. 系统调用 `execute_async` 向 GPU 发射异步推理任务,GPU 开始执行前向注意力计算;
3. 在 GPU 计算尚未结束的窗口内,客户端由于超时断开了 HTTP 连接,取消信号进入待处理队列;
4. 核心调度流程执行控制:
- 首先执行 `grammar = get_grammar(plan)`,准备下一 Token 约束;
- 随后执行 `future()` 阻塞等待 GPU 输出;
- **关键时序点**:在得到 `model_output` 后,必须立刻执行 `process_aborts()`,将 $R_0$ 标记为已取消;
- 最后执行 `update(plan, model_output)`:调度器感知到 $R_0$ 已被取消,安全丢弃其输出 Token 并直接回收其持有的 KV Cache Block,绝不会将废弃数据向前端泄露。
断言 trace 序列严格保持:`["schedule", "execute", "grammar", "wait", "aborts", "update"]`,完整闭环。
## 实践任务:唯一代码填空题
补齐一个最小 `EngineCore.step` 控制流模型。重点不是模拟 GPU,而是保持官方源码中的顺序合同;不得删除 trace 断言。
只能修改 `TODO`/`______` 位置,不得删除断言或放宽通过条件。
```{python}
#| course_role: exercise
def engine_core_step(has_requests, schedule, execute_async,
get_grammar, process_aborts, update):
trace = []
if not has_requests:
return {}, False, trace
# TODO:保持 schedule → async execute/grammar → wait → aborts → update。
plan = ______
trace.append("schedule")
future = ______
trace.append("execute")
grammar = ______
trace.append("grammar")
model_output = ______
trace.append("wait")
______
trace.append("aborts")
outputs = ______
trace.append("update")
return outputs, plan["scheduled_tokens"] > 0, trace
events = []
schedule = lambda: {"scheduled_tokens": 4}
execute = lambda plan: (lambda: {"tokens": [7]})
grammar = lambda plan: {"mask": None}
aborts = lambda: events.append("abort-queue-drained")
update = lambda plan, output: {"r0": output["tokens"]}
out, ran, trace = engine_core_step(True, schedule, execute, grammar, aborts, update)
assert out == {"r0": [7]} and ran
assert trace == ["schedule", "execute", "grammar", "wait", "aborts", "update"]
assert events == ["abort-queue-drained"]
assert engine_core_step(False, schedule, execute, grammar, aborts, update) == ({}, False, [])
```
### 检查方法
运行断言;再让 `scheduled_tokens=0`,解释为什么仍可能走完控制路径但 `ran=False`。
### Q1
`AsyncLLM`、`EngineCore`、`Scheduler` 和 GPU worker 各自拥有什么状态?为什么不能把它们合成一个全局可变对象?
**你的答案:**
### Q2
为什么源码先 `execute_model(non_block=True)`,再取得 grammar bitmask,最后等待 future?这是否意味着 grammar 可以晚于采样?
**你的答案:**
### Q3
某请求在 GPU forward 期间被取消。为何 `EngineCore.step` 要在 `update_from_output` 前处理 abort?
**你的答案:**
## 评分规则
- 代码 4 分:主路径 2 分、边界条件 1 分、能映射回源码对象 1 分;
- Q1~Q3 各 2 分:必须包含对象、状态变化、正确性或成本链;
- 一票否决:把论文峰值写成普遍生产结论;把源码支持写成所有模型/硬件可用;混淆算法正确性与性能;只背类名而说不清状态所有权。
::: {.callout-tip collapse="true" title="💡 点击展开参考答案与详细代码实现" icon="false"}
## 参考答案(仅 answer 分支)
先独立完成。核对后请更换一个 batch、cache 或硬件条件重新推演,不能只复制。
```{python}
#| course_role: solution
def engine_core_step(has_requests, schedule, execute_async,
get_grammar, process_aborts, update):
trace = []
if not has_requests:
return {}, False, trace
plan = schedule()
trace.append("schedule")
future = execute_async(plan)
trace.append("execute")
grammar = get_grammar(plan)
trace.append("grammar")
model_output = future()
trace.append("wait")
process_aborts()
trace.append("aborts")
outputs = update(plan, model_output)
trace.append("update")
return outputs, plan["scheduled_tokens"] > 0, trace
events = []
schedule = lambda: {"scheduled_tokens": 4}
execute = lambda plan: (lambda: {"tokens": [7]})
grammar = lambda plan: {"mask": None}
aborts = lambda: events.append("abort-queue-drained")
update = lambda plan, output: {"r0": output["tokens"]}
out, ran, trace = engine_core_step(True, schedule, execute, grammar, aborts, update)
assert out == {"r0": [7]} and ran
assert trace == ["schedule", "execute", "grammar", "wait", "aborts", "update"]
assert events == ["abort-queue-drained"]
assert engine_core_step(False, schedule, execute, grammar, aborts, update) == ({}, False, [])
```
### Q1 参考答案
1. **各层核心对象的状态所有权矩阵(State Ownership Matrix)**:
- **`AsyncLLM`(前端服务层)**:拥有 HTTP/gRPC/SSE 客户端连接句柄、异步 Request Output 流、Tokenization/Detokenization 缓存以及尚未进入调度的待处理请求队列。负责面向客户端的生命周期生命线;
- **`EngineCore`(调度协调层)**:拥有主事件循环(Busy Loop)、跨进程 IPC 通信通道、输入/输出序列化协议栈,以及对 Executor 的协调控制逻辑;
- **`Scheduler`(权威裁决层)**:拥有全局请求状态机(WAITING、RUNNING、PREEMPTED、FINISHED_*,抢占采用重算而非换出)、逻辑 Token 计数(`num_computed_tokens`, `total_tokens`)、逻辑 KV Block 映射账本以及请求抢占(Preemption)决策权;
- **`GPUModelRunner / Worker`(设备执行层)**:独占物理 GPU 显存张量、PagedAttention 物理 KV 显存池(Block Table)、CUDA Graph 预热与执行环境、采样器状态以及分布式通信(NCCL TP/PP)组。
2. **严禁合成全局可变对象的深层因果**:
- **进程隔离与 GIL 消除前提**:为了突破 Python GIL 限制,Frontend、EngineCore 与 Worker 分布在不同物理进程中。跨进程内存天然不可变共享;
- **杜绝多写者脑裂(Brain-Split & Race Condition)**:若允许 Worker 越权修改 Scheduler 状态,在分布式流水线或异步执行中会产生无序状态竞争,无法确定哪个组件持有真值;
- **故障隔离与可重放契约**:单写者加显式消息契约(IPC / Message Queue)使得每个组件的输入输出严格成对可审计,当某个 GPU Worker 发生 OOM 或 NCCL 崩溃时,EngineCore 能够精准定位故障边界,执行请求安全重排或优雅降级。
### Q2 参考答案
1. **时序与计算重叠机制(CPU-GPU Pipeline Overlap)**:
- 在结构化输出(JSON Schema / Regex 约束解码)中,根据语法树或有限状态机(FSM)计算下一个合法 Token 的 Bitmask 是纯 CPU 密集型任务,计算耗时可能长达数毫秒;
- 源码采用 `execute_model(non_block=True)` 是为了实现**控制流与设备执行的异步解耦**:系统将本次 Batch 的张量元数据非阻塞地发射给 GPU Worker 后立即返回,GPU 开始执行耗时的自注意力与 GEMM 计算;
- 此时 CPU 主线程并没有空转等待,而是立即并发执行 `get_grammar(plan)`,在本地内存中完成 Bitmask 的推导与位运算;
- 随后调用 `future()` 阻塞等待 GPU 输出。此时 CPU 的 Grammar Mask 恰好计算完毕,二者无缝对接。
2. **Grammar 绝不能晚于采样(Sampling Constraint)**:
- 这并不意味着 Grammar 可以晚于采样。GPU ModelRunner 在执行 Transformer Forward 产出未归一化的 Logits 后,必须等待 Grammar Mask 的位掩码注入(将非法 Token 的 Logits 强行置为 $-\infty$),然后才能执行 Softmax 和采样操作;
- `non_block=True` 仅仅是将 Python 端的等待时机从“发射瞬间”推迟到了“提取最终输出之前”,在不破坏物理数据因果依赖的前提下,完全掩盖了 CPU 侧的状态机解析开销。
### Q3 参考答案
若某请求在 GPU Forward 期间被客户端主动取消(Abort),`EngineCore.step` 必须**严格在调用 `scheduler.update_from_output` 之前优先消费并清空 Abort 队列**,因果链如下:
1. **防止已取消请求产出“幽灵 Token”并破坏前端流**:
- GPU 执行的是发射时刻快照决定的批次,因此 Model Output 中必定包含该请求刚刚生成的下一个 Token;
- 若先调用 `update_from_output`,调度器会将该新生成的 Token 视作有效计算结果,更新其已完成计数并触发 Output Processor 向客户端推送。此时已断开连接的客户端不仅无法接收,还会引发前端管道断裂异常或日志报错;
2. **保障物理显存与逻辑账本原子释放**:
- 优先执行 `process_aborts()` 会将该请求在调度器的活动集合中正式注销,并向 KVCacheManager 宣告其占有的所有逻辑与物理 KV Block 可以被安全回收;
- 随后执行 `update_from_output` 时,调度器便能根据最新的“存活请求子集”做过滤,直接将已取消请求的输出丢弃,且绝不会把已被回收释放的 Block 重新标记为活动状态(防止 Block Use-After-Free 悬空指针踩内存);
3. **提供清晰的串行化审计点**:
先处理事件、再应用批次更新,保证了每一次迭代步的结束状态都是自洽、闭合且全局一致的。
## 参考资料
- [vLLM AsyncLLM source](https://github.com/vllm-project/vllm/blob/8e958902eee56ca5158728f1dd5a32246f0246f3/vllm/v1/engine/async_llm.py#L283-L750)
- [vLLM EngineCore source](https://github.com/vllm-project/vllm/blob/8e958902eee56ca5158728f1dd5a32246f0246f3/vllm/v1/engine/core.py#L581-L738)
- [vLLM Scheduler interface](https://github.com/vllm-project/vllm/blob/8e958902eee56ca5158728f1dd5a32246f0246f3/vllm/v1/core/sched/interface.py)
- [vLLM V1 guide](https://docs.vllm.ai/en/stable/usage/v1_guide/)
- [vLLM optimization and process architecture](https://docs.vllm.ai/en/latest/configuration/optimization/)
源码链接固定到本课审阅 commit;current docs、支持矩阵和默认参数会变化,面试或部署前必须按目标版本重新核对。
:::
::: {.course-pagination}
[**← 上一课**<br>第 11 课:动态形状](../runtime/lesson11_dynamic_shapes.qmd){.pagination-link .pagination-prev}
[**下一课 →**<br>第 2 课:vLLM V1 统一调度器、KV...](lesson02.qmd){.pagination-link .pagination-next}
:::