跳转到主要内容

vLLM Async Scheduling:三态配置、投机解码与状态提交

沿 vLLM 源码解释 async scheduling 的三态配置、output placeholder、EAGLE/MTP 状态归属,以及 accepted-count 同步如何影响真实重叠

· 约 10 分钟阅读

Async scheduling 和 speculative decoding 都在“提前”:前者让 CPU 在 GPU 返回前排下一轮,后者让 Drafter 在 Target 确认前猜未来 token。两者组合后,系统必须同时回答:哪些 token 已确认,哪些只是占位,哪些 KV 已经计算但还不能成为可复用历史?

这篇沿 vLLM 源码回答三个具体问题:

  1. async_scheduling=False / True / None 应该是什么契约;
  2. EAGLE 与 MTP 的 hidden、embedding、LM head、draft KV 和 target KV 到底是什么关系;
  3. 为什么 async 已经开启,Trace 里仍可能看不到 scheduler 与 GPU forward 重叠。
30 秒复习
  • 一句话: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_tokensScheduler 认为已经处理到的逻辑前沿,可能包含在途工作
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”不是判断主路径是否干净的标准。更有效的问题是:

  1. 这段状态属于 target verifier 还是 drafter?
  2. Scheduler 把哪一个位置当成 confirmed frontier?
  3. 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 从哪里来、复用什么:

维度EAGLEMTP
训练产物通常是与 Target 匹配的独立 EAGLE checkpoint/head通常是随 Target checkpoint 训练、交付的额外 MTP module/layer
Hidden 输入读取 Target feature;EAGLE3 可组合多层 aux hidden读取 Target hidden;某些架构复用 pre-final mixer 或多流 hidden
Embeddingcheckpoint 可自带;缺失或完全相同时可共享 Target embeddingvLLM 默认共享 Target embedding
LM headcheckpoint 可自带;缺失或相同时可共享vLLM 默认共享 Target LM head
Draft 依赖轻量模型/head 自回归或树状生成多个 MTP step 可连续调用,后一步依赖前一步候选与 hidden
Draft cacheDraft attention layer 有自己的 KV/stateMTP 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 写错了”开始。更有区分度的结构是:

  1. 契约:一个三态性能配置把显式意图与自动策略混在同一可变字段里,造成 silent fallback。
  2. 机制:Async Scheduler 用 placeholder 表示在途 token;spec decode 又让 accepted count 到 GPU 返回后才确定。
  3. 源码纠偏:沿 Config → AsyncScheduler → Scheduler update → GPUModelRunner → EagleProposer 追踪后,发现 EAGLE/MTP 共用 proposer/verify/accounting 主链,推翻了“一个旁路、一个污染主 KV”的简单解释。
  4. Trace 证据:async 类名出现不等于流水有效;要检查 schedule 是否真的覆盖 GPU forward,以及 accepted-count sync 的暴露部分。
  5. 决策:先修配置契约和最终状态回读,再把优化目标定为 GPU-side correction / 延迟 reconciliation,并用固定 Case 做 A/B。
  6. 边界:源码证明机制,单条 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 如何选择 proposervllm/v1/worker/gpu_model_runner.py
hidden、embedding、LM head 如何进入 Draftervllm/v1/spec_decode/llm_base_proposer.py

先沿状态写入者阅读,再看类名和注释。真正能串起系统的是 num_output_placeholdersnum_computed_tokensspec_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 文件名和不可公开参数,但保留实验合同、观测与推理过程。

相关页面

修改历史1 次提交