KV Cache 是因果语言模型为仍可能被后续 Token 访问的历史位置保留的逐层 Attention 状态。它减少自回归解码时重复计算 Key 和 Value 投影,却同时消耗显存容量、带宽和调度槽位。因此,生产设计必须回答四个问题:保留哪些 Token 状态、何时允许复用、状态放在哪里,以及如何验证正确性与租户隔离。
本文只负责 KV 状态生命周期。大模型推理指南解释完整的 Prefill/Decode 链路,分离式推理服务指南负责跨工作池传输 KV 状态。
直接回答
KV Cache 用持久状态换取更少的重复投影计算。Prefill 为 Prompt Token 创建 K/V;每个 Decode Step 读取保留的 K/V,为最新 Token 计算 Q/K/V,再把新 K/V 追加到缓存。
| 问题 | 生产环境中的正确结论 |
|---|---|
| Cache 会让 Decode 变成常数时间吗? | 不会。它消除历史投影的重复计算,但标准 Attention 仍需读取并关注保留的 K/V。 |
| 最大上下文是否等于已分配缓存? | 不一定。Dynamic、Paged、Sliding Window、Static 和共享前缀布局的物理预留方式不同。 |
| 两个请求可以共享缓存吗? | 只有 Token 前缀及所有影响缓存的输入兼容,并且安全策略允许复用时才可以。 |
| Prefix Cache 会加速生成吗? | 它可以减少重复 Prefill 和 TTFT,不会减少新输出 Token 的 Decode 工作量。 |
| 低精度缓存一定更快吗? | 不一定。量化减少字节数,却会引入转换、Scale 元数据、Kernel 开销和质量风险。 |
KV Cache 如何工作
在因果自注意力中,历史 Token 不能依赖未来 Token。只要模型和输入历史不变,某个历史位置的 Key 与 Value 一旦计算完成,就可以在后续 Decode Step 中复用。
对于位置 t 的新 Token,每个 Attention Layer 都会:
- 计算新 Token 的 Query、Key 和 Value;
- 用新 Query 与保留的 Key 计算相关性;
- 按 Attention Weight 聚合保留的 Value;
- 把新 Key 和 Value 追加到该层缓存。
其逻辑操作是:
K_cache <- concat(K_cache, k_t)
V_cache <- concat(V_cache, v_t)
output_t = softmax(q_t @ K_cache.T / sqrt(head_dim)) @ V_cache
之所以只缓存 K 和 V,是因为未来 Query 会重复使用它们;当前 Query 只服务当前位置,后续位置不会再用。Attention Score 也无法复用,因为每个新 Query 都会产生不同的分数向量。
该解释只适用于因果 Transformer Attention。Encoder-Decoder 模型可能分别维护 Self-Attention 与 Cross-Attention Cache;Sliding Window、Chunked、Latent Attention、循环或混合架构保留的状态不同,需要使用模型专属公式。
Prefill 与 Decode 的缓存行为不同
Prefill 并行创建初始缓存,Decode 则逐步增长或更新缓存。只有先区分这两个阶段,才能判断某项优化会改善哪个延迟指标。
| 阶段 | Cache 操作 | 主要压力 | 关键指标 |
|---|---|---|---|
| Prefill | 为 Prompt Token 写入 K/V | 计算量、Prompt 长度、初始分配 | TTFT、输入 Token 吞吐 |
| Decode | 读取保留 K/V 并追加新状态 | 显存带宽、驻留 Token、Batch 调度 | TPOT、输出 Token 吞吐 |
| 跨请求复用 | 查找兼容前缀 Block,跳过重复 Prefill | 命中率、Block 粒度、身份、淘汰 | 命中/未命中 TTFT、复用 Token 数 |
| Offload 或传输 | 在内存层级或 Worker 之间移动 Block | 链路带宽、排队、局部性 | 传输延迟、等待时间 |
Hugging Face 的 Caching 文档解释了逐层追加行为,并明确缓存只应用于推理而非训练。训练会更新权重并使用不同执行语义,不能把推理缓存当作训练捷径。
按驻留 Token 计算 KV Cache 显存
对基础的未压缩 Decoder-only Cache,先计算每个保留 Token 的字节数:
bytes_per_token =
2
* n_layers
* n_kv_heads
* head_dim
* bytes_per_element
logical_cache_bytes =
bytes_per_token
* resident_token_slots
因子 2 分别代表 Key 和 Value。n_kv_heads 是 KV Head 数量,不一定等于 Query Head。若活跃序列保留长度不同:
resident_token_slots = sum(每条活跃序列的保留 Token 数)
这只是逻辑容量,不是 GPU 上的物理分配。还需加入或实测:
- Static Cache 预留或分页取整;
- Block Table、Scale、元数据和分配器开销;
- 重复前缀是物理共享还是各自保存;
- 推测解码或 Beam Search 分支;
- Tensor/Pipeline Parallel 的切分与复制;
- 临时 Attention Workspace 和激活;
- Cross-Attention、Sliding Window 或 Latent State 等模型专属状态。
模型标称上下文窗口只是上限。只有当前布局确实按上限预分配时,它才等于物理 Cache 预留;Dynamic 和 Paged Engine 可更接近实际保留 Token 分配,Static Cache 可能预留更多,Sliding Window Layer 则在训练窗口之外停止保留旧 Token。
运行容量估算
下面的无依赖脚本按活跃序列长度估算逻辑 Cache。它故意不隐藏量化元数据和 Page Overhead,避免给容量规划制造虚假精度。
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
GIB = 1024 ** 3
@dataclass(frozen=True)
class KVCacheSpec:
n_layers: int
n_kv_heads: int
head_dim: int
bytes_per_element: int
def bytes_per_token(self) -> int:
values = (
self.n_layers,
self.n_kv_heads,
self.head_dim,
self.bytes_per_element,
)
if any(value <= 0 for value in values):
raise ValueError("KV cache dimensions must be positive")
return (
2
* self.n_layers
* self.n_kv_heads
* self.head_dim
* self.bytes_per_element
)
def logical_cache_gib(
spec: KVCacheSpec,
retained_sequence_lengths: Iterable[int],
) -> float:
lengths = tuple(retained_sequence_lengths)
if not lengths or any(length < 0 for length in lengths):
raise ValueError("Provide one or more non-negative sequence lengths")
resident_tokens = sum(lengths)
return spec.bytes_per_token() * resident_tokens / GIB
active_sequences = [4096, 2048]
mha = KVCacheSpec(
n_layers=32,
n_kv_heads=32,
head_dim=128,
bytes_per_element=2,
)
gqa = KVCacheSpec(
n_layers=32,
n_kv_heads=8,
head_dim=128,
bytes_per_element=2,
)
try:
print(f"MHA logical cache: {logical_cache_gib(mha, active_sequences):.3f} GiB")
print(f"GQA logical cache: {logical_cache_gib(gqa, active_sequences):.3f} GiB")
except ValueError as error:
raise SystemExit(f"Invalid capacity input: {error}") from error
# MHA logical cache: 3.000 GiB
# GQA logical cache: 0.750 GiB
四倍差异只来自给定参数下 KV Head 从 32 个变为 8 个。这不意味着任意 MHA Checkpoint 都能在运行时直接切换为 GQA。
把 Cache Identity 当作正确性契约
只有产生某个 Block 的计算完全兼容时,缓存才可复用。Token 相同只是必要条件,生产身份通常还包括文本之外的信息。
{
"modelId": "provider/model-name",
"modelRevision": "immutable-revision",
"tokenizerRevision": "immutable-revision",
"adapterId": "none",
"chatTemplateVersion": "chat-v3",
"attentionConfigHash": "sha256:...",
"kvDtype": "bf16",
"multimodalInputHashes": [],
"trustScope": "tenant-acme",
"cacheSaltId": "salt-ref-42"
}
Cache Identity 至少覆盖所有可能改变 K/V 的输入:
- 模型权重与不可变 Revision;
- Tokenizer、特殊 Token 与 Chat Template;
- LoRA 或其他 Adapter Identity;
- 位置/Attention 配置与 Cache Dtype;
- 多模态 Embedding 或输入 Hash;
- 缓存布局不兼容时的 Engine Identity;
- Tenant 或 Trust Group 隔离策略。
vLLM 的 Prefix Cache 设计会 Hash Block Token、父前缀身份,以及 LoRA ID、多模态输入 Hash 等额外值。V1 设计还支持 Per-request Cache Salt,用于隔离 Trust Group 并降低基于命中延迟的信息泄露。这是 vLLM 的具体能力,不能替代业务授权和数据留存策略。
按真实权衡比较 Cache 策略
不同 Cache Strategy 作用于不同维度,也可以组合。只说“开启 KV Cache”不足以决定内存布局。
| 策略 | 改变什么 | 优势 | 成本或边界 |
|---|---|---|---|
| Dynamic Cache | 随 Token 增长保留状态 | 避免直接预留完整最大长度 | 动态 Shape 可能限制编译,分配仍需 Headroom |
| Static Cache | 预分配固定最大 Shape | 支持的 Runtime 可获得稳定 Shape 与编译收益 | Mask 空槽并浪费显存或 Attention 工作 |
| Paged Cache | 把逻辑 Token Block 映射到非连续物理 Block | 减少预留与碎片,便于 Block 共享 | 引入 Block Table、调度策略、分页取整和专用 Kernel |
| Sliding/Chunked Cache | 只保留模型定义的 Window 或 Chunk | 为使用该 Attention Pattern 训练的 Layer 限制状态 | 不能无损改装到 Full-Attention 模型 |
| Prefix Cache | 跨请求复用兼容的完整前缀 Block | 跳过重复 Prefill,并可能物理共享 Block | 需要重复前缀、身份检查、淘汰和隔离 |
| Offloaded Cache | 把状态移动到 CPU 或其他内存层级 | 在 GPU 压力下保留更多状态 | 传输延迟与链路竞争可能抵消收益 |
| Quantized Cache | 用更低精度保存 K/V | 减少字节并容纳更多 Token | 转换开销、元数据、Kernel 支持和质量风险 |
Hugging Face 当前的 Cache Strategy 文档区分 DynamicCache、StaticCache、QuantizedCache 和 Offloaded Variant。文档明确指出:Static Cache 是编译与空槽浪费的权衡;Offload 是显存与传输工作的权衡;在短上下文且显存充足时,量化甚至可能降低速度。
PagedAttention 能证明什么
PagedAttention 改变的是 KV 物理内存管理,而不是模型 Attention 语义。它把一条序列的逻辑 Cache 划分为 Block,按需分配并映射到非连续物理内存。
这形成了类似内存管理器的生命周期:
- 为通过准入的请求预留 Block Capacity;
- Prefill 和 Decode 创建 Token State 时分配 Block;
- Identity 匹配时共享只读前缀 Block;
- 分支发生分歧时复制或新分配 Block;
- 请求结束后释放或标记 Block 可复用;
- 显存压力下淘汰无引用的可复用 Block。
原始 PagedAttention 论文在其模型与负载中,相对 FasterTransformer 和 Orca 报告了相近延迟下 2–4 倍吞吐。这能支持分页设计,却不是对当前引擎、硬件、序列分布和解码策略的通用倍率。
Paging 也不会消除全部浪费:最后一个 Block 可能只填一部分,元数据与 Indirection 仍存在,Block Size 会影响 Kernel 与碎片,Scheduler 也仍可能过度承诺 Token Capacity。
Prefix Cache 优化 Prefill,而不是 Decode
Prefix Cache 在请求之间复用兼容的 Token 前缀 K/V。收益由复用 Token 数、命中率、查找与传输成本,以及 Block 被淘汰前的存活时间共同决定。
vLLM 的 Automatic Prefix Caching 指南把重复查询长文档和多轮对话列为适合负载,同时明确指出核心边界:Prefix Cache 减少的是 Query Prefill,不会缩短新输出 Token 的 Decode 时间。
至少监控:
reused_token_ratio = reused_prefix_tokens / eligible_prefix_tokens
hit_rate = requests_with_reuse / eligible_requests
单看 Request Hit Rate 会误导决策。复用 16 个 Token 和复用 16,000 个 Token 都算一次命中,但省下的 Prefill 工作完全不同。
云服务商的 Prompt Caching 应被视为产品契约。它可能公开 Token 阈值、TTL、折扣或自动匹配,却未承诺某种内部 KV 实现。不能仅凭功能名称推断 Engine Layout、跨请求留存时长或安全边界。
量化与 Offload 必须分别评测
KV Quantization 和 Cache Offload 都是在不同瓶颈之间交换成本,不是自动生效的低显存模式。
KIVI 论文观察到 Key 与 Value 的分布不同,并提出非对称 2-bit 量化:Key 按 Channel,Value 按 Token。论文中的显存、Batch、吞吐和质量结果只属于其模型与实现;更换 Dtype、Kernel、Residual Cache Length 或模型后都要重新测量。
量化需要同时比较:
- 逻辑与物理 Cache Byte;
- Quantize/Dequantize Kernel 时间;
- 不同上下文长度下的 TTFT、TPOT 和吞吐;
- 任务质量与确定性回归切片;
- Scale、Residual Cache、Padding 和元数据开销。
Offload 需要比较:
- 每请求和每输出 Token 的传输字节;
- Host-to-device 与 Device-to-host Queue Time;
- 不同内存层级的命中延迟;
- 并发 Prefill/Decode 下的链路利用率;
- 传输慢于重算时的 Recompute Cost。
TensorRT-LLM 的 KV Cache Reuse 文档把 Host Offload 用于延长可复用 Block 的保留时间,同时明确传输有成本,较旧互连未必获益。该结论依赖硬件和负载。
GQA 与 MQA 是模型架构选择
Grouped-Query Attention 与 Multi-Query Attention 从源头减少 n_kv_heads,从而降低 Cache Byte 和带宽。它们通常是训练后 Checkpoint 的属性,不是无需修改模型就能打开的 Serving Flag。
- MHA 为每个 Query Head 使用一个 KV Head;
- MQA 让所有 Query Head 共享一个 KV Head;
- GQA 在两者之间,让多组 Query 共享较少的 KV Head。
MQA 论文通过共享 Key 与 Value 降低增量 Decode 的带宽需求;GQA 论文把 GQA 定义为质量与效率之间的中间方案,并评测了 Uptraining 方法。容量估算必须读取 Checkpoint 的真实配置,不能从参数量或模型家族名猜 Head 数量。
容量规划必须使用负载分布
Admission Control 应按驻留 Token Demand 预留,而不是按一个醒目的请求数。10 个短请求与 10 个最大上下文请求并不等价,连续批处理还会在每个调度迭代改变活跃集合。
可执行的容量流程是:
- 收集 Prompt、Output、Retained Context 和并发分布;
- 从准确 Checkpoint 与 Cache Dtype 计算 Bytes per Token;
- 建模 Page Rounding、共享前缀、分支与安全余量;
- 从 GPU 容量扣除权重、激活、Workspace 和非 Cache 运行时内存;
- 推导以 Token Capacity 为核心的准入限制;
- 用真实到达突发回放请求分布;
- 在 SLO 边界验证 OOM、Preemption 和延迟。
输入与输出 Token 都要限制。输出上限只约束未来增长;超长 Prompt 在准入时已经消耗 Prefill 计算和 Cache 容量。多轮会话还要明确旧轮次是继续驻留、重新计算、摘要还是淘汰。
用质量与 SLO 门禁评测 Cache 变更
Cache 实验必须固定 Checkpoint、Engine Version、硬件拓扑、请求 Trace 与解码配置,否则吞吐差异无法归因于 Cache Strategy。
| 维度 | 指标 | 必须切片 |
|---|---|---|
| 容量 | 物理 Cache Byte、驻留 Token、Block 利用率、Headroom | 上下文长度、并发、Cache Dtype |
| Prefill | TTFT、输入 Token 吞吐、复用 Token 比例 | 命中/未命中、前缀长度、Tenant |
| Decode | TPOT、输出 Token 吞吐、显存带宽 | 保留长度、Batch Occupancy、输出长度 |
| Scheduler | Queue Time、准入拒绝、Preemption、Recompute | 突发强度、优先级 |
| 生命周期 | Allocation、Free、Eviction、Offload Transfer | Cache Tier、Block Age、模型 Revision |
| 正确性 | Output/Logit 回归、任务质量、Mismatch Error | Dtype、模型、语言、长上下文 |
应报告 Percentile,而不是只看平均值。Prefix Cache Hit Rate 很高时,若长前缀持续被淘汰或远端 Block 到达过慢,P95 TTFT 仍可能很差。
失败模式与安全边界
KV State 由用户输入派生,可能持续保留敏感信息。Cache Memory、Hash、Metadata、传输路径和 Timing Behavior 都应按受保护数据处理。
| 失败 | 原因 | 控制 |
|---|---|---|
| 复用后输出错误 | 模型、Tokenizer、Adapter、Template、模态、位置或 Dtype 不兼容 | 版本化 Cache Identity,拒绝不兼容 Block |
| 跨租户泄露 | Prefix Block 跨 Trust Boundary 复用 | Tenant-scoped Key/Salt、授权、内存清理和留存上限 |
| Timing Side Channel | 命中/未命中延迟暴露某个前缀是否存在 | 隔离策略、引擎支持时使用 Cache Salt、限制明细指标暴露 |
| OOM 或反复 Preemption | 突发长度下 Token Capacity 过度承诺 | Token-based Admission、Headroom、输入输出上限 |
| 命中率高但 SLO 无收益 | 前缀很短或 Decode 占主导 | 按复用 Token 加权,并拆分命中/未命中延迟 |
| Offload 卡顿 | CPU/GPU 或网络链路饱和 | Tier Budget、传输指标、局部性感知调度 |
| 量化质量回退 | Dtype 不支持、Scale 不合适或 Layer 敏感 | 准确 Engine 支持检查与质量门禁 |
| 陈旧状态 | 部署或 Adapter 更新后未失效 | 按不可变 Revision 划分 Namespace,并 Drain 旧代次 |
不要为了调试命中率就记录原始 Prompt、Token ID、Salt 或 Cache 内容。应使用不透明 ID、聚合指标、访问控制和有界留存。
生产决策清单
启用或修改 KV Cache Strategy 前:
- 确认准确的模型、Tokenizer、Adapter、Engine 与 Cache Dtype;
- 按 KV Head 和 Retained Token 分布计算 Bytes per Token;
- 为非 Cache VRAM 与碎片预留 Headroom;
- 分开测量 Prefill、Decode、Reuse 与 Transfer;
- 定义 Allocate、Fork、Free、Evict、Invalidate 和 Rollout 行为;
- 按 Tenant 或明确 Trust Group 隔离 Cache Reuse;
- 对量化、淘汰或压缩执行 Output Quality Regression;
- Canary 新配置,并可回滚到上一代 Cache。
来源与相关内容
一手技术来源:
- Hugging Face:Caching — 逐层 K/V 复用与 Cache Update 语义。
- Hugging Face:Cache Strategies — Dynamic、Static、Offloaded 与 Quantized Cache 的权衡。
- PagedAttention 论文 — 分页 KV 管理及受实验负载约束的吞吐结果。
- vLLM Prefix Cache 设计 — Block Identity、Eviction、LoRA/多模态身份与 Cache Salt。
- vLLM Automatic Prefix Caching — 适用负载与只优化 Prefill 的边界。
- KIVI 论文 — 给定实验条件下的非对称 KV Quantization。
- TensorRT-LLM KV Cache Reuse — Block Reuse、Eviction 与 Host Offload 边界。
本站主题边界:
- 大模型推理 — 从请求到 Token 的完整执行链路。
- 分离式 LLM 推理服务 — Prefill 与 Decode Pool 之间的 KV Transfer。
- Transformer 架构 — Attention、Mask 与模型家族。
- 连续批处理 — 活跃序列的 Scheduler 行为。
- 量化 — 模型与状态的低精度权衡。