把推理成本打下来有四把锤子:量化、分页注意力、投机解码、连续批处理。它们各自砸的是不同的钉子——有的是显存带宽,有的是显存容量,有的是串行依赖,有的是硬件利用率。搞混了就会在错误的地方使劲。
编辑部注 · 本章难度较高,建议在读完前四章后再读。重点是「每个手段针对哪个瓶颈」,而不是记名词。
前面四章建立了三个事实:注意力是平方的、KV Cache 是线性的但占显存、解码是逐步采样的。这一章回答工业界最关心的问题——在这些约束下,怎么把单位 token 的成本压下去。
一、先分清两个阶段,再谈优化
推理分Prefill 和 Decode 两个阶段(Prefill / Decode),它们的瓶颈完全不同,因此优化手段也不同。这是本章最重要的一张表。Prefill 阶段的主要指标是 首 token 时延(TTFT),它卡在算力上。
| Prefill(处理输入) | Decode(逐 token 生成) | |
|---|---|---|
| 做什么 | 把整段输入一次算完,建立 KV Cache | 每步生成一个 token,读取已有缓存 |
| 并行度 | 高(n 个位置可并行) | 低(第 t 步必须等第 t−1 步) |
| 主要瓶颈 | 算力(FLOPs) | 显存带宽(Memory Bandwidth) |
| 典型表现 | 首 token 延迟(TTFT)高 | 吞吐(tokens/s)受限,GPU 利用率低 |
| 对应手段 | 算子融合、FlashAttention、分块计算 | 量化、连续批处理、投机解码 |
Decode 阶段的 GPU 利用率通常只有个位数百分比。原因不是算力不够,而是每生成一个 token,都要把整个模型权重和 KV Cache 从显存搬到计算单元一次。数据搬运的时间远超计算时间——这就是为什么 decode 阶段是「显存带宽受限」而非「算力受限」。
结论:针对 decode 的优化,本质上都是在减少搬运量或增加搬运的并行度。量化减少搬运量,连续批处理让一次搬运服务更多请求,投机解码让一次搬运产出更多 token。
二、四把锤子
2.1 量化:先砍显存与带宽
量化(Quantization)把权重从 fp16 降到 fp8 / int8 / int4,权重体积和 KV Cache 一起变小,于是「装得下」「搬得动」。它同时打两根钉子:显存容量(装下更大模型)和显存带宽(decode 时搬得少)。什么时候该先上量化:模型装不下、或 decode 带宽瓶颈时。什么时候别指望它:prefill 是算力瓶颈,量化对 TTFT 帮助有限;且低比特(W4)会带来精度损失,长链推理、大数运算、代码生成上更明显。安全的起点是 W8A8,W4 要配合分组量化 + 校准才能用。
2.2 PagedAttention:治显存碎片
PagedAttention 把 KV Cache 切成固定大小的块、按需分配、不要求连续,像操作系统分页管理内存那样。它打的是「显存碎片」这根钉子:传统连续分配会让显存里布满无法利用的空洞,能并发的请求数被碎片锁死。附带收益是前缀共享——多个请求共用相同前缀时,可以复用同一批块,这也是下一章 前缀稳定性(Prefix Stability) 在推理框架侧的落点。代价是实现复杂度上升、有寻址开销。
2.3 连续批处理:性价比最高的一刀
连续批处理(Continuous Batching) 不等整批做完,某个请求一结束就立刻补入新请求,让 GPU 始终有活干。它打的是「硬件空转」这根钉子——decode 阶段单请求的 GPU 利用率常是个位数,批处理把它拉高数倍,吞吐成倍提升,是性价比最高的一刀,现代推理框架(vLLM 等)已默认支持。代价是单请求延迟可能变差(被大 batch 拖累),所以对强延迟敏感的场景要控制 batch 上限。
2.4 投机解码:拿算力换串行
投机解码(Speculative Decoding) 让一个小模型(草稿模型)先一次性起草 k 个 token,大模型一次前向并行验证,验证通过的 token 直接采用。它打的是「串行依赖」这根钉子:decode 每步都要等上一步,投机解码让一次前向「搬运」产出多个 token。代价是需要额外的小模型或额外计算,加速比取决于「接受率」——小模型猜得越准,越划算;接受率低时可能反而更慢(净亏损)。它本质上是用「多算一点」换「少等一点」,适合输出长、延迟敏感的场景。
这四把锤子可以叠加(量化 + 连续批处理 + 投机解码,各打各的钉子),但叠加前先量清瓶颈——给 prefill 上量化治不了算力,给 decode 上分块治不了带宽。
三、MLA:结构层面的一次改写
除了上面四个「工程手段」,还有一条更彻底的路:改注意力本身的结构。
DeepSeek 提出的 MLA(Multi-head Latent Attention)(Multi-head Latent Attention)思路是:不直接缓存完整的 K/V,而是把它们压到一个低维的潜在向量里缓存,用的时候再投影回来。
顺带把四把锤子的发力点一次性说清:量化 同时压显存与搬运量,PagedAttention 消灭显存碎片(Memory Fragmentation),连续批处理 填平硬件空转,投机解码 绕开串行依赖——而后两者的提速分别受 接受率(Acceptance Rate) 与 显存带宽(Memory Bandwidth) 制约;此外还有 FlashAttention 与 算子融合(Operator Fusion) 这类「让单次计算更便宜」的手段。
用前面学过的语言描述:它压的是 KV Cache 这张「线性增长的账单」的系数。
- 常规 MHA:缓存量 ∝
n_layers × n_heads × head_dim - GQA:把
n_heads换成更小的n_kv_heads,系数降到 1/8 左右 - MLA:缓存的是压缩后的潜在向量,维度远小于
n_kv_heads × head_dim,系数进一步下降
这解释了为什么在同样的显存预算下,采用 MLA 的模型能支撑显著更长的上下文与更高的并发。这也是「结构创新本身就是成本创新」的最好例子。
优化的第一步不是选手段,是量瓶颈。没有测量就上手段,等于在黑暗中拧螺丝。本刊编辑部
四、动手:测一次,而不是猜一次
下面这段代码用纯 Python 模拟三种 decode 策略的「搬运次数」,用来建立数量级直觉。真实基准请用专业压测工具,但模型足够说明问题。
"""
用「显存搬运次数」估算 decode 成本。
核心假设:decode 每步都要把权重搬运一遍(带宽受限),
所以「总搬运量 = 步数 × 权重体积」,优化就是减少这两个因子。
"""
def weight_gb(n_params_b: float, bytes_per: int = 2) -> float:
"""权重体积:参数量 × 每参数字节数"""
return n_params_b * 1e9 * bytes_per / 1024**3
def naive_decode(n_params_b, out_tokens, bytes_per=2):
"""朴素逐 token:每一步搬一次全量权重"""
w = weight_gb(n_params_b, bytes_per)
return out_tokens * w, out_tokens # (总搬运 GiB, 前向次数)
def continuous_batching(n_params_b, out_tokens, concurrent, bytes_per=2):
"""连续批处理:一次搬运服务 concurrent 个请求(假设长度对齐)"""
w = weight_gb(n_params_b, bytes_per)
steps = out_tokens # 步数不变
# 但每一步服务 concurrent 个请求,等于摊薄了单位 token 的搬运量
return steps * w, steps * concurrent
def speculative(n_params_b, out_tokens, accept_rate, draft_tokens=4, bytes_per=2):
"""投机解码:小模型起草 k 个,大模型一次前向验证"""
calls = out_tokens / (1 + draft_tokens * accept_rate)
return calls * weight_gb(n_params_b, bytes_per), calls
def quantized(n_params_b, out_tokens, bytes_per, baseline_bytes=2):
"""量化:直接按字节比例缩小搬运量"""
return naive_decode(n_params_b, out_tokens, bytes_per)
print('=== 同一个 70B 模型,输出 512 token,fp16 ===')
base, calls = naive_decode(70, 512)
print(f'朴素 decode 搬运 {base:8.1f} GiB 前向 {calls:.0f} 次')
b, c = continuous_batching(70, 512, concurrent=16)
print(f'连续批处理 ×16 搬运 {b:8.1f} GiB 有效产出 {c:.0f} token')
b, c = speculative(70, 512, accept_rate=0.7)
print(f'投机解码 接受率 0.7 搬运 {b:8.1f} GiB 前向 {c:.0f} 次 ← 少了一半以上的搬运')
b, _ = quantized(70, 512, bytes_per=1)
print(f'fp8 量化 搬运 {b:8.1f} GiB ← 直接减半')- 把上面三个函数组合起来(量化 + 连续批处理 + 投机解码),算出叠加后的搬运量,理解为什么这几种手段可以叠加而不会互相抵消;
- 把
accept_rate从 0.9 降到 0.2,观察投机解码何时变成负优化; - 用一句话回答:为什么连续批处理能提升吞吐但可能恶化单请求延迟?
五、常见误区与追问
5.1 误区:Decode 慢是因为算力不够
错在哪:直觉上「生成慢 = 算得慢」。为什么自然:我们习惯用算力衡量深度学习瓶颈。正确做法:Decode 每步只生成一个 token,计算量极小,但每步都要把整个模型权重和 KV Cache 从显存搬到计算单元一次——时间花在搬运上,不是计算上。判据:看 GPU 利用率,若只有个位数百分比却算力充足,说明瓶颈是显存带宽而非算力;优化方向是「少搬 / 一次搬服务多请求 / 一次搬产多 token」,而不是堆算力。
5.2 误区:优化推理成本,先上量化最稳妥
错在哪:量化几乎人人会提,便当成默认第一步。为什么自然:它名字最熟、上手最容易。正确做法:应该先测瓶颈:GPU 利用率低、吞吐上不去 → 先看连续批处理(性价比最高的一刀);显存装不下 → 才上量化;碎片严重 → PagedAttention。判据:没量就上手段等于在黑暗中拧螺丝;被问「先优化哪个」时,先反问「你卡在显存、并发、吞吐还是延迟」,再给手段。
5.3 误区:投机解码总是比朴素 decode 快
错在哪:把「并行验证多个 token」想成必然加速。为什么自然:一次前向验证 k 个草稿,听起来省了 k−1 次前向。正确做法:加速的前提是草稿被大模型接受;接受率低时,草稿白白计算,反而净亏损。判据:设草稿数 k、接受率 r,等效前向次数 ≈ 步数 /(1 + k·r);当 r 很低(如 0.2 配 k=4 时等效约 1/1.8≈0.56 步/ token 的节省并不稳),需实测;接受率掉到阈值以下就关掉投机解码。
5.4 误区:PagedAttention 把注意力的平方复杂度降下来了
错在哪:看到「优化注意力」就联想到降复杂度。为什么自然:名字带 Attention,容易望文生义。正确做法:PagedAttention 只解决 KV Cache 的显存碎片——把缓存切成固定块、按需分配、物理不连续,从而提升可并发请求数;它不改 O(n²) 的注意力计算复杂度。判据:被问「PagedAttention 改了哪个公式因子」时,答案必须是显存利用率,而不是 n²;降复杂度是稀疏/线性注意力与 FlashAttention 的事。
5.5 误区:Prefill 和 Decode 用同一套优化手段
错在哪:把「推理优化」当成一张扁平清单随机挑。为什么自然:清单上都是「优化手段」,容易混用。正确做法:Prefill 瓶颈在算力(n 个位置可并行,卡在 FLOPs),Decode 瓶颈在显存带宽(强串行,卡在搬权重)。判据:问「这一步卡在算还是搬」——Prefill 上量化对 TTFT 帮助有限,Decode 上分块计算治不了带宽;手段必须对准阶段。
5.6 误区:FlashAttention 是推理专属或训练专属的优化
错在哪:按使用场景给它贴标签。为什么自然:常听人在「训练加速」语境下提到它。正确做法:FlashAttention 是训练与推理通用的注意力 IO 优化——通过分块与算子融合,减少中间结果的显存读写,不改变注意力语义。判据:它改的是「注意力怎么算更省 IO」,不是「注意力算什么」;在任何受显存带宽制约的阶段(prefill 尤甚)都能用。
六、自测
七、小结
| 手段 | 针对的瓶颈 | 主要收益 | 主要代价 |
|---|---|---|---|
| 量化 | 显存容量 + 搬运量 | 装得下、跑得快 | 精度损失 |
| PagedAttention | 显存碎片 | 并发数提升 | 实现复杂度、寻址开销 |
| 连续批处理 | 硬件空转 | 吞吐成倍提升 | 单请求延迟可能变差 |
| 投机解码 | 串行依赖 | 单请求延迟下降 | 低接受率时反而变慢 |
| MLA 类结构优化 | KV Cache 系数 | 长上下文 + 高并发 | 训练与实现复杂度 |
这一章的最终落点是那个决定性的问题:当一个成本问题出现时,你的第一反应是「换个更强的模型」,还是「先量一下瓶颈在哪」? 面试官几乎一定会用这个问题来区分这两类人——而这道题,正好是下一部分(Harness 工程)的入口。
八、参考与延伸
本章机制部分只讲到「够用」为止。想往下深挖,下面按「先看图、再看代码、最后读论文」的顺序排好了。全站不做原文转载,这里只登记链接与「为什么值得读」。
先看图(建立直觉)
- vLLM:用 PagedAttention 把 LLM 服务做到又快又省 —— 把 PagedAttention、连续批处理、KV 块管理讲得最直白,还有实测吞吐曲线。读本章第二节「四把锤子」卡住时,来这儿看工程落地最快。
- Lilian Weng · Large Transformer Model Inference Optimization —— 按「压模型」与「改推理系统」两条线把优化手段摊开,量化、剪枝、蒸馏、结构改造各占一节。想给本章那四把锤子补一个外部坐标系、看清哪些手段属于压缩模型、哪些属于改服务系统,读它最快。
再看代码(动手实现)
- 动手学深度学习(中文,zh.d2l.ai) —— 配套可跑代码,把注意力、量化、批处理都实现了一遍。把本章的 decode_cost_model 照着敲一遍,比看十遍都记得牢。
最后读论文(对齐一手定义)
- Efficient Memory Management for LLM Serving with PagedAttention(arXiv:2309.06180) —— PagedAttention 原论文,也是 vLLM 的基础。想搞清楚「碎片到底按什么粒度发生、连续批处理怎么和分页协同」,看它的系统设计那节。
- DeepSeek-V2(MLA)arXiv:2405.04434 —— MLA 的一手来源。重点看它怎么把 K/V 压成低维潜在向量再投影,以及为什么同样显存能撑更长上下文。
- Fast Inference from Transformers via Speculative Decoding(arXiv:2211.17192) —— 投机解码的一手论文。重点看接受率(acceptance rate)怎么决定加速比,以及为什么接受率低会净亏损。
◇ 面试官会怎么问
共 6 条 · 其中 2 条高频 · 先自己答一遍,再展开对照
如果让你把推理成本砍一半,你会动哪几个杠杆? 高频
① 先量瓶颈:是显存不够(并发上不去)、还是首 token 延迟高、还是吞吐被批处理限制?三个瓶颈对应完全不同的杠杆。
② 缓存与上下文侧(几乎无质量损失,优先级最高):提升前缀缓存命中率、裁剪工具结果、压缩历史。
③ 调度侧:连续批处理提高批内利用率、分页管理 KV Cache 降低碎片。
④ 模型侧:量化(FP8/INT8)、分级路由(简单任务走小模型)。
⑤ 采样侧:投机解码用小模型起草、大模型验证。
顺序很关键:能在上下文层解决的问题,不要动到模型精度。
追问链
- 每一档的代价是什么?
- 怎么验证优化没有伤害质量?
量化的代价到底是什么?
① 精度损失,且不是均匀的——对数值敏感的环节(长链推理、精确算术、结构化输出)更容易先崩;
② 长尾能力退化,平均分可能只掉一点,但某些能力会明显下滑,所以必须看分项而不是看总分;
③ 工程复杂度,不同量化格式对推理引擎、算子支持、硬件的要求不同,实际部署时"能不能跑"和"跑得快不快"是两回事。
结论:量化必须配评测,用平均分判断量化是否可行是危险的。
投机解码(speculative decoding)为什么能加速?
因为大模型的一次前向是并行的,验证 k 个 token 的成本接近验证 1 个,所以只要草稿的接受率够高,就能在一次前向上推进多个 token,端到端提速。
前提条件:草稿模型的分布要与目标模型足够接近(否则接受率低、反而更慢),且任务可预测性较强(代码补全、格式化输出这类收益最明显,自由创作收益有限)。
PagedAttention 是必须上的优化吗?什么情况下收益最大、什么情况下几乎没用?
收益最大:长上下文 + 高并发 + 各请求长度差异大。传统实现要按最大长度预留一段连续显存,长度参差时浪费极重;分页把缓存切成固定块按需分配,可并发请求数能显著上升。
收益很小:单请求、短上下文、低并发、长度整齐——此时几乎没有碎片可消灭。
判据:先看显存利用率与可并发请求数,再决定要不要上分页;它不改 O(n²) 的注意力复杂度,指望它降算力是方向性错误。
追问链
- 它和连续批处理是什么关系?
- 如果碎片不严重但吞吐上不去,你会先查什么?
投机解码:草稿模型一次起草 k=4 个 token,接受率 r=0.7 大概能拿到多少加速?r 掉到 0.2 时还能上吗? 高频
1/(1 + k·r)。r=0.7、k=4 时约为
1/3.8 ≈ 26%,也就是大约 3~4 倍的搬运量节省;r=0.2、k=4 时只有 1/1.8 ≈ 56%,而且草稿模型自身的计算与调度开销还没算进去,实际很可能净亏损。判据:上线前先实测接受率;r 低于 0.5 左右就别上,或者先换一个与目标模型分布更接近的草稿模型。
加速比还强依赖任务类型——代码补全、格式化输出这类高可预测任务收益最大,自由创作收益有限。
追问链
- 接受率低是草稿模型的问题还是任务的问题?怎么区分?
- 除了另训一个草稿模型,还有别的「一次前向产出多个 token」的办法吗?
线上看到「吞吐上不去、GPU 利用率只有个位数」,你会先上量化还是先上连续批处理?
GPU 利用率只有个位数说明硬件在空转,根因是「一批请求要等最慢的那个结束才换下一批」;连续批处理让某个请求一结束就立刻补入新请求,直接填满空转,是性价比最高的一刀。
量化解决的是另一类问题——显存装不下或搬运量太大:它减少每次搬运的字节数,对 decode 的带宽瓶颈有效,但对「批内没填满」这种调度问题无能为力。
判据:先问瓶颈是「算」「搬」还是「排」——排队问题用调度解,搬运问题用量化解,算力问题用融合与分块解。
追问链
- 连续批处理会不会让单请求延迟变差?为什么?
- 如果显存也不够、利用率也低,这两件事能一起做吗?
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。