如果你长期使用各类AI对话工具大概率会遇到一个现象同一个模型刚打开时回复飞快越聊越慢喂给它一份几万字的长文档后首字等待时间肉眼可见地拉长。很多人把原因归结为“服务器负载高”或者“网络变卡”但其实更核心的问题出在一个叫 KV Cache 的机制上。KV Cache 是 Transformer 解码阶段用来缓存历史 Key 和 Value 的中间结果它的存在直接决定了长上下文场景下的显存占用、推理速度以及真实成本结构。这篇内容我就从工程实现的角度把 KV Cache 的存储原理、为什么 AI 会越聊越慢、长上下文的成本到底藏在哪以及业内常用的优化手段一次讲清楚。1. 先从一次“越聊越慢”的实测说起1.1 卡顿不是错觉而是生成方式决定的我最早注意到这个问题是在本地部署一个 7B 规模的开源模型时。刚开始冷启动模型加载完随便问几个短问题响应速度都很正常。但当我把一段大约 1 万字的会议纪要粘进去再连续追问几轮细节后生成速度肉眼可见地从“每秒钟十来个字”掉到了“每秒钟两三个字”。这个现象不是网络问题也不是模型变笨了而是解码过程的计算结构决定的。你每次问一句话模型不是先把整句话一次性写出来而是一个字一个字地往后蹦。每蹦一个字它都要重新“读”一遍当前对话里所有已经出现过的内容再决定下一个字是什么。对话历史越长每一步要处理的内容就越多自然越聊越慢。好多人以为多轮对话只是在文本上累加长度模型“看”到的只是最后一句。其实不是它需要把之前所有轮次的用户输入、助手输出全部拼接起来一起参与计算。你聊得越多这个序列就越长每一步生成的计算量和内存读取量就越大。1.2 一次请求被拆成了两段prefill 与 decode要理解 KV Cache 的作用必须先把一次请求的内部过程拆开。一次完整的 LLM 推理可以分成两个阶段第一个阶段叫 prefill也叫预填充阶段。模型拿到你输入的整段文本后会并行地对所有 token 做一次完整的前向计算生成每个位置对应的中间状态最终产出一个预测 next token 的概率分布。这个阶段的特点是并行度高GPU 的算力能充分发挥但计算量会随输入长度增加而显著增长。你粘贴很长的文档后第一字迟迟不出来主要卡在这个阶段。第二个阶段叫 decode也叫自回归生成阶段。模型拿到 prefill 阶段预测出的第一个新 token把它拼到序列末尾重新算一遍前向再预测下一个 token如此循环往复。这个阶段每一步只新增一个 token但为了预测这个 token它仍然要处理包括历史在内的全部序列信息。所以越是生成长文本循环次数越多越是有长历史每一步的负担越重。正是在 decode 阶段KV Cache 登场了。每一步重新计算历史 token 的 Key 和 Value 是极其浪费的因为这些中间结果不会因为新插入一个 token 而改变。把历史计算过的 Key 和 Value 缓存下来是让解码过程不至于慢到无法接受的关键。1.3 为什么多轮对话让你感觉“越来越慢”现在可以解释文章开头说的“越聊越慢”了。多轮对话里系统通常会把历史消息全部拼进输入序列。你聊到第 10 轮时第 10 轮的 decode 阶段要面对的序列长度就是第 1 轮到第 9 轮所有消息的总长度。序列每增加一批 token下一步生成时都要在注意力计算里多算一批历史位置的匹配分数同时也会让 KV Cache 缓存规模变大。我用一个很直白的比喻描述KV Cache 就像你写论文时做的一堆读书卡片。你每写一个新段落都要把之前所有卡片翻一遍看看哪张可以用。卡片越多翻起来越慢卡片多了以后连放卡片的地方都得额外腾出来。这个“放卡片的地方”就是显存这也就牵扯出了长上下文真正的成本问题。2. KV Cache 到底在缓存什么2.1 自注意力里被丢掉的那个“中间结果”很多人接触 Transformer 时注意力机制都被简化为一个公式Q 和 K 做点积除以缩放因子再过 softmax然后和 V 加权求和。这个公式只描述了单个层、单个 token 的计算过程。实际上一个模型有几十层每一层都要对序列里的每个位置计算 Q、K、V。在计算某个时刻的序列时位置 i 的 Key 和 Value 不仅会影响位置 i 之后的注意力也是之后每一个新生成 token 计算注意力时都需要用到的内容。也就是说历史位置里的 K 和 V在整个生成过程中要被反复读取无数次。如果不缓存每一步 decode 都得把所有历史 token 重新过一遍计算 K 和 V那算力开销会膨胀到完全不可接受。KV Cache 本质上是“用显存换算力”的经典工程取舍把历史位置的 K 和 V 计算一次之后存下来后续每一步直接读取避免重复计算。这也是为什么 KV Cache 被称作 Transformer 推理中最重要的缓存机制之一。2.2 KV Cache 的形态与维度KV Cache 不是一块单独的大内存而是按层组织的。模型有多少层 Transformer 层就有多少组 KV Cache。每一组里又按注意力头分为 Key 缓存和 Value 缓存。具体维度可以这样描述假设模型有 L 层每层有 H 个 KV 头如果使用 GQA 或 MQAKV 头数会远小于 Q 头数每个头的维度是 D精度按字节数 P 计那么每个历史 token 产生的 KV 缓存大小大约是2Key 和 Value 两份乘以 L 乘以 H 乘以 D 乘以 P以常见的 7B 规模开源模型为例层数 L 通常为 32KV 头数 H 如果是 8使用了 GQA每头维度 D 为 128模型权重用 FP16 存储和计算那么每个 token 的 KV 缓存就是 2 乘 32 乘 8 乘 128 乘 2 字节算一下是 131072 字节约 128KB。你没看错平均每个 token 就要占用约 128KB 显存。一段 1000 个 token 的对话历史KV Cache 就要吃掉约 128MB如果你跑了 32K 上下文光 KV Cache 就要占 4GB 上下128K 上下文那就要 16GB 左右。这个数字对大多数消费级显卡来说已经是灾难级别了。上下文长度7B模型(32层, 8个KV头, 128维, FP16) KV Cache 占用1K token约 128MB4K token约 512MB32K token约 4GB128K token约 16GB1M token约 128GB很多人会问模型参数不也就 14GB 左右吗怎么一个 KV Cache 能吃掉和参数一样多的显存答案就是长上下文场景下KV Cache 往往是比模型权重更可怕的显存黑洞。2.3 用一段代码算算显存占用我在实际规划部署方案时习惯先写一个简单函数估算 KV Cache 占用避免上线后才发现显存不够。下面这段 Python 代码可以帮你快速做估算。def estimate_kv_cache_bytes( seq_len: int, batch_size: int 1, layers: int 32, kv_heads: int 8, head_dim: int 128, dtype_bytes: int 2, ): # 每个 token 产生的 KV Cache 字节数 bytes_per_token 2 * layers * kv_heads * head_dim * dtype_bytes total_bytes batch_size * seq_len * bytes_per_token return total_bytes total estimate_kv_cache_bytes(seq_len32768, batch_size1) print(f32K 上下文单条请求 KV Cache 占用: {total / 1024**3:.2f} GB) # 如果同时服务 8 条这样的长请求 print(f8 条请求同时占用: {total * 8 / 1024**3:.2f} GB)这段代码没有考虑连续批处理、PagedAttention 或 KV Cache 量化带来的变化但它把问题的规模暴露得很清楚单条 32K 请求就已经逼近 4GB 占用8 条并发直接超过 32GB。很多团队买了一张 80GB 的大卡以为能跑长上下文结果模型权重占 15GB 后真正服务长请求时显存余量很快就见底了。提示估算时别忘了模型权重和激活值也要占显存。KV Cache 只是其中一个大头服务器显存规划至少要按“权重 激活值 KV Cache 中间缓冲区”四部分联合估算。3. 为什么 AI 会“越聊越慢”时间复杂度和显存的双重挤压3.1 decode 阶段每步都在“复习”全部历史回到生成过程。decode 阶段每生成一个 token都需要计算当前 token 的 Query 与历史所有 token 的 Key 之间的注意力分数。也就是说第 n 步生成时模型要扫一遍长度为 n 的完整 KV Cache。从时间复杂度看decode 的单步注意力计算量大致和当前序列长度成正比。序列长度从 1K 涨到 32K单步推理注意力部分的计算量就涨了 32 倍。注意这里说的是每一步而长文本生成可能需要几百上千步。你感受到的“越来越慢”就是这种线性增长叠加到每一步之后的结果。有朋友会问prefill 阶段不是很快吗能不能 prefill 之后直接把历史结果拿到decode 也快这正是 KV Cache 想做的事。prefill 阶段算出来的 K 和 V 被缓存decode 阶段不需要重算历史但注意力运算仍然需要读取历史 KV 来和当前 Query 做匹配。缓存解决了重复计算解决不了“历史本身很长”这个事实。3.2 带宽饥饿慢的根源往往不是算力更隐蔽的问题是显存带宽。很多人以为 GPU 慢是因为算力不够但在 decode 阶段瓶颈经常出现在“数据搬运”而非“数学计算”。还是以 7B 模型为例。模型权重本身 14GBdecode 每步多多少少都要访问一遍所有参数量。另外长上下文下的 KV Cache 也在持续增长。GPU 的显存带宽是有限的每一步既要读权重又要读 KV然后才能完成计算。权重大小固定KV Cache 大小却随序列长度线性增长。序列越长单步需要搬运的数据量越大生成速度自然下降。我经常用一个比喻解释带宽问题算力像厨房里能做菜的大厨显存带宽像给厨房送食材的运输车。生成每个 token 就像是做一道菜每道菜都要求把冰箱里的全部食材搬过来看一眼。冰箱里食材越多运输车来回所需时间越长。哪怕大厨手艺再快菜也出不来。3.3 一个量化对比1K、32K、128K 上下文每步成本为了把“线性增长”说得更直观我整理了一个典型 7B 模型的单步解码数据读取量估算表。假设权重 14GBFP16KV Cache 按前面 128KB/token 计算序列长度单步需读取的 KV Cache权重读取量总数据读取量粗估1K约 0.125GB14GB约 14.1GB4K约 0.5GB14GB约 14.5GB32K约 4GB14GB约 18GB128K约 16GB14GB约 30GB从表里可以看到真正让单步数据读取量大幅变化的正是 KV Cache。当上下文达到 128K 时KV Cache 的读取量已经超过权重本身。在显存带宽固定的情况下生成速度几乎必然下降。当然了这只是注意力部分的粗略估算实际工程里还有 MLP 层、激活重计算、算子融合等其他开销。但方向已经很明确长上下文真正烧掉的不是“计算”而是“搬数据”和“存数据”的成本。4. 长上下文的真正成本显存、算力、延迟和钱4.1 最大的隐藏成本KV Cache 的显存租赁长上下文的第一个成本是显存。现在的商业 API 和内部服务本质上都是把 KV Cache 当资源出租。模型权重是固定的可以加载一次长期复用但 KV Cache 是每个请求独立分配的请求一来就要分配请求结束才能释放。在多用户场景里假设你有 100 个并发用户每个人都在做长文档问答系统就需要同时为这 100 份长对话维护 KV Cache。如果你把 KV Cache 利用率做成一张图往往能看到它吃掉的内存比模型权重还高。这也是为什么很多推理框架都把“KV Cache 管理”做得比模型计算本身还复杂的原因。很多团队的显存扩容申请理由不是“模型放不下”而是“长上下文并发请求多了之后 KV Cache 放不下”。这句话听起来简单真实踩坑时才知道多痛。4.2 碎片化与调度浪费长上下文让吞吐进一步跳水单一显存大小只是问题的一部分。长上下文还会带来碎片化和调度上的麻烦。KV Cache 是随着对话逐渐增长的请求 1 占 1KB请求 2 占 2KB请求 3 占 1.5KB它们各自的内存分配大小不同生命周期也不同。有些请求聊到一半会被挂起和别的请求拼批量继续生成。如果 KV Cache 是按最大可能长度分配的后面补进来的请求就会因为显存碎片而无法分配连续空间表现为“显存明明还有余量但请求就是起不来”。这个问题在长上下文场景下尤其严重因为单条 KV Cache 本身就很大一个碎片空洞可能就是 1GB 级的大坑。为了避免碎片推理服务不得不做更精细化地 KV Cache 块管理比如按固定大小的块分配内存而不是按请求长度一次性切整块。这个思路已经成了近几年推理框架的核心优化点后面我会专门展开。4.3 按 token 计费背后其实是在为缓存付费很多人不理解为什么长文档对话的 API 费用那么高。看计价规则输入 token 和输出 token 都是单独算钱的输入 token 的价格往往低于输出 token但在多轮长对话场景里问题不在于单价而在于数量。用户第一次上传一篇两万字的材料模型读一遍并把 K、V 缓存进显存。用户接着提问第一句API 服务方要处理 2 万 token 的输入。用户再问第二句系统把第一问、第一答、再加上这 2 万字材料重新拼接又处理一遍 2 万多 token 的输入。每多问一句这 2 万字就要重新计费一次。这不是厂商黑心而是技术现实的映射服务端必须把这个长历史的 KV Cache 一直挂在显存里才能随时响应用户下一句话。KV Cache 不释放显存就被占用显存被占用其他请求就进不来。所以长上下文的真实成本可以概括成一句话它是显存的持续租赁成本而不是一次性计算成本。如果用户开一个长上下文会话挂机一小时这一小时内 KV Cache 几乎没被用到但它仍然占据着显存阻塞着其他可能更短的请求。对服务方而言这种浪费不仅存在而且会在高并发时被放大。5. 工程侧是怎么优化 KV Cache 的5.1 GQA/MQA与其缓存所有头不如共享理解 KV Cache 维度后你会立刻想到一个优化方向能不能让 KV 头少一点这就是 MQA多查询注意力和 GQA分组查询注意力的出发点。标准多头注意力里每一组 Query 头都有自己独立的 Key 和 Value 头。也就是说32 个 Query 头对应 32 组 K/V。GQA 的做法是让 4 个 Query 头共享 1 组 K/V 头极端情况下所有 Query 头共享同一组 K/V就是 MQA。这样 KV Cache 的规模可以被压缩到原来的 1/8 甚至 1/32。很多开源 7B 模型已经用上了 GQAKV 头数从 32 降到了 8。这也是为什么我在前文计算 KV Cache 时用的是 8 而不是 32。你训练自己的模型时如果长上下文是核心需求我强烈建议从一开始就用 GQA而不是在事后做缓存压缩。这个决定直接影响模型每 token 的 KV 缓存大小也直接影响未来部署的并发上限。注意GQA 会略微影响模型精度但大多数任务上损失很小。取舍点是“共享多少组合最合适”4 个 Query 头共享 1 个 KV 头是常见折中既不显著掉点又能把 KV Cache 降到 1/4。5.2 KV Cache 量化用精度换显存的常见方案如果模型已经训练完没法改 GQA 结构那 KV Cache 量化是更直接的方案。原理很简单KV Cache 用 FP16 存每 token 是 2 字节把它压到 INT8每 token 变 1 字节压到 INT4每 token 变 0.5 字节。前面的 128KB/token 可以直接折半或者再折半。实际工程中KV Cache 量化和权重量化不太一样。权重是静态的可以离线慢慢校准KV Cache 是实时产生的量化延迟必须足够低。常见做法是维护原始高精度缓存用于计算同时维护一份低精度缓存用于存储或者按 token 块为单位实时做量化。精度损失通常可以通过增加少量校准样本和缩放因子控制住。我自己实测过 8B 规模模型上把 KV Cache 压到 INT8常见问答和摘要场景下几乎看不出差异压到 INT4 后在长上下文细节记忆类任务上会有轻微下降。INT8 是相对稳妥的开工选择INT4 适合显存非常紧张且任务允许一定精度损失的情况。5.3 PagedAttention 与连续批处理从操作系统偷师前面提到过长上下文导致 KV Cache 碎片化。一个跨时代的解法就是 PagedAttention它的核心思路直接借鉴了操作系统的虚拟内存分页机制把连续的逻辑 KV Cache 切分成固定大小的物理块通过块表映射到非连续的显存区域。这样做有几个直接好处。第一不再要求 KV Cache 在显存里连续分配碎片空洞问题大幅缓解。第二块级管理让内存利用率显著提升同样的显存能同时服务更多并发请求。第三块可以按需分配、按块释放长对话挂起时占用的也只是实际使用到的块而不是按最大长度预留一大片。配套的技术是连续批处理。传统批处理思路是所有请求一起前向直到最短的那个也生成结束再换新请求进来。但在长上下文场景里有的请求序列很长有的很短这种“一刀切”批处理会导致大量 GPU 算力浪费。连续批处理的做法是每个 decode 步动态增删请求短请求完成就立刻腾出 KV Cache 空间给新请求长请求继续跑。如果 KV Cache 管理做得不好连续批处理根本没有实施的基础。5.4 局部与稀疏“旧历史”不一定都要精读KV Cache 优化还有一个重要方向能不能少存一些 KV甚至只处理一部分历史这催生了一系列“稀疏注意力”和“动态缓存”策略。以滑动窗口注意力为例它假设当前位置最关注的是最近一段上下文遥远的早期 token 影响不大。于是每个 token 只需要关注一个固定窗口KV Cache 也只需要保留窗口内的部分。这种做法在长文本生成任务上能显著降低显存和计算量但对需要“翻旧账”的场景可能不友好。比如你问“最早那次讨论的结论是什么”滑动窗口注意力可能已经忘了。还有一类工作会在生成过程中动态评估哪些 KV 是重要的把不重要的 K/V 逐出缓存只保留对后续预测最有用的部分。这类方法在摘要、对话等任务上表现不错但需要额外的评分逻辑工程复杂度不低。我的建议是如果是做一个通用长上下文服务优先把注意力集中在“完整保留 高效管理”上如果是针对特定场景比如流式日志分析、窗口式文档总结再考虑滑动窗口和稀疏化。通用场景盲目砍历史很容易出现“看起来上下文很长实际全是半失忆状态”的尴尬情况。5.5 前缀缓存与流式窗口工程实践里的杀手锏工程实施中还有一个非常有价值的优化前缀缓存。如果你的系统 prompt 很长或者很多用户共享同一份长文档背景那么这段公共前缀算出来的 KV Cache 是可以复用的。请求 A 算完前缀后把这份前缀 KV 缓存保留请求 B 也带着同一份前缀进来时直接复用跳过前缀部分的 prefill 计算。这个优化对“系统提示词 长文档 多轮追问”的场景非常有效。实测里共享前缀的命中率一旦上来首字延迟和输入 token 的重复计算成本都会大幅下降。很多框架已经实现了自动前缀缓存但需要服务端做客观的“前缀匹配”逻辑并不是配置一下就有用。流式窗口则是面向超长文本的另一种策略当输入超过模型训练长度时把最早的内容“挤出”窗口只保留最近的一段上下文。这个做法让模型可以“假装”看到很长的历史但实际计算窗口有限。代价是较早的信息天然丢失。它适合流式日志、直播评论等场景不适合需要精确记忆的长文档问答。6. KV Cache 问题的实测排查经验6.1 显存暴涨先查这四件事我自己排查过多次“显存突然不够”的问题几乎每次都绕不开 KV Cache。遇到显存异常飙升我建议按下面四个顺序查。第一查单请求序列长度。很多框架日志里会打印请求的输入 token 数和输出 token 数。你随手一算每 token KV Cache 大小立刻就能判断当前请求是不是吃显存的大头。很多“莫名爆炸”其实是某个用户传了一篇几十万字的文档。第二查并发数和连续批处理挂起请求数。KV Cache 是在请求生命周期内一直占用的挂起的长请求越多累积显存越可观。在监控面板上你需要看到“当前 active 请求的 KV Cache 总大小”而不是只看 GPU 利用率。第三查是否发生了 KV Cache 逐出或前缀复用失败。如果配置了前缀缓存但命中率接近零说明大量请求前缀不匹配KV Cache 反复重建显存压力被放大。此时要检查公共前缀的长度和归一化方式。第四查是否开了过大的 max_seq_len 上限。有些框架为了避免 OOM会按最大序列长度一次性预留 KV Cache导致显存使用率看起来极高。把上限调低一些或者打开分页式按需分配往往能立刻释放大量显存。这些排查点都处理完后显存问题一般都能定位到明确原因。再遇到“GPU 显存突然满了”的告警你大概就能在几秒内判断出是哪一个请求在作怪。6.2 压低 KV Cache 开销的几条部署建议基于我自己的部署经验再补充几条直接可落地的建议。如果你只能用一块显卡且必须支持长上下文优先开启 KV Cache 量化到 INT8并采用分页式 KV Cache 管理。这样 32K 上下文的 7B 模型在 24GB 显存的卡上才有实际可用的余量。如果业务允许尽量把系统 prompt 和常用文档做成公共前缀并且保证前缀内容不被用户的个人输入插入到中间。前缀越稳定前缀缓存命中率越高长上下文输入的首字延迟越低。如果长上下文任务并发高不要盲目把 max_seq_len 调到模型上限。实际服务时我会按 P99 请求长度而不是最大可能长度来配置显存池其余超出部分的请求通过动态排队或者降级到更小上下文模式处理。这样可以显著提高 GPU 资源的整体利用率而不是被少数极端长请求拖垮。最后建议关注一下“推理时间中的 KV 读取占比”。很多 profiling 工具可以统计算子耗时和内存访问量如果注意力计算的耗时随序列长度线性增长那就说明已经进入带宽瓶颈区间此时再调大 batch 往往适得其反。6.3 一些个人体会做 KV Cache 相关优化这几年我最大的感受是很多人把长上下文能力简单理解为“模型能不能处理 128K token”实际上更关键的是“128K token 时的成本和速度能不能被接受”。KV Cache 是决定这一切的核心变量。在我实际部署和踩坑的经历里最值得重复的建议只有三条一是动手部署前先用公式估算 KV Cache 占用不要等到显存炸了才开始排查二是能用 GQA 结构或 KV 量化就尽早用这是成本收益比最高的优化三是永远不要忽略批处理和内存碎片问题长上下文场景下瓶颈往往不在单个请求而在多个长请求如何共享有限显存。如果你接下来要做一个长文档问答、长对话摘要或者高并发长上下文推理服务建议把 KV Cache 作为一等公民来设计而不是把它当成模型推理的附属品。把它的显存、带宽和管理策略想清楚了长上下文这个看起来很贵的能力才能在你的业务里真正落地。