一次前向,白赚 4 个 token:DFlash 2 块扩散投机解码技术详解

自 2026 年 1 月发布以来(论文于 2 月登上 arXiv),DFlash 用七个月完成了从论文到行业标准的跨越:SGLang、vLLM、TensorRT-LLM、llama.cpp 四大推理引擎全部内置,NVIDIA 在 Blackwell 上测得最高 15 倍吞吐提升,Hugging Face 下载量超过 350 万。2026 年 8 月,Inco AI 发布 DFlash 2,在几乎不增加延迟的前提下,把每次验证的产出提高了 20% 以上。

想象一下:生成 100 个 token,普通解码要跑 100 次完整前向;投机解码让大模型一次前向验证小模型猜出的一串词;DFlash 则更进一步——把”猜”这一步也做成了并行。于是一次验证前向,”白赚“的不再是一个 token,而是整整一块:4 个、6 个,乃至更多。

本文以 DFlash 2 官方博客与开源仓库(github.com/z-lab/dflash)代码为基础,从投机解码的基本问题出发,完整梳理 DFlash 1 的块扩散架构、它暴露的两个结构性缺口,以及 DFlash 2 的两个轻量解法——路径选择器与局部卷积。


第一章 背景:投机解码——验证可以并行,草稿仍然串行

自回归解码每生成一个 token 就要执行一次完整的前向传播——这是 LLM 推理延迟与成本的基本账单。投机解码是这个账单上最成功的”减税方案”,但它有一个被长期忽略的暗角:验证是并行的,草稿模型本身仍然是串行的。DFlash 的全部工作,都是从这个暗角开始的。

1.1 解码的真相:每个 token 都是一次完整前向

大模型的文本生成是自回归的:模型根据已生成的全部 token 预测下一个 token,把它拼到序列尾部,再预测下一个。这意味着输出长度为 N 的文本,需要执行 N 次完整的前向传播——每增加一个 token,都要重新跑一遍从第一层到 LM head 的全部计算。KV cache 缓存了注意力机制的历史状态,避免了历史 token 的重复计算,但每一层的计算量依然与序列长度同阶累积(新 token 的注意力仍要扫过全部历史位置)。

进入智能体时代,这张账单被进一步放大。Inco AI 在 DFlash 2 博客里开宗明义:推理是智能体时代的瓶颈。智能体会连续数小时甚至数天阅读、规划、调用工具,token 消耗速率远超聊天场景——聊天一次对话消耗几千 token,一个智能体任务可能消耗几十万,而每一个 token 都对应一次完整前向。推理栈的每一点提速,都会以同样的倍数放大到整个智能体生态。

1.2 投机解码:让大模型验证,而不是生成

投机解码(speculative decoding)是目前服务栈中最成熟的加速范式,基本流程是:

  1. 小模型(draft model)草拟一个长度为 γ 的 token 块;
  2. 目标模型对整块做一次前向,并行计算所有位置的分布;
  3. 逐位置决定接受还是拒绝:接受的 token 免费获得,拒绝处回退并补采样;
  4. 通过 rejection sampling 保证输出分布与目标模型单独解码完全一致——加速不改变模型的输出行为。

一次验证平均接受的 token 数称为接受长度(acceptance length),它是投机解码的核心效率指标:接受长度越接近块长 γ,一次验证前向的产出越高;接近 1 时,草稿纯属额外开销。

这个范式已被主流推理引擎全量内置:vLLM、SGLang、TensorRT-LLM、llama.cpp 都支持从 n-gram 草稿、小型 LM 到 MTP(多 token 预测)的各种草稿方案。投机解码早已不是实验特性,而是生产级服务栈的标准组件。

1.3 草稿模型的自回归困境

投机解码的全部收益来自草稿质量与草稿速度的比值。传统草稿模型是一个小型自回归 LM:为了草拟一个 γ-token 的块,它要逐 token 生成 γ 次,每次都是一次小前向。这里有一组尖锐的矛盾:

  • 草稿要快 → 模型要小,层数要浅;
  • 草稿要准 → 模型要强,上下文建模要深;
  • 即使块验证是并行的,草稿生成本身仍然是串行的——块越长,草稿阶段越慢。

换句话说,投机解码解决了”验证”的并行问题,却把”串行”留给了草稿。DFlash 的出发点就是这一句矛盾:草稿为什么不能也并行?


第二章 问题:DFlash 1 的并行回答

DFlash 1(2026 年 1 月发布,2 月登上 arXiv,arXiv 2602.06036)用”块扩散”回答了草稿并行问题:一次前向预测整块所有位置,用目标模型自身的中间表示做条件,以 rejection sampling 保证无损。它取得了显著的产业成绩,但也暴露了两个结构性缺口——选择余量与块尾衰减,它们恰好构成 DFlash 2 的问题定义。

2.1 关键洞察:把草稿建模为块扩散

DFlash 1 把草稿从”自回归生成”重写为”并行预测”:块内位置先用 mask token(一个表示”该位置待预测”的特殊占位 token)占位,模型一次前向同时输出所有位置的分布——没有任何自回归依赖。这与扩散模型、掩码语言模型的思路同源:从”全掩码”出发,一步还原整块 token。每一块的输入由一个锚点(上一块最后已验证的 token)与 γ 个 mask 占位构成,形如 [锚点 | mask | mask | mask | mask](块长 block_sizemask_token_id 都是模型配置的一部分,dflash/model.py:569-570)。生成流程按块循环,一轮完整的循环如下:

  1. 上一轮验证产出 [✓ ✓ ✗],最后接受的 token 成为下一块的锚点;
  2. 下一块输入 = 锚点 + 4 个 mask 占位:[锚点 | mask | mask | mask | mask]
  3. 草稿模型一次前向,并行预测所有 mask 位置,得到候选 [已定 | cat | sat | on | mat]
  4. 目标模型对整块一次前向验证(只跑 1 次前向),逐位置接受或拒绝;
  5. 拒绝处从 p − q 残差重归一化后补采样;
  6. 本轮产出 [cat | sat | on | under] = 3 个接受 + 1 个补采样 = 4 个新 token,目标只跑了 1 次验证前向。

从后文的基准数据看,一次验证的平均接受长度稳定在 4–6——对 8 长度的块通常有一半以上被接受(对 16 长度块约为四分之一到三分之一),说明并行预测的质量可以非常高。

2.2 DFlash 1 的三层架构

DFlash 1 的草稿模型由三个设计点构成,官方 PyTorch 实现(dflash/model.py)的对应关系如下。

输入:噪声嵌入,与目标共享词嵌入。 块内 token(锚点 + mask 占位)直接用目标模型的嵌入层编码,再乘以缩放因子 input_embedding_scale_raw_input_embeddingsdflash/model.py:135-138)。共享词嵌入让草稿与目标模型处于同一表示空间,缩放因子则控制着整块嵌入相对上下文表示的”噪声强度”——这正是块扩散中”扩散 / 去噪”的落点。

上下文:注入目标模型的中间表示。 草稿不是凭空预测:块的前文是目标模型刚刚验证过的内容。DFlash 从目标模型若干中间层抽取 hidden states 作为上下文特征——target_layer_ids 把草稿层均匀铺到目标层的深度轴上,两端各留出若干层不采样(build_target_layer_ids 从目标第 1 层扫到倒数第 3 层,dflash/model.py:25-34);以 Qwen3.8-27B 为例,5 层草稿对应 64 层目标,取层为 [5, 19, 33, 47, 61]。抽出的表示拼接后经线性层 fc 投影回模型宽度、再过 RMSNorm(拼接在 extract_context_featuredflash/model.py:37-43fc 投影与 RMSNorm 在 DFlashDraftModel.forward 中完成,dflash/model.py:584)。每一层草稿注意力都以这段上下文为条件。

注意力:上下文与块内因果并见。 每个块位置的 query 同时注意两部分 key/value:上下文 token(来自目标 hidden states 的投影 k_ctx/v_ctx)与块内位置(k_noise/v_noise),块内保持因果掩码(提议序列需从左到右被顺序验证,验证才能逐位进行;Qwen3DFlashAttentiondflash/model.py:340-420)。这意味着草稿层内部只有一个注意力算子,同时承担”读上下文”与”建块内依赖”两项任务——这一点对理解 DFlash 2 的局部卷积至关重要。

输出端,块内各位置的 logits 并行产生(可选 output_multiplier 缩放与 logit softcapping,compute_logitsdflash/model.py:599-605):贪心时逐位置取 argmax,采样时按分布抽取。

2.3 无损保证:rejection sampling

并行预测的质量再高,也可能出错。投机解码的契约是:输出分布必须与目标模型自回归解码一致,否则加速就有代价。DFlash 1 实现的标准 rejection sampling 验证(_rejection_sampledflash/model.py:94-124)如下:

  • 目标模型对整块一次前向,得到每个位置的分布 p;
  • 草稿分布 q 已知,按概率比 p/q 逐位置接受;
  • 全部接受时,从目标模型对块尾位置的分布补采一个 bonus token(全接受时白赚的额外 token,来自目标模型自己);
  • 拒绝处,从”p 减去 q 的残差”重归一化后补采样——即把草稿覆盖不足的那部分概率质量按比例归一化,恰好补成完整的分布 p,保证该位置的条件分布精确等于 p。

贪心模式(temperature = 0)则退化为严格的前缀匹配:(block_output_ids[:, 1:] == posterior[:, :-1]).cumprod(dim=1).sum(dim=1) 计算最长匹配前缀(dflash/model.py:290-292)。无论哪种模式,DFlash 都不改变目标模型的输出行为——它只负责让目标模型”一次前向多产出几个 token”。

用一个具体例子收束本节:4 长度的块,草稿猜出 [cat, sat, on, mat],目标验证接受前三个、在第四个处拒绝——拒绝处从残差分布补采出一个新 token(比如 under)。本次验证产出 3 个接受 + 1 个补采样共 4 个新 token,而目标模型只跑了一次前向。

2.4 DFlash 1 的产业成绩

把草稿并行化之后,DFlash 1 迅速进入产业界(数据来自 DFlash 2 博客):

  • NVIDIA 在 Blackwell 上测得最高 15× 吞吐提升(相对自回归解码);Google 在 TPU 上报告 3× tokens/sec
  • CoreWeave 的 Kimi K2.7 Code 生产端点默认运行 DFlash;
  • 四大引擎(SGLang、vLLM、TensorRT-LLM、llama.cpp)全部集成;
  • 多家模型厂商随自家模型附送官方 DFlash drafter:Meta(Muse Glimmer)、Poolside(Laguna)、小米(MiMo-V2.5-Pro)、NVIDIA(Nemotron 3.5 Lightning);
  • Hugging Face 下载量截至 2026 年 8 月超过 350 万次

七个月,DFlash 从一篇论文变成行业标准。但并行草稿也把两个新问题摆上了台面。

2.5 遗留缺口一:选择余量(selection margin)

DFlash 1 的块内各位置是独立预测的——每个位置的候选都合理,但整块可能不连贯。博客给出了一个关键数据(Table 1,五层 Qwen3-4B,GSM8K,条件于前面位置全对):

位置 0 1 2 3 4 5 6 接受长度
Recall@1 85.4% 80.3% 79.4% 78.3% 77.5% 75.9% 72.9% 4.27
Recall@16 99.5% 97.3% 94.8% 92.6% 90.8% 89.4% 87.8% 6.79

首位的 top-1 正确率只有 85.4%,但正确 token 出现在 top-16 里的概率高达 99.5%。如果存在一个总能从 top-16 里选对的 oracle,接受长度会从 4.27 跳到 6.79——候选池是对的,只是选错了。这个 4.27 → 6.79 的差距,就是”选择余量”。

独立预测还有一个更直观的失败模式:相邻两个位置可能选中同一个词(比如 “for” 重复两次),形成”结巴”。这样的块在验证时会在重复处被掐死,前面的计算全部浪费。

2.6 遗留缺口二:块尾衰减(suffix decay)

更难办的是第二个缺口:即使 oracle 级的选择也无法弥补,因为候选本身在块尾变差。Recall@16 从位置 0 的 99.5% 一路衰减到位置 6 的 87.8%——oracle 的选择能力与位置无关,衰减只能说明块尾的 top-16 候选里根本没有足够好的选项。这是 backbone(草稿模型本身)的表达能力问题,论文将其命名为 suffix decay(块尾衰减)

这两个缺口恰好对应 DFlash 2 的两个模块:选择余量由路径选择器解决,suffix decay 由局部卷积解决。


第三章 方案:DFlash 2——在并行框架内补齐连贯性

DFlash 2 的路线不是退回自回归,而是保留并行预测,在其输出之上叠加两个轻量模块:路径选择器补连贯性,局部卷积补块尾表达。选择器只加 2.0M 参数,卷积只加 16.5M 参数,合计约 1.3% 的 cycle 延迟(cycle 延迟:一次草稿—验证循环的耗时,近似单步时延),换来每次验证 20% 以上的额外产出——且输出经 rejection sampling 可证明不变。

3.1 路径选择器:选择比预测便宜

3.1.1 洞察:连贯性主要是局部的

选择余量的根源是”逐位置独立选择”忽略了位置间的依赖。对标方法走的是一条昂贵的路:Domino(arXiv 2605.29707)与 DSpark(arXiv 2607.05147)用顺序头(逐位置串行修正预测的 head)逐位置重写全词表分布,以近似自回归的修正代价换取连贯性。DFlash 2 的观察是:块只有 4–16 个 token,最紧的依赖发生在相邻位置之间——连贯性主要是局部的。因此只需要对相邻位置对打分,不需要触碰全词表。

3.1.2 打分公式:候选间的低秩双线性注意力

具体做法:保留每个位置的 top-16 候选,对每个相邻位置对 (a, b)(a 为前驱候选,b 为后继候选)打分:

\[S_t(a,b) = U_t(b) + \langle A(a) \odot H(h_t), B(b)\rangle\]
  • 第一项 $U_t(b)$ 是草稿模型自己对 b 的偏好(unary logit),完整继承 DFlash 1 的并行预测结果;
  • 第二项是低秩双线性注意力:a、b 各自映射为 256 维嵌入(predecessor_codebook / successor_codebook;256 维即对全词表做低秩压缩,这正是”低秩”的来源),$H(h_t)$ 是当前隐藏状态的线性投影,作为”门控”决定匹配的哪些维度算数。(⊙ 为逐元素乘,⟨·,·⟩ 为内积。)

官方实现把上式落成一行 einsum(CandidateSelector.selectdflash/model.py:515-547):

scores = unary[:, position] + torch.einsum(
    "br,bkr->bk",
    self.predecessor_codebook(predecessor) * hidden[:, position],
    self.successor_codebook(candidates[:, position]),
)

3.1.3 完全并行:先打分,后游走

选择器不引入任何额外 backbone 或 LM head 前向——所有相邻位置对的分数一次算完,完全并行。唯一的顺序工作发生在预计算好的分数表上:从锚点出发,逐位置取分数最高的后继,走出一条连贯路径:

锚点 "The"
  │  S₀ = U₀(候选) + ⟨A("The") ⊙ H(h₀), B(候选)⟩      ← 以锚点为前驱打分
  ▼
位置 0: [ cat 0.55 | dog 0.30 | fox 0.15 ] ── 取最高分 ──▶ 选 "cat"
  │  S₁ = U₁(候选) + ⟨A("cat") ⊙ H(h₁), B(候选)⟩       ← 前驱换成刚选中的 "cat"
  ▼
位置 1: [ sat 0.50 | ran 0.35 | lay 0.15 ] ── 取最高分 ──▶ 选 "sat"

采样模式下则从同一分数表抽样,并把选中索引对应的分布记录进 rejection sampling(propose 返回 draft_indicesdraft_probsdflash/model.py:251-258)。这里有一个容易忽略的关键细节:draft_probs 并不是全词表分布,而是选择器分数在 top-16 候选子集上的分布;而拒绝采样需要的概率比 p/q 要求 q 是全词表口径。_rejection_sample 通过 (draft_indices == draft_tokens[..., None]).sum(-1) 把索引上的概率投射回对应 token、还原出全词表口径的 q 再参与接受判定(dflash/model.py:102-105),拒绝分支同样按索引从目标分布中扣减子集概率(dflash/model.py:116)。”子集 → 全词表”的账目还原是 DFlash 2 无损性最核心的一环——账目差一点,输出分布就偏一点。完成记账之后,无损性依然成立。

3.1.4 成本账:40 倍便宜的修正

博客 Table 2 给出五层 Qwen3-4B 在 GSM8K 上的对比(T=0 为贪心、T=1 为采样):

方法 参数量 延迟 T=0 T=1
DFlash 4.27 3.78
+ DSpark 修正 +77.8M +9.6% 4.49 4.08
+ 路径选择(DFlash 2) +2.0M +0.6% 4.61 4.25

路径选择器在 T=0 提升 0.34 token、T=1 提升 0.47 token,以约 40× 更少的参数、16× 更低的延迟开销取得比 DSpark 修正更好的效果。博客原话:”选择比预测便宜”(Choosing is cheaper than predicting)——预测需要重写全词表分布,选择只需要在已有的好候选之间做局部决策。

3.2 局部卷积:回看一步,买回十层深度

3.2.1 诊断一:深度有效,但”不加区分”

suffix decay 是 backbone 问题,直觉的解法是加深模型。博客做了深度对照实验(Figure 2,Qwen3-4B,GSM8K,T=0,逐位置 Recall@1):

位置 0 1 2 3 4 5 6
DFlash 3 层 85.21% 79.26% 77.18% 75.75% 73.96% 70.4% 64.97%
DFlash 5 层 85.39% 80.31% 79.39% 78.27% 77.39% 76.03% 72.86%
DFlash 15 层(3× 参数) 86.42% 81.61% 80.68% 80.34% 80.59% 79.66% 78.73%
5 层 + 卷积(+3% 参数) 85.83% 80.94% 79.98% 79.68% 79.73% 79.43% 77.61%

3/5/15 层的模型在首位几乎相同(≈85–86%),越靠后分化越大——深度确实能治块尾。但 10 个额外注意力块换来 3× 参数、15.2% 延迟(均为博客报告值),而”5 层 + 卷积”只加 3% 参数、0.7% 延迟,块尾 recall 就拉近了与 15 层的差距。深度”不加区分”地加工了所有位置,而问题只出在块尾。

3.2.2 诊断二:注意力的”兼职”与热力图的证据

注意力热力图(32 头 × 5 层)揭示了更深的原因:DFlash 的注意力同时承担两个任务——读取块前上下文、建模块内依赖。按层平均,块内注意力占比(该头注意力权重中分配给块内位置的比例)从 Layer 1 的约 30% 一路衰减到 Layer 5 的约 8%;按单头看,层内分布从”多头分摊”变为”少数头专精”——Layer 1 已有多个头高达 53.1%、64.3%、60.1%,到 Layer 5 绝大多数头接近 0%,只剩个别头 76.0%、60.2%、29.9%。

结论:让通用注意力算子长期兼任”块内短程工作”是浪费——把块内依赖拆给专用模块,让注意力回归上下文读取

3.2.3 设计:双抽头动态深度卷积

DFlash 2 在每个 attention 与 MLP 子层前后各插一个双抽头(two-tap)动态深度卷积(depthwise,逐通道卷积,与模型层数无关;”动态”指核系数随输入变化;GroupedDynamicCausalConvdflash/model.py:478-512):

\[\text{Conv}_k(x)_t = k_{t,0} \odot x_t + k_{t,1} \odot x_{t-1}\]
  • 每个位置 t 的输出 = 自身表示 × 核 $k_{t,0}$ + 前驱表示 × 核 $k_{t,1}$;
  • 核系数 = 学习到的基核 + 由当前隐藏状态推出的微小修正kernel_projection 线性层输出),每 16 个通道共享一个修正(group_size);
  • 块输入以最后已验证的 token(锚点)开头,因此草稿第一位读取锚点表示、后续每位读取前驱表示,所有位置仍完全并行;
  • 块局部、无状态(PyTorch 实现以零填充处理块首边界,dflash/model.py:485;MLX 后端则保留一个跨块的状态缓存):不触碰 attention、LM head 与验证协议,架构层面可即插即用地叠加到现有 DFlash 模型上(服务引擎侧的适配成本见 4.4)。

实现上,卷积被挂进解码层的前后两段(prepare 在前、finish 在后,Qwen3DFlashDecoderLayerdflash/model.py:423-475);DFlash 2 构造时给每层注入 attention_convmlp_convDFlash2DraftModel.__init__dflash/model.py:639-651)。

3.2.4 效果:回看一步就够

+16.5M 参数(3%)、+0.7% 延迟,Layer 4–5 的平均块内注意力从 9.4% 降到 0.5%——卷积确实吸收了局部工作,注意力回归上下文读取;块尾 Recall@1 从 72.86% 拉到 77.61%,逼近 15 层模型的 78.73%。结论正如博客所说:”回看一步的核,就买回了十层深度的大部分收益:suffix decay 主要是局部问题。”

3.3 合体:平均接受长度的全面领先

两个模块合起来,Table 3 给出 Qwen3.5-4B 在五个数据集(另附均值行)上的平均接受长度(thinking 开启,temperature 1.0,top-p 0.95,top-k 20,presence penalty 1.5,无损 rejection sampling)。口径说明:所有接受长度都含验证器补采的 bonus token,即每次验证实际产出的 token 数(与博客 Table 1 脚注口径一致),并不全是草稿的贡献。另外,top-k/top-p 采样参数在草稿侧与验证侧对称生效——两侧分布都经 _sampling_probs 截断(dflash/model.py:262-264 与 278 行),草稿侧的 top-k=20 与选择器的 top-16 候选是两层独立的截断。

数据集 MTP DFlash DSpark DFlash 2
GSM8K 4.78 4.99 5.69 6.20
MATH-500 5.04 5.42 6.20 6.76
HumanEval 4.84 5.43 5.80 6.28
MBPP 4.16 4.49 4.96 5.41
MT-Bench 3.90 4.26 4.77 5.20
均值 4.54 4.92 5.49 5.97

五个数据集全部领先:比 DFlash 平均多 1.05 token(+21%),比 DSpark 多 0.48,比 MTP 多 1.43。而选择器 + 卷积合计只增加约 1.3% 的 cycle 级延迟(草稿—验证循环的耗时,不等于端到端延迟;实际收益以”每次验证多产出”的形式叠加在验证吞吐上)——各基准整体提升 16–25%。

逐位置条件接受率曲线(Figure 5,MATH-500,位置 0–14)印证了这一点:三条基线(MTP / DFlash / DSpark)从 84–88% 一路滑落到 77–80%,而 DFlash 2 从 88.3% 起步,稳定在 85–86.5% 区间直到块尾(末位 86.48%)——所有基线在块尾比 DFlash 2 低 6–9 个百分点,suffix decay 曲线被压平了。

3.4 生产落地:模型、引擎与吞吐

DFlash 2 随博客同步发布两个 drafter:incoai/Qwen3.8-27B-DFlash2incoai/Muse-Glimmer-30B-DFlash2。在默认采样配置下,两者都击败了各自生态里的最佳基线(Table 4/5,均值):

  • Qwen3.8-27B(block size 8):原生 MTP 4.28 / DSpark 3.62 / DFlash 2 4.80
  • Muse Glimmer(block size 16):官方 DFlash 4.44 / DSpark 4.48 / DFlash 2 5.70——比 DSpark 平均领先超过一个完整 token。

换算成端到端吞吐(batch size 1):Qwen3.8-27B 为自回归解码的 2.7–3.4×,Muse Glimmer 为 3.1–4.6×。博客的总结是:”以约自回归 1/3 的每 token 算力,接近 3× 的速度解码,输出不变。”

值得对照的是块长与收益的关系:同样来自官方基准,8 长度的块平均接受 4.80 token(Qwen3.8-27B),16 长度的块平均接受 5.70 token(Muse Glimmer,模型与块长同时不同,仅供趋势参考)——接受长度的增幅远小于块长翻倍,这呼应了 4.3 对”块长加大后收益曲线是否平坦”的疑问。

部署者可直接照抄以下配置,其余读者可跳过本节细节。四个引擎的接入方式各有配置(README 与博客):SGLang 用 --speculative-algorithm DFLASH + 8 个草稿 token;vLLM 在 speculative config 中设 "method": "dflash""num_speculative_tokens": 7;llama.cpp 用 --spec-type draft-dflash--spec-draft-n-max 7(GGUF Q4_K_M);oMLX(Apple Silicon)需启用 DFlash、设置 draft 量化、runtime block size 5、verify mode dflash

官方仓库同时提供本地推理路径:PyTorch 后端(dflash/model.py)与 MLX 后端(dflash/model_mlx.py,Apple Silicon),统一的 dflash generate / dflash benchmark CLI 覆盖 transformers、mlx、openai 三种模式。DFlash 1 与 DFlash 2 在同一份代码里共存——DFlash2DraftModel 继承 DFlashDraftModel,只多出卷积与选择器(dflash/model.py:630-651)。


第四章 权衡:收益的边界与工程的代价

任何加速方案都有边界。DFlash 2 的收益建立在两个明确假设之上——连贯性是局部的、块尾问题是局部的;而它的工程代价,藏在”无损”两字的实现细节里。

4.1 算力账本:1.3% 延迟换 20%+ 产出

选择器与卷积都是”加法”式模块:不动验证路径、不动目标模型,只提高每次验证的期望接受长度。1.3% 的 cycle 级延迟(草稿—验证循环的耗时)是它们几乎全部的开销。这与 MTP(需要改训练目标)和 DSpark(顺序头 + 全词表重写)不同——DFlash 2 的模块可以叠加在现有 DFlash 管线上,代价是 drafter 需要以新模块重新训练/蒸馏(官方同时发布两套新 drafter 正是为此)。

4.2 无损性的前提:分布必须被精确记账

“输出可证明不变”不是免费的。采样模式下,草稿分布 q 必须被精确记录并参与 rejection sampling——选择器把”选中索引的分布”随结果一起传回(draft_indices 路径),拒绝采样才能按索引还原真实的草稿分布(即 3.1.3 的”子集 → 全词表”记账闭环)。任何近似采样(比如忽略选择器分布、直接拿 unary 分布充数)都会破坏可证明性。贪心模式(temperature = 0)则完全绕开分布记账:验证退化为逐位 argmax 前缀比对,草稿 token 与目标逐位 argmax 相同才接受(dflash/model.py:289-292),输出与目标贪心解码逐 token 一致。另外,实现上残差被减尽或浮点下溢时,_rejection_sample 会回退到直接按目标分布 p 采样(dflash/model.py:119-123),保证任何数值情形下都有合法输出——这是”可证明无损”的工程兜底。

4.3 设计假设的边界

  • 局部性假设:双抽头卷积只建模相邻依赖,选择器只对相邻位置对打分。块内长程结构仍依赖注意力与上下文注入——块长加大后收益曲线是否平坦,需要数据验证(3.4 的块长对照已给出趋势线索);
  • 超参数敏感性:top-16 候选、256 维 codebook、16 通道分组都是实践调出来的配置,依赖词表大小与目标模型的分布特性,换模型族需要重新蒸馏 drafter;
  • 硬件约束:MLX 量化后端的量化 matmul kernel 在验证宽度增大时效率下降,实践要求 block size ≤ 5(README 明示)——量化场景下需要缩小块以换取吞吐,与 DFlash 2 在 8–16 长度上展示的收益之间存在张力。

4.4 工程现实:无损加速的隐性成本

  • 引擎侧重写验证路径:四个引擎都要重写验证路径,以支持”候选 + 索引分布”式的拒绝采样(SGLang、vLLM 各自经 PR 合入,需要特定版本);
  • 权重加载的特殊处理:选择器的 codebook 是 vocab_size × rank 的嵌入表,检查点命名与常规层不同,需要 key_mapping 适配(DFlash2DraftModel.from_pretraineddflash/model.py:632-637);
  • drafter 不是通用组件:草稿模型必须针对目标模型蒸馏/微调,官方同时维护 Qwen3.8-27B 与 Muse Glimmer 两套 drafter,正是这个事实的注脚。

第五章 结语:推理的下限远未触及

七个月,DFlash 从一篇论文变成行业标准:四大引擎内置、350 万+ 下载、多家模型厂商随附官方 drafter。DFlash 2 在同一个设计内继续压榨并行草稿的余量:选择器把”候选对而选错”的损失找回来,卷积把”块尾表达不足”的损失找回来——两者合计,每次验证多免费解码一个完整 token,而验证流程与目标模型分毫未动。

回看整条脉络,DFlash 1 到 DFlash 2 的演进,本质上是同一个问题的两次逼近:如何在不破坏并行性的前提下,逼近自回归的连贯性。第一次用块扩散把草稿从串行变成并行,第二次用选择器与卷积把并行的质量补齐。它仍是 serving 栈中的一个组件——正如博客所言,推理远未触及下限。


代码引用说明:文中 dflash/model.py(PyTorch)与 dflash/model_mlx.py(MLX)的行号均指向 github.com/z-lab/dflash 当前 main 分支;对代码细节不感兴趣的读者可完全跳过这些引用。


参考资料