2959 字
约 9 分钟
12
S23-A26 · 投机解码:接受率决定收益,草稿模型的账怎么算

给推理服务上了投机解码(speculative decoding)。论文上的数字是 2–3 倍加速,实测 P50 快了 8 %,P99 反而慢了 15 %,而且少数长回答里出现了整段重复短语。回滚之后又试了另一种草稿模型,这次 P50 快了 40 %,但 A/B 里人工评分掉了 0.4 分。

两次都栽在同一个地方:把"加速比"当成一个可以拍板的常数。投机解码的收益完全由接受率决定,而接受率由任务形状决定——摘要与分类高、创意与长推理低。更麻烦的是,接受率是个随机变量,它把延迟方差放大,于是均值变好、尾部变坏。

这篇拆三个账:接受率怎么变成加速比、草稿模型自己要吃多少资源、以及什么形状的任务根本不该上这套机制。


1. 它到底在买什么

投机解码的机制很朴素:用一个小模型先猜 k 个 token,再用大模型一次 forward 验证这 k 个。验证是并行的,成本接近"大模型跑 1 个 token";猜对了就直接接受,猜错了就截断到第一个不一致处,剩下的丢掉。

角色 干什么 资源画像 关键约束
草稿模型 自回归猜 k 个 token 权重小、KV 少、但串行 k 步 必须与目标模型同词表
目标模型 一次并行验证 k 个 一次 forward,成本 ≈ 1 token 采样分布必须严格一致
调度器 组批、回滚 KV 批内混跑时对齐 回滚未接受部分

它买的不是"算得更快",是**"用一次大模型 forward 换掉 k 次串行 forward"**;前提是猜对的 token 本来也要算,猜错的等于白送。

// 一轮的时间构成(8B 目标 + 1B 草稿,k=4)
draft:  4 × 6ms  = 24ms      // 串行,小模型也可能不比大模型快很多
verify: 1 × 28ms = 28ms      // 接受 n 个 token(n ≤ 5,含 bonus token)
total:  52ms  → 产出 n+1 个 token
// n=4(全接受):52ms / 5 token = 10.4ms/token   ← 原 28ms/token,2.7×
// n=1(只中 1):52ms / 2 token = 26ms/token     ← 几乎没赚
// n=0(全错):  52ms / 1 token = 52ms/token     ← 亏 1.9×

最后一行是全部风险的来源:接受率低的时候,投机解码是负收益,而它在监控上表现为"没变快"或"变慢",不是"报错"。


2. 接受率是唯一决定收益的变量

设单步接受率为 α、草稿长度 k,则每轮期望产出 token 数为

E[n] = (1 − α^(k+1)) / (1 − α)

α = 0.8、k = 4 时 E[n] = 3.36;α = 0.5 时 E[n] = 1.94。每轮成本约等于"1 次大模型 forward + k 次小模型 forward",把它折算成"单 token 成本 c",收益就是 E[n] / c。

α(单步接受率) k=2 k=4 k=8 实际加速(含草稿开销)
0.9 2.71 4.10 6.13 2.6–3.4×
0.8 2.44 3.36 4.66 2.1–2.6×
0.7 2.19 2.77 3.60 1.6–2.0×
0.6 1.96 2.31 2.75 1.2–1.4×
0.5 1.75 1.94 2.13 0.9–1.05×(临界)
0.4 1.56 1.62 1.66 0.7–0.8×(亏)

三件事从这张表里直接读出来:第一,α < 0.55 就别上,它不是"效果差一点",是稳定亏;第二,k 的边际收益随 α 降低而迅速消失(α=0.4 时 k=2 与 k=8 几乎一样),所以低 α 场景调大 k 是纯粹浪费;第三,α 高的时候 k 越大越好,但 k 太大会让"验证失败"的沉没成本变大——这解释了为什么生产上 k 一般取 3–5,而不取 8。

// 现场估 α 的最简方法:统计一段流量里"连续接受长度"的分布
// 不需要改引擎,读 trace 里的 accepted_token_count 即可
def accept_rate(counts, k):
    // counts[i] = 一轮接受 i 个 token 的次数(i=0..k)
    total = sum(counts)
    drafted = sum(i * counts[i] for i in range(len(counts)))   // 实际发出的草稿数
    accepted = sum(i * counts[i] for i in range(len(counts)))
    return accepted / max(drafted, 1)
// 注意分母口径:按"发出草稿数"算,而不是按"轮数"算

3. 四种地形:α 与草稿成本一起看

只看 α 会漏掉一半。草稿模型自己的成本有两个来源:权重与 KV 占显存(挤掉并发)、串行 k 步的时间(大模型哪怕再快也省不掉)。

地形 α 草稿成本 结果 典型任务
甜点 0.8+ 低(< 0.2×) 2–3× 净收益 摘要、抽取、分类、改写
划算 0.7 中(0.3×) 1.5× 左右 结构化输出、模板化回答
临界 0.55 高(0.4×) 0.9–1.1×,白忙 通用问答
亏损 < 0.5 任意 < 0.8× 创意写作、长链推理、代码生成

最容易被忽略的是第二列:草稿模型不是免费的。一个 1B 草稿在 8B 目标旁边要吃掉 ~2 GB 权重 + 自己的 KV。原来能跑 58 个 8k 并发的卡,现在只能跑 50 个——并发降 14 %,收益要先扣掉这一块。

// 上投机解码前的三行预算检查(任一不满足就别上)
speculative_decoding:
  vram_check: draft_vram + target_vram + kv_at_peak_p99 <= gpu_total * 0.95
  accept_check: measured_alpha_on_real_traffic >= 0.6     // 用真实流量,不用样例
  tail_check: p99_latency_after <= p99_before * 1.05      // 尾部不能变差
  // 三条同时成立才允许灰度;只满足前两条,说明你换来的是均值好看、尾部更抖

4. 为什么尾延迟会变差

均值和尾部走反方向,机制有三层。

第一层是接受率的随机性:α 在 prompt 不同位置不同——开头模板区高,中段自由生成低。P99 请求恰好是那些"α 全程偏低 + 草稿全错"的请求,它们的成本是普通请求的 1.9 倍。

第二层是回滚成本:验证失败要丢弃未接受的 KV,实现不精细时会把整轮的 KV 重算一遍。这层成本不在论文模型里,但在真实引擎里存在。

第三层是批内不齐:一个批里有的接受 4 个、有的接受 0 个,步调被最慢的决定,批越大方差越明显。

现象 机制 可观测信号
P50 变好、P99 变差 α 的方差 + 全错轮 接受长度的分布双峰
输出出现重复短语 回滚边界处理错,KV 与 token 错位 同一段重复出现的 n-gram
吞吐提升但成本没降 并发被草稿模型挤掉 峰值并发下降、单价持平
小流量测试很好,上线变差 样例流量 α 偏高 线上 α 比离线低 0.15+
// 接受长度的直方图:双峰即"P99 被拖坏"的直接证据
n=0  ████████            18%   ← 全错轮,这些请求多等一整轮
n=1  ██████              14%
n=2  ████                 9%
n=3  ██████              14%
n=4  ██████████████████  45%
// 均值 2.4 很好看,但 18% 的请求走了最贵的那条路径

第三行是个典型误判:吞吐(token/s)上去了,但因为并发降了、单位时间处理的请求数没变,每请求成本反而升。这也是为什么评估要盯"每千 token 成本",而不是只盯吞吐。


5. 草稿模型从哪来

三种来源,代价结构完全不同。

来源 α 水平 额外资源 工程成本 适用
同家族小模型(1B / 3B) 0.7–0.85 独立权重 + KV 低(换模型即可) 大多数场景首选
n-gram / prompt 查找 0.5–0.7(模板化任务可到 0.9) 几乎为零 中(要建索引) 高频模板、固定格式
目标模型剪枝 / 蒸馏 0.75–0.9 与目标共享部分结构 高(要训练) 有训练能力的团队

第二行常被低估:模板化的客服话术、固定格式的报表摘要,n-gram 草稿的 α 能到 0.9 且零显存成本,这是性价比最高的一档。而通用问答上 n-gram 的 α 只有 0.5 左右,纯亏。

// 灰度期的三段观测:α、尾部、并发,缺一个就会看错
$ curl -s localhost:8000/metrics | grep -E 'spec_accept|spec_draft|p99|num_running'
// 先看 spec_accept / spec_draft 是否 > 0.6,再看 p99 是否退化,
// 最后看 num_running 是否掉了 > 5%(掉了就是草稿模型在挤并发)

6. 排错:五种"开了投机反而更慢"

现象 八成原因 处理方向
整体慢 0.7–0.9× α < 0.5,纯亏 关掉,或换任务分流
均值快、尾部慢 α 方差 + 回滚重算 检查回滚实现,限 k
内容质量下降 采样分布不等价(未做拒绝采样校正) 核对验证器实现
重复短语 KV 回滚错位 关投机验证,只跑目标模型
吞吐涨、成本没降 草稿挤掉并发 降 k 或降草稿精度

第四行不是"慢",是正确性问题:输出分布要与大模型逐位一致,靠"接受/拒绝采样"校正;少一个 min(1, p_target/p_draft) 就退化成近似 greedy。


7. 边界:什么任务不该上

场景 为什么不该 替代做法
创意写作 / 长链推理 α 常 < 0.5 调批处理与量化
输出极短(< 20 token) 草稿预热成本占比过高 直接跑,减小 k
显存已满(> 90 %) 草稿模型会把并发压垮 先扩卡或降精度
强确定性要求(逐位一致) 实现稍错就漂移 只在验证器充分测试后启用

判断口径一句话:先在真实流量上量出 α,再决定要不要上;α 不够 0.6 就别谈"再调调参数",那不是调参问题。而 α 达标之后,真正的判据换成第三条——P99 不能变差;均值好看而尾部更抖的"加速",在交互场景里等于把投诉从少数用户转移到更多用户身上。


8. 自测

  1. 投机解码买的到底是什么?为什么它的收益不是常数?
  2. 写出 α=0.7、k=4 时的期望产出 token 数,并说明 k 增大为什么边际收益递减。
  3. 为什么 α 低时调大 k 是浪费?α 高时为什么又不建议 k > 6?
  4. "吞吐上去、成本没降"的机制是什么?该盯哪个指标?
  5. 输出出现重复短语说明哪一层实现错了?它属于性能问题还是正确性问题?
  6. 三个上线前置检查分别拦的是什么?

9. 一句话总结

投机解码是把"k 次串行 forward"换成"1 次并行验证",所以它的收益完全由接受率 α 决定:α ≥ 0.7 时净收益 1.5–2.5×,α < 0.55 时稳定亏损,而 α 由任务形状决定、必须在真实流量上量。更硬的判据是尾部——α 的方差会把 P99 推高,均值变好不算赚。上线三条检查:α ≥ 0.6、显存留够、P99 不退化;三条缺一条,这套机制买到的只是更贵的复杂度。

S23-A26 · 投机解码:接受率决定收益,草稿模型的账怎么算
https://www.clxhxhhr.top/posts/6724/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。