做AI应用开发的朋友应该都遇到过这种体验同一个对话机器人刚开聊时反应飞快几轮过后明显变慢如果聊了几十轮、上下文堆到几千字每个字都像从显存里挤出来的。造成这个现象的往往不是网络抖动也不完全是因为模型太大而是推理阶段一个叫 KV Cache键值缓存的机制在发挥作用。这篇文章想把它彻底讲透KV Cache 缓存了什么它怎么随对话变长而膨胀长上下文的真正成本到底落在容量、带宽还是计算上以及工程上主流的优化方案各自是怎么省显存的。适合正在做大模型推理、开发对话类应用、或者单纯想弄明白为什么长上下文这么贵的工程师朋友。1. 对话变慢不是错觉先说清自回归生成的底层逻辑1.1 一个 token 一个 token 地蹦是模型的天性要说 KV Cache得先回到生成机制本身。当前的大语言模型本质上是自回归模型根据给定的前缀预测下一个最可能的 token。你按下回车之后模型并不是先把整段话都想好再一次性发过来而是每前向计算一次预测出一个 token把它拼到已有文本后面再以这个新序列为输入继续预测下一个 token。这个过程中有两个阶段经常被提到prefill 和 decode。prefill 是并行处理整段输入提示词一次性算出所有输入 token 的中间状态decode 则是逐 token 生成输出。你在聊天界面看到的流式输出本质上是 decode 阶段一个一个 token 往外吐。一个 200 字的回复就意味着大约 200 次前向计算每次只推进一个 token。所以模型的响应时间是 N 次前向计算时间的总和。这个逐字生成的机制是理解后面所有问题的地基。很多优化手段本质上都是在和这个一次只能生成一个 token的约束做斗争。1.2 每生成一个 token注意力都要和全部历史做一次对表Transformer 的 Self-Attention 是两两交互的。对于序列中的任意一个新位置它都需要和前面的所有位置计算关联。具体来说生成第 t 个 token 时它的 Query 向量要和前面 t-1 个 token 的 Key 向量逐一做点积得到注意力权重再按权重去加权求和对应的 Value 向量。这意味着什么意味着历史越长单步工作量越大这件事在架构层面就注定了。生成第 100 个 token 时注意力需要和前 99 个 token 的 Key 比对生成第 1000 个 token 时就要和前 999 个比对。就像主持人每报一个新数都要把前面报过的所有数字重新念一遍才能确认下一句历史越长每句话就越费劲。很多人误以为模型变慢是因为它要重新读一遍所有历史文本这个说法其实不完全。模型确实要重新和所有历史算注意力但它不需要把历史的每个词重新理解一遍。区别在哪里就在我们接下来要说的 KV Cache。KV Cache 的存在省掉的是重新理解历史即重新计算历史 token 的 Key 和 Value这部分重复劳动但省不掉和所有历史的 Key 做比对这一步。1.3 最原始的方案每次都把历史的 K/V 重新算一遍代价有多高为了看清 KV Cache 的价值我们假设一个完全没有缓存的朴素实现。生成第 t 个 token 时前 t-1 个 token 的 K/V 全部要重新经过各层前向计算得出。如果一次回复要生成 S 个 token累计下来多做的 K/V 计算量大约是 S 的平方除以 2 个 token 量级。这个数字有多可怕S 只有 1024 时相当于多算约 52 万个 token 的 K/V当 S 到 8192 时这个数超过 3300 万。你每生成一个 token都要把前面所有 token 的 K/V 从头再算一遍那长对话根本没法用任何 GPU 都撑不住。KV Cache 的出现就是把这种重复劳动变成记账历史 token 的 K/V 算过一次之后直接存到显存里生成新 token 时只需要计算新 token 自己的 K/V把它追加到缓存里然后拿着它的 Query 去和缓存里的全部 Key 做注意力。所以 KV Cache 不是锦上添花的优化而是让长对话在工程上可行的前提。但是请注意缓存不等于免费。即使有了 KV Cachedecode 阶段每一步仍然要把全部的 Key 和 Value 都读取一遍才能完成注意力计算。缓存节省的是计算量但引入了新的成本显存容量和显存带宽。这恰恰是长上下文真正变贵的地方。2. KV Cache 到底缓存了什么一张表算清模型账本2.1 Q、K、V 三件套缓存的是给未来用的那一半先在概念层面说清楚 KV Cache 到底存的是什么。Attention 机制里每个 token 都会生成三个向量Query、Key、Value。打个比方Q 像是去图书馆查资料的读者K 像是书架上的索引标签V 像是具体的书页内容。当前 token 要读完整段序列会用自己的 Q 去和所有位置的 K 做匹配算出权重再按权重去加权对应的 V。生成新 token 时新 token 的 Q 当然要现算因为它是当前的检索需求。但历史 token 的 Q 已经没有用了——它们对应的输出 token 早就生成完了。然而历史 token 的 K 和 V 在未来每一步还会被反复用到每一个后续的新 token都要用它的 Q 来和历史 K 算相似度再拿这些相似度去加权历史的 V。所以 KV Cache 只缓存 K 和 V不缓存 Q。而且每一层 Transformer 都有自己独立的注意力头K/V 是在各层各头上分别计算的因此 KV Cache 也是分层、分头独立保存的。它不是一个全局缓存而是一堆按层组织的键值矩阵。2.2 一个 7B 模型每个 token 的 KV Cache 占多大手把手算一遍光说概念不够我们来手推一遍数值。假设一个典型的 7B 级别模型配置如下配置项数值隐藏维度 hidden_size4096层数 layers32注意力头数 heads32头维度 head_dim128先算单层单个 token 的 KV Cache 大小。K 一份、V 一份所以乘以 2每个头一个 K/V所以乘以头数 32每个 K/V 向量的维度是 128默认 FP16 精度每个数占 2 字节。结果单层单 token2 × 32 × 128 × 2 字节 16 KB全部 32 层16 KB × 32 512 KB/token也就是说在这种多头注意力MHA配置下每多聊一个 tokenKV Cache 就要多占 512 KB 显存。这个数字不小因为它是按token 数量 × 每 token 占用线性增长的。下面这张表直接给出不同上下文长度下的总量并和 7B 模型的 FP16 权重约 14 GB做对比上下文长度KV Cache 总量MHAFP16相当于模型权重的比例1K512 MB约 3.7%8K4 GB约 28.6%32K16 GB约 114%128K64 GB约 457%同样算一遍 KV 头数压缩后的 GQA 配置。如果每层只有 8 个 KV 头后面会细讲单 token 每层是 2 × 8 × 128 × 2 4 KB32 层合计 128 KB/token。8K 上下文对应的 KV Cache 就降到 1 GB32K 只有 4 GB。KV 头的数量直接决定了 KV Cache 的比例因子。2.3 复利式增长为什么长对话的 KV Cache 越滚越大对话服务的上下文是由用户历史消息加模型历史回答组成的每一轮都只增不减。假设平均每轮对话新增 1K token按上面 512 KB/token 的 7B MHA 配置来算第 1 轮结束上下文约 1KKV Cache 约 512 MB第 2 轮结束上下文约 2KKV Cache 约 1 GB第 4 轮结束上下文约 4KKV Cache 约 2 GB第 8 轮结束上下文约 8KKV Cache 约 4 GB注意KV Cache 不是一个算完就能扔的临时变量。只要对话还没结束它在后续每一步 decode 都要被重新读取。每轮新增的 token 不仅增加本轮的计算量还抬高了之后每一步的读取成本。这种越滚越大的累积效应就是越聊越慢最直接的物理来源。还有一个工程现象值得注意在线服务通常会把多个会话拼在一个 batch 里做连续批处理。一个超长会话的 KV Cache 会占用大量显存把 batch 里的其他短请求挤出去导致它们也要排队。所以你在客户端感受到的变慢很多时候不完全是当前这个请求本身慢而是整台机器的并发度被长上下文压低了。这一点我们在第 3 章详细算账。3. 长上下文的真实成本容量、带宽、计算、并发四本账3.1 容量账显存是怎么被上下文吃掉的第一个成本最直观KV Cache 直接占显存。我们沿用上面的 7B MHA 配置模型权重 14 GB激活值和其他开销再预留一些一块 80 GB 显存的卡实际可用的空闲显存大约在 50 到 60 GB。8K 上下文单路 KV 4 GB容量压力还不算大可以并行不少路。32K 上下文单路 KV 16 GB和权重一样重了。128K 上下文单路 KV 64 GB单卡根本放不下至少要拆分到多卡而跨卡通信又带来额外延迟。所以很多号称支持 128K 甚至更长上下文的服务如果都按完整 KV Cache 计算单卡根本扛不住。它们要么是用了较大的 KV 压缩比例要么实际上限制了单路并发要么只是在 sparse 场景下才能跑满。上下文窗口这个参数真正的约束条件不在模型结构里而在你的显存预算里。3.2 带宽账decode 阶段是每生成一个字就把整本笔记翻一遍很多人以为 decode 阶段慢是因为计算量大其实不完全对。decode 阶段每一步只生成一个 token理论上对应的计算量并不高大约就是 2 倍参数量的一次前向7B 模型约 14 GFLOPs。这个量在任何一张现代 GPU 上都是瞬间完成的事。真正的瓶颈在数据搬运。每一步前向GPU 需要做两件耗费带宽的事把模型权重完整读一遍7B FP16 约 14 GB把当前序列的 KV Cache 完整读一遍。以 HBM 带宽 2 TB/s 到 3 TB/s 来估算8K 上下文14 GB 权重 4 GB KV 18 GB单 token 的理论下限约 6 到 9 ms。32K 上下文14 GB 权重 16 GB KV 30 GB单 token 的理论下限约 10 到 15 ms。这个数字看着好像还行但注意这只是单条请求、纯理论下限。一旦 batch 里有多条长会话每多一个序列KV Cache 的读取量就多一份带宽消耗线性叠加。而且实际推理还要考虑指令调度、注意力计算、显存随机访问等开销远高于理论值。这背后的本质是decode 阶段是 memory-bound带宽受限不是 compute-bound算力受限。算力像是你的思考速度带宽像是你翻笔记的速度decode 阶段的人一直在翻一本越来越厚的笔记思考反而很快。顺便说一句这也解释了为什么量化 KV Cache 和量化权重同样重要权重只要读一遍但 KV Cache 每一步都要读把它减半意味着整个 decode 阶段的读取总量直接减半TPOT每输出 token 耗时的改善是非常直接的。3.3 计算账prefill 阶段的长提示词为什么拖慢首 token长上下文不只在生成阶段贵喂进去那一刻就已经开始贵了。prefill 阶段要并行处理整段输入提示词这是一个典型的 compute-bound 场景。粗略估算前向计算量可以用一个经验公式FLOPs 约等于 2 × 参数量 × 输入 token 数。一个 7B 模型输入 8K token前向 FLOPs 大约是 2 × 7e9 × 8192 ≈ 115 TFLOPs。单张数据中心级 GPUBF16 稠密算力约 300 TFLOPS 级别理论耗时不到半秒但实际利用率往往只有 30% 到 50%所以实际耗时往往在 1 秒甚至更高。输入越长这个值线性增长。这就是为什么你往模型里塞一份几百页的文档会先等很久才看到第一个字。首 token 延迟TTFT的高低很大程度取决于 prefill 阶段要消化多少输入。长上下文场景下输入端的计算量经常被忽略但它对用户体验的影响非常直接。3.4 并发账batch 不是越多越好长上下文会塞满显存最后一个成本也是很多团队在做容量规划时最容易算漏的并发度。在线推理服务为了吞吐通常会尽量把多个请求拼在一个 batch 里。但 KV Cache 是按请求条数叠加的不是共享的。我们用 80 GB 显存的卡继续算7B 权重 14 GB激活值等各种开销预留 10 GB剩余约 56 GB 可用于 KV Cache。如果每条请求是 8K 上下文每条 KV 4 GB理论上可以并行约 14 路如果每条请求升到 32K 上下文每条 KV 16 GB只能并行 3 到 4 路。更长的上下文直接导致并发度腰斩再腰斩。并发度下降带来的结果是整台机器的吞吐下降请求排队时间上升。这就是长上下文请求会把短请求挤出去的量化解释。在很多生产环境里你用户感受到的变慢不是因为他自己的请求有多重而是因为系统已经被少数几条长会话占满了显存。这里也提示了一个设计原则如果你要同时服务长上下文和短对话最好做物理隔离别让长会话把短会话的并发资源吃光。4. 工程上怎么把 KV Cache 压下来从改架构到改缓存材料4.1 GQA/MQA从源头减少 KV 头显存直接除以 4既然 KV Cache 的大小和 KV 头数成正比那最直接的省法就是从架构上减少 KV 头。传统的多头注意力MHA里每个 Q 头都有自己独立的 K/V 头32 个 Q 头就是 32 份 K/V。多查询注意力MQA则让所有 Q 头共享同一份 K/VKV Cache 直接降到原来的 1/32。MQA 省得最狠但共享得太过彻底在一些需要细粒度注意力表达的任务上会有精度损失。所以现在更流行的折中是分组查询注意力GQA把 32 个 Q 头分成若干组每组共享一份 K/V。比如 32 个 Q 头配 8 个 KV 头KV Cache 就是原来的 8/32也就是 1/4。回头对照第 2 章的算例MHA 下 8K 上下文 KV 4 GB换成 8 个 KV 头的 GQA 直接变成 1 GB32K 只有 4 GB。这也是为什么新出的模型普遍标配 GQA同样的显存预算上下文长度可以翻几倍decode 阶段的带宽压力也骤减。代价是注意力表达的自由度略降但在绝大多数任务上这个代价小到难以察觉工程收益却是决定性的。4.2 给 KV Cache 降精度FP16 到 INT8/INT4 的实际操作第二个思路是降低 KV Cache 每个数值的存储字节数。FP16 换成 INT8 或 FP8KV Cache 减半换成 INT4再减半。这个操作的诱惑力很大因为它的改动成本比换模型架构低得多。但 KV Cache 量化比权重量化更微妙。K/V 矩阵的数值分布并不均匀往往存在一些绝对值很大的异常通道直接做线性缩放会放大误差导致注意力权重偏移。工程上常用的做法包括按 token 维度或通道维度分别做量化而不是全局一个缩放因子对异常值明显的少数通道保留高精度混合精度存储在量化前后做校准用一小批真实数据观察注意力分布的变化。精度损失到底能不能接受取决于任务。闲聊、续写这类对局部语义不敏感的任务INT8 甚至 INT4 通常问题不大长文本检索、多跳推理、摘要这类依赖精细注意力权重的任务就要谨慎建议先在目标任务上做 A/B 对比。真到了上线阶段我的经验是KV Cache 量化 少量异常通道保留 FP16是个性价比很高的组合既拿到容量减半又把精度损失控制住了。4.3 分页管理与前缀缓存从预留改成按需分配第三个方向和缓存的管理方式有关。朴素推理框架在分配 KV Cache 时通常按请求的最大可能长度一次性预留连续显存。比如一个批量请求设计上限是 8K即使实际只聊了 100 token也要按上限预留空间这会造成巨大的浪费。长尾请求越多浪费越严重。分页管理常说的 PagedAttention 思路把 KV Cache 切成固定大小的块像操作系统管理内存页一样按需分配、按需回收。一块物理显存可以被多个请求的不同序列共享还能对相同前缀做只读共享。这个思路在主流开源推理框架里已经是基础设施实测在长对话、多请求负载下显存利用率能比朴素方案提升不少直接反映为可并行请求数上升。和分页管理经常搭配的是前缀缓存。对话服务通常有一段固定不变的系统提示词比如你是一个乐于助人的助手这类设定。如果每次新会话都从头重新 prefill 这段前缀就是纯粹的浪费。前缀缓存可以把这个公共前缀的 KV 计算结果缓存下来新请求直接复用省掉整段重复计算。在系统提示词很长、调用频率很高的场景里这个优化带来的 TTFT 改善非常直观。4.4 激进裁剪窗口注意力、关键 token 保留和注意力汇点前面的方法都不改变保留全部历史 KV的事实只是降低单 token 的成本。当上下文真的长到离谱还可以采取更激进的思路不保留全部历史只保留一部分。滑动窗口注意力只保留最近 W 个 token 的 KV超出窗口的直接丢弃。适合流式理解的场景代价是超出窗口的远程信息完全丢失。按注意力分数挑选关键历史 token有些方法在每步生成后统计哪些历史 token 的注意力权重最高只保留这些重要 token的 KV其他丢弃。这样能在一定程度上保留跨长距离的关键依赖。注意力汇点保留序列最前端的几个 token通常和强制注意力权重配合让 softmax 的数值保持稳定再配合最近窗口。这利用了开头 token 总能吸引一部分注意力分数的特性兼顾了长文本稳定性和有限缓存。这些裁剪方法的共同点是有损。跨章节检索、全局依赖很强的任务激进裁剪一定会伤效果模型可能忘事。工程上比较稳妥的用法是做摘要 最近窗口的混合结构历史全量信息先压缩成摘要存起来KV Cache 里只保留最近几轮对话和摘要本身既能控制缓存大小又能保住长期记忆的可用性。4.5 我的选型思路按业务场景组合这些手段这么多手段不可能全部堆上去得按场景取舍。我自己通常这样决策短对话助手目标是低延迟和高并发模型选 GQA 配置量化到 INT8 即可上下文控制在 4K 到 8K不需要太激进的裁剪。文档问答/代码库分析需要长上下文prefill 阶段可以接受几秒等待重点用分页管理 前缀缓存让公共前缀尽量复用必要时配合 KV 量化降低显存压力。持续性客服会话不要天真地把所有历史都塞进上下文。更合理的方案是每轮或每几轮生成一段结构化摘要KV Cache 里只保留最近几轮加摘要这比无限堆上下文便宜得多也稳定得多。提醒一句不要因为模型声称支持 128K 上下文就真把所有请求都按 128K 设计。在做容量规划之前先算清楚 KV Cache 账本否则线上 OOM 和延迟飙升只是时间问题。5. 实战心得怎么评估一个服务是不是长上下文成本失控5.1 别只看上下文长度要看 TTFT 和 TPOT 两条曲线做容量规划之前至少要量出两个关键指标TTFT 和 TPOT。TTFTTime To First Token从请求发出到收到第一个 token 的时间主要由 prefill 阶段决定输入越长它越高。TPOTTime Per Output Token每个输出 token 的平均生成间隔主要由 decode 阶段的带宽决定KV Cache 越大它越高。测试方法也不复杂准备一组长度递增的提示词比如 2K、4K、8K、16K分别记录 TTFT再让模型对每组输入生成固定长度的回复记录 TPOT。正常规律是 TTFT 随输入长度近似线性上升TPOT 随 KV Cache 规模增大而上升。如果测出来 TPOT 异常陡增或 TTFT 在某一段长度突然跳变那大概率不是模型算力问题而是显存不足导致的换页、显存碎片或者缓存分配策略失效。这时候要回头查服务端的内存管理而不是盲目换更大的模型。5.2 一个可以抄下来的成本估算模板我不建议凭感觉估算直接把计算步骤列出来照着算就行模型权重显存FP16参数量B× 2 GB。比如 7B 是 14 GB13B 是 26 GB。单 token KV CacheFP162 × KV 头数 × 头维度 × 层数 × 2 字节再除以 1024³ 换算成 GB。目标上下文的总 KV单 token KV × 上下文长度。单卡可用并发上限显存总量 - 权重 - 激活预留÷ 单路 KV Cache。举一个例子版本 80 GB 显存的卡7B 权重 14 GB激活预留 10 GB剩余 56 GB。MHA 配置下 8K 上下文单路 KV 4 GB并发上限约 14 路换成 GQA 8 个 KV 头8K 上下文单路 KV 1 GB并发上限约 56 路。同样是 7B 模型光是架构上减少 KV 头并发能力就差了 4 倍。做是否要上长上下文决策时我习惯问自己几个问题业务真的需要全量历史吗还是最近 N 轮就够用户能接受 TTFT 随输入长度上升到什么程度当前并发目标下KV Cache 是否已经挤占了权重和激活空间有没有可以复用的共享前缀这些判断比死磕一个更大的上下文窗口数字重要得多。5.3 几个不花大钱就能缓解越聊越慢的小技巧如果暂时换不了模型架构也扛不住长上下文的显存成本下面几个办法能快速缓解问题用摘要代替历史每轮或每几轮把对话浓缩成一段摘要作为下一轮的系统提示词比无限堆原文便宜得多。做会话分段超过一定轮数强制开新会话把之前的结论写到持久化存储里。记忆不是越长越好而是需要时能找到。超长文档先检索再拼片段不要一次性把几百页塞进上下文先用检索把相关段落挑出来再拼进 prompt。监控线上 TPOT 和 KV Cache 平均占用设置告警某一路请求的 KV Cache 明显异常时及时限流避免一条长会话拖垮整台机器的并发。我自己在调优这类服务时最深的体会是KV Cache 不是一个可以忽视的缓存细节它直接决定了你产品的延迟曲线、显存预算和并发上限。很多团队一上来就追求更大的上下文窗口结果把并发和延迟同时拖垮。真正健康的设计是先算清 KV Cache 的账再决定用多大的窗口、给多少并发、要不要做量化或裁剪。毕竟上下文长度本身不是免费的资源而是每一步生成、每一个并发请求都要持续付费的显存和带宽账单。