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)。这个差距在长序列下是指数级的。n1000 时理论计算量差了三个数量级。实测下来在单张消费级显卡上跑一个 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其中2K 和 V 两份L模型层数n序列长度prompt 已生成d_kv每层的 KV 头维度注意不是隐藏维度是 KV 头数 × 每头维度b每个数值的字节数FP16 是 2FP8 是 1INT8 是 1举个实际例子。一个 7B 级别的模型L32 层采用分组查询注意力GQAKV 头数是 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、Vd_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 Cachebatch size 翻倍缓存占用基本翻倍。所以大 batch 和高并发长上下文是天然矛盾的必须做取舍。Prefill 和 Decode 能不能分开部署可以而且现在越来越流行。因为两个阶段的资源特性完全不同分开部署能让各自跑在最适合的硬件上整体利用率更高。这也是很多推理服务架构演进的方向。7. 最后分享几个实用技巧关于 KV Cache 的优化我个人在实际操作中体会最深的一点是先测量再优化。很多人一上来就想着上各种高级方案结果连瓶颈在哪都没搞清楚。先用 profiling 工具把 Prefill 时间、Decode 时间、显存占用曲线都打出来看清楚到底卡在哪再对症下药往往一个简单的配置调整就能解决 80% 的问题。另外一个小技巧如果你的业务里长 prompt 和长对话都有可以考虑做请求分级。把超长 prompt 的请求和普通请求分到不同的队列甚至不同的实例上避免长请求把短请求的延迟拖垮。这个改动不大但效果立竿见影。还有一个经常被忽略的点KV Cache 的显存是会被碎片化的。即使总容量够如果碎片严重也可能分配不出连续空间。所以除了看总占用也要关注碎片率分页管理能很大程度上缓解这个问题。这个方向后续还能继续深挖比如投机解码怎么和 KV Cache 配合、多模态场景下缓存的特殊处理、以及分布式推理里缓存怎么跨卡切分。每一个都够单独写一篇等后面有机会再展开聊。
阅读完成 · 觉得有帮助?