粗浅理解大模型 KV Cache
我们先来了解一下大模型对于 Token 的处理流程(学习过 RAG 开发的应该大概知道这个处理流程):
Token
│
▼
Embedding // Token向量化
│
▼
Layer1 // 不断加工向量
│
▼
Layer2
│
▼
...
│
▼
LayerN // N次加工向量本文章由于篇幅原因,就不在此展开讲解处理流程相关内容,具体细节可以看:大模型处理Token流程
KV Cache是什么?作用又是什么?
KV Cache(Key-Value Cache)是大语言模型在推理过程中使用的一种缓存机制。它会缓存历史 Token 在注意力(Attention)计算中产生的 Key 和 Value 向量,使模型生成新 Token 时无需重复计算历史内容,从而显著提升推理效率。
假设用户输入一句话:
介绍一下 Go 协程经过 Tokenizer 后,会变成多个 Token(假设状态):
[介绍][一下][Go][协程]进入 Transformer 后,每一层都会计算三组向量:
$$ Q=XW_Q $$
其中:
- Query(Q):当前 Token 用来"查询"其它 Token。
- Key(K):表示每个 Token 的特征,用来和 Query 计算相关性。
- Value(V):真正携带语义信息,被 Attention 加权后输出。
Attention 的计算公式为:
$$ Attention(Q,K,V)=softmax\left(\frac{QK^T}{\sqrt d}\right)V $$
可以发现:
当前 Token 只需要自己的 Query,但需要所有历史 Token 的 Key 和 Value。
这也是 KV Cache 存在的根本原因。
没有 KV Cache 会发生什么?
假设模型已经生成了:
Go 语言是一门现在准备生成下一个 Token。
如果没有 KV Cache,模型实际上需要重新计算整个上下文:
Go
语言
是
一门所有 Token 都要重新经过:
Embedding
↓
Layer1
↓
Layer2
↓
...
↓
LayerN每一层都会重新计算:
Q
K
V即使前面的 Token 已经计算过很多次,它们仍然会被重新计算。
这意味着:每生成一个 Token,整个上下文都会重新 Forward 一遍。
随着上下文越来越长,这种重复计算会迅速成为推理瓶颈。
KV Cache 缓存了什么?
KV Cache 的核心思想非常简单:
历史 Token 的 Key 和 Value 一旦计算完成,在后续推理过程中就不会发生变化。
例如:
Go
语言
是已经完成计算:
Go -> K1 V1
语言 -> K2 V2
是 -> K3 V3这些结果会直接存入 KV Cache。
下一步生成:一门 。
模型只需要计算:
Q4
K4
V4然后把:
K4
V4追加到缓存中。
历史 Token 的:
K1
V1
K2
V2
K3
V3全部直接读取,不需要重新计算。
因此,KV Cache 会随着模型不断生成 Token 而持续增长。
为什么不缓存 Query?
Query 只有一个作用:查询历史。
例如生成第 100 个 Token 时:
Q100会去查询:
K1 ~ K99得到 Attention 权重。
下一步生成:
Q101上一轮的:
Q100已经不会再参与任何计算。
因此,Query 是一次性的。
而:
K1
V1却会一直被未来所有 Token 使用。
所以真正值得缓存的是:
Key
Value而不是 Query。
KV Cache 并不是缓存一句话
很多人第一次接触 KV Cache 时,会误以为:"缓存的是整个上下文"
其实更加准确的说法应该是:
缓存的是每一层、每一个 Token 的 Key 和 Value。
例如一个拥有 32 层的 Transformer:
Layer1
Token1 KV
Token2 KV
Token3 KV
Layer2
Token1 KV
Token2 KV
Token3 KV
...
Layer32因此:
一个 Token 会对应 32 层的 KV。
这也是为什么 上下文 (Context) 越长,显存占用增长得非常快。
Prefill 和 Decode
现代 LLM 的推理通常可以分为两个阶段:Prefill(预填充) 和 Decode(逐 Token 生成)。
第一阶段:Prefill
假设用户输入:
介绍一下 Go 协程此时,Prompt 中的所有 Token 都已经确定,因此模型可以先对整个 Prompt 进行一次完整的前向传播(Forward Pass)。
整个过程可以理解为:
Prompt
│
▼
Embedding
│
▼
Transformer Layer1
│
▼
Transformer Layer2
│
▼
...
│
▼
Transformer LayerN
│
▼
生成所有 Layer 的 KV Cache
│
▼
计算最后一个 Token 的 Logits在这个过程中,每个 Transformer Layer 都会计算所有 Token 的 Key(K)和 Value(V),并将它们保存到 KV Cache 中,供后续生成阶段重复使用。
Prefill 并不会直接结束推理,它会计算完整个 Prompt,并最终得到最后一个位置对应的输出概率分布(Logits)。模型经过采样得到第一个输出 Token 后,才会进入逐 Token 的 Decode 阶段。
为什么 Prefill 的 GPU 利用率很高?
这是因为 Prompt 中的所有 Token 都已经已知。
因此,在每一层 Transformer 内部,所有 Token 都可以同时参与 Attention 和 FFN(Feed Forward Network)的计算,大量矩阵运算能够一次性交给 GPU 并行完成。
需要注意的是:
- Layer 与 Layer 之间仍然需要串行执行,因为后一层需要依赖前一层的输出;
- 但每一层内部所有 Token 可以并行计算,因此 GPU 能够充分发挥并行计算能力。
也正因为如此,Prefill 往往拥有整个推理过程中最高的 GPU 利用率。
Prefill 的产物是什么?
很多初学者容易认为 Prefill 的作用是"生成回答",实际上并不是。
Prefill 真正的产物是 KV Cache。
整个流程可以理解为:
Prompt
│
▼
Prefill
│
▼
生成所有 Layer 的 KV Cache
│
▼
计算最后一个位置的 Logits
│
▼
采样得到第一个输出 Token随后进入 Decode 阶段,每生成一个新的 Token,都可以直接复用已经缓存好的 KV,而无需重新计算整个 Prompt。
因此可以把 Prefill 理解成:
模型先完整"阅读并理解"整个 Prompt,为后续生成建立好上下文状态。
每次都会重新 Prefill 吗?
不一定。
对于一次全新的请求,例如:
System Prompt
User Prompt由于模型没有任何历史 KV Cache,因此需要对整个 Prompt 执行一次完整的 Prefill。
System
│
User
│
Forward Pass
│
KV Cache而在同一次连续生成过程中,如果只是继续输出新的 Token,则无需再次执行 Prefill,而是直接进入 Decode,持续复用已有的 KV Cache。
不过,对于现代 Agent 来说,情况会更加复杂。
许多 Agent 在完成一个阶段性的任务后,并不会简单地继续追加上下文,而是会重新整理当前任务所需的信息,例如:
- 删除已经无关的历史内容;
- 将长对话压缩成摘要(Summary);
- 将工具调用结果提炼为状态(State);
- 保留当前任务真正需要的信息。
这样得到的是一个重新组织后的 Prompt。
由于 Prompt 本身已经发生了变化,因此模型通常会重新执行一次 Prefill,为新的上下文重新建立 KV Cache,然后再继续后续推理。
也就是说,现代 Agent 越来越倾向于:
完成 Task1
│
整理有效信息
│
构建新的 Prompt
│
重新 Prefill
│
继续 Task2这也是近年来 Context Engineering(上下文工程) 的重要思想之一。
它并不是为了放弃 KV Cache,而是在重新组织上下文质量与重新计算 Prefill 成本之间寻找更好的平衡。
第二阶段 Decode
随后模型开始逐 Token 生成内容。
例如:
Go
↓
语言
↓
中
↓
...每生成一个 Token:
都会:
计算新的 Q
↓
计算新的 K
↓
计算新的 V
↓
追加到 Cache因此 KV Cache 会不断增长。
这里需要注意:模型自己生成的 Token,也会成为后续生成的上下文(只适用于同一次 Decode)。
这也是 Transformer 被称为 Autoregressive(自回归)模型 的原因。
为什么 Context Window 越长越慢?
现在很多模型支持:
- 128K
- 200K
- 甚至 1M Context
很多人认为:"上下文越长,只是缓存多一点而已"
事实上,问题没有这么简单。假设已经拥有:
100000 TokenKV Cache 中已经保存了:
100000 Token × 32 Layer × Key × Value当模型生成下一个 Token 时:
虽然不用重新计算历史 Token。
但是,Attention 仍然需要读取:
Q_new × 100000 个历史 Key然后再根据权重读取:
100000 个历史 Value因此:上下文越长,每生成一个 Token,需要读取的 KV 就越多。
真正的瓶颈逐渐从 计算 → 显存带宽(Memory Bandwidth)
超长上下文效果会下降是否与 KV Cache 有关系
阅读完上面对于 KV Cache 的讲解,你可能会联想到我们用 AI 时的超长上下文问题,是不是也会因为 KV Cache 过多而受到影响?
模型在超长 Context 下效果下降,更多来自三个方面:
- Attention 稀释。需要关注的信息越来越多,模型更难把注意力集中到真正重要的位置。
- 位置编码的限制。即使使用 RoPE、YaRN、LongRoPE 等技术,超长距离的位置表示仍然会逐渐失真。
- 训练数据限制。很多模型主要训练于几千到几万 Token 的上下文。
当直接推理 100 万 Token 时,模型实际上并没有见过这么长的上下文,自然容易出现性能下降。
因此KV Cache 主要影响的是推理效率,而不是模型理解能力。
为什么现在几乎所有推理框架都在优化 KV Cache?
对于现代推理框架来说,模型参数通常只需要加载一次。真正不断增长的是 每个用户会话的 KV Cache。
例如:
1000 个用户
↓
1000 份 KV Cache随着 Context 增长,KV Cache 很可能比模型参数本身还占显存。因此,近年来大量推理优化技术,例如:
- FlashAttention
- PagedAttention
- Multi-Query Attention(MQA)
- Grouped-Query Attention(GQA)
几乎都围绕:如何减少 KV Cache 的占用、提升读取效率。
可以说现代大模型推理真正的瓶颈,很多时候不是 GPU 算力,而是如何高效管理和读取 KV Cache。
当然现在很多企业其实一定程度上还是受到 GPU 算力影响,毕竟产品是需要针对于外面客户使用的,越强的算力就意味着能够支撑更好的模型、更快地效率、更强有力的竞争价格。上面的说法只是单独针对于大模型的现阶段的发展方向而言。
总结
KV Cache 的思想其实并不复杂:
- 历史 Token 的 Key 和 Value 不会变化,因此只计算一次。
- Query 是一次性的,因此没有缓存价值。
- Prompt 在 Prefill 阶段一次性建立 KV Cache。
- 模型生成的新 Token 会不断追加自己的 KV,形成新的上下文。
- Context 越长,KV Cache 越大,每一步需要读取的历史信息越多,因此推理速度逐渐下降。
从工程角度来看,KV Cache 是一个强有力的必要优化,是现代 Transformer 推理能够高效运行的基础。如今围绕 KV Cache 的压缩、共享、分页和管理优化,也已经成为 LLM 推理框架中最活跃的研究方向之一。
补充
当模型越来越强,KV Cache 与 Harness 也在发生变化。
KV Cache从“重复计算的缓存”到“可编辑状态”
传统 KV Cache 解决的核心问题上文也说了:避免重复计算。
模型在 Prefill 阶段计算出历史 Token 的 Key 和 Value,后续 Decode 直接复用,因此它本质上是一种计算缓存。也正因为如此,传统 KV Cache 对上下文前缀非常敏感。每当前缀发生变化,变化位置之后的 KV 通常就无法继续复用。
这也导致了一个很自然的工程原则:为了提高 Cache 命中率,尽量保持 Context 前缀稳定。
但现在的一些大模型厂商已经开始尝试突破这个限制并有了一定成果。一个重要的观察是模型在处理上下文时,KV 不只是简单保存 Token 本身的信息,随着计算层层向后推进,前面的信息会逐渐形成对后续计算有影响的 “中间状态”。换句话说,可以把 KV Cache 理解成模型阅读上下文后留下的某种 “笔记”。
这就带来了两个以前很难做到的方向。
第一是编辑(Editing)。
如果上下文中的某个字段发生变化,并不一定意味着所有缓存都必须丢弃。研究尝试让变化沿着已有的计算状态传播,只重新计算受到影响的部分,从而避免完整 Prefill。
第二是组合(Composition)。
如果一段上下文已经被计算成 KV 状态,那么它是否可以经过位置调整后,直接与另一段已经计算好的 KV 状态组合?如果可以,那么技能、记忆、上下文等内容就可以被预先计算成一个个模块,在需要时直接拼接,而不是每次从头计算。
因此,KV Cache 的发展方向就不再只是:
重复计算
↓
缓存
↓
复用而可能进一步变成:
缓存
↓
可编辑
↓
可组合
↓
模块化复用这意味着 KV Cache 的角色可能从单纯的计算优化,逐渐变成 Agent 上下文中的一种可操作中间状态。
不过需要注意,这仍然属于研究方向。当前生产环境中的主流推理系统依然高度依赖稳定前缀和 Prefix Cache,因此现阶段设计 Agent Context 时,仍然应该尽量保持高复用率的前缀稳定。
Harness在模型越来越强之后,还应该限制模型“怎么做”吗?
类似的变化也发生在 Agent Harness 和提示工程上。
早期模型能力有限,因此我们需要大量 Harness 和 System Prompt 帮模型补足能力:
先分析
↓
再规划
↓
调用工具
↓
修改代码
↓
运行测试
↓
检查结果这些规则本质上是在告诉模型 “你应该按照这个流程完成任务”。当模型还不够强时,这非常有效。
但随着模型能力提升,它可能已经能够自己完成:
理解任务 → 规划 → 选择工具 → 执行 → 发现错误 → 调整方案 → 验证结果这时候,如果 Harness 仍然要求模型严格按照预设流程执行,原本用来提升模型能力的工程控制就可能反过来成为限制模型能力的工程约束。
因此,强模型时代的 Prompt Engineering 可能需要发生变化。过去更倾向于告诉模型应该怎么做。未来则更倾向于告诉模型目标是什么、有哪些资源、有什么约束、什么结果算成功。
例如,与其规定:
第一步必须搜索
第二步必须读取
第三步必须规划
第四步才能修改不如提供:
目标:解决这个 Bug
资源:代码库、Shell、测试工具
约束:不能访问生产环境和 Secret
成功标准:Bug 修复并通过测试至于具体应该怎么完成?那是交给模型自己决定的事情。因此,模型能力增强后,Harness 并不是变得 “不重要”,而是控制方式发生变化:
过去:
Harness → 控制模型怎么做
现在:
Harness → 提供环境、工具、状态、权限和安全边界
↓
Model
↓
自主决定怎么做当然,安全边界不能交给模型自由决定。文件权限、Secret、生产环境、危险操作等仍然应该由 Harness、Sandbox 和权限系统强制限制。
所以真正的趋势开始走向把模型已经具备的决策能力还给模型,只保留模型本身不应该决定的环境与安全边界。
这与 KV Cache 的变化其实指向了相似的方向,以前我们会围绕系统的限制设计 Agent,而随着模型能力增强,未来的 Agent 工程更需要做的是减少不必要的限制,让系统去适应模型,而不是让模型去适应旧的系统。
RoLingG | 博客
评论(0)