DFlash 到 DSpark in SGLang:解码提速 4.3×,以及 DeepSeek-V4 支持

DFlash 和 DSpark 是两个新一代的投机解码算法。这篇 blog 详细讲解这两个算法的工作原理。两者都放弃了传统的自回归起草,draft 模型改用 DiT 架构。SGLang 现已完整支持这两个算法:用 DFlash,解码达到非投机基线的 4.3×、MTP 的 1.5×;用 DSpark,混合流量上验证窗口从 5.24 收缩到 2.91 个 token,保持接受上限的 0.88–0.97,DeepSeek-V4-Pro 上 batch 1 解码达到 383.7 tok/s。

Background

先讲投机解码的基本思路。解码是 memory-bound 的:target 模型的一次 forward 大部分时间在搬权重,所以小批次下验证好几个 token 的开销和生成一个差不多。于是让一个小 drafter 先写出 K 个 token,target 用一次 forward 全部检查一遍。通过的前缀被提交,verify forward 还顺带产出一个 bonus token。被拒的草稿只浪费 drafter 的便宜算力:

投机解码图示时间轴 = target forward 次数;块长 6,每块接受数为脚本化场景
非投机:1 次 forward = 1 个 tokentarget forwards 0 · 已生成 0
08162432
块起草 + 块验证target forwards 0 · 已生成 0 · 平均接受 3.0
草稿(小模型)已提交08162432
t = 0/32

这笔交易随负载变亏。batch 为 B、每请求 K 个投机 token 时,target 每步要验证 B×K 个 token。小批次下,这些多出来的 token 几乎搭权重搬运的顺风车。批次填满 GPU 后,每个 verify token 都开始消耗真实的步时,过了某个点,验证的开销就超过了它省下的。下图给同样三种选择在不同负载下算账:

verify 成本图示T(M) = 20 ms + 0.19 ms/token,形状读自原文 Figure 6(a),示意口径;接受数取脚本值:整块 3.0,窗口 3 时 2.5
040080012001600100200300M = 每步 verify 的总 token 数步时 T (ms)非投机整块 verify(窗口 6)裁剪 verify(窗口 3)

本文余下的部分分三块:解这笔交易的两代算法,SGLang 为 serve 它们做的工程,以及这套栈对 DeepSeek-V4 意味着什么。

从 DFlash 到 DSpark

DFlash: parallel drafting with KV injection

大多数投机方案,包括 EAGLE 系列和 DeepSeek-V4 这类模型自带的 MTP head,起草是自回归的:小模型逐 token 生成,草稿越长起草开销越大,为了压延迟只能把 drafter 做得很浅。

Z Lab 开发的 DFlash 用一个轻量的块扩散(block diffusion)模型换掉自回归 drafter,一次 forward 并行产出整块草稿。做到这一点不只是换个 drafter 那么简单。从头训练的小扩散模型接受率很低,拿现成的大扩散 LLM 起草又太贵。DFlash 的答案是 KV injection:最懂 context 的是 target 模型本身,所以把 target 的 hidden states 抽出来,注入 draft 模型每一层的 KV cache。EAGLE 类方法只在 drafter 的输入端喂一次 target 特征,drafter 一深信号就衰减;逐层注入让 context 信号一直在场,drafter 可以完全不建模 context,把全部容量花在预测这一块上:

DFlash 起草图示块长 8 与 5 层 drafter 为示意取值
自回归 drafter(EAGLE / MTP)8 tokens · 8 次 draft forward
draft 层 ×5仅输入端注入
DFlash:块扩散 drafter8 tokens · 1 次 draft forward
draft 层 ×5逐层注入 KV1 次 forward

两个部件的贡献是分开的,论文的消融把这一点摆得很清楚。只用扩散、不用注入,端到端就已经比 EAGLE-3 快,哪怕接受长度更低。只用注入、不用扩散,接受长度反超 EAGLE-3。两个一起用,接受长度保持在 EAGLE-3 的水平,起草却快一个量级:GSM8K 上接受 4.2 个、端到端 3.3×,对 EAGLE-3 的 4.2 个、2.1×。在 Qwen3.5-397B-A17B 上,联合发布的 drafter 在 HumanEval 并发 1 下达到非投机基线的 4.3×、MTP 的 1.5×:

DFlash 消融图示5 层 drafter · Qwen3-4B target · 同一数据集训练;纵轴 = 端到端加速比
1×2×3×2.1×2.4×2.9×3.3×GSM8K2.2×2.3×2.9×3.2×HumanEval1.4×1.5×2.0×2.2×MT-Bench

DSpark:sequential head 加验证预算

纯并行的块留下了两件事没解决。第一,块内各位置的起草互相独立,没有块内依赖,质量向块尾衰减;DSpark 论文称之为 suffix decay。第二,验证侧原封未动:每个请求照旧整块 verify,高并发下就是 Background 里那笔 B×K 的账。

来自 DeepSeek 的 DSpark 用 draft 侧的三件东西从两端一起下手。block drafter 仍然一次 forward 产出 gamma 个 token 的块,但一个轻量的 sequential head(Markov 或 RNN)让每一位以前一位为条件,这就是「半自回归」的含义,也正是对付 suffix decay 的那一手。

为了解决高并发下整块验证的开销,DSpark 引入 confidence head。它是搭在 drafter hidden state 上的一个线性层加 sigmoid,给每个草稿打「能通过验证」的分数。训练标签就是 drafter 在这一位的输出分布与 target 分布的实测差距,所以分数直接反映下文好不好猜:公式和代码把下一个 token 锁得很死,分就高;自由发挥的文本分就低。整块连乘是块的存活率,Sequential Temperature Scaling(STS)再把它校准到真实接受率。

有了这些存活分,验证长度才能按请求来调度:沿着块的存活率曲线往下走,走到「期望多接受的 token 盖不住多验证一个 token 的边际成本」就停。下图跟着一个请求走完一次 draft:

DSpark 草稿块图示块长 γ = 6,存活率为脚本化数值;虚线 = scheduler 的窗口切点
已提交前缀
t = 0/5

在 Qwen3-4B、8B、14B 三个 target 上,论文报告宏平均接受长度比 DFlash 分别高 16.3%、18.4%、18.3%。剩下的是 serving 问题:得有人给边际成本定价、每步切好预算,还要让一批验证长度参差不齐的请求在真实 GPU 上跑得快。这就是 SGLang 的部分。

SGLang 怎么把 DSpark serve 起来

SGLang 在 #30261(2026 年 7 月合入)完成集成,drafter 直接搭在 DFlash 的 draft 模型之上,稠密与稀疏 target 都支持,例如 Qwen3 和 DeepSeek-V4。在 DeepSeek-V4-Flash 的完整并发扫掠中,这套集成保持最优的吞吐/延迟频谱,全程领先 MTP 和非投机基线:

吞吐-延迟频谱图示DeepSeek-V4-Flash,H200 DP4,OSL1024;数据点读自原文 Figure 1(近似);标注 = batch size
0501001502002503002K4K6K8K10K12K单请求解码速度 (tokens/s)总吞吐 (K tokens/s)bs1bs16bs64bs256bs1bs16bs64bs256bs1bs16bs64bs256

本节余下的部分逐个讲把论文的调度变成真实墙钟时间的工程。

Confidence scheduler 与 verify 模式

一句话本质:这个优化把「验证多少个 token」从全局常数变成每个请求、每一步的单独决策。

SGLang 的 confidence scheduler 每步给批次里的每个请求执行上一节的预算规则:沿存活率曲线往下走,走到期望收益盖不住边际成本就停。边际成本由 SGLang 在这台 server 上实测:每多验一个 token,步时多花多少,存成成本表。窗口长短就是这场比较的结果:草稿分数高的请求保住长窗口,分数掉得快的被裁到两三个 token;负载升高会抬高边际成本,所有窗口一起收紧:

confidence scheduler 图示每行一个请求,每格一个草稿位置;颜色深浅 = confidence head 逐位打出的存活分(悬停看具体值),虚线 = 窗口切点
请求 1请求 2请求 3请求 4请求 5请求 6

verify 分三种模式,分的是速度和可观测性的取舍。compact 只验 scheduler 给的窗口,是生产路径,预算省下的开销全靠它兑现。它有个盲区:被裁掉的位置从没验过,看不到整块验证本可以多接受几个,也就无从判断 scheduler 裁得好不好。static 整块全验,是不裁剪的对照基线,用来做 A/B。cap-accept 为测量而生:付出和 static 一样的验证开销,但只提交到窗口,产出和 compact 完全一致,同时记下裁剪放弃了多少。实际生产用 compact 跑流量,cap-accept 当审计臂检查裁剪质量;下文混合流量里 0.88–0.97 的利用率就是这么测出来的:

verify 三模式图示同一请求:块长 6,窗口 3,target 实际会接受前 4 个;bonus token 各模式相同,图中略去
staticverify 6 · 提交 4 · 观测到 ceiling:能 (4)compactverify 3 · 提交 3 · 观测到 ceiling:不能cap-acceptverify 6 · 提交 3 · 观测到 ceiling:能 (4)

Ragged verify under full CUDA graphs

CUDA graph 要求固定的输入形状,而裁剪后每个请求的验证长度都不一样;padding 在这里不是选项,把大家补齐等于把裁掉的又算回来。SGLang 把调度到的 verify token 首尾相接打包进一个 [总 token 数, hidden] 的 buffer,这正是 transformer 做逐行矩阵乘时本来就在用的布局。cu_seqlens 式的变长输入标出每个请求的分段起点,每个 token 只对自己请求的 KV 做 attention,后端现成的 kernel 就能处理(DeepSeek-V4 上是 flash_mla)。graph 则按打包后的总 token 数选档位,向上取整到最近的已捕获档位。预算一收紧,总数落到更小的档位,回放的就是 attention 和 MLP 行数真正更少的 graph。DP attention 下各 rank 共用最大档位、一起降档:

ragged 打包图示3 个请求,窗口 5 / 3 / 2;已捕获档位 tiers = 4 / 8 / 12 / 16
R1R2R3

调度收益

动态预算到底买到了多少?原文把 compact 和 no-trim 对比,后者是把 static 的整块调度跑在同一条 ragged 路径上,比较对象是两个接受率不同的示例负载。收益主要是高批次效应。batch 1 时,target verify 几乎不因多验几个 token 变慢,裁剪省不了什么,两臂打平。并发上去、吞吐开始进入平台期后,裁剪缩短了每一步,compact 拉开领先。在接受率更低的负载上,差距更大、拉开得更早,因为更低的接受率留下了更多可裁的尾巴。

裁剪收益图示compact(逐请求裁剪) vs no-trim(整块验证),同一条 ragged 路径;batch 1→256,数据点读自原文 Figure 3(近似)
高接受负载0501001502002K4K6K8K单请求解码速度 (tokens/s)bs1bs256
低接受负载0501001502002K4K6K8K单请求解码速度 (tokens/s)bs1bs256

这个 scheduler 是第一版朴素实现,所以结论看趋势,不看绝对数字。同质负载的扫掠还会掩盖它的真正意义:同一批次里,一个请求比另一个可预测得多时,两者不该拿到同样的窗口。在 gsm8k、arena-hard、poetry 的混合流量上,窗口随难度收缩,从 5.24 到 3.78 再到 2.91 个 token,对接受上限的利用率保持在 0.88–0.97。约 55% 的 gsm8k 步用满 6 的窗口,约 80% 的 poetry 步只用 3 个以内:

混合流量图示cap-accept 口径,块长 6;左图柱标为原文 Figure 4 的精确值,右图分布读自图形(近似)
每步 verify 预算(token)123454.535.244.39gsm8k(高接受)3.133.782.84arena-hard(中)2.362.912.08poetry(低接受)
每步 verify 长度分布20%40%60%123456本步 verify 的 token 数

Zero-overhead scheduling 与 kernel

把调度变成墙钟时间靠两类工程。kernel 侧,SGLang 把 verify 周围成簇的小算子重写成融合的 Triton kernel:compact scatter、SWA page-index、验证长度的 top-k 调度、ragged 窗口打包。block drafter 的采样路径折叠进融合 kernel,矩阵乘做了 shard。在一个示例 profile 里,target verify 之外的部分缩短了 1.7 ms,而 verify 本身是 7.3 ms。

调度侧,DSpark 几乎不加特判就接入了 SGLang 的 zero-overhead(overlap)scheduler。spec-v2 运行时是随 DFlash 集成一起上线的,当时它的 overlap scheduling 在初版实现之上又多拿了 33% 的吞吐。它在独立 stream 上把下一步的调度和当前 forward 重叠,DSpark 作为一等 worker 加入:forward 输出以 async future 返回,跨迭代的顺序靠运行时的 device 侧 barrier,页表在 device 上,每步不需要 host 同步。confidence relay 走同一条通道,读两步之前的值。解码循环于是完全没有逐步气泡,比关掉 overlap scheduler 紧约 1.5×:

zero-overhead scheduling 图示batch 1 解码;相同时长窗口内两种模式各跑多少步;块宽度为示意比例
GPUCPU scheduler

用 SGLang serve DeepSeek-V4

DeepSeek-V4 有 1.6T 的 Pro 和 284B 的 Flash 两个尺寸,SGLang 从 Day 0 起就支持这个家族,前缀缓存、kernel、并行方式都为它的混合注意力路径专门做过。投机解码架在这之上,是 serving 时的一个选择:--speculative-algorithm 选 EAGLE(跑 checkpoint 自带的 MTP head)、DFLASH 或 DSPARK,三者都不改 target 模型的架构与输出分布。

投机方案目标模型 · 硬件接受长度单请求解码
EAGLE(MTP head)V4-Pro · B200 TP8~2.5199 tok/s
EAGLE(MTP head)V4-Flash · H200 TP4~2.5266 tok/s
DSparkV4-Pro · B300 TP8~5383.7 tok/s

前两行来自 Day-0 blog,第三行来自 DSpark blog。硬件不同代,所以这是参照表,不是严格 A/B;同一台 server 上的对照是本文前面那张吞吐-延迟频谱图。

Acknowledgments

原文致谢 DSpark 作者们与 DeepSeek 提供算法与模型,以及 Z Lab、Modal 和 SGLang 团队带来 DFlash 与 Spec V2。

上手试试 · Try on SGLang 打开 SGLang 仓库,在真实推理引擎里跑一跑这套机制

延伸阅读

可视化教学由 Yichi Zhang 写作。