ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

KV Cache:决定大模型推理速度与长上下文成本的核心缓存

KV Cache:决定大模型推理速度与长上下文成本的核心缓存

如果你用过大模型应用,一定有过这种体验:前几句问答快到飞起,聊到二三十轮之后,每个字都要等一秒钟,有时候干脆卡住报错。如果你自己部署过开源模型做服务,感受会更直接——上下文长度稍微拉高一点,显存占用就像被偷走一样快速上涨,批量稍大一点直接 OOM。大部分人会把问题归因于“模型太大”或者“服务器太差”,但真正的原因往往只有一个:KV Cache。KV Cache 是大模型推理性能的核心变量,也是长上下文成本最容易被低估的部分。这篇文章我从头拆一遍:它到底缓存了什么,为什么对话越长推理越慢,以及“长上下文真正的成本”到底花在了哪里。我尽量不写教科书,直接按工程师视角讲清楚原理、公式和避坑经验,适合正在做大模型推理服务、部署开源模型、或者被长上下文性能折磨过的人。

1. KV Cache 到底缓存了什么

1.1 自回归生成:每生成一个字都要“重读”一遍上文

先理清大模型生成文本的底层逻辑。目前主流大模型基本都基于 Transformer 解码器结构,生成方式叫自回归:给定一段 prompt,先算出一个 token,把它拼到输入末尾,再拿去算下一个 token,以此类推。整个过程像滚雪球,每多生成一个 token,输入序列就变长一格。

这里的关键是:每生成一个新 token,模型都需要重新执行一次完整的前向计算。如果不做任何缓存,这次前向会把输入序列里所有 token 的 Key 和 Value 重新算一遍——我说的是“所有”。你第一次生成时输入里有 10 个 token,那要算 10 个位置的 K/V;等到第 100 个 token 的时候,输入里有 109 个 token,还要把前面那 100 个位置的 K/V 全部重新算一遍。模型不会自己记得之前算过什么。

做过性能分析的人都能一眼看出问题:这是一笔随序列长度线性增长、但每次生成都要重复支付的账单。比如一个 7B 规模的模型有好几十层 Transformer 层,每一层都要算 K/V。上下文长度是 1000 的时候,每生成一个 token 等于重复计算 1000 个位置在几十层里的 K/V;上下文长度是 10000 的时候,重复计算量变成 10000 个位置。这显然不可持续。

所以工程上必须做一件看起来理所当然的事:把历史位置已经算好的 K/V 存下来,下次直接用。这就是 KV Cache 最朴素的动机。

1.2 KV Cache 的本质:把历史计算的中间结果存下来

要理解 KV Cache,得先看注意力机制里三个向量的分工。Transformer 的注意力公式是 Attention = softmax(Q·Kᵀ)·V。其中 K 是 Key,代表“我是什么内容,欢迎大家来查找”;V 是 Value,代表“我实际携带的信息是什么”;Q 是 Query,代表“我现在想找什么”。

生成第 N 个 token 的时候,当前这个位置的 Q 是全新的,它要跟历史上所有位置的 K 做相似度匹配,再按匹配权重去加权所有位置的 V。注意一个细节:历史位置的 K/V 是在过去已经算好的,它们只依赖于历史输入,不依赖于当前这个新 token 的 Q。也就是说,K/V 不会因为来了一个新的 Q 而改变。

这意味着我们可以把历史上所有位置的 K/V 缓存起来。生成下一个 token 时,不需要重新计算历史位置的 K/V,只需要计算当前这个新 token 的 K/V,拼到缓存末尾,然后用当前的 Q 去跟完整的缓存做注意力计算。

所以 KV Cache 不是某种调优选项,它是 Transformer 解码器推理时绕不开的中间结果缓存。它在每一层都存在一份,格式通常是 [batch_size, num_heads, seq_len, head_dim]。有些模型还分 K 缓存和 V 缓存,所以实际占用要再乘 2。如果少了这一步,推理服务的吞吐量会直接崩溃,长上下文推理基本无法落地。

1.3 一个例子算算 KV Cache 的体量

光说“缓存”没有体感,我直接给一个可复现的计算过程。

假设一个常见的 7B 规模开源模型,层数是 32 层,采用了分组查询注意力(GQA),KV 头数设为 8,每个头的维度是 128。注意力计算时通常用半精度(FP16),每个元素占 2 字节。

单个 token 在单层里的 K/V 占用:

  • K 缓存:8 个头 × 128 维度 = 1024 个元素
  • V 缓存:同上 = 1024 个元素
  • 两者合计 2048 个元素
  • 单层、单 token 占用:2048 × 2 字节 = 4096 字节 = 4KB

再乘以 32 层:

  • 单 token 全模型 KV Cache = 4KB × 32 = 128KB

注意前面没有乘 batch。如果 batch_size 是 1 还好说,但这只是 1 个 token 的占用。上下文长度 4096 时:

  • 4096 × 128KB = 524288KB = 512MB

如果 batch_size 是 8:

  • 512MB × 8 = 4GB

这还只是 7B 模型。如果模型层数更多、KV 头更多,或者上下文拉到 32K、128K,KV Cache 会膨胀到比模型权重本身还大。这就是为什么长上下文从来不是免费的:它不增加“智力”,但实打实地增加显存和带宽压力。

2. 为什么 AI 越聊越慢:瓶颈不在计算,在搬运

2.1 解码的两个阶段:prefill 和 decode

推理过程可以分成两个性质完全不同的阶段。第一个阶段叫 prefill(预填充),也就是用户把 prompt 发过来,模型一次性处理整个 prompt,同时生成第一个输出 token。这个阶段的特点是输入序列长、可以高度并行,GPU 的计算单元能充分跑起来,所以通常很快。

第二个阶段叫 decode(解码),从第一个输出 token 之后开始,每次只生成一个 token。生成完了拼回去,下次再生成下一个。这个阶段的特点是每次前向只新增一个 token,计算量本身很小,但每一层都要基于完整序列长度做注意力计算。

这两个阶段的关键差异在于:prefill 阶段是“一次性扫描全部输入”,decode 阶段是“每次都要扫描全部已有序列”。对 KV Cache 而言,prefill 是在构建缓存,decode 是在反复读取缓存。

我见过很多人只在 prefill 上做优化,结果长上下文场景还是很慢,原因就是 decode 阶段被忽略。实际上 decode 占据了生成过程中绝大部分时间,因为一次生成几百个 token,就要执行几百次 decode 前向。

2.2 decode 是访存密集型:慢在带宽

为什么 decode 阶段会越来越慢?核心原因是访存带宽,而不是计算量。

继续用上面的例子。一个 7B GQA 模型,单 token 的 KV Cache 是 128KB。当上下文长度是 10000 时,全部 KV Cache 大约是 128KB × 10000 = 1.28GB。现在每生成一个 token,理论上前向计算需要把这份 1.28GB 的 KV Cache 从显存里读一遍,参与注意力计算。

一个主流数据中心 GPU 的显存带宽大约是每秒 2TB 到 3TB 级别。只读 KV Cache 这一项,1.28GB 就要花约 0.5 到 0.7 毫秒。上下文越长,这个时间越长。到 30K 上下文时,KV Cache 约 3.84GB,每生成一个 token 光读 KV Cache 就要 1 到 2 毫秒。再加上模型权重读取、激活值计算、通信开销,每个 token 延迟轻松超过 50 毫秒,这对交互式应用来说已经慢得明显了。

这也能解释为什么有时候模型计算量没变,但速度掉得厉害。decode 阶段每生成一个 token,计算量大概只增加一点点(因为多一个位置参与计算),但 KV Cache 的读取量随上下文长度线性增长。本质上这不是“算不过来”,而是“搬不过来”。GPU 算力再强,也得先把数据从显存搬到计算单元里。

2.3 为什么每个 token 都要读全部 KV Cache

有人会问:能不能只读最近几个 token 的 KV Cache?很遗憾,注意力机制决定了不能这样偷懒。当前 token 的 Q 要和历史所有位置的 K 计算相似度,再对所有位置的 V 做加权求和。换句话说,新生成的一个 token 要“看”一眼所有历史 token,历史全部位置的信息都要参与。

这不是习惯问题,是注意力公式决定的。Q 和每个历史 K 的相似度都可能影响当前生成的内容。如果你只保留最近的 K/V,模型就会“失忆”,丢失早期信息。

FlashAttention 等技术能优化注意力计算的访存模式,减少中间结果的反复读写,但 decode 阶段至少要把 KV Cache 从头到尾读一遍才能算出注意力权重。所以 FlashAttention 能减少很多额外访存开销,但无法消除 KV Cache 带来的线性读取成本。这也是长上下文场景下,KV Cache 优化比单纯优化 attention 计算更重要的原因。

3. 长上下文真正的成本:显存、带宽与工程复杂度

3.1 显存:批量、序列长度与并发一起挤压容量

聊到上下文成本,很多人的第一反应是“上下文越长,模型理解能力越强”。但工程上看,上下文长度直接换算成显存占用。之前算的是单请求,单 batch,单条对话。真实服务的复杂之处在于多个用户、多个请求同时跑。

假设部署一个 7B GQA 模型,单 token KV Cache 128KB,单条上下文 4096,那么单请求 KV Cache = 128KB × 4096 = 512MB。如果同时并发 16 个请求,光 KV Cache 就是 8GB。这个量级已经跟模型权重(FP16 约 14GB)相当。如果并发 32 个,KV Cache 冲到 16GB,比权重还大。这种情况下,显存分配策略稍有问题,OOM 几乎是必然的。

长上下文服务为什么贵?因为单请求的 KV Cache 不只是由实际用到的长度决定,还受系统为“最大可能长度”预留的显存影响。很多框架在初始化时会按 max_model_len 一次性分配 KV Cache 缓存池。也就是说,你设置了 32K 上下文,哪怕实际请求只有 1K token,系统也会为潜在的 32K 预留显存,其他请求能用的空间就变少了。这是一个非常隐蔽的成本:长上下文是“提前支出”的,不是“用多少付多少”。

3.2 多轮对话的隐性成本:KV Cache 只会增长,不会释放

多轮对话场景下,KV Cache 的累积效应非常明显。每轮用户输入和模型输出,都会产生新的 token,这些 token 的 K/V 会不断追加到缓存里。用户可以怀疑自己记不住前面聊过什么,但模型服务端必须把每一轮的内容都留着,否则下一轮注意力计算就没有依据。

于是你会看到这种曲线:对话刚开始时,显存占用非常低,每轮增长很小;对话进行到中段,每轮新增的内容在累积,单次请求的 KV Cache 越来越大,生成延迟越来越高;到了接近长度上限的时候,服务端要么自动截断历史,要么直接报错。用户端的体感就是“越聊越慢,然后突然断了”。

这里还有个容易被忽视的细节:系统提示词和工具定义也会占用 KV Cache。很多应用在每次请求里都会拼上一大段 system prompt,这段内容不会变,但每次请求都要重新计算并缓存一遍。对话轮次越多,这部分固定开销被摊薄得越少,但如果你开了长上下文窗口,这些“固定内容”同样会占掉大量缓存空间。

3.3 对比计算成本与内存成本的权衡

说到这里,可以正式回答“长上下文真正的成本在哪”这个问题。它不在模型的参数量,也不在推理的计算量,而在 KV Cache 带来的显存占用和带宽消耗。

先看计算成本。没加 KV Cache 时,每生成一个 token 都要重新计算全部历史 token 的 K/V,计算量随上下文长度线性增长,而且增长的是重复计算。加了 KV Cache 后,计算成本降下来了,换来的代价是内存成本上升。而且 decode 阶段每生成一个 token,都要把整个 KV Cache 读一遍,这部分访问时间会直接变成延迟。

所以长上下文问题本质上是一个“算力换内存”的权衡游戏。上下文越长,KV Cache 越大,读取越慢,单个 token 延迟越高;同时显存被缓存占掉,能并发的请求变少,整体吞吐量下降。真正的成本是:每增加一个 token,你都要在显存里多放一份向量,并且在以后生成每个 token 时把它读一遍。理解这一点,后续所有优化手段就有了明确的靶子。

4. 优化 KV Cache 的实际手段

4.1 模型结构层面:GQA/MQA、头大小

最根本的优化思路是让 KV Cache 本身变小。模型结构里有一个直接影响 KV Cache 大小的参数:KV 头的数量。

传统的多头注意力(MHA)里,Query、Key、Value 的头数是完全一致的。假设模型有 32 个头,那 KV Cache 就要存 32 份 K 和 32 份 V。分组查询注意力(GQA)的做法是让 Query 仍然保留 32 个头,但 Key 和 Value 只保留少量几个头,比如 8 个,Query 分组共享这些 KV 头。这样 KV Cache 直接降到原来的四分之一,而效果损失很小。MQA(多查询注意力)更激进,KV 头只留 1 个,KV Cache 更小,但效果损失也更大。

所以如果你在选模型或自己预训练模型,尽量选 GQA 结构,尤其是 KV 头数不超过 8 的。很多开源模型 7B 级别已经默认用 GQA,这是它们能支持长上下文的关键原因之一。微调阶段也可以参考这一点,不要为了省事直接用 MHA 大改模型,KV Cache 会教你做人。

4.2 KV Cache 量化:INT8/FP8/4-bit

如果不方便改结构,那就对 KV Cache 本身下手。KV Cache 里的 Key 和 Value 本质上是一堆浮点向量,默认是 FP16 甚至 FP32。对大多数场景来说,把精度降低到 INT8 或 FP8,对生成质量的影响都很小。

量化之后,每个元素从 2 字节变成 1 字节,KV Cache 占用直接减半。4-bit 量化能进一步减到四分之一,但需要处理缩放系数,部署复杂度更高。实际操作中我一般从 INT8 开始,跑一遍测试集看效果损失,再决定要不要上 FP8 或 4-bit。量化 KV Cache 不仅能省显存,还能减少带宽压力。因为 decode 阶段读取 KV Cache 的数据量也同比减少了,延迟跟着下降。

需要注意,KV Cache 量化不是模型权重量化那种永久性的。有的框架支持在线量化,比如 vLLM 里设置 kv_cache_dtype。但量化粒度很关键,按 token 或按 channel 做缩放的效果差异不小。如果量化后出现明显的回答质量波动,先检查是不是缩放因子算错了,或某些层对精度异常敏感。

4.3 PagedAttention 与显存管理

模型结构和量化帮我们减少了 KV Cache 的体量,但真正想让服务稳定跑起来,还需要管理好显存的分配。传统做法是预分配一大块连续显存给 KV Cache,但不同请求的长度差异很大,连续分配的碎片化问题严重,显存利用率很低。

PagedAttention 的思路很有意思,它把 KV Cache 切分成固定大小的块,按需分配,很像操作系统里的分页。每个请求的 KV Cache 不一定在显存里连续存放,而是通过块表映射到物理位置。这样做的好处有三个:一是显存利用率大幅提升,小块按需分配,不再为大长度预留全部空间;二是碎片化减少,回收和重新分配都更容易;三是请求之间可以共享 KV Cache,比如多条请求开头有相同的前缀时,直接复用同一块缓存。

如果你用过 vLLM 或类似框架,会发现它支持配置 block_size、max_num_seqs、max_model_len、gpu_memory_utilization 等参数。这些参数不是随便填的。block_size 小,内部碎片少,但管理开销大;block_size 大,管理简单,但容易浪费。一般短请求多的场景用小 block_size,长上下文多的场景用大 block_size。gpu_memory_utilization 建议设置在 0.85 到 0.9 之间,留出部分显存给模型权重和激活值,否则可能出现“缓存池太大,权重没地方放”的尴尬。

4.4 Prefix Caching:让相同开头直接复用

还有一种优化叫 Prefix Caching,专门针对“重复内容”问题。很多应用请求会带固定前缀,比如系统提示词、角色设定、工具描述。这部分内容每次都是一样的,理论上不需要重新计算和缓存。

vLLM 的 Prefix Caching 建立在 PagedAttention 的分块机制上,会把相同前缀的 KV 块识别出来,让多个请求共享。实现上需要给每个 KV 块计算哈希,哈希相同就复用。切换到不同请求时如果命中缓存,prefill 阶段的计算量大幅减少,首 token 延迟也随之下降。

实际操作中,Prefix Caching 对多用户场景效果非常明显。我有一个线上服务,每天固定前缀占了完整请求的一半以上。开了 Prefix Caching 之后,prefill 耗时降了 60% 以上,而且显存占用显著下降。但要提醒的是,前缀越长收益越明显;如果前缀很短或每次请求前缀都不一样,效果会很有限。

4.5 产品层面的思路:别把所有历史都塞给模型

技术优化之外,还有一个思维层面非常有效:不是所有场景都需要完整历史。KV Cache 成本本质上由上下文长度决定,而上下文长度很多时候是我们自己控制的。

比如对话场景,可以把历史对话每隔几轮做一次摘要,用摘要替换原始内容。长文档问答,可以先检索出最相关的片段,只把片段拼进上下文,而不是把整本资料都塞给模型。代码仓库场景,先定位到相关文件或函数,再精准拼接。这些手段没有改变模型结构,但直接减少了 KV Cache 的体量,效果立竿见影。

这也是为什么很多长上下文模型宣传得天花乱坠,真正部署时还是要加一层“上下文压缩”。模型能理解长上下文,和“必须给它完整长上下文才能用”,是两件完全不同的事。能用短上下文解决问题,就不要为 KV Cache 买单。

5. 常见问题与排查实录

5.1 对话没超长度上限,却突然 OOM

这是我最常被问到的问题。先检查 KV Cache 分配策略。某些框架为了追求吞吐,会按 max_model_len 和最大并发预算一次性分配大量 KV Cache 块。如果请求长度波动大,短请求占了块,长请求来了没块,就会 OOM,哪怕平均上下文远低于上限。

排查步骤:先看服务日志里每个请求的序列长度,特别是峰值长度;再统计当前并发数;然后手动计算峰值 KV Cache 需求量:单 token KV Cache × 平均长度 × 并发数。如果这个数接近显存容量,说明需要限制并发、减少 max_model_len 或开启前缀共享。不要把 max_model_len 定得过高,除非业务真的需要。

5.2 显存占用不高,但生成越来越慢

有些场景显存还够,但 token 生成延迟持续上涨。这通常是带宽瓶颈。KV Cache 总量不大,但每次 decode 都要读取全部缓存。上下文从 2K 涨到 8K 时,单 token 读取量翻了 4 倍,延迟自然翻倍。

这时候先算单位 token 的 KV Cache 大小,再乘以平均上下文长度,除以 GPU 带宽,得到理论读取耗时。如果理论耗时接近实际单 token 延迟,基本可以确认是带宽瓶颈。优化方向就是量化 KV Cache、减少 KV 头数,或者用更短的上下文长度。

5.3 换了 GQA 模型,为什么还是慢

GQA 能减少 KV Cache,但不会自动消除所有性能问题。有的模型虽然 KV 头数少,但层数非常深、隐层维度非常大,模型权重的读取量仍会拖慢 decode。另外,如果服务端没有正确配置块大小和调度策略,GQA 的收益也可能被碎片化抵消。

排查时不要只盯 KV Cache,也要看模型权重读取和激活值计算。decode 是访存密集型任务,权重和 KV Cache 都要从显存读入计算单元。层数越多、单层参数量越大,读取权重的时间就越长。所以 GQA 不一定能单独解决延迟问题,要结合量化、批处理和调度一起看。

5.4 长上下文回答质量下降,跟 KV Cache 有关吗

如果已开 KV Cache 量化,需要怀疑量化误差。尤其当模型输出出现逻辑混乱或细节丢失时,先切回 FP16 对比测试。如果切回去仍有下降,就不是量化问题,而是上下文长度超过模型训练时的有效长度,或者模型本身在长距离信息聚合上能力有限。

我没少见过这种情况:模型宣传 128K 上下文,实际超过 8K 相关内容就开始“遗忘”。这其实跟 KV Cache 关系不大,更多是模型训练数据和注意力机制本身的问题。工程上可以通过检索增强,把关键内容放在离问题较近的位置,降低跨长距离依赖的难度。

5.5 我的排查顺序参考

我自己的标准流程是先算后查。第一步,算单 token KV Cache 的理论大小;第二步,算当前并发和长度下 KV Cache 总量;第三步,用性能分析工具看 decode kernel 耗时占比;第四步,确认瓶颈在权重读取还是 KV Cache 读取;第五步,针对性调参。大部分情况下,调完 KV Cache 相关参数,问题能缓解一大半。剩下那部分,往往要回到产品设计层面去砍上下文长度。

最后再分享一点体会

做了这么久的大模型推理,我越来越觉得 KV Cache 是理解长上下文真实的钥匙。很多宣传里的“长上下文能力”,落地时都变成了显卡预算和延迟问题。每当你看到模型支持 32K 或 128K 上下文,第一反应不应该是“它能记住多少”,而应是“这份 KV Cache 要占多少显存、每生成一个 token 要读多久”。算完这笔账,你对系统的性能就有了底。反过来,很多所谓“越聊越慢”的线上问题,其实不是模型变笨了,而是 KV Cache 把显存和带宽吃干了。理解它,优化它,比盲目堆显卡重要得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进