☰
KV Cache 如何决定长上下文推理成本?显存带宽与优化方案全解
2026/10/22 6:21:52 网站建设 项目流程

做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 Cache,decode 阶段每一步仍然要把全部的 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 字节。结果:

  • 单层单 token:2 × 32 × 128 × 2 字节 = 16 KB
  • 全部 32 层:16 KB × 32 = 512 KB/token

也就是说,在这种多头注意力(MHA)配置下,每多聊一个 token,KV Cache 就要多占 512 KB 显存。这个数字不小,因为它是按"token 数量 × 每 token 占用"线性增长的。下面这张表直接给出不同上下文长度下的总量,并和 7B 模型的 FP16 权重(约 14 GB)做对比:

上下文长度KV Cache 总量(MHA,FP16)相当于模型权重的比例
1K512 MB约 3.7%
8K4 GB约 28.6%
32K16 GB约 114%
128K64 GB约 457%

同样算一遍 KV 头数压缩后的 GQA 配置。如果每层只有 8 个 KV 头(后面会细讲),单 token 每层是 2 × 8 × 128 × 2 = 4 KB,32 层合计 128 KB/token。8K 上下文对应的 KV Cache 就降到 1 GB,32K 只有 4 GB。KV 头的数量,直接决定了 KV Cache 的比例因子。

2.3 复利式增长:为什么长对话的 KV Cache 越滚越大

对话服务的上下文,是由用户历史消息加模型历史回答组成的,每一轮都只增不减。假设平均每轮对话新增 1K token,按上面 512 KB/token 的 7B MHA 配置来算:

  • 第 1 轮结束:上下文约 1K,KV Cache 约 512 MB
  • 第 2 轮结束:上下文约 2K,KV Cache 约 1 GB
  • 第 4 轮结束:上下文约 4K,KV Cache 约 2 GB
  • 第 8 轮结束:上下文约 8K,KV 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。单张数据中心级 GPU(BF16 稠密算力约 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/V,KV 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 GB,32K 只有 4 GB。这也是为什么新出的模型普遍标配 GQA:同样的显存预算,上下文长度可以翻几倍,decode 阶段的带宽压力也骤减。代价是注意力表达的自由度略降,但在绝大多数任务上,这个代价小到难以察觉,工程收益却是决定性的。

4.2 给 KV Cache 降精度:FP16 到 INT8/INT4 的实际操作

第二个思路是降低 KV Cache 每个数值的存储字节数。FP16 换成 INT8 或 FP8,KV 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。

  • TTFT(Time To First Token):从请求发出到收到第一个 token 的时间,主要由 prefill 阶段决定,输入越长它越高。
  • TPOT(Time Per Output Token):每个输出 token 的平均生成间隔,主要由 decode 阶段的带宽决定,KV Cache 越大它越高。

测试方法也不复杂:准备一组长度递增的提示词,比如 2K、4K、8K、16K,分别记录 TTFT;再让模型对每组输入生成固定长度的回复,记录 TPOT。正常规律是 TTFT 随输入长度近似线性上升,TPOT 随 KV Cache 规模增大而上升。

如果测出来 TPOT 异常陡增,或 TTFT 在某一段长度突然跳变,那大概率不是模型算力问题,而是显存不足导致的换页、显存碎片,或者缓存分配策略失效。这时候要回头查服务端的内存管理,而不是盲目换更大的模型。

5.2 一个可以抄下来的成本估算模板

我不建议凭感觉估算,直接把计算步骤列出来,照着算就行:

  1. 模型权重显存(FP16):参数量(B)× 2 GB。比如 7B 是 14 GB,13B 是 26 GB。
  2. 单 token KV Cache(FP16):2 × KV 头数 × 头维度 × 层数 × 2 字节,再除以 1024³ 换算成 GB。
  3. 目标上下文的总 KV:单 token KV × 上下文长度。
  4. 单卡可用并发上限:(显存总量 - 权重 - 激活预留)÷ 单路 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 的账,再决定用多大的窗口、给多少并发、要不要做量化或裁剪。毕竟上下文长度本身不是免费的资源,而是每一步生成、每一个并发请求都要持续付费的显存和带宽账单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询