投机解码(speculative decoding)常被介绍成“用小模型猜、大模型验“。这个说法不错,但会让人错过两件更要紧的事:它在数学上是无损的,输出分布与大模型逐字节等价;以及它的收益有一个由分布本身决定的硬上限,和草稿模型做得多好无关。本文从这两点出发,最后落到 Omni 类模型上会遇到的具体问题。
一、瓶颈不在算力
生成一个 token,需要把整个模型的权重从显存读一遍。30B 模型 bf16 是 60GB;B300 的 HBM 带宽约 8TB/s,光读权重就要 7.5ms,与你算了多少 FLOP 无关。
而这一步实际做的算术量呢?batch=1 时每个权重只参与一次乘加,算术强度约 2 FLOP/byte。Blackwell 要 300+ FLOP/byte 才能喂饱张量核。也就是说 decode 阶段 GPU 的算力闲置 99%,纯粹在等内存。
由此得到一个关键推论:
一次前向验证 k 个 token,和生成 1 个 token 的耗时几乎一样。
权重只读一次,多出来的 k 倍算术是免费的。投机解码全部的物理基础就在这里——用闲置算力换内存带宽。
二、为什么它是无损的
对每个草稿 token xᵢ,按顺序执行:
以概率 min(1, p(xᵢ)/q(xᵢ)) 接受
若拒绝:从残差分布 norm(max(0, p − q)) 重采一个,并丢弃后面所有草稿
若 k 个全接受:再从 p 白送一个 token
其中 p 是目标模型分布,q 是草稿模型分布。证明输出精确等于 p:
P(x) = q(x)·min(1, p(x)/q(x)) + P(拒绝)·norm(max(0,p−q))(x)
第一项化简为 min(q, p)。第二项需要两个事实:
P(接受) = Σₓ min(p,q) = 1 − TV 其中 TV = ½Σₓ|p−q|
Σₓ max(0, p−q) = TV (残差的总质量恰好是 TV)
于是 P(拒绝) = TV,而残差归一化要除以 TV,两个 TV 正好抵消:
P(x) = min(p,q) + max(0, p−q) = p(x) ✓
最后一步分两种情况:若 p ≥ q,min=q 且 max=p−q,和为 p;若 p < q,min=p 且 max=0,和还是 p。
顺带得到本文最重要的一个等式:
单 token 接受率 α = 1 − TV(p, q)
走一个数
词表 {A,B,C},p = [0.5, 0.3, 0.2],q = [0.4, 0.4, 0.2]。TV = ½(0.1+0.1+0) = 0.1,理论接受率 0.9。
草稿抽到 B(概率 0.4),p(B)/q(B) = 0.75,以 0.75 接受。若拒绝,残差 max(0, p−q) = [0.1, 0, 0],归一化后必出 A。验算:
| 计算 | 结果 | p | |
|---|---|---|---|
| A | 0.4·min(1,1.25) + 0.1·1 | 0.5 | 0.5 ✓ |
| B | 0.4·0.75 | 0.3 | 0.3 ✓ |
| C | 0.2·min(1,1.0) | 0.2 | 0.2 ✓ |
三、加速比,以及一个我算错过的地方
草稿是链式的:第 i 个被接受的前提是前 i−1 个都被接受。若各位置独立且接受率为 α:
tokens/pass = 1 + Σᵢ₌₁ᵏ αⁱ = (1 − α^{k+1}) / (1 − α)
speedup = tokens_per_pass / (1 + k·c) c = 草稿成本 / 目标成本
但独立假设是错的,而且是保守的。看一份真实的 DSpark 草稿模型验证指标(block=7,目标是 Qwen3-Omni-30B-A3B):
| 位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| 接受率 | 0.714 | 0.619 | 0.553 | 0.508 | 0.474 | 0.446 | 0.423 |
按独立假设累乘求和得 1.62,而模型自报的 accept_len 是 3.16。反解可知要产生 3.16,等效恒定 α 需达 0.80——比任何单个位置的实测值都高。
结论:各位置的接受事件是正相关的。直觉上说得通——容易的上下文一口气全接受,难的一个都不接受,而不是每个位置独立掷骰子。所以独立假设给出的是下界,不是估计。
用真实的 3.16 重算(每次目标前向产出 4.16 个 token):
| 草稿成本 c | 净加速 |
|---|---|
| 0.05 | 3.08× |
| 0.10 | 2.45× |
| 0.15 | 2.03× |
| 0.20 | 1.73× |
四、一个常见的对不上
工程上经常看到 accept_rate 和 TV 两个数凑不成 1,进而怀疑实现有 bug。上面那份指标同时报了两个口径,正好解释:
raw_tv = 0.4655
1 − raw_tv = 0.5345
accept_rate_epoch = 0.5345 ← 完全相等
α = 1 − TV 是单位置的等式。若你手上的 accept_rate 是 k 个位置的链式平均,它天然低于 1 − TV,因为后面位置的条件接受率更低。比之前先确认口径,再去查实现。
另一个真实原因是截断词表:EAGLE3/DSpark 类方法常把草稿词表从 151k 截到 32k(靠 d2t/t2d 映射表还原)。词表外的 token 必然拒绝,但若 TV 只在截断支撑集上算,两者就会系统性对不上。
五、草稿从哪来
| 方式 | 草稿来源 | 成本 | 特点 |
|---|---|---|---|
| 独立 draft model | 同族小模型 | 高 | 通用,要多存一个模型 |
| EAGLE / EAGLE3 | 复用目标模型的 hidden state | 低 | 目前主流,共享表征所以 α 高 |
| MTP | 目标模型自带多输出头 | 极低 | 需训练时就设计好 |
| n-gram / prompt lookup | 从上下文查重复串 | ~0 | 免训练,只在有重复时有效 |
六、落到 Omni 模型
Qwen3-Omni 是三级流水:
stage 0 thinker LLM_AR 标准文本 LLM
stage 1 talker LLM_AR 生成音频 codec token
stage 2 code2wav LLM_GENERATION 声码器,非自回归
投机解码只对 LLM_AR 有意义,所以候选是前两级——而难度天差地别。
Thinker:几乎是白送的
它继承自 Qwen3MoeForCausalLM,是标准文本 LLM,推理框架里那套 proposer 直接可用。上面那份 DSpark 草稿模型配的正是 Thinker,accept_len 3.16 也是在这一级上测的。
Talker:真正的难点
Talker 每步产出一个 codec 帧,即 num_code_groups 个残差 VQ 码——第 0 个由主干出,其余由一个小的 code predictor 补齐。
这里有个命名陷阱:代码里那个 code_predictor_mtp 文件名带 “MTP”,但它不是投机解码,只是补齐同一帧内剩下的码本,没有 draft-verify 结构,和加速无关。
Talker 难在分布本身。回到 α = 1 − TV:RVQ 码本训练时就是要最大化码本利用率,分布天生平坦。
| top-1 概率典型值 | 可达 α | |
|---|---|---|
| 文本 | ~0.9 | 0.6–0.8 |
| audio codec | ~0.2 | 低 |
分布越平,再好的草稿接受率也上不去。 这是信息论层面的约束,不是工程问题。所以在 Talker 上投入草稿模型之前,先量它输出分布的熵和 top-k 集中度——一次前向就能出结果,而这个数直接决定后续工作值不值得做。
不过 Talker 也有文本 LLM 没有的结构可以利用:帧率低(25–50 Hz)所以单帧延迟预算更宽松;同一帧的第 1..N 个码本高度依赖第 0 个;音频有强时间连续性,免训练的 n-gram 类草稿可能意外有效。
小结
- 投机解码是无损的,不是近似加速;两个 TV 在推导中抵消是关键
α = 1 − TV是单位置等式,和链式平均口径不同,对不上时先查口径- 独立假设算出的加速是下界;真实场景中各位置正相关,实际更好
- 收益上限由目标分布的平坦程度决定,这是选择在哪一级做投机的第一判据
本文的数值来自 Qwen3-Omni-30B-A3B-Instruct 及其 DSpark 草稿模型的公开验证指标;端到端实测另文再述。