vLLM Blog · 149 篇文章
📖 学习摘要 · 深度分析
DSpark 用置信度调度草稿验证预算,vLLM 一套配置从并发 1 到 256 守住吞吐/延迟前沿。

要点速览

  • 推测解码在批大小 1 时近乎免费,但在高并发下草稿 token 与真实 token 争抢算力,被拒绝的 token 直接浪费吞吐。
  • DSpark 的置信度头为每个草稿位置打分,调度器按存活概率(前缀连乘积)做全局 top-B,只验证最有价值的草稿前缀。
  • 预算 B 通过最大化单位步长时间的期望 token 数得到,成本查表在启动时剖析生成:验证表按 token 数、起草表按请求数索引。
  • 配合 varlen decode CUDA graphs,一张图即可服务每请求 1 至 num_speculative_tokens+1 的任意组合。
  • 在 DeepSeek-V4-Pro-0813、8×B300 上,自适应验证在全并发范围保持在 Pareto 前沿:低并发像长草稿、高并发像短草稿。

解决什么问题

推测解码用更多计算换更少解码步:批大小 1 时 GPU 受内存带宽限制、算力富余,草稿近乎免费;批大小 256 时草稿与真实 token 争抢同一份算力,每个被拒绝的 token 都浪费有效算力,吞吐显著下降。每位置接受率衰减很快(7-token 块末位存活率不足 10%,首位超过 70%),而最优交叉点随负载和负载相关的接受率移动,任何静态 num_speculative_tokens 都无法在所有并发下最优。

核心思路

DSpark 每趟起草 k 个 token 并用学习到的置信度头为每个位置输出置信度,调度器将其转成沿请求的连乘存活概率 S(r,i),该值随位置单调递减。给定草稿预算 B,把槽位分给最可能存活的序列等价于对存活分数做全局 top-B,天然形成每个请求的连续前缀,且槽位跨请求竞争:高置信请求的第 5 位可以胜过低置信请求的第 1 位。B 通过最大化『单位步长时间的期望 token 数』选出,分子是采样奖励 token 加 B 个最优槽位的存活和,分母是启动时剖析得到的起草与验证成本查表,整个选择退化为对累积和做 np.argmax。选 B 在 CPU 上用一步前的双缓冲置信度数组完成,此时 GPU 仍在执行上一步;把 B 个槽位分给各请求则在 GPU 上用当前置信度完成,选择逻辑由 torch.compile 编译为 Triton 且从不回读主机。最后配合 varlen decode CUDA graphs,让变长的验证批次也能进入捕获图执行。

关键要点

  1. 存活概率与全局 top-B 槽位分配 — DSpark 为草稿块每个位置输出置信度,调度器将其转成沿请求的连乘存活概率 S(r,i),随位置单调递减。给定预算 B,把槽位分给最可能存活的草稿序列就是对存活分数做全局 top-B,天然取每个请求的连续前缀。槽位跨请求竞争:高置信请求的第 5 位可以胜过低置信请求的第 1 位。
  2. 用期望吞吐最大化选预算 B — B* 最大化『单位步长时间期望 token 数』:分子是每个采样请求的奖励 token 加 B 个最优槽位的存活和,分母是按 token 数索引的验证成本加上按请求数索引的起草成本。两者都是数组,选择退化为对累积和做 np.argmax,成本单位是微秒。仍在 chunked prefill 中的请求不计入分子。
  3. CPU 选规模、GPU 分槽位的异步流水 — B 的计算在 CPU 上进行,用的是一步前的双缓冲置信度数组,此时 GPU 仍在执行上一步。把 B 个槽位分给各请求则在 GPU 上用当前置信度完成;该选择用 PyTorch 编写、经 torch.compile 编译为 Triton,从不回读主机,避免了同步开销。
  4. varlen decode CUDA graphs 支撑变长验证 — 变长验证批次要进 CUDA 图需要注意力内核支持变长:稀疏 MLA 内核天然 varlen(每个 query token 有独立 top-k),DeepSeek 在 DeepGEMM 中开源的 varlen indexer kernel 也随 PR #47808 集成。图以 num_reqs = min(num_tokens, max_num_seqs) 捕获并承诺 max_query_len = num_speculative_tokens + 1,一张图即可服务每请求 1 到 num_speculative_tokens+1 的任意组合。
  5. 启动期成本剖析与两张查表 — 引擎启动时对固定形状集合(CUDA 图形状加几个超出上限的形状)计时 dummy step,每形状 5 次取中位数,得到验证表(按 token 数)与起草表(按请求数)两张平面查表,相加即步成本。图内成本因 cudagraph padding 呈阶梯状(121 token 跑 128-token 图),超出捕获范围后变连续;离开图区的跳变足够尖锐,会强烈促使预算算法留在 cudagraph 区域内。剖析噪声通过强制曲线单调来平滑,剖析用默认 8192 token 的合成 KV 上下文。
  6. 一套配置守住全并发 Pareto 前沿 — 在 DeepSeek-V4-Pro-0813、8×B300、FP8 KV cache 上以 880 条 prompt、并发 1–256 扫描,自适应验证整段保持在 Pareto 曲线边缘,两端都明显优于不投机。效果可读作:低并发时像长固定块、高并发时像短固定块,无需预先了解负载形态即可兼得两者,也让 DSpark 更容易成为『默认开启』型收益。
  7. 限制与兼容性 — FULL varlen 解码图要求 AttentionCGSupport.ALWAYS,目前 DSV4 sparse-MLA、sparse-SWA 和 indexer 后端在 SM100 上报告该支持,其他环境会在启动时直接拒绝而非回退 PIECEWISE。--enforce-eager(因为成本来自捕获图)、LoRA 和流水线并行均不支持;开启自适应验证时输出 logprobs 会被拒绝,因为验证在前向之后压缩 logits。

关键数据

指标数值条件
草稿末位存活率小于 10%DeepSeek-V4-Pro-0813,7-token 草稿块的最后一位
草稿首位存活率大于 70%同一 7-token 草稿块的第一位
受益并发上限256(num_speculative_tokens=7)开启自适应验证后推测解码仍带来收益,且低并发保留长草稿收益
验证槽位节省(图 1 示例)固定验证 21 个槽位 → 自适应只验证 B=11同一批次在两种策略下的对比
评测硬件与配置TP=8,8×B300 (SM100),FP8 KV cache,max_model_len 16384DeepSeek-V4-Pro-0813,expert parallel,vLLM main @ 73b8394
评测负载880 条 prompt,温度 1.0,最多 2048 输出 token并发 1 到 256 扫描
成本剖析采样次数每个形状 5 次取中位数启动时对 dummy step 计时构建成本查表
成本剖析上下文长度默认 8192 token(可用 VLLM_ADAPTIVE_VERIFICATION_PROFILE_CONTEXT_LEN 调整)成本剖析使用的合成 KV 上下文
cudagraph 填充示例121 token 批次执行 128-token 图图捕获范围内成本呈阶梯状的原因
dspark_block_size5该 checkpoint 上做固定 k 消融时需 k ≥ 5
单图覆盖的每请求 token 数1 到 num_speculative_tokens + 1varlen decode CUDA graph 捕获方式
“the last drafted token of a 7-token block survives less than 10% of the time, against more than 70% for the first”
“the last drafted token of a 7-token block survives less than 10% of the time, against more than 70% for the first”
“With adaptive verification on (`num_speculative_tokens: 7`), speculative decoding is able to provide benefits all the way to concurrency 256 and still maintains the benefits of the longer draft length at lower concurrencies.”

前置知识

推测解码(speculative decoding)基本机制:草稿模型起草、目标模型一次前向并行验证 · vLLM 的 num_speculative_tokens 参数与连续批调度(continuous batching) · CUDA graph 捕获与 cudagraph padding 对步长成本的影响 · DSpark 草稿算法及其置信度头(confidence head)的作用 · DeepSeek 稀疏 MLA/NSA 注意力与 DeepGEMM 的 varlen indexer kernel · 吞吐-交互性(延迟)Pareto 前沿的读图方式

术语表

  • Speculative decoding(推测解码): 用轻量草稿模型先行生成若干 token,再由目标模型一次前向并行验证,以减少解码步数、换取更低延迟。
  • Confidence head(置信度头): DSpark 中一个学习出的输出头,为每个草稿 token 估计其通过目标模型验证的概率。
  • Survival probability(存活概率): 沿请求内位置对置信度做连乘积 S(r,i)=∏_{j≤i} confidence(r,j),表示草稿前缀整体被接受的概率,随位置单调递减。
  • Adaptive verification(自适应验证): 按置信度与系统负载动态决定每步验证多少草稿 token 的机制,vLLM 中通过 enable_adaptive_verification 开启。
  • Varlen decode CUDA graphs(变长解码 CUDA 图): 支持每请求 token 数不同的验证批次共享同一张捕获图的机制,需要注意力内核本身支持变长。
  • Cudagraph padding(CUDA 图填充): 批次大小按捕获形状向上取整:121 token 的批次会执行 128-token 的图并基本支付全部 128 的成本,使图内成本呈阶梯状。
  • Chunked prefill(分块预填充): 长 prompt 的预填充被拆成多个 chunk 逐步处理;仍在处理 chunk 的请求在当步不参与采样,不计入预算公式的分子。
  • Pareto frontier(帕累托前沿): 吞吐与交互性权衡图上不被其他方案同时压制的曲线;文中指自适应验证在各并发下都贴近该曲线。
📍 学习路径定位:本文是『推测解码』路径中自适应验证主题的第一篇,讲 DSpark 如何用置信度头按存活概率调度验证预算;下一篇是无 confidence head 的在线估计器方案,建议对比阅读以理解两类自适应验证的取舍。
2026-08-147 min read 进阶 官方原文 ↗

Adaptive Verification in vLLM: DSpark confidence-scheduled verification

Speculative decoding buys fewer decode steps with more compute. At batch size 1 that is a good trade: the GPU is memory-bound with spare compute, so the extra work (draft tokens) is close to free. At batch size 256 the trade is much more delicate. Draft tokens now compete with real tokens for the same compute, and every rejected token wastes useful compute; with enough rejected tokens, throughput drops significantly.

TL;DR: DSpark's confidence head scores each drafted token's chance of surviving verification, so instead of picking a speculation length per deployment, vLLM can decide per step how much of the draft to verify. With adaptive verification on (num_speculative_tokens: 7), speculative decoding is able to provide benefits all the way to concurrency 256 and still maintains the benefits of the longer draft length at lower concurrencies. This reduces the need for users to tune num_speculative_tokens to their workload and deployment, and makes DSpark an easier "on-by-default" type of win. It landed in PR #47808 as enable_adaptive_verification.

The problem

Per-position acceptance decays fast: on DeepSeek-V4-Pro-0813 the last drafted token of a 7-token block survives less than 10% of the time, against more than 70% for the first. That low probability token costs a slot in every verification batch. While the GPU is memory-bound the slot is effectively free and worth the gamble; once it saturates the "gamble" has a real throughput cost. The challenge is that the crossover moves with load and workload dependent acceptance rates, so no static num_speculative_tokens is optimal across concurrencies. DSpark tackles this by having an adaptive draft budget that takes into account both the load of the system and how confident the DSpark head thinks the target model will accept each draft token.

Scheduling the budget

DSpark drafts a block of k tokens per pass (num_speculative_tokens) and emits a confidence per position using a learned confidence head. The scheduler turns those into survival probabilities, the running product along each request:

$$ S(r, i) = \prod_{j \le i} \mathrm{confidence}(r, j) $$

Survival only decreases with position i, so given a draft token budget of B, allocating it to the most probable draft sequences is just a global top-B over survival scores; that admits a contiguous prefix of each request's draft with no extra constraint. Slots compete across requests: position 5 of a confident request can outrank position 1 of a low-confidence one.

Fixed-length verification versus confidence-scheduled trimming

Figure 1. The same batch under both policies. Fixed verification pays for all 21 slots including the ones with near-zero survival; with adaptive verification we only verify the best B=11.

B comes from maximizing expected tokens per unit of step time:

$$ B^* = \arg\max_B \frac{N_\mathrm{sampling} + \sum_{j < B} S_\mathrm{sorted}[j]}{\mathrm{draft_cost}[\mathrm{num_reqs}] + \mathrm{verify_cost}[T + B]} $$

The numerator is one bonus token per sampling request plus the survival of the B best draft slots; Nsampling counts the requests that will actually sample this step, so a request still working through a chunked prefill contributes nothing. The denominator is a profiled cost table, indexed by the step's token count: T is the tokens already scheduled that are not drafts, so T + B is the whole step. Both are arrays, so the choice is an np.argmax over a cumulative sum and costs are in microseconds.

Sizing runs on the CPU while the GPU is still working on the previous step, from a double-buffered confidence array that is one step old. Handing those B slots out to individual requests runs on the GPU against current values, so the per-request allocation uses current confidences. The selection is written in PyTorch, lowered to Triton by torch.compile, and never reads back to the host.

Varlen decode CUDA graphs

To properly support variable-sized verifications we also need varlen decode CUDA graphs. That requires attention kernel support: the sparse MLA kernels are naturally varlen, since each query token has an independent top-k, and DeepSeek open-sourced a varlen indexer kernel in DeepGEMM, which is integrated as part of PR #47808. Decode graphs are captured with num_reqs = min(num_tokens, max_num_seqs) and a promised max_query_len = num_speculative_tokens + 1, so one graph serves any mix of 1 to num_speculative_tokens + 1 tokens per request.

The cost model

The budget rule divides by a step cost, so that cost has to be cheap to look up and a good approximation of the real cost. At startup the engine times dummy steps across a fixed set of shapes (CUDA graph shapes plus a couple above the max cudagraph size), taking the median of five runs per shape. That becomes two flat lookup tables: the verification table is indexed by token count, and the drafter table by request count, since drafting costs the same regardless of how many tokens are verified. The two are summed.

Measured verify and draft cost curves against the lookup tables

Figure 2. Both cost tables from a real startup profile, with the cost being the median of 5 samples.

Inside the captured CUDA graphs cost is a staircase rather than a line, because of cudagraph padding: a batch of 121 tokens runs the 128-token graph and (mostly) pays for all 128. Past the capture limit the staircase ends and cost really is continuous. There is a notable jump where we fall out of the cudagraph region, and that transition is sharp enough in the cost curve to strongly encourage the budget algorithm to stay within the cudagraph region.

Profiling noise is handled by forcing the curve monotonic. Step cost can genuinely fall as the batch grows, because of kernel tile sizes, so enforcing monotonicity helps smooth out the cost curve. The steps are profiled against a synthetic KV context, 8192 tokens by default and tunable with VLLM_ADAPTIVE_VERIFICATION_PROFILE_CONTEXT_LEN.

Results

DeepSeek-V4-Pro-0813, TP=8 on 8×B300 (SM100), expert parallel, FP8 KV cache, max_model_len 16384, max_cudagraph_capture_size 4096, on vLLM main at 73b8394. The benchmark is 880 prompts at temperature 1.0, up to 2048 output tokens, swept over concurrency 1 to 256.

Aggregate throughput against interactivity for adaptive and fixed speculation lengths

Figure 3. Throughput versus interactivity for different speculation schemes; adaptive verification stays on the Pareto frontier throughout.

Adaptive verification stays on the edge of the Pareto curve for the whole sweep, and well outside no speculation at both ends. The effect is easy to read off the graph: it behaves like a long fixed block at low concurrency and a short one at high concurrency, which gives you both without having to know the shape of your workload in advance.

Limitations

  • FULL varlen decode graphs require AttentionCGSupport.ALWAYS, which the DSV4 sparse-MLA, sparse-SWA, and indexer backends report on SM100. Elsewhere adaptive verification is rejected at startup rather than falling back to PIECEWISE.
  • --enforce-eager (step costs are profiled from captured graphs), LoRA, and pipeline parallelism are all not supported currently.
  • Output logprobs are rejected when adaptive verification is on, because verification compacts logits after the forward pass.

Appendix: reproducing

All the commands below are using PR #47808, now merged into vLLM main; the numbers above were measured at 73b8394.

Server (all measurements; ablations are --speculative-config deltas):

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}'

The draft defaults to the target checkpoint, so "model" can be omitted. --kv-cache-dtype fp8 is required: the fp8_ds_mla layout rejects other KV dtypes. --max-num-seqs matters too — the default is 128, which would cap the batch below the top of the concurrency sweep. We increase the max_cudagraph_capture_size to (num_speculative_tokens + 1) * max_num_seq to ensure every verfication batch is inside a cudagraph. The larger capture size needs more memory for cudagraphs hence --gpu-memory-utilization 0.8; at the default it OOMs while capturing.

  • fixed k: "enable_adaptive_verification": false, "num_speculative_tokens": k, for k ≥ dspark_block_size (5 on this checkpoint)
  • no speculation: omit --speculative-config

Throughput sweep, per concurrency c ∈ {1, 16, 32, 64, 128, 256}, after one warmup pass (--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 plus the fixed prompt set gives every arm identical prompts in identical order; output_throughput from the result JSON is the tok/s plotted above. --speed-bench-output-len is a cap, not a target — requests stop at EOS, so the realized average is well under 2048.

Acknowledgments

This work was done by Lucas Wilkinson (Red Hat) and Benjamin Chislett (NVIDIA). Thanks to the DSpark authors for the drafting algorithm and the confidence head, and to DeepSeek for the DeepSeek-V4 checkpoints.