LLM Infer Infra 学习资料库

KV Cache 原理:复用条件与容量推导

本篇定位:KV 存储方向推荐

聚焦“什么时候能复用、谁负责什么”:容量公式的例外、PagedAttention/APC/LMCache 的分工、为什么文本相同不等于能复用、缓存不是持久化数据库。

收益与 chunk、分层的定量分析在KV 容量与性能模型;复杂模型见复杂模型与 KV 存储。

1. 为什么缓存 K 和 V

一句话:生成新 token 时,模型要“回头看”前面所有 token 的 K 和 V。把它们存下来,就不用每一步都把历史重新算一遍。

以某一层的某一个注意力头为例,每个位置的表示经过投影得到 Q、K、V。生成下一个 token 时,新 token 的 Q 要和历史上所有有效位置的 K、V 做注意力:

Attention ( Q , K , V ) = softmax ( Q K ⊤ d + M ) V

其中 M 是因果掩码,保证每个 token 只看得到它之前的 token。所以历史 token 的 K、V 一旦算出就不会再变,可以保存下来;新 token 只需要算自己的投影。历史的 Q 在普通自回归解码里用不到,所以不缓存。

要注意两点:

Prefill 和 Decode 的差别:Prefill 一次处理 prompt 的很多位置,并行度高,通常更吃算力;Decode 每条序列每步只生成一个 token,很多情况下更吃显存带宽。这不是绝对的——batch 大小、模型、上下文长度、attention kernel 和量化都会改变瓶颈。连续批处理把不同请求的两个阶段交错在一起执行;PD 分离则把两个阶段放到不同的机器池里。

2. 容量公式和它的例外

当每一层的 KV head 数、head 维度和数据类型都相同时(普通注意力):

M KV = 2 × L × T × H KV × D × b

2 表示 K 和 V 各一份;L 是层数;T 是有效 token 数;HKV 是 KV head 数;D 是每个 head 的维度;b 是每个元素的字节数。

这个公式算的是逻辑上的 KV 数据量,不包括分配器的空洞、对齐、量化缩放因子、对象元数据、临时 buffer 和副本。多条不同序列按各自长度相加;共享前缀只有在真的共用同一份物理数据时才能去重。

不同注意力结构算法不同:

结构怎么算常见错误
MHAHKV 通常等于 query head 数把参数量当成 KV 容量
GQA多个 query head 共用一组 KV,用实际的 HKV仍按 query head 数算,结果大好几倍
MQAHKV 通常为 1以为任意 TP 都能不复制地切开
MLA按引擎实际缓存的 latent 和 RoPE 分量相加,没有单独的 V套用 2 × head × D;或以为每个 rank 只存总量的 1/TP
混合 / 滑窗全局层、滑窗层、递归状态层分别算再相加让所有层都随上下文长度线性增长

复杂结构的详细算法见复杂模型与 KV 存储。

2.1 算一个例子

L = 32、HKV = 8、D = 128、BF16(b = 2):

单位别混:GB 是 109 字节,GiB 是 230 字节;网卡的 Gbit/s 是比特。200 Gbit/s 网卡的理论线速是 25 GB/s,传 1 GiB 的纯网络下界约 42.95 ms——不是 5 ms,也不是 1/200 秒。实际还要加上 H2D、协议、排队和布局转换。

下面的小程序可以直接运行验证:

# SPDX-License-Identifier: Apache-2.0
layers = 32
kv_heads = 8
head_dim = 128
bytes_per_element = 2
tokens = 8192
bytes_per_token = 2 * layers * kv_heads * head_dim * bytes_per_element
request_bytes = bytes_per_token * tokens
chunk_bytes = bytes_per_token * 256
network_bytes_per_second = 200e9 / 8
print(f"KV/token: {bytes_per_token / 1024:.0f} KiB")
print(f"KV/request: {request_bytes / 2**30:.2f} GiB")
print(f"KV/chunk: {chunk_bytes / 2**20:.0f} MiB")
print(f"Network lower bound: {request_bytes / network_bytes_per_second * 1000:.2f} ms")

输出依次是 128 KiB、1.00 GiB、32 MiB、42.95 ms。这只是计算,没有测量任何真实硬件。也可以在互动容量计算器里改参数看。

3. PagedAttention、APC 和 LMCache 各管什么

所以如果一个请求已经被 APC 在 GPU 上完全命中,LMCache 不一定再带来读取收益(在某些配置下它仍可能负责卸载或为将来保留数据)。做测试时,必须把 APC 的命中和外部缓存的命中分开记录。

请求 A: [共享 system prompt][共享文档][问题 A]
请求 B: [共享 system prompt][共享文档][问题 B]
         ↑ GPU APC 和 LMCache 都可能复用这段连续前缀

请求 C: [另一个 system prompt][相同文档][问题 C]
         ↑ 前面不同,普通前缀缓存不能复用后面这段文档的 KV

block 和 chunk 不是一回事:block 是引擎分配显存的单位;chunk 是 LMCache 计算 hash、组织对象和传输的单位。MP 模式会协商 chunk 大小,让它和 block、各个注意力组对齐,所以启动时填的值不一定是最终用的值,见 Chunk size 协商。

只存完整 chunk 时,长度 T 有 ⌊T / C⌋ 个完整 chunk,剩下的尾巴按引擎和 Connector 的规则处理。比如 chunk = 256 时,255、256、257 个 token 分别有 0、1、1 个完整 chunk。但不能因此断定最终一定复用 0、256、256 个 token:生成第一个输出需要最后一个位置的 logits,加上掩码、APC 覆盖范围和接口限制,实际命中可能更少。

4. 能不能复用,比“文本相同”更严格

第 i 个 chunk 的身份可以这样理解(教学写法):

h i = H ( h i − 1 , tokens i , extra i )

也就是说,每个 chunk 的 hash 都包含了前一个 chunk 的 hash(前缀链)。同样的一段文字,出现在不同的前文之后,身份就不同。实际实现还涉及初始 hash、序列化方式、算法选择和模型/租户命名空间。不能拿某种语言默认的 hash() 函数对 token ID 列表求值,就当作跨进程稳定的协议。

复用必须保证“产生这份 KV 的计算完全等价”,至少要检查五项:

  1. 模型权重和版本相同,包括影响前向计算的 LoRA/adapter。
  2. 分词、模板展开后的 token 完全相同;多模态还要包含图片、音频等输入的身份。
  3. 历史上下文、注意力掩码、位置编码(RoPE)设置和可见范围相同。
  4. dtype、量化信息、内存布局、层与组、TP/PP 切分方式兼容。
  5. 租户和授权隔离正确——一个字符串 key 不能代替身份认证。

这是系统应当保证的五个方面,并不是说当前的 ObjectKey 已经自动包含了全部。接口里没表达的部分,要靠模型命名空间、版本隔离和兼容性校验补上。

两个常被问到的点:

5. 缓存不是持久化数据库

KV 通常可以由输入和模型重新算出来,所以缓存系统可以接受“丢了就重算”,换来更简单的生命周期管理。但有一条底线:绝不能把没写完或错误的数据当成命中。

6. 什么时候复用才划算

首 token 时间(TTFT)大致由这几段组成:

TTFT ≈ T queue + T lookup + T load,critical + T prefill,remaining + T first output

依次是:排队、查找、关键路径上的加载、剩余部分的 Prefill、生成第一个输出。加载如果是串行的,就是查询、L2 读取、网络、解码和 H2D 相加;如果充分流水,更接近最慢那一段再加上启动和收尾。不能一边把各阶段全部相加,一边又说它们完全重叠。

复用划算的条件是:

T recompute,saved > T lookup + T load,exposed + T interference

即:省下的重算时间,要大于查找时间、没能被隐藏的加载时间,以及对其他请求的干扰之和。

省下多少 Prefill 时间要靠对照测量,不能按命中 token 的比例直接折算:注意力的计算量和长度不是线性关系,剩下的 query 仍要关注旧的 KV,batch 效率和排队也会变化。命中很短、后端往返时间长、或者传输抢了 Decode 的带宽时,重算反而更快。可以用互动盈亏计算器试一试。

7. 自测

输入主题、术语或问题