KV Cache 原理:复用条件与容量推导
本篇定位:KV 存储方向推荐
聚焦“什么时候能复用、谁负责什么”:容量公式的例外、PagedAttention/APC/LMCache 的分工、为什么文本相同不等于能复用、缓存不是持久化数据库。
收益与 chunk、分层的定量分析在KV 容量与性能模型;复杂模型见复杂模型与 KV 存储。
1. 为什么缓存 K 和 V
一句话:生成新 token 时,模型要“回头看”前面所有 token 的 K 和 V。把它们存下来,就不用每一步都把历史重新算一遍。
以某一层的某一个注意力头为例,每个位置的表示经过投影得到 Q、K、V。生成下一个 token 时,新 token 的 Q 要和历史上所有有效位置的 K、V 做注意力:
其中 M 是因果掩码,保证每个 token 只看得到它之前的 token。所以历史 token 的 K、V 一旦算出就不会再变,可以保存下来;新 token 只需要算自己的投影。历史的 Q 在普通自回归解码里用不到,所以不缓存。
要注意两点:
- 缓存省掉的是历史部分的计算,不是读取。注意力每一步仍要把历史 K、V 读一遍,所以长上下文的 Decode 依然可能受显存带宽和容量限制。
- KV 是模型运行时的中间状态,不是原文、embedding、检索向量,也不是最终答案。
Prefill 和 Decode 的差别:Prefill 一次处理 prompt 的很多位置,并行度高,通常更吃算力;Decode 每条序列每步只生成一个 token,很多情况下更吃显存带宽。这不是绝对的——batch 大小、模型、上下文长度、attention kernel 和量化都会改变瓶颈。连续批处理把不同请求的两个阶段交错在一起执行;PD 分离则把两个阶段放到不同的机器池里。
2. 容量公式和它的例外
当每一层的 KV head 数、head 维度和数据类型都相同时(普通注意力):
2 表示 K 和 V 各一份;L 是层数;T 是有效 token 数;HKV 是 KV head 数;D 是每个 head 的维度;b 是每个元素的字节数。
这个公式算的是逻辑上的 KV 数据量,不包括分配器的空洞、对齐、量化缩放因子、对象元数据、临时 buffer 和副本。多条不同序列按各自长度相加;共享前缀只有在真的共用同一份物理数据时才能去重。
不同注意力结构算法不同:
| 结构 | 怎么算 | 常见错误 |
|---|---|---|
| MHA | HKV 通常等于 query head 数 | 把参数量当成 KV 容量 |
| GQA | 多个 query head 共用一组 KV,用实际的 HKV | 仍按 query head 数算,结果大好几倍 |
| MQA | HKV 通常为 1 | 以为任意 TP 都能不复制地切开 |
| MLA | 按引擎实际缓存的 latent 和 RoPE 分量相加,没有单独的 V | 套用 2 × head × D;或以为每个 rank 只存总量的 1/TP |
| 混合 / 滑窗 | 全局层、滑窗层、递归状态层分别算再相加 | 让所有层都随上下文长度线性增长 |
复杂结构的详细算法见复杂模型与 KV 存储。
2.1 算一个例子
L = 32、HKV = 8、D = 128、BF16(b = 2):
- 每 token:131,072 字节 = 128 KiB。
- 8,192 token:1 GiB;一个 256 token 的 chunk:32 MiB。
- 64 条互不共享的 8K 序列:64 GiB。
- TP = 4 且 KV head 正好均分:每个 rank 每个 8K 请求 256 MiB。如果 KV 被复制、PP 切分了层,或者模型有额外状态,要按实际布局重算。
单位别混: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 各管什么
- PagedAttention:引擎把每条序列的 KV 分成逻辑 block,映射到 GPU 上不连续的物理页,不用为每个请求预留一整段最大长度的显存。分页减少了浪费,但不能消除所有碎片和拷贝。
- APC(自动前缀缓存):在分页基础上识别相同前缀,让多个请求共用已有的 GPU block。
- LMCache:把复用扩展到 GPU 之外——主机内存、远端存储,或者独立于引擎进程的服务。
所以如果一个请求已经被 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 的身份可以这样理解(教学写法):
也就是说,每个 chunk 的 hash 都包含了前一个 chunk 的 hash(前缀链)。同样的一段文字,出现在不同的前文之后,身份就不同。实际实现还涉及初始 hash、序列化方式、算法选择和模型/租户命名空间。不能拿某种语言默认的 hash() 函数对 token ID 列表求值,就当作跨进程稳定的协议。
复用必须保证“产生这份 KV 的计算完全等价”,至少要检查五项:
- 模型权重和版本相同,包括影响前向计算的 LoRA/adapter。
- 分词、模板展开后的 token 完全相同;多模态还要包含图片、音频等输入的身份。
- 历史上下文、注意力掩码、位置编码(RoPE)设置和可见范围相同。
- dtype、量化信息、内存布局、层与组、TP/PP 切分方式兼容。
- 租户和授权隔离正确——一个字符串 key 不能代替身份认证。
这是系统应当保证的五个方面,并不是说当前的 ObjectKey 已经自动包含了全部。接口里没表达的部分,要靠模型命名空间、版本隔离和兼容性校验补上。
两个常被问到的点:
- temperature、top-p 会不会让 KV 失效?不会。采样作用在 logits 之后,不改变同一个 prompt 的 KV;只是生成的新 token 不同,后续 KV 自然不同。
- 换了前文,能不能把 K 重新旋转一下就复用?一般不能。RoPE 只改变位置相关的部分,而深层的隐藏状态已经通过注意力“吸收”了原来的上下文。CacheBlend 这类方法通过有选择地重算一部分 token 并评估质量来做非前缀复用,不是无损拼接。
5. 缓存不是持久化数据库
KV 通常可以由输入和模型重新算出来,所以缓存系统可以接受“丢了就重算”,换来更简单的生命周期管理。但有一条底线:绝不能把没写完或错误的数据当成命中。
- 多副本能提高可用性,但内存里的副本不能防断电。
- “写入完成”在不同层含义不同:GPU 拷贝完成、L1 可读、L2 写成功、远端副本持久化,是四个不同的承诺。
- 能重算的前提是保留了原始输入和模型版本。
- 大规模故障时所有请求一起重算,可能把 GPU 和队列压垮。缓存能丢,不代表丢了没有代价。
6. 什么时候复用才划算
首 token 时间(TTFT)大致由这几段组成:
依次是:排队、查找、关键路径上的加载、剩余部分的 Prefill、生成第一个输出。加载如果是串行的,就是查询、L2 读取、网络、解码和 H2D 相加;如果充分流水,更接近最慢那一段再加上启动和收尾。不能一边把各阶段全部相加,一边又说它们完全重叠。
复用划算的条件是:
即:省下的重算时间,要大于查找时间、没能被隐藏的加载时间,以及对其他请求的干扰之和。
省下多少 Prefill 时间要靠对照测量,不能按命中 token 的比例直接折算:注意力的计算量和长度不是线性关系,剩下的 query 仍要关注旧的 KV,batch 效率和排队也会变化。命中很短、后端往返时间长、或者传输抢了 Decode 的带宽时,重算反而更快。可以用互动盈亏计算器试一试。
7. 自测
- 把例子中的 HKV 改成 32,容量变成多少?答案:每 token 512 KiB,8K 是 4 GiB。
- 为什么“把 KV 存到 SSD 就能无限长上下文”不成立?因为仍要计算、搬运,而且引擎能同时用的工作集有限。
- 为什么 key 完全命中但布局不兼容,仍可能产生错误答案?因为内容身份只说明“是哪份数据”,没有说明“字节怎么解释”。
- 为什么按请求数算的命中率 90%,不一定省掉 90% 的 Prefill?因为每次命中的长度和代价都不同。