第 27 讲 · 更好的 KV 缓存:CacheGen、CacheBlend 与 LMCache · KV Cache as a New AI Memory Abstraction

Large Language Model Systems · Lecture 27。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 27 · Wed 4/22 · Guest Lecture by Junchen Jiang(CMU / TensorMesh): KV Cache — A New AI Memory Abstraction(slides:llmsys-27-LMCache_junchenjiang.pdf,51 页)。 阅读材料:Liu et al. (2024) CacheGen: KV Cache Compression and Streaming for Fast Large Language Model Serving(SIGCOMM);Yao et al. (2025) CacheBlend: Fast Large Language Model Serving for RAG with Cached Knowledge Fusion(EuroSys);LMCache 文档与代码。 依据说明:slides 分四部分。工业界的 KV 缓存:KV 缓存怎样省掉重复计算、“KV cache 的搜索热度超过了 attention”、一张 GPU 一天产生多少 KV、存 KV 的经济账(回本命中率 1.06%、1000 个节点 3 年省约 2900 万美元)、KV 正从 GPU 显存走向 CPU 内存、内存池、SSD 与远端存储、两个挑战(放得下、搬得快)。研究界的 KV 缓存:KV 是”AI 原生的数据”、2025 年的 KV 论文是 2023 年的 40 倍、CacheBlend(前缀缓存不够、非前缀复用丢了跨块注意力、选择性重算、计算量与质量的曲线、2WikiMQA 上 3 倍快、代码补全上准确率反而更高)、KV 压缩(量化后仍太大、像视频一样编码:自适应量化 + 统计 + 算术编码、CacheGen 压缩 3–10 倍、传输快 2–8 倍)。为什么需要 LMCache:研究想法难以落地、KV 库跑在推理引擎进程里的痛点、LMCache 把 KV 缓存与推理分离、成为事实上的开源 KV 库、零拷贝的 CPU 共享、两组基准(DP-EP 服务、CPU + 磁盘卸载)、扩展接口。经验教训:一个新的分层架构正在形成、OpenAI API 是事实上的”窄腰”(像 IPv4)、ML 与系统的新融合、开源的游戏规则在变。本讲义的 KV 账本、算术编码器与 CacheGen 式编码器、流式自适应的模拟、“记录世界”模型与 CacheBlend 的实现、多层 KV 存储的模拟,都是本讲义自己设计的,不代表课堂上讲了什么;小模型上的数字只说明机制,规模化的结论以论文为准。 配套代码 Code:code/lecture27/(5 个脚本 + kvcache.py、blend_world.py;复用第 26 讲的时间模型、第 19 / 22 讲的模型与前向;第一次运行会训练一个小模型约 6 分钟) 预计阅读时间:3 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

学完这一讲,你应该能够:

  1. 算出一个模型每个 token 的 KV 有多大、一张 GPU 一天产生多少 KV,并判断”把 KV 存起来、以后取回”何时比”重算”划算。
  2. 说清 CacheGen 的三个经验观察(token 局部性、层敏感度、按通道 / 层分组)与编码器的三步(增量、分层量化、算术编码),以及流式传输的自适应规则。
  3. 解释为什么直接拼接各块预先算好的 KV 会出错(丢了跨块注意力),CacheBlend 怎样只重算少数”偏差最大”的 token、怎样用流水线把重算藏在加载后面。
  4. 说出 LMCache 的定位:把 KV 缓存从推理引擎里分离出来,做成一个跨引擎、跨实例、跨存储层级的”AI 原生数据存储”。
  5. 复述 Junchen Jiang 的三条教训:新的分层架构、OpenAI API 的”窄腰”、ML 与系统的融合。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面再展开 Previewed only
前缀缓存与 RadixAttention(第 22 讲);PagedAttention(第 24 讲)Mooncake 以 KV 为中心的架构(附加讲 A2)
预填充 / 解码分离、KV 传输(第 26 讲)RAG 的检索部分(附加讲 A3)
量化(第 19 讲);RoPE(第 7 讲);归纳头(第 7 讲的上下文学习实验)KV 的稀疏化与淘汰(H2O、SnapKV 等)、跨模型的 KV 翻译
熵、编码的基本概念(前置讲 P2)视频编码的细节

0.3 本讲地图 Map

§1  KV 缓存:从"临时状态"到"数据"——有多大、值不值得存、从哪里取                     ← 代码 01
§2  KV 是 AI 原生的数据:研究界在做什么
§3  CacheGen(一):像压缩视频一样压缩 KV                                              ← 代码 02
§4  CacheGen(二):带宽变化时的流式传输                                                ← 代码 03
§5  CacheBlend:非前缀的 KV 复用与选择性重算                                            ← 代码 04
§6  LMCache:把 KV 缓存从推理引擎里分离出来                                             ← 代码 05
§7  经验教训:分层架构、窄腰、ML 与系统的融合                                           ← 代码 05
§8 小结   §9 常见误区   §10 练习   §11 延伸阅读   §12 下一讲预告

0.4 本讲的两个小模型

  • 城市 LLaMA(第 19 讲):在城市语言上训练的 4 层模型。城市语言的句子彼此独立——后一句的内容不依赖前一句。
  • 记录世界(blend_world.py,本讲新训练):4 层、d = 64 的 LLaMA,上下文是一串随机的”键 值”记录(k₁ v₁ k₂ v₂ … k₁₂ v₁₂,每篇文档的映射都是新的),问题是一个键,要答出它的值。模型必须从上下文里读答案(靠”归纳头”:前面的层让每个值记住”我前面是哪个键”,后面的层去匹配)。训练 12000 步(约 6 分钟),损失在约 5000 步时突然下降——归纳头形成——之后在不重复的记录上 100% 答对。

为什么需要第二个模型?因为第一个太”好对付”:04 号脚本第 1 部分会看到,在城市语言上直接拼接各块的 KV 几乎没有损失——CacheBlend 要解决的问题根本不出现。真实文本里,后文常常依赖前文,记录世界用最简单的方式造出了这种依赖。


1. KV 缓存:从”临时状态”到”数据” · KV cache in industry

1.1 KV 缓存为什么能省计算

slides 第 4–5 页:一个请求 = 复用的部分(比如一段文档、系统提示)+ 新的部分(比如用户的问题)。每一层都要为每个 token 算 Q、K、V;复用部分的 K、V 如果已经在 GPU 显存里,就不用再算——新 token 的查询直接和缓存里的 K、V 做注意力。KV 缓存带来四个好处:降低 GPU 成本、更好的可扩展性、更低的请求时延、避免重复计算。

slides 用一个有趣的数据开场:Google 趋势上 “KV cache” 的搜索热度在 2025 年第一次超过了 “Attention”——“KV 缓存,注意力机制的一部分,第一次得到了比注意力更多的注意”。

1.2 有多大、产生得多快

01 号脚本第 1 部分(BF16):

模型每个 token 的 KV128K token
LLaMA-3.1-8B(GQA,8 组 KV)128 KiB16.8 GB
LLaMA-3.1-70B(GQA)320 KiB41.9 GB
Qwen3-235B-A22B(GQA,4 组 KV)188 KiB24.6 GB
DeepSeek-R1(MLA:每层一个 512 维潜向量 + 64 维 RoPE 键)68.6 KiB9.0 GB
OPT-13B(多头注意力)800 KiB104.9 GB

slides 第 8 页的算式:一张 MI300X 跑 DeepSeek-R1,每秒 2,625 个 token × 每个 token 68.6 KiB × 一天 86,400 秒 ≈ 15 TB / 天(01 号脚本按字节精确计算是 15.9 TB)。绝大部分 KV 用完就丢了;但其中有很多会被再次用到——系统提示、反复引用的文档、多轮对话的历史、智能体的工具说明。

1.3 值不值得存

slides 第 9 页的经济账(假设 8 卡节点、每卡每小时 2 美元;1 TB 的二级存储每小时 0.15 美元):只要约 1.06% 的请求命中缓存就能回本(相当于一段缓存的提示每周被用 2–3 次);如果 10% 的请求命中,1000 个节点 3 年能省约 3300 万美元,存储只花约 400 万。

01 号脚本第 3 部分用自己的算法验证了量级(LLaMA-3.1-70B):一段 1 万 token 的上下文(3.3 GB 的 KV),存一小时 0.0005 美元,命中一次省下的 GPU 时间值 0.0026 美元——平均每小时命中 0.19 次就回本,上下文越长越值得存(重算超线性、存储线性)。

1.4 从哪里取回来

存了还要取得回来。01 号脚本第 2 部分(LLaMA-3.1-70B,8 × H100):

来源 \ 上下文长度1K4K16K64K
GPU 重算(预填充)0.060 秒0.2250.9685.341
CPU 内存(PCIe 5.0)0.0070.0260.1050.419
本地 NVMe SSD0.0470.1870.7492.996
远端存储,100 Gb/s0.0260.1050.4191.678
跨云,10 Gb/s0.2621.0494.19416.777

要让”取”比”算”快,带宽至少要 KV 字节数 / 重算时间 ≈ 4–6 GB/s(这个模型)。CPU 内存、高速网络绰绰有余;10 Gb/s 的跨云链路反而比重算慢——存了也白存。压缩能按压缩比降低这个门槛,这就是 CacheGen 的出发点。

slides 第 10–12 页的趋势:2023 年 KV 只在 GPU 显存里(前缀缓存);2025 年第一季度扩展到 CPU 内存,第三、四季度扩展到内存池、SSD、远端存储(S3、跨云)。随之而来的两个挑战:怎样在 GPU / CPU 里放下更多的 KV(以支持更大的批)?怎样更快地把 KV 搬进 GPU?


2. KV 是 AI 原生的数据 · KV cache in research

slides 第 14 页的观点:KV 缓存不只是张量,而是”AI 原生的数据”——

KV 缓存是……所以 KV 缓存可以被……
中间的计算状态复用,以跳过计算、降低延迟
LLM 对输入的内部”理解”近似地表示(压缩),同时保留语义
直接决定下一个 token 的东西操纵,以影响输出的准确性

2025 年关于 KV 缓存的论文是 2023 年的 40 倍:存储与管理、压缩、传输、跨请求 / 跨模型的复用、拼接(blending)、检索、翻译、训练……但 slides 也指出:工业界和大多数开源系统还停留在最基础的”存储与管理”那一层。下面三节看其中两个研究方向(压缩、拼接),以及把它们带进生产的系统(LMCache)。


3. CacheGen(一):像压缩视频一样压缩 KV · KV cache compression

3.1 直觉

一段上下文的 KV 是一个巨大的三维张量(层 × token × 通道):1K 个 token 就有约 10 亿个数。量化(第 19 讲)能把每个数从 16 位压到 8 位或 4 位,但 slides 说”还是太大,传不动”。CacheGen 的洞见(slides 第 27 页):如果目标只是让它在网络上、存储里变小,就不必保持张量的形状——把 KV 当成视频来编码:视频编码不是逐像素量化,而是利用帧与帧之间的相似性、利用统计规律,再用熵编码压成比特流。

3.2 三个经验观察(CacheGen §5.1)

  1. token 方向的局部性:同一层、同一通道里,相邻 token 的 K / V 值相近。增量(相对于附近 token 的差)比原值集中得多——CacheGen 在 LLaMA 上测到增量的方差比原值小 2.4–2.9 倍。
  2. 层敏感度不同:输出质量对浅层 KV 的误差更敏感(误差会沿着层传播),对深层不太敏感。
  3. 按通道 / 层分组的信息增益:同一个通道(或同一层)里的值分布更集中,按它们分组统计概率分布,熵明显更低;按 token 位置分组几乎没有增益。

3.3 编码器(CacheGen §5.2)

  1. 基于变化的编码:把 token 分成每组 10 个;每组第一个 token 是”锚点”,用 8 位单独编码;其余 token 只编码相对锚点的增量(相对同一个锚点而不是前一个 token,这样可以并行编解码)。
  2. 分层量化:把层分成前、中、后三组,增量的量化步长从浅到深逐渐变大。
  3. 算术编码:量化后的整数用算术编码压成比特流;概率分布按”通道 × 层”离线统计一次(每个模型一套),编码器和解码器共用。CacheGen 报告这比用一个全局分布小最多 53%。

算术编码是什么?它维护一个区间,每来一个符号就把区间缩到这个符号占的那一段(宽度正比于概率);一个概率为 p 的符号平均只花 −log₂ p 位——比”每个符号固定几位”省得多,并且逼近熵。kvcache.py 里有一个 100 行左右的整数实现:02 号脚本第 3 部分验证它的实际码长与理想码长 −Σ log₂ p 只差零点几个字节,解码后与编码前逐位相同。算术编码是无损的,有损只发生在量化那一步。

3.4 在小模型上检验(02 号脚本)

把 CacheGen 的配方搬到记录世界的模型上,结果很有教育意义:

观察CacheGen 在 LLaMA 上记录世界的小模型
局部性增量比原值小 2.4–2.9 倍不成立:增量的标准差是原值的 1.31–1.58 倍(两个独立随机变量之差约 √2 倍)——每个 token 都是一个随机的键或值
层敏感度浅层更敏感相反:只把最后一层量化到 3 位,准确率掉到 54%;量化第 1 层到 2 位毫无影响——这个模型的”查找”发生在最后一层
分组的熵按通道 / 层分组熵更低成立:不分组 4.15 位、按 token 位置 4.12、按层 3.90、按通道 3.26

于是同一个编码器框架的两种用法差别巨大(每个 token 的 KV 在 FP16 下是 512 字节):

编码字节 / token相对 FP16问答准确率
FP165121.00×100%
FP16 + zlib(通用压缩器)4721.08×100%
均匀量化 8 位 / 6 位 / 4 位277 / 213 / 1491.85× / 2.40× / 3.43×100% / 100% / 75.5%
照搬 CacheGen:浅层精细 + 增量(最细一级)1882.72×70.3%
按实测敏感度分配精度、不用增量 + 算术编码1673.07×100%

通用压缩器几乎压不动浮点数(它找的是重复的字节串,浮点数的低位几乎随机)。按实测的敏感度分配精度、再做按通道的算术编码,在不损失准确率的前提下比 8 位量化小 1.66 倍、比同样无损的 6 位量化小 1.28 倍。照搬配方反而最差——它把最大的误差放在了最敏感的那一层。

教训:CacheGen 的三条观察是真实大模型的经验性质,不是数学必然;编码器的设计(增量还是原值、各层的精度)应该在目标模型上实测。CacheGen 在 LLaMA 上比 8 位量化的基线再小 3.5–4.3 倍(slides 的说法是 KV 压缩 3–10 倍、传输快 2–8 倍),靠的正是那里成立的局部性与敏感度规律。


4. CacheGen(二):带宽变化时的流式传输 · Streaming with adaptation

4.1 什么时候传 KV、什么时候传文本

把 KV 存在远端,取回时还要过网络。03 号脚本第 1 部分(LLaMA-3.1-70B,4 × A100,取回 12K token 的上下文):

带宽FP168 位量化CacheGen 级别 1只传文本、GPU 重算
1 Gb/s31.5 秒17.010.33.35
5 Gb/s6.293.412.053.35
25 Gb/s1.260.680.413.35

带宽低时,最好的办法是只传文本让 GPU 重算;带宽高时,传压缩后的 KV 最快。为什么这里用 70B 而不用 8B?因为 8B 模型在 H100 上重算 1.2 万 token 只要 0.36 秒,任何 25 Gb/s 以下的链路都不如重算——KV 复用对大模型、慢 GPU、长上下文最划算。CacheGen 论文的实测:取回并处理上下文的总延迟比 KV 复用的基线降低 3.2–3.7 倍,回答质量几乎不变。

4.2 自适应(CacheGen §5.3)

传一段几 GB 的 KV 要几百毫秒到几秒,带宽在这期间可能变化。CacheGen 的做法与视频流的自适应码率(ABR)同出一辙:

  • 上下文切成若干块(默认每块 1.5K token),每块离线编码成几个级别的比特流(各级别可以独立解码、拼接);
  • 逐块发送:用上一块测到的吞吐估计带宽,假设它不变,为下一块挑”预计能按时完成的、损失最小的”配置;
  • 带宽太低时退回发文本,让 GPU 用已经收到的前文 KV 重算这一块。

块长是个取舍:块太大,对带宽变化反应慢(正在传的那一块只能传完);块太小,退回重算时 GPU 的批太小。

03 号脚本第 2 部分(前 0.3 秒 10 Gb/s,之后到第 3 秒只有 1 Gb/s,再之后 5 Gb/s;SLO:4 秒):

策略完成时间满足 SLO每块的选择质量
固定 8 位量化5.27 秒否8 8 8 8 8 8 8 8100%
固定 CacheGen 级别 13.92 秒勉强1 1 1 1 1 1 1 1100%
CacheGen 自适应3.40 秒是1 0 0 文 文 文 文 396.7%

自适应在带宽好时用损失最小的级别,骤降后改发文本,最后按剩余时间挑了一个更粗的级别——代价是一点质量(这个小模型的级别 3 只答对 74%)。带宽稳定在 25 Gb/s 时,它一路选损失最小的级别。


5. CacheBlend:非前缀的 KV 复用 · Non-prefix KV reuse

5.1 前缀缓存不够用

前缀缓存(第 22 讲的 RadixAttention、vLLM 的自动前缀缓存)只能复用”开头完全相同的那一段”。RAG 的输入是”检索到的 k 段文字 + 问题”:每次检索到的组合和顺序都不一样,只有第一段可能是前缀。CacheBlend 论文:随着检索的段数增加,回答质量先升后降(“迷失在中间”),而前缀缓存只能省下第一段——段数越多省得越少。

自然的想法:每段文字各自预先算好 KV,用的时候直接拼起来(完全复用,full KV reuse;PromptCache 等工作)。位置怎么办?RoPE 是旋转:一段在位置 0 算好的 K,搬到位置 p,只要再旋转 p·θ(shift_kv;CacheBlend 附录 A)。V 不带位置信息,不用动。

5.2 直接拼接丢了什么

位置能修正,跨块注意力修正不了:第二段的 token 在预先计算时看不到第一段。slides 的例子:一段说”梅西在世界杯上进了 13 个球”,另一段说”C 罗进了 8 个球”,问”谁进得多、多几个”——完全复用时模型答错。

04 号脚本把这件事量化了:

  • 城市语言(3 块 × 8 个 token + 8 个 token 的问题):完整预填充的交叉熵 0.6940,完全复用 0.6941,KL 散度 0.00003——几乎无损。城市语言的句子彼此独立,不存在需要跨块的信息。
  • 记录世界(12 条记录切成每块 5 个 token;块长是奇数,有的记录被切开:键在一块的末尾、值在下一块的开头):
做法被切开的记录完整的记录计算量
完整预填充100%100%100%
前缀缓存(只复用第一块)100%100%80%
完全复用拼接的 KV8.8%100%4%
CacheBlend,重算 10% / 20% / 30%85.0% / 99.4% / 100%100%16% / 24% / 30%
随机重算 10% / 20% / 30%18.1% / 21.2% / 30.0%100%16% / 24% / 30%

被切开的值位于某一块的开头,它的 KV 是单独预先算的——“我前面是哪个键”这条信息只能来自跨块注意力。完全复用时,这些记录只剩 8.8%(随机猜是 1/12 ≈ 8.3%);没被切开的记录不受影响。

5.3 CacheBlend:只重算偏差最大的 token

CacheBlend 的两个洞见(§4):

  • 洞见 1:在第 l 层重算”KV 偏差大”的 token(偏差 = 预先算的 KV 与完整预填充的 KV 之差),注意力的偏差下降得最快。偏差大的 token 叫 HKVD(high-KV-deviation)token,通常只占 10–15%——跨块注意力是稀疏的。
  • 洞见 2:偏差大的 token 在相邻层上大多是同一批。

难点是:不做完整预填充,怎么知道谁的偏差大?CacheBlend 的逐层过滤:第一层完整计算,按每个 token 的注意力偏差挑出比目标比例稍多的 token;下一层只为这些 token 重算 KV,再从中挑出偏差最大的更少一批;如此逐层缩小。本讲义的实现(blend_world.blend):

  • 第 0 层:K、V 只取决于 token 和位置,与上下文无关——预先算好的就是对的。但注意力的输出依赖上下文:用同一组 K、V 算两遍注意力,一遍只看本块(= 预先计算时的那一遍),一遍看全部前文,两者之差就是”注意力偏差”。偏差最大的 1.25·r·N 个 token 继续往下算。
  • 第 l ≥ 1 层:只为这些 token 重算 K、V(替换掉预先算好的),按 KV 偏差 ||ΔK||² + ||ΔV||² 留下更少的一批(1.0·r·N、0.75·r·N)。其余 token 一直用预先算好的 KV。问题的 token 每层都完整计算。

04 号脚本第 3 部分验证了两个洞见(块长 5;偏差 = 拼接的 KV 与完整预填充的 KV 之差,逐层):

token 的类别个数平均偏差(第 1–3 层之和)
块首的值(记录被切开)3001654
块首的键30020
其他30003.4

偏差最大的 15% 的 token 占了全部偏差的 98%;各层偏差的秩相关 0.88–0.93。第 0 层按注意力偏差挑出的 token 覆盖了 99% 的”块首的值”。所以 CacheBlend 重算 20% 就把被切开的记录恢复到 99.4%,而随机挑同样多的 token 只有 21%——关键在挑对 token。

块越短,被切开的记录越多,需要重算的比例越高(块长 3 时重算 20% 只恢复到 81%)。CacheBlend 在真实 RAG 数据上的经验值是每层重算约 15%。

5.4 把重算藏在加载后面

第 l 层的选择性重算只依赖上一层的结果,所以”从存储里读第 l + 1 层的 KV”和”重算第 l 层的少数 token”可以同时进行——每层的时间是 max(读取, 重算)。CacheBlend 的加载控制器据此选重算比例:让重算时间约等于读取时间(不增加延迟),但不低于经验上保证质量的 15%;也可以反过来选存储——在不增加延迟的前提下用最便宜的设备。论文的例子:LLaMA-7B、4K 上下文,重算 15% 每层约 3 ms,从 NVMe 读一层的 KV 约 16 ms,重算被完全藏起来;LLaMA-70B 重算 15% 每层约 7 ms,读一层只要约 4 ms。

04 号脚本第 4 部分(RAG:6 块 × 512 token + 32 个 token 的问题;A40):

模型KV 存在哪完整预填充完全复用CacheBlend 15%加速
LLaMA-2-7B(多头注意力)CPU 内存0.437 秒0.0930.0944.7×
NVMe SSD0.4370.4310.4311.0×
LLaMA-3.1-70B(GQA,4 卡)CPU 内存1.6110.1350.3374.8×
NVMe SSD1.6110.3470.3474.6×

多头注意力的 7B 模型 KV 太大,从 NVMe 读一层几乎和整层重算一样慢,复用不划算;GQA 的 70B 模型”每 FLOP 的 KV 字节数”小得多,从 NVMe 读也快 4.6 倍。论文的实测:与完整预填充相比 TTFT 降低 2.2–3.3 倍、吞吐提高 2.8–5 倍,质量与完整预填充相当;slides 的例子(LLaMA-70B,2 × A40,2WikiMQA,6 段各 512 token,KV 存在磁盘上)快约 3 倍。slides 还展示了一个意外的结果:在代码补全上,CacheBlend 甚至比完整预填充的准确率更高。


6. LMCache:把 KV 缓存从推理引擎里分离出来 · Why LMCache

6.1 研究想法为什么难以落地

slides 第 31–32 页:大量 KV 优化停留在论文里。原因之一是现有的 KV 库都跑在推理引擎的进程里(作为一个库):KV 操作的开销直接算在推理时间里;推理引擎很难改;KV 操作的接口很有限。而想动 KV 的人有三类:做研究的、存储厂商、推理编排器。

LMCache 的回答:把 KV 缓存做成一个独立的服务进程(LMCacheMP),推理引擎(vLLM、SGLang)通过接口存取 KV。它于 2024 年 4 月发布,是第一个开源的 KV 缓存库,后来成为事实上的标准:主流推理引擎与编排器(包括 NVIDIA Dynamo)都在用,GPU 厂商、存储厂商也在接入。

6.2 零拷贝的 CPU 共享

05 号脚本第 1 部分(一段 16K token 的 LLaMA-3.1-70B KV,约 5.2 GB,从 GPU A 交给 GPU B):

做法路径额外拷贝串行搬运
每个进程各带 KV 库 + 远端池GPU A → CPU A → 远端池 → CPU B → GPU B21.05 秒
每个进程各带 KV 库,CPU 间直接拷GPU A → CPU A → CPU B → GPU B10.47 秒
独立的 KV 服务进程(LMCacheMP)GPU A → 共享 CPU 池 → GPU B00.21 秒

一个进程的 CPU 内存别的进程看不见,所以库式的做法要么经过远端池中转,要么多拷一次(slides 第 37 页)。独立的服务进程让所有推理进程共享一个 CPU 内存池。

6.3 分层存储与多轮对话

slides 的两组基准:DP-EP 服务(Qwen3-235B-A22B FP8,4 × MI300,DP 4、EP 4;380 个并发用户、每人约 0.4 GB 的 KV):LMCache 的平均 TTFT 0.33 秒,推理引擎自带的卸载 0.80 秒(2.4 倍);解码 14.93 vs 6.56 token/s。CPU + 磁盘卸载(LLaMA-3.1-70B,1 × MI300X;19 个用户、每人约 1.6 GB):TTFT 0.15 vs 1.63 秒(10.8 倍),解码 25.59 vs 11.64 token/s(2.19 倍)。

05 号脚本第 2 部分模拟了同一类问题(LLaMA-3-8B,1 × H100;200 个用户、每人聊 12 轮,每秒 10 个请求;GPU 只能放下约 58 GB 的 KV,所有用户合计 241 GB):

存储历史命中率平均 TTFTP90 TTFTGPU 重算历史
只用 GPU 里的前缀缓存49%5.39 秒18.9 秒166 秒
+ CPU 50 GB(进程内卸载,有效 10 GB/s)77%0.722.8675
+ CPU 50 GB(LMCache,50 GB/s)77%0.150.4375
+ CPU 50 GB + SSD 1 TB97%0.080.2110

只靠 GPU:LRU 把别人的 KV 挤掉,每次都要重算越来越长的历史,GPU 被重算占满、排队爆炸。逐层加上 CPU 与 SSD,命中率与 TTFT 一路改善;同样的 CPU 容量,读得快(零拷贝、大块传输)的比读得慢的好 5 倍——这正是 slides 里 LMCache 与推理引擎自带卸载的差别来源。解码速度的提高(slides 的 2.19 倍)也是同一个原因:GPU 不再忙着重算历史。

6.4 一个面向研究的接口

slides 第 41–42 页:LMCache 想成为研究想法的落地平台——研究者可以写插件:压缩(CacheGen)、拼接(CacheBlend)、跨模型翻译、训练;存储后端(NIXL 等);编排逻辑(淘汰策略、语义匹配);可观测性。目标是把”学术界的 KV 技术”、“工业界越存越多的 KV”和”你的下一个想法”连起来。


7. 经验教训 · Lessons

Junchen Jiang 用最后几页 slides 总结了三条教训。

一个新的分层架构正在形成(slides 第 44–45 页)。时间线:2023 年 1 月开始做 KV 压缩与存储;2024 年初 KV 几乎不出 GPU(“GPU 显存还不够存所有活跃的 KV 吗?“——Ion Stoica, 2024 年 2 月);2024 年 4 月发布 LMCache;2025 年 1 月发布 vLLM Production Stack(第一个基于 Kubernetes 的开源推理编排器,后来成了 llm-d 与 Dynamo 工程师的参考)。到 2026 年,LLM 推理栈和经典计算机系统一一对应:

经典计算机LLM 推理栈
用户态程序智能体 + LLM
系统调用(接口)OpenAI API
进程调度推理编排器(资源仲裁)
文件系统(状态、存储)AI 原生的数据存储(LMCache)
指令集(x86,“窄腰”)token、KV 缓存、LLM
处理器推理引擎(vLLM、SGLang)

OpenAI API 是事实上的”窄腰”(slides 第 46–47 页)。只有少数公司同时控制应用和推理后端;大多数应用通过 OpenAI API 调用后端,所有后端都支持它。但它只携带输入 / 输出 token:哪段输入以后会复用、哪段输出不会再用,后端一概不知。它像 IPv4 一样僵化——slides 提到 Anthropic 曾加入用户自定义的缓存有效期,一年后又撤回。05 号脚本第 3 部分演示了代价:一次性的长提示与会被反复引用的文档混在一起时,只看 token 的 LRU 让文档命中率只有 45%;如果应用能标注”这段以后还会用”,同样的容量命中率是 99%。除非所有后端都支持新接口、所有应用都有强烈的动机,或者窄腰整体上移,很多研究想法会因此难以落地。

ML 与系统的新融合(slides 第 48–49 页)。传统的 MLSys 忠实地执行 ML 算法定下的步骤,因为(1)改变输出的有损优化不受欢迎,(2)模型变化太快,为某个模型专门优化不划算。LLM 让两个假设都变了:工业界习惯了输出的细微差别(同一个问题本来就会得到不同的回答),Transformer 与注意力成了新的”MapReduce”。于是大量结合 ML 与系统的想法被释放出来:KV 的有损压缩、对以前生成结果的语义复用、模型路由……但要注意事实类、格式敏感的查询。给系统方向学生的建议:至少深入理解一种流行的模型架构。本讲的 02 号脚本就是一个例子——不懂模型的”查找”发生在哪一层,就会照搬出最差的编码。

最后一条关于开源(slides 第 50–51 页):开源模型能否追上闭源模型、地缘政治的影响,以及 AI 编程让复制一个开源项目的功能变得容易——“开源起家”的商业模式可能不再成立。


8. 小结 · Summary

  1. KV 是数据:每个 token 几十到几百 KiB,一张 GPU 一天产生约 15 TB;存储比 GPU 便宜几个数量级,命中率百分之一左右就能回本。KV 正从 GPU 显存走向 CPU、SSD 与远端存储。
  2. 取得回来才有用:“取”比”算”快需要足够的带宽(约 KV 字节数 / 重算时间);大模型、长上下文、慢 GPU 时复用最划算,小模型在快 GPU 上常常不如重算。
  3. CacheGen:把 KV 当视频编码——锚点 + 增量、分层量化、按通道的算术编码;按块、多级编码、按测得的带宽自适应,带宽太低时退回发文本重算。它的观察是大模型的经验性质,在别的模型上要实测(小模型上局部性和层敏感度都不成立)。
  4. CacheBlend:非前缀复用丢了跨块注意力;只有少数 token 的 KV 偏差大,且各层一致;逐层过滤地重算约 15% 的 token 就能恢复质量,并用流水线把重算藏在加载后面。
  5. LMCache:把 KV 缓存从推理引擎里分离出来,做成跨引擎、跨实例、跨存储层级的服务;零拷贝共享、分层存储让多轮对话的 TTFT 降一个数量级。
  6. 教训:推理栈正在分层(引擎、数据存储、编排器、API);OpenAI API 这个窄腰隐藏了应用的意图;ML 与系统的边界正在消融。

9. 常见误区 · Common pitfalls

  1. “KV 存起来总比重算快。” 取决于带宽与模型:8B 模型在 H100 上重算 1.2 万 token 只要 0.36 秒,25 Gb/s 以下的链路都不如重算;多头注意力的 7B 模型从 NVMe 读 KV 几乎和重算一样慢(04 号脚本)。
  2. “通用压缩器(zip)就能压 KV。” 浮点数的低位近似随机,zlib 只压到 1.08 倍;有效的压缩要先量化到”刚好够用”,再用懂数据分布的熵编码。
  3. “CacheGen 的配方可以直接用在任何模型上。” 它的局部性、浅层敏感是 LLaMA 上的经验观察;在记录世界的小模型上两者都不成立,照搬反而最差。精度分配要在目标模型上实测。
  4. “RoPE 位置修正好了,拼接的 KV 就是对的。” 位置能修正,跨块注意力不能:后一块的 token 当初没看见前一块(04 号脚本:被切开的记录 8.8%)。
  5. “选择性重算随便挑一些 token 就行。” 随机重算 20% 只恢复到 21%;必须挑偏差最大的 token(99.4%)。
  6. “前缀缓存已经解决了复用问题。” 它只能复用开头完全相同的部分;多段 RAG、多轮对话里大量可复用的内容不是前缀。
  7. “KV 卸载到 CPU 只是多一级缓存,实现方式无所谓。” 拷贝次数与传输粒度决定了读回的有效带宽:同样 50 GB 的 CPU 层,10 GB/s 与 50 GB/s 的平均 TTFT 差 5 倍(05 号脚本)。

10. 练习 · Exercises

  1. LLaMA-3.1-70B 有 80 层、8 组 KV 头、头维度 128。算出每个 token 的 KV 字节数(BF16)。一段 32K token 的上下文有多大?
  2. 用 01 号脚本第 2 部分的数字:LLaMA-3.1-70B 重算 16K token 约 0.97 秒。KV 压缩 3 倍后,“取”比”算”快的带宽下限是多少 Gb/s?
  3. 为什么 CacheGen 让组内所有 token 都参照同一个锚点,而不是参照前一个 token?
  4. 一个通道的量化值分布是 {0: 0.7, ±1: 0.12 各, ±2: 0.03 各}。算出它的熵;如果用固定长度编码,每个值至少要几位?
  5. 把一段在位置 0 预先算好的 KV 放到位置 1000,K 和 V 各要做什么?为什么 V 不用动?
  6. 在记录世界里,块长为 6(偶数)时,有多少条记录会被切开?完全复用的准确率会是多少?
  7. CacheBlend 为什么可以”免费”地在第 0 层算出每个 token 的注意力偏差?在第 1 层还能这样做吗?
  8. 某模型 32 层;从 SSD 读一层的 KV 要 10 ms,完整预填充一层要 20 ms。CacheBlend 的控制器会选多大的重算比例?TTFT 大约是多少(忽略问题部分的计算)?
  9. LMCache 的独立服务进程相比”每个推理进程带一个 KV 库”,除了少拷贝,还有什么好处?代价是什么?
  10. 举一个例子说明”窄腰”如何阻碍了某个 KV 优化的落地,并设想一个可能的 API 扩展。

参考答案

  1. 2 × 80 × 8 × 128 × 2 = 327,680 字节 = 320 KiB;32K token ≈ 10.7 GB。
  2. KV = 16,000 × 327,680 ≈ 5.24 GB,压缩后 1.75 GB;1.75 GB / 0.97 秒 ≈ 1.8 GB/s ≈ 14.4 Gb/s。
  3. 参照同一个锚点,组内各 token 的编码 / 解码互不依赖,可以在 GPU 上并行;参照前一个 token 则必须串行解码,而且误差会沿链累积。
  4. H = −(0.7 log₂ 0.7 + 2 × 0.12 log₂ 0.12 + 2 × 0.03 log₂ 0.03) ≈ 0.360 + 0.734 + 0.304 ≈ 1.40 位;固定长度编码 5 个符号至少要 3 位。算术编码能接近 1.40 位。
  5. K 再旋转 1000·θ(RoPE 的旋转可以叠加);V 不经过 RoPE,与位置无关。
  6. 块长为偶数时,边界都落在偶数位置(键)上,记录不会被切开;完全复用的准确率与完整预填充相同(100%)。——这说明”块的边界是否切断依赖”决定了复用的损失,真实文本里依赖无处不在,切哪里都会断掉一些。
  7. 第 0 层的 K、V 只取决于 token 与位置,预先算的就是正确值;用这同一组 K、V,换两种掩码(只看本块 / 看全部)各算一次注意力,就得到预先计算时与完整预填充时的输出,相减即偏差。第 1 层的输入已经依赖上下文,要知道正确的 K、V 就得先算出第 0 层的输出——所以只能对选中的 token 做。
  8. 让重算时间 ≈ 读取时间:r × 20 ms = 10 ms,r = 50%(不低于 15%)。每层 max(10, 10) = 10 ms,TTFT ≈ 32 × 10 = 320 ms(完整预填充 640 ms)。
  9. 好处:KV 操作不占推理进程的时间;推理引擎升级不影响 KV 服务;不同引擎、不同实例可以共享同一份 KV;研究者可以在 KV 服务里写插件而不必改推理引擎。代价:进程间通信与同步、要和推理引擎约定接口(零拷贝需要共享内存 / GPU 直连)、多了一个需要运维和容错的服务。
  10. 例如:RAG 应用知道哪几段文档会被很多请求复用、哪些用户输入是一次性的,但 API 只传 token,后端的缓存只能按 LRU 猜(05 号脚本:45% vs 99%)。可能的扩展:请求里标注可复用的片段与期望的保留时间、显式的”预热”与”释放”调用、把文档以 ID 而不是文本传入(后端据此查 KV)。难点是让所有后端都支持、所有应用都愿意改。

11. 延伸阅读 · Further reading

  • CacheGen:Liu et al. (2024),SIGCOMM;代码 github.com/UChi-JCL/CacheGen。
  • CacheBlend:Yao et al. (2025),EuroSys;代码已并入 LMCache。
  • LMCache:github.com/LMCache/LMCache;vLLM Production Stack:github.com/vllm-project/production-stack。
  • PromptCache:Gim et al. (2024),MLSys——带位置缓冲区的模块化 KV 复用(完全复用的代表)。
  • KV 压缩与淘汰:KIVI(2 位 KV 量化)、H2O、SnapKV、StreamingLLM(附加讲 A4)。
  • 视频编码与自适应码率:H.264 的帧间预测;ABR 算法(如 BOLA、MPC)——CacheGen 的思路来源。
  • Mooncake(FAST’25 最佳论文):以 KV 缓存为中心的分离式架构(附加讲 A2)。

12. 下一讲预告 · Next

正式课程到此结束。附加讲 A1 回到”一个 LLM 应用是怎样搭起来的”:从数据管道、嵌入与向量数据库、编排、LLM 缓存、校验与日志,到 Triton、TGI、LightLLM 这些模型服务框架的设计取舍——动态批处理、模型仓库、Token Attention 与”预估未来显存”的路由器。

← 返回课程目录