guangzhengli

大模型 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. 生成第 1 个 token 时,计算所有输入 token 的 K、V,存入 KV Cache
  2. 生成第 2 个 token 时,只计算新 token 的 K、V,然后 append 到 KV Cache
  3. 生成第 3 个 token 时,同样只计算新 token 的 K、V,append 到缓存
  4. 后续每步只做 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 内容排布顺序

顺序内容类型说明
1System Prompt / 固定指令永远不变,放最前面,所有请求共享
2长文档 / 知识库 / 工具定义变化少,内容长,缓存收益最大
3Few-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,降低推理成本
PagedAttentionKV Cache 显存碎片化提升吞吐,几乎消除碎片

回顾第一章的四个错误,现在都有了明确的答案:

  • 错误一(System Prompt 含动态内容)→ 动态信息移到用户消息
  • 错误二(每轮压缩历史)→ 只追加,不修改,截断从最旧处开始
  • 错误三(工具顺序随机)→ 固定顺序,内容不变
  • 错误四(空白符不统一)→ 统一换行、空格、编码处理

一句话总结:LLM 缓存的本质是避免对已经见过的 token 重复做 Attention 计算。KV Cache 在单次推理内生效,Prompt Cache 在多次请求间复用,PagedAttention 解决显存管理问题。三者共同构成现代 LLM 推理系统的缓存体系。