DSpark 的实现和测评
DSpark 的实现和测评
DSpark = DFlash 的并行 backbone forward(1 次)+ N 步轻量 Markov 序列修正,全部在 CUDA Graph 内。 本文结合 vLLM 源码分析 DSpark 的实现细节,并在 Qwen3-8B 上实测 deepseek-ai 官方 draft 和社区 Dogacel draft 的效果差异。
1. 背景:投机解码与并行起草
投机解码(Speculative Decoding, SD)用一个小 draft 模型并行猜测 N 个 token,再由 target 模型一次 verify,通过 rejection sampling 保证输出分布不变。SD 的收益来自把 decode 阶段的 memory-bound 转为 compute-bound–bs=1 时 GPU 利用率极低,draft 的轻量 GEMM + target 的 batched verify 填充了 GPU 空闲。
vLLM v1 的 SD 框架支持多种 method:eagle、eagle3、dflash、dspark、medusa、ngram、mtp 等。DSpark 继承自 DFlash,核心改进是序列马尔可夫采样。
继承链:
1 | BaseSpeculator (ABC) |
模型类继承链:
1 | Qwen3ForCausalLM |
2. DSpark vs DFlash:两个核心差异
DSpark 的 docstring 写得非常清楚,和 DFlash 的差异只有两点。
2.1 Anchor-as-first-prediction(锚位即首预测)
DFlash:每个 request 发 1 + N 个 query token(1 个 anchor/bonus + N 个 mask token)。anchor 是上一步验证通过的 token,只有 N 个 mask 位置做预测:
1 | DFlash query layout (1+N=9, N=8): |
DSpark:anchor 本身也是预测位置,每个 request 只发 N 个 query token:
1 | DSpark query layout (N=8): |
代码(DSparkSpeculator.__init__):
1 | self.sample_from_anchor = getattr( |
在 Triton kernel _prepare_dflash_inputs_kernel 中,通过 SAMPLE_FROM_ANCHOR 编译常量控制采样行为:
1 | # DSpark: 所有 N 个位置都采样,sample_pos = query_pos + 1(标准 next-token) |
2.2 Sequential Markov Sampling(序列马尔可夫采样)
这是 DSpark 的核心创新。
DFlash:N 个 mask 位置的 hidden states 一次性并行采样,各位置之间无依赖。
DSpark:先并行 forward 得到所有 N 个位置的 hidden states,然后从左到右逐个采样,每步用前一个采样出的 token 注入一个 Markov bias:
1 | 并行 backbone forward → [h₀, h₁, h₂, ..., h₇] |
代码在 _sample_sequential:
1 | def _sample_sequential(self, num_reqs, head_hidden): |
一句话总结:并行 forward 拿到所有位置的 base prediction,再用 N 步轻量 Markov 修正注入序列依赖–把「N 个独立预测」变成「N 个有依赖的预测」。
3. Markov Head 结构
DSparkMarkovHead 是一个 low-rank 转移偏置头:
1 | prev_token_id |
代码(qwen3_dspark.py):
1 | class DSparkMarkovHead(nn.Module): |
两个权重都是 replicated(disable_tp=True),因为 Markov head 每步都跑,分片会引入 all-reduce 和 full-vocab gather。
参数量 = 。当 (Qwen3 词表)、 时约 19.4M 参数,相比 8B backbone 可以忽略。
4. 完整的 Draft 一步流程
DSparkSpeculator._generate_draft 只有两行:
1 | def _generate_draft(self, num_reqs, num_tokens_padded, ...): |
Step 1:并行 Backbone Forward(继承自 DFlash)
- 输入:N 个 query token(anchor + mask/noise),position 已对齐
- 上下文 KV 已在
precompute_and_store_context_kv中预填充 - 非因果 attention:N 个 query 位置可以互相 attend
- 整个 forward 被 CUDA Graph 捕获
Step 2:Sequential Markov Sampling(DSpark 独有)
- 取出 N 个位置的 hidden states
- 一次性算出 base logits(
compute_draft_logits) - 逐位置:
base_logits[i] + markov_bias(prev) -> gumbel_sample - 这个循环也被 CUDA Graph 捕获(所有 buffer 预分配固定地址)
Context KV 预计算(DFlash 的关键优化)
避免逐层跑 target 的 forward 来填充 draft KV cache,而是用 target 的中间层 hidden states 一次性投影:
1 | target aux hidden states [num_ctx, H_target] |
代码核心(DFlashQwen3Model.precompute_and_store_context_kv):
1 | # 融合所有层的 KV 权重做一次大 GEMM |
5. Probabilistic Rejection Sampling 与 Reduced Vocab
DSpark 支持 draft_sample_method="probabilistic"(Gumbel-based rejection sampling)。Draft 采样时把 logits 通过 Gumbel max trick 得到 draft_logits,Target verify 时用相同 Gumbel seed 验证,保证输出分布不变。
支持 reduced draft vocab:draft 在小词表上算 logits,然后 scatter 到 target vocab 位置:
1 | if self._d2t_scatter_index is not None: |
6. CUDA Graph 覆盖
DFlash/DSpark 的 CUDA Graph 是 FULL mode,覆盖整个 draft step:
1 | # DFlashSpeculator.init_cudagraph_manager |
为了让 Markov 循环能被 CG 捕获,所有 buffer 都是预分配的固定地址:
| Buffer | 用途 | CG 兼容性 |
|---|---|---|
draft_tokens |
输出 token | ✅ 固定地址 |
draft_logits |
probabilistic 模式的 processed logits | ✅ 固定地址 |
_draft_scatter_buf |
reduced vocab scatter buffer | ✅ 固定地址 |
_anchor_idx |
每个 request 的 anchor 位置索引 | ✅ 固定地址 |
input_buffers.input_ids |
anchor token 读取 | ✅ 固定地址 |
7. 模型加载与权重共享
load_dspark_model(dspark/utils.py)做了几件事:
- 创建 draft config,设置非因果注意力
- 加载 draft 模型
- Embed tokens 共享:如果 draft 没有自己的 embedding,用 target 的
- LM head 共享:同理
1 | if _should_share(draft_model, "has_own_embed_tokens", draft_embed, target_embed): |
权重加载(Qwen3DSparkForCausalLM.load_weights):
- 跳过
t2d(训练用映射,推理不需要) d2t->draft_id_to_target_id(推理用的 draft→target 映射)- 跳过
mask_embedding(DSpark 通过 vocab row 做 mask,不用单独参数)和confidence_head(未接入推理) - 调用
_build_fused_kv_buffers()构建 fused KV 权重
8. 实验环境
| 项目 | 配置 |
|---|---|
| Target Model | Qwen/Qwen3-8B |
| 推理引擎 | vLLM v0.26.0 |
| conda 环境 | dspark-vllm |
| nsys 版本 | 2026.1.3(vLLM traces)/ 2025.3.0(DeepSpec trace) |
| 采集参数 | -t cuda,nvtx,osrt,cudnn,cublas --python-backtrace=cuda --cudabacktrace=all |
| Benchmark | SPEED-Bench(qualitative split, coding category) |
Draft Model 配置
| 配置名 | Draft Model | 架构 | 来源 |
|---|---|---|---|
| Baseline | 无(纯 Qwen3-8B) | Qwen3 | - |
| DSpark(deepseek-ai) | deepseek-ai/dspark_qwen3_8b_block7 |
Qwen3DSparkSt | deepseek-ai 官方 |
| DSpark(Dogacel) | Dogacel/Qwen3-8B-DSpark |
EAGLE3 | 社区训练 |
Dogacel 的 vLLM 启动参数:speculative 开启、acceptance: 0.85、num_spec_tokens: 7、max_model_len: 2048。
9. 性能对比
9.1 端到端性能(3 prompts, 各 64 tokens)
| 配置 | 耗时 | vs Baseline | 加速比 |
|---|---|---|---|
| Baseline | 0.75s | - | 1.00x |
| DSpark(deepseek-ai) | 0.50s | -33% | 1.50x |
| DSpark(Dogacel) | 0.77s | +3% | 0.97x |
9.2 投机解码指标(DeepSpec evaluator trace)
| 指标 | DSpark(deepseek-ai) |
|---|---|
| verify_steps | 12 |
| mean_accept_len | 7.1 |
| 推测 | 每次提议 ~7 tokens,几乎全部被接受 |
Dogacel 的 trace 中未发现
dspark_propose/target_verify的 NVTX range,推测 acceptance rate 极低。
mean_accept_len=7.1 意味着 N=8 时几乎全部接受–backbone 的并行预测质量极高,Markov head 的序列修正有效。verify_steps=12 表示 12 步验证共接受约 85 个 token()。
10. Trace 分析
10.1 Trace 文件清单
| 文件 | 大小 | 来源 | CUDA Kernel 数据 |
|---|---|---|---|
| baseline_trace.nsys-rep | 1.5 MB | vLLM profile_baseline.py | ❌ 无 |
| dogacel_trace.nsys-rep | 1.8 MB | vLLM profile_dogacel.py | ❌ 无 |
| trace.nsys-rep(DeepSpec) | 3.5 MB | DeepSpec evaluator | ✅ 有 |
10.2 CUDA Kernel 缺失原因
vLLM 的 EngineCore 在子进程中运行,nsys 默认只 trace 主进程。三个 vLLM trace 均无 GPU kernel 数据。
解决方案:重新采集时添加 --trace-fork 参数:
1 | nsys profile -t cuda,nvtx,osrt,cudnn,cublas \ |
10.3 DeepSpec Evaluator Kernel 分布
| Kernel | 耗时占比 | Instances | 说明 |
|---|---|---|---|
| CUTLASS GEMM (16×16) | 66.3% | 7,177 | 主要 matmul(Q/K/V/O + MLP) |
| elementwise_kernel | 3.9% | 8,522 | RoPE、残差等 |
| reduce_kernel (mean) | 2.8% | 4,298 | RMSNorm |
| CUTLASS GEMM (32×32) | 2.7% | 382 | 大块矩阵乘法 |
| Flash Attention | 1.6% | 864 | Attention 计算 |
| Softmax forward | 1.0% | 168 | Softmax |
关键观察:GEMM 占 66.3%,但 bs=1 decode 时本质是 memory-bound(M=1 瘦矩阵乘)。kernel launch 开销显著(~20000 次 launch)。vLLM 的 CUDA Graph 会消除大部分 launch 开销,fused kernel 会压缩 elementwise/reduce 占比。预期 vLLM 路径下 GEMM 占比升至 80%+。
10.4 NVTX Range 对比
| NVTX Range | Baseline | Dogacel | deepseek-ai dspark |
|---|---|---|---|
| dspark_propose | ❌ | ❌ | ✅ |
| target_verify | ❌ | ❌ | ✅ |
| decode_sample | ❌ | ❌ | ✅ |
| warmup | ❌ | ❌ | ✅ |
| VLLM::EngineCore | ✅ | ✅ | ❌ |
Dogacel 缺少 dspark_propose/target_verify 说明其 draft forward 未走标准 dspark 代码路径。
11. Dogacel 无效原因:架构不匹配
| 维度 | deepseek-ai(有效) | Dogacel(无效) |
|---|---|---|
| Draft 架构 | Qwen3DSparkSt | EAGLE3 |
| 与 vLLM dspark 实现兼容 | ✅ 完全对齐 | ❌ 不匹配 |
| NVTX range 存在 | ✅ propose + verify | ❌ 无 |
| Mean accept len | 7.1 | 推测极低 |
| 端到端加速 | 1.50x | 0.97x(负优化) |
根因:Dogacel 用 EAGLE3 架构训练 draft,中间层 hidden state 接口与 dspark 实现不兼容。即使 draft 能加载运行,acceptance rate 极低,draft 开销 > SD 收益。即使模型本身学得不差,接口不对也白搭。
12. SPEED-Bench 数据集
12.1 整体结构
SPEED-Bench(SPEculative Evaluation Dataset)是 NVIDIA 出的投机解码评测基准。
| Split | 样本数 | 用途 |
|---|---|---|
| qualitative | 880(11 类×80) | 测 SD 质量(acceptance rate) |
| throughput_1k/2k/8k/16k/32k | 1536×5 | 测系统吞吐(高并发) |
12.2 Qualitative Split(质量评测)
从 18 个公开数据源聚合,分成 11 个 category:Coding、Math、Humanities、STEM、Writing、Summarization、Roleplay、RAG、Multilingual、Reasoning、QA。每类 80 个样本,用 OpenAI text-embedding-3-small 做嵌入,greedy 选择 + swap 优化最大化语义多样性(平均 pairwise cosine similarity 从 SpecBench 的 0.22 降到 0.14)。
12.3 Throughput Split(吞吐评测)
固定输入长度桶(1K/2K/8K/16K/32K),每桶 1536 条(512×3),分 3 个难度类别:low_entropy(coding 类)、high_entropy(creative writing 类)、mixed_entropy。用 tiktoken 精确 pad/truncate,不用 random token(会扭曲 MoE routing 和 acceptance behavior)。
12.4 为什么选 coding 类做 benchmark
- Coding 是低熵任务–token 可预测性高,SD 的 acceptance rate 天然高,是 best-case 场景
- 语义多样性好–80 条 prompt 覆盖 Python(27)、C++(9)、Java(10)、Go(13)、JS(11)、Rust(3) 等,来自 LiveCodeBench、Code Contests、HumanEvalPack
- 固定输出长度(
--speed-bench-output-len 2048)–隔离 prefill 影响,纯测 decode - 两种并发对比:
--max-concurrency 32(batched,模拟生产环境)vs--max-concurrency 1(单流,测纯 decode 延迟) --disable-shuffle保证可复现,--temperature 1.0高温采样更反映真实使用场景
13. 接受率与训练效果的关系
Acceptance rate 的天花板由 draft 训练质量决定,工程实现决定能打到多少天花板。
训练侧决定上限
- Draft 的 hidden state 和 target 的中间层对齐越好,token 分布越接近,accept 越高
- deepseek-ai 的 block7 专门按 dspark 接口训练,hidden state 严格对齐 Qwen3-8B 第 7 层,所以
mean_accept_len=7.1 - Dogacel 用 EAGLE3 方式训练,hidden state 映射方式不同,接口不对
工程侧决定下限
- vLLM dspark 的 propose → verify pipeline 是否正确对接 draft
- KV cache 的 layout、position ID 对齐、temperature sampling 一致性
- CUDA Graph 是否覆盖 draft forward(没覆盖的话 launch overhead 会吃掉 SD 收益)
| 维度 | deepseek-ai(训练+工程都对) | Dogacel(工程接口不对) |
|---|---|---|
| Draft 架构 | Qwen3DSparkSt | EAGLE3 |
| Hidden state 接口 | ✅ 正确对接 | ❌ 不匹配 |
| NVTX range | ✅ propose + verify | ❌ 无 |
| Mean accept len | 7.1 | 推测极低 |
| 端到端加速 | 1.50x | 0.97x |
一句话总结:训练决定 draft 能不能猜对,工程决定猜对的部分能不能高效用上。Dogacel 的情况是工程接口就不对,猜得再准也走不进去。
14. vLLM 推理引擎优化对 Kernel 分布的影响
无 vLLM 优化的 kernel 分布(DeepSpec evaluator)
| Kernel | 占比 | 说明 |
|---|---|---|
| CUTLASS GEMM (16×16) | 66.3% | bs=1 时是 memory-bound |
| elementwise | 3.9% | RoPE、残差等,未融合 |
| reduce (mean) | 2.8% | RMSNorm,未融合 |
| Flash Attention | 1.6% | decode 时计算量小 |
| Softmax | 1.0% | 未融合 |
| 总 kernel launch | ~20000 次 | launch 开销显著 |
vLLM 优化后的预期变化
- CUDA Graph:20000 次 kernel launch → 1 次 graph launch
- Fused kernel:RMSNorm + residual + RoPE 融合为 1 个 kernel
- FlashInfer/FlashAttention decode-optimized:attention kernel 更高效
- GEMM 占比升至 80%+:其他开销被压缩后,GEMM 成为绝对瓶颈
对投机解码的启示
Baseline 的 decode 在 vLLM 下 GEMM 占 80%+,本质是 memory-bound(M=1 瘦矩阵乘,GPU 利用率低)。SD 的价值在于用 draft 的轻量 GEMM + target 的 batched verify 填充 GPU 空闲。当 batch size 增大(高并发),decode 从 memory-bound 转向 compute-bound,SD 收益下降–这也是 SPEED-Bench throughput split 存在的意义。
15. 总结
- DSpark = DFlash 并行 backbone forward + N 步 Markov 序列修正,全部在 CUDA Graph 内,用极小的开销把并行预测的「无依赖」缺陷补上
- 实测 deepseek-ai 官方 draft 在 Qwen3-8B 上实现 1.50x 加速,
mean_accept_len=7.1(N=8 几乎全接受) - Dogacel 社区 draft 因架构不匹配(EAGLE3 vs DSpark)完全无效,0.97x 负优化
- 接受率天花板由训练决定,工程决定下限–hidden state 接口对齐是前提
- SPEED-Bench coding 类是 SD 的 best-case 场景,低熵任务下 acceptance rate 天然高
下一步
- 重新采集 vLLM trace(加
--trace-fork),获取真实 kernel 数据 - 检查 vLLM v0.26.0 是否支持 EAGLE3 method(Dogacel 应走 EAGLE3 而非 dspark)
- 用 throughput split +
--max-concurrency 32测高并发下 SD 效果 - 尝试 ngram、medusa 等 baseline 对比
参考:
- vLLM 源码:
vllm/v1/worker/gpu/spec_decode/dspark/speculator.py、vllm/model_executor/models/qwen3_dspark.py - vLLM PR:#50138、#50694、#50737
- 模型:
deepseek-ai/dspark_qwen3_8b_block7、Dogacel/Qwen3-8B-DSpark - 数据集:nvidia/SPEED-Bench,arXiv: 2604.09557