第7章:推理的代价——工程挑战的起源
训练一个大模型需要数百万美元和数月时间,这是众所周知的。但真正让 LLM 工程师夜不能寐的,往往不是训练,而是推理(Inference)。
本章是第一部分"历史脉络"的收尾,也是第二部分"推理优化"的引言。我们不讲解决方案——我们只讲问题从哪里来。读完本章,你将理解为什么推理优化领域会涌现出 KV Cache、PagedAttention、Continuous Batching、Speculative Decoding、FlashAttention、GQA 等一系列技术,以及它们各自试图解决的那个具体的工程痛点。
7.1 为什么推理是个独立的大问题
训练与推理的本质差异
训练是一次性成本:花钱买算力,跑完就结束。哪怕训练一次要一个月,只要结果好,都是可以接受的。
推理则完全不同——每一次用户请求,都要跑一遍模型。用户不会等你五分钟,也不会为一次对话付出云计算大客户的价格。推理必须:
- 快:用户体验要求首字延迟(Time-To-First-Token,TTFT)在秒级以内
- 省:单次推理的边际成本决定了产品能否盈利
- 并发:一台服务器要同时服务成百上千个用户
这三个要求相互制约,而且每一个单独拎出来都很难满足。
计算特点的根本差异
训练和推理在硬件利用方式上截然不同:
| 维度 | 训练 | 推理(Decode 阶段) |
|---|---|---|
| Batch 大小 | 大(512、2048…) | 小(1 到几十) |
| 瓶颈类型 | 计算密集(Compute-bound) | 访存密集(Memory-bound) |
| GPU 利用率 | 高(算力跑满) | 低(带宽成瓶颈) |
| 主要开销 | 矩阵乘法 | 从显存读取权重和 KV Cache |
"计算密集"意味着 GPU 的算力(FLOPS)是瓶颈,加快计算就能提速。"访存密集"意味着算力闲着,瓶颈在显存带宽(HBM Bandwidth)——权重和缓存数据搬运的速度限制了推理速度。
:::info 为什么 Decode 是访存密集的? 生成每个新 token 时,模型需要从显存中读取所有参数(一个 70B 模型的参数量约 140GB,以 FP16 存储)和所有历史 KV Cache,却只做极少量的计算。这种"读很多、算很少"的模式正是访存密集的定义。 :::
理解了这个根本差异,后面所有的工程挑战都会变得顺理成章。
7.2 朴素推理的瓶颈:重复计算
自回归生成的本质
LLM 生成文本的方式是自回归(Autoregressive):每次只生成一个 token,然后把这个 token 加入输入,再生成下一个。
设输入序列长度为 ,则:
- 生成第 个 token:需要计算对前 个 token 的 Attention
- 生成第 个 token:需要计算对前 个 token 的 Attention
- ……
在标准 Attention 中,对序列中的第 个位置,注意力得分计算为:
关键在于 和 :这是所有历史 token 的 Key 和 Value 矩阵。
朴素实现的问题
最简单的实现方式:每生成一个新 token,就把整个序列(包括历史部分)重新过一遍 Transformer。这意味着:
- 生成第 个 token 时,要对长度为 的历史序列重新计算所有层的 、
- 总计算量约为 ,其中 是最终序列长度
具体数字:生成一段 1000 token 的回复,总共需要计算 次"完整序列注意力"。序列每增长一倍,计算量增长四倍。
:::warning 这是不可接受的 对于一个 4096 token 的长对话,朴素实现的计算量约是 1000 token 场景的 16 倍。用户需要等待的时间也是线性增长的。 :::
这个问题的解法呼之欲出:既然历史 token 的 、 反正不变,为什么不把它们缓存起来? 这正是 KV Cache 的动机——我们将在第8章详细讲解。
7.3 KV Cache 带来的新问题
引入 KV Cache 后,每个 token 的 、 只计算一次,推理速度有了质的飞跃。但新问题随之而来:缓存存在哪里?
显存占用的线性增长
KV Cache 需要存在显存(GPU HBM)里,才能被快速访问。每个 token 的缓存大小为:
以 LLaMA-2 70B(FP16)为例:
- 层数
- 每层 KV 头数 (GQA 之前)
- 头维度
- FP16:每个值 2 bytes
4096 个 token 的上下文:
并发请求:显存直接爆
一台配备 4 张 A100(每张 80GB)的服务器,总显存 320GB。扣除模型权重约 140GB,可用于 KV Cache 的显存约 180GB。
100 个并发请求,每个上下文 4096 token:
这比可用显存多出近 6 倍。即使把并发降到 10,也需要 102GB,几乎耗尽全部剩余显存。
预分配浪费严重
早期系统的做法是为每个请求预分配最大长度的 KV Cache(比如 4096 token)。但实际对话可能只用了 200 token 就结束了,剩下的 3896 个 token 的显存槽位白白浪费。
:::info 内存碎片问题 更糟的是,不同请求的 KV Cache 大小不同,随着请求不断到来和结束,显存会产生大量碎片,实际利用率可能只有 20%~40%。 :::
这一系列问题——显存不够用、预分配浪费、碎片严重——共同催生了 PagedAttention(第9章)的诞生:借鉴操作系统虚拟内存的分页机制,按需分配 KV Cache。
7.4 GPU 利用率的困境
Decode 阶段的天然低效
即使解决了显存问题,还有一个更基本的困境:Decode 阶段每次只生成 1 个 token。
一块 A100 GPU 的峰值算力约为 312 TFLOPS(BF16)。但生成一个 token 时,需要的计算量只有约 (粗略估计,每个参数一次乘加)。
哪怕不考虑访存瓶颈,这个计算量也只能占满 A100 峰值的 0.04%。实际测量的 GPU 利用率(MFU,Model FLOP Utilization)在 Decode 阶段往往只有 1%~5%。
简单 Batching:木桶效应
最直觉的解法是把多个请求的 Decode 合并成一个 Batch 一起做。但这会遇到新问题:
请求 A:已生成 500 token,还需要生成 500 token
请求 B:已生成 50 token,还需要生成 450 token
请求 C:已生成 900 token,只需要生成 100 token ← 很快就完成了
如果把 A、B、C 打包成一个 Batch,必须等所有请求都完成才能释放资源。请求 C 完成后,它的显存槽位和算力槽位被白白占着,等待 A 和 B 慢慢生成。
同时,新到来的请求 D 只能在队列里等,即使此时 GPU 还有余量。
这个"短请求等长请求"的木桶效应,是Continuous Batching(第10章) 要解决的核心问题:允许完成的请求立即离开 Batch,新请求随时插入,让 GPU 的利用率趋于连续饱和。
7.5 能不能生成得更快
大模型和小模型的两难
到目前为止讨论的都是如何更高效地跑同一个模型。但有时候问题更根本:模型本身就是瓶颈。
70B 参数的大模型质量好,但生成速度慢(受访存带宽限制,每个 token 需要从显存搬运约 140GB 的权重)。7B 的小模型速度快 10 倍,但质量差。
有没有办法得到大模型的质量,同时接近小模型的速度?
投机解码的直觉
观察发现:在大量文本中,很多 token 的生成其实是"显而易见"的——不需要大模型来确认。比如 "中国的首都是北__" 后面必然是 "京",7B 模型就能预测对。
Speculative Decoding(投机解码) 的思路:
- 用小模型快速生成多个候选 token(draft)
- 用大模型并行验证这些 token 是否正确
- 如果验证通过,一次性接受多个 token
关键在于:大模型验证 个 token 的计算成本,几乎等于生成 1 个 token——因为可以并行处理。如果平均能接受 3 个 draft token,就相当于速度提升了 3 倍。
:::tip 为什么能接受多个 token? 大模型验证时是并行跑一次前向传播(Forward Pass),计算量是 而不是 ,因为我们只关心每个位置的验证,不需要序列依赖。但注意:这需要精细的数学设计来保证输出分布不变。第11章将详细推导。 :::
7.6 注意力计算本身的显存瓶颈
前面的问题都集中在 KV Cache 和系统层面。但 Attention 计算本身也有一个隐藏的瓶颈:完整的 注意力矩阵。
标准 Attention 的实现
标准实现中,对长度为 的序列,Attention 计算分三步:
- 计算得分矩阵 ,大小为
- Softmax:,大小为
- 输出:,大小为
问题出在步骤 1 和 2: 的矩阵必须完整写入显存,再读回来。
对 (32K 上下文),FP16:
每个注意力头都要这样一次。一个有 64 个头的模型,单次 Attention 就需要 的显存吞吐。
瓶颈在搬运,不在计算
更糟的是,这个 矩阵会在 HBM(高带宽显存)和 SRAM(片上缓存)之间来回搬运多次。A100 的 HBM 带宽约 2 TB/s,SRAM 带宽约 19 TB/s。
瓶颈不是 CUDA Core 的计算速度,而是数据搬运的带宽。哪怕 GPU 的算力完全闲置,Attention 也快不了,因为数据在排队等着搬运。
这个认识催生了 FlashAttention(第12章):通过分块计算(Tiling),让中间结果尽量留在 SRAM 里,避免写回 HBM,从根本上减少数据搬运次数。
7.7 多头注意力的参数冗余
MHA 的 KV Cache 负担
标准多头注意力(Multi-Head Attention,MHA)中,每个注意力头都有独立的 、 投影矩阵。对于有 个头的模型,KV Cache 的大小正比于 。
以 GPT-3 175B 为例(96 层,96 头,头维度 128):
这比 LLaMA-2 70B 的例子还要大。
不同头之间的冗余
研究发现,不同注意力头的 、 矩阵之间存在大量冗余:多个头可能在关注几乎相同的上下文信息,只是查询(Query)方向不同。
这个观察催生了一系列改进:
| 方案 | 全名 | 核心思想 | KV 头数 |
|---|---|---|---|
| MHA | Multi-Head Attention | 每头独立 K、V | |
| MQA | Multi-Query Attention | 所有头共享同一对 K、V | |
| GQA | Grouped-Query Attention | 组,每组共享 K、V | |
| MLA | Multi-head Latent Attention | 低秩压缩 KV,解压后使用 | 等效 |
MQA 把 KV Cache 压缩到 ,但质量有损失。GQA(LLaMA-3、Mistral 等采用)在 KV Cache 和质量之间取得平衡。MLA(DeepSeek V2 提出)通过低秩分解进一步压缩,同时保持较高的模型质量。
:::info 为什么这很重要? 从 MHA 到 GQA,KV Cache 可以减少 4×~8×。这意味着同样的显存能支持 4×~8× 更多的并发请求,或者支持 4×~8× 更长的上下文。在推理服务的成本和容量规划中,这是巨大的差异。 :::
第13章将详细推导 MQA、GQA、MLA 的数学原理和工程权衡。
本章小结
本章梳理了推理工程挑战的来龙去脉。每个痛点都是一个问题,每个问题都催生了一项技术:
| 问题 | 根本原因 | 对应技术 | 章节 |
|---|---|---|---|
| 每步重复计算历史 K、V | 自回归生成,历史 token 结果可复用 | KV Cache | 第8章 |
| KV Cache 显存不足、碎片严重 | 预分配导致浪费,并发请求显存爆 | PagedAttention | 第9章 |
| GPU 利用率低、短请求等长请求 | Decode 每次只生成 1 token | Continuous Batching | 第10章 |
| 大模型速度慢 | 访存带宽是瓶颈,权重搬运耗时 | Speculative Decoding | 第11章 |
| 注意力矩阵显存瓶颈 | 中间结果反复在 HBM 和 SRAM 间搬运 | FlashAttention | 第12章 |
| MHA 的 KV Cache 随头数线性增长 | 不同头的 K、V 存在大量冗余 | MQA / GQA / MLA | 第13章 |
推理优化不是一个单一问题,而是一族相互关联的工程挑战。理解了这个问题族,接下来的每一章都会是一个清晰的"问题 → 解法 → 代价"故事。
接下来,让我们从最基础的优化开始——第8章将深入 KV Cache 的实现细节,看看这个"缓存"到底缓存了什么、怎么组织、以及为什么它的实现方式会直接影响后面所有优化技术的设计。