KV Cache深度解析:大模型推理显存占用的核心与优化实践
2026/9/8 1:20:04 网站建设 项目流程

你有没有算过这样一笔账:同一段长对话,为什么越聊到后面,GPU 显存越像漏水一样往下掉?很多开发者的第一反应是“模型参数太大”,但真正在推理期间长期占用显存的,往往不是那几十 GB 的模型权重,而是每一层 Transformer 都要维护的 KV Cache。

KV Cache 这个词几乎出现在所有大模型推理框架、API 计费文档和面试题中,但它不是 Redis 那种业务缓存,也不是浏览器缓存。它的本质是 Transformer 在自回归生成时,把已经算过的注意力 Key 和 Value 保留在显存里,避免每个新 token 都重新计算一遍整个上文。这篇文章会从“没有缓存会怎样”讲起,逐步拆解 KV Cache 的存储结构、显存计算公式,再落到 vLLM 等推理引擎如何优化缓存命中率,最后把面试里最高频的几种问法整理成可以直接拿来讲的答题框架。

如果你正准备大模型应用开发、推理优化相关岗位,或者只是好奇为什么大模型回复一个字有时也要等几秒,这篇文章都值得读完。理解 KV Cache 之后,你对 token 计费、上下文窗口、并发量瓶颈的判断会准确很多。

1. 一次大模型推理,到底在做什么

先回到最基础的过程。大模型通常以“下一个 token 预测”的方式工作:你输入一段文本,模型把它切成 token 序列,然后逐个预测下一个 token,把新 token 接到序列末尾,再继续预测。

这个过程叫自回归生成,它决定了三件事:

  • 每一次前向计算都依赖上一步已经生成的 token;
  • 生成长度越长,序列越长,计算量随之增长;
  • 每一步都在对“当前完整序列”做注意力计算。

这里需要先解释什么是注意力。Transformer 模型通过自注意力机制计算序列中每个位置之间的关联。每个 token 都会生成三个向量:Query、Key、Value。Query 可以理解成“我要去查询什么”,Key 是“我凭什么被查到”,Value 是“查到我之后给出什么信息”。

计算过程大致是:用当前 token 的 Query 和所有 token 的 Key 做相似度打分,得到注意力权重,再用权重加权所有 Value,得到当前 token 的上下文表示。

关键点来了:在生成长句子的过程中,新的 token 不断追加。每生成一个 token,新的 Query 需要和序列里所有旧 token 的 Key、Value 做交互。如果没有缓存,模型只能把旧的 Key、Value 重新算一遍。这就像你每次写文章都要把前面所有段落重新抄一遍才能续写下一句,成本显然不可接受。

2. 没有 KV Cache 时,每一步都在做重复劳动

我曾经用最原始的方式理解过这个问题:假设模型已经生成到第 100 个 token,下一步要生成第 101 个 token。模型需要把前 100 个 token 的完整序列重新输入一遍,重新计算所有层的 Key、Value,再让第 101 个 token 的 Query 跟它们做注意力。

看起来只是多算了一个 token,但实际是每一层、每个 token 都要重算。

我们可以做一个粗粒度的算量对比。假设用户输入了一篇 1000 token 的提示词,模型要回复 200 token。

没有 KV Cache 时,生成第 i 个回复 token,需要以 1000 + i 个 token 作为上下文重新计算一遍完整 K 和 V。总键值计算量大约是:

(1000 + 1) + (1000 + 2) + ... + (1000 + 200) = 200000 + 20100 = 220100

也就是大约 22 万次 token 级别的键值计算。而有缓存时,预填充阶段一次性计算 1000 个 token 的 KV,之后每生成一个 token 只需要计算新增那一个 token 的 KV,总计算量大约是:

1000 + 200 = 1200

两相对比,计算量差了接近两个数量级。这不是一个精确的耗时基准,但它能解释一个现象:KV Cache 不是“优化技巧”,而是 Transformer 自回归生成里不可或缺的基础设施。没有它,长文本生成的时间复杂度会从 O(n) 恶化到 O(n²),长对话业务根本无法落地。

这里也要顺势解释一个误区:很多人以为 KV Cache 是某个缓存中间件,其实它是模型推理引擎内部维护的张量缓存,存放的是数值矩阵,不是文本结果。它和 Redis、本地缓存这些概念完全不在一个层次上。

3. KV Cache 到底缓存了什么

KV Cache 不是把整个中间状态都缓存下来,它只缓存每一层自注意力计算出的 Key 矩阵和 Value 矩阵。

为什么只需要缓 Key 和 Value?因为自回归生成时,新增 token 的 Query 是新的,必须现算,而旧 token 的 Key、Value 一旦算出来,后续不会再变化。新 token 的注意力只需要拿着自己的 Query 去和旧 Key 做匹配,再用注意力权重去聚合旧 Value。旧 Key 和旧 Value 是纯被读取方,不需要更新,所以可以被缓存。

为什么不缓存整个隐藏状态?因为隐藏状态经过线性变换后会产生 Q、K、V,其中只有 K 和 V 会参与“历史上下文”的匹配;Q 只代表当前正在生成的位置,是动态的。缓存隐藏在每一层的完整隐藏状态,相当于把下一层还没算的东西也提前留了一份,既浪费显存,也没有必要。真正被反复读取的,只有 K 和 V。

下面这个表格能帮你快速建立概念:

术语通俗理解技术含义
Query当前这个词想查什么由当前 token 生成,决定注意力关注点
Key每个历史词能被怎样检索到由历史 token 生成,旧的不变
Value历史词实际携带的信息由历史 token 生成,旧的不变
KV Cache把历史 Key、Value 全部暂存每层、每个注意力头、每个序列都保存一份
Attention Score当前词和历史词的相关度Query 与 Key 的点积经过 softmax 归一化
Context 表示当前词融合上下文后的结果注意力权重对 Value 的加权求和

再往深一层看,KV Cache 是按“层”存放的。一个模型有多少层 Transformer,每一层就都有自己独立的 K 缓存和 V 缓存。所谓显存里的 KV Cache 占用,是所有层占用之和。

这也是为什么面试官喜欢问“能不能共享 KV Cache 给多个请求”。从机制上说,每个序列的 KV 是独立的,因为上下文内容不同。但工程上如果两个请求有相同的 prompt 前缀,前缀部分的 KV 可以复用,这就是后面要讲的前缀缓存。

4. 带缓存的解码过程:Prefill 和 Decode 分离

工程实现里,推理阶段被分为两个阶段。

PreFill,也叫预填充阶段。在这一阶段,模型一次性读入用户的整个 prompt,计算所有 prompt token 的 Key 和 Value,并存入 KV Cache。这个过程没有输出,也不会让用户感知到具体耗时,但它决定了“第一个 token 从输入到输出的延迟”。很多用户觉得长 prompt 的响应很慢,主要成本之一就在这里。

Decode,也叫解码阶段。这一步逐 token 生成结果。每次只输入最近生成的那一个 token,模型计算它的 K、V,追加到对应的 KV Cache 中,然后预测下一个 token。由于每次只处理一个新 token,解码阶段是串行的,很难通过提高并行度来无限加速。

用代码表达会直观很多。下面是一段简化后的自注意力代码,重点展示缓存如何被读取和追加:

import math import torch import torch.nn as nn import torch.nn.functional as F class KVCacheAttention(nn.Module): def __init__(self, d_model, n_heads): super().__init__() self.d_model = d_model self.n_heads = n_heads self.head_dim = d_model // n_heads self.wq = nn.Linear(d_model, d_model) self.wk = nn.Linear(d_model, d_model) self.wv = nn.Linear(d_model, d_model) self.wo = nn.Linear(d_model, d_model) self.k_cache = None self.v_cache = None def reset_cache(self): self.k_cache = None self.v_cache = None def forward(self, x, use_cache=True): # x 形状: [batch, seq, d_model] batch, seq, _ = x.shape q = self.wq(x).view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2) k = self.wk(x).view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2) v = self.wv(x).view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2) # 如果已经有缓存,就把新的 K/V 追加到历史缓存后面 if use_cache and self.k_cache is not None: k = torch.cat([self.k_cache, k], dim=2) v = torch.cat([self.v_cache, v], dim=2) # 注意力打分:Q 与所有 K 做点积 scores = q @ k.transpose(-2, -1) / math.sqrt(self.head_dim) # 省略因果 mask,实际推理需要屏蔽未来 token attn = F.softmax(scores, dim=-1) out = attn @ v # 用注意力权重聚合所有 V out = out.transpose(1, 2).contiguous().view(batch, seq, -1) out = self.wo(out) if use_cache: self.k_cache = k.detach() self.v_cache = v.detach() return out

这段代码把最核心的逻辑暴露出来了:在解码阶段,输入 x 只有一个新 token,所以 q、k、v 的 seq 长度是 1。但 k 和 v 会通过torch.cat和历史缓存拼起来,变成“已经处理过的完整序列长度”。注意力计算时,新 token 的 Q 会和历史上所有 K 做相似度计算,再聚合所有 V。这就是为什么 KV Cache 能少算但不丢失信息。

实际推理中,每个 Transformer 层都有一个这样的缓存对象。你可以把每一层想象成一道工序,每一步生成时,所有工序都要读自己的缓存、追加自己的缓存。任何一个环节的缓存溢出,整个推理请求都会失败。

5. KV Cache 显存占用怎么算:一道必考计算题

这是面试和实践中都绕不开的问题。已知模型的层数、注意力头数、每个头的维度、数据类型,让你估算 KV Cache 占用。

公式并不复杂。对于一个 token,它在某一层的一个注意力头里,K 是一个长度为head_dim的向量,V 也是一个长度为head_dim的向量。所以 KV Cache 的大小可以写成:

def kv_cache_bytes(layers, heads, head_dim, dtype_size, batch_size, seq_len): # 每个 token 在每个层每个头都需要一份 K 和一份 V per_token_bytes = 2 * layers * heads * head_dim * dtype_size total = per_token_bytes * batch_size * seq_len return total, per_token_bytes # 以常见 7B 级别模型配置为例,具体结构因模型而异 layers = 32 heads = 32 head_dim = 128 dtype_size = 2 # bf16 占 2 字节 total_bytes, per_token = kv_cache_bytes( layers, heads, head_dim, dtype_size, batch_size=1, seq_len=4096 ) print("每个 token 需要约 %.2f KB" % (per_token / 1024)) print("单条 4096 token 序列需要约 %.2f GB" % (total_bytes / 1024 / 1024 / 1024))

使用这组常见配置估算:

  • 一个 token 需要2 × 32 × 32 × 128 × 2 = 524288字节,约 512KB;
  • 单条 4096 token 序列,KV Cache 约 2GB;
  • 如果上下文窗口是 32K,单条序列约 16GB,8 个并发请求就是 128GB。

这不是一个精确的官方数值,不同模型的层数、头数、头维度差异很大,但数量级是真实的。这也是为什么很多长上下文模型在单卡上只能低并发部署,也为什么 GQA、MQA 这类“减少 KV 头”的架构越来越流行。

GQA 的全称是 Grouped Query Attention,意思是多个 Query 头共享一组 Key 和 Value 头。它直接削减了 KV Cache 的存储量,模型只牺牲少量效果,换来了更高的并发上限和更低的显存占用。理解 KV Cache 之后,你才能真正看懂这些架构设计到底在优化什么。

6. 为什么长对话和后缀 token 会耗尽显存

KV Cache 的显存占用是和序列长度线性增长的。对话越长,新 token 持续追加,KV Cache 一路膨胀。当它超过显存剩余空间时,常见的策略有以下几种:

第一种是截断上下文。模型只保留最近 N 个 token 的 KV Cache,更早的内容直接丢出缓存。这个策略实现简单,但代价是模型会“忘记”很早之前的对话内容。很多 chat 应用之所以记不住长对话的开头,本质原因并不是模型能力不够,而是 KV Cache 被裁剪掉了。

第二种是滑动窗口缓存。它只保留一个滑动的窗口,比如最近 2048 个 token。旧 token 的 KV 被抛弃,但窗口内的信息仍然完整。这种方式适合流式对话和摘要类任务。

第三种是缓存落盘和换出。某些推理框架会把不常用的 KV Cache 从显存转移到 CPU 内存或磁盘,需要时再换回。这是工程上的显存换页策略,能提高吞吐但会增加延迟。

这里要插入一个关键判断:KV Cache 不是“越多越好”。缓存越多,显存剩余越少,能同时处理的并发请求就越少。很多服务在压测时遇到CUDA out of memory,不是因为权重放不下,而是因为 KV Cache 把显存吃满了。在容器里部署推理服务时,通过监控序列长度和 KV Cache 占用率来动态限流,比盲目调高并发数更有效。

另外,大多数大模型 API 对长上下文计费,本质也是在为这部分存储和计算买单。token 计费不只是因为你输入了多少文本,更是因为推理引擎要保存这些 token 的 KV Cache,并让它们参与后续所有生成步骤。

7. vLLM 等推理引擎如何优化 KV Cache 命中率

如果你用过 vLLM 这类推理框架,应该听说过 PagedAttention。它把 KV Cache 切分成固定大小的块,像操作系统管理内存页一样管理显存,避免因显存碎片化而浪费空间。简单说,以前你要为一条请求预留一段连续的显存,现在可以在多个不连续的页里存放它的 KV Cache,按需分配,极大提高了显存利用率。

和标题里的热搜词“vllm如何优化大模型的缓存命中率”直接相关的是前缀缓存。在多轮对话、Agent 任务、批量评测场景中,大量请求共享同一个前缀。比如系统提示词、工具说明、机器人设定,它们几乎不会变。如果每个请求都重新计算这一大段前缀的 KV Cache,既慢又浪费。推理引擎可以把相同前缀的 KV Cache 缓存起来,新请求到来时先匹配前缀,命中后直接复用,只需要计算后续差异部分。

这种情况和我们在浏览器缓存、CDN 缓存里说的“命中率”很像。命中前缀缓存,意味着预填充成本大幅降低,首 token 延迟明显缩短。这也是很多调用大模型 API 的团队愿意把系统提示词写得很长、很稳定的原因:提示词稳定,前缀缓存命中率才高,成本和延迟才能降下来。

可以用下面的伪代码理解前缀复用的基本原理:

def match_cached_prefix(prompt_tokens, cache): # cache 是以 token 前缀索引的 KV 缓存 prefix_len = 0 while prefix_len < len(prompt_tokens) and prefix_len in cache: prefix_len += 1 return prefix_len # 能复用的前缀长度

实际框架会比这段复杂得多,但核心思想是一样的。如果前缀匹配成功,模型不需要重新计算前缀部分的 K 和 V,直接把缓存中对应的张量拼接进当前请求即可。这种优化在长文档、长 Agent 对话场景中的收益非常明显。

需要提醒的是,不同框架对缓存复用的边界实现不同,能复用多少还与显存策略、请求调度有关。不要以为只要提示词前缀一样,缓存就一定百分百命中。它受框架配置、上下文窗口、缓存淘汰策略等多重因素影响。

8. 面试高频问题:从内存公式到工程细节

KV Cache 在面试中出现频率极高,尤其是在大模型应用开发和推理优化岗位。这里整理几类常见问法和答题要点。

常见问题核心回答方向加分回答
为什么只需要缓存 K 和 V,不需要缓存 Q?Q 是当前生成 token 的动态查询向量,K/V 是历史上下文静态信息从自注意力计算式出发,新 token 生成时只需用新 Q 和旧 K 匹配
KV Cache 在什么阶段创建?Prefill 阶段初始化,Decode 阶段逐步追加可以把 prefill 理解为“建索引”,decode 是“增量查询”
KV Cache 显存怎么估算?给出完整公式,并带入典型模型算一个数量级能指出 bf16、fp8 等不同数据类型对大小的影响
为什么长上下文推理会变慢?Decode 阶段每一步都要和全部 KV Cache 做注意力计算能解释显存带宽限制:哪怕不需要重算,也要反复读取历史 K/V
KV Cache 能跨请求复用吗?完整序列不能直接复用,但相同前缀可以复用说出 PagedAttention、前缀缓存、系统提示词命中优化
GQA 和 MQA 为什么能降低显存占用?减少参与缓存的注意力头数量能说明它是 KV Cache 优化在模型结构层的体现

第一个提问本质上考察注意力计算公式的理解。自注意力中,当前 token 的 Q 只代表当前位置的查询意图,历史 token 的 K/V 不依赖当前 token 的内容,因此可以在生成上一个 token 时就算好并固定下来。Q 必须随着当前生成位置变化,无法提前缓存。

关于显存计算,面试官通常不希望听到只会背公式,而是希望你理解变量含义。层数越多,K/V 副本越多;注意力头越多,K/V 副本越多;头维度越大,单个向量越长;上下文越长,位置维度越大;并发数越大,相同上下文被复制份数越多。只要你把这个链条讲清楚,公式背不背得完整反而是次要的。

还有一个加分点是可以主动提到:KV Cache 不仅占用显存,还会影响吞吐。生成大量 token 时,每一步都要把所有历史的 K/V 从显存读出来做矩阵乘法,显存带宽会成为瓶颈。这是理解长文本生成延迟的关键,也是很多优化工作(如更小的 KV Cache 数据类型、多头共享)要解决的真正问题。

9. 实际工程落地时的常见误区与注意点

第一,不要把 KV Cache 当成一种业务层面的缓存。有次团队里讨论“要不要把用户常见问题缓存起来减少模型调用”,有人顺手接了一句“那我们就用 KV Cache 实现”。这是一个概念错位。KV Cache 解决的是模型内部重复计算问题,它不改变输入输出逻辑,也不负责把用户请求结果缓存下来。业务层的相似问题缓存应该用 Redis、向量数据库或专门的语义缓存服务。

第二,不要只看模型参数大小来判断显存需求。部署一个 7B 模型,很多人第一反应是“bf16 权重约占 14GB”,但实际压测时,KV Cache 可能比权重更早打满显存。尤其是长上下文和高并发场景,KV Cache 往往成为部署瓶颈。上线前要做的不是只看 Model Card,而是用目标上下文长度和目标并发数做一次显存估算。

第三,要注意 KV Cache 的会话隔离。在多租户场景中,不同用户的会话不能共享 KV Cache,否则会出现数据串扰。这也是生产环境里需要重点测试的部分。如果推理框架支持prefix cache,也要确认它是否包含鉴权隔离,避免一个用户的前缀内容被另一个用户命中。

第四,要设计序列长度上限。很多真实事故都是用户不断发长文本,把显存打满导致整个推理服务崩溃。生产环境建议在网关层设置当前会话的最大 token 数,超出后触发摘要压缩或强制开启新会话。这类策略虽然简单,但能有效避免 KV Cache 无节制增长。

第五,选用合适的数据类型。KV Cache 不一定要用和模型权重相同的精度。很多推理框架支持 fp16、bf16、fp8 甚至 int8 的 KV Cache,通过低精度缓存换取更低显存和更高吞吐。代价是有精度损失,需要针对业务场景做质量评估。不要盲目压低精度,但也不要在所有场景都坚持高精度。

10. 总结与后续学习方向

KV Cache 不是一个孤立的缓存技巧,它是理解大模型推理性能、显存规划、长文本对话成本、并发上限的一条主线。这篇文章从“没有缓存会有多慢”推出 KV Cache 存在的必要性,再拆开它的存储结构、显存计算公式和 Prefill/Decode 两阶段流程,最后落到 vLLM 等框架的缓存复用优化和面试答题框架。核心收获可以归纳成四句话:KV Cache 缓存的是每层自注意力的 K 和 V;它让生成计算量从 O(n²) 降到接近 O(n);它的显存占用直接决定长上下文和高并发能跑多大;前缀缓存优化能显著提升系统提示词稳定场景的命中和成本表现。

下一步建议你在真实模型上做一次小验证:使用常见的深度学习框架加载模型,开启和关闭use_cache分别生成同样长度的文本,观察显存占用和耗时差异。你也可以直接阅读推理框架里关于 PagedAttention 和前缀缓存的实现,对照本文的公式把每一处显存开销对应起来。再往后,GQA、滑动窗口注意力、StreamingLLM、状态空间模型这类话题,都是沿着“如何在保证效果的前提下减少 KV Cache 开销”这个方向延伸的。把这根主线抓住,你再看模型结构和推理框架的优化手段,会清晰很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询