对应 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
学完这一讲,你应该能够:
- 算出一个模型每个 token 的 KV 有多大、一张 GPU 一天产生多少 KV,并判断”把 KV 存起来、以后取回”何时比”重算”划算。
- 说清 CacheGen 的三个经验观察(token 局部性、层敏感度、按通道 / 层分组)与编码器的三步(增量、分层量化、算术编码),以及流式传输的自适应规则。
- 解释为什么直接拼接各块预先算好的 KV 会出错(丢了跨块注意力),CacheBlend 怎样只重算少数”偏差最大”的 token、怎样用流水线把重算藏在加载后面。
- 说出 LMCache 的定位:把 KV 缓存从推理引擎里分离出来,做成一个跨引擎、跨实例、跨存储层级的”AI 原生数据存储”。
- 复述 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 的 KV | 128K token |
|---|---|---|
| LLaMA-3.1-8B(GQA,8 组 KV) | 128 KiB | 16.8 GB |
| LLaMA-3.1-70B(GQA) | 320 KiB | 41.9 GB |
| Qwen3-235B-A22B(GQA,4 组 KV) | 188 KiB | 24.6 GB |
| DeepSeek-R1(MLA:每层一个 512 维潜向量 + 64 维 RoPE 键) | 68.6 KiB | 9.0 GB |
| OPT-13B(多头注意力) | 800 KiB | 104.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):
| 来源 \ 上下文长度 | 1K | 4K | 16K | 64K |
|---|---|---|---|---|
| GPU 重算(预填充) | 0.060 秒 | 0.225 | 0.968 | 5.341 |
| CPU 内存(PCIe 5.0) | 0.007 | 0.026 | 0.105 | 0.419 |
| 本地 NVMe SSD | 0.047 | 0.187 | 0.749 | 2.996 |
| 远端存储,100 Gb/s | 0.026 | 0.105 | 0.419 | 1.678 |
| 跨云,10 Gb/s | 0.262 | 1.049 | 4.194 | 16.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)
- token 方向的局部性:同一层、同一通道里,相邻 token 的 K / V 值相近。增量(相对于附近 token 的差)比原值集中得多——CacheGen 在 LLaMA 上测到增量的方差比原值小 2.4–2.9 倍。
- 层敏感度不同:输出质量对浅层 KV 的误差更敏感(误差会沿着层传播),对深层不太敏感。
- 按通道 / 层分组的信息增益:同一个通道(或同一层)里的值分布更集中,按它们分组统计概率分布,熵明显更低;按 token 位置分组几乎没有增益。
3.3 编码器(CacheGen §5.2)
- 基于变化的编码:把 token 分成每组 10 个;每组第一个 token 是”锚点”,用 8 位单独编码;其余 token 只编码相对锚点的增量(相对同一个锚点而不是前一个 token,这样可以并行编解码)。
- 分层量化:把层分成前、中、后三组,增量的量化步长从浅到深逐渐变大。
- 算术编码:量化后的整数用算术编码压成比特流;概率分布按”通道 × 层”离线统计一次(每个模型一套),编码器和解码器共用。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 | 问答准确率 |
|---|---|---|---|
| FP16 | 512 | 1.00× | 100% |
| FP16 + zlib(通用压缩器) | 472 | 1.08× | 100% |
| 均匀量化 8 位 / 6 位 / 4 位 | 277 / 213 / 149 | 1.85× / 2.40× / 3.43× | 100% / 100% / 75.5% |
| 照搬 CacheGen:浅层精细 + 增量(最细一级) | 188 | 2.72× | 70.3% |
| 按实测敏感度分配精度、不用增量 + 算术编码 | 167 | 3.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 的上下文):
| 带宽 | FP16 | 8 位量化 | CacheGen 级别 1 | 只传文本、GPU 重算 |
|---|---|---|---|---|
| 1 Gb/s | 31.5 秒 | 17.0 | 10.3 | 3.35 |
| 5 Gb/s | 6.29 | 3.41 | 2.05 | 3.35 |
| 25 Gb/s | 1.26 | 0.68 | 0.41 | 3.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 8 | 100% |
| 固定 CacheGen 级别 1 | 3.92 秒 | 勉强 | 1 1 1 1 1 1 1 1 | 100% |
| CacheGen 自适应 | 3.40 秒 | 是 | 1 0 0 文 文 文 文 3 | 96.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% |
| 完全复用拼接的 KV | 8.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 层之和) |
|---|---|---|
| 块首的值(记录被切开) | 300 | 1654 |
| 块首的键 | 300 | 20 |
| 其他 | 3000 | 3.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.093 | 0.094 | 4.7× |
| NVMe SSD | 0.437 | 0.431 | 0.431 | 1.0× | |
| LLaMA-3.1-70B(GQA,4 卡) | CPU 内存 | 1.611 | 0.135 | 0.337 | 4.8× |
| NVMe SSD | 1.611 | 0.347 | 0.347 | 4.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 B | 2 | 1.05 秒 |
| 每个进程各带 KV 库,CPU 间直接拷 | GPU A → CPU A → CPU B → GPU B | 1 | 0.47 秒 |
| 独立的 KV 服务进程(LMCacheMP) | GPU A → 共享 CPU 池 → GPU B | 0 | 0.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):
| 存储 | 历史命中率 | 平均 TTFT | P90 TTFT | GPU 重算历史 |
|---|---|---|---|---|
| 只用 GPU 里的前缀缓存 | 49% | 5.39 秒 | 18.9 秒 | 166 秒 |
| + CPU 50 GB(进程内卸载,有效 10 GB/s) | 77% | 0.72 | 2.86 | 75 |
| + CPU 50 GB(LMCache,50 GB/s) | 77% | 0.15 | 0.43 | 75 |
| + CPU 50 GB + SSD 1 TB | 97% | 0.08 | 0.21 | 10 |
只靠 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
- KV 是数据:每个 token 几十到几百 KiB,一张 GPU 一天产生约 15 TB;存储比 GPU 便宜几个数量级,命中率百分之一左右就能回本。KV 正从 GPU 显存走向 CPU、SSD 与远端存储。
- 取得回来才有用:“取”比”算”快需要足够的带宽(约 KV 字节数 / 重算时间);大模型、长上下文、慢 GPU 时复用最划算,小模型在快 GPU 上常常不如重算。
- CacheGen:把 KV 当视频编码——锚点 + 增量、分层量化、按通道的算术编码;按块、多级编码、按测得的带宽自适应,带宽太低时退回发文本重算。它的观察是大模型的经验性质,在别的模型上要实测(小模型上局部性和层敏感度都不成立)。
- CacheBlend:非前缀复用丢了跨块注意力;只有少数 token 的 KV 偏差大,且各层一致;逐层过滤地重算约 15% 的 token 就能恢复质量,并用流水线把重算藏在加载后面。
- LMCache:把 KV 缓存从推理引擎里分离出来,做成跨引擎、跨实例、跨存储层级的服务;零拷贝共享、分层存储让多轮对话的 TTFT 降一个数量级。
- 教训:推理栈正在分层(引擎、数据存储、编排器、API);OpenAI API 这个窄腰隐藏了应用的意图;ML 与系统的边界正在消融。
9. 常见误区 · Common pitfalls
- “KV 存起来总比重算快。” 取决于带宽与模型:8B 模型在 H100 上重算 1.2 万 token 只要 0.36 秒,25 Gb/s 以下的链路都不如重算;多头注意力的 7B 模型从 NVMe 读 KV 几乎和重算一样慢(04 号脚本)。
- “通用压缩器(zip)就能压 KV。” 浮点数的低位近似随机,zlib 只压到 1.08 倍;有效的压缩要先量化到”刚好够用”,再用懂数据分布的熵编码。
- “CacheGen 的配方可以直接用在任何模型上。” 它的局部性、浅层敏感是 LLaMA 上的经验观察;在记录世界的小模型上两者都不成立,照搬反而最差。精度分配要在目标模型上实测。
- “RoPE 位置修正好了,拼接的 KV 就是对的。” 位置能修正,跨块注意力不能:后一块的 token 当初没看见前一块(04 号脚本:被切开的记录 8.8%)。
- “选择性重算随便挑一些 token 就行。” 随机重算 20% 只恢复到 21%;必须挑偏差最大的 token(99.4%)。
- “前缀缓存已经解决了复用问题。” 它只能复用开头完全相同的部分;多段 RAG、多轮对话里大量可复用的内容不是前缀。
- “KV 卸载到 CPU 只是多一级缓存,实现方式无所谓。” 拷贝次数与传输粒度决定了读回的有效带宽:同样 50 GB 的 CPU 层,10 GB/s 与 50 GB/s 的平均 TTFT 差 5 倍(05 号脚本)。
10. 练习 · Exercises
- LLaMA-3.1-70B 有 80 层、8 组 KV 头、头维度 128。算出每个 token 的 KV 字节数(BF16)。一段 32K token 的上下文有多大?
- 用 01 号脚本第 2 部分的数字:LLaMA-3.1-70B 重算 16K token 约 0.97 秒。KV 压缩 3 倍后,“取”比”算”快的带宽下限是多少 Gb/s?
- 为什么 CacheGen 让组内所有 token 都参照同一个锚点,而不是参照前一个 token?
- 一个通道的量化值分布是 {0: 0.7, ±1: 0.12 各, ±2: 0.03 各}。算出它的熵;如果用固定长度编码,每个值至少要几位?
- 把一段在位置 0 预先算好的 KV 放到位置 1000,K 和 V 各要做什么?为什么 V 不用动?
- 在记录世界里,块长为 6(偶数)时,有多少条记录会被切开?完全复用的准确率会是多少?
- CacheBlend 为什么可以”免费”地在第 0 层算出每个 token 的注意力偏差?在第 1 层还能这样做吗?
- 某模型 32 层;从 SSD 读一层的 KV 要 10 ms,完整预填充一层要 20 ms。CacheBlend 的控制器会选多大的重算比例?TTFT 大约是多少(忽略问题部分的计算)?
- LMCache 的独立服务进程相比”每个推理进程带一个 KV 库”,除了少拷贝,还有什么好处?代价是什么?
- 举一个例子说明”窄腰”如何阻碍了某个 KV 优化的落地,并设想一个可能的 API 扩展。
参考答案
- 2 × 80 × 8 × 128 × 2 = 327,680 字节 = 320 KiB;32K token ≈ 10.7 GB。
- KV = 16,000 × 327,680 ≈ 5.24 GB,压缩后 1.75 GB;1.75 GB / 0.97 秒 ≈ 1.8 GB/s ≈ 14.4 Gb/s。
- 参照同一个锚点,组内各 token 的编码 / 解码互不依赖,可以在 GPU 上并行;参照前一个 token 则必须串行解码,而且误差会沿链累积。
- 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 位。
- K 再旋转 1000·θ(RoPE 的旋转可以叠加);V 不经过 RoPE,与位置无关。
- 块长为偶数时,边界都落在偶数位置(键)上,记录不会被切开;完全复用的准确率与完整预填充相同(100%)。——这说明”块的边界是否切断依赖”决定了复用的损失,真实文本里依赖无处不在,切哪里都会断掉一些。
- 第 0 层的 K、V 只取决于 token 与位置,预先算的就是正确值;用这同一组 K、V,换两种掩码(只看本块 / 看全部)各算一次注意力,就得到预先计算时与完整预填充时的输出,相减即偏差。第 1 层的输入已经依赖上下文,要知道正确的 K、V 就得先算出第 0 层的输出——所以只能对选中的 token 做。
- 让重算时间 ≈ 读取时间:r × 20 ms = 10 ms,r = 50%(不低于 15%)。每层 max(10, 10) = 10 ms,TTFT ≈ 32 × 10 = 320 ms(完整预填充 640 ms)。
- 好处:KV 操作不占推理进程的时间;推理引擎升级不影响 KV 服务;不同引擎、不同实例可以共享同一份 KV;研究者可以在 KV 服务里写插件而不必改推理引擎。代价:进程间通信与同步、要和推理引擎约定接口(零拷贝需要共享内存 / GPU 直连)、多了一个需要运维和容错的服务。
- 例如: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 与”预估未来显存”的路由器。