跳到主要内容

第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 加入输入,再生成下一个。

设输入序列长度为 nn,则:

  • 生成第 n+1n+1 个 token:需要计算对前 nn 个 token 的 Attention
  • 生成第 n+2n+2 个 token:需要计算对前 n+1n+1 个 token 的 Attention
  • ……

在标准 Attention 中,对序列中的第 ii 个位置,注意力得分计算为:

Attention(Qi,K1:i,V1:i)=softmax(QiK1:iTdk)V1:i\text{Attention}(Q_i, K_{1:i}, V_{1:i}) = \text{softmax}\left(\frac{Q_i K_{1:i}^T}{\sqrt{d_k}}\right) V_{1:i}

关键在于 K1:iK_{1:i}V1:iV_{1:i}:这是所有历史 token 的 Key 和 Value 矩阵

朴素实现的问题

最简单的实现方式:每生成一个新 token,就把整个序列(包括历史部分)重新过一遍 Transformer。这意味着:

  • 生成第 kk 个 token 时,要对长度为 k1k-1 的历史序列重新计算所有层的 KKVV
  • 总计算量约为 O(L2)O(L^2),其中 LL 是最终序列长度

具体数字:生成一段 1000 token 的回复,总共需要计算 1+2+3++999500,0001 + 2 + 3 + \cdots + 999 \approx 500{,}000 次"完整序列注意力"。序列每增长一倍,计算量增长四倍。

:::warning 这是不可接受的 对于一个 4096 token 的长对话,朴素实现的计算量约是 1000 token 场景的 16 倍。用户需要等待的时间也是线性增长的。 :::

这个问题的解法呼之欲出:既然历史 token 的 KKVV 反正不变,为什么不把它们缓存起来? 这正是 KV Cache 的动机——我们将在第8章详细讲解。


7.3 KV Cache 带来的新问题

引入 KV Cache 后,每个 token 的 KKVV 只计算一次,推理速度有了质的飞跃。但新问题随之而来:缓存存在哪里?

显存占用的线性增长

KV Cache 需要存在显存(GPU HBM)里,才能被快速访问。每个 token 的缓存大小为:

每 token KV Cache 大小=2×nlayers×nheads×dhead×dtype_size\text{每 token KV Cache 大小} = 2 \times n_{\text{layers}} \times n_{\text{heads}} \times d_{\text{head}} \times \text{dtype\_size}

以 LLaMA-2 70B(FP16)为例:

  • 层数 nlayers=80n_{\text{layers}} = 80
  • 每层 KV 头数 nheads=64n_{\text{heads}} = 64(GQA 之前)
  • 头维度 dhead=128d_{\text{head}} = 128
  • FP16:每个值 2 bytes
每 token=2×80×64×128×2=2,621,440 bytes2.5 MB\text{每 token} = 2 \times 80 \times 64 \times 128 \times 2 = 2{,}621{,}440 \text{ bytes} \approx 2.5 \text{ MB}

4096 个 token 的上下文4096×2.5 MB10 GB4096 \times 2.5 \text{ MB} \approx 10 \text{ GB}

并发请求:显存直接爆

一台配备 4 张 A100(每张 80GB)的服务器,总显存 320GB。扣除模型权重约 140GB,可用于 KV Cache 的显存约 180GB。

100 个并发请求,每个上下文 4096 token

100×4096×2.5 MB=1,024 GB100 \times 4096 \times 2.5 \text{ MB} = 1{,}024 \text{ GB}

这比可用显存多出近 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 时,需要的计算量只有约 2×70×109140 GFLOPS2 \times 70 \times 10^9 \approx 140 \text{ GFLOPS}(粗略估计,每个参数一次乘加)。

哪怕不考虑访存瓶颈,这个计算量也只能占满 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(投机解码) 的思路:

  1. 用小模型快速生成多个候选 token(draft)
  2. 用大模型并行验证这些 token 是否正确
  3. 如果验证通过,一次性接受多个 token

关键在于:大模型验证 kk 个 token 的计算成本,几乎等于生成 1 个 token——因为可以并行处理。如果平均能接受 3 个 draft token,就相当于速度提升了 3 倍。

:::tip 为什么能接受多个 token? 大模型验证时是并行跑一次前向传播(Forward Pass),计算量是 O(k)O(k) 而不是 O(k2)O(k^2),因为我们只关心每个位置的验证,不需要序列依赖。但注意:这需要精细的数学设计来保证输出分布不变。第11章将详细推导。 :::


7.6 注意力计算本身的显存瓶颈

前面的问题都集中在 KV Cache 和系统层面。但 Attention 计算本身也有一个隐藏的瓶颈:完整的 n×nn \times n 注意力矩阵

标准 Attention 的实现

标准实现中,对长度为 nn 的序列,Attention 计算分三步:

  1. 计算得分矩阵 S=QKT/dkS = QK^T / \sqrt{d_k},大小为 n×nn \times n
  2. Softmax:P=softmax(S)P = \text{softmax}(S),大小为 n×nn \times n
  3. 输出:O=PVO = PV,大小为 n×dvn \times d_v

问题出在步骤 1 和 2:n×nn \times n 的矩阵必须完整写入显存,再读回来

n=32768n = 32768(32K 上下文),FP16:

32768×32768×2 bytes2 GB32768 \times 32768 \times 2 \text{ bytes} \approx 2 \text{ GB}

每个注意力头都要这样一次。一个有 64 个头的模型,单次 Attention 就需要 64×2 GB=128 GB64 \times 2 \text{ GB} = 128 \text{ GB} 的显存吞吐。

瓶颈在搬运,不在计算

更糟的是,这个 n×nn \times n 矩阵会在 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)中,每个注意力头都有独立的 KKVV 投影矩阵。对于有 HH 个头的模型,KV Cache 的大小正比于 HH

以 GPT-3 175B 为例(96 层,96 头,头维度 128):

KV Cache per token=2×96×96×128×2=4,718,592 bytes4.5 MB\text{KV Cache per token} = 2 \times 96 \times 96 \times 128 \times 2 = 4{,}718{,}592 \text{ bytes} \approx 4.5 \text{ MB}

这比 LLaMA-2 70B 的例子还要大。

不同头之间的冗余

研究发现,不同注意力头的 KKVV 矩阵之间存在大量冗余:多个头可能在关注几乎相同的上下文信息,只是查询(Query)方向不同。

这个观察催生了一系列改进:

方案全名核心思想KV 头数
MHAMulti-Head Attention每头独立 K、VHH
MQAMulti-Query Attention所有头共享同一对 K、V11
GQAGrouped-Query AttentionGG 组,每组共享 K、VGHG \ll H
MLAMulti-head Latent Attention低秩压缩 KV,解压后使用等效 1\approx 1

MQA 把 KV Cache 压缩到 1/H1/H,但质量有损失。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 tokenContinuous Batching第10章
大模型速度慢访存带宽是瓶颈,权重搬运耗时Speculative Decoding第11章
n×nn \times n 注意力矩阵显存瓶颈中间结果反复在 HBM 和 SRAM 间搬运FlashAttention第12章
MHA 的 KV Cache 随头数线性增长不同头的 K、V 存在大量冗余MQA / GQA / MLA第13章

推理优化不是一个单一问题,而是一族相互关联的工程挑战。理解了这个问题族,接下来的每一章都会是一个清晰的"问题 → 解法 → 代价"故事。


接下来,让我们从最基础的优化开始——第8章将深入 KV Cache 的实现细节,看看这个"缓存"到底缓存了什么、怎么组织、以及为什么它的实现方式会直接影响后面所有优化技术的设计。