1. 从一次线上事故说起:为什么聊到第37轮,响应突然卡住了
去年冬天,我们内部有个智能问答助手,平时跑得好好的,用户问一句它答一句,延迟稳定在800毫秒左右。结果有一天运营同学跑来做压力测试,连续对话了三十多轮,到第37轮的时候,首字延迟直接飙到了4秒以上,后面几轮更夸张,直接触发了网关超时。当时第一反应是模型服务挂了,查了GPU利用率、显存占用、网络带宽,全都正常。最后把每一轮的推理耗时打出来一看,问题非常明显:随着对话轮次增加,单次推理的耗时几乎线性增长。
这个现象其实不是个例。任何做过大模型推理服务的人,迟早都会撞上这堵墙。而墙背后站着的核心概念,就是KV Cache。很多人第一次听到这个词,以为它是个优化开关,打开就完事了。实际上它既是让推理从“不可用”变成“可用”的关键机制,也是长上下文场景下真正的成本黑洞。这篇文章我就把KV Cache从头到尾拆一遍,包括它为什么存在、怎么算显存、为什么越聊越慢、长上下文到底贵在哪里,以及工程上怎么把它压下去。
如果你正在做推理服务、正在评估长上下文方案、或者单纯好奇为什么大模型聊久了会变慢,这篇内容应该能帮你把这条链路彻底理清楚。我会尽量用从业者之间交流的方式来讲,不堆公式,但关键的计算过程一个都不会省。
2. KV Cache到底是什么:没有它,推理根本跑不起来
2.1 自回归生成的本质:一个字一个字往外蹦
要理解KV Cache,得先理解大模型是怎么生成文本的。现在主流的生成式模型都是自回归的,也就是说,它每次只预测下一个token,然后把预测出来的token拼接到输入后面,再预测下一个,如此循环。你看到的一段完整回答,其实是模型跑了几百次甚至几千次推理拼出来的。
这里有个关键点:每一次预测,模型都需要看到前面所有的token。因为注意力机制的本质,就是当前token要和前面每一个token计算相关性。所以生成第N个token的时候,输入序列长度是N。生成第N+1个token的时候,输入序列长度是N+1。看起来只是多了一个token,但计算量完全不是线性增长那么简单。
2.2 没有缓存的世界:每一步都在重复造轮子
假设我们不用任何缓存,那么生成第N个token时,模型要对前N个token全部重新做一遍注意力计算。生成第N+1个token时,又要把前N+1个token全部重新算一遍。这里面绝大部分计算是完全重复的——前N个token的Key和Value投影结果,在上一轮已经算过了,这一轮又算了一遍。
我打个比方。你在做一道需要反复查同一本字典的题。没有缓存的做法,相当于每查一个新词,都要把前面查过的所有词重新翻一遍字典。查第10个词的时候翻10次,查第100个词的时候翻100次。整个生成过程的总计算量,就是1+2+3+...+N,也就是O(N²)级别。序列越长,浪费越恐怖。
2.3 KV Cache的核心思想:把算过的存下来
KV Cache做的事情非常朴素:把每一层注意力计算中已经算好的Key和Value张量缓存起来,下一步直接复用,不再重算。注意,这里只缓存Key和Value,不缓存Query。因为Query是当前token自己的,每步都会变;而Key和Value是历史token的“身份信息”,一旦算出来就不会变。
这样一来,生成第N+1个token时,只需要计算当前这个新token的Query、Key、Value,然后把新的Key和Value追加到缓存里,用当前Query去和缓存中所有的Key做注意力即可。计算量从O(N²)直接降到O(N),每一步只和序列长度成正比。
提示:KV Cache缓存的是每一层、每一个注意力头的Key和Value。层数越多、头数越多、维度越大,缓存占用的显存就越夸张。这是后面所有成本问题的根源。
2.4 为什么只缓存K和V,不缓存Q
这个问题我面试过不少人,能答清楚的不到一半。核心原因在于注意力机制的计算公式:Attention(Q, K, V) = softmax(QKᵀ/√d)V。在自回归生成中,每一步只有当前token的Q是新的,历史token的K和V是固定的。如果你把Q也缓存了,下一步也用不上,因为下一步的Q是另一个token的。缓存Q纯属浪费显存,没有任何复用价值。
理解了这一点,你就能明白为什么KV Cache的显存占用会随着对话轮次线性增长——每多一轮对话,就多一批K和V要存着,而且这些数据在整个会话期间都不能释放。
3. 显存账本:KV Cache到底吃掉多少资源
3.1 一个可以直接套用的计算公式
KV Cache的显存占用,有一个非常实用的估算公式:
KV Cache显存 = 2 × batch_size × seq_len × num_layers × num_heads × head_dim × dtype_bytes其中:
- 2 是因为要存Key和Value两份
- batch_size 是并发请求数
- seq_len 是当前序列长度(包括输入和已生成部分)
- num_layers 是Transformer层数
- num_heads × head_dim 等于模型的隐藏维度
- dtype_bytes 是数据类型字节数,FP16是2,FP32是4,INT8是1
这个公式我建议每个做推理的人都背下来,因为它能让你在选模型、估显存、定并发的时候心里有底。
3.2 拿一个具体模型算一遍
假设我们用一个隐藏维度4096、层数32、注意力头数32、head_dim为128的模型,用FP16推理,单条序列长度4096,batch_size为1。代入公式:
KV Cache = 2 × 1 × 4096 × 32 × 32 × 128 × 2 bytes = 2 × 4096 × 32 × 4096 × 2 = 2 × 4096 × 4096 × 64 ≈ 2.1 GB单条4096长度的序列,KV Cache就要2.1GB。如果并发是16,那就是33.6GB。而模型权重本身可能才13GB左右。也就是说,在高并发长上下文场景下,KV Cache的显存占用会远超模型权重本身。这就是为什么很多推理服务明明显存够放模型,却跑不了几个并发。
3.3 不同序列长度下的显存增长曲线
我把上面这个模型在不同序列长度下的KV Cache占用列了个表,单条序列、FP16:
| 序列长度 | KV Cache占用 | 相对4096的倍数 |
|---|---|---|
| 1024 | 0.52 GB | 0.25x |
| 2048 | 1.05 GB | 0.5x |
| 4096 | 2.10 GB | 1x |
| 8192 | 4.20 GB | 2x |
| 16384 | 8.39 GB | 4x |
| 32768 | 16.78 GB | 8x |
| 131072 | 67.11 GB | 32x |
这张表说明一个很残酷的事实:上下文长度翻倍,KV Cache显存就翻倍。从4K到128K,显存需求涨了32倍。而模型权重是不变的。所以长上下文真正的成本,大头不在模型本身,而在KV Cache。
3.4 为什么说“越聊越慢”本质是显存和带宽问题
回到开头那个事故。对话到第37轮,序列长度可能已经累积到几千token。每一步生成都要读取整个KV Cache,而KV Cache的读取是显存带宽密集型操作。GPU的算力再强,如果数据搬运跟不上,计算单元就得等着。序列越长,每步要搬运的KV数据越多,单步延迟就越高。
所以“越聊越慢”不是模型变笨了,而是每生成一个token,需要搬运的历史KV数据越来越多。这是物理层面的瓶颈,跟算法优不优化没关系,除非你减少要搬运的数据量。
4. 长上下文的真实成本:不只是显存,还有延迟和吞吐
4.1 成本一:显存容量被吃光,并发上不去
这是最直接的成本。前面算过,128K上下文的KV Cache单条就要67GB。一张80GB的卡,放完模型权重之后,可能连一条128K序列都撑不住,更别说并发了。很多团队做长上下文demo很惊艳,一上生产就发现并发只能开到个位数,成本根本摊不平。
4.2 成本二:显存带宽成为瓶颈,首字延迟飙升
生成第一个token之前,模型要先把整个输入序列的KV Cache算出来并写入显存。输入越长,这个预填充阶段越久。而后续每生成一个token,都要把全部KV Cache读一遍。以128K序列为例,每步要读67GB的数据,就算显存带宽是2TB/s,光读数据就要33毫秒,加上计算,单步延迟轻松破百毫秒。生成100个token就是10秒以上。
4.3 成本三:注意力计算量随长度平方增长
虽然KV Cache把重复计算省了,但注意力本身的计算量还是O(N²)——当前token要和前面N个token算相关性。序列从4K涨到128K,注意力计算量涨了1024倍。这部分计算虽然比重复计算划算,但在超长上下文下依然是可观的开销。
4.4 成本四:调度复杂度上升,碎片化严重
长上下文请求的KV Cache占用是动态的,不同请求长度差异巨大。如果显存分配策略不当,很容易产生碎片,导致明明总显存够,却分配不出连续空间。这也是为什么现在主流推理框架都在做分页式KV Cache管理,把KV Cache切成固定大小的块,按需分配,减少碎片。
提示:评估长上下文方案时,不要只看“能不能跑通”,一定要看“在目标并发和延迟下,单token成本是多少”。很多方案在demo阶段很美好,一算成本就崩了。
5. 工程上怎么把KV Cache压下去:几条实战路线
5.1 路线一:量化KV Cache,用精度换空间
最直接的办法是把KV Cache从FP16量化到INT8甚至INT4。INT8能把显存占用直接砍半,INT4再砍半。实测下来,INT8量化对生成质量的影响通常很小,在多数对话场景下几乎感知不到。INT4则要看具体模型和任务,有些对精度敏感的任务会出现明显退化。
具体操作上,现在主流推理框架都支持KV Cache量化。以常见的配置为例,你可以在启动参数里指定KV Cache的dtype:
# 伪代码示意,具体参数名以所用框架文档为准 --kv-cache-dtype int8需要注意的是,量化本身有开销,如果序列很短,量化带来的收益可能抵不过反量化的计算成本。所以短序列场景不建议开KV Cache量化,长序列场景收益才明显。
5.2 路线二:分组查询注意力,从结构上减少KV头数
这是模型结构层面的优化。传统多头注意力里,每个注意力头都有自己独立的K和V。而分组查询注意力让多个Query头共享同一组K和V。比如32个Query头只配8组KV头,KV Cache直接减少到四分之一。
现在新出的模型基本都默认用GQA或者更激进的MQA(所有头共享一组KV)。如果你在选模型,同等参数规模下优先选KV头数少的,长上下文场景下显存优势非常明显。
5.3 路线三:分页式管理,把KV Cache切块
这个思路借鉴了操作系统的虚拟内存分页。把KV Cache切成固定大小的block,比如每块存16个token的KV。请求来了按需分配block,不要求连续。这样能大幅减少显存碎片,提升利用率。现在主流的高吞吐推理框架基本都实现了这套机制。
实测下来,分页式管理能让同样的显存多跑20%到40%的并发,尤其是在请求长度参差不齐的场景下,效果更明显。
5.4 路线四:前缀共享,相同开头只存一份
很多应用场景下,大量请求共享相同的前缀,比如同一个系统提示词、同一段背景知识。如果每个请求都独立存一份前缀的KV Cache,纯属浪费。前缀共享技术让这些请求复用同一份前缀KV,只在分叉之后各自维护自己的部分。
这个优化在多轮对话、RAG场景下收益巨大。系统提示词动辄几百上千token,如果100个并发请求都共享,省下的显存非常可观。
5.5 路线五:滑动窗口与注意力稀疏化
如果业务允许,可以限制模型只关注最近N个token,更早的KV直接丢弃。这就是滑动窗口注意力。它把KV Cache的占用固定在一个上限,不再随对话轮次无限增长。代价是模型会“忘记”更早的内容。
另一种思路是稀疏化,只保留重要的KV,丢弃不重要的。但这类方法实现复杂,效果不稳定,生产环境用得相对少。
6. 实操排查:线上KV Cache问题的定位思路
6.1 先确认是不是KV Cache的问题
遇到延迟随对话增长而升高,先别急着改代码。按这个顺序排查:
- 打印每一轮的首字延迟和单token延迟,看是否随序列长度增长
- 检查显存占用是否随对话轮次上升
- 对比短序列和长序列的延迟差异
如果延迟和显存都随长度增长,基本可以锁定是KV Cache相关。
6.2 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 首字延迟高,后续正常 | 预填充阶段慢 | 检查输入长度、预填充批处理策略 |
| 每token延迟随轮次增长 | KV Cache读取量增大 | 检查序列长度、是否开启量化 |
| 显存够但分配失败 | 碎片化严重 | 检查是否启用分页管理 |
| 并发上不去 | KV Cache占用过高 | 检查KV头数、量化配置、前缀共享 |
| 长上下文质量下降 | 量化精度损失 | 对比FP16和量化后的输出 |
6.3 几个我踩过的坑
第一个坑:盲目开KV Cache量化。有次为了省显存,把所有请求都开了INT4量化,结果短对话场景延迟反而升高了,因为反量化开销超过了省下的带宽。后来改成只对超过一定长度的请求开量化,才平衡好。
第二个坑:忽略前缀共享。早期没做前缀共享,系统提示词每个请求都重算一遍KV,白白浪费了大量显存和计算。加上前缀共享之后,同样的硬件并发直接翻倍。
第三个坑:显存估算不留余量。按公式算出来刚好够,实际跑起来就OOM。因为除了KV Cache,还有激活值、临时缓冲区、框架自身开销。我的经验是至少留20%余量,长上下文场景留30%。
7. 几个容易被忽略的细节
7.1 批处理对KV Cache的影响
批处理能提升吞吐,但KV Cache占用是按batch_size线性增长的。batch开太大,KV Cache直接把显存吃光。所以批处理大小要和序列长度权衡。长序列场景下,小batch甚至单条处理反而更稳。
7.2 预填充和解码阶段的差异
预填充阶段是计算密集型,要处理整个输入序列;解码阶段是显存带宽密集型,每步只处理一个token但要读全部KV。这两个阶段的瓶颈完全不同,优化手段也不一样。很多框架会把它们分开调度,预填充优先保证吞吐,解码优先保证延迟。
7.3 会话结束要及时释放
KV Cache在会话期间一直占着显存。如果会话管理不当,用户关了页面但服务端没释放,显存就会慢慢泄漏。一定要有超时释放机制,我一般设5到10分钟无活动就回收。
7.4 监控要盯住KV Cache占用率
光看GPU显存总占用不够,要单独监控KV Cache的占用和block利用率。这样才能提前发现碎片化或者泄漏问题。我习惯把KV Cache占用率、block碎片率、平均序列长度放在同一个面板上,一眼就能看出异常。
8. 选型建议:不同场景下怎么权衡
8.1 短对话场景
序列长度普遍在2K以内,KV Cache压力不大。这时候优先保证延迟,可以不开量化,batch可以适当开大。重点优化预填充和调度效率。
8.2 长文档问答场景
输入长、输出短,预填充是瓶颈。重点优化预填充的批处理和并行度,KV Cache量化收益明显,前缀共享如果有相同文档也能用上。
8.3 多轮对话场景
序列随轮次增长,KV Cache持续累积。重点是控制单会话长度上限,超长时做摘要压缩或者滑动窗口。前缀共享对系统提示词效果显著。
8.4 高并发场景
显存是硬约束。优先选GQA/MQA模型,开KV Cache量化,启用分页管理和前缀共享。batch大小要精细调优,找到吞吐和延迟的平衡点。
提示:没有一套参数能通吃所有场景。我的做法是准备几套配置模板,按业务场景切换,而不是指望一个配置打天下。
9. 我个人在实际操作中的体会
KV Cache这个东西,刚接触的时候觉得就是个缓存,没什么大不了。真正做过推理服务之后才发现,它是整个长上下文成本结构的核心。你去看那些长上下文方案的成本报告,大头基本都在KV Cache上。模型权重是固定的,但KV Cache是随长度和并发动态膨胀的,这才是真正难控的部分。
我现在评估任何一个推理方案,第一件事就是问:KV Cache怎么管的?支不支持量化?有没有前缀共享?分页做没做?这几个问题的答案,基本决定了这个方案在生产环境能不能扛住。至于模型本身多大、跑分多高,反而是次要的。
还有一个体会是,长上下文不是越长越好。很多业务其实用不到128K,硬上长上下文只会让成本和延迟都失控。先把实际需要的上下文长度摸清楚,再针对性优化,比盲目追长要务实得多。我见过太多团队为了一个“支持128K”的标签,把推理成本推高了好几倍,结果业务上根本用不到那么长。
最后分享一个小技巧:如果你不确定KV Cache是不是瓶颈,可以做个简单实验——固定输入长度,只改变对话轮次,看延迟曲线。如果延迟随轮次明显上升,那KV Cache就是主要矛盾,优化它准没错。这个实验十分钟就能做完,比看一堆文档都管用。