Skip to content

第12章 投机解码:以小博大

"It is better to be approximately right than precisely wrong." — Warren Buffett

本章要点

  • 理解 LLM Decode 是 memory-bound 的根本瓶颈:每步要把 140 GB 模型权重从 HBM 搬一遍,算力 TensorCore 在空转
  • 掌握投机解码的数学等价性——拒绝采样证明了输出分布和原始 decode 完全相同,不是近似
  • 读懂四种 Proposer 的源码:Draft Model / EAGLE / n-gram / MTP (multi-token prediction),各自的适用场景
  • 看懂 num_speculative_tokens=k 时,一次 forward 到底塞了什么进 GPU:k+1 个 position、cu_seqlens_q 的混合构造、attention mask 的特殊处理
  • 学会推导加速比闭式公式:speedup=1αk+1(1α)(1+ck)\text{speedup} = \frac{1 - \alpha^{k+1}}{(1-\alpha)(1 + c \cdot k)} 及其指导意义
  • 理解"batch 大了投机解码反而变慢"这个反直觉现象:draft cost 不再可忽略、budget 抢占 + decode 请求变多
  • 识别三类高接受率场景(代码生成、翻译、长文 summarization)和三类低接受率场景(创意写作、角色扮演、非英文 LoRA 模型)
  • 拿到 V1 spec decode 的完整配置模板与调优建议

12.1 自回归枷锁:memory-bound 的根本代价

要理解投机解码为什么能工作,得先理解它在解决什么——LLM Decode 是被内存带宽而不是算力拖慢的

12.1.1 一次 decode step 的时间分解

跑 Llama-2-70B FP16 (140 GB 权重) 在 A100-80GB (2 TB/s HBM 带宽) 上做 decode。单请求、context_len=1024:

操作时间占比
读取所有模型权重一次140 GB / 2 TB/s = 70 ms80%
读取 KV Cache (1024 tokens × 12 MB/token)12 MB / 2 TB/s = 6 ms7%
实际算术运算(FLOPs)300 GFLOPS / 312 TFLOPS = < 1 ms1%
其他(kernel launch, tokenization)10 ms12%

decode 总时长 ≈ 87 ms。其中 GPU 的 TensorCore 算力实际只利用了 1%——99% 的时间在"把数据从 HBM 搬到 SRAM"

这是一个令人沮丧的事实:你花 20 万买了一张 A100,它的 312 TFLOPS TensorCore 算力在 Decode 时几乎全空转。

12.1.2 Batch 能救一点,但不多

提高 batch size 可以分摊权重读取成本——一次把 140 GB 读进来可以同时处理 N 个请求。但:

  • batch_size=1 → 单请求 87 ms/token
  • batch_size=8 → 8 个请求共享权重读取,单步 ~95 ms(每请求 12 ms)
  • batch_size=64 → ~140 ms(每请求 2.2 ms),开始逼近 GPU 计算上限
  • batch_size=256 → 进入 compute-bound,加速饱和

问题:单个请求在低并发下依然慢得令人发指。一个人问 ChatGPT 一句话,希望 50 tokens/s 的流畅感——87 ms/token 意味着 11.5 tokens/s,体验不合格。

12.1.3 投机解码的直觉:反正要读一遍权重,顺便多验几个 token

核心洞察:既然每步都要读一遍 140 GB 权重,为什么不在这一次读取中"顺便"验证多个候选 token?

一次 forward pass 喂进去 k 个 token,GPU 输出 k 个位置的 logits——FLOPs 是 k 倍,但权重只读一次。由于 decode 是 memory-bound,时间几乎不增加:

  • 1 个 token 的 decode:87 ms
  • 5 个 token 的 batched forward:~95 ms(多 10%,因为 FLOPs 稍增)
  • 这多出的 10% 时间,换来了一次性验证 5 个位置

如果 5 个候选 token 里能有 3 个被接受,那这一 step 等于 3 个 decode 的效果。吞吐就提升了 3 倍。

这就是"以小博大"——用一个廉价的 draft(小模型 / 预测头 / n-gram)产生候选,用大模型一次 forward 并行验证。如果 draft 猜得准,加速立竿见影

12.2 拒绝采样:为什么投机解码是精确的而不是近似的

一个常见的误解:投机解码是"用小模型近似大模型"。。投机解码产出的 token 分布与直接用大模型采样完全一致。这是通过一个叫"拒绝采样(Rejection Sampling / Speculative Sampling)"的精妙算法保证的。

12.2.1 算法原理

设大模型的 token 分布为 p(x)p(x),草稿模型的分布为 q(x)q(x)

对每个候选位置:

  1. 草稿阶段:从 qq 中采样一个 token xqx^* \sim q
  2. 验证阶段:大模型计算 p(x)p(x^*)
  3. 接受/拒绝
    • 如果 p(x)q(x)p(x^*) \geq q(x^*)(大模型比草稿更看好这个 token),无条件接受
    • 否则,以概率 p(x)q(x)\frac{p(x^*)}{q(x^*)} 接受
    • 拒绝时,从修正分布 p(x)=max(0,p(x)q(x))ymax(0,p(y)q(y))p'(x) = \frac{\max(0, p(x) - q(x))}{\sum_y \max(0, p(y) - q(y))} 中重新采样

12.2.2 数学证明(接受的 token 分布 = pp

考虑任一 token xx 被最终采纳为输出的概率:

\Pr(\text{output} = x) = \underbrace{q(x) \cdot \min\left(1, \frac{p(x)}{q(x)}\right)}_{\text{作为候选被接受}} + \underbrace{(1 - A) \cdot p'(x)}_{\text{上一个被拒后作为重采样}}

其中 A=ymin(p(y),q(y))A = \sum_y \min(p(y), q(y)) 是平均接受概率。

化简第一项:q(x)min(1,p(x)q(x))=min(p(x),q(x))q(x) \cdot \min\left(1, \frac{p(x)}{q(x)}\right) = \min(p(x), q(x))

化简第二项:

(1A)p(x)=(1ymin(p,q))max(0,p(x)q(x))ymax(0,p(y)q(y))(1 - A) \cdot p'(x) = \left(1 - \sum_y \min(p, q)\right) \cdot \frac{\max(0, p(x) - q(x))}{\sum_y \max(0, p(y) - q(y))}

注意 ymax(0,p(y)q(y))=1ymin(p(y),q(y))=1A\sum_y \max(0, p(y) - q(y)) = 1 - \sum_y \min(p(y), q(y)) = 1 - A,所以第二项 = max(0,p(x)q(x))\max(0, p(x) - q(x))

合起来:

Pr(output=x)=min(p(x),q(x))+max(0,p(x)q(x))\Pr(\text{output} = x) = \min(p(x), q(x)) + \max(0, p(x) - q(x))

  • 如果 p(x)q(x)p(x) \geq q(x):= q(x)+(p(x)q(x))=p(x)q(x) + (p(x) - q(x)) = p(x)
  • 如果 p(x)<q(x)p(x) < q(x):= p(x)+0=p(x)p(x) + 0 = p(x)

两种情况下都等于 p(x)p(x)。QED.

12.2.3 直觉解释

小模型和大模型的概率分布如果"大致一致",小模型提议的候选 token 通常会被接受(因为 p(x)q(x)p(x) \geq q(x) 的概率高)。只有当小模型"错判"(给了一个大模型不看好的 token 高概率),才会被拒绝——被拒绝后从"大模型独有的倾向分布"中重采样,保证整体分布等于大模型。

这个算法最早由 DeepMind 的 Chen et al. (arXiv:2302.01318) 和 Google 的 Leviathan et al. (arXiv:2211.17192) 独立提出,2023 年成为 LLM 推理优化的显学。vLLM 的 V1 spec decode 实现完全忠于这个算法,没有做任何"为了性能而精度打折"的简化

12.2.4 贪心采样(temperature=0)的退化形式

temperature=0\text{temperature} = 0,分布退化为 one-hot(所有概率集中在 argmax token)。拒绝采样变成简单的相等比较:

draft_token == target_token → 接受
draft_token != target_token → 拒绝,用 target 的 argmax

接受率就是"两个模型在当前上下文下恰好选同一个 argmax 的概率"。这种 case 的接受率通常比随机采样更高(因为模型在贪心模式下更确定),但也更依赖模型相似度。

12.3 num_speculative_tokens=k 一次 forward 里发生了什么

弄清楚数学之后,我们看工程——给定 k=5,一次 spec decode step 实际上 GPU 里跑的是什么。

12.3.1 Draft 阶段(5 个并行 step)

如果用 Draft Model(一个独立的小 LLM),5 次自回归 forward:

step-draft-1: input = [last_target_token], output = draft_token_1
step-draft-2: input = [draft_token_1],     output = draft_token_2
step-draft-3: input = [draft_token_2],     output = draft_token_3
step-draft-4: input = [draft_token_3],     output = draft_token_4
step-draft-5: input = [draft_token_4],     output = draft_token_5

小模型(比如 Llama-1B)每步 ~3 ms,5 步 = 15 ms。

如果用 EAGLE / MTP(共享大模型 hidden state 的预测头),5 次预测头 forward:

hidden_0 (from target) → head(hidden_0) → draft_token_1 + hidden_1
hidden_1 → head(hidden_1) → draft_token_2 + hidden_2
hidden_2 → head(hidden_2) → draft_token_3 + hidden_3
...

预测头 < 1M 参数,每步 < 0.5 ms,5 步 < 3 ms。

如果用 n-gram,纯 CPU 查表 < 0.1 ms。

12.3.2 Verify 阶段:大模型一次 forward 5+1 个 token

Verify 阶段把 5 个 draft token 追加到输入序列,调用大模型 forward。大模型会输出6 个位置的 logits

  • 位置 0(last_target_token 之后):验证 draft_token_1
  • 位置 1(draft_token_1 之后):验证 draft_token_2
  • 位置 2(draft_token_2 之后):验证 draft_token_3
  • 位置 3(draft_token_3 之后):验证 draft_token_4
  • 位置 4(draft_token_4 之后):验证 draft_token_5
  • 位置 5(draft_token_5 之后):总会被用到的 bonus token

第 5 位的 logits 是"如果 draft 全接受,顺便给你生成的下一个 token"。这是 spec decode 的免费午餐——无论前面接受几个,总会多得这 1 个 bonus token。

12.3.3 cu_seqlens 的构造

FlashAttention-3 的 varlen batch(第 11 章)为此优化:

python
# 假设 batch 里 3 个 decode 请求都做 spec decode with k=5
# 每个请求贡献 6 个 Q 位置(5 draft + 1 bonus)

query_tensor.shape = [3 * 6, num_heads, head_dim] = [18, H, D]
cu_seqlens_q = [0, 6, 12, 18]  # 每请求 6 个 Q

# K/V 方面,每请求的 KV 长度是它的 context + 5 draft
# req_A: context=2000, kv_len=2005
# req_B: context=1500, kv_len=1505
# req_C: context=3000, kv_len=3005
cu_seqlens_k = [0, 2005, 3510, 6515]

Attention mask 有一个 subtle 细节——5 个 draft token 之间要有因果掩码(draft_token_2 能看 draft_token_1,但不能看 draft_token_3-5)。FlashAttention-3 原生支持这种 "causal mask within each q segment" 模式,通过 causal=True 标志启用。

12.3.4 接受 n 个 token 后的状态更新

Verify 返回 6 个位置的 logits 后,拒绝采样逐个检查 5 个 draft token:

if accepted all 5:  next step starts at position + 6 (5 accepted + 1 bonus)
if accepted 3:      next step starts at position + 4 (3 accepted + 1 resampled)
if accepted 0:      next step starts at position + 1 (0 accepted + 1 resampled)

关键是 KV Cache 的处理——draft 阶段我们已经把所有 5 个 token 的 KV 写进 KV Cache 了。如果只接受了 3 个:

  • 前 3 个 draft token 的 KV 是正确的(大模型验证通过)
  • 第 4 个位置:draft 被拒,从大模型重采样出一个新 token;这个新 token 的 KV 已经在 Verify 阶段被大模型算好了(position=3 位置输出的 logits 就用了正确的前缀)
  • 第 5 个 draft token 的 KV 需要废弃 — 因为它是基于错误的 draft_4 算的

V1 的 KVCacheManager 会"逻辑上"rollback 被拒部分的 KV——把 num_computed_tokens 回退到正确位置。由于 KV 是 block 级的,如果被拒的 token 恰好跨过 block 边界,可能整个 block 的部分内容需要重写。

12.4 四种 Proposer 深度对比

vLLM V1 支持四种 proposer,在 vllm/v1/spec_decode/ 下各有实现:

12.4.1 Draft Model

最经典的形态——用一个小得多的同系列模型做 draft。例如 Llama-3-8B-Instruct 给 Llama-3-70B-Instruct 做 draft。

python
# vllm/v1/spec_decode/draft_model_proposer.py(概念性)
class DraftModelProposer:
    def __init__(self, draft_model_config, ...):
        # 加载 draft 模型到 GPU(占一部分显存)
        self.draft_runner = GPUModelRunner(draft_model_config, ...)

    def propose(self, target_ids, num_speculative_tokens, ...):
        # 自回归跑 k 步 draft
        draft_ids = []
        current_input = target_ids[-1:]
        for _ in range(num_speculative_tokens):
            logits = self.draft_runner.forward(current_input, past_kv=...)
            draft_token = sample(logits, temperature=draft_temp)
            draft_ids.append(draft_token)
            current_input = draft_token.unsqueeze(0)
        return draft_ids

优点

  • 接受率最高(同系列模型训练数据、tokenizer、架构一致)
  • 草稿质量可控(可以用 draft_temperature 单独调小模型的随机性)
  • 现成的 checkpoint 可直接用(Llama-3 系列的 1B/8B/70B 都有)

缺点

  • 显存占用大(Llama-3-1B FP16 = 2 GB,8B = 16 GB)
  • 每 draft step 有不可忽略的 GPU 开销(2-5 ms)
  • 长序列 draft 时,小模型自己也要做 KV Cache 管理

12.4.2 EAGLE / EAGLE3

EAGLE (arXiv:2401.15077) 的洞察:大模型最后一层的 hidden state 已经包含了"下一个 token 应该是什么"的几乎所有信息。只需要一个超轻量的"预测头"就能从 hidden state 里解码出后续 token。

python
# vllm/v1/spec_decode/eagle.py(概念性)
class EagleProposer:
    def __init__(self, eagle_head_config, target_model, ...):
        self.eagle_head = load_eagle_head(eagle_head_config)  # 1-2 层 Transformer
        # 共享 target 模型的 hidden states,不需要独立跑大模型

    def propose(self, target_hidden_states, target_token_ids, ...):
        # 从 target 最后一层 hidden 出发,跑 k 步 eagle head
        hidden = target_hidden_states[-1:]
        draft_ids = []
        for _ in range(num_speculative_tokens):
            # eagle_head 是个小 transformer,输入 hidden + last_token
            next_hidden, next_logits = self.eagle_head(hidden, ...)
            next_token = sample(next_logits)
            draft_ids.append(next_token)
            hidden = next_hidden
        return draft_ids

EAGLE3 在 EAGLE 基础上进一步压缩预测头规模、引入多层 hidden 融合,接受率更高(0.6-0.7 对比 EAGLE 的 0.5)。

优点

  • 显存开销极小(预测头 < 500 MB)
  • Draft cost 极低(单层 transformer < 1 ms)
  • 不需要额外的 tokenizer / config(复用 target 的一切)

缺点

  • 需要针对特定 target 模型预训练 EAGLE head——不是所有模型都有现成 checkpoint
  • 接受率比 Draft Model 略低(因为预测头容量有限)

12.4.3 MTP (Multi-Token Prediction)

DeepSeek-V3 引入的架构创新——模型本身就内置了多 token 预测头,训练时就按 "预测 next-4-tokens" 的目标训练。使用时:

python
# DeepSeek-V3 / V3.1 风格
def propose_mtp(target_output):
    # MTP heads 是模型的一部分,训练时已对齐
    for i in range(num_mtp_heads):
        draft_token_i = target_output.mtp_heads[i].logits.argmax()
    return draft_tokens

优点

  • 接受率极高(0.85+,因为预测头和主模型完全端到端协同训练)
  • 零额外显存(MTP 头本身就是模型的一部分)
  • 零额外延迟(MTP 头在主模型一次 forward 里已经算完)

缺点

  • 只有原生支持 MTP 的模型能用(目前 DeepSeek-V3/V3.1 / R1,还会扩展到更多)
  • 不能跨模型适配(不像 Draft Model 那样你可以自选一对模型)

V1 对 MTP 的支持是 2025 年新增的,是 LLM 推理领域 spec decode 进化的一个里程碑。

12.4.4 n-gram

最简单但在对的场景下惊人有效。原理:在已生成的上下文里找重复模式。

python
# vllm/v1/spec_decode/ngram_proposer.py(概念性)
@numba.jit(nopython=True)
def ngram_match(context: np.ndarray, min_n: int, max_n: int, k: int):
    """从 context 的末尾找匹配,返回匹配后的 k 个 token。"""
    for n in range(max_n, min_n - 1, -1):  # 先试长 n-gram
        suffix = context[-n:]
        # 在 context[:-n] 里找 suffix 的出现位置
        for i in range(len(context) - 2 * n + 1):
            if (context[i:i+n] == suffix).all():
                # 找到了!返回 i+n 开始的 k 个 token
                end = min(i + n + k, len(context))
                return context[i+n:end]
    return None  # 没找到,放弃 spec decode 这一步

典型场景是代码生成

python
# 已生成的 context
"def foo(a, b): return a + b\ndef bar(x, y): return x + "
# 末尾 "return x + " 没有完全匹配,但 "return " 之前出现过
# 查表找到 "return a + b\n" 这个模式,猜测后续是 "y\n"
# 大多数情况会被验证通过

优点

  • 零 GPU 开销(纯 CPU + Numba JIT)
  • 零显存占用
  • 实现简单,几十行代码
  • 在代码/翻译/摘要等高重复性任务上接受率可以超过 0.5

缺点

  • 创意写作、对话场景接受率极低(0.1-0.3)
  • 依赖 context 长度(越长找到匹配概率越高)

12.4.5 选择建议

12.5 加速比的闭式公式

推一下 spec decode 的理论加速比。

12.5.1 接受率与期望接受长度

设每个位置被接受的独立概率为 α\alpha(接受率),num_speculative_tokens=k。期望接受的 token 数:

E[accepted]=i=0k1αi(1α)i+αkk=1αk+11α1E[\text{accepted}] = \sum_{i=0}^{k-1} \alpha^i \cdot (1 - \alpha) \cdot i + \alpha^k \cdot k = \frac{1 - \alpha^{k+1}}{1 - \alpha} - 1

加上那 1 个 bonus token,每 step 净产出:

E[tokens per step]=1αk+11αE[\text{tokens per step}] = \frac{1 - \alpha^{k+1}}{1 - \alpha}

12.5.2 时间成本

设 verify 一次 forward 的时间为 TvT_v(和原始 decode 接近,略多一点因为 FLOPs 略大),draft 的相对开销为 c=Td/Tvc = T_d / T_v(比如 Draft Model 的 c0.2c \approx 0.2,EAGLE 的 c0.05c \approx 0.05,n-gram 的 c0c \approx 0)。

每 step 总时间:

Tstep=Tv(1+ck)T_{\text{step}} = T_v \cdot (1 + c \cdot k)

12.5.3 加速比

原始 decode 每 step 产出 1 token,时间 TvT_v(简化:忽略 verify 比原 decode 稍多的那一点)。所以:

speedup=E[tokens per step]Tstep/Tv=1αk+1(1α)(1+ck)\text{speedup} = \frac{E[\text{tokens per step}]}{T_{\text{step}} / T_v} = \frac{1 - \alpha^{k+1}}{(1 - \alpha)(1 + c \cdot k)}

12.5.4 数值计算

几个常见配置的理论加速比:

Proposerαck=3k=5k=7最优 k
Draft Model (同系列)0.750.22.28×2.62×2.72×7
Draft Model (跨系列)0.550.21.74×1.79×1.73×5
EAGLE30.650.052.15×2.52×2.68×8-10
MTP (DeepSeek)0.850.012.78×3.71×4.33×~12
n-gram (代码)0.500.001.75×1.93×1.98×6-8
n-gram (对话)0.250.001.29×1.31×1.30×5

几个洞察:

  • α\alpha 时,k 越大越好(MTP 可以把 k 拉到 10+)
  • α\alpha 时,k 存在饱和点(跨系列 Draft Model 过 k=5 后收益递减)
  • cc 决定了"最优 k"的位置——cc 越小,最优 k 越大

实际生产中:

  • MTP: k=8
  • Draft Model: k=5
  • EAGLE: k=5-6
  • n-gram: k=5(再大也没用)

12.6 为什么"batch 大了投机解码反而变慢"

这是一个经常让工程师困惑的现象。原因是高并发下三个变化:

12.6.1 Draft cost 不再可忽略

低并发下 GPU 算力大量空闲,draft 的额外 FLOPs 几乎不增加墙钟时间。但 batch=128 时 GPU 已经接近 compute-bound,draft 每 step 的 2-5 ms 开始实打实加在每 step 总时间上。

12.6.2 Verify 的 k 倍 Q 占用 budget

一个 running 请求 decode 时 num_scheduled_tokens=1;开了 spec=5 后变成 6。如果 max_num_batched_tokens=4096,原本能塞 4000 个 decode 请求的 step,现在只能塞 650 个。

换句话说,spec decode 把 budget 消费放大了 k 倍。在中高并发下,这个放大效应让 batch 压不下来,吞吐反而降。

12.6.3 拒绝采样的部分浪费

接受率 α=0.7 时,平均每 5 个 draft 浪费 1.5 个——它们被写进 KV 又被逻辑 rollback。这部分显存带宽的浪费,在 compute-bound 场景下体现为纯开销。

12.6.4 经验法则

  • batch_size < 8:spec decode 几乎总是赢的(低并发是它的主场)
  • batch_size 8-32:看接受率,α>0.7 仍能赢
  • batch_size > 64:通常弊大于利,关掉 spec decode
  • 混合流量:V1 允许按请求动态开关 spec——高优先级 / 小 batch 的请求开,高并发的 decode 请求关

V1 支持在 SamplingParams 层面打开或关闭 spec decode:

python
sampling_params = SamplingParams(
    temperature=0.7,
    use_spec_decode=True,  # 这个请求用投机解码
    # use_spec_decode=False 则走标准 decode
)

这让 vLLM 可以按请求权衡——单用户 chatbot 会话开 spec decode 降延迟,高并发 API 场景关掉保吞吐。

12.7 生产配置模板与调优

12.7.1 单用户对话(低并发,追求 TPOT)

bash
vllm serve meta-llama/Llama-3-70B-Instruct \
    --speculative-model meta-llama/Llama-3-8B-Instruct \
    --num-speculative-tokens 5 \
    --speculative-draft-tensor-parallel-size 1 \
    --max-num-seqs 16 \
    --gpu-memory-utilization 0.92

期望效果:TPOT 从 25 ms → 12 ms(2× 提升),总显存多占 16 GB。

12.7.2 DeepSeek-V3 启用 MTP

bash
vllm serve deepseek-ai/DeepSeek-V3 \
    --speculative-config '{"method": "deepseek_mtp", "num_speculative_tokens": 3}' \
    --max-num-seqs 64

DeepSeek 原生的 MTP 头,接受率 ~0.85,提速 2.5×+。

12.7.3 代码生成工作流(n-gram)

bash
vllm serve meta-llama/Llama-3-70B-Instruct \
    --speculative-config '{"method": "ngram", "num_speculative_tokens": 5, "prompt_lookup_min": 3, "prompt_lookup_max": 8}' \
    --max-num-seqs 32

零显存开销,代码场景接受率 ~0.55,提速 1.6×。

12.7.4 调优关键指标

vLLM 暴露 spec decode 的专用 Prometheus 指标:

vllm:spec_decode_num_accepted_tokens_total   # 累计接受 token 数
vllm:spec_decode_num_draft_tokens_total      # 累计 draft token 数
vllm:spec_decode_num_emitted_tokens_total    # 累计 emit token 数

计算实时接受率:α=accepteddraft\alpha = \frac{\text{accepted}}{\text{draft}},如果持续 < 0.4,考虑降 k 或关掉 spec。

12.8 投机解码的未来

Spec Decode 这个方向从 2022 年初出现以来,每半年就有一代新方法。我们可以预期的演进方向:

  • 模型协同设计——像 MTP 这样"训练时就考虑 spec"的模型会越来越多
  • Tree-based 投机——一次 draft 多个分支(tree),验证时挑最好的那支;已在 EAGLE2 / Medusa-2 中实现
  • 层次化投机——draft 嵌套 draft(小小模型 + 小模型 + 大模型),像 CPU cache 的 L1/L2/L3
  • 跨请求共享 draft——同 prompt 前缀的多个请求共用同一段 draft
  • 硬件协同——draft 上 CPU / NPU 异构加速,verify 上 GPU

vLLM 的 V1 为 spec decode 设计了可插拔接口(第 18 章讲过的 Pluggable),新方法发布后通常一个月内就有 PR 合并进主线。这让它能保持对前沿的跟进能力。

12.9 本章小结

投机解码是打破 LLM decode memory-bound 枷锁的关键武器:

  • 动因:decode 99% 时间在搬权重,计算空转;一次读权重顺便验多 token 几乎免费
  • 数学等价:拒绝采样证明输出分布 = 原始大模型 decode,不是近似,是精确
  • 一次 forward 内部:k+1 个 Q 位置、varlen cu_seqlens、causal mask within draft span
  • 四种 Proposer:Draft Model(通用)/ EAGLE(轻量)/ MTP(原生最强)/ n-gram(CPU 零开销)
  • 加速比闭式公式1αk+1(1α)(1+ck)\frac{1-\alpha^{k+1}}{(1-\alpha)(1+c \cdot k)},用来预测不同配置收益
  • 反直觉现象:batch 大了 spec decode 反而变慢;V1 支持按请求动态开关
  • 实战配置:低并发 / MTP 模型 / 代码场景给出三种典型配方

下一章我们进入 量化(Quantization)——用数值精度换速度与显存的系统性工程。如果说 Chunked Prefill 和 Spec Decode 是"策略层"的优化,量化就是"电流层"的优化——它让每一次权重读取、每一次矩阵乘法都变得更便宜。


源码导航

  • V1 投机解码主目录:vllm/v1/spec_decode/
  • Proposer 接口:vllm/v1/spec_decode/interface.py
  • Draft Model 实现:vllm/v1/spec_decode/draft_model_proposer.py
  • EAGLE 实现:vllm/v1/spec_decode/eagle.py
  • n-gram 实现:vllm/v1/spec_decode/ngram_proposer.py
  • MTP 实现:vllm/v1/spec_decode/deepseek_mtp.py
  • 拒绝采样:vllm/v1/sample/rejection_sampler.py
  • Metrics:vllm/v1/metrics/stats.py

论文

  • Leviathan et al., "Fast Inference from Transformers via Speculative Decoding", ICML 2023 (arXiv:2211.17192)
  • Chen et al., "Accelerating Large Language Model Decoding with Speculative Sampling", 2023 (arXiv:2302.01318)
  • Cai et al., "Medusa: Simple LLM Inference Acceleration Framework", 2024 (arXiv:2401.10774)
  • Li et al., "EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty", ICML 2024 (arXiv:2401.15077)
  • DeepSeek-AI, "DeepSeek-V3 Technical Report", 2024 (arXiv:2412.19437) — MTP section

基于 VitePress 构建