对应 Maps to:Stanford CS349E / EE339 Efficient ML Inference at Scale(Autumn 2026),Lecture 6 · Thu Oct 8 · Optimizing Autoregressive Decode(Zain Asgar)。阅读材料:G.-I. Yu et al., Orca: A Distributed Serving System for Transformer-Based Generative Models(OSDI 2022);Y. Leviathan, M. Kalman, Y. Matias, Fast Inference from Transformers via Speculative Decoding(ICML 2023)。 依据说明:课程网站只公开了课表和阅读材料,没有 slides。本讲以两篇阅读材料为主线,另外用到这些公开文献:Chen et al. 2023(DeepMind 同期提出的 speculative sampling)、SpecInfer(Miao et al., ASPLOS 2024)、Medusa(Cai et al., ICML 2024)、EAGLE / EAGLE-3(Li et al., ICML 2024 / 2025)、DeepSeek-V3 技术报告(MTP 用于投机解码的数字)。补充材料是我选的,不代表课堂上实际讲了什么。 配套代码 Code:
code/lecture06/(numpy;一个共享模块 + 5 个脚本,最长的约 15 秒) 和 CS349D 的关系:本仓库 CS349D 第 2 讲(批处理与调度)和第 5 讲(投机解码)读的是同样的论文,偏服务系统;本讲更多从 roofline 和硬件的角度看这两种技术,两边可以互相参照。 预计阅读时间:3–4 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说清楚 decode 慢在哪里:每一步只给每个序列产出 1 个 token,却要把全部权重读一遍;提高 decode 效率的两条路——每步多放几个序列(批处理)和每步给同一个序列多算几个 token(投机解码)——本质上都是在提高算术强度。
- 解释请求级调度的两个问题(早结束的请求不能返回、新请求要等整批结束),以及 Orca 的迭代级调度和选择性批处理怎样解决它们;知道选择性批处理为什么几乎不损失效率(注意力没有权重)。
- 理解 KV cache 的预留在调度里的作用:按最大长度预留浪费显存,按声明的上限预留仍然浪费,不预留又可能死锁——这是第 7 讲的出发点。
- 证明投机采样不改变输出分布;说出接受概率 ;认出两种常见的错误实现。
- 用 Leviathan 的公式算每轮期望产出的 token 数、墙钟加速比、最优草稿长度和运算量的增加;知道这些公式依赖哪些假设。
- 用 roofline 判断投机解码什么时候划算:代价系数 取决于硬件;批一大验证就不再免费;树形验证(Medusa、EAGLE、SpecInfer)在小批时很划算、在大批时不划算。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲会讲 Covered here | 后面再展开 Later |
|---|---|---|
| 第 1 讲 §3.6、§4.2:临界批大小、decode 每步读一遍权重 | Orca:迭代级调度、选择性批处理、KV 预留 | 分页 KV、抢占、前缀缓存(第 7 讲) |
| 第 2 讲 §5:KV cache 与生成循环 | 投机采样的正确性证明、Leviathan 的分析 | 变长注意力内核怎么写(第 10、11 讲) |
| 第 5 讲:TTFT / TPOT、prefill 与 decode 的干扰、分块 prefill | 投机解码的硬件视角;树形验证与各种草稿 | 草稿放到 SRAM 芯片上(第 5 讲 §8 已讲) |
0.3 本讲地图 Map
§1 decode 为什么需要专门优化:一步一个 token,强度太低;两条路
§2 请求级调度的问题:早结束、晚加入、补齐 ← 代码 01
§3 迭代级调度(Orca):每次迭代重新选请求;KV 预留与死锁;流水线 ← 代码 01
§4 选择性批处理:拍平做线性层,拆开做注意力 ← 代码 02
§5 Orca 的结果,以及它之后发生了什么 ← 代码 01
§6 投机解码:猜几个、一次验证 ← 代码 03
§7 正确性:投机采样为什么不改变分布 ← 代码 03
§8 能快多少:Leviathan 的公式与实测 ← 代码 04
§9 硬件视角:c、批大小、上下文长度 ← 代码 05
§10 草稿从哪里来:小模型、n-gram、自草稿、Medusa、EAGLE、MTP;树形验证 ← 代码 03、05
§11 两者放在一起:批处理里的投机解码
§12 现状(2025–2026)
§13–17 小结、常见误区、练习(含答案)、延伸阅读、下一讲预告
0.4 记号 Notation
| 记号 | 含义 |
|---|---|
| , | 目标模型(target)和它给出的下一个 token 的分布(已经按温度、top-k 等调整过) |
| , | 草稿模型(draft / approximation model)和它的分布 |
| 每轮草稿几个 token | |
| 某一个位置上草稿 token 被接受的概率; 是平均接受率 | |
| 代价系数:草稿模型跑一次的时间 / 目标模型跑一次的时间 | |
| 草稿模型每个 token 的运算量 / 目标模型每个 token 的运算量 | |
| 每次目标模型前向平均产出的 token 数(EAGLE 叫 average acceptance length) | |
| initiation / increment | Orca 对 prefill / decode 的叫法 |
1. decode 为什么需要专门优化 · Why decode needs its own optimizations
第 1 讲 §4.2 算过:decode 每一步,批里的每个序列只多出 1 个 token,可全部权重都要从显存里读一遍。Llama-3.1-8B 在一块 H100 上、批大小 1 时,一步大约 6.65 ms(代码 05 演示 1 的 roofline 估计),几乎全部时间在读 16 GB 的权重;矩阵乘法的算术强度约等于 1,而 H100 的临界强度约 295。GPU 的算力 99% 以上闲着。
想让 decode 更划算,就要让”读一次权重”服务更多的 token。有两条路:
- 每一步多放几个序列(批处理)。一步处理 个序列,权重读一次、给 个 token 用,强度约等于 。难点不在数学,在调度:请求随时到达,输入、输出长短不一,有的刚到、有的快完。§2–§5 讲 Orca 怎样解决这个问题。
- 每一步给同一个序列多算几个 token(投机解码)。难点是自回归的依赖:第 个 token 要等第 个出来才知道。投机解码先用便宜的办法猜几个,再让大模型一次验证——验证 个位置和生成 1 个位置一样,都只读一遍权重。§6–§11 讲它为什么正确、能快多少、什么时候划算。
两条路抢的是同一种资源:闲着的算力。批处理把批做大以后,留给投机解码的空间就少了(§9)。这一讲的主线就是这句话。
2. 请求级调度的问题 · The trouble with request-level scheduling
代码:
01_continuous_batching.py演示 1
2.1 当时的服务系统怎么做
Orca 之前,生成式模型的服务通常分两层(论文图 2):上层是通用的推理服务框架(如 Triton Inference Server),负责收请求、凑批;下层是执行引擎(如 NVIDIA FasterTransformer),收到一批之后跑完所有迭代才返回。这对 ResNet、BERT 这种”跑一次就出结果”的模型很合适,对生成式模型却有问题:一个请求要跑几十到几千次迭代,而且每个请求要跑几次事先不知道。
2.2 两个问题:早结束、晚加入
论文图 3 的例子:两个请求 x1、x2 一起进引擎,x2 只生成 1 个 token 就结束了,x1 还要再生成 3 个。引擎是按”批”调度的,于是:
- 早结束的请求不能早返回:x2 已经结束,却要陪着 x1 跑完、和整批一起返回。用户白等。它那一行在后面几次迭代里还在算(图里的”-”)——白算。
- 晚到的请求要等整批结束:批开始之后到达的请求,哪怕 GPU 有空,也只能在队列里等。
还有第三个问题:一批里的输入长度不同,第一次迭代要把它们补齐(padding)到同一个长度才能拼成一个 的张量。
代码 01 演示 1 在 Orca 论文的合成负载上(输入长度 、输出长度 ,泊松到达)数了数请求级批处理”算过的 token 位置”和”真正需要的”:
| 到达速率 | 平均批大小 | 白算的比例 |
|---|---|---|
| 1 req/s | 2.02 | 26.6% |
| 2 req/s | 5.32 | 38.4% |
| 4 req/s | 5.92 | 39.0% |
负载越高、批越满,白算得越多。不过 decode 受带宽限制,多算几行几乎不花时间——白算本身不是主要损失,时间才是:早结束的请求要等,新请求进不来。
2.3 批为什么凑不大:KV 的预留
还有一个更隐蔽的限制。FasterTransformer 在一批开始前,给每个请求按模型的最大序列长度(GPT-3 是 2048)预留 KV cache。GPT-3 13B 每个 token 的 KV 是 KB;一块 A100 40GB 放下 25.7 GB 的权重后,还能放约 12,596 个 token 的 KV(代码按 10% 的预留算)——只够 个请求。论文 §6.1 也说 FasterTransformer 在 13B 模型上用不了 8 以上的批。可实际请求平均只有 个 token,按最大长度预留浪费了大约六分之五的显存。
3. 迭代级调度 · Iteration-level scheduling (Orca)
代码:
01_continuous_batching.py演示 2–4
3.1 思路:每次迭代都重新选
Orca 的第一个想法很简单:调度的粒度从”请求”改成”迭代”。调度器反复做三件事(论文 §3 S1):
- 从请求池里选出这次迭代要跑的请求;
- 让引擎只跑一次迭代(每个请求前进一个 token;刚到的请求处理完整个输入);
- 收回结果:结束的请求立刻返回给用户,没结束的放回池里。
于是:结束的请求在它最后一个 token 出来的那次迭代之后就返回;新到的请求最多等一次迭代就能加入。这就是后来被叫作连续批处理(continuous batching,TGI / vLLM 的叫法)或 in-flight batching(TensorRT-LLM 的叫法)的东西。
3.2 选谁:先到先服务 + 最大批大小
Orca 的选择规则(论文算法 1)刻意简单:
- 迭代级先到先服务(iteration-level FCFS):先到的请求跑过的迭代次数不少于后到的。
- 最大批大小 max_bs:批越大,吞吐的增长越慢、每次迭代越久,所以给一个上限,由运维按延迟要求调。
- KV 预留:一个请求第一次被选中时,按它声明的
max_tokens(输入 + 最多生成几个)预留 KV 空间;预留不下就停止接纳新请求(算法 1 第 23–25 行)。这保证已经接纳的请求一定能跑完。
代码 01 的 run_iteration_level 就是这个算法;每次迭代的时间用第 5 讲的 roofline 模型(StepModel.mixed_time:decode 的请求各 1 个 token、新请求的全部输入、权重读一遍)。
3.3 结果:延迟–吞吐曲线
代码 01 演示 2 在 GPT-3 13B、一块 A100 上复现 Orca 图 10 的形状(每格:吞吐 req/s / 每 token 延迟中位数 ms,“每 token 延迟”= 端到端延迟 / 生成的 token 数):
| 到达 req/s | 请求级 bs=1 | 请求级 bs=8 | 迭代级 bs=8 | 迭代级 bs=16 | 迭代级 bs=32 |
|---|---|---|---|---|---|
| 0.5 | 0.50 / 37 | 0.50 / 27 | 0.50 / 23 | 0.50 / 23 | 0.50 / 23 |
| 1 | 0.69 / 3510 | 1.00 / 46 | 1.00 / 23 | 1.00 / 23 | 1.00 / 23 |
| 2 | 0.63 / 8628 | 1.94 / 171 | 2.00 / 24 | 2.00 / 24 | 2.00 / 24 |
| 4 | 0.60 / 11203 | 1.83 / 2320 | 3.96 / 26 | 3.97 / 26 | 3.97 / 26 |
| 8 | 0.59 / 12488 | 1.72 / 3601 | 4.54 / 748 | 7.79 / 36 | 7.84 / 31 |
| 12 | 0.59 / 12922 | 1.69 / 4053 | 4.33 / 1172 | 7.88 / 343 | 11.55 / 40 |
在”每 token 延迟中位数 ≤ 45 ms(空载的 2 倍)“的要求下二分查找最高速率:请求级 bs=8 只能到 0.94 req/s,迭代级 bs=32 能到 12.4 req/s,吞吐约 13 倍。Orca 论文在 175B、16 块 GPU(含流水线并行)上按”每 token 190 ms”比较,报告 36.9 倍;我们只模拟单卡、没有流水线,倍数取决于延迟要求定在哪里,方向一致。
两个细节值得注意:
- 迭代级调度时,加大 max_bs 只推迟过载点,几乎不增加低负载时的延迟(23 ms 一列不变)——因为批里没人陪跑、也没人等整批。这和论文图 10 一致。
- 请求级 bs=8 的过载点很早,原因之一是 §2.3 的预留:它实际只有 6 的批。
3.4 公平的对照:请求一样长时
如果所有请求的输入、输出长度都一样,“早结束”和”补齐”的问题就消失了(论文图 11)。代码 01 演示 3:
| 负载 | 请求级 bs=8 | 迭代级 bs=8 | 迭代级 bs=32 |
|---|---|---|---|
| 输入 32、输出 32 | 7.87 | 10.80 | 31.27 |
| 输入 256、输出 256 | 0.95 | 1.20 | 3.08 |
(最高吞吐 req/s。)同样 bs=8 时,迭代级只高 26%–37%,剩下的差别来自”新请求要等整批结束”。bs=32 高得多,主要来自更大的批——请求级被 2048 的预留卡在 6。所以 Orca 的收益要分开看:迭代级调度解决”长短不一”,按请求上限预留解决”批凑不大”。
3.5 KV 预留与死锁
代码 01 演示 4 把问题推得更远:假设每个请求声明 max_gen = 512,但实际生成 个就遇到 EOS(真实服务里很常见:用户设了很大的上限)。三种预留方式(最大批 64):
| 速率 | 预留方式 | 平均预留 | 平均实际写入 | 吞吐 | 结果 |
|---|---|---|---|---|---|
| 4 | 按模型最大长度 2048 | 11,488 | 1,783 | 3.74 | 每 token 144 ms |
| 4 | 按 prompt + max_gen(Orca) | 4,672 | 1,885 | 3.97 | 26 ms |
| 4 | 不预留,用多少分配多少 | 1,885 | 1,885 | 3.97 | 26 ms |
| 8 | 按模型最大长度 2048 | 11,981 | 1,860 | 3.46 | 1298 ms |
| 8 | 按 prompt + max_gen(Orca) | 10,782 | 4,350 | 7.77 | 46 ms |
| 8 | 不预留 | 4,370 | 4,370 | 7.84 | 31 ms |
| 12 | 按 prompt + max_gen(Orca) | 11,704 | 4,722 | 7.60 | 384 ms(过载) |
| 12 | 不预留 | 7,724 | 7,724 | – | 第 49.7 秒死锁,只完成 516 个 |
(单位:token;12 req/s 时按 2048 预留的那一行略去。)读这张表:
- 按 2048 预留:显存几乎全被预留占满,真正写入的不到六分之一。
- Orca 的预留好得多,但仍是实际写入量的约 2.5 倍——请求声明的上限远大于实际用量。这就是第 7 讲 PagedAttention 论文说的”预留浪费”(reservation waste)。
- 不预留:显存全给真正写入的 KV,延迟最低。但负载一高,正在生成的请求把显存占满了,每个请求下一步都还要再写 1 个 token 的 KV——谁都动不了。没有抢占(把某些请求换出到 CPU,或者丢掉以后重算)就会死锁。Orca 选择预留,就是为了避开这个问题(论文 §4.2 说”naïve 实现会让调度器陷入死锁”)。
第 7 讲的答案是两件事一起做:KV 按小块(page)按需分配,不预留;显存不够时抢占一部分请求。
3.6 流水线并行:不需要切微批
Orca 还把迭代级调度用在了流水线并行上(论文 §4.2、图 8)。FasterTransformer 做流水线时,要把一批切成几个微批(microbatch)轮流送进各段,否则后面几段会闲着;可微批越小,每段的批处理效率越低——两难。Orca 的调度器不等一批返回就注入下一批(直到在飞的批数等于流水线段数),每一批都是”当前最该跑的那些请求”;某个请求的下一次迭代只需要等它自己上一次迭代返回。所以不需要把批切小,流水线也不会空。我们的模拟只有一块 GPU,没有模拟这一点——这是论文的 36.9 倍比我们的 13 倍大的原因之一。
Orca 的另一个工程细节:控制消息(这次迭代有哪些请求、各自在哪个位置)走 gRPC,张量数据走 NCCL,避免每次迭代 CPU 和 GPU 之间的同步。论文图 9c 显示这让 175B 模型的引擎比 FasterTransformer 快最多 47%。
4. 选择性批处理 · Selective batching
4.1 问题:形状对不上
迭代级调度带来一个新问题:每次迭代选出来的请求处在不同的状态,它们的张量形状不一样,没法拼成一个 的批。论文 §3 C2 列了三种情况:
- 都在 initiation(prefill),但输入长度不同;
- 都在 increment(decode),但已经处理的 token 数不同——KV cache 的长度不同;
- 一个在 initiation、一个在 increment——一个要处理几百个 token,一个只处理 1 个。
只有”同一阶段、同样长度”的请求才能按传统方式批处理。批越大,凑巧形状完全一样的概率越小。
4.2 办法:拍平做线性层,拆开做注意力
Orca 的观察:Transformer 里大部分运算是逐 token 的——线性层、LayerNorm、残差加法、激活函数——它们根本不需要知道哪个 token 属于哪个请求。只有注意力需要:一个 token 只能看同一个请求里的 token。所以(论文图 5):
- 把这次迭代所有请求的新 token 拼成一个 的矩阵——没有”批”这一维;
- 线性层、归一化等直接在这个矩阵上算;
- 注意力之前 Split:按请求切开,每个请求和自己的 KV cache 做注意力;
- 注意力之后 Merge:拼回 ,继续往下算。
代码 02 演示 1 在第 2 讲的 numpy Transformer 上复现论文图 5 的例子:x1、x2 在 decode(KV cache 里分别有 3 个、1 个 token),x3、x4 刚到(输入 2 个、3 个 token),一共 7 个 token:
| 请求 | 阶段 | 已有 KV | 新 token | 注意力分数矩阵 | 和单独计算的最大差 |
|---|---|---|---|---|---|
| x1 | increment | 3 | 1 | 3.6e-15 | |
| x2 | increment | 1 | 1 | 4.9e-15 | |
| x3 | initiation | 0 | 2 | 0 | |
| x4 | initiation | 0 | 3 | 0 |
和”每个请求单独前向”一致到 (只是加法顺序不同)。硬补齐的话要 个 token 位置(有用的 7 个),注意力分数要 个(有用的 19 个),还要额外的掩码。
4.3 为什么几乎不损失效率:注意力没有权重
拆开做注意力,是不是放弃了批处理的好处?代码 02 演示 3 记了一笔账(同样 7 个 token,选择性批处理一次 vs 4 个请求各自单独前向,TINY 模型、BF16 计):
| 运算 | FLOPs | 读权重:选择性 | 读权重:单独 | 读写 KV |
|---|---|---|---|---|
| 线性层(含 lm_head) | 10.6 M(两者相同) | 1.52 MB | 6.06 MB | 0 |
| 注意力核心 | 38.9 K(两者相同) | 0 | 0 | 5.63 KB(两者相同) |
批处理在 decode 时省的是读权重(第 1 讲 §4.2):权重读一次、给很多 token 用。注意力没有权重,它读的是每个请求自己的 KV——本来就不能共享。所以拆开做注意力,字节一点没多(论文 §3:“applying batching to Attention has no benefit of reducing the amount of GPU memory reads”)。论文图 9 的微基准里,Orca 引擎不批处理注意力,和 FasterTransformer 全部批处理相比只是”相近或略慢”。
4.4 但内核启动不是免费的
账本没算的一项是内核启动。如果真的每层给每个请求单独启动一个注意力内核,代码 02 演示 4(GPT-3 13B、A100)算了一下:
| 这次迭代里有 | 选择性批处理 | 每个请求单独前向 | 注意力逐请求启动(3 µs / 10 µs) |
|---|---|---|---|
| 32 个请求在生成 | 29.29 ms | 700.23 ms(24×) | +13% / +44% |
| 32 个在生成 + 2 个新请求(输入 300、450) | 108.97 ms | 805.75 ms(7×) | +4% / +12% |
纯 decode 的迭代里,40 层 × 32 个请求 = 1280 次启动,每次几微秒,加起来是这次迭代的 13%–44%。所以 Orca 实现时把各个请求的注意力内核拼成一次启动:不同请求的线程块放进同一个内核(论文 §5;它承认这违反”一个内核里的线程块应该做同样的事”的惯例,但实测更快)。
今天的引擎走得更远:FlashAttention 的 varlen 接口、FlashInfer、vLLM 的 PagedAttention 内核,一次启动就处理一批长短不一的请求,每个请求的 KV 按块存放、用一张表找到(第 7 讲)。这就是选择性批处理的现代形态。第 5 讲的分块 prefill(Sarathi-Serve)也站在它的肩膀上:既然 prefill 和 decode 的 token 能放进同一次迭代,就可以控制每次放多少 prefill token。
5. Orca 的结果,以及它之后 · Orca’s results and what came after
论文的端到端实验(GPT-3 13B / 101B / 175B / 341B,最多 32 块 A100):在 175B 模型上,要达到每 token 190 ms 的中位数延迟,FasterTransformer 只有 0.185 req/s,Orca 有 6.81 req/s——36.9 倍。而且 Orca 加大最大批大小只提高吞吐,不明显增加延迟。
四点要记住的:
- 调度粒度是这篇论文最持久的贡献。今天所有主流引擎都用迭代级调度,区别只在”每次迭代选谁、放多少 token”。
- 选择性批处理的思想被专门的变长注意力内核继承下来。
- KV 预留是 Orca 留下的问题:它按
max_tokens预留来避免死锁,但预留浪费显存,限制了批大小。vLLM 的 PagedAttention(第 7 讲)一年后专门解决这个问题。 - 论文的实验假设所有请求都生成到 max_gen(“我们让模型永远不输出 EOS”,因为没有真实的模型权重),所以它看不到预留浪费——演示 4 的问题在它的实验里不存在。
6. 投机解码:猜几个,一次验证 · Speculative decoding
6.1 直觉:处理器里的分支预测
处理器遇到条件分支时,不等条件算出来,先猜一个方向往下执行;猜对了,省下等待的时间;猜错了,把猜着执行的结果丢掉重来。这叫投机执行(speculative execution)。只要猜对的概率够高、猜错的代价不大,平均就更快。
Leviathan 等人(以及同期 DeepMind 的 Chen 等人)把它搬到了自回归生成上,基于两个观察:
- 有些 token 很容易:句号之后多半是空格或换行,“New York”之后多半是”City”,代码里的括号要配对。一个小得多的模型也能猜对。
- 验证比生成便宜:给大模型一段”已经猜好的”序列 ,它可以一次前向同时算出每个位置上自己的分布 ——就像 prefill 一次处理整段提示。decode 受带宽限制,一次前向送进 个 token 和送进 1 个 token 读的权重一样多,时间几乎一样(§9 量化这个”几乎”)。
所以:小模型先连续猜 个 token,大模型一次前向检验它们,留下猜对的那一段。每次大模型前向至少产出 1 个 token(不会比普通 decode 更慢地前进),运气好时产出 个。
6.2 算法
Leviathan 算法 1(一轮):
- 草稿:用 自回归地抽 个 token:。
- 验证:用 一次前向算出 ,其中 。
- 逐个检验:对 :抽 ,若 就接受 ,否则停下。设接受了 个。
- 补一个:若 (第 个被拒绝),从 抽一个 token;若全部接受,从 白拿一个。
- 输出 加上补的那一个。
实现时还要处理 KV cache:草稿模型和目标模型都为被拒绝的位置写过 KV,要回滚。代码 decode_lab.speculative_generate 用一个”待定 token”不变式:两个模型的 cache 里都存着已确定序列去掉最后一个 token 的部分;每轮开始时把最后一个已确定的 token 和 个草稿一起喂给目标模型;结束时把 cache 的长度改回”新的已确定序列 − 1”。第 2 讲的 KVCache 总是从 length 往后写,所以回滚就是把 length 改小,后面的旧数据下次会被覆盖。(分页的 KV 也是改块表里的长度,第 7 讲。)
7. 正确性:投机采样为什么不改变分布 · Why speculative sampling is exact
7.1 一步的证明
只看一个位置。草稿抽到 的概率是 ,接受它的概率是 ,所以”草稿抽到 并被接受”的概率是
被拒绝的总概率是 ,其中
被拒绝后从 里抽——注意 ,归一化常数正好是 。所以输出 的总概率是
直觉:草稿在某些 上”给多了”(),接受规则按 的比例把多出来的部分退掉;在另一些 上”给少了”(),差额 正好由重新采样补上。多轮的情形:被接受的 服从 ;给定 ,下一个位置用的是 ,同样的论证再来一遍——归纳下去,整个序列的分布和 自己生成的一样。
代码 03 演示 1 不抽样、直接按概率算出每个位置的输出分布,和 比较(第 3 讲的模型、8 条序列 × 64 个位置):
| 草稿 | 的平均(温度 1) | |
|---|---|---|
| INT8 量化的自己 | 4.4e-16 | 0.993 |
| INT4 量化的自己 | 5.6e-16 | 0.900 |
| 只用前 3 层 | 6.7e-16 | 0.827 |
| 只用前 1 层 | 4.4e-16 | 0.699 |
| 二元语法表 | 5.6e-16 | 0.341 |
| 无关的小模型 | 4.4e-16 | 0.160 |
草稿再差,输出分布也是 ——差的草稿只是接受得少、跑得慢,不会让输出变差。这是投机解码最重要的性质:它是一种纯粹的系统优化,模型的行为不变,所以可以默认打开、不需要重新评估模型质量。
7.2 接受概率 = 1 − 总变差距离
有一个漂亮的解释。总变差距离(total variation distance) 衡量两个分布差多少(0 = 相同,1 = 完全不重叠)。因为 ,
论文把 记作 (定理 3.5、推论 3.6:)。代码 03 演示 1 的最后一列验证了 在 量级。所以”一个草稿好不好”有一个精确的度量:它的分布和目标的总变差距离。
7.3 采样设置:p 和 q 要一起调整
温度、top-k、top-p(nucleus)这些采样方式,都可以写成”从一个调整过的分布里采样”(论文 §2.2 的”标准化采样”):贪心 = 把非最大值清零再归一化;温度 = logits 除以 再 softmax;top-k = 把第 名之后的置为 。投机采样对调整后的 和 做,输出就服从调整后的 。代码 03 演示 1 在温度 0.5、温度 1 + top-50 下也验证到了 。要点是两边用同样的调整;如果只调整 、用原始的 去抽草稿,规则依然正确(证明里 是什么都行),只是接受率会变低。
7.4 贪心的特例
温度 0(贪心)时 、 都是 one-hot:接受 ⇔ 草稿的 token 恰好是目标的 argmax;被拒绝时 的唯一正项就是目标的 argmax。所以输出和目标模型自己贪心生成的逐 token 相同。代码 03 演示 4:三种草稿、4 个提示、各 64 个 token,输出全部相同;目标模型的前向次数从 64 次降到 19.2 次(INT4 草稿)、24.8 次(跳层草稿)、53 次(二元语法表)。这是最好写单元测试的情况,真实引擎也是这么测的。
7.5 两种常见的错误实现
代码 03 演示 2 在一个极小的模型(词表 64)上跑了 4 万个独立的投机解码(,温度 1),统计前两个输出 token 的联合分布(4096 格),和目标模型的精确分布比:
| 做法 | TV(联合分布) | TV(第一个 token) |
|---|---|---|
| 直接从精确分布抽 4 万个(噪声水平) | 0.0629 ± 0.0014 | – |
| 投机解码(正确的规则) | 0.0646 | 0.0123 |
| 错误 1:被拒绝后直接从 重抽 | 0.1817 | 0.1191 |
| 错误 2: 才接受(不掷骰子) | 0.6090 | 0.3998 |
正确的规则和”直接从正确分布抽样”的噪声水平相当。两个错误:
- 被拒绝后从 重抽:草稿偏爱的 token 先通过接受规则拿到了 ,重抽时又按 再给一次,被多算了。具体到第一个 token 偏得最多的 token 6:正确概率 0.167,错误 1 给了 0.121,错误 2 给了 0.310。
- 去掉随机性: 就一律拒绝,相当于丢掉了 里本该接受的那部分,偏差最大。
所以那个看起来很怪的残差 不是装饰,它正好补上” 给少了”的那部分概率。
8. 能快多少 · How much faster
8.1 每轮产出几个 token
假设每个位置被接受的概率 独立同分布、均值 。第 个草稿被接受,要求前 个都被接受(概率 ),所以每轮产出的 token 数(被接受的草稿 + 补的那一个)的期望是
时趋于 : 时最多 5 个, 时最多 10 个。这个上界来自”一错就停”:草稿再长,越往后越难全部猜中。
8.2 墙钟时间与最优的 γ
一轮的时间 = 次草稿 + 1 次验证。设草稿一次的时间是目标一次的 倍(代价系数),并假设目标模型一次验证 个 token 和一次普通 decode 一样快,那么(定理 3.8)
代码 04 演示 1 复现了论文表 1(),逐格一致:
| 速度 | 运算量 | ||
|---|---|---|---|
| 0.6 | 2 | 1.96× | 1.53× |
| 0.7 | 3 | 2.53× | 1.58× |
| 0.8 | 2 | 2.44× | 1.23× |
| 0.8 | 5 | 3.69× | 1.63× |
| 0.9 | 2 | 2.71× | 1.11× |
| 0.9 | 10 | 6.86× | 1.60× |
草稿不免费时,最优的 在”多猜几个”和”草稿的代价”之间权衡(论文图 3)。代码 04 演示 2 的表(每格:最优 和加速比):
| \ | 0.01 | 0.05 | 0.1 | 0.2 | 0.5 |
|---|---|---|---|---|---|
| 0.6 | 7, 2.30× | 4, 1.92× | 3, 1.67× | 2, 1.40× | 1, 1.07× |
| 0.8 | 14, 4.23× | 8, 3.09× | 6, 2.47× | 4, 1.87× | 2, 1.22× |
| 0.9 | 24, 7.49× | 13, 4.67× | 10, 3.43× | 7, 2.37× | 3, 1.38× |
推论 3.9:只要 , 就有加速,至少 倍; 时不赚不赔。
不是模型的属性,它取决于硬件和实现(论文 §3.3)。论文里草稿比目标小两个数量级, 总是小于 0.05。但 §9 会看到,Llama-3.2-1B 配 Llama-3.1-8B 在 H100 上 ——草稿只读 15% 的字节,可每一步的固定开销(内核启动、调度、采样)是一样的。同一个草稿放到 SRAM 芯片上, 可以小十几倍(第 5 讲 §8.3)。
8.3 独立同分布的假设
式 (1) 把每个位置的接受概率当成独立的。它其实不要求每个位置的 一样——只要各位置独立, 依然成立。真正的假设是独立。
代码 04 演示 3 在第 3 讲的模型上真的跑了投机解码(温度 1、8 个提示 × 96 个新 token),把”每次目标前向产出的 token”和式 (1) 用实测 算出的预测比:
| 草稿 | 实测 | ||||
|---|---|---|---|---|---|
| INT4 量化的自己 | 0.900 | 1.88 / 1.90 | 2.68 / 2.71 | 3.73 / 4.10 | 6.05 / 6.13 |
| 只用前 3 层 | 0.827 | 1.82 / 1.83 | 2.53 / 2.51 | 3.43 / 3.54 | 4.74 / 4.73 |
| 只用前 1 层 | 0.699 | 1.69 / 1.70 | 2.17 / 2.19 | 2.92 / 2.77 | 3.10 / 3.19 |
| 二元语法表 | 0.341 | 1.37 / 1.34 | 1.43 / 1.46 | 1.55 / 1.51 | 1.49 / 1.52 |
(每格:实测 / 预测。)在抽样误差以内一致,有高有低。这个随机初始化的模型里,相邻位置的难度几乎不相关,所以公式成立。真实文本不同:难的位置往往成片出现(一段需要推理的话、一个罕见的专有名词),而一轮要求”前面全部接受”,成片的难位置会让后面整串作废。CS349D 第 5 讲的模拟里,接受成片出现时把实测 代入式 (1) 会高估约 5%。实践中的建议:直接测”每次验证产出几个 token”(EAGLE 叫 ),不要只测 。
论文还算了一个上界:如果有一个”神谕”告诉你每一步该草稿几个(猜对的那一段有多长就草稿多长),期望产出变成 ,比固定 最多高约 60%(§3.5)。这为后来”动态决定草稿长度 / 树形状”的工作(EAGLE-2 等)留了空间。
8.4 运算量增加,读权重减少
一轮里目标模型算了 个位置,平均只留下式 (1) 那么多,被拒绝之后的都白算了。总运算量的增加倍数(定理 3.11)是
但读权重的次数减少了:一次前向读一遍权重,产出好几个 token(§3.4:“the number of memory accesses … can go down”)。代码 04 演示 4(跳层草稿,,只看目标模型的矩阵乘法,不含提示预填充):
| 做法 | 目标模型运算量 | 定理 3.11() | 目标模型读权重 |
|---|---|---|---|
| 普通 decode | 1.00× | – | 1.00× |
| 投机 | 1.21× | 1.20× | 0.41× |
| 投机 | 1.35× | 1.41× | 0.28× |
| 投机 | 1.90× | 1.90× | 0.22× |
这张表是理解投机解码的钥匙:它用更多的运算换更少的读取。decode 受带宽限制、算力闲着,这笔交易就划算——和批处理是同一个道理(提高算术强度),只不过”批”是同一个序列里的几个位置。算力不闲的时候,这笔交易就亏了(§9)。
8.5 真实模型上的 α
- Leviathan 表 3:草稿比目标小约两个数量级时, 一般在 0.5–0.9;连一元、二元语法表都有非零的 (T5-XXL 英德翻译、二元语法表 , 时快 1.25 倍)。
- Leviathan 表 2(T5-XXL 11B,TPU v4,批大小 1):T5-small(77M)当草稿最好,翻译任务温度 0 时快 3.4 倍、温度 1 时 2.6 倍;摘要任务 3.1 / 2.3 倍。比 T5-large 当草稿更快——更准的草稿太贵了。
- EAGLE 表 2(MT-bench):每个位置的接受率约 0.64–0.79,平均接受长度 在温度 0 时 3.6–4.0、温度 1 时 3.2–3.5。
一个温度上的反例:真实模型上 在温度 0 时更高(Leviathan:“the sharper the adjusted distribution, the higher the α values”;EAGLE 表 2 也是)。代码 03 演示 3 在随机初始化的模型上看到的却相反:
| 草稿 | |||
|---|---|---|---|
| INT4 量化的自己 | 0.900 | 0.841 | 0.783 |
| 只用前 3 层 | 0.827 | 0.728 | 0.643 |
| 二元语法表 | 0.341 | 0.218 | 0.201 |
原因: 是”第一名相同”的比例, 是 。随机模型的分布很平(第一名的平均概率只有 0.107),小扰动很容易换掉第一名,整体形状却变化不大。真实语言模型相反:大部分位置有一个明显”容易”的答案,大小模型都会选它,分歧主要在尾部低概率的词——第一名的一致率高于 。这是玩具模型和真实模型的差别,提醒我们:机制可以在玩具上验证,数字不能搬。
9. 硬件视角:什么时候划算 · The hardware view
Leviathan 的分析有两个关于硬件的假设:(1) 验证 个 token 和普通 decode 一步一样快;(2) 是已知常数。用 roofline 看看它们什么时候成立。
9.1 批大小 1:验证几乎免费,但 c 不小
代码 05 演示 1:Llama-3.1-8B 做目标、Llama-3.2-1B 做草稿(同一个分词器,常见的搭配),一块 H100,批大小 1,上下文 1024,roofline 时间 + 固定开销(目标 1 ms、草稿 0.3 ms,假设值):
- 目标模型普通 decode 一步 6.65 ms;草稿一步 1.23 ms,。
- 目标模型一次送进 个 token: 到 都是 6.65 ms(前 129 个 token 都在”读权重”的阴影里), 才涨到 7.76 ms。
所以假设 (1) 在批大小 1 时完全成立;最优 和加速比由 和 决定:
| 最优 | 每轮 token | 加速比 | |
|---|---|---|---|
| 0.6 | 2 | 1.96 | 1.43× |
| 0.7 | 3 | 2.53 | 1.63× |
| 0.8 | 4 | 3.36 | 1.93× |
| 0.9 | 7 | 5.70 | 2.48× |
把加速比压在 2 倍左右。想更快,要么草稿更便宜(单层草稿、n-gram、放到 SRAM 芯片上),要么更准,要么一次验证更多候选(§10.3 的树)。
9.2 批变大:验证不再免费
代码 05 演示 2(;每格:最优 、加速比、括号里是”验证的时间 / 普通一步”):
| 批大小 | 上下文 512 | 上下文 4096 | 上下文 32768 |
|---|---|---|---|
| 1 | 4, 1.93×(1.00) | 4, 1.92×(1.00) | 4, 1.88×(1.00) |
| 16 | 4, 1.91×(1.00) | 4, 1.84×(1.00) | 放不下 |
| 64 | 3, 1.85×(1.00) | 4, 1.76×(1.00) | 放不下 |
| 128 | 2, 1.60×(1.11) | 放不下 | 放不下 |
| 256 | 1, 1.38×(1.09) | 放不下 | 放不下 |
| 512 | 1, 1.10×(1.41) | 放不下 | 放不下 |
批大小 512、上下文 512 时,普通一步 19.45 ms,一次验证 5 个 token 要 66.92 ms(3.4 倍): 个 token 让矩阵乘法变成算力受限(H100 按 60% / 80% 的效率,临界约 220 个 token,第 1 讲 §3.6)。验证的时间开始按 增长,最优 缩到 1,加速比缩到 1.1 倍。
长上下文时,显存先装不下大批(32K 上下文最多十几个序列),decode 一直受带宽限制——而且同一个序列的 个 query 共享一次 KV 读取(融合注意力内核把它们放在同一块里)——验证始终几乎免费。所以投机解码在长上下文、小批、交互式的场景里最值钱;在离线批处理、大批量的场景里收益有限,甚至可能变慢。
9.3 批处理和投机抢同一种资源
把 §1 的话再说一遍:连续批处理和投机解码都在利用 decode 时闲着的算力。连续批处理把批做大以后,闲着的算力少了,留给投机的空间就少了。服务系统因此要按负载决定:低负载时用投机换延迟,高负载时减小 或关掉投机换吞吐。vLLM、SGLang、TensorRT-LLM 都支持这类调整;有研究专门按 goodput 动态选择每个请求的草稿长度。
另外两个容易忽略的代价:
- 草稿模型占显存:它的权重和它自己的 KV cache 都要放在 GPU 上,减少了目标模型能放的序列数(本讲的模拟没有计入)。
- MoE 的陷阱:验证 个 token 时,每个 token 各选几个专家,要读的专家变多(第 5 讲 §8.3)。EAGLE 在 Mixtral 8x7B 上只快 1.5 倍(表 3),论文给的解释正是这个。
10. 草稿从哪里来 · Where drafts come from
10.1 一个独立的小模型
最直接的做法(Leviathan、Chen et al.):同一系列、同一个分词器的小模型,比如 Llama-3.2-1B 配 Llama-3.1-8B / 70B。优点是现成;缺点是 不小(§9.1),而且要单独部署、占显存。可以在目标模型的输出上蒸馏草稿以提高 (DistillSpec 等)。
10.2 不需要训练的草稿
- n-gram / 查表:。Leviathan 发现二元语法表都有 。
- 提示查找(prompt lookup decoding):在已有的上下文里找和最近几个 token 匹配的片段,把它后面的内容当草稿。对摘要、改写代码、多轮对话里”重复已有内容”的输出特别有效(Leviathan §3.6 已经提到这个思路)。
- 自草稿(self-speculative):用目标模型自己的一部分当草稿——跳过一些层(Draft & Verify、LayerSkip),或者用量化版的自己。代码 03 演示 3 的数字:INT4 量化的自己 ,只用前 3 层 0.83;但代码 04 演示 4 指出,跳层草稿的 ,太贵。自草稿的优点是不用额外的模型和显存。
10.3 额外的预测头和树形验证:Medusa、EAGLE、SpecInfer
Medusa(Cai et al. 2024)在目标模型最后一层的隐状态上加 个额外的”头”,第 个头直接预测第 个 token。草稿几乎免费(几个小矩阵),但越远的头越不准。为了弥补,它在每个位置取头的前几名,组合成一棵候选树,用一次前向验证整棵树——这需要树形注意力掩码:每个节点只看得见自己的祖先。Medusa-1(冻结主干,只训练头)报告 2.2 倍以上,Medusa-2(和主干一起训练)2.3–2.8 倍。注意:Medusa 还提出了”典型接受”(typical acceptance),在温度 > 0 时放宽接受条件——它不再严格保持输出分布,和 §7 的投机采样不是一回事。
EAGLE(Li et al. 2024)观察到:在特征层(目标模型倒数第二层的隐状态)上做自回归,比在 token 上做更容易;但采样带来不确定性(同一个特征后面可能接”am”也可能接”always”),所以把提前一步的 token 也作为输入。草稿只有一个 Transformer 层。EAGLE 报告在 LLaMA2-Chat 70B 上 2.7–3.5 倍,并保持输出分布。EAGLE-2 按草稿的置信度动态决定树的形状;EAGLE-3(2025)不再预测特征,改为直接预测 token,并融合多层特征(“training-time test”),报告最高 6.5 倍,在 SGLang 里批大小 64 时吞吐提高 1.38 倍。
SpecInfer(Miao et al. 2024)最早系统地提出”token 树”验证:用一个或多个小模型生成多个候选,合并成树,一次验证。
代码 05 演示 3 量化了树的利弊。在第 3 讲的模型上,目标模型贪心生成 64 条序列,草稿在这些真实序列上算出”正确答案在草稿里排第几”;贪心时,树在第 层只要正确答案在草稿的前 名里就能继续,所以接受长度可以精确算出来。再把它换算成 H100 上 Llama-3.1-8B 的加速比(草稿每层的代价假设为目标一步的 5%):
| 草稿形状 | 每轮验证的草稿 token | 弱草稿 | 批 1 | 批 32 | 强草稿 | 批 1 | 批 32 |
|---|---|---|---|---|---|---|---|
| 链 | 4 | 1.83 | 1.52× | 1.52× | 3.33 | 2.77× | 2.77× |
| 链 | 6 | 1.87 | 1.44× | 1.43× | 3.93 | 3.03× | 3.02× |
| 树 2×2×2 | 14 | 2.37 | 2.06× | 1.32× | 3.64 | 3.16× | 2.02× |
| 树 4×2×2×1 | 44 | 2.82 | 2.35× | 0.57× | 4.46 | 3.71× | 0.90× |
| 树 8×4×2×1 | 168 | 3.32 | 2.76× | 0.19× | 4.63 | 3.85× | 0.26× |
(弱草稿 = 只用第 1 层,正确答案在前 1/2/4/8 名里的比例 0.44 / 0.64 / 0.79 / 0.91;强草稿 = INT4 量化的自己,0.79 / 0.94 / 0.98 / 1.00。)
- 树让 明显变大,弱草稿受益更多:它的第一名常错,但前几名常对。
- 代价是每轮验证几十到几百个 token。批 1 时几乎免费,大树加速最多;批 32 时, 几百个 token 让验证变成算力受限,大树比链还慢得多。
- EAGLE 的消融(表 5、图 7):树让平均接受长度多 0.6–0.8、加速比只多 0.3–0.5——接受长度涨得多、加速比涨得少,原因就是每次前向的 token 变多了。
10.4 训练时就为投机做准备:多 token 预测(MTP)
DeepSeek-V3 在训练时加了多 token 预测(MTP)模块:主模型预测下一个 token 的同时,一个小模块预测再下一个。推理时这个模块当草稿用。技术报告说第二个 token 的接受率在 85%–90%,解码速度(TPS)提高到 1.8 倍。此后不少开源模型(如 Qwen3-Next、GLM-4.5)也自带 MTP 层。这代表一个趋势:草稿不再是事后拼上去的,而是模型设计的一部分——又一个”协同设计”的例子。
11. 两者放在一起:批处理里的投机解码 · Speculation inside a continuous batch
真实引擎里,投机解码是在连续批处理的框架里做的,这带来几个工程问题:
- 批里每个序列接受的个数不同:下一轮各序列的长度又不一样了。这正好是选择性批处理 / 变长注意力内核要解决的问题——它天然支持”每个请求这次喂几个 token”不同。
- KV 回滚:被拒绝的位置写过的 KV 要作废。分页的 KV(第 7 讲)里,回滚 = 改块表里的长度、释放多出来的块。
- 调度器要算新的账:一次迭代的 token 数 = 各序列的 之和(加上 prefill)。批大时,§9.2 说验证不再免费,调度器要减小 或关掉投机。
- 草稿和目标的调度:草稿模型可以和目标模型共用 GPU(分时),也可以放在别的设备上(第 5 讲 §8 的异构方案,此时 里还要加通信延迟)。
12. 现状(2025–2026)· State of the field
- 连续批处理是默认:vLLM、SGLang、TensorRT-LLM、TGI 都是迭代级调度 + 变长 / 分页注意力内核;在此之上叠加分块 prefill(第 5 讲)、前缀缓存(第 7 讲)、解耦(第 5 讲)。
- 投机解码是标配:主流引擎都支持独立草稿模型、n-gram / 提示查找、EAGLE 系列、MTP。EAGLE-3 一类的单层草稿在小批时常见 2–4 倍的加速;生产系统按负载自动调整或关闭投机。
- 草稿成为模型的一部分:DeepSeek-V3 的 MTP 之后,越来越多的模型在训练时就带上 MTP 层,推理时直接用于投机。
- 硬件上:投机解码让 decode 更像”小 prefill”(每步处理几个 token),提高了算术强度;异构方案把草稿放到 SRAM 芯片上,让 接近 0(第 5 讲的 Gimlet 博客)。
- 新的方向:并行 / 非自回归的草稿(Jacobi 解码、Lookahead decoding)、扩散语言模型(一次生成一块 token)都在尝试打破”一步一个 token”。
13. 小结 · Summary
- decode 的问题是算术强度太低:每步每个序列 1 个 token,却要读全部权重。批处理(多放序列)和投机解码(同一个序列多放 token)都是提高强度,抢的是同一种资源——闲着的算力。
- Orca 的迭代级调度让结束的请求立刻返回、新请求最多等一次迭代;在 GPT-3 13B / A100 的模拟里,同样的延迟要求下吞吐约是请求级的 13 倍(论文 175B 上 36.9 倍)。
- 选择性批处理把 token 拍平做线性层、按请求拆开做注意力;注意力没有权重,拆开不多读一个字节,但要把各请求的注意力内核合成一次启动。
- KV 预留是调度的约束:按最大长度预留浪费显存;按声明的上限预留仍是实际的约 2.5 倍;不预留又会死锁——第 7 讲用分页 + 抢占解决。
- 投机采样严格保持输出分布:输出 的概率 = ;接受概率 。被拒绝后必须从残差抽样。
- 每轮期望产出 个 token,加速比还要除以 ;运算量增加、读权重减少。公式假设各位置独立,真实文本里难的位置成片出现时会高估。
- 硬件决定划不划算:批小时验证免费,但 常常不小(1B 配 8B 约 0.19);批大到算力受限时验证变贵,投机收益缩水;长上下文、小批时最值钱。树形验证在小批时很划算,大批时很亏。
14. 常见误区 · Common pitfalls
- “投机解码会降低输出质量”——正确实现的投机采样输出分布和目标模型完全一样(§7)。降低质量的是不严格的变体(如 Medusa 的典型接受),要分清。
- “草稿越准越好”——还要看它多贵。Leviathan 的 T5-XXL 实验里,T5-small 当草稿比更准的 T5-large 快(§8.5)。
- “被拒绝后从目标分布重新采样就行”——必须从 的残差采样,否则分布会偏(代码 03 演示 2,TV 从 0.065 涨到 0.18)。
- ” 等于参数量之比”—— 是时间之比,取决于硬件和实现:固定开销、草稿的 KV、跑在哪种芯片上都会改变它(§8.2、§9.1)。
- “投机解码总是能提速”——批大时验证变成算力受限,加速比可以降到 1 附近甚至变慢(§9.2)。
- “选择性批处理放弃了注意力的批处理,所以慢”——注意力没有权重,批处理它不省读取;真正要注意的是内核启动次数(§4.3–4.4)。
- “连续批处理只是把批做大”——它解决的是早结束、晚加入和补齐;同样的批大小上限下,请求一样长时它的收益很小(§3.4)。
- “不预留 KV 最省显存,所以最好”——没有抢占就可能死锁(§3.5)。
- “式 (1) 要求每个位置的接受率一样”——它要求的是各位置独立;接受成片出现时公式会高估(§8.3)。
- “树越大越好”——树越大,每次验证的 token 越多;批 32 时 168 个节点的树比链慢 5 倍以上(§10.3)。
15. 练习 · Exercises
练习 1(请求级批处理的浪费) 一批 3 个请求,输入长度 100、300、200,输出长度 10、50、30。请求级批处理补齐输入、所有行跑到最长的输出结束。算过的 token 位置有多少?真正需要的有多少?白算的比例?
答案:算过的 = ;需要的 = ;白算的比例 。
练习 2(预留决定批大小) GPT-3 13B 在 A100 40GB 上能放 12,596 个 token 的 KV。(a) 按 2048 预留,最多几个请求?(b) Orca 的负载里,平均输入 272、平均输出(= max_gen)64.5,按 prompt + max_gen 预留,平均能放几个请求?(c) 如果请求声明 max_gen = 512 但平均只生成 64.5 个,按 Orca 的方式预留呢?
答案:(a) 。(b) 。(c) ——只有按需分配时的一小半,这就是演示 4 里预留量约是实际写入的 2.5 倍的原因(还有一部分是输入长度不同带来的差别)。
练习 3(手算投机采样) 词表只有 3 个 token,,。(a) 接受概率 ?(b) 残差分布 ?(c) 验证输出分布等于 。
答案:(a) ;也等于 。(b) ,归一化后 。(c) token a:;token b:;token c:。✓
练习 4(错误变体偏多少) 同上的 、,如果被拒绝后直接从 重抽,输出分布是什么?和 的 TV 距离?
答案:被拒绝的概率仍是 0.3。a:;b:;c:。TV 。 偏爱的 b 被多给了。
练习 5(复现论文的加速比) Leviathan 表 2:T5-small 当草稿,英德翻译、温度 0、、,实测快 3.4 倍。(a) 式 (1) 预测每轮产出几个 token?(b) 如果 ,加速比的预测是多少?(c) 反推实测对应的 。
答案:(a) 。(b) 3.60 倍。(c) ——和论文说的” 常常接近 0”一致(实测还有实现细节的误差,论文附录 A.3)。
练习 6(最优 γ 附近很平) 、。分别算 的加速比。最优 选错一些代价大吗?
答案::;:(最优,和演示 2 的表一致);:。最优附近很平,选错几个只损失几个百分点;但 太小损失更多(从 8 降到 4 损失 9%)。
练习 7(运算量) 、。(a) 时总运算量增加几倍?(b) 草稿每 token 的运算量是目标的 10%()呢?
答案:每轮期望 token 。(a) 倍(论文表 1)。(b) 倍。
练习 8(批大时为什么变慢) H100 上 Llama-3.1-8B、批大小 128、上下文 512、,代码 05 算出验证 3 个 token 的时间是普通一步的 1.11 倍。用”临界 token 数约 220”解释为什么不再是 1;批大小 16 时为什么还是 1?
答案:验证一次送进 个 token,超过约 220 的临界值,矩阵乘法的时间由算力决定,随 token 数增长。批 16 时 个 token(),远低于临界值,时间仍由读权重和 KV 决定,和普通一步一样。
练习 9(树的接受长度) 弱草稿的正确答案在前 1、2、4 名里的比例是 0.44、0.64、0.79。树 4×2×2×1:(a) 每轮验证几个草稿 token?(b) 假设各层独立,估算 ,和代码 05 实测的 2.82 比较。
答案:(a) 。(b) ,,和实测 2.82 接近(差别来自各层并不完全独立)。
练习 10(死锁的最小例子) KV 容量 10 个 token,不预留、没有抢占。两个请求同时到达,各有 4 个输入 token,都要生成 5 个 token。按”放得下输入就接纳”的规则,会发生什么?Orca 的预留规则会怎样处理?
答案:两个都被接纳,prefill 后占 8 个(产出各自的第 1 个 token)。下一次迭代各写 1 个 → 10 个(各 2 个 token)。再下一次每个都要再写 1 个,没有空位,两个请求都还差 3 个 token,谁也完不成、谁也不释放——死锁。Orca 按 prompt + max_gen = 4 + 5 = 9 预留:第一个请求预留 9 个,第二个预留不下就等着;第一个完成后释放,第二个再开始。慢一些,但一定能完成。(严格说最后一个 token 不需要写 KV,所以每个请求实际最多用 8 个,预留 9 是保守的。)
练习 11(读权重减少多少) 代码 04 演示 3 里,跳层草稿 时每次目标前向平均产出 4.74 个 token。目标模型读权重的总量应该降到普通 decode 的多少?演示 4 测出 0.22,为什么略大于你的估计?
答案:。演示 4 的读权重里还包括提示预填充的那一次前向(两种做法都有),它让比值往 1 靠了一点;另外最后一轮可能多生成几个 token(被截掉),也多算一次。
16. 延伸阅读 · Further reading
阅读材料
- G.-I. Yu et al., Orca: A Distributed Serving System for Transformer-Based Generative Models, OSDI 2022. https://www.usenix.org/system/files/osdi22-yu.pdf
- Y. Leviathan, M. Kalman, Y. Matias, Fast Inference from Transformers via Speculative Decoding, ICML 2023. https://arxiv.org/abs/2211.17192
投机解码的其他工作
- C. Chen et al., Accelerating Large Language Model Decoding with Speculative Sampling, 2023(DeepMind,同期独立提出). https://arxiv.org/abs/2302.01318
- X. Miao et al., SpecInfer: Accelerating Generative LLM Serving with Tree-based Speculative Inference and Verification, ASPLOS 2024. https://arxiv.org/abs/2305.09781
- T. Cai et al., Medusa: Simple LLM Inference Acceleration Framework with Multiple Decoding Heads, ICML 2024. https://arxiv.org/abs/2401.10774
- Y. Li et al., EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty, ICML 2024. https://arxiv.org/abs/2401.15077 ;EAGLE-3:https://arxiv.org/abs/2503.01840
- DeepSeek-AI, DeepSeek-V3 Technical Report, 2024(§5.4.3 MTP 用于投机解码). https://arxiv.org/abs/2412.19437
调度
- W. Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023(第 7 讲的阅读材料)。
- A. Agrawal et al., Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve, OSDI 2024(第 5 讲讨论过的分块 prefill)。
本仓库
- CS349D 第 2 讲:连续批处理的调度策略(FCFS / SJF、预留策略、抢占)。
- CS349D 第 5 讲:投机解码的更多变体(树形掩码的实现、SpecInfer 的多草稿)。
17. 下一讲预告 · Next lecture
本讲两次碰到同一堵墙:迭代级调度要为 KV 预留空间,预留浪费显存、不预留会死锁(§3.5);投机解码要回滚 KV(§6.2)。第 7 讲专门讲 KV cache 的管理:
- PagedAttention(vLLM,SOSP 2023):借用操作系统的虚拟内存——KV 按固定大小的块按需分配,用块表把”逻辑上连续”映射到”物理上分散”;块可以共享(并行采样、束搜索、共同前缀),写时复制;显存不够时抢占(换出或重算)。
- Mooncake(Kimi,FAST 2025):把 KV cache 当成整个集群的一等公民——解耦的 prefill / decode 之外,再加一个分布在 CPU 内存和 SSD 上的 KV 存储池,按 KV 的复用来调度请求,过载时提前拒绝。