模型: Qwen3.5-9B
前置阅读: Speculative Decoding · EAGLE 系列 · MTP · Qwen3.5 架构

一句话总结: Qwen3.5 的 3+1 混合架构让树形投机解码必须同时处理两种验证机制——Full Attention 层用 tree attention mask 并行验证,Gated DeltaNet 层用 fork/checkpoint 逐深度处理。性能开销 <2%,内存是真正的代价。

一、为什么要做树形验证

经典投机解码的链式 draft-verify 一次只验证一条路径:

1
2
链式草稿:  I → make → a → plan
猜错任何一个就中断,后面全部作废

如果第 2 个 token “make” 被拒绝,后面的 “a”、”plan” 即使猜对了也没用——因为它们的前提已经不成立了。

树形验证的思路是:在每个位置保留 top-k 个候选,形成一棵 draft tree,一次 forward 验证所有路径

1
2
3
4
5
6
7
树状草稿:        I
/ \
make help
/ | |
a out you

一条路被拒,还有其他分支可以接受

EAGLE-2 用置信度驱动的动态草稿树,把平均接受长度从 3.7 提升到 4–5.5。树形验证的收益很直接:同样一次 target model forward,链式最多验证 γ 个 token,树形可以覆盖 k^d 条路径

对于纯 Transformer 模型,树形验证几乎是”免费的”——通过 tree attention mask,一次 forward pass 就能并行验证整棵树。但 Qwen3.5 的混合架构打破了这个前提:3/4 的层是 Gated DeltaNet(线性注意力),它们不支持 attention mask。


二、Tree Attention Mask 回顾

先回顾纯 Transformer 下的树形验证是怎么做的(详见 EAGLE 文章的 Tree Attention 部分)。

草稿树被展平为一维序列送入 target model,但不同分支之间不应该互相看到。Tree attention mask 通过定制每个 token 的可见范围来解决这个问题:每个 token 只 attend 自己从根到该节点路径上的祖先

1
2
3
4
5
     It
/ \
is has
/ \ \
a the to

展平序列:[It, is, has, a, the, to]

tree attention mask:

1
2
3
4
5
6
7
         It  is  has  a  the  to
It ✓
is ✓ ✓
has ✓ ✓
a ✓ ✓ ✓
the ✓ ✓ ✓
to ✓ ✓ ✓

“a” 只能看到 [It, is, a]——从根到它的完整路径。”has”、”the”、”to” 对 “a” 完全不可见。同一次 forward pass 中,target model 对每条路径独立计算了概率分布,等价于逐条路径单独验证,但只付出一次 forward 的代价。

对于纯 Full Attention 的 Transformer,这套方案完美且零额外成本。问题出在 Qwen3.5 的 32 层里有 24 层不是 Full Attention。


三、混合架构的挑战

Qwen3.5 的 3+1 混合设计:每 4 层为一组,前 3 层 Gated DeltaNet(线性注意力),第 4 层 Full Attention。

1
2
3
4
Block 0:  [GDN] [GDN] [GDN] [FullAttn]    Layer  0- 3
Block 1: [GDN] [GDN] [GDN] [FullAttn] Layer 4- 7
...
Block 7: [GDN] [GDN] [GDN] [FullAttn] Layer 28-31

当一棵草稿树送入 target model 时,不同类型的层需要不同的验证策略

Full Attention 层(8 层)

完全支持 tree attention mask。和纯 Transformer 一样,展平序列 + 定制 mask,一次 forward 并行验证所有路径。没有额外开销。

Gated DeltaNet 层(24 层)

问题的核心在于 GDN 的状态更新方式。回顾 GDN 的状态更新公式

是一个全局的状态矩阵,每个 token 的处理都会不可逆地改变 。这带来两个根本性问题:

问题 1:不能用 mask 隔离分支。

Full Attention 中,attention mask 控制每个 query 能看到哪些 key/value。但 GDN 的”记忆”不在 KV cache 里,而在状态矩阵 里。 是全局的——所有 token 共享同一个 ,没有”这个 token 只看到 的一部分”这种操作。

1
2
Full Attention:每个 token 有独立的 KV 条目 → mask 控制可见范围 → 分支隔离
GDN: 所有 token 共享一个 S 矩阵 → 没有 mask 的概念 → 无法隔离

问题 2:状态更新不可逆。

如果按展平顺序 [It, is, has, a, the, to] 处理,处理完 “is” 后 已经包含了 “is” 的信息。接下来处理 “has” 时,”has” 和 “is” 是兄弟节点——“has” 不应该看到 “is” 的信息,但 里已经有了。

1
2
3
4
展平序列处理:
S₀ → 处理 It → S₁ → 处理 is → S₂ → 处理 has → S₃

has 看到了 is 的信息,但它们是兄弟节点!

更糟糕的是, 的更新包含衰减项 ,即使想”减去” is 的贡献也做不到——因为 已经把 整体缩放了。


四、解决方案:Fork/Checkpoint

解法很直接:在分支点保存 矩阵的快照(checkpoint),每个分支从快照独立演化

以一棵简单的 k=2, depth=2 的树为例:

1
2
3
4
5
    root
/ \
A B
/ \ / \
C D E F

GDN 层的处理流程:

1
2
3
4
5
6
7
8
9
10
1. 从当前 S₀ 出发
2. 处理 A → 得到 S_A [保存 checkpoint: S₀]
3. 处理 C → 得到 S_AC [保存 checkpoint: S_A]
4. 恢复 S_A ← 从 checkpoint 恢复
5. 处理 D → 得到 S_AD
6. 恢复 S₀ ← 从 checkpoint 恢复
7. 处理 B → 得到 S_B [保存 checkpoint: S₀ 已有]
8. 处理 E → 得到 S_BE [保存 checkpoint: S_B]
9. 恢复 S_B
10. 处理 F → 得到 S_BF

每个叶子节点最终拥有独立的 状态,互不污染。

逐深度处理

实际实现中,更高效的做法是按深度分层处理(depth-by-depth):

1
2
3
4
5
6
7
8
9
10
11
深度 0:处理 [A, B]
- A 从 S₀ 出发 → S_A
- B 从 S₀ 出发 → S_B
(A 和 B 可以 batch 化,各自从 S₀ 的副本独立演化)

深度 1:处理 [C, D, E, F]
- C 从 S_A 出发 → S_AC
- D 从 S_A 出发 → S_AD
- E 从 S_B 出发 → S_BE
- F 从 S_B 出发 → S_BF
(四个节点 batch 化,各自从父节点的 checkpoint 出发)

同一深度的所有节点可以并行处理——它们互相之间没有依赖,只依赖各自父节点的 checkpoint。

分层协同

关键是:GDN 层和 Full Attention 层在同一轮验证中用不同策略,但处理的是同一棵草稿树

1
2
3
4
5
6
7
8
9
10
11
Layer 0 (GDN):    depth-by-depth + fork checkpoint
Layer 1 (GDN): depth-by-depth + fork checkpoint
Layer 2 (GDN): depth-by-depth + fork checkpoint
Layer 3 (FullAttn):展平序列 + tree attention mask,一次 forward

Layer 4 (GDN): depth-by-depth + fork checkpoint
Layer 5 (GDN): depth-by-depth + fork checkpoint
Layer 6 (GDN): depth-by-depth + fork checkpoint
Layer 7 (FullAttn):展平序列 + tree attention mask,一次 forward

...重复 8 次...

GDN 层处理完后,hidden states 被传给 Full Attention 层,后者照常用 tree mask 做并行验证。两种机制在层与层之间无缝衔接。


五、Checkpoint 的双重用途

Checkpoint 机制不仅用于树形分支,还用于验证后的回滚。

用途 1:树形分支(验证阶段)

上文已详述。在分支点 fork 状态矩阵,每条路径独立演化。

用途 2:拒绝后回滚

验证完成后,target model 接受了最长的合法路径(比如 root → A → C),拒绝了其他分支。此时需要把 GDN 的状态回滚到被接受的最后一个 token 对应的

1
2
3
4
验证结果:接受 root → A → C,在 C 之后拒绝

Full Attention 层:丢弃 D, B, E, F 的 KV cache 条目(简单截断)
GDN 层: 恢复到 S_AC 的 checkpoint(否则 S 里还有 D, E, F 的信息)

这个回滚对于链式验证同样必需——如果链式草稿 [t₁, t₂, t₃, t₄, t₅] 被拒绝,GDN 的 已经包含了 的信息,必须恢复到 的 checkpoint。

1
2
3
4
链式验证也需要 checkpoint,这不是树形独有的问题。
区别在于:
链式需要 γ 个 checkpoint → 每个 draft token 一个
树形需要 total_nodes 个 → 每个树节点一个

六、开销分析

内存开销

单个 checkpoint 需要保存 24 层 GDN 的全部状态矩阵:

1
2
单个 checkpoint = 24 层 × 16 key_heads × 128(K_dim) × 128(V_dim) × 2 bytes
≈ 12 MB

不同验证策略的 checkpoint 总内存:

策略 checkpoint 数量 总内存
链式(γ=5) 5 60 MB
树形(k=2, d=3, ~15 节点) 15 180 MB
树形(k=3, d=3, ~40 节点) 40 480 MB

性能开销

Fork 操作的本质是 memcpy 一个 12 MB 的状态矩阵:

1
2
3
fork 一次 ≈ memcpy 12 MB
在 GPU HBM(~2 TB/s)上约 6 μs
在 CPU DDR5(~50 GB/s)上约 240 μs

整棵树所有分支点的 fork 总计几十次,总耗时约几 ms。相比 target model 一次 forward pass(通常几十到上百 ms),fork 的性能开销 <2%

1
2
3
4
性能:几乎无损失(<2%)
内存:这才是真正的代价
- 链式 60 MB → 可接受
- 树形 180-480 MB → 需要根据可用内存权衡树的规模

七、完整验证流程

以 Qwen3.5-9B 为例,一轮完整的 draft-verify 流程:

Step 1:MTP Module 生成树形草稿

Qwen3.5 内置的 MTP module 是串行设计(mtp\_num\_hidden\_layers: 1),可以自回归循环生成任意深度的草稿。配合 top-k 采样,每步产生 k 个分支,自然形成树状结构。

1
2
3
4
5
MTP Module → 预测 t+1 → top-k 采样 → k 个候选
↓ 对每个候选
MTP Module → 预测 t+2 → top-k 采样 → k 个候选

...继续到目标深度

这和 EAGLE 的 Autoregression Head 本质相同——区别在于 MTP module 是预训练时联合训练的,EAGLE 是事后训练的。

需要注意:Draft 阶段只跑 MTP module(1 层 decoder),不经过主模型的 32 层。MTP module 用上一步主模型 forward 的 hidden state 作为起点,之后在自己内部自回归循环。主模型的 GDN 状态管理问题只出现在下面的 verify 阶段。

Step 2:展平草稿树

将树上所有节点展平为一维序列,同时构造:

  • Tree attention mask(供 Full Attention 层使用)
  • 树的拓扑信息:每个节点的父节点索引(供 GDN 层确定 fork 关系)

Step 3:送入 Target Model 的 32 层

1
2
3
4
5
6
7
8
9
10
11
12
对每个 Block(共 8 个):

├─ Layer 0,1,2(GDN):
│ 逐深度处理:
│ 深度 0 的节点 → 各自从父节点 checkpoint fork → batch forward → 保存新 checkpoint
│ 深度 1 的节点 → 各自从父节点 checkpoint fork → batch forward → 保存新 checkpoint
│ ...
│ 每个叶子节点得到独立的 hidden state 和 S 状态

└─ Layer 3(Full Attention):
展平序列 + tree attention mask → 一次 forward
输出每个位置的 hidden state

Step 4:验证与回滚

LM Head 把最终 hidden states 映射为 token 分布,逐路径验证。找到最长被接受的路径后:

1
2
3
4
5
6
7
Full Attention 层:
- 保留被接受 token 的 KV cache
- 丢弃被拒绝 token 的 KV cache(Gather-Compact 或直接截断)

GDN 层:
- 恢复到被接受的最后一个 token 对应的 S checkpoint
- 丢弃所有其他 checkpoint

下一轮 draft-verify 从恢复后的状态继续。


八、优化策略

控制树的规模

最直接的优化——限制树的宽度 k 和深度 d,控制 checkpoint 总数:

1
2
k=2, d=3 → 约 15 个节点 → 180 MB checkpoint
k=2, d=2 → 约 7 个节点 → 84 MB checkpoint

树的规模应根据可用内存动态调整。

选择性 Fork

结合 EAGLE-2 的置信度机制:如果某个分支的 draft 置信度很高(>0.95),可以跳过 fork——因为这个分支几乎确定会被接受,不需要为它保留回滚点。

1
2
置信度 > 0.95 → 直接处理,不保存 checkpoint
置信度 < 0.95 → 正常 fork + 保存 checkpoint

高置信度节点占比通常不低,选择性 fork 可以显著减少 checkpoint 数量。

Checkpoint 压缩

子节点的 往往和父节点的 差异不大(只处理了一个 token 的更新)。可以只存储 delta:

delta 通常是稀疏的(只有被当前 影响的行/列有变化),可以压缩 2–4 倍。代价是恢复时需要一次加法。


九、总结

Qwen3.5 的混合架构让树形投机解码必须同时协调两种验证机制:

1
2
3
4
5
6
7
8
9
10
Full Attention 层(8 层):
✓ Tree attention mask → 一次 forward 并行验证
✓ 零额外成本,与纯 Transformer 完全一致
✓ KV cache 按标准方式管理(保留/丢弃)

Gated DeltaNet 层(24 层):
✗ 不支持 attention mask(RNN 式全局状态)
✓ Fork/checkpoint → 分支点保存 S 快照,独立演化
✓ 逐深度 batch 处理 → 同深度节点并行
△ 性能开销 <2%,内存开销 60–480 MB

关键结论:

  • Fork/checkpoint 同时解决两个问题:树形分支(验证时)和拒绝后回滚(验证后)
  • 链式验证同样需要 checkpoint(用于回滚),树形只是需要更多
  • 性能几乎无损失,内存是唯一需要权衡的维度
  • MTP module 天然支持树形草稿——串行设计 + top-k 采样即可构造 draft tree

混合架构下的树形投机解码不是一个新问题的出现,而是同一个 fork/checkpoint 机制的两种应用场景的统一。理解了这一点,Qwen3.5 上的树形投机与纯 Transformer 上的树形投机在工程实现上的差异就只剩一个:GDN 层需要额外维护一个 checkpoint 栈。