1. 从一次线上事故说起:AI 为什么越聊越慢
去年帮一个团队排查他们的大模型对话服务,现象很典型:单轮问答响应飞快,首字延迟稳定在 400 毫秒左右;但只要用户连续追问七八轮,或者粘贴一份两万字的合同让模型总结,响应时间就会从几百毫秒一路爬到十几秒,GPU 显存占用也跟着水涨船高,最后直接 OOM 崩掉。他们一开始怀疑是网络抖动,后来又怀疑是推理框架版本问题,折腾了整整两天,最后定位到的根因其实就一个词——KV Cache。
这个场景在做过大模型推理部署的人里太常见了。很多人第一次接触 KV Cache,是在读 Transformer 论文或者看推理框架源码的时候,知道它是"缓存注意力机制的 Key 和 Value 以加速解码",但真正到了线上,才发现它的行为远比一句定义复杂:它既是让自回归生成从 O(n²) 降到 O(n) 的关键优化,也是长上下文场景下显存爆炸、吞吐骤降的罪魁祸首。它像一个双刃剑,用得好是性能倍增器,用不好就是压垮服务的最后一根稻草。
这篇内容我打算把 KV Cache 从头到尾讲透。不是那种"KV Cache 就是缓存 K 和 V"的科普,而是从为什么需要它、它到底占多少显存、长上下文的真实成本在哪里、工程上怎么优化这几个角度,把我在实际部署中踩过的坑和验证过的方案都摊开讲。适合已经跑过至少一次大模型推理、想搞清楚性能瓶颈到底出在哪的工程师,也适合正在做长上下文应用、被显存和延迟折磨的开发者。看完你至少能回答三个问题:为什么 AI 越聊越慢、长上下文贵在哪、以及怎么把这块成本压下来。
2. KV Cache 到底解决了什么问题
2.1 自回归生成的本质:一个 token 一个 token 往外蹦
要理解 KV Cache,得先理解大模型是怎么"说话"的。现在主流的 decoder-only 架构(比如各类 GPT 式模型),生成方式是自回归的:给定一段输入,模型预测下一个 token,然后把预测出的 token 拼回输入,再预测下一个,如此循环,直到遇到结束符或者达到长度上限。
这个过程有个关键特点——每一步的计算都依赖前面所有的 token。生成第 100 个 token 时,模型需要看到前 99 个 token 的信息;生成第 101 个时,又需要看到前 100 个。注意力的计算方式是:当前 token 的 Query 要和前面所有 token 的 Key 做点积,算出注意力权重,再对前面所有 token 的 Value 做加权求和。
如果没有缓存,每生成一个新 token,都要把前面所有 token 的 K 和 V 重新算一遍。生成第 n 个 token 时,前面 n-1 个 token 的 K、V 计算量是白费的重复劳动——因为它们的值根本没变。这就是 KV Cache 要解决的核心问题:把已经算过的 K 和 V 存下来,下一步直接复用,不再重算。
2.2 有缓存和没缓存的差距有多大
我用一个具体的例子算给你看。假设模型有 L 层,隐藏维度是 d,序列长度是 n。每生成一个 token:
- 没有 KV Cache:每一层都要对全部 n 个 token 重新做一次 K、V 的线性投影,计算量正比于 n,累积到第 n 步就是 O(n²) 的总计算量。
- 有 KV Cache:每步只需要算当前这 1 个 token 的 K、V,然后从缓存里读取前面 n-1 个,计算量正比于 1,累积到第 n 步是 O(n)。
这个差距在长序列下是指数级的。n=1000 时,理论计算量差了三个数量级。实测下来,在单张消费级显卡上跑一个 7B 模型,开启 KV Cache 后生成速度能提升 5 到 10 倍,序列越长差距越明显。所以现在几乎所有推理框架(不管是哪家的)默认都开 KV Cache,这不是可选项,是必选项。
但问题也随之而来:缓存是要占显存的。你省下了计算,代价是把这些中间结果全存进了显存里。序列越长、并发越高,这块占用就越夸张,最终成为长上下文场景下最硬的瓶颈。
2.3 一个容易被忽略的细节:Prefill 和 Decode 是两个阶段
很多人把推理当成一个整体,其实它分成两个性质完全不同的阶段,KV Cache 在其中的角色也不一样。
Prefill(预填充)阶段:处理用户输入的整段 prompt。这时候所有 token 是并行计算的,一次性把每一层的 K、V 都算出来存进缓存。这个阶段是计算密集型的,GPU 算力吃满,显存带宽反而没那么紧张。首字延迟(TTFT)主要就取决于这个阶段。
Decode(解码)阶段:一个一个往外吐 token。每步只算一个新 token,但要读取整个 KV Cache。这个阶段是显存带宽密集型的,算力用不满,瓶颈在把缓存从显存搬到计算单元的速度上。
理解这个区分特别重要,因为后面讲的所有优化手段,针对的都是不同阶段。比如长 prompt 卡的是 Prefill,长对话卡的是 Decode,两者的优化思路完全不一样。我见过不少人一上来就调 batch size,结果发现根本没打到点上,就是因为没分清自己卡在哪个阶段。
3. 长上下文的真实成本:算一笔显存账
3.1 KV Cache 显存占用的计算公式
这是整篇内容最核心的部分,我建议你把这几个公式记下来,因为它能帮你快速判断一个配置到底跑不跑得起来。
单个序列的 KV Cache 显存占用大致是:
KV Cache 大小 = 2 × L × n × d_kv × b其中:
- 2:K 和 V 两份
- L:模型层数
- n:序列长度(prompt + 已生成)
- d_kv:每层的 KV 头维度(注意不是隐藏维度,是 KV 头数 × 每头维度)
- b:每个数值的字节数(FP16 是 2,FP8 是 1,INT8 是 1)
举个实际例子。一个 7B 级别的模型,L=32 层,采用分组查询注意力(GQA),KV 头数是 8,每头维度 128,那么 d_kv = 8 × 128 = 1024。用 FP16 存储:
- 序列长度 4096 时:2 × 32 × 4096 × 1024 × 2 字节 =512 MB
- 序列长度 32768 时:这个数字直接变成4 GB
- 序列长度 128K 时:16 GB
注意,这还只是一个序列。如果你要支持 16 路并发,上面每个数字都要乘以 16。128K 上下文、16 并发,光 KV Cache 就要 256 GB 显存——这已经超过任何单张卡的容量了。这就是为什么长上下文服务那么贵,贵的不只是模型权重,更是这块随长度线性膨胀的缓存。
3.2 为什么 GQA 和 MQA 能救命
上面公式里的 d_kv 是关键变量。早期的多头注意力(MHA)里,KV 头数和 Query 头数一样多,比如都是 32 头,那 d_kv 就是 32 × 128 = 4096,是 GQA 的 4 倍。这意味着同样的序列长度,MHA 的 KV Cache 占用是 GQA 的 4 倍。
这就是为什么现在新模型几乎都转向GQA(分组查询注意力)甚至MQA(多查询注意力)。MQA 极端到所有 Query 头共享一组 K、V,d_kv 直接降到 1 头的量级,缓存占用能压到 MHA 的几十分之一。代价是模型表达能力略有下降,所以 GQA 成了折中方案——几个 Query 头共享一组 KV,既省显存又保住质量。
我实测过一个对比:同一个 13B 模型,MHA 版本在 8K 上下文下单序列就要 2 GB 缓存,换成 GQA 版本后降到 512 MB,同样的卡能支持的并发数直接翻了 4 倍。所以选模型的时候,看它用没用 GQA,比看参数量更能决定你的部署成本。
3.3 长上下文真正的成本结构
把账算清楚之后,长上下文的成本可以拆成三块:
| 成本项 | 随长度变化 | 主要影响 | 优化方向 |
|---|---|---|---|
| 模型权重 | 不变 | 固定显存占用 | 量化、蒸馏 |
| KV Cache | 线性增长 | 并发能力、OOM 风险 | GQA、量化、分页 |
| 注意力计算 | 平方增长(Prefill) | 首字延迟 | 稀疏注意力、分块 |
很多人只盯着模型权重,觉得"7B 模型才 14 GB,一张 24 GB 卡绰绰有余",结果一上长上下文就崩。真相是:权重是固定的,KV Cache 才是那个会失控的变量。一个 7B 模型在 128K 上下文下,KV Cache 占用可能比权重还大。这就是长上下文"真正的成本在哪"的答案——不在模型本身,在那块随对话长度不断膨胀的缓存。
4. 工程优化:把 KV Cache 的成本压下来
4.1 PagedAttention:把显存碎片问题解决掉
KV Cache 有个很讨厌的特性:它的长度是动态的。你不知道用户下一句会问多长,也不知道他会聊几轮。如果按最大长度预分配显存,浪费极其严重;如果按需分配,又会产生大量碎片,最后明明有空间却分配不出来。
PagedAttention这个思路借鉴了操作系统的虚拟内存分页机制:把 KV Cache 切成固定大小的 block(比如每块存 16 个 token 的 K、V),用一张页表来管理逻辑位置到物理 block 的映射。这样不同序列的 block 可以非连续存放,显存利用率能从传统的 20% 到 40% 提升到 90% 以上。
我在实际部署里对比过:同样的硬件,开启分页管理后,能同时服务的并发请求数提升了 2 到 3 倍,长对话场景下 OOM 的概率大幅下降。这个优化基本是现在推理框架的标配,如果你用的框架还不支持,强烈建议换一个。
4.2 KV Cache 量化:用精度换空间
既然缓存占的是显存,那最直接的办法就是让每个数值占更少的字节。FP16 是 2 字节,量化到 FP8 或 INT8 就变成 1 字节,缓存直接减半;激进一点量化到 INT4,能压到四分之一。
但这里有个坑:KV Cache 的量化比权重量化敏感得多。权重是静态的,可以离线校准;KV Cache 是动态生成的,每步都在变,量化误差会沿着序列累积。我试过把某个模型的 KV Cache 直接 INT8 量化,短对话没问题,但长上下文下生成质量明显下降,出现重复、跑题的情况。
比较稳妥的做法是:
- 优先用 FP8,对质量影响很小,收益又明显
- 如果一定要 INT8,只量化 K 或者只量化 V,别两个都动
- 量化后一定要跑长上下文的质量评测,别只看短对话
提示:KV Cache 量化对显存的节省是线性的,但对质量的伤害是非线性的。序列越长,误差累积越严重。上线前务必用真实的长文本任务做回归测试。
4.3 前缀缓存与共享:多轮对话的省钱利器
多轮对话有个特点:每一轮的新输入,都包含前面所有轮次的内容。也就是说,第 3 轮的 prompt 里,前两轮的部分和之前完全一样。如果每次都重新算一遍,纯属浪费。
前缀缓存(Prefix Caching)就是把这个重复部分识别出来,直接复用之前算好的 KV Cache,只计算新增的那部分。在客服、助手这类多轮场景里,命中率能到 70% 以上,首字延迟直接砍掉一大半。
实现上要注意两点:一是要有高效的前缀匹配算法,快速找到最长公共前缀;二是要管理好缓存的淘汰策略,别让不常用的前缀一直占着显存。我见过有团队开了前缀缓存但没做淘汰,结果缓存越积越多,最后反而比不开还慢。
4.4 滑动窗口与注意力稀疏化
如果业务上不需要模型记住全部历史,可以用滑动窗口注意力:只保留最近 N 个 token 的 KV,更早的直接丢掉。这样缓存占用就被钉死在 N 这个上限上,不再随对话无限增长。代价是模型会"忘记"更早的内容,适合那些只关心近期上下文的场景。
更精细一点的是注意力稀疏化,比如只保留注意力权重高的那些 token 的 KV,把权重低的丢掉。这个思路在长文本任务上效果不错,但实现复杂,需要模型本身支持或者做额外的训练,不是开箱即用的方案。
5. 实操排查:线上问题怎么定位
5.1 一张表看懂问题出在哪
线上遇到性能问题,先别急着改配置,按这张表对号入座:
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 首字延迟高 | Prefill 慢,prompt 太长 | 看 TTFT 指标随输入长度的变化 | 前缀缓存、分块预填充 |
| 吐字速度慢 | Decode 阶段显存带宽瓶颈 | 看 TPOT 指标、显存带宽利用率 | KV 量化、减少并发 |
| 越聊越慢 | KV Cache 随轮次增长 | 看显存占用随对话轮次曲线 | 滑动窗口、前缀缓存 |
| 突然 OOM | 长序列 + 高并发叠加 | 看峰值显存、并发数 | 分页管理、限流 |
| 质量下降 | KV 量化过度 | 对比量化前后长文本输出 | 降量化等级、只量化单侧 |
5.2 我踩过的几个坑
坑一:只看平均延迟,不看 P99。平均延迟好看不代表服务健康。长尾请求(超长 prompt、超多轮对话)会把 P99 拉到天上,用户体验断崖式下跌。一定要盯 P99 和显存峰值。
坑二:并发数拍脑袋定。并发不是越大越好。KV Cache 是并发 × 长度的乘积,并发翻倍,缓存占用就翻倍。我一般会先算出单序列在最大长度下的缓存占用,再用可用显存反推安全并发数,留 20% 余量。
坑三:忽略 prompt 长度分布。很多团队按平均 prompt 长度做容量规划,结果被少数超长请求打爆。正确做法是看 P95、P99 的 prompt 长度,按最坏情况留余量,或者对超长请求单独限流。
坑四:量化后不做长文本回归。短对话测不出量化问题,一定要用真实的长文档、多轮对话做测试,重点看有没有重复、逻辑断裂、事实错误。
5.3 容量规划的快速估算方法
给你一个我常用的快速估算流程:
- 确定模型:层数 L、KV 头维度 d_kv、精度 b
- 确定业务上限:最大序列长度 n_max、目标并发数 C
- 算单序列缓存:
2 × L × n_max × d_kv × b - 算总缓存:单序列 × C
- 加上模型权重和其他开销,对比可用显存
- 如果超了,按优先级砍:先降并发,再上量化,最后考虑滑动窗口
这套流程能让你在选型阶段就判断出方案可不可行,而不是等上线了才发现跑不起来。
6. 几个容易被问到的细节
KV Cache 能不能跨请求共享?只有前缀完全相同的部分能共享,这就是前缀缓存的原理。完全不同的请求之间没法共享,因为 K、V 是跟具体 token 绑定的。
为什么 Decode 阶段是显存带宽瓶颈?因为每生成一个 token,都要把整个 KV Cache 从显存读一遍。序列越长,读的数据量越大,而计算量却很小,GPU 的算力根本用不上,全卡在数据搬运上。这也是为什么长上下文下 GPU 利用率看着不高但就是慢。
Batch size 和 KV Cache 是什么关系?Batch 里的每个序列都有自己独立的 KV Cache,batch size 翻倍,缓存占用基本翻倍。所以大 batch 和高并发长上下文是天然矛盾的,必须做取舍。
Prefill 和 Decode 能不能分开部署?可以,而且现在越来越流行。因为两个阶段的资源特性完全不同,分开部署能让各自跑在最适合的硬件上,整体利用率更高。这也是很多推理服务架构演进的方向。
7. 最后分享几个实用技巧
关于 KV Cache 的优化,我个人在实际操作中体会最深的一点是:先测量,再优化。很多人一上来就想着上各种高级方案,结果连瓶颈在哪都没搞清楚。先用 profiling 工具把 Prefill 时间、Decode 时间、显存占用曲线都打出来,看清楚到底卡在哪,再对症下药,往往一个简单的配置调整就能解决 80% 的问题。
另外一个小技巧:如果你的业务里长 prompt 和长对话都有,可以考虑做请求分级。把超长 prompt 的请求和普通请求分到不同的队列甚至不同的实例上,避免长请求把短请求的延迟拖垮。这个改动不大,但效果立竿见影。
还有一个经常被忽略的点:KV Cache 的显存是会被碎片化的。即使总容量够,如果碎片严重,也可能分配不出连续空间。所以除了看总占用,也要关注碎片率,分页管理能很大程度上缓解这个问题。
这个方向后续还能继续深挖,比如投机解码怎么和 KV Cache 配合、多模态场景下缓存的特殊处理、以及分布式推理里缓存怎么跨卡切分。每一个都够单独写一篇,等后面有机会再展开聊。