对应原课:CMU 11-667 Fall 2025 · Lecture 19 · Tue 11-Nov-2025 · Efficient Inference Methods 原课 slides:Efficient Inference 阅读材料:Leviathan et al. (2023), Fast Inference from Transformers via Speculative Decoding;Dao et al. (2022), FlashAttention;Kwon et al. (2023), Efficient Memory Management for Large Language Model Serving with PagedAttention(vLLM) 配套代码:code/lecture19/(1 个模块 + 5 个演示脚本) 本讲的写法:先弄清”生成一个 token 为什么慢”(prefill 与 decode、内存带宽),再看四类方法:
- KV 缓存与 FlashAttention:不重复计算、少读写显存;
- PagedAttention:少浪费显存;
- 量化:少读字节;
- 推测解码:一次检查多个 token。
每个实验都检查”加速有没有改变输出”:哪些是精确的,哪些是近似的。GPU kernel 和服务系统的实现细节见本仓库的 CMU11-868。所有数字都来自配套代码的实际输出(计时数字取决于机器)。
目录
- 0. 本讲导读
- 1. 生成一个 token 为什么慢
- 2. KV 缓存
- 3. FlashAttention:分块与在线 softmax
- 4. PagedAttention 与推理服务
- 5. 量化
- 6. 推测解码
- 7. 小结、常见误区、练习
0. 本讲导读
0.1 这一讲要回答的问题
训练一个模型是一次性的开销;推理是每天、每个用户、每个请求都要付的开销。一个被广泛使用的模型,推理的总算力很快就会超过训练(第 12 讲练习 6、第 15 讲练习 8)。所以推理效率直接决定了成本、延迟和能耗。这一讲要回答:
- 生成一个 token 的瓶颈在哪里?为什么 GPU 的算力在 decode 时大部分闲着?
- 哪些加速是精确的(输出完全不变),哪些是近似的(有质量损失)?
- 推测解码怎样做到”更快,而且输出分布严格不变”?
0.2 学完这一讲你应该能
- 区分 prefill 和 decode,用算术强度解释为什么 decode 受内存带宽限制、批处理为什么有用;
- 计算 KV 缓存的大小,说出减小它的办法;
- 推导在线 softmax,解释 FlashAttention 为什么精确、省在哪里;
- 解释连续批处理和分页 KV 缓存解决的问题;
- 比较权重量化和激活量化,解释离群通道和 LLM.int8() / SmoothQuant;
- 证明推测解码的输出分布等于目标模型的分布,计算期望的加速。
0.3 实验平台
- 目标模型:d = 128、4 层,806,144 个参数,测试 NLL 1.348。
- 草稿模型:d = 32、1 层,15,968 个参数。训练 6 遍的”好草稿” NLL 1.363,训练 1 遍的”差草稿” NLL 1.799。
- 从零实现:带 KV 缓存的解码器、分块注意力、量化、推测解码。
1. 生成一个 token 为什么慢
1.1 自回归生成的两个阶段
用户发来一个提示(例如 500 个 token),模型生成一个回答(例如 300 个 token):
- Prefill(预填充):把整段提示一次送进模型,算出每一层的 key / value(存进 KV 缓存,§2)和第一个输出 token 的分布。所有提示 token 并行处理:矩阵 × 矩阵。
- Decode(解码):每一步只处理一个新 token,生成下一个。300 个 token 就要 300 次前向,一次接一次,不能并行:矩阵 × 向量。
用户感受到的两个延迟指标:
- TTFT(time to first token):主要由 prefill 决定;
- TPOT(time per output token):由 decode 决定。
服务方还关心吞吐量(每秒总共生成多少 token)。
1.2 算术强度与”内存墙”
一个 N 参数的模型,每处理一个 token 大约要 2N 次浮点运算(每个参数一次乘法、一次加法)。decode 的一步:
- 计算:2N·B 次运算(B 是批里的序列数);
- 读内存:每个权重都要从显存读一遍,2N 字节(fp16),和 B 无关。
算术强度 = 运算次数 / 读写字节数 ≈ B。
硬件的”平衡点”:A100 的算力约 312 TFLOPS,显存带宽约 2 TB/s,比值约 156 FLOP/字节。算术强度低于这个值时,算力在等数据——受内存带宽限制(memory-bound)。
- batch = 1 的 decode,强度约 1,GPU 的算力 99% 以上闲着(练习 1)。
- prefill 一次处理几百个 token,强度高,受算力限制(compute-bound)。
1.3 实验:prefill vs decode,以及批处理
代码 01 §2(CPU 上的计时,数字取决于机器):
- 处理同样的 256 个 token:prefill 一次前向 3.5 ms(约 73,000 token/s);decode 逐个处理 95.2 ms(约 2,700 token/s),慢了约 27 倍。
- decode 的批处理(每个序列的缓存长度 64):
| 批大小 B | 每步用时 | 吞吐量(token/s) |
|---|---|---|
| 1 | 0.34 ms | 2,909 |
| 4 | 0.47 ms | 8,558 |
| 16 | 1.12 ms | 14,315 |
| 64 | 2.38 ms | 26,873 |
| 256 | 15.07 ms | 16,993 |
- 批不太大时,每步的时间增长得远比 B 慢:读一遍权重,可以给 B 个序列都用上。推理服务因此要把很多用户的请求拼成一批。
- 批大到一定程度,计算本身(以及注意力读 KV 缓存)成为瓶颈,吞吐量不再增长、甚至下降。这台 CPU 上 B = 256 反而比 B = 64 慢;GPU 上的拐点通常在更大的批。
延迟和吞吐量的权衡:批越大,吞吐量越高(每个 token 越便宜),但每个用户的 TPOT 变长。推理服务要在两者之间按产品需求取舍。
2. KV 缓存
2.1 不重复计算
因果注意力里,位置 t 的 key 和 value 只依赖 t 及之前的 token。生成第 t+1 个 token 时,前面所有位置的 k、v 和上一步完全一样,没有理由重算。
- KV 缓存:每一层把算过的 k、v 存下来,新 token 只算自己的 q、k、v,再和缓存里的全部 k、v 做注意力。
- 计算量:
- 不用缓存时,第 t 步要重算整个前缀:生成 n 个 token 的总计算量 ∝ n²·(模型大小)。
- 用缓存时,每步的线性层计算量和 t 无关,只有注意力读缓存的部分 ∝ t:总计算量 ∝ n·(模型大小) + n²·d。
2.2 实验:正确性与加速
代码 01 §1:
- 正确性:同一段 50 个 token,“一次完整前向” vs “先 prefill 20 个、再逐个 step 30 个”,logits 的最大差别 4.3e-6(浮点误差)。
- 速度:
| 生成 token 数 | 不用缓存 | 用 KV 缓存 | 加速 | 采样结果相同 |
|---|---|---|---|---|
| 64 | 0.04 s | 0.02 s | 1.7× | ✓ |
| 128 | 0.11 s | 0.04 s | 2.6× | ✓ |
| 256 | 0.40 s | 0.10 s | 4.1× | ✓ |
| 512 | 2.16 s | 0.28 s | 7.7× | ✓ |
生成长度翻倍,不用缓存的时间翻四倍以上;用缓存的时间大约翻倍。KV 缓存是精确的:用同样的随机数,采样出的 token 完全相同。
2.3 KV 缓存有多大
每个 token 的 KV 缓存 = 2(k 和 v)× 层数 × KV 头数 × 每头维度 × 每个数的字节数。代码 01 §3(fp16):
| 模型 | 每 token | 4K 上下文 | 128K 上下文 | 权重 |
|---|---|---|---|---|
| Llama-2-7B(MHA,32 个 KV 头) | 512 KB | 2.0 GB | 64.0 GB | 13 GB |
| Llama-2-70B(GQA,8 个 KV 头) | 320 KB | 1.25 GB | 40.0 GB | 130 GB |
| Llama-3-8B(GQA,8 个 KV 头) | 128 KB | 0.5 GB | 16.0 GB | 15 GB |
一个 128K 上下文的请求,Llama-2-7B 式的 KV 缓存要 64 GB,比权重大好几倍。批里有很多请求时,KV 缓存决定了一张卡能同时服务多少请求。减小它的办法:
- GQA / MQA(第 4 讲 §3):多个 query 头共享一组 KV 头。Llama-3-8B 的 KV 缓存只有 Llama-2-7B 的 1/4。
- KV 缓存量化:存成 8 比特甚至 4 比特(§5)。
- 滑动窗口 / 稀疏注意力:只保留最近的或重要的 token(第 21 讲)。
- 分页管理:减少浪费(§4)。
3. FlashAttention:分块与在线 softmax
3.1 注意力慢在哪里
标准的注意力实现:
- ,写进显存;
- ,读 S、写 P;
- ,读 P。
T × T 的矩阵 S、P 要在显存里来回读写好几次。GPU 有两种内存:
- 显存(HBM):几十 GB,约 2 TB/s;
- 片上 SRAM:每个计算单元约 192 KB,比显存快一个数量级。
注意力的计算量不大,慢在读写显存;而且 T = 32K 时,一个头的 S 就有 10 亿个数,根本放不下。
FlashAttention(Dao et al. 2022)的想法:
- 把 Q、K、V 分块读进 SRAM,在 SRAM 里算完一块的注意力,直接累加到输出上,从不把 S、P 写回显存。
- 难点在于 softmax 的分母要看完整行的分数才知道。
3.2 在线 softmax
数值稳定的 softmax 要先减去最大值:,。代码 02 §1:
- 分数 [1000, 999, 998, 10] 直接算 exp 会溢出成 inf,得到 nan;
- 减去最大值后得到 [0.665, 0.245, 0.090, 0.000]。
但”先求最大值、再求和”要扫两遍。在线 softmax(Milakov & Gimelshein 2018)只扫一遍,逐块维护”目前的最大值 m”和”目前的和 l”:
最大值变大时,之前算的指数统一乘以 缩小,等价于从一开始就用新的最大值。上面的例子分两块处理,得到 m = 1000、l = 1.503215,和一次性算的完全相同(练习 3)。
分块注意力:对每个 query 维护 (m, l, acc),每读入一块 K、V:
最后输出 acc / l。
3.3 实验:分块注意力 = 标准注意力
代码 02 §2:
- 随机数据:Q、K、V 随机(T = 256,d = 64,因果掩码),块大小 1、16、64、256 时,和标准注意力的最大差别都在 1e-14 量级。任何时候只需要存 T × 块大小 的分数。
- 真实模型:目标模型第 1 层、4 个头、一篇 52 个 token 的测试文档,分块注意力和标准注意力的最大差别 1.3e-15。
FlashAttention 是精确的:它改变的是计算的顺序和中间结果存在哪里,不是近似。
3.4 省在哪里
代码 02 §3 用一个简化的模型数读写显存的元素个数(一个头,d = 128,SRAM 放得下的块大小 B = 128):
- 标准:≈ 4Td + 4T²;
- 分块(FlashAttention-2 的顺序):每次把 B 行 Q 留在 SRAM,依次读入全部 K、V 块,≈ 2Td·(T/B) + 2Td。
| T | 标准(百万个元素) | 分块(百万个元素) | 比值 | 标准要存的 S、P(每个头) |
|---|---|---|---|---|
| 1024 | 4.7 | 2.4 | 2.0× | 4 MB |
| 4096 | 69.2 | 34.6 | 2.0× | 64 MB |
| 16384 | 1082.1 | 541.1 | 2.0× | 1 GB |
| 65536 | 17213.4 | 8606.7 | 2.0× | 16 GB |
在这个简化模型里,读写量只少了一半(K、V 要被反复读 T/B 次)。更大的收益来自两方面:
- 不存 T × T 的中间矩阵:显存从 O(T²) 降到 O(T),长上下文才放得下。反向传播时重算注意力,而不是把它存下来——用计算换显存,在显存带宽受限时反而更快。
- kernel 融合:矩阵乘、掩码、softmax、再乘 V 在一个 kernel 里完成,不再多次启动、多次读写。
实测 FlashAttention 比标准实现快 2~4 倍。FlashAttention-2 改进了并行方式(按 query 块并行);FlashAttention-3 利用了 H100 的异步和 FP8 特性。decode 阶段还有专门的变体(FlashDecoding:按 key 的长度方向并行)。
4. PagedAttention 与推理服务
4.1 连续批处理
推理服务同时收到很多请求,每个请求的提示长度、回答长度都不同。
- 静态批处理:一批请求一起开始,等最长的那个结束才开始下一批。短的请求早早结束,位置空着。
- 连续批处理(continuous batching,Orca,Yu et al. 2022):每一步都可以让结束的请求离开、新的请求加入,批始终是满的。
4.2 KV 缓存的显存管理
每个请求最后会生成多少 token,事先不知道。传统做法是给每个请求预留”提示 + 最大生成长度”的连续显存。这带来三种浪费:
- 预留了没用上:最大长度 2048,实际只生成了 100 个;
- 内部碎片:为将来的 token 提前占着;
- 外部碎片:空闲的总量够,却凑不出一段足够长的连续空间。
Kwon et al. (2023) 测得已有系统只有 20%~40% 的 KV 缓存显存真正被用上。
PagedAttention(vLLM)借用操作系统的分页:
- 把 KV 缓存切成固定大小的块(例如 16 个 token),每个请求有一张”块表”,记录它的逻辑块对应哪些物理块。
- 块按需分配,不必连续。
- 注意力 kernel 按块表去取 K、V。
4.3 实验:服务模拟
代码 03(纯 Python 的模拟):
- 请求:400 个请求一次性到达,提示 16~512 个 token。回答长度服从几何分布:均值 128、中位数 94、最长 683;服务允许的上限是 1024。
- 预算:KV 缓存能放 32,768 个 token。
- 调度:连续批处理,每步给每个运行中的请求生成一个 token。
| 管理方式 | 总步数 | 平均批大小 | 显存利用率 | 预留占比 | 浪费 | 抢占 |
|---|---|---|---|---|---|---|
| 连续,预留 提示 + 上限 | 2706 | 18.8 | 22.7% | 74.4% | 51.7% | 0 |
| 连续,预知真实长度(不可能) | 1361 | 37.4 | 45.1% | 59.1% | 14.0% | 0 |
| 分页(块大小 16) | 1092 | 46.8 | 56.3% | 57.4% | 1.1% | 158 |
(利用率 = 真正存着 KV 的 token 占预算的比例;浪费 = 预留占比 − 利用率。都是对全部步数的平均,包括队列排空后只剩少数长请求在跑的尾部阶段。)
- 预留最大长度:每个请求占”提示 + 1024”个位置,实际平均只用几百个。一半的显存被”订”了却空着,同时运行的请求很少,总步数最多。
- 即使假想能预知真实长度,连续分配也有两种损失:为还没生成的 token 提前占着的位置(14%),以及外部碎片。
- 分页:浪费的只有每个请求最后一块里没填满的部分(平均半块),只有 1.1%。同时运行的请求最多,总步数比预留最大长度少了 60%。
- 代价是显存满了要抢占:这里把被抢占的请求放回队首、之后从头重算,重算的步数已经算在总步数里;vLLM 也支持把它的块换出到 CPU 内存。
- 注意力 kernel 要按块表去找 K、V。
4.4 共享前缀
分页还让多个序列共享同一段 KV 缓存:几个序列的块表指向同一批物理块,用引用计数管理;只有需要修改的块才复制(写时复制,copy-on-write)。
代码 03 §3:同一个 512 个 token 的提示,并行采样 8 个回答,每个 128 个 token(best-of-n、自洽性投票都这样做):
- 不共享:5,120 个 token 的空间;
- 共享提示的 32 个块:1,536 个 token 的空间,省了 70%。
同样的机制让多轮对话、共享系统提示的请求复用前缀的 KV 缓存(前缀缓存,prefix caching;SGLang 的 RadixAttention 用基数树管理共享的前缀)。
4.5 其他服务层面的优化
- 分块 prefill(chunked prefill):把长提示切成几段,和其他请求的 decode 混在一批里,避免一个长提示让所有人卡住。
- prefill / decode 分离(disaggregation):两个阶段的瓶颈不同(算力 vs 带宽),放到不同的 GPU 上分别优化(Splitwise、DistServe)。
- 专门的推理引擎:vLLM、SGLang、TensorRT-LLM、llama.cpp 等。
5. 量化
5.1 为什么量化能加速
decode 受内存带宽限制:每生成一个 token,都要把全部权重读一遍。把权重从 16 比特压到 4 比特,读的字节少了 4 倍,显存也省了 4 倍,可以放更大的模型或更大的批。代价是误差——这是近似的加速。
对称(absmax)量化:,,反量化 。s 由一组数里绝对值最大的那个决定,所以”一组”的范围(粒度)很重要:
- 逐张量:整个矩阵一个 s;
- 逐通道:每个输出通道一个 s;
- 分组:每 g 个数一个 s(GPTQ / AWQ 常用 g = 128),要多存缩放因子。
5.2 实验:只量化权重
代码 04 §1:
- 权重分布:一个权重矩阵(第 1 层 FFN 的 up,512 × 128)的最大绝对值 0.323、标准差 0.067。
- 测试 NLL:未量化 1.348;对照 16K 参数的草稿模型 1.363。
| 比特 | 逐张量 | 逐通道 | 分组 32 | 分组时每个权重的实际比特数 |
|---|---|---|---|---|
| 8 | 1.348 | 1.348 | 1.348 | 8.5 |
| 4 | 1.350 | 1.348 | 1.348 | 4.5 |
| 3 | 1.354 | 1.351 | 1.351 | 3.5 |
| 2 | 9.141 | 1.520 | 1.383 | 2.5 |
- 8 比特和 4 比特几乎无损;3 比特开始有一点损失;2 比特时逐张量量化完全崩溃,分组还能用。
- 分组越细,一个极端值影响的数越少,代价是要多存缩放因子(16 比特 / 32 个数 = 每个数多 0.5 比特)。
- 这个小模型对量化特别宽容。真实的 LLM 在 4 比特时常要用更聪明的方法才能接近无损:
- GPTQ(Frantar et al. 2023)用二阶信息逐列修正量化误差(CMU11-868 第 20 讲);
- AWQ(Lin et al. 2024)根据激活的大小保护重要的权重通道。
- 3 比特以下损失明显。
5.3 激活量化与离群通道
只量化权重时,矩阵乘法前要把权重反量化回 16 比特,省的是读内存。W8A8 让激活也变成 8 比特,矩阵乘法直接用整数单元,省的是算力,适合 prefill 和大批量。激活每个输入都不同,缩放因子只能在推理时动态算:逐张量(整批一个),或逐 token(每个 token 一个)。
离群通道:Dettmers et al. (2022, LLM.int8()) 发现,6.7B 以上的模型里,少数几个激活维度会比其他维度大几十倍,而且几乎每个 token 都这样。我们的小模型没有,代码 04 §3 用一个不改变函数的变换造了一个:
- 把每个 LayerNorm 第 5 维的缩放乘以 100,后面 Linear 权重的第 5 列除以 100。测试 NLL 仍是 1.3481。
- 第 1 层 LayerNorm 输出的各通道最大绝对值:最大的 286.0,其次 4.6,中位数 2.8。
| 激活比特 | 原模型,逐 token | 离群,逐张量 | 离群,逐 token | LLM.int8() | SmoothQuant |
|---|---|---|---|---|---|
| 8 | 1.348 | 1.482 | 1.353 | 1.348 | 1.348 |
| 6 | 1.348 | 10.119 | 3.413 | 1.348 | 1.348 |
| 4 | 1.349 | 10.120 | 8.550 | 1.349 | 1.349 |
- 一个通道大了 100 倍,整个 token(或整批)的缩放因子就被它决定,其他通道的值都被挤进了很少几个格子,信息几乎全丢了。逐 token 也救不了,因为离群通道在每个 token 里都在。6 比特时模型已经崩溃。
- LLM.int8():把离群通道留在 16 比特单独算,其余照常量化(混合精度分解),完全恢复。代价是实现复杂,离群那部分不能用整数单元。
- SmoothQuant(Xiao et al. 2023):
- 每个通道选一个 ,激活除以 、权重乘以 ,函数不变。
- 这把激活的”难”迁移一部分给权重。权重本来很”平”(最大值只有标准差的 5 倍左右),分到一部分难度也还承受得住。
- 之后再把权重也量化成 8 比特(逐通道),W8A8 的测试 NLL 是 1.348。
5.4 其他
- 数值格式:除了整数,还有 FP8(E4M3、E5M2,H100 原生支持)和 FP4(Blackwell);浮点格式对分布的尾部更宽容。
- KV 缓存量化:长上下文时 KV 缓存比权重大(§2.3),把它量化到 8 或 4 比特同样重要(KIVI 等)。
- 量化感知训练(QAT)和极低比特:BitNet b1.58 用三值权重从头训练,需要训练时就考虑量化。
6. 推测解码
6.1 想法
decode 受内存带宽限制(§1.2):目标模型一次前向处理 1 个 token 和处理 5 个 token 的时间差不多,读权重的开销一样。推测解码(Leviathan et al. 2023;Chen et al. 2023)利用这一点:
- 一个小得多的草稿模型连续猜 γ 个 token(便宜);
- 目标模型一次前向同时算出这 γ 个位置(以及后面一个位置)的分布;
- 按下面的规则从前往后检查每个猜测,第一次拒绝时停下。
接受 / 拒绝规则:草稿模型在某个位置给出分布 q、猜出 x;目标模型在同一位置的分布是 p。
- 以概率 接受 x;
- 否则拒绝,从修正分布 里重新采一个 token,丢掉后面的猜测;
- γ 个全部接受时,再从目标模型的最后一个分布里采一个 token(白送的)。
6.2 为什么输出分布严格不变
输出 x 的总概率:
第一项等于 。拒绝的概率是 (两个分布的总变差距离),正好和第二项的分母约掉。所以
每个 token 的分布严格等于目标模型的分布,无论草稿模型多差。草稿模型只影响速度(接受率),不影响质量。
6.3 实验:逐个位置的精确验证
代码 05 §1:在 200 篇测试文档的每一个位置上,用两个模型的真实分布直接算上面的式子。
| 草稿模型 | 测试 NLL | 参数 | 接受率 α = E[Σ min(p, q)] | max |P_输出 − p| |
|---|---|---|---|---|
| 差的草稿(训练 1 遍) | 1.799 | 15,968 | 0.694 | 4.8e-7 |
| 好的草稿(训练 6 遍) | 1.363 | 15,968 | 0.921 | 4.8e-7 |
| 目标模型自己 | 1.348 | 806,144 | 1.000 | 0 |
输出分布和目标模型的差别只有浮点误差。这和量化、剪枝这类”近似”加速有本质的不同。
6.4 实验:1000 篇文档
代码 05 §2:用差的草稿模型(γ = 4)做推测解码,生成 1000 篇文档,和其他方式比较。“完全合语法”用花园世界的真实语法检查整篇文档。
| 采样方式 | 完全合语法 | 平均长度 | 前两个词的分布与目标的 TV |
|---|---|---|---|
| 直接从目标模型采样 | 95.0% | 17.4 | 0 |
| 再从目标模型采样一次(换种子) | 94.0% | 16.6 | 0.091 |
| 推测解码(差的草稿) | 94.6% | 16.3 | 0.089 |
| 直接从差的草稿模型采样 | 8.4% | 15.1 | 0.211 |
草稿模型自己只能写出 8.4% 合语法的文档。用它做推测解码,输出和直接从目标模型采样没有区别:和目标的差别与”目标模型换一个种子”一样大,只是抽样噪声。
6.5 实验:能快多少
如果每个猜测独立地以概率 α 被接受,每轮(每次调用目标模型)期望得到的 token 数是
代码 05 §3(每种设置 100 篇文档):
| 草稿模型 | γ | 实测 token / 次 | 理论值 | 估计加速 |
|---|---|---|---|---|
| 差的草稿 | 1 | 1.67 | 1.69 | 1.64× |
| 差的草稿 | 4 | 2.67 | 2.74 | 2.47× |
| 差的草稿 | 8 | 3.04 | 3.14 | 2.62× |
| 好的草稿 | 1 | 1.89 | 1.92 | 1.85× |
| 好的草稿 | 4 | 4.03 | 4.27 | 3.73× |
| 好的草稿 | 8 | 6.16 | 6.62 | 5.31× |
(估计加速 = token / 次 ÷ (1 + γc),c = 草稿 / 目标的计算量 ≈ 参数量之比 = 0.020。)
- 实测略低于理论值:文档在
<eos>处结束,最后一轮的猜测常常用不完;α 在不同位置也不一样,开头难猜、尾巴好猜。 - 好的草稿(α ≈ 0.92)在 γ = 8 时一次调用平均得到 6 个 token;差的草稿(α ≈ 0.69)γ 再大也只有 3 个左右。连续猜对 γ 个的概率是 ,猜得越多,后面的越容易白猜。γ 有一个最优值(练习 7)。
- 真实的加速还取决于硬件和实现:
- 目标模型一次处理 γ+1 个 token 的时间要和处理 1 个差不多,这在 decode 受带宽限制时成立;
- 草稿模型要足够便宜;
- 批很大时 decode 已经接近算力受限,推测解码的收益会变小。
- Leviathan et al. 报告 T5-XXL 上 2~3 倍的加速。
6.6 变体
- 贪心解码时更简单:草稿的 token 等于目标模型的 argmax 就接受,输出和目标模型的贪心解码完全相同。
- 不用单独的草稿模型:
- Medusa(Cai et al. 2024)在目标模型上加几个预测”后面第 k 个 token”的小头;
- EAGLE(Li et al. 2024)用目标模型的隐藏状态预测下一个隐藏状态;
- 自推测(self-speculative)用目标模型跳过一些层作为草稿。
- 树状推测:草稿给出多个候选分支,目标模型用特殊的注意力掩码一次验证整棵树(SpecInfer)。
- 查表草稿:Lookahead decoding、prompt lookup decoding 用 n-gram 或提示里出现过的片段作为猜测,对”复述提示”类的任务(摘要、改写代码)特别有效。
7. 小结、常见误区、练习
7.1 小结
- 瓶颈:
- prefill 受算力限制(矩阵 × 矩阵),decode 受内存带宽限制(矩阵 × 向量,算术强度 ≈ 批大小)。
- 批处理提高吞吐量,但有延迟代价和上限。
- KV 缓存:
- 精确地去掉重复计算(512 个 token 快 7.7 倍,结果完全相同)。
- 它的大小 = 2 × 层数 × KV 头数 × 头维度 × 字节数,长上下文时比权重还大。
- FlashAttention:
- 在线 softmax + 分块,精确(误差 1e-14 量级)。
- 省掉 T × T 的中间矩阵(显存 O(T²) → O(T)),减少显存读写、融合 kernel。
- PagedAttention:
- 按块按需分配 KV 缓存,浪费从 52% 降到 1%,同样的显存能同时服务 2.5 倍的请求。
- 共享前缀省 70% 的显存(并行采样)。
- 量化(近似):
- 权重 8 / 4 比特几乎无损,分组让 2~3 比特可用。
- 激活的离群通道会毁掉激活量化,LLM.int8() 和 SmoothQuant 能解决。
- 推测解码(精确):
- 接受 / 拒绝规则保证输出分布严格不变(误差 5e-7;差的草稿 8.4% 合语法,推测解码 94.6%)。
- 每次调用的 token 数 ≈ (1 − α^(γ+1))/(1 − α)。
7.2 常见误区
- “GPU 越快,生成越快”:batch 小时 decode 受显存带宽限制,算力再强也闲着(§1.2)。
- “KV 缓存是一种近似”:它是纯粹的不重复计算,结果完全相同(§2.2)。
- “FlashAttention 用了近似的注意力”:它是精确的,只改变了计算顺序(§3.3)。
- “FlashAttention 减少了注意力的计算量”:计算量不变(反向传播时甚至更多);它减少的是显存读写和中间结果的显存(§3.4)。
- “量化到 4 比特就是损失 75% 的信息”:权重的冗余很大,4 比特常常几乎无损;难的是激活的离群值(§5)。
- “推测解码用小模型生成,所以质量会下降”:输出分布严格等于目标模型(§6.2、§6.4)。
- “草稿模型越大越好”:草稿越大 α 越高,但每次猜测的成本也越高;要在 α 和 c 之间权衡(练习 7)。
7.3 练习
练习 1(decode 的上限):一个 7B 参数的模型(fp16,14 GB 权重)在 A100 上做 batch = 1 的 decode。
(a) 每个 token 读权重至少要多久?算力上算完要多久?每秒最多生成多少 token? (b) 批大小多大时,算力才开始成为瓶颈?
参考答案
(a) 读权重:14 GB / 2 TB/s = 7 ms。计算:2 × 7×10⁹ = 1.4×10¹⁰ 次运算 / 312×10¹² = 0.045 ms。读内存比计算慢 150 多倍,每秒最多约 1 / 7 ms ≈ 140 个 token(还没算 KV 缓存的读取)。
(b) 算术强度 ≈ B,硬件平衡点 ≈ 312 TFLOPS / 2 TB/s = 156,所以 B ≈ 150 左右才开始受算力限制。实际上 KV 缓存的读取随 B 和上下文长度增加,平衡点会更早到来,而且显存也限制了 B 能开多大(§2.3)。
练习 2(KV 缓存与并发):一张 80 GB 的 A100 跑 Llama-2-7B(权重 13 GB),剩下约 60 GB 给 KV 缓存(fp16)。
(a) 每个请求的上下文是 4K 时,最多能同时服务多少个请求? (b) 换成 Llama-3-8B(GQA,8 个 KV 头)呢? (c) 再把 KV 缓存量化到 8 比特呢?
参考答案
(a) 每 token 512 KB,4K 上下文每个请求 2 GB,60 / 2 = 30 个。
(b) 每 token 128 KB,每个请求 0.5 GB,60 / 0.5 = 120 个(权重约 15 GB,剩下约 58 GB,差不多)。
(c) KV 缓存减半,约 240 个。
这就是 GQA 和 KV 缓存量化在推理服务里如此重要的原因:并发数直接决定了每个 token 的成本。分页管理(§4)让这些理论上的容量真正被用上,而不是被预留浪费掉。
练习 3(在线 softmax 手算):分数分两块到来:先是 [2, 1],再是 [3]。按在线 softmax 的更新规则算 (m, l),并和一次性计算的结果比较。
参考答案
第一块后:m = 2,l = e⁰ + e⁻¹ = 1 + 0.368 = 1.368。
第二块:m’ = max(2, 3) = 3;l’ = 1.368 × e^(2−3) + e^(3−3) = 1.368 × 0.368 + 1 = 1.503。
一次性:Σ exp(x − 3) = e⁻¹ + e⁻² + e⁰ = 0.368 + 0.135 + 1 = 1.503。✓
softmax(3) = 1/1.503 = 0.665,和 §3.2 的例子(1000、999、998 的相对差相同)一样。
练习 4(注意力的中间矩阵):T = 32K,32 个头,fp16,batch = 1。
(a) 标准注意力在一层里要存的 S(分数矩阵)有多大? (b) FlashAttention(块大小 128)任何时候需要的分数存储呢?
参考答案
(a) 每个头 32768² × 2 字节 = 2 GB,32 个头 64 GB(P 还要再来一份)。一张 80 GB 的卡连一层都放不下。
(b) 每个头 T × 128 个分数(实际实现里是 128 × 128 的块,在 SRAM 里),和 T 成线性关系:32768 × 128 × 2 字节 = 8 MB,32 个头 256 MB,而且根本不写回显存。长上下文训练和推理离不开 FlashAttention。
练习 5(分页的块大小):§4.3 用块大小 16。
(a) 平均每个请求浪费多少个 token 的空间?块大小 256 呢? (b) 块越小越好吗?
参考答案
(a) 最后一块平均半满:块大小 16 时平均浪费约 8 个 token;块大小 256 时约 128 个——对平均只生成 128 个 token 的请求来说,浪费接近一半。
(b) 不是。块越小:
- 块表越长,查表开销越大;
- 注意力 kernel 每次读的连续内存越短,访存效率越低;
- 分配 / 释放更频繁。
vLLM 默认用 16。此外,前缀共享以块为单位:块太大,能共享的部分变少。
练习 6(量化误差):§5.2 的权重矩阵最大绝对值 0.323、标准差 0.067。用逐张量的 4 比特对称量化()。
(a) 量化步长 s 是多少? (b) 均匀量化的均方误差约为 s²/12,信噪比(权重方差 / 误差方差)是多少? (c) 如果矩阵里有一个 3.0 的极端值呢?
参考答案
(a) s = 0.323 / 7 = 0.046。
(b) 误差方差 ≈ 0.046² / 12 = 1.8×10⁻⁴,权重方差 0.067² = 4.5×10⁻³,信噪比约 25(14 dB)。实验里 4 比特逐张量量化只让 NLL 从 1.348 变成 1.350。
(c) s = 3.0 / 7 = 0.43,比大多数权重(|w| < 0.2)大一倍以上:几乎所有权重都被量化成 0。信噪比约 0.067² / (0.43²/12) ≈ 0.3,信息基本丢光。这就是离群值的问题,分组量化、逐通道量化、或者把离群值单独存(§5.3)都是为了它。
练习 7(推测解码的最优 γ):接受率 α = 0.8,草稿模型的成本是目标模型的 c = 0.05。估计加速 = 。
(a) 算 γ = 1、4、8、10 时的加速。 (b) 最优的 γ 是多少?为什么不是越大越好?
参考答案
(a) γ = 1:1.80 / 1.05 = 1.71×;γ = 4:3.36 / 1.20 = 2.80×;γ = 8:4.33 / 1.40 = 3.09×;γ = 10:4.57 / 1.50 = 3.05×。
(b) 最优在 γ = 8 附近(3.09×)。
- 分子(每次得到的 token 数)随 γ 增长越来越慢,趋于 1/(1−α) = 5:连续猜对的概率 α^γ 衰减。
- 分母(成本)随 γ 线性增长。
α 越高、c 越小,最优的 γ 越大。实际系统还会根据最近的接受率动态调整 γ。
练习 8(贪心推测解码):目标模型用贪心解码(每步取 argmax)。
(a) 推测解码的接受规则应该怎样改? (b) 证明输出和目标模型的贪心解码完全相同。
参考答案
(a) 草稿的猜测 x 等于目标模型在该位置的 argmax 就接受,否则拒绝并输出目标模型的 argmax。这是一般规则在 p 退化成 one-hot 时的特例:p(x) = 1 时 min(1, p/q) = 1,p(x) = 0 时为 0;max(0, p − q) 归一化后就是那个 one-hot。
(b) 归纳法:
- 假设到当前位置为止,输出和贪心解码相同,那么目标模型在这个位置看到的前缀相同,argmax 相同。
- 接受的 token 等于 argmax;拒绝时输出的也是 argmax。所以这个位置也相同。
被接受的猜测都在目标模型”一次前向”里检查过——它们的前缀就是已经确认的输出,所以检查是有效的。
练习 9(思考:设计一个聊天服务的推理栈):一个聊天产品,用户希望第一个字快(TTFT < 1 秒),之后打字速度流畅(每秒 20 个 token 以上);公司希望每个 token 尽量便宜。只讨论原理,说出你会用哪些方法,以及它们之间的权衡。
参考答案(开放)
一定要用的(精确,几乎没有代价):
- KV 缓存;
- FlashAttention;
- 分页 KV 缓存 + 连续批处理:并发数决定成本;
- 前缀缓存:系统提示和多轮对话的历史在每个请求里重复。
按需权衡的:
- 批大小:越大越便宜,但每个用户的 TPOT 变长。要设一个上限,保证每秒 20 个 token。
- 长提示:用分块 prefill 和其他请求的 decode 混合,避免一个长文档的 prefill 让所有人卡住;流量大时可以把 prefill 和 decode 分到不同的机器上。
- 量化:
- 权重 8 比特或 4 比特(GPTQ / AWQ)省显存、提速 decode;
- KV 缓存 8 比特提高并发。
- 需要在自己的评测集上确认质量损失可以接受(第 5 讲)。
- 推测解码:
- 输出不变,能降低 TPOT。在批小(深夜流量低)时收益最大;高峰期批很大、接近算力受限时收益变小,可以动态开关。
- 草稿可以用同系列的小模型或 Medusa / EAGLE 头。
- GQA 的模型:选型时就考虑 KV 缓存的大小。
还要考虑的:
- 监控 TTFT、TPOT 的分布(尤其是 P99),而不只是平均值;
- 不同长度请求的公平调度;
- 抢占策略;
- 模型更新时的兼容性。
7.4 延伸阅读
- 原课阅读:
- Leviathan et al. (2023), Fast Inference from Transformers via Speculative Decoding
- Dao et al. (2022), FlashAttention
- Kwon et al. (2023), Efficient Memory Management for Large Language Model Serving with PagedAttention
- 推理的瓶颈与服务:
- Pope et al. (2022), Efficiently Scaling Transformer Inference
- Yu et al. (2022), Orca: A Distributed Serving System for Transformer-Based Generative Models(连续批处理)
- Zheng et al. (2024), SGLang(RadixAttention)
- Agrawal et al. (2024), Sarathi-Serve(分块 prefill)
- Zhong et al. (2024), DistServe
- 注意力:
- Milakov & Gimelshein (2018), Online normalizer calculation for softmax
- Dao (2023), FlashAttention-2
- Shah et al. (2024), FlashAttention-3
- Shazeer (2019), Fast Transformer Decoding: One Write-Head is All You Need(MQA)
- Ainslie et al. (2023), GQA
- 量化:
- Dettmers et al. (2022), LLM.int8()
- Xiao et al. (2023), SmoothQuant
- Frantar et al. (2023), GPTQ
- Lin et al. (2024), AWQ
- Liu et al. (2024), KIVI(KV 缓存量化)
- Ma et al. (2024), The Era of 1-bit LLMs(BitNet b1.58)
- 推测解码:
- Chen et al. (2023), Accelerating Large Language Model Decoding with Speculative Sampling
- Cai et al. (2024), Medusa
- Li et al. (2024), EAGLE
- Miao et al. (2024), SpecInfer
- Fu et al. (2024), Lookahead Decoding
- 本仓库的相关内容:
- 第 4 讲(GQA)
- 第 13 讲(混合精度)
- 第 21 讲(长上下文)
- 第 22 讲(MoE 推理)
- CMU11-868(第 9 讲解码、第 10 讲融合 kernel、第 19~20 讲量化与 GPTQ)
7.5 下一讲预告
到这里,我们有了一个训练好、对齐过、跑得快的模型。可是用户面对的不是”一个续写模型”,而是一个聊天助手,甚至是一个能调用工具、在网页上替你做事的智能体。第 20 讲:聊天机器人、LLM 智能体与人工评估。我们会看:
- 对话模板和”只在助手的回答上算损失”;
- 智能体的”观察 → 思考 → 行动”循环;
- Chatbot Arena 怎样用众包的成对比较给模型排名(又是 Bradley–Terry);
- 为什么人工评估本身也可能不可靠。