大模型 LLM 缓存机制:从原理到工程实践
2026年2月21日
本课程涵盖 KV Cache、Prompt Cache、PagedAttention 的完整原理,以及如何在工程实践中最大化缓存命中率。
第一章 你可能一直在做的错误实践
在介绍原理之前,先来看几个极其常见、但会让缓存完全失效的做法。如果你中招了,不用担心——看完这门课你就知道该怎么改。
错误一:在 System Prompt 里塞动态内容
"你是一个助手。当前用户:{username},登录时间:{datetime},
会员等级:{level},本次请求 ID:{request_id}"这是最常见、破坏性最大的错误。每个用户、每次请求的 System Prompt 都不一样,缓存永远无法命中,每次都要从头计算。
错误二:每轮对话都重新压缩历史
// 每次请求前,把历史消息摘要一遍再注入
const summary = await summarize(history);
messages = [systemPrompt, summary, newUserMessage];摘要后的内容每次都不同,即使 System Prompt 是固定的,后面的内容也已经破坏了前缀一致性,已缓存的历史部分全部失效。
错误三:工具定义顺序随机变化
// 每次请求时动态拼装 tools 数组,顺序不固定
const tools = shuffle([weatherTool, searchTool, calendarTool]);工具定义放在 System Prompt 之后,顺序一变,后续所有内容的缓存全部失效。
错误四:换行符和空格不统一
# 有时用 \n,有时用 \n\n,有时末尾有空格,有时没有
prompt_v1 = "你是助手。\n请帮助用户。"
prompt_v2 = "你是助手。\n\n请帮助用户。" # 不同 token!\n 和 \n\n 会产生不同的 token 序列,同样的文字不同的空白处理,前缀精确匹配失败,缓存失效。
这些错误有一个共同的根源:不了解缓存是如何工作的。接下来我们从原理讲起。
第二章 为什么 LLM 需要缓存?
大型语言模型在推理阶段面临一个根本性的计算瓶颈:自回归生成。模型每次只生成一个 token,但每次生成都需要对序列中所有已有 token 进行 Attention 计算。随着序列长度增加,计算量呈平方级增长,在实际应用中会造成严重的延迟和成本问题。
缓存机制的核心目标只有一个:避免对已经见过的 token 重复进行 Attention 计算。
LLM 的缓存分为两个层次:
- 推理层缓存(KV Cache):在单次推理过程中,避免已生成 token 的重复计算。
- 服务层缓存(Prompt Cache):在多次请求之间,复用相同前缀内容的计算结果。
两者协同工作,共同大幅降低延迟与推理成本。
第三章 KV Cache:推理层缓存
3.1 Transformer Attention 的计算原理
在 Transformer 中,每个 token 都需要与序列中所有其他 token 做 Attention 运算。标准的 Scaled Dot-Product Attention 公式为:
Attention(Q, K, V) = softmax(QK^T / √d) · V其中 Q(Query)、K(Key)、V(Value)是通过对输入做线性变换得到的矩阵。在每一层 Transformer 中,每个 token 都有对应的 K 向量和 V 向量。
3.2 自回归生成中的重复计算问题
LLM 生成文本的过程是逐 token 的:生成第 1 个 token,然后基于前 1 个 token 生成第 2 个,以此类推。
问题在于,每次生成新 token 时,如果重新计算所有 token 的 K、V 矩阵:
- 生成第 n 个 token 时,需要对前 n-1 个 token 全部重新计算
- 总计算量为 O(1) + O(2) + ... + O(n) = O(n²)
- 对于 2048 token 的序列,这意味着数百万次不必要的重复计算
3.3 KV Cache 的工作机制
KV Cache 的思路非常简单:已经计算过的 K、V 矩阵不变,把它们缓存起来,下次直接复用。
具体流程:
- 生成第 1 个 token 时,计算所有输入 token 的 K、V,存入 KV Cache
- 生成第 2 个 token 时,只计算新 token 的 K、V,然后 append 到 KV Cache
- 生成第 3 个 token 时,同样只计算新 token 的 K、V,append 到缓存
- 后续每步只做 O(1) 的新计算,总体复杂度从 O(n²) 降为 O(n)
效果:在长序列生成场景下,KV Cache 可以将推理速度提升数十倍,是 LLM 实际部署中最基础、最重要的优化手段。
3.4 KV Cache 的显存代价
KV Cache 是以显存换计算速度的典型权衡。它的显存占用量与以下因素成正比:
- 序列长度:序列越长,缓存越大
- 模型层数:每一层 Transformer 都有独立的 KV Cache
- 隐藏层维度(hidden dim):维度越大,每个 token 的 KV 向量越大
- 并发请求数量(batch size):每个请求有独立的 KV Cache
以 LLaMA-2 70B 模型为例,在 4096 token 的序列长度下,单个请求的 KV Cache 就可能超过 4GB。这是大模型推理中显存瓶颈的主要来源之一,直接限制了可并发处理的请求数量。
第四章 Prompt Cache:跨请求前缀缓存
KV Cache 解决的是单次推理内部的重复计算。但在实际服务中,还存在另一种更大规模的重复:不同用户请求之间,往往有大量相同的前缀内容。
4.1 什么是 Prompt Cache?
如果一个服务的所有请求都以相同的 System Prompt 或长文档开头,那么每次都对这段前缀做 Prefill(预填充)计算是极大的浪费。
核心思路:把请求前缀对应的 KV Cache 预先计算并持久化存储。后续请求如果命中相同前缀,直接复用这段缓存,跳过整个 Prefill 阶段。
4.2 命中条件
Prompt Cache 的命中是严格的前缀精确匹配:
- 请求的 token 序列必须从头开始与缓存完全一致
- 哪怕只有一个 token 不同,该位置之后的所有缓存全部失效
- 原因:Attention 是全局依赖的,某位置的 K/V 依赖于它之前所有 token
这也解释了第一章里那些错误为什么会破坏缓存——它们都在「前缀」里引入了变化。
4.3 主要实现方案
| 实现方案 | 特点 |
|---|---|
| Anthropic Claude Prompt Caching | 开发者可显式用 cache_control 标记缓存断点,最多 4 个断点,缓存有效期约 5 分钟 |
| Google Gemini Context Caching | 支持将大段文档/视频的 token 结果缓存,按 token 数计费 |
| vLLM Prefix Caching (RadixAttention) | 用前缀树自动管理 KV Cache 块,跨请求自动匹配共享前缀 |
| SGLang RadixAttention | 同样基于前缀树,性能极为出色,常作为高吞吐推理引擎 |
4.4 Anthropic API 的显式标记方式
使用 Anthropic API 时,可以用 cache_control 精确控制缓存边界:
{
"system": [
{
"type": "text",
"text": "你是一个专业的代码助手...",
"cache_control": { "type": "ephemeral" }
}
],
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "以下是完整代码文件内容:...",
"cache_control": { "type": "ephemeral" }
},
{
"type": "text",
"text": "请解释第 42 行的逻辑"
}
]
}
]
}最多可以设置 4 个缓存断点,灵活分层缓存不同粒度的内容。
第五章 PagedAttention:显存管理革命
KV Cache 的显存问题在高并发场景下尤为突出。传统方式是为每个请求预分配一块连续的显存空间,这会带来严重的显存碎片化问题。
5.1 传统显存分配的问题
- 预分配浪费:必须按最大序列长度预留空间,但实际生成长度不可预知
- 显存碎片:不同长度的请求导致显存中出现大量不连续的空洞
- 并发受限:碎片化使得实际可用显存远少于物理显存
5.2 PagedAttention 的解决方案
vLLM 借鉴操作系统虚拟内存的**分页(Paging)**思想,提出了 PagedAttention:
- 将 KV Cache 切分成固定大小的 Block(类比内存页),例如每块包含 16 个 token 的 KV
- Block 不需要在显存中连续,通过块表(block table)做地址映射,类比页表
- 显存按需分配:序列生成多少 token,就分配多少块,不提前预留
- 不同请求的 Block 可以共享,例如有相同前缀的请求共用同一份 KV 块
效果:PagedAttention 几乎消除了显存碎片,使 vLLM 相比传统方式吞吐量提升 2-4 倍,是目前高性能 LLM 推理引擎的标配技术。
5.3 Copy-on-Write 与前缀共享
PagedAttention 还支持 **Copy-on-Write(写时复制)**机制:多个请求可以共享相同前缀对应的只读 KV Block,只有在内容发生分叉时才复制一份新块。这与操作系统中 fork() 的原理完全相同,极大地节约了共享前缀场景下的显存。
第六章 工程挑战与权衡
6.1 缓存失效的代价
在使用 Prompt Cache 时,最重要的工程挑战是避免意外破坏前缀的一致性。以下情况都会导致缓存完全失效(这也是第一章那些错误的根因):
- 在 System Prompt 中插入动态内容(用户名、时间戳、随机 ID)
- 对话历史摘要/压缩后重新注入(内容变了,前缀不再一致)
- 工具定义(Tools/Functions)的顺序或内容改变
- 换行符、空格的微小差异(不同 tokenize 结果导致不同 token 序列)
6.2 显存与吞吐的权衡
KV Cache 的大小与 batch size 存在竞争关系:KV Cache 越大(序列越长),每个请求占用的显存越多,能同时处理的并发请求就越少,吞吐量下降。这是一个根本性的工程权衡:
- 延迟敏感型服务:优先保留大 KV Cache,保证单次请求速度快
- 吞吐优先型服务:限制单请求最大序列长度,提高并发数
6.3 多模态场景的挑战
图像、音频、视频输入在 tokenize 后会产生大量 token(一张高分辨率图像可能对应数千个 token),KV Cache 的显存压力成倍增加。目前主流的处理方式是对视觉 token 做专门的压缩处理,或者限制多模态输入的分辨率。
6.4 缓存驱逐策略
当显存不足时,需要决定驱逐哪些 KV Cache 块。常见策略包括:
- LRU(最近最少使用):驱逐最长时间未被访问的块
- LFU(最不常用):驱逐访问频率最低的块
- 基于序列优先级的驱逐:优先保留高优先级请求的缓存
第七章 正确实践:如何提高缓存命中率
有了前面的原理基础,现在回头看第一章的错误,答案就清晰了。
核心原则:稳定的内容放最前面,动态的内容放最后面。把 Prompt 想象成一棵树的根——根越粗越稳定,缓存就越有效。动态的东西永远长在枝梢。
7.1 内容排布顺序
| 顺序 | 内容类型 | 说明 |
|---|---|---|
| 1 | System Prompt / 固定指令 | 永远不变,放最前面,所有请求共享 |
| 2 | 长文档 / 知识库 / 工具定义 | 变化少,内容长,缓存收益最大 |
| 3 | Few-shot 示例 | 固定顺序和格式,缓存效率高 |
| 4 | 对话历史 | 每轮追加,但已有部分不修改 |
| 5 | 当前用户输入 | 每次都不同,放最末尾 |
7.2 System Prompt 设计原则
System Prompt 是缓存的「地基」,必须保持极度稳定:
❌ 错误(对应第一章错误一):
"你是一个助手。当前用户:{username},时间:{datetime},
会员等级:{level},请求 ID:{request_id}"
✅ 正确:
System: "你是一个专业的客服助手。"
User: "我是张三,黄金会员,订单号 #12345,我的问题是..."动态的业务信息应该放在用户消息里,而不是 System Prompt 里。
7.3 对话历史管理
❌ 错误(对应第一章错误二):
每轮重新摘要/压缩历史后注入 —— 前缀变了,缓存全部失效
✅ 正确:
每轮只在末尾追加新消息,完全不修改已有历史如果上下文太长必须截断,应该从最旧的消息开始删除,保留 System Prompt 和近期历史的连续性,让后半段的缓存得以保留。
7.4 工具定义管理
// ❌ 错误(对应第一章错误三):随机顺序
const tools = shuffle([weatherTool, searchTool, calendarTool]);
// ✅ 正确:固定顺序,内容不变
const tools = [weatherTool, searchTool, calendarTool]; // 永远如此7.5 长文档场景(RAG)
如果需要把一份长文档(如代码库、PDF、知识库)注入 Prompt,将文档内容放在 System Prompt 紧接之后、用户问题之前:
[System Prompt] ← 永远固定
[完整文档内容] ← 这部分被缓存
[用户的具体问题] ← 每次变化,但前面已缓存这样,当用户针对同一份文档提出不同问题时,文档的 Prefill 计算只需要做一次,后续问题直接命中缓存,大幅节省首 token 延迟(TTFT)和推理成本。
7.6 Token 一致性细节
❌ 错误(对应第一章错误四):
prompt_v1 = "你是助手。\n请帮助用户。"
prompt_v2 = "你是助手。\n\n请帮助用户。" // 不同 token!
✅ 正确:
统一换行符、统一编码、不在固定部分插入随机内容第八章 总结
LLM 的缓存体系可以概括为三个层次,每个层次解决不同粒度的重复计算问题:
| 缓存类型 | 解决的问题 | 效果 |
|---|---|---|
| KV Cache | 单次推理内重复计算 | 生成速度从 O(n²) 降到 O(n) |
| Prompt Cache | 多请求间重复的前缀计算 | 减少 TTFT,降低推理成本 |
| PagedAttention | KV Cache 显存碎片化 | 提升吞吐,几乎消除碎片 |
回顾第一章的四个错误,现在都有了明确的答案:
- 错误一(System Prompt 含动态内容)→ 动态信息移到用户消息
- 错误二(每轮压缩历史)→ 只追加,不修改,截断从最旧处开始
- 错误三(工具顺序随机)→ 固定顺序,内容不变
- 错误四(空白符不统一)→ 统一换行、空格、编码处理
一句话总结:LLM 缓存的本质是避免对已经见过的 token 重复做 Attention 计算。KV Cache 在单次推理内生效,Prompt Cache 在多次请求间复用,PagedAttention 解决显存管理问题。三者共同构成现代 LLM 推理系统的缓存体系。