城里有家深夜食堂,只认一位大主厨。他做菜极慢,每道菜都要从头到尾想一遍,客人排起长队。后来食堂请来一个手脚麻利的学徒:学徒先把接下来五道菜快速写好,端给主厨——主厨只花一道菜的工夫,就能把五道菜一起“验”完:合口味的直接留下,不合的当场改掉,再让学徒从改掉的地方重新写。客人拿到手的菜,味道和主厨亲手做的一模一样,上菜速度却快了两三倍。这就是投机解码(Speculative Decoding):用一个小模型当学徒,快速写出候选 token 串,让大模型一次前向并行验证,实现 2-3 倍无损加速。本文从问题背景、核心流程、无损保证、加速原理、接受率因素、草稿选型六个方面,带你彻底看懂这项大模型推理加速技术。
学会投机解码,你就能在不重训模型、不牺牲质量的前提下,把大模型推理延迟与成本砍掉一半以上——这是 LLM 部署、推理优化与 Infra 岗位面试和实战都绕不开的高价值技能。
可视化演示
动态演示投机解码用小模型加速大模型推的核心机制,通过概览/流程/对比/趋势四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
投机解码用小模型加速大模型推:自回归解 自回归解码的延迟瓶颈与加速思路 草稿模型生成候选token流程 核心机制可视化 投机解码用小模型加速大模型推整体架构与关键组件
问题背景:主厨的瓶颈
想象一家只靠一位主厨撑场面的餐厅。主厨手艺精湛,客人认准他的味道,但有致命问题:他做事极慢,必须先想清楚第一道菜,才能想第二道,绝不敢同时开工。每一道菜都要从备料到装盘走完一整套流程,后厨排队的客人越来越多。
大模型生成文本就像这位主厨。它采用“自回归”架构:要生成一句话,必须一个词一个词地来——先有“今天”,才能算“天气”,算出“天气”再想“不错”。每吐一个字,都要把前面的所有字重新“读”一遍,执行一次完整的前向传播。生成 100 个字,就要排队等 100 轮。
逐字生成的概率模型可以写成:
主厨慢,慢在“每道菜必须单独决定”。那能不能让手快的人先把菜备好,主厨一次性验收?这正是投机解码的思路:请一位学徒打前站,让主厨一次尝一桌菜。
- 序列概率 $P(y_1 y_2 \cdots y_m)$整句文本的生成概率,是自回归语言模型的建模目标,衡量“这句话像不像模型会说的话”。
- 连乘符号 $\prod_{i=1}^{m}$从第 1 个字一路乘到第 m 个字,表示每一步都不能跳步、不能并行,这是延迟的根源。
- 条件概率 $P(y_i \mid y_{<i})$第 i 个字只在前 i-1 个字确定之后才能决定,符号 $y_{<i}$ 表示“前 i-1 个字”。
核心流程:草稿与验收
餐厅老板请来一位学徒。学徒没有主厨十成功力,但手脚麻利,还背熟了主厨的常用菜谱。他先快速写出接下来几道菜的方案——比如“今天天气不错”六个字——像先写一张菜单,端到主厨面前。
这张“菜单”就是草稿模型(Draft Model)生成的候选 token 序列。它比目标模型小得多、快得多,质量略糙但大方向靠谱。关键在第二步:目标模型(Target Model)不是把菜单上的字一个个单独验,而是用自己的“大脑”一次性并行读完这串候选,同时给出每个位置它自己会选的字。
草稿阶段的数学描述如下:草稿模型按自己的分布,逐字采样出 K 个候选:
验证阶段才是精髓:目标模型对这 K 个位置同时算出自己的分布 p,然后逐一对比——候选字与目标模型的选择一致就接受;从第一个不一致的地方开始,改用目标模型自己的字,并让学徒从那里重新写菜单。学徒写菜单→主厨一把验→留下对的、改掉错的→学徒重写,每转一圈,主厨只“动一次脑筋”,却往往能确认好几个字。
- 草稿分布 $q$草稿模型对下一个字的概率估计函数,它会比大模型粗糙,但算得快得多。
- 采样符号 $\sim$从概率分布中随机抽一个候选字,而不是机械地取最大的那个,随机性保留了口味的多样性。
- 条件前缀 $x_{<i}$已经写出的前 i-1 个字,草稿每一步都要把新字接到旧字后面,模拟自回归。
- 候选串长度 $K$草稿一次连写 K 个字,组成一张“菜单”,交给大模型一把验证。
无损保证:口味不打折
客人最大的疑问:让学徒代笔,菜还是主厨的味道吗?投机解码设计者的答案是:只要验菜时用对规则,口味可以做到分毫不差——不是“差不多”,而是概率上完全一致。
这把“验菜规则”叫拒绝采样(Rejection Sampling),是统计学里的老工具。当学徒端上一个候选字 x 时,主厨不直接说行或不行,而是比较自己对这个字的喜欢程度 p(x) 和学徒的喜欢程度 q(x),再掷一次骰子做裁决:
有了这套规则,最终端上桌的每道菜都像主厨亲手做的——输出的概率分布与“完全不用学徒”时一模一样。拒绝采样是投机解码“无损加速”的根本保证,也是它区别于各种“近似蒸馏加速”的关键。
- 目标概率 $p(x)$目标模型认为下一个字是 x 的真实概率,代表主厨的口味。
- 草稿概率 $q(x)$草稿模型给 x 的概率,代表学徒的猜测。
- 比值 $p/q$比较主厨和学徒谁更喜欢这个字。若主厨更喜欢,比值大于 1,则直接放行;若学徒“自作多情”,比值小于 1,则按比值概率随机放行。
- 取最小值 $\min(1, \cdot)$把放行概率压在 1 以内,保证它是一个合法的概率。
- 拒绝后的补偿被拒之后,主厨从“自己的偏好减去学徒已经占掉的部分”重新采样补足缺口,让两边的分布严丝合缝。
加速原理:一次尝一桌
为什么请学徒反而更快?想象主厨每次“尝菜”都要摆开一整套架势,不管尝一道还是尝五道,准备时间几乎一样。既然如此,与其让他一趟趟跑,不如把五道菜一次端到他面前。
工程上,目标模型一次前向传播的开销近似固定:它读入 K 个候选位置并并行算出各自分布,耗时远小于串行 K 次前向。所以“每轮能确认几个字”直接决定加速倍数。
设每个候选 token 被接受的概率为 α,草稿长度为 K,则每轮期望确认的 token 数为:
代入典型值 α = 0.8、K = 4,得到 E[N] ≈ 3.36——一轮平均确认 3.36 个字,而原来一轮只产 1 个字。再扣除草稿成本与调度开销,工业界普遍报告 2-3 倍实际加速。投机解码的聪明之处,是把“主厨必须亲自做的轮数”从字数变成了桌数:菜越多、越对味,省下的时间越可观。
- 接受率 $\alpha$单个草稿字被大模型放行的概率,衡量学徒的“懂主厨程度”,取值在 0 到 1 之间。
- 连续全收的概率 $\alpha^{K+1}$K+1 个字全部被接受的概率,意味着学徒整张菜单完美无缺。
- 等比数列求和每轮接受的期望个数,是“至少收 1 个、收了 2 个、收了 3 个……”这些情况的加权平均,闭式解就是上式。
接受率因素:投其所好
学徒写十个字,主厨能放过几个?答案不取决于学徒“字写得漂不漂亮”,而取决于它有多懂主厨的口味。两人口味越接近,验菜越顺利;差距越大,主厨就越频繁皱眉。
这个“口味接近程度”在数学上就是草稿分布 q 与目标分布 p 的重合度。理想情况下 q = p,接受率 α = 1,草稿每字必过;糟糕时 q 与 p 几乎不重叠,α 趋近 0,验证变成空转。两者关系可以精确写成:
除了分布距离这个“天花板”,实际操作中还有三个因素决定收益:一是草稿模型训练是否充分、是否与目标同源;二是采样温度,温度越高分布越平,峰值被拉开,接受率下降;三是候选长度 K,K 越大单轮上限越高,但越靠后的位置越难猜、接受率自然衰减,所以工业实现常动态调整 K。想提高接受率,不能逼学徒“闭着眼多写菜单”,而是让学徒日复一日观察主厨的偏好。
- 总变差距离 $D_{TV}(q, p)$两个概率分布差异的“平均度量”,是统计学中衡量分布距离的经典指标。
- 绝对值求和 $\frac{1}{2}\sum_{x \in V} |q(x) - p(x)|$对词表 V 里每个词的概率差取绝对值、求和再折半,值越小说明两个分布越像。
- 接受率与距离的关系分布距离越小,接受率越高;分布完全一致时距离为 0,接受率为 1,实现“每字必收”。
草稿选型:挑个好帮手
既然学徒是提速的关键,那是不是手脚越快越好?未必。请一个完全不认识主厨口味的学徒,写十道菜被拒九道,主厨的验菜功夫全白费;但若学徒太“胖”——自己备菜都备半天——省下的时间又被学徒吃掉了。
草稿模型选型有三大铁律:共享词表、低延迟、分布接近。共享词表保证主厨能“看懂”学徒写的字;低延迟保证学徒成本占比低;分布接近保证接受率高。三者缺一,加速都打折扣。
综合“草稿成本占比”与“每轮验收数”,加速比可估算为:
优秀实践里,草稿模型通常比目标模型小一到两个数量级(如 7B 目标配 0.5B 草稿),并与目标模型同源、经过蒸馏适配;有些系统甚至用 n-gram 模型或检索结果当“学徒”,连训练都省了。一个好帮手不是越小越好,而是“快、懂、配合”三者平衡——就像餐厅挑学徒,看的是他能不能让主厨一桌一桌顺利验下去,而不是他跑得有多快。
- 目标模型轮耗 $T_{\text{大}}$目标模型完成一轮验证所需时间,即主厨“尝一桌菜”的耗时。
- 草稿模型总耗时 $K \cdot T_{\text{小}}$草稿模型写 K 个候选字的总时间,$T_{\text{小}}$ 是草稿模型每写一个字的耗时。
- 分母 $1 + \dfrac{K T_{\text{小}}}{T_{\text{大}}}$草稿成本占比越小,分母越接近 1,加速比越接近理论值 E[N]。
AI 应用场景
大模型 API 服务提速降本
阶段:推理部署阶段(Decode 阶段优化)
解决的问题:传统逐字解码让 GPU 利用率低、长文本生成延迟高,服务商只能靠堆卡扛并发。投机解码让“主厨”从逐字上菜变成批量验菜:草稿模型先写候选,大模型一次验证,每请求延迟降一半以上,吞吐翻倍,成本同步下降。
具体案例:某公司用 vLLM 部署 70B 对话模型,开启投机解码并搭配 0.5B 同词表草稿模型后,长文档总结场景的平均每 token 延迟从 42ms 降到 16ms,单卡吞吐提升约 2.4 倍,且输出质量与关闭投机时完全一致。
端侧离线模型加速
阶段:端侧推理部署
解决的问题:手机、电脑上跑私有化模型受显存带宽限制,逐字生成每字要等几百毫秒。用微型草稿模型先写、大模型后验,既保住主厨口味,又不用把数据送到云端。
具体案例:在手机上部署 7B 本地助手,配 0.3B 草稿模型,闲聊场景的 token 生成速度从约 6 token/s 提升到 15 token/s,首字延迟几乎不变,离线也能流畅对话。
代码补全与实时写作
阶段:流式生成与交互体验优化
解决的问题:代码补全、实时翻译对“每个字的到达速度”极度敏感,用户等一个补全片段就像等主厨一道一道上菜。投机解码让大模型一次确认一长串候选,补全体验从“卡顿”变“跟手”。
具体案例:某 IDE 补全插件接入投机解码后,一次函数补全(约 30 个 token)的完整等待时间从 800ms 降到 300ms 左右,用户几乎感觉不到“思考中”。
Python 演示
# -*- coding: utf-8 -*-"""投机解码最小演示:小模型写草稿,大模型一次验收,输出与逐字生成完全一致""" import random class CharLM: """极简字符级 n-gram 语言模型:统计前 n 个字符出现后下一个字符的分布""" def __init__(self, text, order=2, smooth=0.2): self.order = order self.smooth = smooth self.vocab = sorted(set(text)) self.counts = {} for i in range(len(text) - order): ctx = text[i:i + order] ch = text[i + order] if ctx not in self.counts: self.counts[ctx] = {} self.counts[ctx][ch] = self.counts[ctx].get(ch, 0) + 1 def dist(self, ctx): """返回该上下文下每个字符的概率分布""" if ctx not in self.counts: return {ch: 1.0 / len(self.vocab) for ch in self.vocab} raw = self.counts[ctx] total = sum(raw.values()) + self.smooth * len(self.vocab) return {ch: (raw.get(ch, 0) + self.smooth) / total for ch in self.vocab} def top1(self, ctx): return max(self.vocab, key=lambda ch: self.dist(ctx)[ch]) def sample(self, ctx): r = random.random() acc = 0.0 for ch in self.vocab: acc += self.dist(ctx)[ch] if r <= acc: return ch return self.vocab[-1] def greedy_decode(prompt, model, need): """基线:目标模型逐个 token 贪婪生成,每生成一个字算一次前向""" out = list(prompt) for _ in range(need): out.append(model.top1("".join(out[-model.order:]))) return "".join(out) def verify_round(prefix, cand, target): """目标模型一次前向(概念上并行)算出 K 个位置的贪婪结果""" result = [] ctx = prefix[-target.order:] for ch in cand: result.append(target.top1(ctx)) ctx = (ctx + ch)[-target.order:] return result def speculative_decode(prompt, draft, target, draft_len=6, need=40, max_rounds=60): """投机解码主循环:草稿 -> 一次验证 -> 接受/拒绝 -> 重新草稿""" x = prompt rounds = 0 while len(x) - len(prompt) < need and rounds < max_rounds: rounds += 1 cand = [] ctx = x[-draft.order:] for _ in range(draft_len): ch = draft.sample(ctx) cand.append(ch) ctx = (ctx + ch)[-draft.order:] ver = verify_round(x, cand, target) for i in range(draft_len): if cand[i] == ver[i]: x += cand[i] else: x += ver[i] break else: if len(x) - len(prompt) < need: x += target.top1(x[-target.order:]) return x, rounds def main(): random.seed(7) corpus = ("春天的花开秋天的风以及冬天的落阳忧郁的青春年少的我曾经无知的这么想" "流水它带走光阴的故事改变了一个人就在那多愁善感而初次等待的青春") draft = CharLM(corpus, order=1) # 小模型:只看前 1 个字符,快但粗糙 target = CharLM(corpus, order=3) # 大模型:看前 3 个字符,更准但更慢 prompt = "春天的花" need = 40 greedy = greedy_decode(prompt, target, need) spec, rounds = speculative_decode(prompt, draft, target, draft_len=6, need=need) match = spec[:len(prompt) + need] == greedy print("逐字生成:", greedy) print("投机解码:", spec[:len(prompt) + need]) print("输出完全一致:", match) print("目标模型前向次数:投机解码 = %d,逐字生成 = %d" % (rounds, need)) print("速度提升约:%.2f 倍(按前向次数估算)" % (need / rounds)) if __name__ == "__main__": main()
draft = CharLM(corpus, order=1)小模型只看前 1 个字符,生成快但粗糙,对应“学徒”。target = CharLM(corpus, order=3)大模型看前 3 个字符,更准但更慢,对应“主厨”。cand.append(draft.sample(ctx))草稿模型逐个采样出候选 token,连成一串“菜单”。ver = verify_round(x, cand, target)目标模型一次性验证整串候选,概念上等价于一次并行前向。if cand[i] == ver[i]:逐个验收,第一个不一致处就是“拒绝点”。x += ver[i]被拒位置改用目标模型自己的 token,保证输出与主厨亲自做的一致。spec[:len(prompt) + need] == greedy验证投机解码与逐字生成的结果完全一致,体现“无损”。need / rounds用前向轮数估算加速倍数,直观展示 2 倍左右的收益。
常见误区
误区用一个小模型打草稿,等于给大模型“降级”,输出文本的质量多少会打折扣。
正解投机解码使用拒绝采样:当草稿模型某个字的概率高于目标模型时,以 p/q 的概率随机放行;被拒后从“目标分布减去草稿分布”的剩余部分重新采样。数学上可证明,最终输出分布与目标模型直接生成完全一致——味道不是“差不多”,而是“一模一样”,这是无损加速。
为何常见:直觉上“小模型参与 = 质量下降”,忽略了验证环节的纠错与重采样设计。
误区反正草稿会被验证纠错,草稿模型随便选个最小的,越小越快。
正解草稿太小会导致接受率骤降:写十个字被拒九个,大模型每轮只推进一两个字,草稿成本却照付,加速变成负优化。选型要看“分布接近 + 低延迟”的综合性价比,通常比目标小一到两个数量级的同源模型最合适。
为何常见:把“小”等同于“快”,忽略了接受率对整体收益的决定性影响。
误区只有和目标模型结构一样的模型才能当草稿,否则大模型“看不懂”。
正解约束其实只有一个:共享同一套词表/分词器。架构可以不同,甚至可以是 n-gram 统计模型或检索式草稿。真正影响配合效果的是分布接近度,而不是结构相似度。
为何常见:把“训练时的结构对齐”误解成了“推理时的协同前提”。
误区只要打开投机解码开关,速度翻倍还不花任何代价。
正解草稿模型的生成有算力开销;目标模型一次处理 K 个 token 的计算量也高于处理 1 个 token;验收失败时的重采样同样要成本。好在这些开销通常远小于省下的逐字前向,净收益显著,但绝不是零成本。
为何常见:只比较了“大模型前向次数”,漏算了草稿推理与批处理带来的额外计算。
误区教程里都用 greedy 举例,采样模式(temperature > 0)下投机解码就不灵了。
正解拒绝采样机制天然支持随机采样,无损性在最一般的采样设定下依然成立。只是温度越高,草稿与目标分布都越“摊平”,接受率下降、收益变小,所以工业实践中常配合低温度使用。
为何常见:大多数介绍文章为简洁只用 argmax 讲解,导致读者以为该方法绑定贪婪模式。
面试题
Q1. 投机解码的核心思路是什么?
- A.小模型草稿加大模型验证← 正确答案
- B.让大模型一次生成多个候选词
- C.用剪枝去掉低概率的候选词
- D.用蒸馏把大模型变小来加速
答案:A。投机解码的本质是“小学徒预先写菜,主厨一把验菜”:草稿模型快速写出候选 token,目标模型一次前向并行验证并纠错。B 只做了半步没有验证,C 是剪枝、D 是蒸馏,都不是投机解码的思路。
Q2. 投机解码中草稿模型的主要职责是什么?
- A.独立完成全部文本的生成
- B.快速写出待验证的候选序列← 正确答案
- C.最终决定每个位置的选词
- D.压缩目标模型的权重规模
答案:B。草稿模型只负责“打前站”,快速生成一长串候选 token,真正的裁决权永远在目标模型手里。A 和 C 把最终决定权交给了草稿模型,D 与投机解码无关。
Q3. 关于投机解码的“无损”,下列说法正确的是?
- A.输出文本与目标模型精度略有差异
- B.输出分布与目标模型完全不可区分← 正确答案
- C.只在贪婪解码下才能保持无损
- D.需要通过蒸馏保持草稿一致性
答案:B。拒绝采样机制保证最终输出分布与目标模型直接生成一致,这就是“无损”。C 错在采样模式下定理同样成立;A 与“无损”矛盾;D 的蒸馏只是可选优化手段,不是无损的必需条件。
Q4. 目标模型验证 K 个候选 token,需要几次完整前向传播?
- A.与候选数相同的 K 次
- B.只需要 1 次并行前向← 正确答案
- C.需要 2 次以确保正确
- D.需要 K+1 次完整验证
答案:B。投机解码的核心就是把 K 个候选位置合并成一次并行前向处理,目标模型“一把尝一桌菜”。选 A 是把逐字生成的开销误套过来;C、D 没有依据。
Q5. 以下哪个因素最能决定草稿 token 被接受的概率?
- A.草稿与目标分布的接近程度← 正确答案
- B.目标模型参数量的大小
- C.显卡显存容量的高低
- D.训练语料来源的多少
答案:A。接受率 α 直接由草稿分布 q 与目标分布 p 的距离决定,分布越近接受率越高,正如学徒越懂主厨口味越容易被放行。参数量、显存、语料都是间接因素。
Q6. 草稿模型选型需要满足哪些条件?
- A.与目标模型共享同一张词表← 正确答案
- B.草稿模型的前向延迟更低← 正确答案
- C.草稿分布接近目标模型分布← 正确答案
- D.参数量必须小目标一百倍
答案:ABC。共享词表保证“主厨看得懂”,低延迟保证草稿成本占比小,分布接近保证接受率高,三者缺一不可。D 错在“必须小一百倍”——大小是权衡结果而非硬性条件。
Q7. 当草稿模型对某 token 的概率 q(x) 高于目标模型概率 p(x) 时,该 token 被接受的概率是?
- A.以 p(x)/q(x) 的概率接受← 正确答案
- B.以 q(x)/p(x) 的概率接受
- C.无条件接受这个草稿 token
- D.无条件拒绝这个草稿 token
答案:A。接受规则为 min(1, p/q)。当 q > p 时,说明学徒“自作多情”,只按 p/q 的比例随机放行,这与拒绝采样的补偿机制一起保证了无损。B 会得到大于 1 的概率,没有意义;C 只在 p ≥ q 时才成立;D 过于绝对。
Q8. 下列哪些做法能提升投机解码的加速效果?
- A.让草稿与目标分布更对齐← 正确答案
- B.根据上下文动态调整长度← 正确答案
- C.把草稿长度缩短为一个字
- D.停止投机只用逐字生成
答案:AB。A 提高接受率直接提升每轮验收数;B 动态调整 K 可兼顾单轮收益与尾部接受率衰减。C 把 K 缩到 1 等于退回逐字生成,失去并行优势;D 完全放弃投机自然没有加速。
Q9. 草稿长度 K=4、接受率 α=0.8 时,每轮期望接受约多少个 token?
- A.每轮平均接受约 2.0 个 token
- B.每轮平均接受约 3.4 个 token← 正确答案
- C.每轮平均接受约 4.0 个 token
- D.每轮平均接受约 5.0 个 token
答案:B。代入公式 E[N] = (1-α^(K+1))/(1-α) = (1-0.8^5)/0.2 = (1-0.32768)/0.2 ≈ 3.36。一轮平均确认约 3.4 个字,所以相比逐字生成有显著加速。选 C 或 D 是忽略了“越往后越难全中”的衰减。
Q10. 投机解码相比普通自回归解码的优势有哪些?
- A.减少目标模型的前向次数← 正确答案
- B.显著降低端到端的推理延迟← 正确答案
- C.让输出分布变得更确定
- D.让模型学会写更长文本
答案:AB。核心优势是把多次逐字前向合并成“一次验一串”,从而减少目标模型前向次数、降低延迟和成本。C 改变输出分布恰恰违反无损原则;D 与投机解码无关。