KV Cache:大模型推理里真正烧钱的“油箱”
你问 AI 一句话,它像打字员一样,一个字一个字往外吐。
看起来,最贵的环节肯定是输出。
API 价目表也很配合:输出 Token 通常比输入 Token 贵不少。于是很多人得出结论:模型生成内容贵,贵在“写字”。
这个结论只对了一半。
站在推理系统的角度,真正让人睡不着的,往往是 KV Cache。
它像大模型推理时随身背着的记忆包。没有它,模型每生成一个新 Token,都得把前面整段对话重新读一遍。那画面很离谱:你每问一句,客服都把你们从第一句话开始的聊天记录重新朗读一遍,再决定怎么回。
KV Cache 解决了重复计算的问题,也带来了新的账单:显存、显存带宽、调度复杂度,以及长上下文带来的容量压力。
先把一个误会掰正:输出贵,不等于只有输出在花钱
一次大模型请求,大致分成两个阶段:
- Prefill(预填充):模型读取你的 Prompt、历史消息、RAG 检索资料、系统指令。
- Decode(解码生成):模型每次生成一个 Token,再接着生成下一个。
假设你给模型塞了 20,000 Token 的资料,要求它写 500 Token 的摘要。
在 Prefill 阶段,模型会并行处理那 20,000 Token。计算量很大,但 GPU 擅长这种大批量并行活。
进入 Decode 阶段后,事情变味了。
模型生成第 1 个 Token,要参考前面 20,000 Token 的上下文;生成第 2 个 Token,要参考前文加上第 1 个输出;后面每多吐一个 Token,都要读取越来越长的历史状态。
这里的瓶颈常常不再是纯计算,而是:
GPU 得不断从显存里搬运 KV Cache。
所以输出阶段慢、贵,背后不只是“生成要算很多层网络”,还有一个很硬核的原因:每一步都要读记忆。
KV Cache 到底缓存了什么?
Transformer 的注意力机制里,每个 Token 都会生成几组中间结果,其中最关键的是:
- K(Key,键)
- V(Value,值)
当模型准备生成下一个 Token 时,它需要拿当前状态去查询前面所有 Token 的 K 和 V,判断哪些内容更相关。
如果不缓存,模型每次生成新 Token,都要把历史 Token 的 K、V 重新算一遍。
这纯属重复劳动。
KV Cache 的做法很直接:
- 用户输入的内容计算一次 K、V;
- 结果留在显存里;
- 新 Token 生成时,直接复用历史 K、V;
- 新 Token 自己的 K、V 再追加进缓存。
于是,模型不必反复重算历史上下文。
代价也很直接:上下文越长,KV Cache 越大。并发越高,显存越紧。
为什么有人说:KV Cache 是 AI 时代的汽油?
因为模型跑起来以后,KV Cache 几乎是持续消耗的基础资源。
你可以把 GPU 显存想成一家餐厅的座位。
- 模型权重,是厨房设备。设备得常驻,搬不走。
- KV Cache,是每桌客人的餐盘和食材。
- 并发请求,是同时进店的顾客。
- 上下文长度,是每桌摊开的菜品数量。
厨房再豪华,桌子被占满了,新客人也进不来。
这就是很多推理服务的真实处境:GPU 算力看着还有余量,显存却已经满了。系统只能排队、拒绝请求,或者把部分缓存挪到 CPU 内存里——速度立刻掉一截,用户开始盯着转圈圈发呆。
对服务商而言,KV Cache 直接影响:
- 一张 GPU 能同时服务多少用户;
- 能支持多长的上下文窗口;
- 首字响应时间有多快;
- 每百万 Token 背后的实际硬件成本;
- 高峰期会不会排队到让人想砸键盘。
API 定价里的“输入贵不贵、输出贵不贵”,只是你看得到的表面。
推理集群真正要算的账,藏在显存利用率、缓存命中率和请求调度里。
一个直观例子:为什么长对话特别容易贵
假设你在做一个 AI 客服。
每次请求都带上:
- 2,000 Token 系统提示词
- 8,000 Token 商品知识库
- 5,000 Token 历史对话
- 500 Token 用户新问题
一轮请求刚开始,模型就要处理 15,500 Token 左右的上下文。
如果用户只问一句“这个能退货吗?”,模型最终可能只输出 80 Token。
从用户视角看:回答很短。
从推理系统视角看:这 80 Token 是站在 15,500 Token 的历史上生成的。每生成一步,都得读取那份长长的 KV Cache。
更麻烦的是,10,000 个用户同时这么问,系统需要保存 10,000 份会话状态。
这时候,真正炸掉的通常不是“模型不会回答”,而是显存。
KV Cache 和 Prompt Cache,别混成一锅粥
这两个名字很像,作用却不在一个层面。
KV Cache:单次生成过程中的上下文记忆
它主要服务于当前请求或当前会话。
模型生成第 50 个 Token 时,要用前 49 个输出和全部输入的注意力状态。KV Cache 就是这份状态。
没有它,逐 Token 生成会慢得难以接受。
Prompt Cache:复用重复前缀,少做一次 Prefill
很多请求有相同开头,比如:
系统提示词
+ 产品说明书
+ 输出格式要求
+ 用户问题
其中“系统提示词 + 产品说明书 + 输出格式要求”可能每天被重复发送几十万次。
如果平台支持前缀缓存,这段固定内容就能被复用。后续请求只需要计算变化的那部分,比如用户问题。
你会得到两类收益:
- 输入成本下降:重复的长前缀不必每次都完整计算;
- 首字更快:少做一大段 Prefill,模型更快开始回答。
注意一个关键细节:
缓存通常要求前缀完全一致,连顺序和空格都可能影响命中。
你今天把系统提示词里的两段说明调了位置,明天又在开头插入当前时间,缓存命中率可能瞬间掉得很难看。
想省钱、提速度,Prompt 应该怎么写?
别急着压缩每一句文案。很多时候,优化结构比删几个字更值钱。
1. 把稳定内容放在 Prompt 最前面
推荐结构:
[固定系统规则]
[固定角色设定]
[固定知识库或产品资料]
[固定输出格式]
[本次用户问题]
[本次动态变量]
不推荐这样写:
当前时间:2025-xx-xx xx:xx
用户 ID:xxx
用户问题:xxx
系统规则:...
产品资料:...
动态信息放在前面,会让后面的固定内容无法作为稳定前缀复用。
把不变的内容顶到最前面,把每次变化的内容压到后面。这是前缀缓存最实用的一条规则。
2. 不要把整本知识库塞进每一次对话
很多 RAG 项目有个经典操作:用户问“退款流程”,系统把 50 篇文档、30,000 Token 材料一股脑塞进去。
模型当然能读,GPU 也会默默发热。
更合适的做法:
- 先检索,再重排;
- 每次只给最相关的 3~8 段内容;
- 文档切块别太碎,也别大到一块塞满半页屏幕;
- 给每段资料加标题、来源和编号,方便模型引用;
- 对重复资料做去重。
目标不是追求“上下文越长越安全”。
目标是让模型拿到足够回答问题的证据。
3. 给历史对话设预算
聊天机器人最容易出现“越聊越贵”。
用户聊了 80 轮,你还把全部记录原封不动带上。模型可能记住了三周前的一句玩笑,却把用户刚说的预算范围埋在角落里。
一套好用的策略:
- 保留最近 6~12 轮原始对话;
- 更早的对话压缩成一段结构化摘要;
- 用户偏好单独存储,例如语言、预算、地区、禁忌;
- 任务状态单独存储,例如订单号、当前步骤、已确认信息;
- 每轮只带回当前任务需要的字段。
示例:
{
"user_profile": {
"language": "中文",
"budget": "500元以内",
"preference": "不接受二手"
},
"task_state": {
"intent": "查询退货政策",
"order_id": "A1024",
"status": "等待客服确认"
},
"conversation_summary": "用户购买耳机后发现左耳无声,已上传订单号,想确认是否支持上门取件。"
}
这比带着几十轮闲聊进模型靠谱得多。
4. 给输出长度设上限
输出越长,Decode 越久,KV Cache 也会继续增长。
如果你的业务只需要一句分类结果,就别允许模型写一篇散文。
可以直接约束:
只输出 JSON。
reason 字段不超过 30 个字。
不得解释推理过程。
API 参数里也要配合设置:
max_tokens/max_output_tokens- 停止词
stop - JSON Schema 或结构化输出
短输出不只是省 Token 钱。
它能减少等待时间,也能避免模型越说越兴奋,顺手写出三段免责声明。
工程侧怎么扛住 KV Cache 压力?
如果你在做自部署推理服务,下面这些词迟早会遇到。
Paged Attention:把缓存按“页”管理
传统做法里,每个请求的 KV Cache 往往需要一段连续显存。请求长度不一,就很容易留下碎片。
Paged Attention 把 KV Cache 切成固定大小的小块,像操作系统分页一样管理。
好处很实在:
- 显存碎片更少;
- 不同长度的请求更容易混合调度;
- 并发容量更高;
- 长请求和短请求不至于互相卡死。
vLLM 之类的推理框架之所以受欢迎,这就是核心原因之一。
Continuous Batching:别傻等一整批请求结束
传统批处理像发班车:凑齐一批人,一起出发,一起结束。
大模型请求长度差异很大。有的人只要 20 Token,有的人一开口就让模型写 2,000 Token。整批绑在一起跑,短请求会被长请求拖住。
连续批处理会在生成过程中动态插入新请求、移除已完成请求。
效果是:GPU 少发呆,吞吐量更高,用户排队时间更短。
KV Cache Quantization:用更低精度保存缓存
KV Cache 常见精度是 FP16 或 BF16。精度降低到 FP8、INT8 后,显存占用能明显下降。
代价是可能损失一点输出质量,尤其在超长上下文、复杂推理或特定模型上,需要压测。
别一上来就全量切换。建议拿你自己的真实数据集对比:
- 准确率有没有掉;
- 幻觉率有没有升;
- 首字延迟有没有改善;
- 每张卡的并发数涨了多少;
- 单请求成本降了多少。
跑完再决定,别只看别人贴出来的 benchmark 图。
Prefix Cache:让重复系统提示词少占一次计算
多租户客服、代码助手、企业知识库问答,都特别适合前缀缓存。
前提是你得管住 Prompt 模板。
建议把模板版本化:
prompt_version: support_v3
一旦改了系统提示词,明确知道缓存会在哪个版本失效。别让运营同学偷偷加一句“请用温暖亲切的语气”,然后你们盯着成本曲线怀疑人生。
常见坑:很多团队钱就是这么烧掉的
坑 1:以为输入便宜,就无限塞上下文
输入单价低,不代表输入没有成本。
长输入会拖慢 Prefill,也会生成更大的 KV Cache。用户量一上来,显存和排队都会教你做人。
坑 2:每轮都把动态变量放在 Prompt 开头
时间戳、随机 UUID、用户 ID、临时追踪字段,都会破坏前缀缓存。
把它们放到尾部。固定前缀要尽量稳定。
坑 3:把“缓存命中”理解成永久记忆
缓存可能有 TTL,可能被容量淘汰,也可能因为实例切换而失效。
不要把业务正确性建立在“缓存一定在”这件事上。
缓存是加速器,不是数据库。
坑 4:只盯 Token 单价,不看延迟分位数
平均响应 1 秒,不代表体验好。
高峰期有 5% 用户等了 12 秒,他们会直接觉得产品卡死。
监控至少要看:
- TTFT:首个 Token 返回时间;
- TPOT:每个输出 Token 的平均耗时;
- P50、P95、P99 延迟;
- 输入 Token、输出 Token;
- KV Cache 使用率;
- 前缀缓存命中率;
- 请求排队时间;
- OOM 次数。
坑 5:为了省显存,盲目把缓存卸载到 CPU
CPU Offload 能救急,也能把速度拖进泥潭。
如果用户期待实时对话,频繁在 CPU 内存和 GPU 显存之间搬 KV Cache,延迟会非常难看。
这招适合低频、长任务、能接受慢一点的场景。别拿去扛高并发客服。
一套能直接执行的优化清单 ✅
如果你正在做 AI 应用,照着这个顺序查:
- 统计真实请求的输入和输出 Token 分布。别只看平均值,重点盯 P95 长度。
- 拆开固定内容和动态内容。固定规则、固定资料放前面;用户变量放末尾。
- 验证前缀缓存是否命中。同一模板连续请求,检查平台返回的缓存 Token 指标。
- 给输出设置合理上限。分类、抽取、路由任务尤其要严格。
- 压缩历史会话。近期原文 + 结构化摘要,比全量聊天记录更划算。
- 收紧 RAG 检索结果。按相关性给资料,不要把检索当文档搬家。
- 做压测。模拟短请求、长请求、突发并发混合到一块的真实场景。
- 监控 KV Cache 和显存利用率。GPU 利用率高不高,不等于服务跑得健康。
结语:别只盯着“输出贵”
输出 Token 的价格高,是你在账单上能直接看到的数字。
KV Cache 的影响更隐蔽。它藏在首字延迟里,藏在长上下文的显存占用里,也藏在高峰期突然排起的请求队列里。
做 AI 应用,Prompt 不是写完就完了。它还是一份资源配置文件。
少塞一点无关上下文,把稳定前缀排好,控制无意义的长输出,做好会话摘要和检索过滤。这样做,通常比纠结“要不要少写一句提示词”有用得多。
你的 GPU 会轻松一点,你的用户也能少等几秒。这个差别,真能决定产品看起来像个玩具,还是像个能上班干活的工具。