对应 Maps to:Stanford CS349D AI Inference Infrastructure(Spring 2026),#9 Mon Apr 27 Context Management。必读:R. Qin, Z. Li, W. He, M. Zhang, Y. Wu, W. Zheng, X. Xu, Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving(月之暗面 / 清华大学;FAST 2025 版的标题是 Mooncake: Trading More Storage for Less Computation);Y. Liu, H. Li, Y. Cheng 等, CacheGen: KV Cache Compression and Streaming for Fast Large Language Model Serving(SIGCOMM 2024)。选读:Z. Xie, Z. Xu, M. Zhao, Y. An, V. S. Mailthody, S. Mahlke, M. Garland, C. Kozyrakis, Strata: Hierarchical Context Caching for Long Context Language Model Serving(2025)。 依据说明:原课没有公开 slides。本讲依据 Mooncake 的 arXiv 技术报告全文(不是 FAST 版)、CacheGen 和 Strata 的全文。Strata 的作者里有本课的主讲 Christos Kozyrakis 和助教 Zhiqiang Xie。§1 的”加载还是重算”分析、§5 的综合和 §6”之后的发展”是我补充的。讲义里的纸上数字全部来自配套代码的实际运行。 配套代码 Code:
code/lecture09/(numpy;4 个脚本 + 1 个模块context_cache.py:存储层级与加载 vs 重算、tiny_llm 上真实的 CacheGen 式压缩、前缀缓存与合成负载、Strata 的 I/O 模型、Mooncake 式的集群调度与过载模拟;代码 03 约 20 秒,其余几秒) 预计阅读时间:3–4 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 解释 KV cache 为什么要离开 GPU 显存(多轮对话、文档问答、agent 都在反复读同一段长上下文),估算一个 token 的 KV 有多大、各层存储能放多少。
- 用”盈亏平衡带宽 = 每 token 的 KV 字节 ÷ 重算一个 token 的时间”判断一段上下文该存在哪里、该加载还是该重算,并说明它随上下文长度、模型大小、注意力结构怎样变化。
- 说清 Mooncake 的 KVCache-centric 架构:分布式 KV 池、前缀链式哈希、缓存感知 + 负载均衡的调度(算法 1)、热点迁移、分块流水线并行的 prefill、过载时的提前拒绝及其引起的负载振荡。
- 说清 CacheGen 的思路:网络带宽才是瓶颈时,把 KV 当作要压缩、流式传输的数据——三条经验观察、“差分 + 按层量化 + 按通道分布的算术编码”、按块自适应的压缩级别与”传文本”兜底。
- 说清 Strata 发现的两个问题:分页带来的碎片化 I/O(Little 定律)和 loading-bound 的 prefill,以及它的解法:GPU 辅助的 I/O、排布解耦、延迟命中的推迟、均衡组批、空档填充。
- 知道这几种技术各在什么条件下有用,以及我们的玩具实验在哪些地方没有复现论文的结论。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| 第 1 讲:KV cache、prefill / decode、TTFT | 存储与网络硬件:PCIe、RDMA、NVMe(前置讲 P5) |
| 第 3 讲:PagedAttention 的分页、RadixAttention 的前缀树与 LRU | 缓存理论:Belady 最优、LRU / LFU(前置讲 P3) |
| 第 4 讲:注意力的计算量随上下文长度增长 | 排队论与 Little 定律的推导(前置讲 P4) |
| 第 7 讲:prefill / decode 解耦、KV 传输、goodput | 集群级的前缀感知路由(第 11 讲 Preble);agent 的记忆(第 14 讲) |
0.3 本讲地图 Map
§1 为什么 KV cache 要走出显存:大小、存储层级、加载还是重算 ← 代码 01
§2 Mooncake:以 KV cache 为中心的解耦架构、调度与过载 ← 代码 03 演示 1、代码 04
§3 CacheGen:把 KV 压成比特流,按带宽自适应地发送 ← 代码 02
§4 Strata:分层缓存的 I/O 效率与 loading-bound 的调度 ← 代码 03 演示 2–4
§5 综合:放在哪、压不压、何时加载
§6 之后的发展
§7–11 小结、常见误区、练习、延伸阅读、下一讲预告
0.4 记号 Notation
| 符号 | 含义 |
|---|---|
| KV / token | 一个 token 在所有层的 K 和 V 一共多少字节: 层数 KV 头数 头维度 字节数 |
| 前缀缓存(prefix caching) | 复用另一个请求已经算好的、相同前缀的 KV;只能复用从头开始连续相同的部分 |
| 块(block)/ 页(page) | KV 的存储和匹配单位:Mooncake 的 trace 用 512 token 一块,vLLM 默认 16,SGLang 默认 1 |
| 命中率 | 命中的块(或 token)占总数的比例 |
| 盈亏平衡带宽 | 加载 KV 的带宽低于它时,重算比加载更快 |
| TTFT | 首 token 延迟;本讲里它常常由”加载 KV”或”重算 prefill”主导 |
| loading-bound | prefill 的时间由”把缓存的 KV 搬回 GPU”决定,而不是由计算决定(Strata 的用语) |
0.5 关于数字 · About the numbers
本讲的数字有三类:(1) numpy 上的真实计算:tiny_llm(8 层、随机权重)的 KV 在压缩前后对输出的影响(代码 02 演示 1–2);(2) 纸上 GPU + 传输模型:重算时间来自第 1 讲的 hw_model,搬运时间 = 次数 × 固定开销 + 字节 ÷ 带宽,其中 PCIe 的固定开销按 Strata 图 3 校准;(3) 模拟:合成负载上的缓存回放、prefill 集群的离散事件模拟、过载准入的模拟。合成负载的参数调到和 Mooncake 开源 trace 的复用潜力接近,但它不是真实流量。论文里的数字写成”论文报告”。
1. 为什么 KV cache 要走出 GPU 显存 · Why context caching
1.1 同一段上下文被反复读
第 3 讲的前缀缓存(RadixAttention)已经告诉我们:多个请求共享同一段前缀时,KV 只需要算一次。在今天的应用里,这种复用无处不在:
- 多轮对话:每一轮都把之前的全部历史重新送进模型,第 20 轮要把前 19 轮再读一遍;
- 文档问答 / RAG:同一份财报、同一份技术手册被很多用户、很多问题反复使用;
- agent:同一个任务的几十步推理共享越来越长的上下文(工具调用的结果、之前的思考);
- 系统提示:几乎每个请求都以同一段系统提示开头。
问题是 KV cache 太大了。Llama-3-8B 每个 token 的 KV 是 128 KiB(代码 01 演示 1):
| 模型 | KV / token | 40 GB 显存能放 | 1 TB CPU 内存能放 | 一份 10 万 token 文档的 KV |
|---|---|---|---|---|
| Llama-3-8B | 128 KiB | 0.31 M token | 7.6 M token | 13.1 GB |
| Qwen2.5-14B | 192 KiB | 0.20 M token | 5.1 M token | 19.7 GB |
| Llama-3-70B | 320 KiB | 0.12 M token | 3.1 M token | 32.8 GB |
| Llama-3-8B 的假想 MHA 版(32 个 KV 头) | 512 KiB | 0.08 M token | 1.9 M token | 52.4 GB |
40 GB 显存只能放约 30 万个 token 的 KV(Strata 论文的数字也是约 0.3 M)——几份长文档、几百轮对话就满了。CacheGen 举的例子:亚马逊 2023 年的年报约 8 万 token,用 Llama-34B 处理,KV 有 19 GB,和模型本身一样大(论文报告)。同一份文档的两次使用可能隔几个小时,KV 不可能一直占着显存。
于是 KV cache 必须分层存放:显存放正在用的,CPU 内存(TB 级)和 SSD(几十 TB)放可能还会用的,甚至放到别的机器上。用的时候再搬回来。
1.2 加载还是重算
搬回来要时间,重算也要时间,谁快?代码 01 演示 2 算了 Llama-3-8B 在单块 H100 上的账(8,000 token 的上下文,KV 1.05 GB,重算整段 188 ms):
| 存储层 | 有效带宽 | 加载时间 | 加载 / 重算 |
|---|---|---|---|
| CPU 内存(PCIe 5.0 x16) | 51.2 GB/s | 20.5 ms | 0.11 |
| 远端内存(400 Gb/s RDMA) | 40.0 GB/s | 26.2 ms | 0.14 |
| 本地 NVMe SSD | 5.6 GB/s | 187 ms | 1.00 |
| 数据中心网络 10 Gb/s | 1.0 GB/s | 1.05 s | 5.59 |
| 云服务器之间 3 Gb/s | 0.3 GB/s | 3.50 s | 18.6 |
从 CPU 内存或远端内存加载,比重算快 7–9 倍——这是 Mooncake、Strata 这类分层缓存系统的基本账。SSD 和重算打平。可在普通的云网络上,搬 KV 比重算慢得多:KV 是 GB 级的浮点张量,“省了计算”的代价是”费了网络”——这正是 CacheGen 的出发点(§3)。
一个简洁的判据:盈亏平衡带宽 = 每个 token 的 KV 字节数 ÷ 重算一个 token 的时间。加载通道比它快就加载,比它慢就重算(代码 01 演示 3,每块 GPU 的值):
| 模型 | 上下文 1K | 8K | 32K | 128K |
|---|---|---|---|---|
| Llama-3-8B | 6.2 GB/s | 5.6 GB/s | 4.1 GB/s | 1.9 GB/s |
| Qwen2.5-14B | 5.0 | 4.5 | 3.2 | 1.5 |
| Llama-3-70B(8 卡,每卡加载 1/8) | 1.6 | 1.5 | 1.3 | 0.7 |
| Llama-3-8B 的假想 MHA 版 | 22.4 | 20.5 | 15.1 | 7.4 |
三个规律:
- 上下文越长,越值得存:注意力的计算随长度平方增长,加载只随长度线性增长,平衡带宽随之下降;
- 模型越大,越值得存:70B 每个 token 的计算是 8B 的约 9 倍,KV 只多 2.5 倍(GQA);
- KV 越小,越容易存:GQA 版的平衡带宽只有多头注意力版的约四分之一;MLA(第 8 讲)把 KV 压得更小。
代码 01 演示 4 把它放进一段 20 轮的对话:第 20 轮的历史有 11,400 个 token,不缓存时 TTFT 282 ms;显存命中时 6.7 ms;缓存在 CPU 内存时 35.9 ms;在 3 Gb/s 的网络另一端时 4.99 s,比重算还慢得多。
分层缓存因此有四个设计问题:放在哪一层(容量 vs 带宽)、要不要压缩(CacheGen)、加载能不能和计算重叠、搬运本身效率高不高(Strata)、调度器知不知道 KV 在哪、加载要多久(Mooncake、Strata)。
2. Mooncake:以 KV cache 为中心的架构 · Mooncake
2.1 架构:把整个集群的 CPU 内存和 SSD 变成 KV 池
Mooncake 是月之暗面 Kimi 的服务平台。它的出发点是一个观察:KV cache 的调度是 LLM 服务调度的核心。提高吞吐有两条路——尽量复用 KV(少算),尽量把批做大(算得更有效率);但从远处搬 KV 会拉长 TTFT,大批会拉长 TBT。所以调度要围绕 KV cache 来做。
Mooncake 的架构(论文图 1)有三部分:
- prefill 池和 decode 池:两个阶段分开部署(第 7 讲的解耦);
- 分布式 KV 池:把 GPU 集群里闲置的 CPU 内存、SSD 和 RDMA 网卡拼成一个大的 KV 缓存,“不额外花钱地”提供大容量和高带宽;KV 以分页的块存储,块之间用 GPUDirect RDMA 传输(Messenger 组件);
- Conductor(全局调度器):根据 KV 的分布和各实例的负载派发请求,必要时复制热块、换出冷块。
一个请求的流程(论文图 4):
- KV 复用:Conductor 选一对 prefill 实例和 decode 实例;prefill 实例把能复用的前缀 KV 从(可能是远端的)CPU 内存加载进 GPU;
- 增量 prefill:只算没命中的部分;新算出的 KV 存回 CPU 内存;未命中的 token 超过一个阈值(通常 1000 多)时分块、流水线地做;
- KV 传输:与 prefill 重叠,逐层把 KV 流式发到 decode 实例的 CPU 内存(第 7 讲 Splitwise 的逐层传输);
- decode:KV 全部到达后,请求加入 decode 实例的连续批处理;decode 实例的本地调度器会再检查一次 TBT 的 SLO,必要时拒绝——这时 prefill 就白做了(§2.5)。
prefill 阶段的优化目标是”在 TTFT 的 SLO、最低 MFU、KV 不超过 DRAM 的约束下最大化缓存复用”;decode 阶段是”在 TBT 的 SLO、KV 不超过显存的约束下最大化吞吐”(论文图 1)。
2.2 前缀链式哈希与真实负载
KV 池里的每个块带一个哈希值,它由”本块的 token”和”前一块的哈希值”共同决定(论文图 3):
于是两个请求的第 块哈希相同,当且仅当它们前 块的全部 token 都相同——一次哈希比较就判断了”整个前缀是否相同”,既用于匹配,也用于去重。vLLM 的前缀缓存用的是同样的思路(代码里的 chain_block_ids)。
Mooncake 开源了一个小时的真实请求 trace(23,608 个请求,只有到达时间、输入输出长度和重映射后的块哈希,没有任何用户内容)。统计特征(论文报告):平均输入 8,590 个 token、平均输出 182 个——长上下文、短输出;假设一个无限大的全局缓存,也只有约 55% 的块能复用;超过一半的块从未被复用,个别块被访问上万次。三种淘汰策略的命中率:
| 容量(块) | 无限 | 100,000 | 50,000 | 30,000 | 10,000 | 1,000 |
|---|---|---|---|---|---|---|
| LRU | 0.55 | 0.55 | 0.55 | 0.54 | 0.46 | 0.34 |
| LFU | 0.55 | 0.55 | 0.55 | 0.50 | 0.40 | 0.34 |
| LengthAware(类似 LFU,优先淘汰请求中靠后的块) | 0.55 | 0.54 | 0.53 | 0.50 | 0.40 | 0.34 |
论文的解读:LRU 最好,大概是因为请求在时间上的局部性;不要因为容量超过 5 万块后命中率不再上升就认为大缓存没用——这个 trace 只是真实流量的一个子集。
我们的合成负载(代码 03 演示 1)把三类请求(文档问答、多轮对话、一次性请求)的比例和长度调到和真实 trace 接近:无限容量下约 52% 可复用,84% 的块从未被复用,前 1% 的块贡献了一半以上的命中。但它的命中率随容量下降得更快(1,000 块时只有 0.19–0.25),而且容量很小时 LFU 最好、中等容量时 LRU 最好——真实流量的时间局部性比合成负载强,淘汰策略的优劣取决于负载。
2.3 prefill 池:为什么分块了还要解耦,以及分块流水线并行
有了分块 prefill(第 7 讲 Sarathi-Serve),还需要单独的 prefill 池吗?Mooncake 选择保留,两个理由:
- 长上下文需要跨节点并行。128K 甚至 1M 的输入,单台 8 卡机器的 TTFT 太长。跨节点张量并行每层要两次 RDMA all-reduce,MFU 很低;序列并行(Ring Attention 等)每层至少一次跨节点通信,还要和 KV 传输抢网络。Mooncake 的分块流水线并行(chunked pipeline parallelism, CPP):每 X 个节点组成一个流水线组,一个请求的输入切成块,不同的块同时在不同的节点上处理——只在流水线级之间通信,容易与计算重叠;对短上下文也没有额外开销,不需要频繁地动态调整节点分组。
- 节省显存。一个请求的 KV 大小为 、处理时间为 ,它占用显存的代价是 。分块 prefill 把 拉长,代价变大。Mooncake 的逐层 prefill:每层算注意力之前等这一层的 KV 加载完、同时触发下一层的加载;算完立刻异步存回。加载和存储都与计算重叠,prefill 调度因此可以不管显存的余量(只要放得下一个请求),只看 KV 的分布和 DRAM 的余量。
2.4 KVCache-centric 调度
为 prefill 选实例时,Mooncake 不只看负载,还看每个实例本地能命中多长的前缀(论文算法 1,简化后):
对每个 prefill 实例 i:
前缀_i ← 实例 i 本地能匹配的块数
若 最长前缀 ÷ 前缀_i < 阈值: # 本地的前缀已经够好
TTFT_i ← 排队时间_i + prefill 时间(输入长度, 前缀_i)
否则: # 别的实例有长得多的前缀
TTFT_i ← 传输时间(最长前缀 − 前缀_i) + 排队时间_i + prefill 时间(输入长度, 最长前缀)
选 TTFT 最小的实例;若 TTFT 或 TBT 超过 SLO 就拒绝(返回 HTTP 429)
若选中的实例要从别处拉前缀,就把那些块复制过来(热点迁移)
prefill 时间用离线测量的模型按”输入长度、命中长度”预测(Transformer 的计算很规律,误差小);排队时间是队列里各请求预测时间之和;最难预测的是传输时间——它取决于网络是否拥塞,这也是热块必须复制到多台机器上的原因。Mooncake 不去预测每个块未来的热度(流量变化太快),而是用启发式的热点迁移:请求没有被派到前缀最长的实例时(因为那里太忙),新实例会主动拉取并本地保存这些块;热门前缀于是自然地出现在多台机器上。
论文图 8 的实验(8 个 prefill 实例 + 8 个 decode 实例,回放 23,000 个真实请求):随机调度、负载均衡、缓存感知、KVCache-centric 的平均 TTFT 依次降低(图中四个柱子的数值约为 92、60、14、6 秒;对应关系是我按”依次降低”推断的)。
代码 04 演示 1 在纸上复现了这个排序(8 个 Llama-3-70B prefill 实例,每实例 8 块 A100、1 TB 本地 KV 缓存;高负载 12.2 req/s 时):
| 调度 | 平均 TTFT | P90 TTFT | token 命中率 | 跨机拉取 |
|---|---|---|---|---|
| 随机 | 25.1 s | 56.1 s | 0.29 | — |
| 负载均衡 | 8.74 s | 23.7 s | 0.29 | — |
| 缓存感知(本地前缀 + 排队) | 0.89 s | 2.11 s | 0.43 | — |
| KVCache-centric(可跨机拉取) | 0.64 s | 1.53 s | 0.47 | 35,766 块 |
随机和负载均衡把同一份文档、同一个会话的请求分散到 8 台机器上,命中率只有 0.29,等于多花了算力;缓存感知把命中率提到 0.43,平均 TTFT 低一个数量级;KVCache-centric 再允许”从别处拉更长的前缀”——800 Gb/s 的 RDMA 拉几 GB 只要几十毫秒,比 70B 模型重算几秒便宜得多(§1.2 的盈亏平衡带宽:每卡约 1.6 GB/s),所以它经常拉,命中率升到 0.47,平均 TTFT 再降 28%。我原本以为它的好处来自更均匀的负载,结果”最忙实例 / 平均”和缓存感知差不多:好处来自”不在本机的前缀也能用上”。
2.5 过载:提前拒绝与负载振荡
大多数论文假设所有请求都会被处理。Mooncake 面对的现实是 GPU 供应有限、Kimi 的用户增长很快,高峰期常态过载。这时必须拒绝一部分请求,而且要尽早:Mooncake 定义的 goodput 只计完整完成的请求,做到一半被拒的请求所消耗的资源全部浪费。
- 基线:到达时只看 prefill 实例的负载;decode 实例的负载要等 prefill 做完才检查,满了就拒——prefill 白做。
- 提前拒绝(early rejection):到达时就同时看 prefill 和 decode 两个池的负载,取较大者决定接不接。
- 负载振荡:提前拒绝在生产中引起了 prefill 与 decode 负载的反相振荡(论文图 9)。原因是时间差:决定是基于”此刻”的 decode 负载做的,而这个请求要几秒后才进入 decode。decode 低时大量接收 → prefill 满载 → 这批请求进入 decode,decode 变高 → 拒绝新请求,prefill 空下来 → decode 渐渐排空 → 又大量接收……(论文图 10a)。prefill 机器越少、prefill 越长,振荡越明显。
- 基于预测的提前拒绝:预测这个请求 prefill 做完时 decode 池的负载。请求级的预测要知道每个请求的输出长度,太难;Mooncake 用系统级的预测:假设每个请求的 decode 都持续同样的时间 ,把会在那之前做完 prefill 的请求加进去、把会在那之前结束的请求去掉,算平均 TBT 与 SLO 之比。
论文表 3(8P + 8D,23,000 个真实请求以 2 倍速回放):拒绝的请求数从基线的 4,183 降到提前拒绝的 3,771、基于预测的 3,589(论文报告)。
代码 04 演示 2 的模拟(到达率约为能力的 2 倍;prefill 池 6 个实例,decode 池最多 12 个请求,TTFT 的 SLO 12 s;3 个随机种子的平均):
| 准入策略 | 完成 | prefill 后被拒 | 浪费的 prefill | prefill 负载的波动(标准差) |
|---|---|---|---|---|
| 基线(只看 prefill) | 1,840 | 512 | 2,088 s | 0.094 |
| 提前拒绝 | 1,722 | 329 | 1,401 s | 0.191 |
| 基于预测的提前拒绝 | 1,750 | 298 | 1,404 s | 0.129 |
| 理想预测(已知每个请求的 decode 时长) | 1,791 | 186 | 1,133 s | 0.136 |
一部分复现了:基线浪费了两千多秒的 prefill;提前拒绝把浪费减少约三分之一,同时确实造成了振荡(prefill 负载的波动翻倍,时间线里 prefill 频繁空闲);基于预测的版本明显减弱了振荡。但没有复现的是:三种提前拒绝完成的请求都比基线少 3%–7%,连理想预测也少 3%。基线虽然浪费 prefill,却总有刚做完 prefill 的请求等着填 decode 的空位,decode 几乎一直是满的;提前拒绝偶尔会拒掉”等它做完 prefill 时其实会有空位”的请求。我试了多组参数,方向都一样。一个可能的解释:真实系统里白做的 prefill 会推迟其他请求、让它们的 TTFT 超标而失败,而我们的模型里被接受的请求一定满足 TTFT(预测是精确的),浪费没有”传染性”。这提醒我们:准入控制的收益高度依赖于系统里哪一部分才是真正的瓶颈,以及预测有多准。
2.6 评测与论文卡片
论文用一个与 LLaMA2-70B 结构相同的”假”模型(保护商业信息),每个实例 8 块 A800,节点间 RDMA 最高 800 Gb/s,对比 vLLM(论文报告):
- 公开数据集:Mooncake-[3P+1D] 相对 vLLM-[4M](4 个共置实例)在 ArXiv 摘要和 L-Eval 上吞吐分别高 20% 和 40%(L-Eval 有大量前缀复用);[2P+2D] 的 TTFT 不如 [3P+1D]——P / D 的比例很重要(第 7 讲)。
- 模拟数据(16K–128K 的长输入,50% 可复用):吞吐高 50%–525%;长输入严重干扰 vLLM 的 decode。
- 真实负载(10P+10D vs 20 个 vLLM 实例,TTFT 上限 30 s、TBT 上限 0.1 s):两者的 TTFT 都几乎全部达标,但 vLLM 只有 57% 的请求满足 TBT,Mooncake 约 100%;在 SLO 下 Mooncake 能多处理约 75% 的请求。
| 项目 | 内容 |
|---|---|
| 论文 | Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving,Qin 等(月之暗面、清华大学);FAST 2025 |
| 一句话 | 用整个集群闲置的 CPU 内存和 SSD 组成分布式 KV 池,以 KV cache 的位置和复用为中心调度 prefill / decode 解耦的服务,并在过载时尽早拒绝 |
| 问题 | 长上下文、高复用、常态过载的生产负载下,既要尽量复用 KV、又要满足 TTFT / TBT 的 SLO |
| 关键洞察 | KV 的调度是服务调度的核心;用存储换计算(复用 KV 比重算便宜);过载时只有完成的请求才算数 |
| 机制 | 分布式 KV 池 + RDMA 传输;前缀链式哈希;分块流水线并行与逐层 prefill;缓存感知 + 负载均衡 + 热点迁移的调度;基于预测的提前拒绝 |
| 评测 | 模拟场景吞吐最多高 525%;真实负载下多处理约 75% 的请求(论文报告) |
| 局限与假设 | 用假模型和回放 trace 评测;传输时间与热度难以预测,阈值手调;系统级预测假设 decode 时长相同;P / D 比例需预设;真实 trace 最多只有 55% 可复用 |
| 与本课的关系 | 第 3 讲的前缀缓存从”一块 GPU”扩展到”整个集群的存储”;第 7 讲的解耦加上 KV 的分层存储和过载控制 |
3. CacheGen:把 KV cache 压成比特流 · CacheGen
3.1 被忽视的网络瓶颈
当 KV 不在本机时(文档的 KV 存在专门的存储服务里,或者下一个请求被派到了别的 GPU),就要通过网络取回来。以前的系统要么假设 KV 一直留在同一块 GPU 上,要么假设有 NVLink 这样几百 Gb/s 的高速链路。可普通云服务器之间的带宽常常只有个位数 Gb/s——§1.2 的表里,3 Gb/s 下加载 8K 上下文的 KV 比重算慢 18 倍。
CacheGen 的思路(论文图 2):
- 传文本:数据最少,但要重算 prefill,计算延迟最高;
- 传 KV:不用重算,但要传的数据最多;
- 传压缩过的 KV(或在带宽很差时传文本):两头都省。
关键的区分:已有的 KV 压缩(丢掉不重要的 token,如 H2O;或量化 KV)是为了缩小 KV 在显存里的大小,所以必须保持张量的形状、让模型能直接使用;CacheGen 要缩小的是 KV 在传输时的大小,因此可以把它编码成任意紧凑的比特流,到了 GPU 再解码。两者还可以叠加。
3.2 三条经验观察
CacheGen 在 Llama-7B / 13B 和 LongChat 数据集上观察到(论文 §5.1):
- 相邻 token 的局部性:同一层、同一通道里,位置相近的 token 的 K、V 值很像;相邻 token 的差值比原值集中得多,方差低 2.4–2.9 倍。
- 各层敏感度不同:给浅层的 KV 加误差,输出质量下降得多;给深层加同样的误差影响小。
- 按(层, 通道)分组的信息增益远大于按 token 分组:把数按层或按通道分组后,每组的熵明显下降;按 token 位置分组几乎没用。
我们在随机权重的 tiny_llm(8 层)上检查了这三条(代码 02 演示 1):
- 观察 1 不成立:差分后的方差反而比原值大(原值 / 差分 = 0.51–0.65)。随机权重的模型里相邻 token 的 KV 几乎独立,两个独立量相减方差翻倍。真实模型的局部性是训练出来的,我们的玩具模型没有。
- 一个合成的”像 LLM 的” KV(每个通道有固定偏移,相邻 token 的相关系数为 0.8)上,差分后方差小 2.45 倍,与论文的量级相当;可如果先减去每个通道的均值再比,差分反而更差(0.80)——在这组数据里,差分的好处全部来自去掉通道偏移,按通道量化也能拿到。相关系数到 0.97 时,去掉偏移之后差分仍有 3.1 倍的好处。差分有多大用,取决于相邻 token 到底有多像。
- 观察 2 成立:同样量化到 2 位,只动最浅的两层时输出的 KL 散度是只动最深两层的约 5 倍。浅层的误差会经过后面所有层放大。
3.3 编码器
三条观察对应编码器的三步(论文 §5.2):
- 以锚点为参照的差分:每 10 个连续 token 为一组,组里第一个 token 是”锚点”,用 8 位量化单独存;其余 token 存”自己 − 锚点”。和锚点而不是和前一个 token 做差,是为了各 token 能并行解码(类似视频编码里的 GOP,但更利于并行)。
- 按层分组的量化:把层分成前、中、后三组,量化步长从浅到深依次变大——把比特花在敏感的层上。
- 算术编码:把量化后的符号无损地压成比特流。算术编码给常见符号分配短码,效率取决于概率分布有多准、多”尖”。CacheGen 为每个(层, 通道)组合离线统计一个分布(用同一个模型在其他上下文上产生的 KV),所有上下文共用——比一个全局分布最多省 53%。
解码在 GPU 上用 CUDA 实现(每个线程负责一个 token),并与下一块的传输流水线进行,额外开销很小。
我们在 tiny_llm 上做了消融(代码 02 演示 2;分布用另外 3 段上下文离线统计,码长用 估计,算术编码能逼近它到 1% 以内):
| 编码方式 | 位 / 数 | 相对 FP16 | KL 散度 |
|---|---|---|---|
| 均匀量化 8 位 | 8.00 | 2.0× | 0.00000 |
| 均匀量化 4 位 | 4.00 | 4.0× | 0.00050 |
| 均匀量化 3 位 | 3.00 | 5.3× | 0.00201 |
| 量化(步长 0.4)+ 熵编码,全局分布 | 3.38 | 4.7× | 0.00025 |
| + 每个(层, 通道)一个分布 | 3.24 | 4.9× | 0.00025 |
| + 相对锚点的差分 | 4.07 | 3.9× | 0.00021 |
| + 按层的步长(不用差分) | 3.41 | 4.7× | 0.00016 |
“量化 + 熵编码”是主要的收益:约 3.2 位就达到了 4 位均匀量化一半的误差——量化后的符号集中在 0 附近,固定位宽浪费了。按通道的分布只多省约 0.1 位(真实模型的通道之间差异大得多,有离群通道);差分在我们的数据上反而多花 0.8 位(和观察 1 一致);按层的步长多花约 0.2 位,KL 降低约 36%。这些是随机模型上的数字,只说明机制;论文报告:同样的质量下比 8 位量化小 3.5–4.3 倍。
3.4 按带宽自适应地发送
一次 KV 传输要几百毫秒到几秒,期间带宽会变。CacheGen 借鉴视频流的做法(论文 §5.3):
- 离线把上下文切成块(默认 1.5K token),每块编码成几个不同压缩级别的比特流,块与块之间独立解码;
- 发送时一块一块地发,每块选一个配置:某个压缩级别,或者传文本、让 GPU 在前面各块的 KV 之上重算这一块;
- 用上一块实测的吞吐估计带宽,选”预期延迟仍在 SLO 内、压缩损失最小”的配置;带宽波动时最多晚一块反应过来。
块长度的取舍:太大,对带宽变化反应慢;太小,传文本重算时 GPU 的批太小、利用率低。
代码 02 演示 3 用 Mistral-7B、9,600 token 的上下文做了模拟(三个示意的级别:中档取论文表 1 的 176 MB ≈ 每个数 2.3 位):
| 带宽模型 | SLO | 8 位量化 | CacheGen 中(固定) | CacheGen 高(固定) | CacheGen 自适应 |
|---|---|---|---|---|---|
| 分段常数、平均持续 1 秒 | 0.5 s | 超时 100% | 23%(2.3 位) | 34%(3.0 位) | 17%(2.6 位) |
| 分段常数、平均持续 1 秒 | 1 s | 49% | 5%(2.3 位) | 10%(3.0 位) | 7%(2.9 位) |
| 每块独立随机 | 1 s | 91% | 20%(2.3 位) | 29%(3.0 位) | 32%(2.9 位) |
压缩本身是最大的收益(8 位量化在 SLO = 0.5 s 时全部超时)。带宽的变化有持续性时,自适应策略在质量更高的同时超时更少;可如果每块的带宽完全独立随机,“用上一块预测下一块”就是在追噪声,自适应反而更差。论文图 13 说它的带宽是每块独立抽样的,却报告 SLO = 1 s 时超时比例从量化基线的 81% 降到 8%——我们的简单策略复现不了这一点。
3.5 评测与论文卡片
论文在 Mistral-7B、Llama-34B、Llama-70B 和 4 个长上下文数据集(662 段上下文,1.4K–16K token)上评测,A40 GPU,带宽 3 Gb/s(论文报告):
- KV 大小:同样的质量下比量化基线小 3.5–4.3 倍;质量下降不超过 2%(准确率)、0.1%(F1)、0.1(困惑度)。表 1 的例子(Mistral-7B、LongChat):8 位量化 622 MB,CacheGen 176 MB(准确率 0.98 vs 1.00);叠加在 H2O 上从 282 MB 到 71 MB,叠加在 LLMLingua 上从 492 MB 到 183 MB。
- TTFT:比量化基线快 3.2–3.7 倍,比传文本重算快 3.1–4.7 倍;即使和几乎无损的 8 位量化比也快 1.67–1.81 倍。
- 自适应:SLO = 1 s 时超时比例从 81% 降到 8%;SLO = 0.5 s 时同样质量下超时少 60%。
- 编码一段上下文约 200 ms,存多个级别的总存储量与量化基线相当;用户研究里 QoE 更高。
| 项目 | 内容 |
|---|---|
| 论文 | CacheGen: KV Cache Compression and Streaming for Fast Large Language Model Serving,Liu 等,SIGCOMM 2024(芝加哥大学等) |
| 一句话 | 把要通过网络取回的 KV cache 编码成紧凑的比特流(差分 + 按层量化 + 按通道分布的算术编码),并按带宽逐块选择压缩级别或改传文本 |
| 问题 | KV 复用省了计算,但 KV 不在本地时,GB 级的张量在普通网络上传输会让 TTFT 比重算还长 |
| 关键洞察 | 传输时不必保持张量格式;KV 有可被利用的统计结构(局部性、层的敏感度、通道的分布) |
| 机制 | 锚点差分;三组层的量化步长;离线统计的(层, 通道)分布 + GPU 算术编码;1.5K token 的块、多级别编码、按块自适应、传文本兜底;解码与传输流水线 |
| 评测 | KV 小 3.5–4.3×,TTFT 快 3.2–3.7×(相对量化基线),质量下降很小(论文报告) |
| 局限与假设 | 有损压缩会改变输出(虽然很少);依赖 KV 的统计结构(在我们的随机模型上局部性不存在);离线分布与模型绑定;自适应的效果依赖带宽变化的时间尺度;评测在单机、中等长度的上下文上 |
| 与本课的关系 | §1 的”加载 vs 重算”在网络瓶颈下的答案:先压缩,再逐块决定;和 Mooncake 的 KV 池、第 7 讲的 KV 传输互补 |
4. Strata:分层缓存的 I/O 效率与调度 · Strata
Mooncake 和 CacheGen 回答了”KV 存在哪、怎么传”。Strata 关心一个更底层的问题:KV 已经在本机的 CPU 内存里了,把它搬回 GPU 为什么还这么慢?以及调度器为什么对此视而不见?Strata 建立在 SGLang 之上,已在生产中部署。
4.1 问题一:分页造成碎片化的 I/O
Strata 用 Little 定律分析 I/O 的吞吐:设并发的传输数为 、每次传输的大小为 、每次的延迟为 ,则可达到的吞吐
要提高吞吐,就要更高的并发、更大的传输或更低的延迟。CPU 发起的 cudaMemcpyAsync 每次都有不可忽略的启动与调度延迟,并发也受驱动和硬件队列的限制;实际上能调的主要是 。要用到 PCIe 5.0 峰值的 75%–80%,每次传输要到 1–2 MB。
可推理引擎偏爱小页:PagedAttention 的页越小,显存碎片越少(第 3 讲),前缀匹配越精细(页内的部分前缀无法复用)。TensorRT-LLM、vLLM、SGLang 的默认页大小分别是 32、16、1 个 token;再加上 GPU 里通常按层排布(每层一个 K 池、一个 V 池),一页一层的 K 只有几 KB 到几十 KB。结果(Strata 图 3):Llama-3.1-8B、8,192 个 token、页大小 32,只用到 PCIe 5.0 峰值的约 22%;在 GH200(CPU 与 GPU 之间是 NVLink-C2C,峰值高 6 倍)上低至约 5%。
页能不能放大?Strata 图 2(Mistral-24B、ShareGPT):页从 1 增大到几百,命中率大幅下降,平均和 P90 TTFT 分别涨了 2 倍和 2.9 倍。缓存想要小页,搬运想要大块——这是一个真实的矛盾。
代码 03 演示 3 用”时间 = 传输次数 × 固定开销 + 字节 ÷ 带宽”复现了这组数字(固定开销 3.6 µs,按 22% 校准):
| 平台 | 按层排布、每段一次 memcpy(页 32) | 按页排布、每页一次 memcpy(页 32) | GPU 辅助的 I/O |
|---|---|---|---|
| PCIe 4.0 | 36% | 97% | 78%(假设) |
| PCIe 5.0 | 22%(校准点) | 95% | 78%(Strata:约 50 GB/s) |
| GH200 NVLink-C2C | 5% | 75% | 78%(假设) |
页大小 1 时,按层排布的有效带宽不到峰值的 1%;带宽越高的平台,固定开销越占主导。而块大小对命中率的影响取决于共享前缀有多长(代码 03 演示 2):我们的合成负载里共享前缀动辄几千 token,块从 16 增大到 512 只损失 5% 的命中率,到 4096 损失 42%;ShareGPT 这种几百 token 的对话就敏感得多。
4.2 问题二:prefill 变成 loading-bound,以及”延迟命中”
已有系统(例如 CachedAttention)假设逐层重叠能把加载藏在计算后面:算第 层时加载第 层的 KV。可当请求带着很长的缓存上下文、却只有几个新 token 时(长文档上的一个短问题),计算很短,加载却随上下文线性增长,假设失效。Strata 图 1:用 SGLang 把 KV 卸载到 CPU 内存、跑 LooGLE 数据集,74% 的 prefill 时间卡在 KV 传输上,吞吐最多降低 4 倍;即使用了 Strata 优化过的 I/O,仍有最多 24% 的时间在等加载。
代码 03 演示 4 在纸上看这个拐点(Qwen2.5-14B、H200、每次 256 个新 token):
| 加载 / 计算(token 数之比) | 1 | 4 | 10 | 20 | 50 | 100 | 400 |
|---|---|---|---|---|---|---|---|
| 卡顿占比(默认 memcpy,页 32) | 0% | 25% | 68% | 83% | 92% | 94% | 97% |
| 卡顿占比(GPU 辅助 I/O) | 0% | 0% | 0% | 40% | 70% | 81% | 88% |
在纸上 GPU 里,GPU 辅助 I/O 的拐点在 10 到 20 之间;Strata 在真实系统上观察到卡顿大约从比值 100 开始(它的计算比纸上 GPU 慢),并用 100 作为组批的阈值。拐点的具体位置取决于硬件和模型,但”长上下文 + 短问题”会变成 loading-bound,这一点是一样的。
延迟命中(delay hit,网络缓存研究里的概念):多个请求要用同一段还没缓存的上下文时,第一个请求的”未命中”还没解决(KV 正在算),后面的请求也被当作未命中,于是同一段 prefill 被重复计算。现代引擎的异步调度器(在当前批执行时就准备下一批)把这个窗口拉长到一整个批的执行时间,长上下文时代价更大。
结论:调度器必须把 CPU–GPU 带宽当作一等资源,和 GPU 计算、显存一起考虑。
4.3 Strata 的设计
GPU 辅助的 I/O。不再由 CPU 反复调用 cudaMemcpyAsync 搬小块,而是启动一个 CUDA 内核,成千上万个线程各自把一小段数据从源(GPU 显存或 CPU 的锁页内存)读进寄存器、再写到目标。好处:并发 极高(GPU 天生有几千个线程);128 字节的粒度就能高效传输,页再小也不用放大;搬运时顺手做一点地址计算几乎免费,所以可以转换排布。代价是和计算内核争抢 SM、寄存器和缓存。Strata 只启动很少几个大线程块,让硬件调度器把它们限制在很少的 SM 上,并用绕过缓存的指令减少污染:在 H200 上用 2 个 1024 线程的块就达到约 50 GB/s,对 prefill 的干扰不到 5%、对 decode 约 10%(论文图 5)。默认给加载(关键路径)2 个块、给备份到 CPU(非关键路径)1 个块。
排布解耦。GPU 里保持按层排布(方便计算内核),CPU 内存和 SSD 改成按页排布(一页的所有层连在一起,便于大块传输);两者之间的转换由 I/O 内核完成(论文图 6)。从 SSD 加载时,命中后在请求排队期间就开始预取到 CPU 内存(尽力而为)。
缓存感知的调度(基于扩展了的基数树 HiRadixTree,记录每个页在 GPU、CPU 的位置和命中次数):
- 推迟延迟命中:在树里插入”临时节点”,标记某段上下文”正在排队”或”正在计算”;后来的请求如果匹配到临时节点(超过 100 个 token),就推迟到下一轮、但放在队首,等那段 KV 算好再用,既不重复计算、也不太影响 TTFT。
- 均衡组批(论文算法 1):按队列顺序挑请求,加进来会让这一批变成 loading-bound(加载 / 计算超过阈值)的先放进推迟列表;批没满再用推迟的补;推迟的请求保持原顺序、下一批从队首开始,防止饿死。和本批共享同一段上下文的请求优先加进来(bundle hit,与延迟命中相反:只加载一次、还省显存)。
- 空档填充(bubble filling):某一批仍然 loading-bound 时,在等加载的空档里先执行一个 decode 批——decode 吃的是 HBM 带宽,加载吃的是 PCIe 带宽,两者几乎不冲突。
代码 03 演示 4 模拟了均衡组批的效果:请求成批到达(先 8 个”长文档上的短问题”,再 8 个”全新的长提示”,重复两轮),每批 2 个。先来先服务时”加载重”的请求凑在同一批,总时间 4.14 s、其中卡顿 1.59 s;均衡组批把两类请求搭配起来,总时间 2.72 s、卡顿 0.18 s。(我的第一版把两类请求交替排进队列,先来先服务本来就均衡,我的算法反而更差——均衡组批的价值在于请求不是均匀到达的时候。)
4.4 评测与论文卡片
论文在 8×H200(每块经 PCIe 5.0 x16 连 CPU,64 GB/s)和 GH200 上,用 Llama-3.1-8B、Qwen2.5-14B-1M、Llama-3.1-70B(4 卡 TP),LooGLE、NarrativeQA(RAG 式的长文档问答)、ReviewMT(多 agent 对话)和 ShareGPT(短对话)评测(论文报告):
- 不做分层缓存的系统在长上下文负载上很快耗尽显存、反复重算;有分层缓存的系统借助 CPU 内存达到约 95% 的命中率。
- 相对已有的分层缓存方案,Strata 在同样的 TTFT 下吞吐更高:Llama-8B / LooGLE 上相对 SGLang-HiCache、vLLM-LMCache、TensorRT-HiCache 分别 3.2、2.6、1.9 倍;Llama-70B 上 5、5、3.75 倍。摘要里的说法是 TTFT 比 vLLM + LMCache 低最多 5 倍、比 TensorRT-LLM 快 3.75 倍。
- 预热后的稳态(NarrativeQA):相对 vLLM-LMCache 2.3–2.6 倍。短上下文(ShareGPT)不变差。
- 消融:只用调度、只用 I/O 优化,峰值吞吐分别提高 1.8 倍和 2.3 倍;请求率低时调度的收益更大,请求率高时 I/O 成为主要瓶颈。
| 项目 | 内容 |
|---|---|
| 论文 | Strata: Hierarchical Context Caching for Long Context Language Model Serving,Xie 等,2025(Stanford、NVIDIA 等;作者含本课的主讲与助教) |
| 一句话 | 用 GPU 内核搬运碎片化的 KV 页并解耦 GPU / CPU 的排布,再让调度器把 CPU–GPU 带宽当作一等资源(推迟延迟命中、均衡组批、空档填充) |
| 问题 | 分层缓存时,小页带来的碎片化 I/O 只能用到 PCIe 带宽的一小部分;长上下文 + 短问题的 prefill 变成 loading-bound;调度器对加载时间视而不见 |
| 关键洞察 | Little 定律:提高并发比放大页更好,而 GPU 天生有极高的并发;加载和计算要在组批时就配平 |
| 机制 | GPU 辅助 I/O(少量 SM、绕过缓存);层优先 / 页优先的排布转换;HiRadixTree 与临时节点;均衡组批与 bundle hit;用 decode 批填加载空档 |
| 评测 | TTFT 比 vLLM + LMCache 低最多 5×,比 TensorRT-LLM 快 3.75×;短上下文不变差(论文报告) |
| 局限与假设 | GPU 辅助 I/O 占用 SM、对 decode 有约 10% 的干扰;阈值(100)依赖硬件和模型;评测主要是 CPU 内存层,没有用到磁盘;P / D 共置的设计 |
| 与本课的关系 | 第 3 讲 RadixAttention 的分层扩展(SGLang 的 HiCache);第 4 讲”I/O 才是瓶颈”的思想用在 CPU–GPU 之间;第 7 讲逐层传输的局限 |
5. 综合:放在哪、压不压、何时加载 · Putting it together
把本讲的三篇论文放在 §1 的账本上看:
| 情形 | 主要矛盾 | 应对 | 代表系统 |
|---|---|---|---|
| KV 在本机 CPU 内存,上下文长、问题短 | PCIe 的有效带宽、加载与计算不平衡 | 大块或 GPU 辅助的 I/O;逐层重叠;均衡组批;空档填充 | Strata |
| KV 在集群里别的机器上(RDMA) | 选哪台机器:复用 vs 负载;热点块的网络拥塞 | 缓存感知 + 负载均衡的调度;热点复制;逐层流式传输 | Mooncake |
| KV 在普通网络的另一端(几 Gb/s) | 网络带宽远低于盈亏平衡带宽 | 压缩成比特流;按块自适应;必要时传文本重算 | CacheGen |
| 请求太多、处理不完 | 资源浪费在不会完成的请求上 | 提前拒绝,并预测未来的负载 | Mooncake |
三个贯穿始终的原则:
- 先算账:盈亏平衡带宽(每 token 的 KV ÷ 重算一个 token 的时间)决定了一段上下文值不值得存、存在哪一层。上下文越长、模型越大、KV 越小(GQA / MLA),越值得存。
- I/O 是一等资源:调度器不能只看 GPU 的计算和显存,还要看 PCIe / 网络的带宽和 KV 在哪里。
- 复用要有局部性:命中率取决于负载(时间局部性、共享前缀的长度),淘汰策略、块大小、放置都要按负载调;没有放之四海皆准的设置。
6. 之后的发展 · Since then
(这一节是我的补充,不是原课内容。)
- 开源的 KV 缓存层:LMCache(与 CacheGen 同一个团队)为 vLLM 提供跨层级的 KV 存储与传输;Mooncake 开源了 Transfer Engine 和 Mooncake Store,被 vLLM、SGLang 等集成;SGLang 的 HiCache 把 RadixAttention 扩展到 CPU 内存和外部存储(Strata 的工作就建立在这条线上);NVIDIA Dynamo 的 KV 管理(第 7 讲)和 NIXL 传输库也属于这一类。
- 商业的上下文缓存 API:主要的模型服务商都提供了显式或自动的提示缓存(prompt / context caching),缓存命中的输入 token 价格明显低于普通输入——“复用 KV”被直接写进了定价。
- 非前缀的复用:前缀缓存要求从头开始完全相同;RAG 里同一份文档常常出现在不同的位置、前面跟着不同的内容。后续工作研究在非前缀位置复用文档的 KV,再选择性地重算少量 token 来修正跨文档的注意力。
- 更小的 KV:KV 量化(FP8、更低位宽)、MLA(第 8 讲)、混合架构里不需要 KV 的层(第 10 讲)、稀疏注意力只读一部分 KV(第 10 讲 DeepSeek-V3.2),都让 §1 的账更好算。
- agent 与长期记忆:agent 的上下文越来越长、跨会话保留,KV 缓存与”记忆”的界限变得模糊(第 14 讲)。
7. 小结 · Summary
- 多轮对话、文档问答、agent 反复读同一段长上下文;KV 太大(Llama-3-8B 每 token 128 KiB,40 GB 显存只放约 30 万 token),必须分层存放在 CPU 内存、SSD、远端。
- 加载还是重算看盈亏平衡带宽:Llama-3-8B 约 6 GB/s(每块 GPU),上下文越长越低。从 CPU 内存或 RDMA 加载比重算快 7–9 倍,普通网络上加载反而更慢(代码 01)。
- Mooncake:以 KV cache 为中心的解耦架构;分布式 KV 池、前缀链式哈希、缓存感知 + 负载均衡 + 热点迁移的调度(纸上:高负载时平均 TTFT 从负载均衡的 8.7 s 降到 0.64 s,代码 04);过载时提前拒绝,并用预测减轻负载振荡——我们复现了振荡和浪费的减少,但没有复现”完成更多请求”(代码 04 演示 2)。
- CacheGen:网络是瓶颈时把 KV 压成比特流:锚点差分、按层量化、按(层, 通道)分布的算术编码;按块自适应、传文本兜底。在 tiny_llm 上,“量化 + 熵编码”和”浅层更敏感”成立,“相邻 token 局部性”不成立;自适应只在带宽变化有持续性时有用(代码 02)。
- Strata:小页让 CPU–GPU 的有效带宽只有峰值的 22%(GH200 上 5%),“长上下文 + 短问题”的 prefill 卡在加载上;GPU 辅助 I/O、排布解耦、推迟延迟命中、均衡组批(纸上总时间 4.14 s → 2.72 s)、空档填充(代码 03)。
- 共同的原则:先算账;把 I/O 当作一等资源;复用依赖负载的局部性。
8. 常见误区 · Common pitfalls
- “缓存命中总比重算快。” 只有加载带宽高于盈亏平衡带宽时才是。在几 Gb/s 的网络上加载 8K 上下文的 KV 比重算慢 18 倍(代码 01 演示 2)。
- “KV 放进 CPU 内存,加载就是 PCIe 的峰值带宽。” 分页的 KV 被切成成千上万个小块,每次传输的固定开销让有效带宽只剩 22%(页 32)甚至不到 1%(页 1)(代码 03 演示 3)。
- “逐层重叠能把加载完全藏起来。” 新 token 少、缓存的上下文长时,计算太短,盖不住加载(代码 03 演示 4)。
- “页越小越好。” 小页命中率高、碎片少,但搬运效率低;大页反过来。要么用 GPU 辅助的 I/O 解耦两者,要么按负载的共享前缀长度选页大小。
- “把请求派给前缀最长的机器就行。” 热门前缀所在的机器会被挤爆;要同时看排队时间,并把热点复制出去(代码 04 演示 1)。
- “KV 压缩就是 KV 量化。” 量化保持张量格式、缩小显存占用;CacheGen 为传输编码比特流,可以用熵编码进一步压缩,而且能叠加在量化或丢 token 之上。
- “CacheGen 的差分对任何 KV 都有用。” 它依赖相邻 token 的相关性;在我们的随机模型上差分反而让码长变长;合成数据显示它的一部分好处来自去掉通道偏移(代码 02 演示 1–2)。
- “过载时越早拒绝越好。” 提前拒绝减少浪费,却可能因为决策基于过时的信息而造成负载振荡、拒掉本来能完成的请求;效果取决于预测的准确度和真正的瓶颈在哪(代码 04 演示 2)。
9. 练习 · Exercises
练习 1、3、4 的参考答案里的数字是用
context_cache.py的函数实际算出来的。
练习 1(盈亏平衡带宽)。Qwen2.5-14B(每 token 的 KV 192 KiB)在单块 H100 上 prefill 8,000 个 token 要 351 ms。(a) 它的盈亏平衡带宽是多少?(b) 一块 NVMe SSD(有效 5.6 GB/s)上的 KV 值得加载吗?(c) 如果模型换成每 token KV 只有 70 KB 的 MLA 结构,计算不变,呢?
参考答案
(a) 每个 token 的重算时间 351/8000 ≈ 43.9 µs,KV 196,608 字节,平衡带宽 ≈ 196,608 / 43.9e-6 ≈ 4.5 GB/s。(b) 5.6 GB/s 高于 4.5 GB/s,加载略快(约快 20%);如果再算上 SSD 的寻道和小块读取的开销,两者差不多,优势不大。(c) 平衡带宽降到约 70e3 / 43.9e-6 ≈ 1.6 GB/s,SSD 远高于它,加载明显更快——更小的 KV 让更慢的存储层也变得有用。
练习 2(链式哈希)。块大小 512。请求 A 的内容是”系统提示(1,200 token)+ 文档 D(3,000 token)+ 问题 1(80 token)“,请求 B 是”系统提示 + 文档 D + 问题 2(150 token)”。(a) A 有几个完整的块?(b) B 能复用 A 的几个块?(c) 如果 B 是”系统提示 + 文档 E + 问题”,能复用几块?
参考答案
(a) A 共 4,280 个 token,完整的块有 ⌊4280/512⌋ = 8 个(最后 184 个 token 不满一块,不缓存)。(b) A、B 的前 4,200 个 token 相同,前 ⌊4200/512⌋ = 8 个块的内容完全相同,哈希也相同——B 能复用 A 的全部 8 块(第 8 块覆盖第 3,585–4,096 个 token,仍在共享区间内)。(c) 前 1,200 个 token 相同,只有 ⌊1200/512⌋ = 2 块可复用;第 3 块包含系统提示的最后 176 个 token 和文档的开头,内容不同,哈希不同,从此之后都不同。可以用 chain_block_ids 核对。
练习 3(CacheGen 的账)。Mistral-7B(每 token 65,536 个 KV 数),9,600 token 的上下文,网络 3 Gb/s,A40 上重算整段约 1.51 s。(a) FP16、8 位量化、CacheGen(约 2.3 位 / 数)的 KV 各有多大?各要传多久?(b) 哪种方式的 TTFT 最短?(c) 网络升到 10 Gb/s 呢?
参考答案
(a) FP16:9,600 × 65,536 × 2 字节 = 1.26 GB,3.36 s;8 位:0.63 GB,1.68 s;CacheGen:0.18 GB,0.48 s。(b) CacheGen(0.48 s 加上很小的解码开销)< 重算(1.51 s)< 8 位量化(1.68 s)< FP16(3.36 s)。注意 8 位量化在 3 Gb/s 下比重算还慢——不压缩到足够小,就不如传文本。(c) 10 Gb/s 时:FP16 1.01 s、8 位 0.50 s、CacheGen 0.14 s,三者都比重算快;带宽越高,压缩的相对收益越小(论文图 11 在 20 Gb/s 以上看到同样的趋势)。
练习 4(Little 定律)。用 估计:(a) 每次 cudaMemcpyAsync 搬 64 KB、延迟 4 µs、同时只有 1 个在飞,吞吐是多少?占 PCIe 5.0(64 GB/s)的几成?(b) 同时有 2 个呢?(c) GPU 内核用 1,000 个线程、每个每次搬 128 字节、延迟 1 µs 呢?
参考答案
(a) 64 KB / 4 µs = 16 GB/s,约 25%(和 Strata 测到的 22% 同一量级)。(b) 32 GB/s,50%。(c) 1000 × 128 B / 1 µs = 128 GB/s,超过了 PCIe 的峰值,所以实际会被链路带宽限制在接近峰值处——高并发让小粒度也能打满带宽,这就是 GPU 辅助 I/O 的原理(实际还受 SM 数、内存系统的限制,Strata 用 2 个线程块做到约 50 GB/s)。
练习 5(KVCache-centric 的决策)。一个请求有 20 块。实例 A 本地命中 4 块、排队 0.5 s;实例 B 本地命中 18 块、排队 3 s。设每个未命中的块 prefill 要 0.1 s,跨机拉取每块 0.002 s。比较三种选择的 TTFT:A 本地算、A 从 B 拉 14 块再算、派给 B。
参考答案
A 本地:0.5 + 16 × 0.1 = 2.1 s。A 从 B 拉:0.5 + 14 × 0.002 + 2 × 0.1 = 0.728 s。派给 B:3 + 2 × 0.1 = 3.2 s。选”A 从 B 拉”,而且拉过来的 14 块存在 A 上,这份热门前缀从此有了两个副本(热点迁移)。如果拉取变慢(网络拥塞,每块 0.15 s),A 从 B 拉要 0.5 + 2.1 + 0.2 = 2.8 s,不如 A 本地重算——这就是 Mooncake 说传输时间最难预测、需要阈值的原因。
练习 6(延迟命中)。一份 30,000 token 的新文档在 10 ms 内被 3 个请求同时问到;重算它要 2 s。(a) 三个请求被放进同一批或相邻的两批时,最坏情况下这段 prefill 被算几次?(b) Strata 的推迟策略能省多少?它的代价是什么?
参考答案
(a) 三个请求都在第一个请求的 KV 算好之前被调度,都被当作未命中,最多算 3 次(6 s 的 GPU 时间),而且它们在同一批里还会占用三份显存。(b) Strata 在基数树里给这段上下文插入”正在排队 / 正在计算”的临时节点,后两个请求匹配到它就推迟到下一轮(放在队首),等第一个请求算完再直接命中:只算 1 次,省下 4 s 的 GPU 时间。代价是后两个请求的 TTFT 多了一个批的等待;Strata 只在匹配超过 100 个 token 时才推迟,避免为了很短的共享前缀耽误请求。如果三个请求能同时被调度进同一批并共享那段 KV(bundle hit),效果更好。
练习 7(思考:提前拒绝为什么振荡)。用控制系统的语言解释提前拒绝造成的振荡。除了 Mooncake 的”系统级预测”,你还能想到哪些减轻振荡的办法?
参考答案(要点)
这是一个带延迟的反馈回路:控制量(接受多少请求)依据的观测(decode 负载)比它影响到的状态晚了一个 prefill 的时间;回路增益又很大(decode 一满就拒绝所有请求,一空就接受所有请求),延迟 + 高增益就会振荡。减轻的办法:(1) 预测——用”请求进入 decode 时的负载”代替”此刻的负载”(Mooncake 的做法),本质是补偿延迟;(2) 降低增益——不要非黑即白地全拒 / 全收,而是按负载平滑地调整接受概率,或者给 decode 留一点余量;(3) 缩短延迟——prefill 越快、越少排队,延迟越小;(4) 预测输出长度,让 decode 负载的预测更准。我们的模拟里,理想预测(已知输出长度)把浪费降得最低,但完成数仍不如”不提前拒绝”,说明还要看瓶颈到底在哪里。
练习 8(思考:一个百万 token 的 agent)。一个编程 agent 每天围绕同一个代码仓库(约 100 万 token)工作几百步,每步追加几百个 token。用本讲的工具讨论:KV 存在哪?每步要不要重新加载?用什么页大小?能否压缩?
参考答案(要点)
以 Llama-3-8B 的 KV 计,100 万 token 约 131 GB,放不进单卡显存,需要多卡(例如按 KV 切分,第 8 讲 Helix)或放在 CPU 内存。每步只追加几百个 token,最好让 KV 一直留在显存里(跨步不释放),只有被换出时才从 CPU 内存加载——这正是”长上下文 + 短问题”的 loading-bound 场景,需要大块 / GPU 辅助的 I/O 和逐层重叠;131 GB 经 PCIe 5.0(51 GB/s)要 2.6 s,必须尽量避免每步都重新加载。共享前缀很长(整个仓库),页大小对命中率影响很小,可以用较大的页或页优先的 CPU 排布提高搬运效率。跨机器迁移或冷存储时可以用 CacheGen 式的压缩,但留在显存里参与计算的那份要保持模型能直接读取的格式。更根本的办法是减少要读的 KV:稀疏注意力只读相关的部分(第 10 讲),或者把仓库的知识”写进”更小的表示(第 14 讲 Cartridges)。
10. 延伸阅读 · Further reading
本讲的必读材料
- R. Qin, Z. Li, W. He, M. Zhang, Y. Wu, W. Zheng, X. Xu. Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving. arXiv:2407.00079(FAST 2025 版:Mooncake: Trading More Storage for Less Computation — A KVCache-centric Architecture for Serving LLM Chatbot)。
- Y. Liu, H. Li, Y. Cheng, S. Ray, Y. Huang, Q. Zhang, K. Du, J. Yao, S. Lu, G. Ananthanarayanan, M. Maire, H. Hoffmann, A. Holtzman, J. Jiang. CacheGen: KV Cache Compression and Streaming for Fast Large Language Model Serving. SIGCOMM 2024。
选读
- Z. Xie, Z. Xu, M. Zhao, Y. An, V. S. Mailthody, S. Mahlke, M. Garland, C. Kozyrakis. Strata: Hierarchical Context Caching for Long Context Language Model Serving. arXiv:2508.18572,2025。
相关工作
- B. Gao et al. Cost-Efficient Large Language Model Serving for Multi-turn Conversations with CachedAttention(AttentionStore). USENIX ATC 2024(分层 KV 缓存与逐层重叠,Mooncake 与 Strata 都引用)。
- L. Yu, J. Lin, J. Li. Stateful Large Language Model Serving with Pensieve. EuroSys 2025(多轮对话的分层 KV 缓存,Strata 的对照之一)。
- Z. Zhang et al. H2O: Heavy-Hitter Oracle for Efficient Generative Inference of Large Language Models. NeurIPS 2023(按注意力分数丢 token,CacheGen 的对照)。
- H. Jiang et al. LLMLingua: Compressing Prompts for Accelerated Inference of Large Language Models. EMNLP 2023(压缩文本上下文)。
背景
- 第 3 讲:PagedAttention 与 RadixAttention(本讲的分页与前缀树)。
- 第 7 讲:prefill / decode 解耦、逐层 KV 传输。
- 前置讲 P3(缓存与淘汰)、P4(Little 定律)、P5(PCIe、RDMA)。
11. 下一讲预告 · Next lecture
这一讲的每个问题都源于同一个事实:注意力的 KV cache 随上下文长度线性增长,读它的代价也随之增长。有没有模型根本不需要这样的 KV?第 10 讲:混合架构与稀疏注意力。状态空间模型(Mamba)用一个固定大小的状态代替 KV cache,生成每个 token 的代价与上下文长度无关;今天的很多新模型把少量注意力层和大量 Mamba / 线性注意力层混合起来。可这给推理系统带来了新问题:Mamba 层的状态是”把整个前缀压进一个向量”,无法像 KV 那样按 token 截取,前缀缓存只能在特定位置保存状态的快照——Marconi 研究怎样为混合模型做前缀缓存(准入:哪些位置值得存快照;淘汰:每字节能省多少计算)。另一条路是保留注意力、但只读一部分 KV:DeepSeek-V3.2 的稀疏注意力用一个轻量的”索引器”为每个 query 挑出最相关的 top-k 个 token。我们会在 numpy 里对比线性递归与注意力的显存和计算,给混合模型做一个玩具版的前缀缓存来比较 Marconi 式的淘汰与 LRU,并测量 top-k 稀疏注意力的近似质量与代价。