跳转到正文
Silasye 的博客
返回

我对大模型 token 缓存理解

Table Of Content

缓存

本文仅我自己的理解撰写的,对底层的具体技术细节、参数并不透彻,所以不一定对,但是我认为并不影响非模型研究人员理解模型缓存这个概念。如果有幸被你看见本文,希望对你有帮助。

作为后端开发,对于“缓存”这个词并不陌生,总之本质上就是做处理的加速,对于相同的输入,在一定时间内,给出相同的输出。大模型的缓存,本质上也是在做处理的加速,只是大模型的 kv 缓存和后端常规意义上的缓存有一些偏差。

为什么模型服务需要 kv 缓存?

我们要怎么理解模型的缓存?

我个人理解,现代的大模型,不论机构如何优化,从底层看都是输入一个向量,经过模型权重计算,然后输出一个向量。从计算的角度看,相同的输入,应有相同的输出。当然实际上在调用的时候会发现并不是这样,这个可能和模型的 temperature、topK 等参数有关,但不是本文关心的重点。

基于此,那么在模型 api 的服务里面就可以针对输入向量 k,把它的输出向量 v 给存起来,下次再有输入向量 k,就可以直接从缓存中获取到输出 v,而不是再次把 k 过一遍模型权重的计算。

举个例子:比如我先问你 1+2+3+…+100 等于多少,第一次你会计算 (1+100) * 50 = 5050,所以你回答 5050,我紧接着再问你 1+2+3+…+100 等于多少,你不会重新去算,因为刚刚已经算过了,你还记得等于 5050,所以你会直接回答 5050,而且比第一次回答得更快。这里第二次询问的时候,少的这一步计算,就是缓存带来的效果,通俗的说,就是节省算力,响应还快。

实际怎么用的呢?

可以从日常使用 AI 的场景来看。

平时和 AI 对话的时候,会发现多轮对话后,它还记得你最开始问了什么问题,就好像和人一样有记忆。最简单粗暴的实现方式其实就是,在同一个对话框里面发多条消息给 AI,其实每次发消息都会把之前你问的问题,以及模型回答的内容按顺序一起发给 AI,AI 自然就知道之前聊过了什么话题,实际上确实很多 Agent 都是这么干的。

这里需要思考的是,每次发消息,历史消息其实是不可能变化的,只有刚新发的消息才是 AI 新的内容,就像你不可能改变昨天发生的事一样,但是今天会发生什么,你还能控制。

昨天的事已经发生,事件的结果已经固定了,今天是按昨天的结果继续发展,对应 AI 的处理其实是一样的,你之前发的 3 个问题,AI 已经解答了,你发出第四个问题的时候,AI 不会再去重新处理你前面的三个问题,而是根据前面三个问题已有的结论,结合你问的第四个问题,做出新的回答。

再深入一层,就是 token,你发出的每条消息,AI 回复的每条消息,都会被程序拆分成很多个 token,AI 处理信息是从前往后一个 token 一个 token 处理的,对于你之前已经发过的消息,每条消息都有自己的 token 序列(对于相同一句话,会拆分成哪些 token,各个 token 之间的顺序,都是固定的),在交给 AI 做计算之前,就会发现,这个 token 序列,我之前处理过相同的呀,那就算了,把之前的结果直接拿出来用就行了,也就是说,到这里可以简单理解为 kv 缓存,是存的一个 token 序列对应的处理结果,这里的 token 序列和处理结果,在模型层面,其实都是高维向量。

但是还不够,还有问题:那就是 token 序列的问题,如果有两个序列分别是 AAABBB、CCAAABBB,序列有很大部分的重合,从语义上看,这两次可能根本就不是一个问题,比如“想吃饭”和“不想吃饭”,就有重合,但是表达的就不是一个意思。

如果像这种情况,在处理的时候也认为 CCAAABBB 的后面部分能命中 AAABBB 的缓存,那就有大问题,就纯粹是“你说大猪蹄子,我说胯骨肘子”了,所以实际的缓存处理是按前缀来的,也就是说,CCAAABBB 处理的时候,发现已有的缓存是 A 开头的,而自己是 C 开头的,那就直接不走缓存了,而是直接做模型权重计算,做一次完整的推理,得到回答。

那如果两个序列分别是 AAABBB、AAABCB 呢?

还是按照前缀缓存来看,第二个 token 序列只能命中 AAAB 这一部分的缓存,剩下的 CB 这两个 token 还是得拿着 AAAB 的结果继续做权重计算,做推理得出结果。

这就是前缀缓存。

命中缓存价格很低,所以缓存真的就相当于不要钱了吗?

先说结论,不是的。

在一个会话里面,上下文会随着对话轮次不断上涨,每次发消息给 AI,历史消息都会跟着一起发出去,这次请求中,历史消息这部分内容会命中缓存,你新发的消息内容不会命中缓存。

比如命中缓存是 0.01 元/每百万 token,未命中缓存是 0.1 元/每百万 token,输出是 2 元/每百万 token,现在上下文是 300000,你发了一条新的消息,新消息做 token 解析后,占用 1000 个 token,最后模型回答的内容是 1000 token,也就是模型输出 1000 token,那本次对话的花费是:0.01 * 300000/1000000 + 0.1 * 1000/1000000 + 2 * 10000/1000000 = 0.003 + 0.0001 + 0.002 = 0.0051 元。

很便宜吧?但是你会发现两个问题:

  1. 命中缓存的部分,花费其实是大头,占用超过了 50%
  2. 这只是一次对话,随着对话轮次变多,上下文的大小不会保持 300000 不变,而是会一直上涨,每轮对话里面,缓存的费用支持比例会越来越高,直到触发上下文压缩。

我想说的是,缓存虽然比较低价,但是并不代表不要钱,对话轮次多了,缓存的费用反而会越来越高。

但并不代表缓存这个东西没意义,因为你要是把上下文那 30 万按每命中缓存算,那直接贵了 10 倍,很多厂商可能还不止十倍。

这里还有一个误区,那就是对话轮次并不是按你发了几次消息来算的,比如你直接问豆包 “今天北京天气如何?”,实际的处理流程可能是这样的:

  1. 第一轮对话,用户问:”今天北京天气如何?“
  2. 第一轮对话,豆包大模型回答:”需要调用中国天气网接口查询才知道,查询参数是 城市=北京“。第一轮对话结束
  3. 豆包的服务器调用中国天气网接口,参数是 城市=北京,拿到北京的天气是”阴转多云“
  4. 第二轮对话,豆包的服务器将”北京今天阴转多云“发给豆包大模型,同时将第一轮的问题和回答一起发给豆包大模型
  5. 第二轮对话,豆包大模型回答:”今天北京天气还不错哦,是阴转多云“,第二轮对话结束。

最后你就会看到豆包回答:今天北京天气还不错哦,是阴转多云

会发现,虽然你只问了一次问题,但是实际的对话轮次其实已经过了两轮,实际的使用场景下,可能你问一个问题,得到答案之前,其实已经过了很多轮了,中间的工具调用、skill 调用、mcp 调用都要算轮次的。特别是在写代码的场景下,搞不好一个问题就已经一两百轮对话过去了,上下文可能也已经飙到了二三十万。所以才会出现,问一个问题就直接消耗了几千万 token 的情况,实际的上下文长度命名才二三十万,这是因为这二三十万 token 长度的上下文,可能已经被发给大模型几百次了,token 消耗自然就到了千万级别。

缓存命中率

缓存命中率是怎么算的?

当初 deepseek v4 发布之后,很多人突然就开始关心起缓存命中率。

还是以前面的例子来算,其实就是把每次请求命中缓存的 token 数和每命中缓存的 token 数各自加起来,然后算个比例,就是缓存命中率,这是简单的加减乘除计算。

为什么有时候某一轮对话的缓存命中率会突然变低,然后又升高?

这里说的是某一轮,现在大部分 Agent 展示的都是整体的缓存命中率,而不会展示某一轮的命中率,所以如果不专门做监控的话,其实是感知不到这个问题的,但是你会发现某一轮对话的花费突然明显升高了。。。

我目前知道的原因有两个:

  1. 缓存过期了。缓存是有时间的,厂商不会一直把缓存留着,比如厂商缓存 5 分钟,要是 5 分钟内没有新的轮次,缓存就会被删掉,你再继续对话的话,因为缓存被删了,所以所有输出 token 都得重新计算,所以缓存命中率就会极低
  2. 历史消息被改了。虽然前面说过历史消息不会发生变更,但是架不住有些 Agent 本身实现得比较离谱,会改历史消息,比如会把较早轮次的工具调用结果压缩替换,前缀缓存自然就命中不了了,需要重新推理计算的 token 数量自然就变多了,命中率就会降低。

总结

  1. 缓存,是为了降低算力消息,同时也提升模型推理的效率
  2. 缓存是按前缀匹配的,也就是常说的前缀缓存。
  3. 缓存虽然低价,但是上下文大了之后,缓存命中的量大,缓存的花费将是巨量的。
  4. 使用哪缓存命中率比较高的 Agent,更省钱
  5. 如果问题都比较复杂,一个会话尽量只解决一个问题,新的问题开新的会话,更省钱。
  6. 如果一个问题复杂到把上下文撑到很大,最好是让 AI 总结一下问题和关键结论,写一个交接文档,然后新开会话,拿着交接文档继续讨论,因为上下文过大,不仅很费钱,AI 的注意力还会分散,幻觉问题会更严重。

分享这篇文章:

下一篇
MCP