如果你经常调用大模型 API,或者自己部署过开源模型,大概率会遇到几个“习以为常但细想很怪”的现象:同一个模型,回答第一个字时总是等得最久;多轮对话越往后,单次请求的计费越高;同样长度的 prompt,长上下文模型明显更贵。这些现象背后不是计费系统随意定的,而是 Transformer 架构本身决定了一个关键的中间产物:KV Cache。
我的判断是:KV Cache 不是可选的性能优化,而是大模型推理中“算力换显存”的结构性设计。谁理解了 KV Cache,谁就理解了大模型推理的一半。从推理延迟、显存规划、API 计费,到 vLLM 这类框架为什么能大幅提升吞吐,全都围绕这条主线展开。
本文会用通俗方式讲清楚四件事:KV Cache 到底存了什么、为什么没有它不行、token 复用是怎么发生的、以及面试笔试中关于 KV Cache 的高频考点和工程排坑经验。
1. 先搞清楚 token 与大模型推理的两个阶段
在理解 KV Cache 之前,先要统一两个基础概念:token 和自回归生成。
token 可以理解为大模型处理文本的最小单位。它不是严格的“字”,也不是严格的“词”,而是分词器切出来的子词片段。对于中文,一个汉字可能对应一个或多个 token;对于英文,一个单词可能被切成一到两个 token。不同分词器的切分结果差异很大,所以只能说“大概”,不能把所有模型都套同一个比例。
大模型的生成方式是自回归的:一次只预测下一个 token,然后把新预测出的 token 拼到输入末尾,再继续预测。这个过程是严格串行的,第 2 个 token 必须等第 1 个 token 生成完后才能开始。这就是为什么大模型回答越长,等待时间越明显。
在推理流程上,行业里习惯把它分成两个阶段:
- prefill(预填充)阶段:把用户输入的整段 prompt 一次性喂给模型,并行计算每个输入 token 的隐藏状态。这个阶段计算密集,但只需要一次。
- decode(解码)阶段:逐个生成新 token。每步只生成一个 token,然后把它追加到序列末尾,继续预测下一个。这个阶段访存密集,也是 KV Cache 发挥作用的地方。
很多人第一次接触这两个阶段时会有一个疑问:decode 阶段每生成一个新 token,模型凭什么能“记得”前面说过什么?答案是注意力机制。注意力机制需要拿当前 token 的查询向量 Q,去和历史所有 token 的键向量 K、值向量 V 做计算。那么问题就来了:每次生成新 token,历史 token 的 K 和 V 也需要重新算一遍吗?
2. 从注意力机制推导出 KV Cache 的价值
先看 Transformer 中最核心的注意力公式:
Attention(Q, K, V) = softmax(Q * K^T / sqrt(d_k)) * V通俗解释是:当前 token 有一个“查询” Q,它去和所有历史 token 的“键” K 做相似度计算,得到注意力权重,再用权重去加权“值” V。K 代表“这个 token 能被什么内容匹配到”,V 代表“这个 token 实际携带的信息”,Q 代表“我现在想找什么”。
假设现在生成了第 t 个 token。对于第 1 到第 t-1 个历史 token,它们的 K 和 V 在第 t-1 步生成时其实已经算过了。如果第 t 步从头再算一遍所有历史 K 和 V,每一步的代价都会随序列长度增加,整体生成成本会变成立方级增长,这在工程上完全不可接受。
KV Cache 做的就是把历史 token 的 K 和 V 矩阵缓存下来。后续每生成一个新 token,只需要做三件事:
- 计算当前 token 的 Q、K、V。
- 用当前 Q 与“缓存的全部历史 K”做注意力计算。
- 把当前 K、V 追加到缓存中,供下一步使用。
这个过程可以类比为做数学题时用草稿纸:你算完一部分就把中间结果抄下来,后面用到时直接翻草稿,而不是每次从头推导。KV Cache 就是那张草稿纸。
| 对比维度 | 不使用 KV Cache | 使用 KV Cache |
|---|---|---|
| 每步计算量 | 重新计算所有历史 token 的 K/V,随长度平方增长 | 只计算当前 token 的 K/V,随长度线性增长 |
| 显存占用 | 低(不保存中间结果) | 线性增长 |
| 推理速度 | 极慢,无法支撑实际产品 | 快,成为工业界默认方案 |
| 实现复杂度 | 简单但不可用 | 需要管理显存,复杂度高 |
所以 KV Cache 的本质是“空间换时间”:用显存换推理速度。它从诞生之初就不是优化技巧,而是 Transformer 架构在生成场景下的必然选择。
3. KV Cache 到底有多大:显存估算与 token 的线性增长
既然 KV Cache 是拿显存换速度,那它到底吃多少显存?这是面试里非常高频的问题,也是做推理部署时必须算的第一笔账。
KV Cache 的显存占用可以这样粗估:
KV Cache 大小 = 2(K 和 V 两份) × 层数 × 每层 KV head 数 × head 维度 × 序列长度 × 精度字节数以 FP16 精度为例,写一个 Python 脚本方便直接算:
def estimate_kv_cache_size( num_layers: int, num_kv_heads: int, head_dim: int, seq_len: int, dtype_bytes: int = 2, ) -> float: """ 粗略估算单个请求的 KV Cache 显存占用。 num_kv_heads 要按模型实际配置填写,GQA 模型通常小于 Q head 数。 """ bytes_per_token = 2 * num_layers * num_kv_heads * head_dim * dtype_bytes total_bytes = bytes_per_token * seq_len return total_bytes / 1024 / 1024 # 返回 MB # 示例:32 层、8 个 KV head、head_dim=128、FP16 per_token_mb = estimate_kv_cache_size(32, 8, 128, 1) print(f"单个 token 占用约: {per_token_mb * 1024:.2f} KB") for seq_len in [2048, 4096, 8192]: total = estimate_kv_cache_size(32, 8, 128, seq_len) print(f"序列长度 {seq_len}: 单请求 KV Cache 约 {total:.1f} MB")按这个参数估算,每个 token 大约占 128KB,2048 个 token 的请求会产生约 256MB 的 KV Cache,8192 个 token 时会超过 1GB。要注意,这只是单个请求的量。如果是并发 10 个请求,再乘以 10,显存压力立刻体现出来。
实际推理时显存分配远不止 KV Cache,还要加载模型权重、放激活值和临时计算图。以 70B 级别模型为例,单机很难跑起来的原因不仅是权重体积大,KV Cache 在长上下文下的膨胀同样不可忽视。
理解了这一点,就能明白长上下文模型为什么“贵”:上下文窗口拉长后,模型权重可以不变,但 KV Cache 会随长度线性增长,同时注意力计算量也在增长。API 按 token 计费并不是单纯的市场行为,而是计算量和显存成本的直接映射。
这里还有一个容易被忽略的点:KV Cache 只在 decode 阶段逐步增长。prefill 阶段是并行计算 prompt,不会先产生完整缓存;decode 每生成一个新 token,缓存就会变长一点。所以同样的上下文长度,生成的新 token 越多,最终 KV Cache 越大。
4. 前缀缓存与 token 复用:同一份内容为什么不用算两遍
前面讲的都是单次请求内部的 KV Cache。但在真实生产环境中,有大量请求之间存在重复内容,而默认情况下每个新请求都会重新计算整个 prompt 的 KV Cache。
举几个典型场景:
- 多轮对话:用户在第 3 轮修改了最后一个问题,但系统提示词和前两轮对话完全没变。
- RAG 问答:多个用户问同一个知识库文档,文档内容被反复拼进 prompt。
- Agent 应用:系统提示词、工具描述、few-shot 示例基本固定,只有最后的用户指令在变。
如果每次请求都把相同前缀重新计算一遍,等于花钱买重复劳动。前缀缓存(Prefix Caching)就是为了解决这个问题:把公共前缀 token 的 KV Cache 保存下来,下一次请求如果以相同前缀开头,直接复用,跳过这部分 prefill。
举个例子,请求 A 是ABCDEFG,请求 B 是ABCDHIJ。如果按 token 切分后,前 4 个 tokenABCD完全一致,那么这 4 个 token 的 KV Cache 可以被请求 B 直接复用。实际系统中,前缀比较精确到 token 维度,只要前缀 token 序列一致,就有机会命中缓存。
vLLM 等推理框架已经把前缀缓存做成可配置功能。vLLM 较新版本启动时可以通过--enable-prefix-caching开启:
vllm serve meta-llama/Llama-3.1-8B-Instruct \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --enable-prefix-caching这段命令的含义是:启动一个兼容 OpenAI API 的服务,允许模型占用单卡 85% 的显存,最大上下文长度设为 8192,并开启前缀缓存。不同 vLLM 版本参数会有差异,实际使用时以官方文档和vllm serve --help输出为准。
但前缀缓存有几个容易踩的坑。第一,它要求前缀的 token 序列完全一致,哪怕中间差了一个空格、换行或特殊符号,后续前缀就无法完整命中。第二,模型版本、量化方式、推理精度必须一致,否则缓存的 K/V 和当前模型的计算结果可能不匹配。第三,如果提示词里包含了时间戳、随机数这类每次都在变的内容,会影响命中率。
工程上,前缀缓存通常用于共享公共 prompt 前缀的场景,比如固定系统提示词、固定工具描述、固定知识库文档。对于完全随机、毫无公共前缀的请求,前缀缓存很难发挥价值。
5. vLLM 与 PagedAttention:缓存命中率背后的显存管理
前缀缓存解决的是“跨请求复用”,但还有一个更底层的问题:KV Cache 在显存里怎么分配?
最早期的实现做法是:每个请求预先申请一块连续显存,长度按最大可能上下文预留。问题在于不是每个请求都会用满,预留越多浪费越多。而且不同请求的长度参差不齐,频繁分配释放会造成显存碎片,就像磁盘碎片一样,总量看着够用,但放不下新请求。
vLLM 的核心贡献 PagedAttention 借鉴了操作系统虚拟内存分页的思想:不再给每个请求分配一整块连续显存,而是把 KV Cache 划分成固定大小的块(block),按需分配、动态增长。这样请求短就少占块,请求长就多占块,显存利用率大幅提升。
与之一同发挥作用的还有 continuous batching(连续批处理)。传统推理是等一批请求全部结束后再处理下一批,慢请求会拖慢同批快请求。continuous batching 允许边生成边插入新请求,某个请求结束后立即腾出位置给新请求,整体吞吐明显提高。
这些优化和“缓存命中率”的关系在于:当 KV Cache 不再是一整块连续内存,而是按块管理后,前缀命中、块复用、动态调度才有了实现基础。你可以把 vLLM 做的事情理解为三层:
- PagedAttention 解决“KV Cache 怎么放”的问题。
- Prefix Cache 解决“相同内容怎么复用”的问题。
- Continuous Batching 解决“多请求怎么调度”的问题。
对于读者来说,不需要马上读懂 vLLM 源码,但需要建立这个认知:大模型推理优化不是玄学,而是在算力、显存、调度三个维度上做精细化管理。KV Cache 是这些优化共同的抓手。
6. 用 transformers 观察 KV Cache 的最小实验
如果你平时用 Hugging Face transformers 做推理,其实已经接触过 KV Cache 了。model.generate()默认开启 KV Cache,参数叫use_cache。为了直观理解,可以写一个最小实验:
from transformers import AutoModelForCausalLM, AutoTokenizer model_name = "gpt2" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name) inputs = tokenizer("KV Cache is", return_tensors="pt") # 默认 use_cache=True,生成时缓存历史 K/V out_cache_on = model.generate( **inputs, max_new_tokens=10, use_cache=True, ) # 不使用缓存,每步都重复计算历史 K/V out_cache_off = model.generate( **inputs, max_new_tokens=10, use_cache=False, ) print(tokenizer.decode(out_cache_on[0])) print(tokenizer.decode(out_cache_off[0]))这个实验本身不复杂,重点是观察行为差异。开启use_cache=True时,generate()会保存每一步的past_key_values,也就是各层 K/V 缓存;关闭后,每一步重新计算全部历史 token 的注意力。在长序列场景下,两者的耗时差距会非常明显。
如果你想进一步看缓存结构,可以在model.forward()或自定义推理循环里打印past_key_values的形状。它的结构通常是每个层对应一个二元组,分别存储该层的 K 和 V,随生成步数逐层增长。
对新手来说,这个实验最大的价值是建立“缓存是一步步变长”的直觉,而不是把它当成一个黑盒概念。面试里如果被问到“KV Cache 存在哪”,除了说显存,还可以补充一句:在 transformers 这类框架里,它对应past_key_values这一组每层 K/V 张量。
7. 面试笔试高频考点整理
KV Cache 是大模型推理方向最常考的知识点之一。下面整理几个高频问题,每个问题都附上回答要点。
| 面试问题 | 考察点 | 建议回答要点 |
|---|---|---|
| 什么是 KV Cache?为什么需要它? | 基础概念 | Transformer 自回归生成时,每步需要历史 token 的 K/V 做注意力计算;KV Cache 缓存这些中间结果,避免重复计算,用显存换推理速度。 |
| KV Cache 在哪个阶段产生? | prefill 与 decode 理解 | prefill 阶段并行计算输入 token,decode 阶段逐步生成并追加 K/V 到缓存;缓存随生成动态增长。 |
| KV Cache 显存如何估算? | 数量级意识 | 按 2 × 层数 × KV head 数 × head 维度 × 序列长度 × 精度字节数估算;并发再乘以请求数。 |
| 多轮对话为什么越来越慢、越来越贵? | token 复用逻辑 | 每轮都会把历史 token 的 KV Cache 继续保留并追加,序列越长,单步注意力和缓存显存越大。 |
| KV Cache 可以跨请求复用吗? | 前缀缓存 | 可以,但要求前缀 token 完全一致,且模型版本、精度、量化方式匹配;vLLM 等框架提供 prefix caching。 |
| 如何减少 KV Cache 显存? | 优化手段 | 使用 GQA/MQA 减少 KV head 数;使用 KV Cache 量化;使用 PagedAttention 减少碎片;对超长历史做上下文压缩。 |
| KV Cache 能无限缓存吗? | 工程边界 | 不能,显存有上限,超长会 OOM;需要配合并发控制、长度限制和缓存淘汰策略。 |
面试时不要只背结论,最好能结合场景讲。比如问到“为什么 vLLM 吞吐更高”,可以回答:vLLM 用 PagedAttention 把 KV Cache 按块管理,减少显存碎片,并通过 continuous batching 提升 GPU 利用率,再叠加前缀缓存降低重复计算。把一个点讲透,比抛出一堆名词更有说服力。
8. 工程实践中的常见坑与排查方法
KV Cache 在理论上是“空间换时间”,但在真实生产环境里,它带来的问题往往集中在显存、速度和缓存命中率三方面。下面整理几个常见问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 推理服务 OOM | 并发请求多,KV Cache 累积超过显存上限 | 查看显存监控和模型日志中的 OOM 报错 | 降低并发、限制max_model_len、调整gpu-memory-utilization、开启前缀缓存,必要时换更大显存 |
| 多轮对话越聊越慢 | 历史 KV Cache 不断增长,单步 attention 计算量变大 | 观察不同轮次的 decode 时延变化 | 对历史做摘要压缩、滑动窗口裁剪、限制上下文长度,或拆分会话 |
| 输出在生成长文本时中断 | max_new_tokens达到上限,或上下文总长度超出模型窗口 | 检查 API 返回的 finish_reason 和日志 | 合理设置max_tokens,在业务层拆分长文本生成任务 |
| 前缀缓存命中率低 | prompt 前缀包含时间戳、随机参数等变化内容 | 分析线上请求的公共前缀比例 | 固定系统提示词,把易变内容放到后缀;确认模型版本和量化方式一致 |
| 开启缓存后结果异常 | 量化精度或模型版本不一致,缓存了不兼容的 K/V | 对比关闭缓存前后的输出 | 清理旧缓存,确保缓存 key 中包含模型版本、精度等标识 |
| 显存总量够但请求仍排队 | 显存碎片导致无法分配连续大块内存 | 查看可分配内存与实际占用差异 | 使用支持 PagedAttention 的框架,或重启服务整理碎片 |
特别提醒:以上很多操作都涉及生产环境。调整显存参数、清理缓存、更新推理框架之前,先在测试环境验证,做好配置备份和回滚方案。如果是外部服务,优先采用灰度发布。KV Cache 相关的缓存清理比普通业务缓存更敏感,一旦缓存与模型不匹配,轻则命中率下降,重则输出质量异常。
还有一个容易混淆的点:这里讨论的 KV Cache、前缀缓存,与 Web 开发里的 Redis 缓存、浏览器缓存、JWT token 完全是两回事。LLM 的 KV Cache 是模型内部的张量缓存,不能把它当成业务缓存来管理。面试时如果被问到“token 失效”,也要先明确对方问的是 API 鉴权 token 还是模型文本 token,两者名字相似,技术含义完全不同。
9. 最佳实践与后续学习方向
KV Cache 相关优化的本质是在有限的显存里,尽可能减少重复计算。落到实际项目中,有四个层面的做法。
模型层面:优先选择使用 GQA(分组查询注意力)或 MQA(多查询注意力)的模型,它们通过共享 KV head 显著减少 KV Cache 体积。同样是 7B 级别模型,KV head 从 32 降到 8,KV Cache 理论占用能降到原来的四分之一。对推理框架支持 KV Cache 量化的模型,可以进一步降低显存占用。
框架层面:生产环境建议直接使用业界比较成熟的推理框架,比如 vLLM、SGLang、TensorRT-LLM 等。这些框架对 KV Cache 的内存管理和调度做了大量优化,自己从零写推理服务很难达到同等效果。部署前要仔细看官方文档,不同版本参数的兼容性差异很大。
应用层面:尽量固定系统提示词的顺序和内容,让它成为可复用的公共前缀;把时间戳、用户 ID、随机数这类易变内容放到 prompt 后缀;对超长对话历史做摘要或裁剪,而不是无限往上追加。这些改动对缓存命中率的影响,比调框架参数更直接。
监控层面:关注三个指标:KV Cache 利用率、前缀缓存命中率、prefill 与 decode 的平均时延。这些指标能帮你判断瓶颈是在显存、重复计算还是调度策略上。
如果你想继续深入学习,建议按这个路径走:先读透 attention 机制和 transformers 生成源码,理解past_key_values的来龙去脉;再读 PagedAttention 论文,理解分页块管理;最后看 vLLM 的 cache engine 实现,把“缓存命中率”从概念变成可优化的工程指标。
KV Cache 这个知识点,看起来只是 Transformer 推理的一个细节,但它串联起了 token 切分、显存规划、API 计费、推理框架设计这整条链路。下次当你看到“上下文 128K”“每秒输出多少 token”这类宣传时,不妨多问一句:它的 KV Cache 是怎么管理的?只要你能回答清楚这个问题,对大模型推理的理解就已经超过了大多数只看 API 文档的人。建议收藏备用,面试或做架构选型前翻一遍,会有帮助。