
大模型推理为什么慢:注意力机制与 KV Cache
很多人第一次调本地大模型都会有同一个疑问:参数量差不多的模型,为什么有的每秒能吐十几个字,有的连两个字都卡。除了硬件差异,更关键的是推理过程本身的特征。这篇从注意力计算量出发,梳理推理的两个阶段和 KV Cache 的作用。
推理分为两个阶段
大模型的生成过程可以拆成两个明显不同的阶段:
- Prefill(预填充):把整段输入一次性送进模型,计算所有 token 的表示。这一步是高度并行的矩阵运算,通常能把 GPU 打满。
- Decode(解码):每次只生成一个 token,生成完再把它拼回输入,继续下一次。这个过程要重复成百上千次。
Prefill 的瓶颈是算力,Decode 的瓶颈则是显存带宽。每生成一个 token,模型都要把全部权重从显存里读一遍,真正参与计算的却只有一个向量。所以衡量体验最重要的指标通常不是总耗时,而是 Decode 阶段的每 token 延迟。
注意力的计算量
自注意力在序列长度为 n、隐藏维度为 d 时的复杂度是:
注意力计算量 ≈ O(n² · d)
n² 这一项意味着序列翻倍,注意力部分的计算量大约变成四倍。这也是长上下文贵的根本原因。不过要注意,在 Decode 阶段 n 是逐步增长的,真正决定单步延迟的往往不是 n²,而是每步都要重新计算的重复部分。
KV Cache 解决了什么
普通的自回归实现里,生成第 t 个 token 时会把前 t-1 个 token 重新跑一遍,得到它们的 Key 和 Value。这些结果其实和上一步完全一样,属于纯粹的重复劳动。
KV Cache 的思路很直接:把每一层已经算过的 Key、Value 缓存下来,下一步只计算新 token 的 Q、K、V,再和缓存拼接。这样每步的注意力计算量从 O(t²) 降到 O(t)。
# 简化示意:只传入新 token,历史 K/V 由缓存提供
outputs = model(
input_ids=next_token_id,
past_key_values=past_key_values, # 上一步缓存的 K/V
use_cache=True,
)
past_key_values = outputs.past_key_values
代价是显存占用随序列长度线性增长。缓存大小大致等于:
2 × 层数 × 头数 × 头维度 × 序列长度 × 精度字节数
在 32 层、32 头、头维度 128、FP16 的配置下,单条 8K 上下文的 KV Cache 就要几百 MB。并发一上来,这部分显存会迅速吃满,这也是吞吐量上不去的主要原因。
常见的优化方向
围绕上面的分析,工业界常用的手段基本都指向同一个目标——减少每步读取的数据量:
- 量化权重与 KV Cache:把 FP16 换成 INT8/INT4,直接降低带宽压力,代价是精度损失。
- PagedAttention:把 KV Cache 按页管理,避免为最大长度预留显存,显著提升并发下的显存利用率。
- 连续批处理(Continuous Batching):请求随时进出批次,而不是等一整批跑完,提升 GPU 利用率。
- 投机解码(Speculative Decoding):用小模型草拟、大模型校验,把多次串行 Decode 换成一次并行验证。
- GQA / MQA:多个注意力头共享同一组 K/V,从结构上把缓存体积降下来。
小结
理解 Prefill 和 Decode 的区别,是排查推理性能问题的第一步。如果你的场景首字延迟高,先看 Prefill;如果吐字慢、并发上不去,那多半是显存带宽和 KV Cache 的问题。顺着这条线索去调量化和批处理,通常比盲目换硬件更有效。