音乐
暂未播放
DLoop 完全拆解:让一次验证覆盖多个草稿阶段的循环投机解码
背景:草稿与验证的固定交替,以及那个被浪费的机会#
投机解码的基本结构不会因为草稿模型怎么设计而改变:草稿模型 Md 先生成一个包含 k 个 token 的块,目标模型 Mt 用一次前向传播整体验证这个块,接受一部分 token,再补出一个经拒绝采样校正的 bonus token。这一结构无损地等价于目标模型直接采样,这一点从 Leviathan 等人提出投机解码起就是共识,本站此前拆解的 EAGLE 系列以及 vLLM 的投机解码实现都是围绕这个交替循环展开的。
问题在于这个“固定交替”本身。草稿一个块只便宜,验证一个块才昂贵。以 DSpark 在 Qwen3-4B 上的实测为例(1024 token 前缀、H100、bf16):一次草稿阶段只要 3.90 ms,而一次验证要 22.45 ms。也就是说,在一个“草稿一次、验证一次”的循环里,超过八成的时间花在验证上。想让投机解码更快,最直接的思路向来是提高草稿质量、让验证一次接受的 token 更多;但 DLoop 论文先指出了一个被几乎所有草稿器忽略的机会——相当多的草稿阶段本来就完全被接受。
论文在 Qwen3-4B 上统计了 EAGLE-3、DFlash、Domino、DSpark 四个草稿器在数学、代码、聊天三类任务中的 Acceptance Length 分布,发现“一次验证接受整块”的阶段占比非常高:DSpark 即使把块大小拉到 7 个 token,仍有大约三成以上的草稿阶段整体正确;数学类任务上的比例还要更高,论文 Figure 1 给出的就是这组占比。既然一个草稿阶段经常全对,把它和一个必然要做的验证绑定在一起,本质上是在用一次昂贵验证的固定开销,去买少量确实需要校正的 token。
这里有个结构性的上限值得单独说清楚。无论草稿模型多准,“草稿 k 个 + 验证一次 + bonus”的结构决定了每次验证至多产出 k+1 个 token。这条上限与草稿质量无关,与接受率无关,是结构本身给的:想要更多 token,就必须再做一轮草稿,再做一次验证。而验证的成本随验证长度只适度增长——论文在 EAGLE-3 上实测,一次验证的 token 数从 1 增到 16,延迟从 21.9 ms 只升到 25.0 ms——所以“多验证几次”的代价是线性叠加的多份 22 ms,“一次验证吞下更多 token”的代价却几乎不变。这正是 DLoop 切入的位置。
为什么自适应草稿长度帮不了并行草稿器#
“增加两次验证之间产出的 token 数”并不是新想法,社区里已经积累了大量自适应草稿长度方法:BiLD、Draft & Verify、AdaEDL、SpecDec++、DISCO、AdaEAGLE、DSpark 内置的置信度调度器等等。它们的核心逻辑都是在一个草稿阶段内部决定“这次草稿走够了没有”——草稿步进时逐 token 看草稿模型的置信度,一旦不确定就停下来交给验证。对自回归草稿器,这套方法有效,因为草稿每多写一个 token 就多付一次草稿前向的开销,提前停下能省钱。
但对以 DFlash、Domino、DSpark 为代表的并行草稿器,“在阶段内部提前停”在两个层面上失效。
第一层是成本结构。并行草稿器的一个阶段就是一次固定开销的批量前向:块扩散模型一次 denoise 步生成整块 token,或者解码器并行展开整块。无论最终只接受 2 个 token 还是全部 7 个,草稿那一发的成本一分不省。提前停止只改变验证覆盖的长度,不改变已经花掉的钱。
第二层更致命:再往前草稿没有输入可用。自回归草稿器写第 k+1 个 token 时,条件是已草稿的前 k 个 token,这是草稿自己就能提供的信息。但并行草稿器的每个阶段都条件在目标模型侧的隐状态 h1:t−1 和锚点 token xt 上——这个锚点在上一个验证发生后才有(通常就是 bonus token)。如果想在还没验证的阶段后面再接一个阶段,需要的 xt 对应的是未经验证的草稿 token,它的目标侧隐状态还不存在,于是草稿条件根本构造不出来。这就是为什么此前所有自适应长度方法在 DSpark 这类模型上最多打平:论文的对照实验里,九种自适应长度基线在 EAGLE-3 上最多带来 +0.14× 的加速,在 DSpark 上没有一个跑赢固定长度的基线。
DLoop 的解法是把问题换一个方向问:不延长一个阶段,而是在两次验证之间多放几个阶段。既然下一个阶段的锚点必须来自验证,那就干脆保持“阶段串行、验证集中”的结构,让第 s 个阶段完成后用草稿自己的信息判断一下“值不值得继续草稿”,值得就再草稿一段,直到判断说不值得,才把攒下来的 S 个块一起交给目标模型做一次验证。
机制一:置信度门控,让一个草稿阶段决定是否再来一个#
门控分数:块内 log 概率求和#
DLoop 需要一个只依赖草稿侧信息、开销可忽略的信号来判断“刚草稿完的这段整体可信吗”。它选的信号最朴素:cs 等于草稿模型对第 s 个块内每个已草稿 token 给出的 log 概率之和:
cs=xi∈bs∑logpMd(xi∣⋅)其中 bs 是第 s 个草稿阶段的 token 块,pMd(xi∣⋅) 是草稿模型(带自身的解码头)对 token xi 的概率。块级 log 概率求和有一个数学性质:它是非正的,任何一个不太确定的 token 都能凭一个很负的 log 概率把整块分数拖下来。这与“整块都要对才算全对”的需求正好同构——验证要整块全接受才有额外收益,那门控就该在任何一个 token 蹊跷时踩刹车。
门控规则的形式化是:
fgate(bs)={1,0,cs≥gcs<gcs≥g 则允许紧跟着再草稿一个阶段;cs<g 则停止草稿,把 s 个块一起送去验证。阈值 g 是唯一的超参数,论文在 {−0.25,−0.5,−0.75,−1.0} 里扫过,统一取 −0.5 效果最好,仅 EAGLE-3 配 Qwen3-8B 时用 −0.75。log 概率还有个工程上的优点:草稿前向本来就要算完整 logits,分数是免费的副产品,不用额外网络。论文对比过为门控专门训练的分类头、隐状态池化后接分类头、熵等方案,log 概率之和在 AUROC 和精度上都最好;这一信号离理论可达的上界也还有距离——DSpark 上 Oracle 门控能做到 9.50× 加速,实际 8.59×,说明门控质量依然是优化空间而不是瓶颈本身。
DLoop 之前最接近的工作 LLMCad 同样是块级置信度累计,但它决定的是“一个阶段内部要不要提前收手”,作用在草稿的 token 维度上;DLoop 的门控决定的是“要不要再追加一个完整阶段”,作用在阶段维度上。这个看似细微的差别,正是它对并行草稿器有效而前述方法无效的原因:阶段级判断不需要未验证 token 的目标侧信息,门控只消费草稿自己产出的概率。
循环阶段:草稿条件换成草稿自己的隐状态#
额外阶段接在哪里?第一个阶段照旧条件在目标侧隐状态 h1:t−1 与锚点 xt 上;从第二个阶段起,DLoop 把条件里的锚点换成草稿模型自己的最终层隐状态 h~,拼在前缀 h1:t−1 之后:
h~t=concat(h1:t−1, hlast(d))这里 hlast(d) 是草稿模型处理完上一个草稿块后的最终层输出。它替代了原本只有验证才能提供的锚点信息,让一个“待验证”的草稿块也能作为下一个阶段的起点。这个替换的代价立即引出下一节的问题——草稿模型从未在 h~ 条件下被训练过。
验证与无损性:整块一起过,逐段照常校正#
循环积累的 S 个块最终拼成一个长度 Sk 的候选序列,由目标模型一次前向验证。验证逻辑与投机解码的标准流程完全一致:逐段(或逐树)按草稿模型的分布做拒绝采样,每个块的尾部产出经校正的 bonus token。既然每个块内部都没有动接受规则,循环结构与“先验证第一个块、接受后草稿第二个块、再验证”在输出分布上严格等价——无损性质原封不动地保留。
收益结构却变了。一次验证覆盖 S 个阶段,上限不再是 k+1,而是 Sk+1,且 Sk+1 个 token 共享那一份 22 ms 级别的验证开销。两种结构的差异可以用同一个 DSpark 草稿器上的实测数字算清楚:标准结构里每个 3.90 ms 的草稿阶段都跟着一次 22.45 ms 的验证,产出至多 8 个 token(7 草稿 + 1 bonus),每 token 至少要摊上 (3.90 + 22.45) ÷ 8 ≈ 3.29 ms;循环结构让多个草稿阶段共享一份验证开销后,验证次数按比例减少,同样的验证开销摊到更多已草稿 token 上。
机制二:循环感知训练,补上并行草稿器缺失的那块能力#
草稿模型从未见过以 h~ 为条件的输入,直接接循环,性能应当崩掉。DLoop 的第二根支柱就是为此设计的训练方案,思路与 EAGLE-3 的 training-time test 一脉相承——在训练时就让模型经历推理时才出现的输入形态。
训练流程分三步。
采样阶段数。 每个样本先把标准的一个草稿阶段训到底,然后均匀采样一个 S−1∈{1,…,N},决定这次训练模拟多少个循环阶段。N 取 3,意味着每个样本最多连出 4 个阶段。附录 A.7 的深度消融显示:N=1(只多训一层循环)就已经能带动所有草稿模型,DSpark 的接受长度从 5.75 涨到 8.13;N 提到 3 对 DFlash 和 DSpark 还有进一步收益;N=5 则不再带来提升,训练时间和显存却继续上升。
冻结目标,只切坏尾。 循环链上每个阶段的草稿都基于草稿自己产的隐状态 h~,链一长,错误会累积,最后一个阶段的草稿条件是“最脏”的。DLoop 的损失刻意只保留链上两个端点:
L=21(L(1)+L(S))L(1) 就是标准投机解码的训练目标(如 EAGLE 或 DSpark 的原有损失),保住单阶段质量不退化;L(S) 是链尾阶段在最长 h~ 累积下的损失,直接对准推理时最难的情形。中间阶段的输出不计算 loss、也不算 logits,纯属前向通道上的铺垫。这个设计的取舍很明确:如果给每个中间阶段都加监督(论文称 mid-CE),中间质量问题反而引入噪声,DSpark 在 N=5 时甚至 memory 溢出;而只训两端,单阶段能力由 L(1) 锚定,循环能力由 L(S) 兜底,两端都紧,中间不需要单独照顾。
不改架构,不用额外验证目标。 整个训练过程不增加任何参数,不新增目标模型前向——L(S) 只需要草稿模型往前多跑几步,损失在草稿侧就能算完。一个 epoch、PerfectBlend 数据集上的 1.3M 条提示、从公开权重起训,就能把循环能力补齐。对部署方来说,这是一笔很便宜的一次性投入。
消融实验(论文 Table 2,以 Qwen3-4B 为例)把两根支柱拆开做对照:
| 草稿器 | 基线 | 仅开门控 | 门控 + 训练 |
|---|---|---|---|
| EAGLE-3 | 6.92 / 3.35× | 8.04 / 3.52× | 9.63 / 3.77× |
| DFlash | 6.10 / 4.19× | 6.11 / 4.16× | 7.21 / 4.67× |
| Domino | 7.08 / 4.38× | 7.15 / 4.32× | 7.90 / 4.58× |
| DSpark | 5.47 / 3.77× | 5.77 / 3.79× | 8.59 / 4.76× |
三个规律值得逐一拆开。开门控而不开训练,接受长度在所有组合里都上升,但加速比的变化分两派:对本来就用自己隐状态串行写 token 的自回归草稿器(EAGLE-3、配 Qwen3.5-9B 的 MTP),加速比同样上升;其余组合几乎不动甚至下降——多出来的草稿阶段时间把接受长度的收益吃掉了。这组数字直接印证了上一节的结构论证:没有训练,并行草稿器做不了循环。
单独做循环感知训练,两个指标的变化都不足 7%,草稿模型和公开发布版几乎一样——训练本身不改善单个阶段的质量。但一旦和门控叠加,所有组合的两个指标都全面上升,且相对基线的总增益超过两者单独增益之和。论文把这概括为循环感知训练是为循环做准备,而不是改善每个草稿阶段本身:单阶段能力与循环能力不是线性可加的资源,而是一对彼此解锁的要素。
实验结果:四个草稿器上的统一抬升#
主表(Table 1)覆盖 Qwen3-4B、Qwen3.5-9B(MTP 草稿)、Gemma-4-12B(MTP 草稿),八个基准(数学 GSM8K、MATH-500、AIME25,代码 HumanEval、MBPP、LiveCodeBench,聊天 MT-Bench、Alpaca),贪心解码 T=0,温度为 1 的结果同样正向。表中数字含义:接受长度 / 加速比,均为八基准平均。加 DLoop 之后:
- EAGLE-3:6.92 / 3.35× 提升到 9.63 / 3.77×
- DFlash:6.10 / 4.19× 提升到 7.21 / 4.67×
- Domino:7.08 / 4.38× 提升到 7.90 / 4.58×
- DSpark:5.47 / 3.77× 提升到 8.59 / 4.76×
- Qwen3.5-9B MTP:3.32 / 1.55× 提升到 6.70 / 2.18×(接受长度翻倍)
- Gemma-4-12B MTP:4.86 / 3.17× 提升到 7.50 / 3.68×
两个跨系统规律值得注意。其一,接受长度增加 12%–102%,而整体多草稿的墙钟开销只有 5%–41%——这也是整个方法成立的经济学基础:验证远贵于草稿,用便宜的草稿换昂贵的验证怎么算都是赚的。其二,DFlash 等原本就很激进的系统提升相对小(它们已经把验证吞吐用得很满),原本“慢”的 EAGLE-3 和 MTP 草稿提升相对大——DLoop 修的是结构浪费,哪里的结构浪费大,哪里涨得多。
分解来看(Figure 3),总解码时间降幅 4.9%–24.3%,其中 DSpark 降幅最大;34%–71% 的 token 来自 S>1 的循环(DSpark 达 71%);每 token 时间从 S=1 时 4.60–6.62 ms 单调降到 S≥5 时 0.68–2.97 ms。验证次数减少 22.7%(DSpark,无树)到 26.9%(带树),而草稿阶段数增加 3.5%–24.5%——交换方向正如设计,用更多便宜的草稿前向换更少昂贵的验证前向。
内存与时间开销可以放心:从 S=1 到 S=4,峰值显存增量小于 0.1 GB;门控本身耗时不超过 0.33 ms,相对 22 ms 量级的验证可以忽略。
与树形验证的组合,以及自适应长度方法的对比#
DLoop 不改如何产生草稿 token,它改的只是验证前面挂几个块。因此它与任何树形验证方案正交,可以直接叠加,而这正是它最亮眼的数字。
以 DFlash 为例,单独开 DDTree 把加速从 4.19× 提到 5.79×;再叠加 DLoop,推到 6.48×,接受长度 10.37。Domino 配 DARTree 加 DLoop 达到 11.17 / 5.40×;DSpark 配 DARTree 加 DLoop 达到 10.14 / 5.15×,比单纯开树再提 22%–25%。树形验证把一次前向的接受面拉得很宽,这恰恰意味着“多数草稿会被吃掉”——DLoop 在树上做的事,是让最高置信度的根到叶路径继续向外生长,再由同一次验证吃掉;两者方向天然一致。
反过来,把它与“自适应草稿长度”那一族相比,差距是全面的。Table 3 里九种基线在 EAGLE-3 上最好的也只有 +0.14×,在 DSpark 上没有一个跑赢固定长度;DLoop 在两个系统上都拿到最好的数字,例如 EAGLE-3 的 MATH 上 12.04 / 4.58×,DSpark 的 MATH 上 11.02 / 6.02×。同是“看置信度调草稿”,阶段内决策与阶段间决策的适用范围完全不同,这是选型时要记住的分界线。
部署视角下的边界#
吞吐场景的衰减。论文用 SGLang 在不同并发下测了服务化表现:并发 1 到 8 时增益基本保持;并发抬到 32 时,DFlash + DLoop 偶现低于基线,DSpark + DLoop 仍保持正增益。原因是循环把序列拉长后,同一 batch 里不同请求的草稿链条长度参差,批量执行时需要对齐与重分组,这些同步开销在低并发下可忽略、在高并发下开始可见。把它用到线上服务,需要评估并发档位与打批策略。
超参数的通用性。门控阈值 −0.5 在大多数组合里就是最优,但不是处处最优,EAGLE-3 配 Qwen3-8B 需要 −0.75。它是全局单一阈值,没有按任务或按 token 位置的自适应;对于拒绝率分布特殊的场景,重新扫一遍阈值是必要的工作量。
门控离上界还有距离。Oracle 门控在 DSpark 上能到 9.50× / 5.32×,实际 8.59× / 4.76×,差 10% 左右。log 概率之和是免费信号,不是最强信号,更好的门控特征(比如结合草稿隐状态)是自然的后续方向。
对 N 的依赖。训练时的 N=3 是“够用就好”,门控实际允许的循环数分布与之需要匹配;如果未来想支持更长的循环链,训练采样范围和损失端点的选择都要跟着改。
小结#
DLoop 的价值在于把投机解码的优化主战场从“怎么草得准”换到了“一次验证能吃下多少草”。前者是一条越走越窄的路:草稿质量提升一倍,验证次数未必能减半;后者则指着一个明确的经济学事实——验证一次的费用与一次草稿不在一个量级。置信度门控提供了阶段间决策的最小开销实现,循环感知训练补上了并行草稿器做循环时的能力缺口,两者单独看都不够,合在一起才构成一次完整的方案。
它对本站已经拆解过的投机解码系列是一块直接的拼图:EAGLE 系列解决“草得更准”,DSpark、DFlash、DFlow 解决“一块草得更多”,DeepSeek 的 MTP 说明草稿器可以长在目标模型上,而 DLoop 解决“验证为什么总是这么频繁”。这四件事相互正交,可以逐层叠加;vLLM 侧的落地可以看到工程流水线如何把草稿与验证拼在一起(参见 vLLM 源码完全拆解(十)),DLoop 这类结构级改动未来若进入主流推理框架,动的大概就是这一层。
参考资料#
- DLoop: Looped Speculative Decoding via Confidence-Gated Drafting and Loop-Aware Training
- Fast Inference from Transformers via Speculative Decoding(Leviathan 等)
- EAGLE-3: Scaling up Inference Acceleration of Large Language Models via Training-Time Test
- DSpark: Confidence-Scheduled Speculative Decoding with Semi-Autoregressive Generation
- DFlash: Block Diffusion for Flash Speculative Decoding
- Domino: Decoupling Causal Modeling from Autoregressive Drafting in Speculative Decoding
- SpecDec++: Boosting Speculative Decoding via Adaptive Candidate Lengths
- AdaEDL: Early Draft Stopping for Speculative Decoding of Large Language Models
- SGLang: Efficient Execution of Structured Language Model Programs
必要图解:DLoop 为什么能把“拒绝”变成下一轮的起点#
只看接受率或加速比,很容易忽略 DLoop 与普通推测解码真正不同的地方:验证失败后,它不会把已经完成的草稿计算全部丢掉,而是把边界处的信息送回草稿模型,继续生成下一段候选。下面两张图分别给出执行路径和计算复用方式。
1<path d="M220 162 H302" class="arrow" />2<g aria-label="候选 token 序列">3 <rect x="318" y="124" width="48" height="48" rx="10" fill="#16a34a" /><text x="342" y="149" class="token">A</text>4 <rect x="374" y="124" width="48" height="48" rx="10" fill="#16a34a" /><text x="398" y="149" class="token">B</text>5 <rect x="430" y="124" width="48" height="48" rx="10" fill="#16a34a" /><text x="454" y="149" class="token">C</text>6 <rect x="486" y="124" width="48" height="48" rx="10" fill="#dc2626" /><text x="510" y="149" class="token">×</text>7 <text x="426" y="202" class="sub">候选 token</text>8</g>9
10<path d="M534 162 H616" class="arrow" />11<rect x="632" y="82" width="250" height="160" class="box" fill="#f5f3ff" stroke="#7c3aed" />12<text x="757" y="122" class="label">目标模型并行验证</text>13<text x="757" y="153" class="sub">接受 A、B、C</text>14<text x="757" y="179" class="sub">在分歧位置采样校正 token</text>15<text x="757" y="205" class="sub">保证输出分布不变</text>16
17<path d="M758 242 C758 318 126 324 126 221" class="arrow" />18<rect x="332" y="278" width="300" height="52" rx="26" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />19<text x="482" y="310" class="label">校正结果回流,启动下一轮</text>这张图里最关键的是底部的回流箭头。普通推测解码通常把一次“起草—验证”视为相对独立的回合;DLoop 则显式维护跨回合的连续性,让被验证过的上下文、接受前缀和校正 token 共同成为下一轮状态。它优化的不是模型本身的概率分布,而是生成与验证之间的调度方式。
1<text x="28" y="54" class="r-title">普通推测解码</text>2<line x1="180" y1="50" x2="910" y2="50" stroke="#cbd5e1" stroke-width="2" />3<g>4 <rect x="190" y="80" width="88" height="52" rx="10" fill="#0284c7" /><text x="234" y="107" class="r-token">起草</text>5 <rect x="294" y="80" width="88" height="52" rx="10" fill="#7c3aed" /><text x="338" y="107" class="r-token">验证</text>6 <rect x="398" y="80" width="166" height="52" rx="10" fill="#16a34a" /><text x="481" y="107" class="r-token">接受前缀</text>7 <rect x="580" y="80" width="88" height="52" rx="10" fill="#dc2626" /><text x="624" y="107" class="r-token">分歧</text>8 <path d="M668 106 H728" class="r-line" />9 <rect x="744" y="80" width="166" height="52" rx="10" fill="#94a3b8" /><text x="827" y="107" class="r-token">重新准备</text>10 <text x="481" y="158" class="r-text">每轮边界更明显,分歧后的调度容易形成空隙</text>11</g>12
13<text x="28" y="234" class="r-title">DLoop</text>14<line x1="104" y1="230" x2="910" y2="230" stroke="#cbd5e1" stroke-width="2" />15<g>16 <rect x="190" y="260" width="88" height="52" rx="10" fill="#0284c7" /><text x="234" y="287" class="r-token">起草</text>17 <rect x="294" y="260" width="88" height="52" rx="10" fill="#7c3aed" /><text x="338" y="287" class="r-token">验证</text>18 <rect x="398" y="260" width="166" height="52" rx="10" fill="#16a34a" /><text x="481" y="287" class="r-token">接受前缀</text>19 <rect x="580" y="260" width="88" height="52" rx="10" fill="#ea580c" /><text x="624" y="287" class="r-token">校正</text>20 <path d="M668 286 H728" class="r-line" />21 <rect x="744" y="260" width="166" height="52" rx="10" fill="#0284c7" /><text x="827" y="287" class="r-token">下一轮起草</text>22 <path d="M827 312 C827 364 234 364 234 314" class="r-line" />23 <text x="481" y="379" class="r-text">沿同一状态链继续推进,减少回合切换造成的空转</text>24</g>因此,阅读实验结果时不能只看“每轮起草了多少 token”。更值得同时观察的是接受前缀长度、目标模型验证耗时、回流调度成本,以及不同 batch size 下设备是否被充分利用。图中的闭环越顺畅,DLoop 才越可能把理论上减少的串行步骤转化为真实的端到端加速。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
评论区
分享你的想法,与大家交流讨论
音乐
暂未播放



