vLLM Async Scheduling:三态配置、投机解码与状态提交
沿 vLLM 源码解释 async scheduling 的三态配置、output placeholder、EAGLE/MTP 状态归属,以及 accepted-count 同步如何影响真实重叠
Async scheduling 和 speculative decoding 都在“提前”:前者让 CPU 在 GPU 返回前排下一轮,后者让 Drafter 在 Target 确认前猜未来 token。两者组合后,系统必须同时回答:哪些 token 已确认,哪些只是占位,哪些 KV 已经计算但还不能成为可复用历史?
这篇沿 vLLM 源码回答三个具体问题:
async_scheduling=False / True / None应该是什么契约;- EAGLE 与 MTP 的 hidden、embedding、LM head、draft KV 和 target KV 到底是什么关系;
- 为什么 async 已经开启,Trace 里仍可能看不到 scheduler 与 GPU forward 重叠。
- 一句话:Async Scheduler 用 placeholder 代表尚未回来的 token,再按 accepted prefix 修正逻辑前沿;EAGLE/MTP 都走 draft → target verify,不应按“旁路是否污染主 KV”二分。
- 三个判断:先区分 requested config 与 resolved runtime;再区分 target token、placeholder、draft token 与 computed frontier;最后用 Trace 验证 schedule 是否真的覆盖 GPU forward。
- 核心模型:
confirmed frontier = num_computed_tokens - num_output_placeholders。Draft 被拒绝时,回退的是逻辑前沿;线性位置的旧 KV 随后会被正确计算覆盖。 - 边界:本文锁定社区
main@beca88e59;框架能力、支持矩阵和默认值会演进,生产结论必须绑定版本与 backend。
1. 三态配置不是三个布尔值
一个性能开关若允许 None,就同时承担了“用户意图”和“框架决策”两层语义:
| 请求值 | 含义 | 不兼容时 |
|---|---|---|
False | 用户明确关闭 | 保持关闭 |
True | 用户明确要求开启 | 启动失败并返回原因 |
None | 自动决策 | 支持则开启;不支持则关闭并记录原因 |
最容易出错的写法,是把用户传入值原地改掉:
if async_scheduling and incompatible:
async_scheduling = False
if async_scheduling:
# 这里已经不知道用户原来是否传了 True
...
更稳的实现会分开:
requested: False | True | None
resolved: False | True
reason: explicit_disabled | compatible_default | unsupported_backend | ...
社区当前源码把默认值设为 None,显式 True 遇到不兼容组合会 hard fail,None 才允许自动降级:
这不只是 API 美观问题。性能特性若 silent fallback,服务可以正常启动,但使用者误以为优化已生效,随后把“配置未开启”误诊成“优化没有收益”。
2. Async Scheduler 提前了哪一步
同步执行每轮都等待结果:
schedule(N)
→ execute/sample(N)
→ update(N)
→ schedule(N+1)
Async Scheduler 希望把下一轮 CPU 工作前移:
GPU: execute + sample(N) ──────────────┐
CPU: schedule + prepare(N+1) ─┼→ reconcile(N)
└→ execute(N+1)
CPU 此时不知道下一 token 的值,只知道“本轮至多会产生多少 token”。因此它先记占位数量、分配执行资源,输出回来后再兑现。
五本不能混写的账
| 状态 | 表示什么 |
|---|---|
output_token_ids | 已返回并提交到请求的真实 token |
num_output_placeholders | 已排程、尚未由 GPU 输出兑现的位置 |
spec_token_ids | 下一轮交给 Target 验证的 draft token;在途时可先填 -1 |
num_computed_tokens | Scheduler 认为已经处理到的逻辑前沿,可能包含在途工作 |
| KV block / slot | 已分配、甚至已被 Kernel 写入的物理状态 |
vLLM 的 AsyncScheduler._update_after_schedule() 会预加 sampled-token 与 draft-token placeholder;输出返回后再减去已兑现的数量,并只把 confirmed frontier 之前的 block 作为可缓存状态:
可以把关键不变量记成:
confirmed frontier
= num_computed_tokens - num_output_placeholders
num_computed_tokens 不是“用户已经看到几个 token”,placeholder 也不是假 token 值;它们共同描述 CPU 为在途 GPU 工作预留到了哪里。
3. Speculative Decoding 又增加了一层不确定性
假设当前 confirmed context 是 A B C,Drafter 提出:
D E F
Target 批量验证后可能得到:
D accepted
E rejected
X correction token
F invalid after first rejection
下一轮真实起点是 A B C D X,但 CPU 想提前调度时还不知道接受了几个 draft token。accepted count 会改变:
- position ids 与 query length;
- block table、slot mapping 与可复用 KV 边界;
- repetition penalty、bad words 等 token 历史;
- structured output FSM;
- stop / max-token 判断;
- Mamba、GDN 等递推状态的有效位置。
所以 async + spec decode 的根冲突不是“会不会丢 token”,而是:
CPU 排下一轮所需的 commit frontier,仍在 GPU 的 rejection sampling 结果里。
输出回来后,Scheduler 根据 num_draft_tokens - num_accepted 得到 rejected 数量,同时回退 computed frontier 与 placeholders:
4. Target KV 会不会被“污染”
Target 验证候选时,确实会为候选位置执行 forward 并写 KV。这里 EAGLE 与 MTP 没有本质差异。
首次拒绝后的那些 KV 不能作为正确历史继续使用,但线性 accepted-prefix 场景通常不需要清空整段显存:
回退逻辑 frontier
→ 下一轮从 accepted prefix 的后一位置继续
→ 正确 token 的 KV 覆盖相同逻辑位置
正确性取决于 position、slot mapping、block 生命周期和 confirmed frontier 一致。树状 speculative decoding 的 accepted path 可能来自非连续槽位,才需要额外整理或复制接受分支。
因此,“有没有计算过候选 KV”不是判断主路径是否干净的标准。更有效的问题是:
- 这段状态属于 target verifier 还是 drafter?
- Scheduler 把哪一个位置当成 confirmed frontier?
- rejected 位置会被忽略、覆盖,还是需要搬运?
5. EAGLE 与 MTP 的真实差别
在当前 vLLM 中,常见 EAGLE 与 MTP 都会进入 use_eagle() 分支并构造 EagleProposer:
共同 serving 协议是:
Target hidden
→ Drafter 提出候选
→ Target 批量验证
→ RejectionSampler 保留 accepted prefix / 产生 correction
→ Scheduler commit + rollback logical frontier
差别主要在 Drafter 从哪里来、复用什么:
| 维度 | EAGLE | MTP |
|---|---|---|
| 训练产物 | 通常是与 Target 匹配的独立 EAGLE checkpoint/head | 通常是随 Target checkpoint 训练、交付的额外 MTP module/layer |
| Hidden 输入 | 读取 Target feature;EAGLE3 可组合多层 aux hidden | 读取 Target hidden;某些架构复用 pre-final mixer 或多流 hidden |
| Embedding | checkpoint 可自带;缺失或完全相同时可共享 Target embedding | vLLM 默认共享 Target embedding |
| LM head | checkpoint 可自带;缺失或相同时可共享 | vLLM 默认共享 Target LM head |
| Draft 依赖 | 轻量模型/head 自回归或树状生成 | 多个 MTP step 可连续调用,后一步依赖前一步候选与 hidden |
| Draft cache | Draft attention layer 有自己的 KV/state | MTP draft layer同样有自己的 KV/state;特定架构可能还有递推 cache |
| Target verify | 为候选位置计算 Target state | 相同 |
| Scheduler commit | 按 accepted prefix 统一结算 | 相同 |
源码直接展示了参数共享策略:
一句更准确的话是:
EAGLE 是独立训练的 feature drafter,MTP 是模型内生、随 Target 交付的 drafter;二者都属于 speculative branch,也都要由 Target 验证。
6. 为什么 MTP 的工程实现仍可能更难
“两者共用 serving 协议”不代表实现成本完全相同。MTP 的额外复杂度通常来自:
- 多个 draft step 的 hidden/KV 依赖链;
- 与 Target 共享 embedding、LM head 或中间 buffer;
- MTP module 和 Target 架构、checkpoint layout 绑定更紧;
- GDN/Mamba/short-conv/HC 等模型还维护 attention KV 之外的递推状态;
- 多 step MTP 的 metadata、padding、CUDA Graph shape 更复杂。
这些约束支持的结论是“需要逐模型和 backend 验证”,而不是“MTP 不能 async”。社区当前配置已经把 EAGLE/MTP 列入 async scheduling 支持范围。
7. 一次真实优化分析应该怎样写
只看到一个 7ms 的 cudaEventSynchronize,很容易直接写成“去掉同步可省 7ms”。这通常过早。
一条更完整的证据链是:
| 层次 | 要回答什么 | 示例 |
|---|---|---|
| 事实 | Trace 里实际发生了什么 | async 类已启用,但 schedule 没进入当前 GPU execute 窗口 |
| 归因 | 哪个依赖阻止了提前调度 | CPU 等待 accepted count,才能修正下一轮 position / slot |
| 排除 | 等待本身是否就是 GPU 空泡 | 同步期间 GPU 可能仍在执行上一轮尾部,因此不能把等待时长直接当收益 |
| 优化目标 | 真正想拿回什么 | 恢复 scheduler/prepare 与 GPU forward 的重叠,而非删除一个 API 名 |
| 验证 | 如何证明收益 | 同一 Case 对比 schedule-overlap、step wall time、TPOT、吞吐和输出一致性 |
推荐的 A/B 合同
固定:
model: checkpoint + revision
runtime: vLLM commit + backend
hardware: GPU + topology
workload: ISL/OSL distribution + concurrency
speculation: method + gamma + sampling
对比:
A: async_scheduling=False
B: async_scheduling=True
至少记录:
- 每轮 proposed / accepted / rejected token;
schedule与 GPU execute 的重叠比例;- accepted-count copy / event wait 的 CPU self-time 和 GPU overlap;
- step wall time、TTFT、TPOT、aggregate throughput;
- placeholder 最终归零、输出一致性、preemption / structured-output 错误。
如果再改 accepted-count GPU correction,应增加第三组并单独验证 batch、logprobs、tree spec decode 与 stateful model 边界。
8. 面试时怎么讲这个案例
不要从“我发现一个 if 写错了”开始。更有区分度的结构是:
- 契约:一个三态性能配置把显式意图与自动策略混在同一可变字段里,造成 silent fallback。
- 机制:Async Scheduler 用 placeholder 表示在途 token;spec decode 又让 accepted count 到 GPU 返回后才确定。
- 源码纠偏:沿
Config → AsyncScheduler → Scheduler update → GPUModelRunner → EagleProposer追踪后,发现 EAGLE/MTP 共用 proposer/verify/accounting 主链,推翻了“一个旁路、一个污染主 KV”的简单解释。 - Trace 证据:async 类名出现不等于流水有效;要检查 schedule 是否真的覆盖 GPU forward,以及 accepted-count sync 的暴露部分。
- 决策:先修配置契约和最终状态回读,再把优化目标定为 GPU-side correction / 延迟 reconciliation,并用固定 Case 做 A/B。
- 边界:源码证明机制,单条 Trace 证明一个 Case;没有受控实验前不承诺收益倍数。
90 秒版本
我遇到过一个 async scheduling 三态配置问题:显式 True 在某些 guard 里会被静默改成 False,而自动模式又比实际 capability 更保守。继续追源码后,我发现问题不只是配置。Async Scheduler 会提前为下一轮 token 和 KV 分配 placeholder,但 speculative decoding 的 accepted count 要到 GPU rejection sampling 后才知道,所以 computed frontier 必须回退。EAGLE 和 MTP 在 vLLM 里都走 EagleProposer 和 Target verify,差别主要是 Drafter 的来源与参数/状态复用,并不是一个碰主 KV、一个不碰。Trace 里即使 AsyncScheduler 已启用,也要验证 schedule 是否真的覆盖 GPU forward;一个 event wait 如果期间 GPU 仍在忙,不能把整个等待时长算成可回收收益。最后我的方案是把 requested/resolved config 分开、增加结构化 reason,再通过 GPU-side correction 或延迟 reconciliation 恢复重叠,并用相同 workload 做正确性和 TPOT/吞吐 A/B。
这段叙述同时展示了配置设计、Scheduler 状态机、KV 生命周期、源码追踪、Trace 归因和实验边界,比罗列优化名词更有说服力。
9. 读源码的最短路径
| 问题 | 入口 |
|---|---|
| 三态默认值是什么 | vllm/config/scheduler.py |
| 显式与自动模式如何解析 | vllm/config/vllm.py |
| placeholder 何时增加/兑现 | vllm/v1/core/sched/async_scheduler.py |
| rejected token 如何回退 | vllm/v1/core/sched/scheduler.py |
| MTP/EAGLE 如何选择 proposer | vllm/v1/worker/gpu_model_runner.py |
| hidden、embedding、LM head 如何进入 Drafter | vllm/v1/spec_decode/llm_base_proposer.py |
先沿状态写入者阅读,再看类名和注释。真正能串起系统的是 num_output_placeholders、num_computed_tokens、spec_token_ids、block/slot frontier 如何一起变化。
10. 证据边界
- 当前社区源码证明 EAGLE/MTP 已进入 async compatibility matrix,不证明任意模型/backend 组合都无 bug。
- Config resolved 为
True只证明选择了 async 路径,不证明 CPU/GPU 已获得有效重叠。 - Trace 中某个同步 span 很长,不等于整段时间都是 GPU idle 或可回收收益。
- MTP/EAGLE 的 accepted length、额外验证成本与容量影响需要真实 workload 指标。
- 生产优化案例公开时应移除服务身份、私有 commit、Trace 文件名和不可公开参数,但保留实验合同、观测与推理过程。
相关页面
- 投机解码基础 — Draft/Verify、接受前缀、分布等价与收益模型
- DSpark 与 MTP — Drafter 结构、置信度和硬件感知验证预算
- 批处理与调度 — Continuous Batching 与 Scheduler 的基础合同
- KV Cache — block、slot、逻辑序列与物理状态
- Kernel / Runtime 优化 — 同步、launch、CUDA Graph 与端到端占比
- Profiling → Simulation 证据链 — 事实、归因、模型和决策如何分层
← 被以下页面引用(4)
- DSpark 与 MTP:DeepSeek 投机解码调研ai-systems · synthesis
- LLM 推理系统全栈地图ai-systems · synthesis
- 批处理与调度:推理服务的灵魂ai-systems · concept
- 投机解码:突破 decode 一次只出一个 token 的限制ai-systems · concept
修改历史1 次提交
- docs: explain async speculative schedulingxiaocheng··
55e093e