对应 Maps to:Stanford CS349E / EE339 Efficient ML Inference at Scale(Autumn 2026),原课第 10 次课 · Thu Oct 22 · KV Cache Management(Fredrik Kjolstad)。本讲义把它提前到”推理系统”部分(见 SYLLABUS §0.1)。阅读材料:W. Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention(SOSP 2023);R. Qin et al., Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving(阅读材料链接的是 ACM 期刊版;会议版发表在 FAST 2025)。 依据说明:课程网站只公开了课表和阅读材料,没有 slides。本讲以两篇阅读材料为主线(Mooncake 依据 arXiv 2407.00079 的 v5 版本),另外用到这些公开文献:SGLang 的 RadixAttention(Zheng et al., 2024)、vLLM 的自动前缀缓存文档、操作系统教材里的虚拟内存。补充材料是我选的,不代表课堂上实际讲了什么。 配套代码 Code:
code/lecture07/(numpy;三个模块 + 5 个脚本,最长的约 50 秒) 和 CS349D 的关系:本仓库 CS349D 第 3 讲(PagedAttention 与 SGLang 的前缀缓存)和第 9 讲(Mooncake、分层 KV 存储)读的是同样的论文,偏服务系统;本讲更多从显存和存储层次的账来看。 预计阅读时间:3–4 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说清楚 KV cache 为什么难管:它很大、随生成动态增长、长度事先不知道;以前的系统把每个请求的 KV 放在一整块连续显存里,浪费在三个地方——预留、内部碎片、外部碎片。
- 用操作系统的虚拟内存解释 PagedAttention:物理块、块表、按需分配、引用计数、写时复制、换出;知道块大小怎样取舍。
- 说出共享带来的好处:并行采样、束搜索、共同前缀;会算共享省下的显存和复制量。
- 解释显存不够时的抢占:为什么是”全有或全无”、换出和重算各在什么时候划算;为什么分页 + 抢占不会像第 6 讲那样死锁。
- 理解前缀缓存:链式哈希怎样认出”这段前缀以前算过”;缓存容量和命中率的关系;LRU 为什么比 LFU 好。
- 复述 Mooncake 的设计:把 KV 当成集群的一等公民,放在 CPU 内存 / SSD 组成的池子里(“用存储换计算”);按 KV 的位置调度 prefill;过载时提前拒绝及其副作用。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲会讲 Covered here | 后面再展开 Later |
|---|---|---|
| 第 2 讲 §5–6:KV cache 的大小、MHA / GQA / MLA | 显存的三种浪费;分页、块表、共享、写时复制、抢占 | 分页注意力内核怎么写(第 10、11 讲) |
| 第 5 讲:prefill / decode 解耦、TTFT / TPOT | 前缀缓存、链式哈希、缓存策略 | KV 的量化(第 3 讲 §3 已讲) |
| 第 6 讲:迭代级调度、选择性批处理、KV 预留与死锁 | Mooncake:分层 KV 池、按 KV 调度、提前拒绝 | 把 KV 搬过网络的代价(第 5 讲 §7 已讲) |
| P1:存储层次、带宽与延迟 | 操作系统虚拟内存的十分钟入门(§2) |
0.3 本讲地图 Map
§1 KV cache 为什么难管:大、会长、长度未知
§2 背景:操作系统的虚拟内存(十分钟入门)
§3 以前的系统:显存去哪了 ← 代码 02
§4 PagedAttention:块、块表、按块注意力;块大小 ← 代码 01、02
§5 共享:并行采样、束搜索、共同前缀;写时复制 ← 代码 01、04
§6 调度与抢占:全有或全无;换出 vs 重算 ← 代码 03
§7 端到端:分页让服务快多少 ← 代码 03
§8 跨请求的前缀缓存:链式哈希、命中率、缓存策略 ← 代码 04
§9 Mooncake:以 KV cache 为中心的集群 ← 代码 04、05
§10 其他让 KV 变小、变便宜的办法
§11 现状(2025–2026)
§12–16 小结、常见误区、练习(含答案)、延伸阅读、下一讲预告
0.4 记号 Notation
| 记号 | 含义 |
|---|---|
| 块大小(一个物理块存几个 token 的 KV);vLLM 默认 16,Mooncake 的 trace 按 512 | |
| 槽位 slot | 存一个 token 的 KV 的空间 |
| 块表 block table | 一个序列的”逻辑块 → 物理块”映射 |
| 引用计数 ref count | 有几个序列的块表指向这个物理块 |
| 写时复制 CoW | copy-on-write:往共享的块里写之前先复制一份 |
| Orca (Max / Pow2 / Oracle) | vLLM 论文给 Orca 设的三种预留方式:按最大长度、输出按 2 的幂、恰好按真实长度 |
1. KV cache 为什么难管 · Why KV cache is hard to manage
第 2 讲 §5.5 算过 KV cache 的大小:每个 token 要存 个数(K 和 V、每层、每组 KV 头)。几个例子(BF16):
| 模型 | 注意力 | 每 token 的 KV | 一个 2048 token 的请求 |
|---|---|---|---|
| OPT-13B(vLLM 论文用的) | MHA,40 头 | 819 KB | 1.68 GB |
| Llama-3.1-8B | GQA,8 组 | 128 KB | 0.27 GB |
| LLaMA2-70B(Mooncake 的哑模型) | GQA,8 组 | 320 KB | 0.67 GB |
OPT-13B 放在一块 A100 40GB 上:权重 26 GB,KV 能用约 12 GB(论文表 1)——只够 7 个最大长度的请求。第 1 讲 §4.4 说过:decode 受带宽限制,一步的时间几乎不随批大小变化,所以能同时放下几个请求,吞吐就是几倍。KV 的显存管理直接决定吞吐。
难在三点:
- 大:每个请求几百 MB 到几 GB。
- 会长:decode 每一步每个序列多一个 token,KV 跟着长;请求结束又一下子释放。
- 长度事先不知道:用户的提示长短不一,生成多长要等模型输出 EOS 才知道。
第 6 讲 §3.5 已经撞上了这堵墙:按最大长度预留,显存大部分是”占着不用”;按请求声明的上限预留,仍是实际用量的约 2.5 倍;不预留,负载一高就死锁。vLLM 的答案来自一个老地方:操作系统。
2. 背景:操作系统的虚拟内存 · Background: virtual memory in ten minutes
如果你没学过操作系统,这一节是 PagedAttention 需要的全部背景。
问题:很多程序同时运行,每个程序需要的内存大小不一、随时变化。如果每个程序必须占一整段连续的物理内存,就会出现:
- 内部碎片(internal fragmentation):分给你的比你用的多,多出来的别人也用不了;
- 外部碎片(external fragmentation):空闲内存加起来够,但被切成很多不连续的小段,一个大请求放不进去。
分页(paging)的办法:
- 把物理内存切成固定大小的页(page,常见 4 KB);
- 每个程序看到的是连续的虚拟地址,由一张页表(page table)把”虚拟页 → 物理页”翻译过去——物理页可以散在任何地方;
- 按需分配(demand paging):程序真正用到某一页时才给它分配物理页;
- 共享:两个程序的虚拟页可以指向同一个物理页(比如共享库);用引用计数记录有几个人在用;
- 写时复制(copy-on-write):
fork()创建子进程时不复制内存,父子共享所有页,并标记为只读;谁先写某一页,就给谁复制一份; - 换出(swapping):物理内存不够时,把某些页写到磁盘,需要时再读回来。
代价是一次间接寻址(查页表),硬件用 TLB 缓存页表来加速。
| 操作系统 | vLLM |
|---|---|
| 物理页(4 KB) | 物理 KV 块( 个 token 的 K、V) |
| 进程的虚拟地址空间 | 一个序列的逻辑 KV(位置 0, 1, 2, …) |
| 页表 | 块表(block table) |
| 按需分配页 | 序列长到需要新块时才分配 |
fork() + 写时复制 | 并行采样 / 束搜索:共享块,往共享块里写时复制 |
| 共享库 | 共同的系统提示 / few-shot 前缀 |
| 换出到磁盘 | 换出到 CPU 内存,或干脆丢掉以后重算 |
vLLM 论文 §8 指出了两个”LLM 特有”的改造:(1) 一个序列的所有块总是一起被用到(每次 decode 都要读全部 KV),所以换出是”全有或全无”;(2) KV 可以重算出来(操作系统的页丢了就没了,KV 丢了还能从 token 重新算),所以恢复多了一个选项。
3. 以前的系统:显存去哪了 · Where the memory went
3.1 三种浪费
vLLM 论文 §3.1(图 3):以前的系统(FasterTransformer、Orca)要求一个请求的 KV 是一个连续的张量,于是按”最大可能长度”给每个请求预留一整块。浪费有三种:
- 预留(reserved):为将来要生成的 token 预留的槽位。它们最终会被用到,但在请求的整个生命周期里一直占着,本来可以先给别的请求用。
- 内部碎片(internal fragmentation):预留的比请求最终用到的多——请求提前结束了,或者分配器向上取整了。永远用不到。
- 外部碎片(external fragmentation):分配器(比如伙伴分配器)在请求之间留下的空隙。
论文图 2 的测量:以前的系统里,真正存着 token 的 KV 只占 20.4%–38.2%;vLLM 是 96.3%。
3.2 伙伴分配器的例子
vLLM 论文假设 Orca 用伙伴分配器(buddy allocator)在一大块显存里分配:它只分配 2 的幂大小的块,释放时和”伙伴”合并。代码 02 演示 1(总共 16 个槽位):
| 事件 | 结果 | 空闲合计 | 最大的连续空闲块 |
|---|---|---|---|
| A 要 5 个 | 给 8 个(内部碎片 3) | 8 | 8 |
| B 要 3 个 | 给 4 个(内部碎片 1) | 4 | 4 |
| C 要 2 个 | 给 2 个 | 2 | 2 |
| B 结束 | – | 6 | 4 |
| D 要 6 个 | 失败 | 6 | 4 |
D 失败时空闲的 6 个槽位正好够,但它们是 4 + 2 两块、不连续——外部碎片。
3.3 一块 A100 上的账
代码 02 演示 2 用迭代级调度(第 6 讲的 Orca)模拟 OPT-13B 在一块 A100 40GB 上服务(KV 能用 12,596 个 token 的槽位),过载时各种管理方式下显存的去向(按迭代平均):
| 管理方式 | 存着 token | 预留(将来会用) | 内部碎片 | 外部碎片 |
|---|---|---|---|---|
| Orca (Max):按 2048 预留 | 23.3% | 15.6% | 58.3% | 2.4% |
| Orca (Pow2):输出按 2 的幂预留 | 23.5% | 15.8% | 49.8% | 10.4% |
| Orca (Oracle):恰好按真实长度预留 | 39.4% | 26.4% | 24.1% | 9.2% |
| vLLM:分页,块 16 | 92.8% | 1.6% | 0 | 1.3% |
(ShareGPT 式的负载:平均输入 161、输出 338 个 token,长度分布是我按论文的平均值合成的;3 req/s,过载。)
- 按 2048 预留时,真正存着 token 的约四分之一,大头是内部碎片——预留了却永远用不到。
- 即使事先知道每个请求生成多长(Oracle,现实中做不到),伙伴分配器向上取整、加上”还没生成到的部分被提前占着”,有效利用率也只有约四成。
- 分页时九成以上存着 token。
3.4 能同时跑几个请求
代码 02 演示 3(论文图 13 的设置):
| 负载 | Max | Pow2 | Oracle | vLLM |
|---|---|---|---|---|
| ShareGPT,2 req/s | 6.0 | 6.0 | 10.1 | 18.0 |
| Alpaca,30 req/s | 6.0 | 31.6 | 34.5 | 35.2 |
ShareGPT 上分页能同时跑的请求数约是 Oracle 的 1.8 倍、Max 的 3 倍(论文在 OPT-13B 上测到 2.2 倍和 4.3 倍)。Alpaca 的请求很短(平均输入 19、输出 58),除了 Max 以外显存都不是瓶颈,批大小由到达速率决定。
4. PagedAttention · Paged KV blocks
4.1 块与块表
PagedAttention 把每个序列的 KV 切成固定大小的KV 块,每块存 个 token 的 K 和 V(论文 §4.1)。显存被预先切成很多物理块;每个序列有一张块表,记录”第 个逻辑块在哪个物理块、填了几个”(§4.2)。
代码 01 演示 1 复现论文图 6 的例子:提示”Four score and seven years ago our”(7 个 token),块大小 4。池子里有些块已经被别人占着:
| 这一步 | 块表(逻辑 → 物理) | 已写入 | 最后一块 |
|---|---|---|---|
| prefill 7 个 token | [4, 2] | 7 | 3/4 |
| decode:写入”fathers” | [4, 2] | 8 | 4/4 |
| decode:写入”brought” | [4, 2, 0] | 9 | 1/4 |
- 按需分配:prefill 只分配 2 块;第一次 decode 写进第 2 块剩下的槽位;第 2 块满了才分配第 3 块。
- 物理块不连续:逻辑上连续的 KV 散在显存各处,靠块表找回来。
- 浪费只在最后一块:任何时候每个序列最多浪费 个槽位。
4.2 按块计算注意力
注意力要用到一个序列全部的 K 和 V,可它们现在散在各处。论文式 (4) 把注意力写成按块的形式:对第 个块 、,
内核按块表逐块读 K、V(vLLM 让一个 warp 读一个块,保证合并访存),算分数、做 softmax、乘 V。代码 paged_kv.blockwise_attention 就是这样写的:和”把块拼成连续数组再算”相比,最大差 3e-16。
代码 01 演示 2 更进一步:把第 6 讲的选择性批处理(ragged_forward)接到分页的 KV 上——它只要求 cache 有 length / write / read 三样东西,分页的序列实现这三样就行。4 个长短不一的序列做 prefill + 20 步 decode,不管块大小是 1、4、16 还是 64,logits 和连续的 KV 一致到 。分页只改变 KV 放在哪里,不改变算什么。
4.3 代价:间接寻址
查块表是一次额外的间接寻址,内核要处理不规则的访问、变长的序列。vLLM 论文图 18a:PagedAttention 的注意力内核比 FasterTransformer 高度优化的内核慢 20%–26%。但它只影响注意力,不影响线性层,端到端仍然快得多(§7)。为了减小开销,vLLM 写了三个融合内核(§5.1):写 KV 时一次完成”切块 + 重排 + 写入”;读 KV 时把”按块表读”融合进注意力;写时复制要复制的块攒起来一次复制。
4.4 块大小的取舍
代码 02 演示 4(ShareGPT 式的长度分布,最终长度平均 493 个 token):
| 块大小 | 最后一块的浪费 | 平均块表长度 | 一个块(一层的 K 或 V) |
|---|---|---|---|
| 1 | 0.00% | 493 | 10 KB |
| 16 | 1.48% | 31 | 164 KB |
| 64 | 5.97% | 8 | 655 KB |
| 256 | 20.11% | 2 | 2.6 MB |
- 块越大,最后一块平均浪费越多(约半块)。
- 块越小,块表越长、每次读的数据越碎,GPU 的并行度用不满;换出时要做很多次小拷贝(§6.2)。
- vLLM 论文图 18b:块大小 16–128 在 ShareGPT 上最好,Alpaca(短序列)上 16、32 最好;默认取 16。
4.5 系统结构
vLLM 用一个集中式调度器(论文图 4):调度器里的块管理器维护所有序列的块表;每次迭代,调度器决定哪些序列这次跑、各需要几个新块,然后把 token 和块表一起发给 GPU worker。张量并行时(第 4 讲),每块 GPU 处理一部分注意力头,但所有 GPU 处理的是同样的 token 位置,所以它们共用同一张块表、同样的物理块编号——每块 GPU 的物理块里只存自己那几个头的 K、V(§4.6)。
5. 共享:并行采样、束搜索、共同前缀 · Sharing
代码:
01_paged_attention.py演示 3–4、04_prefix_caching.py演示 2
5.1 并行采样:fork + 写时复制
代码补全里常见”给我 n 个候选”:同一个提示生成 个样本。提示的 KV 只需要算一次、存一份(论文图 8):
- prefill 完成后,把这个序列
fork出 个——新序列的块表指向同样的物理块,每块的引用计数 +; - 每个样本往自己的最后一块写新 token 的 KV。如果最后一块是共享的(引用计数 > 1),先复制一份再写;
- 样本结束时
free:引用计数 −1,减到 0 的块才真正回到空闲链表。
代码 01 演示 3(提示 100 个 token,各生成 40 个,块大小 16):
| 样本数 | 输出 vs 各自一份 KV | 最后一步用的块 | 不共享时要的块 | 平均省下 | 写时复制次数 |
|---|---|---|---|---|---|
| 2 | 相同 | 12 | 18 | 37.6% | 1 |
| 4 | 相同 | 18 | 36 | 56.4% | 3 |
| 6 | 相同 | 24 | 54 | 62.7% | 5 |
提示的前 96 个 token 在 6 个满块里,第 7 块只填了 4 个。往那个半满的块里写时,前 个样本各复制一次,最后一个发现引用计数已经是 1,直接写——所以复制 次。论文图 15 测到并行采样省 6.1%–9.8%(Alpaca)、16.2%–30.5%(ShareGPT);我们的提示比输出长,所以省得更多。
5.2 束搜索:共享一起走过的路
束搜索每一步保留累计概率最高的 个候选;新的候选来自旧候选的扩展,几个新候选可能来自同一个父候选,也有父候选被淘汰。候选之间不只共享提示,还共享一起走过的那段路,而且共享关系每一步都在变(论文图 9,像操作系统里一连串 fork 出来的进程树)。
以前的系统要在候选之间频繁复制 KV:一个子候选要继承父候选的全部 KV。分页时,子候选 fork 父候选(共享块),只有往共享的最后一块里写时才复制一块。代码 01 演示 4(提示 64、生成 32 步、块大小 16):
| 束宽 | 个结果 | 整份复制:复制的 token | 分页:写时复制的 token | 平均省下的块 |
|---|---|---|---|---|
| 2 | 相同 | 5,088 | 208 | 39.1% |
| 4 | 相同 | 10,176 | 560 | 60.3% |
| 6 | 相同 | 15,264 | 976 | 67.5% |
复制量少了 16–24 倍;显存省 39%–68%(论文:37.6%–55.2% Alpaca、44.3%–66.3% ShareGPT)。结果完全相同。
5.3 共同前缀
很多请求以同样的系统提示或 few-shot 例子开头。服务方可以预先算好这些前缀的 KV、放在一组物理块里(像操作系统的共享库),新请求的块表直接指向它们,只算自己的部分(论文图 10)。§8 会把这个想法推广成自动的前缀缓存。
vLLM 用三个操作实现所有这些解码方式(§5.2):fork(从已有序列创建新序列)、append(追加 token)、free(删除序列)。模型和内核只看到每个序列的物理块编号列表,不需要知道共享关系——不同解码方式的请求可以放在同一个批里。
6. 调度与抢占 · Scheduling and preemption
代码:
03_paged_serving.py演示 3–4
6.1 不预留,就要能抢占
分页不预留:接纳一个请求时只看”现在放不放得下它的输入”。之后序列变长,块可能不够——第 6 讲 §3.5 的死锁就是这么来的。vLLM 的办法是抢占(§4.5):
- 先到先服务:最早到的请求优先;要抢占时,抢最晚到的。这保证最老的请求总能前进——只要单个请求放得下,就不会死锁。
- 全有或全无:一个序列的所有块总是一起被用到,所以要么全部换出、要么全部留下。一个请求里的几个序列(束搜索的候选、并行采样的样本)共享块,作为一个序列组一起调度、一起抢占。
6.2 被抢占的 KV 怎么恢复:换出还是重算
- 换出(swapping):把 KV 块拷到 CPU 内存,之后再拷回来。代价是 PCIe 的带宽,以及每次拷贝的固定开销。
- 重算(recomputation):直接丢掉,之后把”提示 + 已生成的 token”当作一个新的提示做一次 prefill。prefill 是一次大的矩阵乘法,比当初逐个 decode 快得多。
代码 03 演示 4 的模型(OPT-13B;PCIe 有效 25 GB/s、每次拷贝 10 µs,都是假设值;vLLM 每层的 K、V 各自成块,一个序列要拷贝 层数 × 2 × 块数 次):
| 序列长度 | 块大小 | 换出 + 换回 | 重算 |
|---|---|---|---|
| 1024 | 1 | 1.71 s | 143.77 ms |
| 1024 | 16 | 169.51 ms | 143.77 ms |
| 1024 | 64 | 92.71 ms | 143.77 ms |
| 1024 | 256 | 73.51 ms | 143.77 ms |
块小时拷贝次数多、固定开销占主导,重算快;块大时换出快。重算的时间和块大小无关。这和论文图 19a 的形状一致;论文实测重算最多比换出慢约 20%(在我们的假设下块很大时换出快约一倍),端到端在块大小 16–64 时两者相当。vLLM 默认用重算。
6.3 过载时的代价
代码 03 演示 3(ShareGPT,1500 个请求):
| 速率 req/s | 每 token 延迟 | 抢占次数 | 重算的 token / 所有输入 token |
|---|---|---|---|
| 1.5 | 29 ms | 99 | 10% |
| 2 | 53 ms | 709 | 63% |
| 3 | 465 ms | 2,099 | 159% |
| 4 | 890 ms | 2,517 | 191% |
刚过容量时抢占很少;严重过载时被抢占的请求反复重算,重算量超过了原本的输入量——这是用计算换显存。实际的 vLLM 还留一点水位(watermark)、限制每步接纳的数量,减少来回抢占。
7. 端到端:分页让服务快多少 · End-to-end
代码:
03_paged_serving.py演示 1–2
代码 03 演示 1 画了延迟–速率曲线(论文图 12 的模拟版;每格是平均每 token 延迟,ms):
| ShareGPT | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 req/s |
|---|---|---|---|---|---|---|
| Orca (Max) | 30 | 1500 | 3404 | 4387 | 4981 | 5377 |
| Orca (Oracle) | 23 | 65 | 769 | 1533 | 2103 | 2522 |
| vLLM | 23 | 25 | 29 | 53 | 208 | 465 |
每条曲线先平、后突然上翘:速率超过系统的处理能力以后,队列无限变长。要求”平均每 token 延迟 ≤ 50 ms”时二分查找最高速率(演示 2):
| 负载 | Max | Pow2 | Oracle | vLLM | vLLM / Oracle | vLLM / Max |
|---|---|---|---|---|---|---|
| ShareGPT | 0.63 | 0.51 | 0.95 | 1.98 | 2.1× | 3.2× |
| Alpaca | 4.01 | 22.09 | 40.15 | 91.90 | 2.3× | 22.9× |
论文的数字:ShareGPT 上 vLLM 能承受的速率是 Orca (Oracle) 的 1.7–2.7 倍、Orca (Max) 的 2.7–8 倍,是 FasterTransformer 的最多 22 倍;摘要里概括为”同样延迟下吞吐提高 2–4 倍”。共享前缀的翻译负载上,比 Orca (Oracle) 高 1.67 倍(1-shot)和 3.58 倍(5-shot);聊天负载上 2 倍。我们的模拟方向和量级一致。注意 Oracle 是”事先知道输出长度”的理想情况——分页不需要知道就超过了它。
论文 §8 也说了什么时候分页没用:显存不是瓶颈的负载(训练时形状固定;非 LLM 的模型多半受算力限制),分页的间接寻址只会带来开销。
8. 跨请求的前缀缓存 · Prefix caching across requests
8.1 从”共享”到”缓存”
§5.3 的共同前缀需要服务方事先知道哪些前缀会被共享。更一般的做法:请求结束后不立刻释放它的 KV 块,留在一个缓存里;新请求进来时,看看它的开头有没有已经算过的部分。真实负载里这种机会很多:系统提示、few-shot 例子、同一篇长文档上的多个问题、多轮对话(第 轮的输入 = 第 轮的输入 + 回答 + 新问题)、智能体的反复调用。
两种主流实现:
- SGLang 的 RadixAttention:用一棵基数树(radix tree)按 token 序列组织所有缓存的 KV,新请求沿树往下走找到最长的公共前缀;淘汰时按 LRU 从叶子开始删(CS349D 第 3 讲详细讲过)。
- vLLM 的自动前缀缓存(automatic prefix caching)和 Mooncake:按块做链式哈希,哈希相同的块就是可以复用的块。
8.2 链式哈希
每个完整块的哈希 = hash(前一块的哈希 ‖ 这一块的 token)(Mooncake 图 3)。代码 04 演示 1(块大小 4):两个请求共享系统提示和开头,前 3 块哈希相同、第 4 块不同;把系统提示里一个 token 改掉,那一块以及之后所有块的哈希都变了。
为什么要”链式”?因为一个 token 的 K、V 不只取决于它自己,还取决于它前面的全部 token(注意力看得见前文)。两段 token 完全相同、但前文不同的块,KV 是不一样的,不能复用。链式哈希让”哈希相同”⇔“从开头到这一块完全相同”。所以前缀缓存只能复用从开头开始连续相同的部分。不满一块的零头不进缓存。
8.3 在真实模型上复用
代码 04 演示 2 复现 vLLM 论文图 16 的设置:8 个请求共享一段 few-shot 前缀(1-shot 80 个 token、5-shot 341 个 token),各自的问题 10–30 个 token。第一个请求从头算,把完整的块登记进缓存;后面的请求按哈希找到缓存的块,块表直接指向它们(引用计数 +1、不复制),只算自己的后缀:
| 前缀 | 8 个请求的输入 token | 实际算的 | 省下 | 和从头算的最大差 |
|---|---|---|---|---|
| 1-shot(80) | 791 | 231 | 71% | 0 |
| 5-shot(341) | 2,875 | 523 | 82% | 0 |
5-shot 的前缀是 21 个满块 + 5 个零头,零头每次要和后缀一起重算。前缀越长、后缀越短,省得越多。
8.4 命中率与缓存容量
Mooncake 开源了一个真实 trace(1 小时、23,608 个请求,只有到达时间、输入输出长度和按 512 个 token 一块的哈希编号,不含内容):平均输入 8,590、输出 182 个 token(平均输入是平均输出的约 47 倍,论文说逐请求的输入 / 输出比平均约 720——Kimi 以长上下文著称)。超过一半的块之后再没被用到,少数块被访问上万次。论文表 1:单一的全局缓存,容量从 1,000 块增加到 50,000 块,LRU 的命中率从 0.34 涨到 0.55,再大就几乎不涨。
代码 04 演示 3 用一个仿这些统计量的合成 trace(4,157 个请求、24 分钟,平均输入 8,709、输出 183;56% 的块只出现一次):
| 策略 \ 容量(块) | 无限 | 30,000 | 10,000 | 3,000 | 1,000 |
|---|---|---|---|---|---|
| LRU | 0.58 | 0.58 | 0.56 | 0.50 | 0.31 |
| LFU | 0.58 | 0.58 | 0.50 | 0.30 | 0.20 |
| 论文 LRU | 0.55 | 0.54 | 0.46 | – | 0.34 |
| 论文 LFU | 0.55 | 0.50 | 0.40 | – | 0.34 |
- 命中率先快速上升、再平缓:无限容量时的上限由”第一次出现的块”决定,再大的缓存也没用。
- LRU 比 LFU 好(论文也是):复用多半发生在时间上相近的请求之间(同一个会话的下一轮);LFU 让”历史上很热、现在没人用”的块赖着不走。
8.5 省下的计算,和需要的存储
代码 04 演示 4 把命中率换算成 prefill 的 GPU 时间(LLaMA2-70B、8 块 A800):无限缓存时 prefill 的总 GPU 时间从 1.05 小时降到 0.42 小时(2.46 倍),容量 10,000 块时 2.34 倍、1,000 块时 1.46 倍。
但缓存很大:一块 512 个 token 的 KV 是 168 MB,10,000 块要 1.68 TB——一台 8 卡机器的 HBM 一共只有 0.64 TB。前缀缓存想要高命中率,就必须走出 GPU 显存。这就是 Mooncake 的出发点。
9. Mooncake:以 KV cache 为中心的集群 · Mooncake
9.1 背景与目标
Mooncake 是月之暗面(Moonshot AI)的 Kimi 的服务平台。它面对的现实:长上下文(输入是输出的几十倍)、大量可复用的前缀、GPU 供给有限、经常过载。作为模型即服务(MaaS)的提供方,目标是:在满足 TTFT 和 TBT(time between tokens,即第 5 讲的 TPOT)SLO 的前提下,最大化有效吞吐——只有完整完成的请求才算数,做了一半被放弃的请求,前面花的资源全部白费(§2)。
9.2 架构
(论文图 1、图 4)
- prefill 池和 decode 池分开(第 5 讲的解耦);
- 再加一个分布式 KV 池:用 GPU 集群里本来闲着的 CPU 内存、SSD 和 RDMA 网卡,按块存放 KV(块带链式哈希,见 §8.2),淘汰可以用 LRU / LFU 等;块在 CPU 和 GPU、机器和机器之间的传输由一个基于(GPUDirect)RDMA 的组件 Messenger 负责;
- 中心是全局调度器 Conductor:为每个请求选一对 prefill 实例和 decode 实例,并决定 KV 块的复制和换出。
一个请求的四步:(1) KV 复用:把能复用的前缀 KV 从(可能是远程的)CPU 内存读到选中的 prefill 实例;(2) 增量 prefill:只算没命中的部分,长的输入切成块、流水线执行,新算出的 KV 写回 CPU 内存;(3) KV 传输:按层把 KV 流式传到 decode 实例的 CPU 内存,和 prefill 重叠;(4) decode:KV 到齐后加入 decode 实例的连续批处理。decode 实例在接纳时会再确认一次 TBT SLO,可能拒绝——这时 prefill 就白做了(§9.6 的问题)。
9.3 用存储换计算
为什么值得把 KV 存到 CPU 内存、甚至 SSD 上?代码 05 演示 1 比较每个 token”读回来”和”重算”的时间(带宽都是假设值,按一台 8 卡机器合计):
| 模型与硬件 | 每 token 的 KV | 重算 | 本机 CPU 内存(PCIe) | 远程内存(RDMA 800 Gbps) | 本机 SSD |
|---|---|---|---|---|---|
| LLaMA2-70B(GQA),8 块 A800 | 328 KB | 98.9 µs | 1.6 µs(60×) | 3.3 µs(30×) | 13.7 µs(7×) |
| OPT-13B(MHA),1 块 A100 | 819 KB | 155.1 µs | 32.8 µs(4.7×) | 65.5 µs(2.4×) | 273.1 µs(0.6×) |
(括号是”重算 / 读回”,大于 1 表示读回更快。)
- LLaMA2-70B 每个 token 的 KV 只有 320 KB(GQA 把 KV 头从 64 减到 8),重算一个 token 却要约 140 GFLOP:从哪一层存储读回来都比重算快一个数量级以上——“用存储换计算”。
- MHA 的 OPT-13B 每个 token 的 KV 是 819 KB,计算量却只有 70B 的五分之一:读回来只快几倍,从 SSD 读反而更慢。KV 缓存值不值得放到慢的存储上,取决于模型结构——GQA、MLA 这类 KV 小的设计(第 2 讲 §6)让分层缓存更划算。这是又一个模型和系统协同设计的例子。
另外,Mooncake 按层重叠(§5.2 layer-wise prefill):第 层计算时异步读第 层的 KV、异步写出第 层新算的 KV。代码 05 演示 1:一个 8.7K token、命中一半的请求,按层重叠时 prefill 从 870 ms 降到 454 ms,读 KV 的时间几乎全藏在计算后面;不重叠时从 SSD 读要多 60 ms。论文还指出这让 prefill 节点几乎不受 GPU 显存大小的限制(只要放得下一个请求)。
9.4 按 KV 调度:Conductor 的算法
(论文算法 1、§6)
以前的负载均衡只看每个实例上有多少请求。Mooncake 的 prefill 调度还要看谁手里有这个请求的前缀:对每个 prefill 实例,估计
选最小的(排队时间 = 排在前面的请求的预计 prefill 时间之和;prefill 时间用离线测得的预测模型)。如果最好的那个实例太忙,而另一个实例命中的前缀长得多(超过一个阈值倍数),就把那段 KV 拉过来(估计时加上传输时间),拉过来的块存在本地——热门的前缀因此自动复制到多台机器上(hot-spot migration),避免所有请求都挤到同一台机器去拉。如果哪个实例都满足不了 TTFT SLO,直接返回 HTTP 429。
代码 05 演示 2(8 个 prefill 实例,各有 3,000 块的本地缓存,TTFT SLO 5 秒):
| 调度策略 | 平均 TTFT(7.1 req/s) | TTFT ≤ 5 s | 命中的输入 token | 实际算的 token |
|---|---|---|---|---|
| 随机 | 17.99 s | 23.8% | 27% | 26.6 M |
| 按负载(队列最短) | 10.61 s | 43.9% | 27% | 26.3 M |
| 按缓存(估计 TTFT 最小) | 0.56 s | 100% | 56% | 16.1 M |
| 按缓存 + 热点 KV 复制 | 0.42 s | 100% | 59% | 14.8 M |
随机或按负载分配时,同一篇文档的请求散到 8 个实例上,每个实例的缓存都只命中一小部分;按缓存分配让后续请求自然落到算过它的实例上,命中率翻倍、要算的 token 少约 40%,负载高时 TTFT 差一个数量级以上。加上 KV 复制再好一些(多了 6.8 M 个 token 的网络传输)。排序和论文图 8 一致(论文在 8P+8D 的集群上回放 23,000 个真实请求)。
9.5 长上下文的 prefill:分块流水线并行
长上下文(128K 甚至 1M token)的 prefill 一台 8 卡机器太慢。跨机器做张量并行要每层两次跨机 all-reduce(第 4 讲),太贵;序列并行(ring attention 等)每层也要跨机通信,还要和 KV 传输抢网络。Mooncake 用分块流水线并行(chunked pipeline parallelism,CPP,§5.1):把 台 prefill 机器组成一条流水线(每台负责一部分层),把一个长请求的输入切成块,不同的块同时在不同的机器上算——利用了 decoder-only 的因果性(后面的块只依赖前面块的 KV)。只在流水线段的边界通信,容易和计算重叠;对短请求几乎没有额外开销,也不需要频繁调整并行配置。
9.6 过载:提前拒绝与振荡
(论文 §7)
真实服务经常过载:用户请求的增长远快于 GPU 的增长。过载时必须拒绝一部分请求,问题是什么时候拒绝。
- 基线:到达时只看 prefill 能不能按时做完。做完 prefill 才发现 decode 实例放不下(TBT SLO),拒绝——prefill 白做了。
- 提前拒绝(early rejection):到达时就同时看 prefill 和 decode 的负载,decode 现在已经满了就直接拒绝。
- 振荡:可是”现在的 decode 负载”和”这个请求真正进入 decode 时的负载”之间隔着一个 prefill 的时间。decode 满时拒绝一切,prefill 闲下来;decode 空出来以后一下子接很多,等它们做完 prefill 涌进 decode,decode 又满了——prefill 和 decode 的负载此起彼伏(论文图 9、10a,在 20 台机器的集群上观测到)。
- 基于预测的提前拒绝:预测这个请求 prefill 完成那一刻的 decode 负载。Mooncake 目前用系统级的预测:假设每个请求的 decode 都花同样的时间 ,据此推算那一刻哪些请求已经结束、哪些新请求会进来。
论文表 3(8P+8D、真实 trace 2 倍速回放):拒绝的请求数从 4,183(基线)降到 3,771(提前拒绝)和 3,589(基于预测)。
代码 05 演示 3 做了一个小模拟(8 个 prefill + 8 个 decode 实例;为了分开”负载本身的波动”和”调度引起的波动”,用平稳的泊松到达,速率是 decode 容量的 2 倍):
| 策略 | 完成 | prefill 后才拒绝 | 白做的 prefill | prefill 与 decode 负载的相关系数 |
|---|---|---|---|---|
| 基线 | 4,129 | 1,871 | 1,646 秒 | +0.15 |
| 提前拒绝 | 4,156 | 564 | 480 秒 | −0.33 |
| 按预测提前拒绝( 相同) | 3,924 | 1,204 | 1,091 秒 | +0.23 |
- 提前拒绝让白做的 prefill 少了约 70%,负载出现了论文描述的反相(相关系数为负)。
- 但完成的请求几乎一样多:在这个模拟里 decode 池才是瓶颈,完成数由它决定;省下来的是 prefill 的算力(可以少开几台 prefill 机器,或者去做别的事)。
- 按论文的办法(假设每个请求 decode 时间相同)做预测,在这个输出长度长尾分布的负载上预测不准:白做的 prefill 只少了约三分之一,完成数反而少了约 5%。我们的玩具复现不出论文”拒绝得更少”这一点——论文的 SLO 统计(P90 TTFT / TBT)、decode 的接纳条件、真实负载的形状都和这里不同。我把这个结果如实留下:提前拒绝的价值首先是不浪费资源;它能不能让更多请求完成,取决于哪个阶段是瓶颈,以及预测有多准。
9.7 结果
论文的端到端实验都用和 LLaMA2-70B 同结构的哑模型(保护商业信息),8×A800 节点、800 Gbps RDMA:
- 公开数据集(ArXiv 摘要、L-Eval)上,Mooncake-[3P+1D] 比 vLLM-[4M] 吞吐高 20% 和 40%(L-Eval 的前缀缓存命中率高);
- 模拟的长上下文数据(16K–128K)上,吞吐高 50%–525%(vLLM 的长 prefill 严重干扰 decode);
- 真实负载回放:Mooncake-[10P+10D] 约 100% 的请求满足 TBT SLO,vLLM-[20M] 只有 57%;同样的 SLO 下多处理约 75% 的请求。
10. 其他让 KV 变小、变便宜的办法 · Other ways to make KV cheaper
本讲的方法都不改变计算结果(分页、共享、缓存、换出)。还有一类方法直接让 KV 变小,代价是可能改变模型的输出:
- 改结构:MQA / GQA / MLA(第 2 讲 §6)。DeepSeek-V3 的 MLA 每个 token 只缓存一个压缩的潜在向量。
- 量化 KV:FP8 / INT8 / INT4 的 KV cache(第 3 讲 §3.5)。显存和带宽都按比特数缩小。
- 丢掉不重要的 token:StreamingLLM(只保留开头几个”注意力汇聚”token 和最近的窗口)、H2O、SnapKV 等按注意力分数挑选要保留的 token。对长上下文很有效,但是有损的。
- 压缩后传输:CacheGen 等把 KV 编码压缩,减少跨机器传输和存储的字节(Mooncake 这类系统的补充)。
- 稀疏注意力:只读一部分 KV(如 DeepSeek 的稀疏注意力),减少带宽,但 KV 仍要存。
这些和本讲的方法正交,可以叠加:分页管理量化过的 KV,前缀缓存存 MLA 的潜在向量。
11. 现状(2025–2026)· State of the field
- 分页是默认:vLLM、SGLang、TensorRT-LLM 都用分页的 KV;FlashAttention、FlashInfer 都有直接读块表的注意力内核。
- 前缀缓存是默认:vLLM V1 的自动前缀缓存(按块哈希)、SGLang 的 RadixAttention 都默认打开;多轮对话、智能体负载(第 15 讲)让命中率非常高。
- KV 走出 GPU:KV 卸载到 CPU 内存和 SSD 成为常见功能(LMCache、NVIDIA Dynamo 的 KV 块管理器、Mooncake Store);Mooncake 的传输引擎开源后被集成进 vLLM 和 SGLang 的 prefill / decode 解耦实现。
- KV 感知的路由:多副本部署时,按”哪个副本缓存了这个请求的前缀”路由(SGLang router、Dynamo 的 KV-aware router),和本讲的 Conductor 是同一个思路。
- 硬件跟进:更大的 HBM(Blackwell Ultra 288 GB,第 1 讲 §5.6)、CPU 和 GPU 之间更快的互连(Grace Hopper / Grace Blackwell 的 NVLink-C2C),让”KV 放在 CPU 内存里”更便宜。
12. 小结 · Summary
- KV cache 大、会长、长度事先不知道;能同时放下几个请求,decode 的吞吐就是几倍,所以 KV 的显存管理直接决定吞吐。
- 连续分配有三种浪费:预留、内部碎片、外部碎片。vLLM 论文测到以前的系统只有 20%–38% 的 KV 显存存着 token;我们的模拟里按最大长度预留约 23%、事先知道长度也只有约 39%。
- PagedAttention 把 KV 切成固定大小的块、用块表映射,按需分配,浪费只在最后一块;块大小 16 是浪费和效率的折中;注意力内核慢 20%–26%,但端到端快得多。
- 块可以共享:引用计数 + 写时复制让并行采样、束搜索、共同前缀省下大量显存和复制;结果完全相同。
- 不预留就要能抢占:先到先服务、抢最晚到的、全有或全无;恢复时块小用重算、块大用换出。
- 端到端:同样延迟下,分页能承受的请求速率约是 Oracle 的 2 倍、按最大长度预留的 3 倍以上(论文 1.7–2.7 倍、2.7–8 倍)。
- 前缀缓存用链式哈希认出算过的前缀;命中率随容量饱和、LRU 好于 LFU;高命中率需要 TB 级的存储,GPU 显存装不下。
- Mooncake 把 KV 放到集群的 CPU 内存 / SSD 池里(GQA 让读回比重算快一个数量级以上)、按 KV 的位置调度 prefill(命中率翻倍、TTFT 降一个数量级)、过载时提前拒绝(少浪费 prefill,但可能引起负载振荡;能否多完成请求取决于瓶颈和预测)。
13. 常见误区 · Common pitfalls
- “分页会改变注意力的结果”——分页只改变 KV 放在哪里;代码 01 验证 logits 一致到 。
- “预留的显存反正会被用到,不算浪费”——它在请求的整个生命周期里占着显存,本来可以先给别的请求用;而且实际生成往往远短于声明的上限。
- “块越小越好,浪费最少”——块太小,块表变长、内核读的数据太碎、换出要做很多次小拷贝;vLLM 取 16。
- “共享前缀的 KV 只要 token 相同就能复用”——还要求前面的全部内容相同(注意力看得见前文),所以用链式哈希、只复用从开头连续相同的部分。
- “不预留 KV 会死锁”——没有抢占才会;分页 + 抢占最晚到的请求,最老的请求总能前进。
- “换出总比重算好,因为不浪费计算”——块小时换出被拷贝次数拖垮;prefill 很快时重算更省事。
- “前缀缓存越大越好”——命中率会饱和(第一次出现的块再大的缓存也帮不上);TB 级的缓存要放在 CPU 内存 / SSD 上,读回的带宽和延迟要算进去。
- “KV 放到 SSD 上一定比重算快”——取决于每 token 的 KV 字节和计算量之比:GQA 的 70B 模型快 7 倍,MHA 的 13B 模型反而更慢(代码 05 演示 1)。
- “提前拒绝一定让更多请求完成”——它首先是少浪费 prefill;decode 是瓶颈时完成数几乎不变,还可能引起负载振荡(代码 05 演示 3)。
- “LFU 适合热门内容,所以比 LRU 好”——KV 的复用多半在时间上相近的请求之间,LRU 更好(Mooncake 表 1 和我们的模拟都是)。
14. 练习 · Exercises
练习 1(KV 有多大) OPT-13B(40 层、hidden 5120、MHA、FP16)。(a) 每个 token 的 KV 多少字节?(b) 一个 2048 token 的请求呢?(c) A100 40GB 上权重 26 GB,如果剩下的全给 KV,最多放几个 2048 token 的请求?
答案:(a) 字节 ≈ 819 KB(论文按 1 KB = 1024 B 写作”800 KB”)。(b) GB。(c) ,最多 8 个;按代码里给激活留 10% 的算法,KV 有 12,596 个槽位, 个。
练习 2(伙伴分配器的内部碎片) 三个请求分别要 300、600、1100 个槽位,伙伴分配器各给多少?内部碎片占分配总量的多少?
答案:512、1024、2048,共 3584;浪费 ,占 44%。
练习 3(块大小与浪费) 请求的最终长度平均 493 个 token。假设最后一块平均填一半,估算块大小 16 和 256 时最后一块的浪费比例;和代码 02 演示 4 的 1.48%、20.11% 比较。
答案:16:,和 1.48% 接近。256:,实测 20.11% 更低——“平均填一半”对大块不成立:块比很多请求还长时,浪费 = 块大小 − 长度,和长度分布有关,不再是半块。
练习 4(并行采样的写时复制) 个样本 fork 自同一个序列,最后一块半满。写下一个 token 时一共发生几次写时复制?如果提示长度正好是块大小的整数倍呢?
答案: 次:前 个写的样本各复制一次,每次复制后原块的引用计数 −1,最后一个样本看到引用计数是 1,直接写。整数倍时最后一块是满的,下一个 token 写到新分配的块里,不需要复制(代码 01 的自检也测了这种情况)。
练习 5(束搜索的复制量) 束宽 ,提示 64 个 token,生成 32 步。“整份复制”的做法:第一步 4 个子候选各复制 64 个 token 的 KV;之后第 步()每个子候选复制父候选当时的 个 token。一共复制多少个 token?和代码 01 演示 4 的 10,176 对照。
答案:。✓ 分页时只复制了 560 个 token(35 次写时复制 × 16)。
练习 6(换出 vs 重算的交叉点) 用代码 03 演示 4 的模型(OPT-13B,PCIe 25 GB/s,每次拷贝 10 µs,每层 K、V 各自成块),1024 个 token 的序列,重算要 143.77 ms。块大小 32 时,换出 + 换回要多久?哪个更快?
答案:每块一层的 K(或 V) 字节,拷贝一次 ;次数 ,换出 59.2 ms,换出 + 换回 118.3 ms < 143.77 ms,换出更快。交叉点在块大小 16 到 32 之间。
练习 7(共享前缀省多少) 5-shot 前缀 341 个 token(块大小 16),8 个请求,后缀平均 20 个 token。估算前缀缓存省下的 prefill token 比例。
答案:341 = 21 个满块(336)+ 5 个零头。第一个请求算 ;其余 7 个各算 。实际算 ,总共 ,省 81%(代码 04 演示 2 实测 82%,后缀长度是随机的)。
练习 8(用存储换计算) LLaMA2-70B:(a) 每个 token 的 KV(80 层、8 组 KV、、BF16)?(b) 每个 token 的矩阵乘法约 137 GFLOP,8 块 A800 按 60% 峰值(每块 312 TFLOPS),重算一个 token 要多久?(c) 从 100 GB/s 的 RDMA 读一个 token 的 KV 要多久?从一块 6 GB/s 的 SSD 呢?
答案:(a) 字节。(b) (加上注意力约 99 µs,代码 05 演示 1)。(c) RDMA (快约 28 倍);一块 SSD ——仍然比重算快(约 1.7 倍)。
练习 9(为什么 GQA 让分层缓存更划算) 比较 OPT-13B 和 LLaMA2-70B 的”每 FLOP 的 KV 字节”。这个比值和”读回 vs 重算”有什么关系?
答案:OPT-13B: B/FLOP;LLaMA2-70B: B/FLOP,小约 13 倍。读回时间 ∝ KV 字节 / 带宽,重算时间 ∝ FLOPs / 算力,所以读回 / 重算 = (B/FLOP) × (算力 / 带宽)。同样的硬件上,B/FLOP 小 13 倍,读回就相对便宜 13 倍——GQA / MLA 让”把 KV 存在慢存储上”变得划算。
练习 10(链式哈希) 一个请求有 5 个完整的块。如果第 3 块里的一个 token 被改了,哪些块的哈希会变?为什么不能每块单独哈希?
答案:第 3、4、5 块的哈希都变(第 1、2 块不变)。每块单独哈希会把”token 相同但前文不同”的块当成同一块——可它们的 KV 不同(注意力看得见前文),复用就会算错。
练习 11(提前拒绝什么时候能让更多请求完成) 代码 05 演示 3 里提前拒绝几乎没有增加完成的请求数。说说在什么条件下它会增加。
答案:完成数由瓶颈阶段决定。decode 是瓶颈时,基线也能把 decode 一直填满(prefill 多做的那些只是白做),完成数不变。如果 prefill 也是瓶颈,白做的 prefill 会挤占别的请求的 prefill,让它们的 TTFT 超过 SLO 被拒绝——这时提前拒绝把 prefill 留给”之后进得了 decode”的请求,完成数会增加。另外,如果 TTFT / TBT 的 SLO 按 P90 统计(论文的做法),排队变短也会让更多请求达标。预测要准,否则(像我们按”decode 时间相同”预测长尾输出时)会误拒能完成的请求。
练习 12(为什么分页 + 抢占不会死锁) 第 6 讲 §3.5 里”不预留”会死锁。vLLM 也不预留,为什么不会?
答案:显存不够时,vLLM 抢占最晚到的请求,释放它的全部块。只要单个请求放得下,最早到的那个请求永远不会被抢占,它总能前进直到完成、释放显存;然后下一个最早的请求前进……所以系统总有进展。代价是被抢占的请求要换出或重算(代码 03 演示 3:严重过载时重算量可以超过原本的输入量)。
15. 延伸阅读 · Further reading
阅读材料
- W. Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023. https://arxiv.org/abs/2309.06180
- R. Qin et al., Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving. arXiv 2407.00079(会议版 FAST 2025;阅读材料链接 ACM 期刊版 https://dl.acm.org/doi/abs/10.1145/3773772)。开源 trace 与代码:https://github.com/kvcache-ai/Mooncake
前缀缓存与 KV 存储
- L. Zheng et al., SGLang: Efficient Execution of Structured Language Model Programs, NeurIPS 2024(RadixAttention). https://arxiv.org/abs/2312.07104
- Y. Liu et al., CacheGen: KV Cache Compression and Streaming for Fast Large Language Model Serving, SIGCOMM 2024. https://arxiv.org/abs/2310.07240
- vLLM 文档:Automatic Prefix Caching(设计说明里有按块哈希的细节)。
让 KV 变小
- G. Xiao et al., Efficient Streaming Language Models with Attention Sinks(StreamingLLM), ICLR 2024. https://arxiv.org/abs/2309.17453
- Z. Liu et al., KIVI: A Tuning-Free Asymmetric 2bit Quantization for KV Cache, ICML 2024. https://arxiv.org/abs/2402.02750
操作系统背景
- R. Arpaci-Dusseau & A. Arpaci-Dusseau, Operating Systems: Three Easy Pieces(免费在线教材),“Paging”、“Free-Space Management”(含伙伴分配器)两章。
本仓库
- CS349D 第 3 讲:PagedAttention 与 SGLang 的 RadixAttention(基数树、LRU 叶子淘汰、缓存感知调度)。
- CS349D 第 9 讲:Mooncake、CacheGen 和分层 KV 存储的更多模拟。
- CS349D 前置讲 P3:操作系统基础(分页、写时复制、页面替换算法)。
16. 下一讲预告 · Next lecture
到这里,“推理系统”这一部分结束了:我们从一步 decode 的 roofline(第 1 讲)出发,讲了模型怎样改(第 2、3 讲)、怎样切到多块芯片上(第 4 讲)、prefill 和 decode 怎样分开(第 5 讲)、decode 怎样批处理和投机(第 6 讲)、KV 怎样管理(本讲)。一直有一个东西被我们当成黑盒:硬件本身——“峰值算力”和”显存带宽”从哪里来?为什么 H100 的矩阵乘法比向量运算快 15 倍?为什么 TPU 用脉动阵列?
第 8 讲打开这个黑盒:
- GPU:SM、warp 调度、SIMT、Tensor Core 的演进(Volta → Hopper → Blackwell)、TMA 与异步拷贝、warp specialization、存储层次;
- TPU:脉动阵列怎样让每个数据被用很多次;Jouppi et al. 2017 的测量——为什么推理(尤其是 decode 式的负载)受限于内存带宽;
- SRAM 芯片:把权重放在片上的另一条路。