推理效率与压缩 — 为什么慢在「搬数」,怎么把模型塞进小显存
这一章讲三件事: 推理为什么慢——用算术强度这笔账证明,慢不在「算不动」而在「搬不动」(内存墙); 加速的两路办法:系统级省搬运、算法级省次数(投机解码:小模型起草、大模型验收); 压缩的三套刀法:量化(16 位压 4 位)、蒸馏(大模型教小模型)、剪枝(剪掉不重要的参数)。 主走查: 书里的量化算例——一组真数从浮点压成整数再还原,看误差到底有多大。
1. KV cache 与两阶段:每步只算一个 token
第 05 章讲过,自回归生成是一个词一个词挨着来。如果每生成一个词都把整段历史重算一遍, 成本会爆炸。原书把推理拆成两个阶段,拆解的依据是一个朴素的观察: 生成第 t+1 个词时,前 t 个词算过的 key/value 一个字都没变—— 把它们缓存起来,每步只需为新来的那个词做计算。这就是键值缓存(KV cache)1:
预填充(prefill)阶段:整段输入一次性算完,顺便把每个位置的 key/value 存进缓存
│ —— 并行度高,像训练时的前向
↓
解码(decoding)阶段:每步只输入最新那个词,从缓存里取历史的 key/value 算注意力,
│ 生成一个词,再把它的 key/value 追加进缓存
└──── 循环直到结束 —— 每一步都绕不开「读整个缓存+读全部参数」
图说:两阶段的瓶颈完全不同,这就是下一节要算的账。
2. 内存墙:慢的不是算力,是带宽
「哪个阶段慢」可以算出来。先给显卡定个性:它有两个指标—— 算力(每秒多少次浮点运算,A100 是 312 TFLOP/s,即每秒 3.12×10¹⁴ 次) 显存带宽:每秒能从显存搬多少字节——A100 是 2039GB/s。 两者相除得到一个临界值:A100 的最大算术强度 ≈ 142.51 次/字节—— 平均每搬一个字节,最多供得起 142 次运算2。
再给模型的每个操作算同样的账:计算量 ÷ 访问量 = 这个操作的算术强度。 强度高于 142.51,瓶颈在算力(compute-bound);低于它,瓶颈在搬数(memory-bound)。 代进 LLaMA-7B 的真实形状(批量 8、序列 1024)——原书表 9.1/9.2 的完整推导,结论两行3:
| 阶段 | 线性变换强度 | 注意力强度 | 判定 |
|---|---|---|---|
| 预填充 | ≈ 2731 | ≈ 115 | 算力受限(高于或接近 142.51) |
| 解码 | 全部 ≤ 8 | ≤ 8 | 搬数受限——「内存墙」 |
「带宽」这个词就是上一段的「显存带宽」:每秒能从显存搬多少字节;解码阶段的强度远低于 142.51, 意味着瓶颈不在算而在搬。
直觉解释:解码阶段每步只处理一个词,计算量小得可怜,但每一步都要把 整个模型的参数(7B 模型是十几 GB)和整个 KV 缓存从显存搬一遍—— 活儿不多,路太远。所以推理低效的根源在解码阶段的搬数,不在算力。 这个判据直接决定了下面所有优化方法的方向:要么少搬,要么少跑。
3. 系统级三招:各省一种搬运
FlashAttention:标准注意力要把 QKᵀ 这个 T×T 的中间矩阵写出来再读回去, T 一大这就是最大的搬运户。它把计算分块(切成一小块一小块)、把中间结果一直留在高速缓存里, 最终输出才写显存——LLaMA-2 7B(序列 2048、批量 8)的注意力时间降到原来的十分之一4。
PagedAttention:KV 缓存的传统管理是每追加一个词就重新申请一块显存、整体拷贝, 又慢又碎。它借鉴操作系统的分页(把内存预先切成固定大小的块来管理)思想:显存预先分成固定块,缓存按块分配、按需取用, 免去反复申请拷贝;查询还能与多个缓存块并行计算5。
连续批处理(continuous batching):传统批处理要等整批最长的那条生成完才放下一批, 短请求全在陪等。它把调度粒度改到单条请求:谁生成完谁先走、新请求随时插入, 等效批量始终拉满——批量越大,每次搬参数摊到的计算越多,算术强度越高。 DeepSpeed-MII 的 SplitFuse 更进一步,把预填充切块,让两个阶段在同一批里混跑6。
4. 算法级:投机解码主走查
系统级省搬运,算法级省大模型出场的次数。代表是投机解码,洞察是: 生成步骤的难度不均——「微软的创始人是」难猜,但「微软的创始人是比尔」后面接「盖茨」, 小模型也会。那就让小模型 先起草,大模型只做验收7:
输入:「Give me six hours to chop down a tree」
大模型完整回答共 9 个词:「I will spend the first four sharpening the axe」
—— 原书例(数字全是真的):
第 ① 轮:小模型起草 3 词「I will use」
→ 大模型一次前向验收 3 个位置:前两个对,第三个错
→ 改成「I will spend」(对的全留,错的由大模型当场改)
第 ② 轮:小模型起草「four hours to」→ 验收:第一个就错 → 改成「the」
第 ③ 轮:小模型起草「first three sharpening」→ 第二个错 → 改成「first four」
第 ④ 轮:小模型起草「sharpening the axe」→ 全对,收下
账本:小模型共起草 12 个词(便宜),大模型只跑了 4 次前向(每次验收一整段,还是并行);
不用投机解码,大模型要跑 9 次。约 2 倍加速,**且输出与大模型逐词生成完全一致**——
因为每个词最终都过了大模型的验收。
图说:省的不是计算,是大模型「逐词出场」的次数;质量不变是验收机制保证的。
同族还有三招,各换一个角度8:级联解码(FrugalGPT)让模型从快到慢排队, 每一级先跑二分类(只答「是/否」两选一)的判断——「当前答案够不够好」,够好就不再麻烦更大的模型;
非自回归/半自回归(Medusa)给模型加额外的预测头,一次出 2~3 个词, 但单独用质量差,通常给投机解码当「起草员」;提前退出给每层配一个预测头, 如果某一层的输出分布已经很有把握(用熵(分布的「不确定程度」,越集中在少数词上熵越低)衡量), 就不再往更深的层走。