3.1 对话历史压缩与 KV Cache 管理


文档摘要

3.1 对话历史压缩与 KV Cache 管理 我帮一个团队做成本审计时发现一个反直觉的现象:他们的"平均每请求 tokens"只有 800,但推理集群的 GPU 利用率却高得离谱,显存长期吃紧。深挖之后才找到原因——他们的产品有大量多轮长对话,第 10 轮的请求,上下文要带上前 9 轮的全部内容,单次请求的实际 prefill 长度高达 6000 tokens,但他们统计"每请求 tokens"时只算了新生成的部分。更糟的是,他们的推理服务是完全无状态的(1.3 反模式一),同一会话每轮都要重新 prefill 整个历史,KV Cache 一点没复用。结果是:看似 800 tokens 的请求,实际消耗了 6000 tokens 的 prefill 算力,而且这个算力被白白重复了 9 次。

3.1 对话历史压缩与 KV Cache 管理

我帮一个团队做成本审计时发现一个反直觉的现象:他们的"平均每请求 tokens"只有 800,但推理集群的 GPU 利用率却高得离谱,显存长期吃紧。深挖之后才找到原因——他们的产品有大量多轮长对话,第 10 轮的请求,上下文要带上前 9 轮的全部内容,单次请求的实际 prefill 长度高达 6000 tokens,但他们统计"每请求 tokens"时只算了新生成的部分。更糟的是,他们的推理服务是完全无状态的(1.3 反模式一),同一会话每轮都要重新 prefill 整个历史,KV Cache 一点没复用。结果是:看似 800 tokens 的请求,实际消耗了 6000 tokens 的 prefill 算力,而且这个算力被白白重复了 9 次。

这一节讲两件事:怎么把过长的对话历史压下来(省 prefill 算力),以及怎么让 KV Cache 在多请求间复用(省显存)。这两者是高并发大模型服务降低推理成本的基础功,不做的话,后面再多的优化都是空中楼阁。

上下文为什么会"膨胀"

先说清楚问题有多大。一次多轮对话,每一轮都要把完整历史作为上下文送给模型。第 N 轮的请求,上下文包含前 N-1 轮的所有内容。这导致三个连锁问题。

prefill 成本线性增长。上下文越长,每次 prefill(预填充)要算的 token 越多,算力消耗和 TTFT 都线性上升。第 1 轮 prefill 500 token,到第 10 轮可能要 prefill 5000 token,单从 prefill 看成本就涨了 10 倍。

KV Cache 占显存。Transformer 架构下,每个 token 在每一层都要存 K 和 V 两个向量,用于后续的注意力计算。上下文 8000 token、模型 80 层、每层 KV 向量维度 128、FP16 存储——粗算下来一个请求的 KV Cache 就要数百 MB。显存是 GPU 上最稀缺的资源,KV Cache 占太多,能并发的请求数就少,吞吐就上不去。

成本失控。长对话的单请求成本可能是短对话的几十倍。如果一个平台有 30% 的长对话流量,这 30% 可能贡献了 70% 以上的总推理成本。不治理长上下文,成本结构永远不可能健康。

对话历史压缩的三种策略

滑动窗口(Sliding Window)

最简单的策略:只保留最近 N 轮对话,更早的丢弃。实现成本极低,立竿见影地把上下文长度卡在一个上限。但代价是丢失早期上下文——如果用户在第 1 轮设定了偏好("我喜欢简洁的回答"),到第 10 轮这个设定就没了,模型行为会"漂移"。

滑动窗口适合上下文相关性集中在近期的场景,比如闲聊、简单问答。但对需要长期记忆的场景(客服咨询、长任务协作),单纯丢弃早期信息是不能接受的。

摘要压缩(Summarization)

用模型把早期对话摘要成一段简短文字,替换原始长历史。比如前 8 轮一共 4000 token 的对话,摘要成 300 token 的"用户咨询了退款政策,已告知 7 天无理由,用户表示商品已拆封,正在协商折价退款"。这样既保留了关键信息,又大幅缩短了上下文。

摘要压缩是生产里用得最多的策略,因为它在信息保留和压缩率之间取得了最好的平衡。代价是摘要本身要一次额外推理(让模型生成摘要),但这次推理是低频触发的——比如每 5 轮触发一次摘要,摊到每轮的成本微乎其微。

实现摘要压缩有几个细节要注意。摘要的触发时机,通常按 token 数阈值触发(上下文超过 2000 token 就摘要),而不是按轮次,因为轮次长短差异大。摘要的质量校验,摘要可能丢失关键信息或引入错误,生产里通常会保留原始历史的备份(存到外部状态库),摘要只用于上下文,必要时可以"回溯"原文。摘要的 prompt 设计,要让模型摘要出"事实和决策"而非"对话流程"——"用户问了 A,得到答案 B"是有用的摘要,"用户和客服进行了愉快的交流"是废话摘要。

结构化抽取

把对话中的关键信息抽取成结构化字段,存到外部状态库,不进上下文。比如把"用户姓名=张三,订单号=12345,诉求=退款"存成键值对,每次推理时只在系统提示词里注入这些字段,而不带完整对话历史。

结构化抽取适合信息高度结构化的场景,如表单填写、工单流转、电商售后。这些场景的"上下文"本质上是少数几个关键字段,把它们抽出来比带着整个对话历史高效得多。

生产中最常见的做法是组合使用这三种策略:近期对话用滑动窗口保留原文(保细节),早期对话用摘要压缩(保要点),关键信息抽取到外部状态库(保结构化数据)。这样既控制了上下文长度,又最大限度地保留了必要信息。组合策略的调参空间很大,需要根据具体业务的上下文特征来定制。

KV Cache 管理:省显存的杠杆

对话历史压缩解决的是"prefill 算力"问题,KV Cache 管理解决的是"显存"问题。KV Cache 是推理显存占用的大头(vLLM 教程里有详细的显存公式),百万级场景下,KV Cache 管理直接决定成本。

跨请求复用(Prefix Caching)

同一会话的多轮请求,前缀是共享的——系统提示词和早期历史在每轮里都一样。如果每轮都从头重新算 prefill,是巨大浪费。Prefix Caching 让共享前缀的 KV Cache 在请求间复用,省掉重复 prefill。

这要求会话亲和路由(见 1.3 反模式一)——请求要落到持有其 KV Cache 的节点。如果请求被打到另一台没有缓存它的节点,前缀复用就无从谈起。这也是为什么我们在第一章反复强调"会话亲和"——它不仅影响状态正确性,还直接影响 KV Cache 复用这个降本杠杆能不能生效。

vLLM 等现代推理框架已经原生支持自动 prefix 复用,会自动检测请求间的公共前缀并复用其 KV Cache。但要让它真正发挥作用,上层路由必须配合——把同一会话路由到同一节点,否则框架再智能也复用不了跨节点的缓存。

KV Cache 淘汰策略

显存有限,不能无限缓存所有会话的 KV。一个百万级平台同时有数百万活跃会话,每个会话的 KV Cache 都不小,全缓存下来显存根本不够。必须有淘汰策略。

LRU(最近最少使用)淘汰最久没活动的会话 KV,简单有效,是默认选择。基于 TTL 的策略,超时未活动的会话 KV 主动释放,适合"会话活跃度有明确时间边界"的场景(如客服会话通常 30 分钟内结束)。优先级策略,付费用户的 KV 保留更久,免费用户的更快淘汰——这是商业逻辑在技术上的体现,高价值用户的体验优先保障。

淘汰策略的调优关键在于"命中率"——被淘汰后又马上被请求的 KV(缓存抖动)是浪费。要监控 KV Cache 的命中率,低了说明淘汰太激进,高了说明显存还有余量可以多缓存。

KV Cache 卸载(Offload)

把不活跃会话的 KV Cache 从 GPU 显存卸载到 CPU 内存或 SSD,活跃时再加载回来。这是用时间换空间——卸载和加载有开销,但能显著降低显存压力,让有限的 GPU 显存服务更多会话。

卸载适合"长尾会话"场景——大量会话是低活跃的(偶尔发一条消息),但需要保持上下文。把它们的 KV 卸载到便宜的 CPU 内存,GPU 显存只服务活跃会话,整体成本下降。代价是低活跃会话再次请求时,要等 KV 从 CPU 加载回 GPU,TTFT 会略高。这个权衡在生产里通常是可以接受的——低活跃用户本身对延迟不那么敏感。

这一节的关键结论

上下文压缩和 KV Cache 管理是降低长对话成本的两条腿。压缩策略方面,滑动窗口最简单但丢信息,摘要压缩是生产主流,结构化抽取适合特定场景,三者常组合使用。KV Cache 管理方面,prefix 复用省 prefill 算力(依赖会话亲和路由),淘汰策略控显存占用,卸载换空间。

两者结合,能大幅降低长对话的 prefill 算力和显存成本。回到开头那个案例,我们给那个团队做了两件事:给推理服务加上会话亲和路由(让 KV Cache 能复用),给长对话加上摘要压缩(控制上下文长度)。结果是单请求平均 prefill tokens 从虚高的 6000 降到 1500 左右,GPU 利用率回落到健康区间,显存压力消失,整体推理成本降了 60% 以上。这就是上下文治理的威力。

下一节 3.2 讲降本的另一个杠杆——语义缓存,它能把"问法不同但语义等价"的请求导向历史响应,进一步砍掉一两成的推理算力。


作者与出处
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 会发光的石头的小龙虾 转发
评论区 (0)
U