DSpark:半自回归 + 置信度调度的 Speculative Decoding 深度分析

Speculative decoding 的 drafter 架构正在经历一次范式转移。DFlash 用 block diffusion 把 drafting 从串行变并行,实现了 6× 加速;DSpark 在此基础上补了两刀——半自回归解决并行生成的后缀衰减,置信度调度解决高并发下的验证浪费。本文围绕这两篇论文,结合源码逐行分析,澄清训练注意力结构中的常见困惑,并讨论其架构设计、核心 trade-off 和工程落地。


一、背景:从串行到并行的 Drafter

Speculative decoding 的加速比为 η=Ltarget/L\eta = L_{\text{target}} / L,其中每个 cycle 的 per-token 延迟为 L=(Tdraft+Tverify)/τL = (T_{\text{draft}} + T_{\text{verify}}) / \tauτ\tau 是每个 cycle 期望接受的 token 数。

Autoregressive drafter 和 diffusion drafter 的最大区别在于 drafting 的计算方式。自回归 drafter 一个 token 一个 token 地算,Tdraft=γtstepT_{\text{draft}} = \gamma \cdot t_{\text{step}} 与 block size 线性增长。为了控制延迟,只能用极浅架构(Eagle3 仅 1 层 transformer),τ\tau 很快饱和,加速比卡在 23×\sim 2{-}3\times。Diffusion drafter 一次并行算出整个 block 的 token,TdraftT_{\text{draft}}γ\gamma 基本不敏感,因此可以用更深的网络获得更高的 τ\tau

DFlash 就是这样一个并行 diffusion drafter。它的关键设计是 KV injection:从目标模型提取 hidden context features,注入到 draft 模型每一层的 Key-Value cache 中,让 draft 模型利用目标模型的深度表征来做条件预测,而不是从头猜。但纯并行生成引入了新问题:block 内 token 之间没有依赖建模


二、DFlash:用 Diffusion 做 Drafter

2.1 核心思路

DFlash 的核心 insight 很简单:目标模型知道未来

大型自回归模型的 hidden states 隐含了多个未来 token 的信息。DFlash 不让小模型从头猜,而是把目标模型的 hidden features 作为条件,让 draft 模型变成一个"扩散适配器"——利用目标模型的深度表征来并行预测未来 block。

2.2 “Diffusion” 到底在哪?

DFlash 名字里有个 D,但翻遍代码你会发现一个事实:没有多步去噪,没有噪声调度,没有连续时间 SDE。所谓的 diffusion 只体现在两件事上:

  1. Mask token 构造:待预测位置填充为 mask token,类似于 BERT 的 [MASK],作为"全噪"起点
  2. 双向注意力is_causal=False):block 内 token 互相可见,一次 forward 出所有位置

就这两点,没有迭代去噪。标准 block diffusion(Arriola et al., 2025)还有多步迭代,DFlash 把它压成了单步。传承链条是这样的:

1
2
3
连续扩散 (LLaDA)  ->  Block 级离散扩散 (BlockDiff)  ->  单步 mask-predict (DFlash)
高斯噪声 多步迭代去噪 一步出结果
连续时间 SDE 离散 mask token BERT-style

每一步都在往"更像自回归、更不像 diffusion"的方向走。DFlash 到了极致——名字叫 diffusion,实质是 parallel mask prediction。论文用 “diffusion” 这个词主要是学术传承定位,不是方法描述。

那 mask token 的作用是什么?模型需要知道哪些位置是待预测的,哪些是已知信息。如果不用 mask token,直接放随机 embedding 进去,模型会把这些位置当成已知输入去做 attention。Mask token 是一个学习到的"我不知道"信号,跟 BERT 的 [MASK] 一回事。

2.3 KV Injection:不是输入融合,是每层注入

Eagle3 也用目标模型的 hidden features,但只在输入层融合,随着 draft 模型变深,目标信息逐渐稀释。DFlash 采用了完全不同的策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
目标模型 hidden states (5层)

│ concat + 线性投影 W_c (5*hidden -> hidden)

H_ctx = RMSNorm(W_c [H(l1); ...; H(l5)]) ← 压缩后的上下文特征

│ 注入到 draft 模型每一层的 KV cache

Draft Layer 1: K = [W^K · H_ctx; W^K · H_d] ← 目标特征 + draft 特征拼接
V = [W^V · H_ctx; W^V · H_d]

Draft Layer 2: 同上(H_ctx 共享)

...

Draft Layer 5: 同上

关键区别:目标特征作为额外的 KV entry 直接注入每一层,而不是经过 draft 模型的 Q projection、output projection 和 FFN。这意味着目标信息在每一层都是"常驻"的,不会因深度而稀释。

从源码看(dflash/model.py),KV 注入的实现极其直接:

1
2
3
4
5
6
# 每个 draft decoder 层的 attention 中
k_ctx = self.k_proj(target_hidden) # KV 注入:target context
k_noise = self.k_proj(hidden_states) # draft 自身的 K/V
k = torch.cat([k_ctx, k_noise], dim=1) # 拼接 K
v = torch.cat([v_ctx, v_noise], dim=1) # 拼接 V
# is_causal = False ← 双向注意力,block 内 token 互相可见

所有层共享同一份 target_hidden(经 fc + RMSNorm 投影后),且 attention 设为 is_causal=False——block 内 token 双向可见,这是并行扩散生成的必要条件。

一句话总结:KV injection 把目标模型的 hidden features 变成 draft 模型每一层的"持久上下文",让深层 draft 模型也能充分利用目标模型的信息。

2.4 共享 Embedding 和 LM Head:设计意图

在深入推理和训练流程之前,需要先澄清一个贯穿两篇论文的基础设计:共享 embedding 和 LM head

DFlash 论文明确阐述了这一设计的动机:

“To improve training efficiency, the draft model shares the token embedding layer and language modeling head with the target model and keeps them frozen during training. Only the draft Transformer layers are updated. This design reduces the number of trainable parameters and encourages the draft model to function as a lightweight diffusion adapter tightly aligned with the target model’s representation space.”

这里的"target model"就是你想加速的那个大模型——最终产出正确 token 的 LLM(如 Qwen3-4B、DeepSeek-V4)。target.lm_head 不是什么特殊构造,它就是目标模型自带的最后一个线性层——把 hidden state 映射到词汇表 logits 的那一层。

以 Qwen3-4B 为例:

  • embed_tokensvocab_size(151936) × hidden_size(2560) ≈ 390M 参数
  • lm_headhidden_size(2560) × vocab_size(151936) ≈ 390M 参数
  • 两者合计占 Qwen3-4B 总参数(4B)的约 20%

关键词是 “lightweight diffusion adapter”——共享 + 冻结 embed/lm_head 的本质目的不是省参数,而是强制 draft 模型在目标模型的表征空间内工作embed_tokens 决定输入空间,lm_head 决定输出空间,两者都锁定后,draft 模型只能学习"如何把目标模型的 hidden states 转换成未来 token 的预测",而不能自己学一套独立的表征。这正是 KV injection 设计的配套——KV injection 让目标模型的信息每层注入,共享 embed/lm_head 让 draft 的输入输出空间与目标对齐,两者合在一起确保 draft 是一个纯粹的"适配器"而非独立模型。

两份源码的共享方式不同

  • DFlash(推理时直接借用)DFlashDraftModel 类本身不持有 embed_tokenslm_head 模块。在 dflash_generate() 函数中直接调用 target 对象的属性:

    1
    2
    3
    # model.py 第111-112行 - 直接调用,不存副本
    noise_embedding = target.model.embed_tokens(block_output_ids)
    draft_logits = target.lm_head(model(...))
  • DSpark(训练时复制 + 冻结):draft 模型有自己的 embed_tokenslm_head 模块(modeling.py 第 227-246 行定义),初始化时把 target 的权重逐字节复制过来然后冻结:

    1
    2
    3
    4
    5
    6
    def initialize_embeddings_and_head(self, *, embed_tokens, lm_head, freeze=True):
    with torch.no_grad():
    self.embed_tokens.weight.copy_(embed_tokens.weight.detach())
    self.lm_head.weight.copy_(lm_head.weight.detach())
    if freeze:
    self.set_embedding_head_trainable(False) # requires_grad=False

    训练时必须用独立模块供 PyTorch autograd 走完整前向传播;推理时则像 DFlash 一样直接调用 target 的 lm_head。

2.5 推理流程:极简实现

DFlash 的仓库极其精简(4 个 Python 文件,核心逻辑 ~370 行)。推理主循环 dflash_generate() 的核心步骤:

1
2
3
4
5
6
7
8
9
while not done:
① 构造 [prev_token, mask, mask, ..., mask] block
② Draft 前向:单次并行生成整个 block 的 logits
- noise_embedding = target.model.embed_tokens(block_output_ids) # 直接用 target 的 embed
- draft_logits = target.lm_head(draft_model(...)) # 直接用 target 的 lm_head
③ 采样 draft tokens
④ 目标模型单次 forward 验证整个 block
⑤ 计算 accept_length(cumprod 找到第一个 reject 的位置)
⑥ 裁剪 draft 和 target 的 KV cache 到接受位置

注意:draft 模型在推理时直接使用目标模型的 embed_tokenslm_head(通过传入的 target 对象直接访问),自己只持有 5 个 decoder 层。block_size=1 时退化为普通自回归解码(用于 baseline 对比)。

重要说明:DFlash 仓库只包含推理代码,训练 recipe 尚未开源。DSpark 在 DFlash 架构基础上增加了独立的训练 pipeline,完整实现在 DeepSpec 仓库中。两者不是共享同一套训练框架——DSpark 的训练代码是独立开发的,包含了 Markov head、confidence head、anchor sampling 等 DSpark 特有组件。

2.6 并行扩散 drafting

DFlash 用 block diffusion 一次生成 γ\gamma 个 token:

TdraftDFlash=tparallel(与 γ 无关)T_{\text{draft}}^{\text{DFlash}} = t_{\text{parallel}} \quad (\text{与 } \gamma \text{ 无关})

这意味着 draft 模型可以用更深的架构(5 层 vs Eagle3 的 1 层),而不会让 drafting 延迟失控。实验显示,5 层 DFlash 生成 16 个 token 的延迟,低于 1 层 Eagle3 生成 8 个 token 的延迟。

需要注意的是,历史 context 的 KV cache 仍然是 causal 的。目标模型的 forward pass 是标准 causal attention,产出的 hidden states 已经编码了"只能看前面"的因果历史。DFlash 通过 KV injection 把这些 hidden states 注入到 draft 模型,所以 draft block 整体的 attention pattern 是:

1
2
3
4
5
[历史 context(causal,来自目标模型 KV 注入)]  [draft block(bidirectional)]

mask tokens 互相可见
但都只能看到历史 context
看不到"未来"

2.7 训练设计

DFlash 的训练 recipe 未开源。以下分析基于 DSpark 论文和 DeepSpec 仓库源码,两者的训练设计在 backbone 层面一致(KV injection、共享 embed/lm_head、anchor sampling 等核心机制相同),DSpark 额外增加了 Markov head 和 confidence head 的训练。

2.7.1 序列布局:拼接式而非交错式

训练时的输入序列布局是 concatenated(拼接式),不是 interleaved(交错式):

1
2
[ context: p1 p2 p3 r1 r2 r3 r4 r5 ]  [ draft: B0 B1 B2 ... ]
← 拼接在 context 之后
  • Context 部分:完整的训练样本 [prompt | response],全部是 ground truth token。目标模型对这段序列做一次 forward,提取 5 个中间层的 hidden states 作为 KV injection 来源。
  • Draft 部分:512 个 block 拼接而成,每个 block 是 [anchor, mask, mask, ..., mask],block_size=7。

2.7.2 随机 Anchor 采样

不从 response 均匀分块,而是随机采样 anchor 位置作为每个 block 的起点。源码(common.py 第 164 行)显示采样后会 .sort() 排序:

1
anchors = gathered[:, :max_n].sort(dim=1).values  # 随机采样后排序

排序后 block 按位置从小到大排列,每个 block 的 anchor 是一个真实的 ground truth token(teacher-forced),紧跟 block_size - 1 个 mask token。一次训练前向传播同时覆盖 512 个位置。

2.7.3 Flex Attention:用函数描述稀疏注意力模式

DSpark 的注意力模式高度稀疏——每个 7-token block 只能看到 anchor 之前的 context + 自己 block 的 7 个 token。用稠密矩阵(Q_LEN × KV_LEN 布尔矩阵)太浪费。

PyTorch 2.5+ 的 flex_attention API 提供了解法:用函数描述注意力模式,而不是构造稠密矩阵。

流程

  1. 提供一个 mask_mod(b, h, q_idx, kv_idx) -> bool 函数,告诉它"query 位置 q 能不能看到 key 位置 k"
  2. create_block_mask() 把这个函数编译成块稀疏格式——把整个矩阵切成小块(比如 128×128),只保留含 True 的块
  3. 实际 attention 计算时跳过全 False 的块,只算有内容的块

源码核心(common.py 第 86-96 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
def dspark_mask_mod(b, h, q_idx, kv_idx):
q_block_id = q_idx // block_size
anchor_pos = anchor_positions[b, q_block_id]

is_context = kv_idx < seq_len
mask_context = is_context & (kv_idx < anchor_pos) # 只看 anchor 之前的 context

is_draft = kv_idx >= seq_len
kv_block_id = (kv_idx - seq_len) // block_size
mask_draft = is_draft & (q_block_id == kv_block_id) # 只看同一个 block

is_valid_block = block_keep_mask[b, q_block_id]
return (mask_context | mask_draft) & is_valid_block

2.7.4 Attention Mask 的两条规则

整个注意力可见性只有两条规则:

  1. Block 之间互不可见q_block_id == kv_block_id)——不同 block 的 draft token 完全隔离,双向的
  2. Anchor 之前的前缀可见kv_idx < anchor_pos)——context 中 anchor 位置之前的 token 可见

这两条规则产生了注意力矩阵中的阶梯(staircase)结构。

2.7.5 Invisible Tokens:到底是什么

论文训练图中的"白色 = invisible tokens"让人困惑。Invisible tokens 分两类:

Invisible 类型 条件 原因
Context 中 anchor 位置及之后的 token kv_idx >= anchor_poskv_idx < seq_len 因果一致性:这些是 draft 要预测的答案,看了就是 data leakage
其他 block 的 draft token q_block_id != kv_block_id 块间隔离:防止不同 block 之间的梯度互相干扰

关键澄清:Context 边界是 token 级别的,不是 block 级别的

这是理解训练图最容易混淆的地方。看注意力矩阵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
         Context (causal)                    Draft blocks (bidirectional)
c0 c1 c2 c3 c4 c5 c6 c7 B0(a m m) B1(a m m) B2(a m m)
B0 ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗
B0 ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗
B0 ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗

B1 ✓ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓ ✗ ✗ ✗
B1 ✓ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓ ✗ ✗ ✗
B1 ✓ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓ ✗ ✗ ✗

B2 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓
B2 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓
B2 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓
↑ ↑ ↑
anchor=2 anchor=4 anchor=6
(B0的边界) (B1的边界) (B2的边界)
  • B0(anchor=2):只看到 context [c0, c1]——2 个 token
  • B1(anchor=4):看到 context [c0, c1, c2, c3]——4 个 token(包含 B0 看到的 + 更多)
  • B2(anchor=6):看到 context [c0, c1, c2, c3, c4, c5]——6 个 token

蓝色(可见 context)形成一个阶梯。阶梯的每一级台阶在 anchor 位置(2、4、6),是单个 token 的位置。

为什么 context 看起来也按 block 切了?

这是视觉错觉。两个相邻 anchor 之间的 context 段(比如 [c2, c3])对 B0 不可见、对 B1 和 B2 可见,在图里看起来像一个"块"。但边界是随机 anchor 的 token 位置,不是固定的 block 边界。如果 anchor 随机采到位置 1、4、9,分段就完全不同。

为什么 anchor 之后的不看?

训练时虽然完整序列都在手里,但必须用 mask 模拟推理条件。推理时 draft 模型只能看到 anchor 之前的 token(因为后面的还没生成),所以训练时也必须只让它看 [0, anchor_pos)。这和标准自回归训练的 causal mask 完全同理——你有完整序列,但人为限制可见性防止作弊,只是这里"未来"的定义从"当前位置之后"变成了"anchor 位置之后"。

每个 block 内所有 token 共享同一个 anchor_pos,所以它们看到的 context 前缀完全一样。在注意力矩阵里,这表现为同一 block 的所有行在 context 区域的可见性模式完全一致——画出来就是一个矩形块,视觉上像是 context 也按 block 对齐了。但决定可见/不可见边界的是 anchor_pos 这一个整数,是 token 级别的。

2.7.6 KV Injection 在训练中的结构

训练时的 KV injection 和推理时完全一致——目标模型的 hidden states 经过 fc(5×hidden → hidden) + RMSNorm 投影后,作为额外的 K/V entry 注入到 draft 模型每一层:

1
2
3
每层 attention 的 K/V 拼接:
K = [k_proj(target_hidden) ; k_proj(draft_hidden)] ← 两部分拼接
V = [v_proj(target_hidden) ; v_proj(draft_hidden)]

目标特征绕过 Q projection、output projection、FFN,直接作为 KV entry 进入 attention。所有层共享同一份投影后的 target_hidden。

这里的"KV"不是推理时增量生成的 KV cache,而是指 KV injection 的结构——目标模型的 hidden states 作为"常驻 KV"注入每一层。

2.7.7 指数衰减位置加权

wk=exp(k1γ)w_k = \exp\left(-\frac{k-1}{\gamma}\right)

Speculative decoding 中,早期 token 的错误会级联失效整个 block 后缀。loss 加权反映了这种不对称性——前面的 token 更重要。

2.7.8 训练 vs 推理

维度 训练 推理
Anchor Ground truth token(teacher-forced) 目标模型上一步的 bonus token
Block 数量 512 个 block 一次 forward 一次一个 block
KV injection 与推理一致(每层注入) 同左
Attention mask Flex attention block mask 标准 bidirectional
串行 head(DSpark) Teacher-forced,所有位置并行计算 Autoregressive,逐 token 串行

2.8 结果与局限

DFlash 在 Qwen3-8B 上实现 6.1× 加速,比 Eagle3 快 2.5×。但存在两个结构性局限:

  1. 后缀衰减:纯并行生成无法建模 block 内依赖。当上下文有多个合理续写(如 “of course” vs “no problem”)时,各位置独立预测可能产生不一致的组合(“of problem”)
  2. 验证浪费:所有 draft token 都送去验证,高并发场景下低置信度的后缀 token 占用 batch 容量

DSpark 正是来解决这两个问题的。


三、DSpark:半自回归 + 置信度调度

3.1 整体架构

DSpark = DFlash backbone + 轻量串行 head + 置信度 head + 硬件感知调度器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
目标模型生成 bonus token (anchor)


┌─────────────────────────────────────┐
│ 并行 backbone (DFlash) │
│ 输入: anchor + (γ-1) mask tokens │
│ 输出: hidden h_1..h_γ, base logits │
└──────────┬──────────────────────────┘

┌──────┴──────┐
│ │
▼ ▼
┌────────┐ ┌──────────────┐
│串行 head│ │置信度 head │
│B_k(·) │ │c_k = σ(w·h) │
└───┬────┘ └──────┬───────┘
│ │
▼ ▼
采样 x_k prefix survival
(条件于 概率估计
x_<k)
│ │
▼ ▼
draft tokens ┌──────────────┐
E F G H │硬件感知调度器 │
│ 截断低置信后缀 │
└──────┬───────┘


目标模型验证
E F G (H 被砍掉)

3.2 半自回归生成:解决后缀衰减

问题本质

并行 drafter 在每个位置独立预测,相当于对前缀所有可能的 token 做 marginal 平均。当上下文存在多个合理续写路径时,不同位置可能选到不同路径的 token,产生不连贯的组合。

以论文中的例子:上下文允许 “of course” 和 “no problem” 两种续写。并行 drafter 在位置 1 独立采样得到 “of”,位置 2 仍然不知道位置 1 选了什么,可能选 “problem” 而非 “course”。这就是多模态碰撞(multi-modal collision)

解法:并行 backbone + 串行 head

DSpark 把生成拆成两个阶段:

并行阶段:DFlash backbone 一次 forward 产生所有位置的 hidden states h1,,hγh_1, \ldots, h_\gamma 和 base logits U1,,UγU_1, \ldots, U_\gamma

串行阶段:在 base logits 上叠加一个 transition bias BkB_k,逐 token 左到右采样:

pk(vx0,x<k)=exp(Uk(v)+Bk(x0,x<k,v))uexp(Uk(u)+Bk(x0,x<k,u))p_k(v | x_0, x_{<k}) = \frac{\exp(U_k(v) + B_k(x_0, x_{<k}, v))}{\sum_{u} \exp(U_k(u) + B_k(x_0, x_{<k}, u))}

关键在于 BkB_k 条件于前面已采样的 token,解决了独立预测的问题。一旦位置 1 采样了 “of”,串行 head 在位置 2 boost “course” 并 suppress “problem”。

三种串行 Head 实现

源码在 markov_head.py 中实现了三种变体,复杂度递增:

类型 参数 机制 自回归程度
VanillaMarkov markov_w1(Embed) + markov_w2(Linear) logits+=W2[W1[xk1]]\text{logits} += W_2[W_1[x_{k-1}]] 最轻,仅依赖前一 token
GatedMarkovHead + gate_proj(Linear) logits+=W2[gate(h+emb)emb]\text{logits} += W_2[\text{gate}(h+\text{emb}) \cdot \text{emb}] 门控混合 draft hidden
RNNHead + joint_proj(Linear) GRU-like state 跨位置传播 最强,维护整个前缀历史

Markov head(默认):BkB_k 只依赖前一个 token,低秩分解 B=W1W2B = W_1 W_2W1RV×rW_1 \in \mathbb{R}^{V \times r}W2Rr×VW_2 \in \mathbb{R}^{r \times V}r=256r=256

1
2
3
4
给定前一个 token x_{k-1}:
B(x_{k-1}, ·) = W_1[x_{k-1}] · W_2 ← 查表 + 矩阵乘,O(r) 复杂度
p_k(·) = softmax(U_k + B(x_{k-1}, ·))
采样 x_k ~ p_k

RNN head 比 Markov head 略好但实现更复杂,收益有限(论文 Figure 4 显示差距很小),生产默认用 Markov head。

训练 vs 推理:一个关键区别

训练时是 teacher-forced,可以并行计算。用 ground truth token ids 作为前缀输入,所有位置的 bias 一次算完,不需要串行循环:

1
2
3
4
5
训练(teacher-forced,并行):
位置1: bias = W_1[anchor] · W_2 ← 用 ground truth 的 anchor
位置2: bias = W_1[gt_token_1] · W_2 ← 用 ground truth 的 token_1
位置3: bias = W_1[gt_token_2] · W_2 ← 用 ground truth 的 token_2
所有位置一次 forward 算完

推理时必须逐 token 串行,因为位置 k 的 bias 依赖位置 k-1 实际采样出来的 token,不是 ground truth:

1
2
3
4
5
6
推理(autoregressive,串行):
位置1: bias = W_1[anchor] · W_2
采样 x_1 ~ softmax(U_1 + bias) ← 这步必须先完成
位置2: bias = W_1[x_1] · W_2 ← 用上一步采样的 x_1
采样 x_2 ~ softmax(U_2 + bias)
...

因为每步只是一个 embedding 查表 + 低秩矩阵乘,非常轻量,在 batch size 128 的生产环境下延迟开销只有 ~1%。这也是 DSpark 叫"半自回归"的原因——backbone 是并行的,串行 head 是自回归的。

一句话总结:半自回归 = 并行 backbone 出 base logits + 轻量串行 head 加条件 bias,用 1% 的延迟代价换来 16-18% 的接受长度提升。

3.3 位置分析:为什么并行能赢自回归?

DSpark 论文里最精彩的实验是位置层面的条件接受率分析(Figure 2)。这个分析揭示了一个反直觉的结论。

反直觉:并行 drafter 在位置 1 显著优于自回归 drafter

原因纯粹是架构容量:自回归 drafter 受限于 TdraftγT_{\text{draft}} \propto \gamma,只能用 1 层 transformer;并行 drafter 延迟与 γ\gamma 无关,可以用 5 层甚至 8 层。更深的网络在位置 1(只依赖目标模型上下文,不需要 block 内依赖)自然更准。

论文 Figure 2 按不同 domain(Math、Code、Chat)分别给出了位置接受率曲线。大致趋势是:

1
2
3
4
位置:    1     2     3     4     5     6     7
DFlash: 高位 ↘ ↘ ↘ ↘ ↘ ↘ ← 缓慢但持续衰减
Eagle3: 低位 ↗ ↗ ↗ ↗ ↗ ↗ ← 反而上升(利用已采样的前缀)
DSpark: 最高 ↘ ↘ ↘ ↘ ↘ ↘ ← 高位稳定衰减

Eagle3 在后续位置反超 DFlash,因为自回归模型条件于已采样 token,随着前缀确定,后续预测变得更容易。DFlash 在后续位置仍然是独立预测,持续衰减。

DSpark 两头兼得:位置 1 继承并行 backbone 的高容量优势,后续位置由串行 head 维持稳定。

注意:以上趋势基于论文 Figure 2 的定性描述。具体数值请参阅论文原图,不同 domain(Math/Code/Chat)的绝对值差异较大。

为什么位置 1 最重要?

Speculative decoding 是严格的 prefix-matching:位置 1 被 reject,整个 block 作废。位置 1 的接受率对整体 τ\tau 的杠杆最大。

这个分析也解释了为什么 DFlash 能比 Eagle3 快 2.5×——不是因为并行生成更快(虽然确实更快),而是因为更深的网络在位置 1 的优势被 prefix-matching 机制放大了。

3.4 置信度调度验证:从固定长度到自适应

问题:高并发下的验证浪费

DFlash 和 Eagle3 都用固定长度验证:draft 模型生成 γ\gamma 个 token,全部送去目标模型验证。但在高并发场景下:

  • 每个 extra verification token 都占用目标模型的 batch 容量
  • 低置信度的后缀 token 大概率被 reject,验证它们是纯浪费
  • 被浪费的 batch 容量本可以服务其他请求

解法:Confidence Head + Hardware-Aware Scheduler

Confidence Head 的源码实现极其极简——就是一个单层线性投影:

1
2
3
4
5
6
# eval/dspark/confidence_head.py
class AcceptRatePredictor(nn.Module):
def __init__(self, input_dim: int):
self.proj = nn.Linear(input_dim, 1) # 单层线性投影
def forward(self, features):
return self.proj(features).squeeze(-1)

输入特征是 [hidden_states, markov_prev_embeddings] 拼接,输出经过 sigmoid 后得到每个位置的条件生存概率:

ck=σ(w[hk;W1[xk1]])c_k = \sigma(w^\top [h_k; W_1[x_{k-1}]])

监督信号是解析的 per-step 接受率:ck=112pkdpkt1c_k^* = 1 - \frac{1}{2}\|p_k^d - p_k^t\|_1(TV distance 的补)。训练时用 BCE loss。

推理时的置信度裁剪同样简洁:

1
2
3
4
# draft_ops.py - 找到第一个低于阈值的置信度位置,截断
below_threshold = confidence_logits.sigmoid() < threshold
first_below = torch.nonzero(below_threshold[0])[0].item()
return first_below # 只验证 [0, first_below) 的 token

Sequential Temperature Scaling (STS) 校准:原始 confidence 通常过自信(ECE 3-8%)。STS 逐位置做 1D grid search,最小化累积乘积 ikci\prod_{i \leq k} c_i 的 ECE,校准后 ECE 降到 ~1%。

Hardware-Aware Prefix Scheduler 把验证长度选择形式化为全局吞吐量最大化问题:

Θ=τSPS(B),其中 τ=r=1R(1+j=1rar,j),B=r=1R(1+r)\Theta = \tau \cdot \text{SPS}(B), \quad \text{其中 } \tau = \sum_{r=1}^{R}\left(1 + \sum_{j=1}^{\ell_r} a_{r,j}\right), \quad B = \sum_{r=1}^{R}(1 + \ell_r)

  • SPS(B)\text{SPS}(B):引擎的 steps-per-second 容量曲线,初始化时 profiling 一次
  • ar,j=ijcr,ia_{r,j} = \prod_{i \leq j} c_{r,i}:request rr 在位置 jj 的 prefix survival 概率
  • 目标:选择每个 request 的验证长度 1,,R\ell_1, \ldots, \ell_R,最大化 Θ\Theta

因为 ar,ja_{r,j} 单调递减,可以贪心求解:全局排序所有 (r,j)(r, j)ar,ja_{r,j} 降序,逐个加入验证 batch,直到 Θ\Theta 不再上升。

1
2
负载低 → SPS(B) 几乎不变 → 多验证 token 划算 → 验证长度大
负载高 → SPS(B) 快速下降 → 少验证 token 划算 → 砍掉低置信度后缀

一句话总结:置信度调度把"验证多少"从静态配置变成动态优化问题——根据每个请求的 draft 质量和当前系统负载,全局分配验证算力。

3.5 训练目标

DSpark 的 loss 三项加权和:

L=αceLce+αtvLtv+αconfLconf\mathcal{L} = \alpha_{\text{ce}} \mathcal{L}_{\text{ce}} + \alpha_{\text{tv}} \mathcal{L}_{\text{tv}} + \alpha_{\text{conf}} \mathcal{L}_{\text{conf}}

Loss 项 作用 权重
Lce\mathcal{L}_{\text{ce}} 交叉熵,预测正确 token 0.1
Ltv\mathcal{L}_{\text{tv}} TV distance,匹配目标分布 0.9
Lconf\mathcal{L}_{\text{conf}} BCE,校准置信度预测 1.0

Ltv\mathcal{L}_{\text{tv}} 权重最高,因为 TV distance 直接对应接受率:per-step 接受概率 =112pdpt1= 1 - \frac{1}{2}\|p^d - p^t\|_1,最小化 TV distance 就是最大化期望接受率。


四、工程落地:从论文到 DeepSeek-V4 线上

4.1 生产部署架构

DSpark 部署在 DeepSeek-V4-Flash 和 V4-Pro 上。

配置项 DeepSeek-V4 生产环境 开源 checkpoint(如 dspark_qwen3_4b_block7)
Draft backbone 3 层 MoE + mHC + sliding window attention 128 标准 dense transformer 层
Block size 5 7
串行 head Markov head (r=256) 同左
置信度 head 线性投影 + sigmoid 同左
校准 STS (held-out validation set) 同左
调度器 异步硬件感知 prefix scheduler 仅 Transformers 评估器

注意:生产环境使用 MoE + mHC 架构和 block_size=5;开源 checkpoint 使用标准 dense 层和 block_size=7,便于社区复现。两者核心算法一致,架构配置不同。

4.2 异步调度:解决 ZOS 冲突

算法 1 的同步版本与生产系统的 Zero-Overhead Scheduling (ZOS) 冲突——ZOS 需要在当前 step 完成前知道下一步的 batch size。DSpark 的解法是用两步前的 confidence 预测来确定当前步的截断长度

1
2
3
4
Step N-2:  生成 draft + confidence
Step N-1: 用 N-2 的 confidence 确定截断 → 验证
Step N: 用 N-1 的 confidence 确定截断 → 验证
↑ 同时生成新 draft + confidence(供 N+1 使用)

这引入了轻微的时间偏差,但选择机制是 rank-preserving 的——最自信的 draft token 总是优先验证。更重要的是,异步设计形成了一道"因果屏障":截断决策只依赖历史信息,不会泄露未来 token,保证了 lossless guarantee。

4.3 生产性能

指标 V4-Flash V4-Pro
每用户速度提升(matched throughput) 60%–85% 57%–78%
吞吐提升(moderate SLA) +51% +52%
极端 SLA 下吞吐优势 +661%(baseline 接近崩溃) +406%

关键结论不是倍数本身,而是DSpark 扩展了可行的交互性边界。在 120 TPS/user 的严格 SLA 下,MTP-1 baseline 几乎无法运作,DSpark 仍然稳定——这意味着原来达不到的延迟等级现在可以服务了。


五、两篇论文的对照

维度 DFlash DSpark
Drafter 类型 纯并行 block diffusion 半自回归(并行 + 串行)
Block 内依赖 无建模 Markov/RNN head
验证策略 固定长度全验证 置信度 + 硬件感知自适应
位置 1 优势 深层网络 继承 DFlash backbone
后缀稳定性 快速衰减 串行 head 维持
高并发友好 验证浪费 动态截断
生产验证 SGLang 实验 DeepSeek-V4 线上流量
vs Eagle3 2.5× 更快 τ\tau 再 +16-18%
训练代码开源 未开源 完整 pipeline(DeepSpec)
推理后端 Transformers/SGLang/vLLM/MLX 仅 Transformers

DSpark 不是对 DFlash 的替代,而是增量改进。DFlash 解决了"能不能用 diffusion 做 drafter"的问题,DSpark 解决了"用得好不好"的问题。两者共享 KV injection 条件化、共享 embedding/LM head、位置加权 loss 等核心设计。


六、可迁移的启示

1. "目标模型知道未来"是一个深刻的观察。 大模型的 hidden states 隐含了远超 next-token 的信息。DFlash 的 KV injection 和 DSpark 的串行 head 都在利用这一点——draft 模型不需要从头推理,只需要"解读"目标模型已经知道的东西。

2. 并行 vs 自回归不是二选一。 DSpark 的半自回归架构证明,用并行 backbone 做"重活"+ 串行 head 做"精修",可以在 1% 延迟代价下获得 16-18% 的质量提升。这个思路在更广泛的 LLM 加速领域也适用——不要追求纯并行或纯串行,找正确的分割点。

3. Speculative decoding 是系统问题,不只是算法问题。 DSpark 的置信度调度把验证长度从算法参数变成系统调度参数,在真实流量下实现了负载感知的自适应。这提醒我们:脱离部署环境谈 drafter 架构是不完整的。

4. 位置 1 的杠杆最大。 在 prefix-matching 机制下,位置 1 的接受率对整体 τ\tau 的影响远大于后续位置。这意味着 draft 模型的架构选择应该优先考虑位置 1 的容量,而非后续位置的依赖建模——这正好是并行 drafter 的天然优势。

5. 共享 embed/lm_head 不是省参数,是锁定表征空间。 冻结 embed 和 lm_head 后,draft 模型被强制在目标模型的表征空间内工作,成为纯粹的"适配器"。这是 KV injection 的配套设计——前者保证空间对齐,后者保证信息每层注入。


参考

  • DFlash: Chen et al., “DFlash: Block Diffusion for Flash Speculative Decoding”, ICML 2026. arXiv:2602.06036
  • DSpark: Cheng et al., “DSpark: Confidence-Scheduled Speculative Decoding with Semi-Autoregressive Generation”, 2026. arXiv:2607.05147
  • Eagle3: Li et al., “Eagle-3: Scaling up Inference Acceleration of LLMs via Training-Time Test”, 2025. arXiv:2503.01840

开源仓库

  • DeepSpec(deepseek-ai/DeepSpec):69 个 Python 文件,包含 Eagle3、DFlash backbone、DSpark 三种 drafter 的统一训练框架,支持 Qwen3 和 Gemma4 系列目标模型。完整训练 pipeline(数据准备 → 训练 → 评估)。
  • DFlash(z-lab/dflash):4 个 Python 文件,仅含推理代码(Transformers/SGLang/vLLM/MLX 四种后端),训练 recipe 尚未开源。