KV Cache

自回归生成有一个看似昂贵的性质:每生成一个 token,都要对全部历史 token 重算注意力。幸运的是,历史 token 的 Key 和 Value 投影不会因为尾部新增 token 而改变——KV Cache 正是利用这一点,把每一层注意力的 K/V 保存下来,让 decode 时只需计算新 token 的投影并与缓存交互,避免重复计算整个前缀。

但省下的计算立刻变成显存账单。KV Cache 的容量公式是:

1
KV bytes = 2 (K和V) × batch × seq_len × layers × Hkv × head_dim × dtype_bytes

每个因子都随需求增长:长上下文推高 seq_len,高并发推高 batch。在 70B 级模型 × 长上下文 × 大并发场景下,KV Cache 会成为 VRAM 里最大的一块,甚至超过模型权重本身——而权重是固定的,缓存是随负载波动的。

这个账本直接解释了一系列连锁设计:

  • Grouped-Query Attention / MQA:从架构上减少 Hkv,源头缩小缓存;
  • KV 量化:降低 dtype_bytes;
  • prefix cache:相同系统提示/前缀的请求共享缓存段,不重复存储。

更隐蔽的问题在分配策略上。如果每个请求的 KV 必须按 max_seq_len 预分配连续内存,会产生双重浪费:请求实际只用了 300 个 token 却预留了 8K 位置(内部碎片);动态进出 batch 的请求无法灵活共享空间(外部碎片)。预留式管理直接限制了可容纳的并发数——这正是 continuous batching 的隐性瓶颈。

PagedAttention 借鉴操作系统虚拟内存分页的思想解决它:把 KV 切成固定大小的 block 按需分配,逻辑连续、物理离散。碎片大幅下降,同样的显存能容纳更多并发,Continuous Batching 的调度能力才真正释放。

边界上,KV Cache 是单次推理的计算状态,进程结束即消失;不要与 Agent State and Model APIs 里跨轮次的对话/推理状态混淆。后者即使 KV cache miss 也依然存在——miss 的代价只是新请求要重新 prefill 整个历史,多付一次计算和延迟,而不是“失忆”。

在网络中,KV Cache 是 serving 系统的枢纽资源:上游连接模型架构(GQA 决定它的体积),中游是引擎的核心管理对象(PagedAttention、prefix cache),下游约束拓扑决策(Prefill-Decode Disaggregation 需要 KV transfer)。容量规划几乎总要从这张账本开始。

一个容量估算例子

假设每层 Hkv=8head_dim=128、32 层、BF16(2 bytes),单请求历史长度 8K:

1
2 × 1 × 8192 × 32 × 8 × 128 × 2 bytes ≈ 1 GiB

这只是一个请求、一个序列长度和纯 KV 数据的粗估,不包含 block table、padding、临时 workspace 或多模态额外状态。它说明为什么 GQA、量化、prefix sharing 和分页分配会互相咬合:每项都在公式的不同因子或浪费项上动手。

它不是什么

KV Cache 不是模型长期记忆,不携带跨进程的语义承诺,也不是把完整 hidden states 保存下来。它是特定模型权重、token 顺序、位置编码和 attention 配置下的中间计算状态;换模型、换 tokenizer、改变 RoPE 配置或丢失 block 映射后都不能直接复用。

评测与故障定位

  • cache miss 主要表现为额外 prefill 与 TTFT,不等于对话语义丢失。
  • cache eviction 或容量不足会造成抢占、重算、排队和尾延迟上升。
  • prefix hit rate 高但吞吐不升,可能是命中前缀太短、copy-on-write 或 decode 带宽成为新瓶颈。
  • 规划容量时同时记录活跃请求数、长度分布、KV 峰值、命中率、抢占/重算次数和 TTFT/TPOT P99。