跳到正文
vLLM Blog· vLLM Team·· 2026-08-14精选AI 评分62

vLLM 上线 DSpark 自适应验证,投机解码在并发 256 下仍有效

Adaptive Verification in vLLM: DSpark confidence-scheduled verification

AI 导读

vLLM 在 PR #47808 中引入 DSpark 自适应验证(enable_adaptive_verification),由置信度头为每个草稿 token 打分,按步决定验证多少草稿,而非按部署固定投机长度。

推荐理由

vLLM 官方给出自适应验证的调度机制与实测曲线,读者可据此判断投机解码在高并发下是否值得默认开启。

正文 · AI 翻译

投机解码以更多计算换取更少的解码步数。在 batch size 为 1 时这是一笔划算的交易:GPU 受限于内存带宽而计算资源有余,因此额外的工作(草稿 token)几乎不花代价。在 batch size 为 256 时,这笔交易就微妙得多。草稿 token 现在要与真实 token 争夺同样的计算资源,而每一个被拒绝的 token 都会浪费有用的计算;一旦被拒绝的 token 足够多,吞吐量就会显著下降。

TL;DR:DSpark 的置信度头会为每个草稿 token 打分,评估其通过验证的概率,因此 vLLM 无需为每次部署选定一个投机长度,而是可以逐步决定要验证草稿中的多少部分。开启自适应验证后(num_speculative_tokens: 7),投机解码能够在并发度高达 256 时依然带来收益,同时在较低并发度下仍保持较长草稿长度带来的好处。这减少了用户根据自身工作负载和部署来调优 num_speculative_tokens 的需要,使 DSpark 成为一种更易于“默认开启”的收益。它已作为 enable_adaptive_verification 合入 PR #47808。

问题所在

逐位置的接受率衰减很快:在 DeepSeek-V4-Pro-0813 上,一个 7-token 块中最后一个草稿 token 的存活率不到 10%,而第一个则超过 70%。那个低概率 token 在每次验证批次中都要占用一个槽位。当 GPU 受限于内存带宽时,这个槽位实际上是免费的,值得一赌;一旦 GPU 饱和,这场“赌博”就会带来真实的吞吐量代价。挑战在于,这个临界点会随负载和依赖于工作负载的接受率而移动,因此没有哪个静态的 num_speculative_tokens 能在所有并发度下都最优。DSpark 通过采用自适应草稿预算来解决这一问题,该预算同时考虑系统的负载,以及 DSpark 头认为目标模型会接受每个草稿 token 的置信程度。

调度预算

DSpark 每次传递草拟一个包含 k 个 token 的块(num_speculative_tokens),并使用学习到的置信度头为每个位置输出一个置信度。调度器将这些转换为存活概率,即每个请求沿位置方向的连乘积:

存活率仅随位置 i 递减,因此在给定草稿 token 预算 B 的情况下,将其分配给最可能的草稿序列,就只是在存活分数上取全局 top-B;这允许每个请求的草稿取一个连续前缀,而无需额外约束。槽位在各请求之间竞争:一个高置信度请求的位置 5 可以排在低置信度请求的位置 1 之前。

Fixed-length verification versus confidence-scheduled trimming
固定长度验证与置信度调度裁剪的对比

图 1. 同一批次在两种策略下的情况。固定验证要为全部 21 个槽位付出代价,包括那些存活率近乎为零的槽位;而采用自适应验证时,我们只验证最优的 B=11 个。

B 来自最大化每单位步长时间的期望 token 数:

分子是每个采样请求的一个奖励 token,加上 B 个最佳草稿槽位的存活率;Nsampling 统计的是本步实际会进行采样的请求数,因此仍在处理分块预填充的请求不贡献任何值。分母是一张经性能分析得到的成本表,以该步的 token 数为索引:T 是已调度的非草稿 token,因此 T + B 就是整个步。两者都是数组,因此该选择就是在累积和上取一个 np.argmax,而成本以微秒计。

尺寸计算在 CPU 上运行,而 GPU 仍在处理上一步,基于一个滞后一步的双缓冲置信度数组。将这些 B 个槽位分配给各个请求则在 GPU 上针对当前值运行,因此每个请求的分配使用当前置信度。选择逻辑用 PyTorch 编写,由 torch.compile 降低到 Triton,并且从不回读到主机。

变长解码 CUDA 图

为了正确支持可变大小的验证,我们还需要变长解码 CUDA 图。这需要注意力内核支持:稀疏 MLA 内核天然就是变长的,因为每个查询 token 都有独立的 top-k,而 DeepSeek 在 DeepGEMM 中开源了一个变长索引器内核,它作为 PR #47808 的一部分被集成。解码图使用 num_reqs = min(num_tokens, max_num_seqs) 和一个承诺的 max_query_len = num_speculative_tokens + 1 来捕获,因此一个图可以服务每个请求 1 到 num_speculative_tokens + 1 个 token 的任意组合。

成本模型

预算规则除以步进成本,因此该成本必须易于查找,并且能很好地近似真实成本。启动时,引擎在一组固定形状(CUDA 图形形状加上几个超过最大 cudagraph 大小的形状)上对虚拟步骤计时,每个形状取五次运行的中位数。这形成两个扁平查找表:验证表按 token 数量索引,草稿表按请求数量索引,因为无论验证多少 token,草稿成本都相同。两者相加。

Measured verify and draft cost curves against the lookup tables
实测验证与草稿成本曲线与查找表的对比

图 2。来自真实启动配置的两个成本表,成本为 5 个样本的中位数。

在捕获的 CUDA 图内部,成本是阶梯状而非直线,因为存在 cudagraph 填充:一个 121 个 token 的批次会运行 128 个 token 的图,并且(大部分)支付全部 128 个 token 的成本。超过捕获限制后,阶梯结束,成本真正连续。在我们脱离 cudagraph 区域的地方有一个显著跳跃,并且该过渡在成本曲线中足够尖锐,强烈促使预算算法保持在 cudagraph 区域内。

通过强制曲线单调来处理分析噪声。由于内核 tile 大小,步进成本确实可能随批次增大而下降,因此强制单调性有助于平滑成本曲线。这些步骤针对合成 KV 上下文进行分析,默认 8192 个 token,可通过 VLLM_ADAPTIVE_VERIFICATION_PROFILE_CONTEXT_LEN 调整。

结果

DeepSeek-V4-Pro-0813,在 8×B300(SM100)上 TP=8,专家并行,FP8 KV 缓存,max_model_len 16384,max_cudagraph_capture_size 4096,在 vLLM main 上以 73b8394 运行。基准测试为 880 个提示,温度 1.0,最多 2048 个输出 token,并发度从 1 扫描到 256。

Aggregate throughput against interactivity for adaptive and fixed speculation lengths
自适应与固定推测长度下的聚合吞吐量与交互性对比

图 3。不同推测方案的吞吐量与交互性对比;自适应验证始终位于帕累托前沿。

自适应验证在整个扫描范围内都保持在帕累托曲线的边缘,并且在两端都远优于无推测。效果从图中很容易看出:它在低并发时表现得像长固定块,在高并发时表现得像短固定块,这样无需提前了解工作负载的形状就能兼得两者。

限制

  • FULL 变长解码图需要 AttentionCGSupport.ALWAYS,DSV4 稀疏 MLA、稀疏 SWA 和索引器后端在 SM100 上报告了这一点。在其他地方,自适应验证在启动时被拒绝,而不是回退到 PIECEWISE。
  • --enforce-eager(步进成本从捕获的图中分析得出)、LoRA 和流水线并行目前均不支持。
  • 当自适应验证开启时,输出 logprobs 会被拒绝,因为验证会在前向传播后压缩 logits。

附录:复现

以下所有命令均使用 PR #47808,现已合并到 vLLM main;上述数字是在 73b8394 处测得的。

服务器(所有测量;消融实验为 --speculative-config 增量):

vllm serve deepseek-ai/DeepSeek-V4-Pro-0813 \
  --tokenizer-mode deepseek_v4 --trust-remote-code \
  --tensor-parallel-size 8 --enable-expert-parallel \
  --kv-cache-dtype fp8 --max-model-len 16384 --max-num-seqs 256 \
  --max-num-batched-tokens 16384 --gpu-memory-utilization 0.8 \
  --compilation-config '{"max_cudagraph_capture_size":4096}' \
  --speculative-config '{"method":"dspark","attention_backend":"FLASH_ATTN","num_speculative_tokens":7,"draft_sample_method":"probabilistic","enable_adaptive_verification":true}'

草稿模型默认使用目标检查点,因此可以省略 "model"。--kv-cache-dtype fp8 是必需的:fp8_ds_mla 布局会拒绝其他 KV 数据类型。--max-num-seqs 也很重要——默认值为 128,这会将批大小限制在并发扫描上限以下。我们将 max_cudagraph_capture_size 增加到 (num_speculative_tokens + 1) * max_num_seq,以确保每个验证批次都在 cudagraph 内。更大的捕获大小需要更多内存用于 cudagraph,因此需要 --gpu-memory-utilization 0.8;在默认值下捕获时会 OOM。

  • 固定 k:"enable_adaptive_verification": false、"num_speculative_tokens": k,对于 k ≥ dspark_block_size(此检查点上为 5)
  • 无推测:省略 --speculative-config

吞吐量扫描,每个并发 c ∈ {1, 16, 32, 64, 128, 256},在一次预热后(--speed-bench-output-len 256 --num-prompts 64 --max-concurrency 32):

MODEL=deepseek-ai/DeepSeek-V4-Pro-0813
for c in 256 128 64 32 16 1; do
  n=880; [ "$c" = 1 ] && n=240
  vllm bench serve \
    --backend openai-chat --base-url http://127.0.0.1:8000 \
    --endpoint /v1/chat/completions --model "$MODEL" \
    --tokenizer "$MODEL" --tokenizer-mode deepseek_v4 \
    --dataset-name speed_bench --dataset-path <speed-bench-dir> \
    --speed-bench-dataset-subset qualitative --speed-bench-output-len 2048 \
    --num-prompts $n --max-concurrency $c --request-rate inf \
    --skip-chat-template --disable-shuffle --temperature 1.0 --seed 0 \
    --save-result --result-filename adaptive_on_c${c}.json
done

--disable-shuffle 加上固定提示集,使每个分支以相同顺序获得相同提示;结果 JSON 中的 output_throughput 是上面绘制的 tok/s。--speed-bench-output-len 是上限,不是目标——请求在 EOS 处停止,因此实际平均值远低于 2048。

致谢

这项工作由 Lucas Wilkinson(Red Hat)和 Benjamin Chislett(NVIDIA)完成。感谢 DSpark 的作者提供草稿算法和置信度头,并感谢 DeepSeek 提供 DeepSeek-V4 检查点。

来源:vLLM Blog · vllm.ai