首页
/ vLLM 动态投机解码实战:按运行期批大小自动调整 Draft Token 深度 K

vLLM 动态投机解码实战:按运行期批大小自动调整 Draft Token 深度 K

2026-09-06 15:54:18作者:苗圣禹Peter

投机解码(Speculative Decoding, SD)以固定的 draft token 数 K 换取解码加速,但 K 并非越大越好。vLLM 的动态投机解码(Dynamic Speculative Decoding, DSD)允许用户按"并发区间"配置 K 值:调度器在每个调度步骤根据当前实际 batch size 从预生成的查表数组中取出该步应使用的 K,从而在高并发时自动收缩甚至关闭投机,在低并发时放大投机收益。读完本文,你将掌握 num_speculative_tokens_per_batch_size 的完整语义与校验规则、从配置到每步 K 查取的源码链路,以及该功能在 CUDA Graph、数据并行等场景下的限制与自动回退行为。

一、为什么需要动态投机解码

SD 方法在解码阶段需要为每个序列验证 K 个 draft token。随着 batch size(BS)增大,验证阶段的有效批大小变为 BS × K,验证所需的计算量随之线性增长。当 BS × K 越过某个临界 BS 后,投机解码非但不再加速,反而会拖慢解码速度(TPOT 变差)。DSD 的作用就是把 K 动态调到一个随并发变化的最优值,让服务持续吃到投机解码的收益而不被其反噬。

(引自 dynamic_speculative_decoding.md 的 "Why is Dynamic SD needed?" 一节)

二、典型适用场景

文档给出了两类典型用例:

  • 变并发工作负载:同一部署承载波动并发的流量。并发升高时 K 自动降低,并发回落时 K 恢复,无需重启或改配置;
  • RL rollout 长尾阶段:RL 训练 rollout 初期 batch size 很高,但末期往往只剩少数长尾请求还在生成大量 token,此时 BS 已经变小。DSD 可以在 rollout 末期自动把 K 调大,减少长尾等待。

这与 Speculative Decoding 总览 中对 Dynamic Speculative Decoding 的定位一致:"Useful for RL or workload with fluctuating QPS"。

三、--speculative-config 中的 DSD 配置项

要启用动态 SD,只需在某个 SD 方法的 --speculative-config JSON 中额外提供 num_speculative_tokens_per_batch_size 字段(列表的列表)。每个条目是一个三元组 [start_bs, end_bs, optimal_K]:当运行期并发落在闭区间 [start_bs, end_bs] 内时,draft token 数使用 optimal_K

文档给出的标准示例:

--speculative-config '{
    "method": "eagle",
    "model": "yuhuili/EAGLE-LLaMA3.1-Instruct-8B",
    "num_speculative_tokens": 3,
    "num_speculative_tokens_per_batch_size": [
      [1, 64, 3],
      [65, 128, 1],
      [129, 512, 0]
    ]
  }'

含义逐条拆解:

并发区间 使用的 K 行为
[1, 64] K=3 全量投机
[65, 128] K=1 只投 1 个 draft token
[129, 512] K=0 不产生任何 draft token(等价于关闭投机,但仍走验证路径)

注意两点:

  1. 静态的 num_speculative_tokens 仍然需要填写(示例中为 3),它既是各区间 K 的上限,也是后续 DP 回退时的静态值;
  2. 该配置项在 SpeculativeConfig 中定义为 list[tuple[int, int, int]] | None,只要该字段非 None,vLLM 就判定处于动态投机模式(见 speculative.pyuses_dynamic_speculative_decoding()):
# required configuration params passed from engine
num_speculative_tokens_per_batch_size: list[tuple[int, int, int]] | None = None
"""Batch-size schedule used to dynamically choose speculative-token count.

Each entry is ``(range_start, range_end, num_speculative_tokens)`` with an
inclusive batch-size range.
"""

四、调度表的校验与展开:从区间列表到稠密查表数组

DSD 的实现核心在 vllm/v1/spec_decode/dynamic/utils.py,由两个函数构成。

4.1 校验与归一化:validate_and_normalize_dynamic_sd_schedule

启动阶段会对用户传入的 schedule 做严格校验(utils.py):

  • 字段必须是非空 list,每个条目必须是 3 元素序列;
  • range_startrange_end 必须为正整数,且 start <= end
  • K 值必须 >= 0(K=0 是合法语义:该区间不投机);
  • 各区间按 range_start 排序后必须互不重叠
  • 第一个区间的 range_start 必须为 1——保证任意运行期批大小都有定义好的 K。

这些约束在文档中未逐条列出,但从源码可见:如果并发区间覆盖不完整(例如从 5 开始),启动会直接报错,而不是静默取默认值。

4.2 展开为稠密查表数组:build_dynamic_sd_schedule_lookup

校验通过后,schedule 会被展开成一张 1 起始索引的稠密数组 dense_scheduleutils.py),索引 batch_size 处直接存放该批大小应使用的 K,使调度器每步只需一次数组下标访问,而无需对区间列表做查找。展开逻辑有三个值得注意的细节:

  1. K 会被静态上限裁剪:每个位置实际写入的是 min(vllm_num_speculative_tokens, num_speculative_tokens),即区间内配置的 K 不可能超过 num_speculative_tokens
  2. 区间空洞向前填充:若配置了 [(1, 16, 3), (32, 128, 2)],则 17–31 的空洞区会沿用上一个区间的 K=3 填充;
  3. 末尾延伸到 vllm_max_batch_size:最后一个配置区间之后的所有批大小,沿用最后一个 K 值填充到系统最大批大小为止。因此文档示例中 [129, 512, 0] 之后的更高并发同样是 K=0。

vllm_max_batch_size 由调度器传入,取值为 scheduler_config.max_num_seqs(见 scheduler.py)。

五、调度器如何按步选 K

Scheduler 初始化时,若检测到 num_speculative_tokens_per_batch_size 配置,就会调用上述函数构建 self.dynamic_sd_lookupscheduler.py):

self.dynamic_sd_lookup: list[int] | None = None
if speculative_config is not None:
    if speculative_config.num_speculative_tokens_per_batch_size:
        self.dynamic_sd_lookup = build_dynamic_sd_schedule_lookup(
            speculative_config.num_speculative_tokens_per_batch_size,
            vllm_max_batch_size=self.scheduler_config.max_num_seqs,
            vllm_num_speculative_tokens=self.num_spec_tokens,
        )

在每次调度步骤中,当本轮有请求被调度时,调度器用当前实际批大小作为索引取 K(scheduler.py):

if self.dynamic_sd_lookup is not None and len(num_scheduled_tokens) > 0:
    num_spec_tokens_to_schedule = self.dynamic_sd_lookup[...]

也就是说,K 是随每个调度步骤的运行期批大小动态变化的:并发下降(比如 rollout 长尾只剩几个请求)后,同一批配置会立即切到更大的 K,无需任何人工干预。相应地,当 dynamic_sd_lookup 存在时,调度约束也切换为动态模式——源码中可见只有 self.num_spec_tokens > 0 and self.dynamic_sd_lookup is None(静态 K)或动态 K 两种模式二选一(scheduler.py)。

六、CUDA Graph 支持情况

DSD 下每步的 K 不同,意味着验证阶段的 token 数不再是固定值,这直接影响 CUDA Graph 的捕获方式。文档的 Limitations 一节明确:

  • Full Cudagraph 仅在 Model Runner V2 下可用
  • MRv1 仅支持 piece-wise CUDA Graph

在源码中可以看到两处对应实现:

  1. cudagraph_utils.py 在检测到 uses_dynamic_speculative_decoding() 时,会基于同一份 schedule 构建稠密查表,用于按批大小确定各捕获批次对应的 K;
  2. vllm/config/vllm.py 中的 _maybe_override_dynamic_sd_cudagraph_mode() 会在动态 SD 启用时覆写 cudagraph 模式(对不满足 full graph 条件的场景降级为 piece-wise)。

这也是文档"Online Examples"里显式带上 VLLM_USE_V2_MODEL_RUNNER=0 的原因:示例运行在 Model Runner V1 上,此时动态 K 走的是 piece-wise CUDA Graph 路径。

七、与数据并行的不兼容及自动回退

DSD 不兼容数据并行(--data-parallel-size > 1。原因:各 DP rank 独立调度,运行期批大小可能不同,从而选出不同的 K,这会引发 DP 集合通信(collective)分歧与死锁。

vLLM 对此采取了自动防御而非报错:在配置解析阶段,_maybe_disable_dynamic_sd_for_data_parallel()vllm/config/vllm.py)检测到 data_parallel_size > 1 且启用了动态 SD 时,会打印日志

"Disabling num_speculative_tokens_per_batch_size and falling back ..."

并将 speculative_config.num_speculative_tokens_per_batch_size 置为 None,回退到静态 num_speculative_tokens 值。也就是说在 DP 部署中配置 DSD 不会崩溃,但动态特性会被静默关闭,建议留意启动日志确认是否真的生效。

八、完整上线示例

文档提供的两个可直接参考的 vllm serve 示例(完整继承原文档):

8.1 Dynamic SD + EAGLE drafter

VLLM_USE_V2_MODEL_RUNNER=0 vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --speculative-config '{
    "method": "eagle",
    "model": "yuhuili/EAGLE-LLaMA3.1-Instruct-8B",
    "num_speculative_tokens": 3,
    "num_speculative_tokens_per_batch_size": [
      [1, 64, 3],
      [65, 128, 1],
      [129, 512, 0]
    ]
  }'

8.2 Dynamic SD + EAGLE3 drafter

VLLM_USE_V2_MODEL_RUNNER=0 vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --speculative-config '{
    "method": "eagle3",
    "model": "yuhuili/EAGLE3-LLaMA3.1-Instruct-8B",
    "num_speculative_tokens": 3,
    "num_speculative_tokens_per_batch_size": [
      [1, 16, 5],
      [17, 32, 4],
      [33, 64, 3],
      [65, 128, 1],
      [129, 512, 0]
    ]
  }'

两个示例的梯度思路一致:低并发段给足 K,中段收缩,高并发段归零。EAGLE3 示例还展示了细粒度多档写法(5/4/3/1/0),说明 schedule 的档位数量没有硬性限制,只受第四节的校验规则约束。

九、限制与注意事项汇总

  1. 方法兼容性:官方仅验证过 Eagle、Eagle-3 和 DFlash;其他 SD 方法(如 draft_model、ngram)是否开箱可用不作保证;
  2. CUDA Graph:Full Cudagraph 仅 Model Runner V2 支持;MRv1 下仅 piece-wise CUDA Graph;
  3. 数据并行--data-parallel-size > 1 时功能自动禁用并回退静态 K(见第七节),不会产生正确性问题但也不会产生动态收益;
  4. 配置合法性:区间必须从 1 开始、互不重叠、K ≥ 0,且区间内 K 会被 num_speculative_tokens 裁剪——配置时建议让静态值 ≥ 所有区间的 K 值,避免被静默截断。

十、相关资源

登录后查看全文
热门项目推荐
相关项目推荐