Mooncake KV Metadata
导言
最初的问题很直接:Mooncake 管理 KV Cache,那么“KV Cache 的元数据”能否选择放在内存或 SSD?继续追问后才发现,元数据这个词把几种完全不同的东西揉在了一起:PagedAttention 的 Block Table、Master 保存的全局副本目录、SSD backend 保存的文件位置索引,以及为了故障恢复生成的 snapshot。
只有先回答“哪一种元数据真正负责定位 KV Cache 数据页”,才有可能讨论它应该放在哪里。本文基于 Mooncake 固定版本 0518784d,沿代码中的两级定位链路展开,再解释 Master snapshot 如何以较短在线停顿保存控制面状态,以及它为什么不能替代 KV payload 的持久化。
一句话结论:Mooncake 用 Master 元数据定位副本所在节点和介质,再用节点内 backend 索引定位 SSD 文件与 offset;两者运行时都依赖 DRAM,snapshot 只是 Master 控制面目录的持久化 checkpoint,不是在线 SSD 元数据查询层,也不包含 KV 数据本身。
先说清楚“数据页”¶
当我们说“定位 KV Cache 数据页”时,至少可能在问四种映射:
| 定位关系 | 负责层 | 典型结果 |
|---|---|---|
| 请求、Layer、Token Block → Mooncake Key | vLLM、SGLang 等集成层 | Block Hash 或对象 Key |
| Mooncake Key → 哪台机器、哪个副本 | Master | MEMORY、LOCAL_DISK 等 Replica |
| Mooncake Key → SSD 文件内的位置 | 本地 Storage Backend | bucket_id + offset 或数据文件 offset |
| 文件 offset → SSD 物理块 | 文件系统、内核和 SSD 控制器 | LBA、闪存物理页 |
Mooncake Store Core 主要认识的是 string key → object bytes。它不会仅凭一个请求 ID 自动理解这是第几层 Transformer、哪一段 Token 的 KV Block;这层语义需要 Connector 或推理引擎先转换成 Mooncake Key。
同样,Mooncake backend 中的 offset 也不是 SSD NAND 的物理页号。它是文件或 backend 数据区内的字节偏移,之后仍要经过文件系统和块设备映射。
不是同一张页表
PagedAttention 的 Block Table 保存“当前请求的逻辑 KV Block → 本次运行分配的 GPU Block ID”。Mooncake 的元数据保存“可复用对象 Key → 分布式副本位置”。前者服务 Attention Kernel,后者服务跨实例缓存查询,不能把两者当成同一张可延伸到 SSD 的页表。
问题收窄以后,本文真正关心的是中间两层:Master 怎样找到副本,本地 backend 又怎样找到数据。
一次查询要经过两级定位¶
用图书馆作类比,Master 像总目录,它知道一本书在哪个分馆、放在普通书架还是归档库;分馆内部还有一张货架卡,记录它位于哪个柜子、从第几格开始。总目录不能代替货架卡,货架卡也无法告诉外部读者应该先去哪个分馆。
flowchart LR
A[逻辑 KV Block] --> B[Mooncake Key]
B --> C[Master ObjectMetadata]
C -->|MEMORY Replica| D[endpoint + buffer address + size]
C -->|LOCAL_DISK Replica| E[client_id + endpoint + size]
E --> F[远端 FileStorage]
F --> G[本地 backend index]
G --> H[bucket/file + offset + data_size]
图中最重要的分叉是:内存副本基本可以由 Master 直接给出地址,而 Local SSD 副本还需要节点内的第二次查询。
Master 保存全局副本目录¶
Master 使用固定 1024 个 shard 保存对象目录:
metadata_shards_[1024]
└── unordered_map<TenantId, TenantState>
└── unordered_map<string, ObjectMetadata>
└── vector<Replica>
ObjectMetadata 保存对象大小、checksum、数据类型、Lease、Pin、Group 和 Replica 列表等控制状态;它不是 KV payload。结构定义见 master_service.h,两层 Map 与 shard 定义见 master_service.h。
GetReplicaList() 直接访问这些内存 Map,再返回可读的 Replica Descriptor;查询过程中不会按 Key 去读 SSD、snapshot 或 etcd。相关路径见 MetadataAccessorRO 与 GetReplicaList。
对于 MEMORY Replica,MemoryDescriptor 最终携带:
所以查询结果已经能够表达“通过哪个传输端点,从哪个远端内存地址读取多少字节”。字段定义见 allocator.h。
SSD backend 保存本地精确位置¶
LocalDiskDescriptor 不包含文件路径和 offset,只保存:
它回答“哪个 Client 持有 SSD 副本”,但没有回答“数据在该 Client 的 SSD 哪里”。定义见 replica.h。
以 Bucket backend 为例,本地 object_bucket_map_ 把 Key 映射到:
struct StorageObjectMetadata {
int64_t bucket_id;
int64_t offset;
int64_t key_size;
int64_t data_size;
std::string transport_endpoint;
};
读取时先查 object_bucket_map_[key],再根据 bucket_id + offset 读取对应 .bucket 文件。结构与读取路径见 types.h 和 storage_backend.cpp。
OffsetAllocator backend 则维护 Key → ObjectEntry:
struct ObjectEntry {
uint64_t offset;
uint32_t total_size;
uint32_t value_size;
AllocationPtr allocation;
uint64_t fifo_seq;
};
这里的 offset 指向 kv_cache.data 中的记录位置,见 storage_backend.h。
如果问题是“哪一种元数据最像传统存储系统里定位数据页的索引”,答案就是:SSD backend 本地索引。但一次跨节点查询仍然离不开 Master 的全局副本目录,所以完整答案是两级索引,而不是二选一。
元数据能否放到 SSD¶
这个问题现在可以分层回答:
| 对象 | 运行时位置 | 可用于恢复的持久化记录 | 能否改成只查 SSD |
|---|---|---|---|
| Master 全局对象目录 | Master DRAM | snapshot;HA 模式另有 OpLog 记录变更 | 不能 |
| Bucket 本地位置索引 | Client DRAM | 每个 Bucket 的 .meta 文件 |
不能 |
| OffsetAllocator 本地位置索引 | Client DRAM | 可选 kv_cache.meta checkpoint |
不能 |
| KV payload | MEMORY 或 LOCAL_DISK | 由 Replica 放置策略决定 | 可以 offload |
所以,Mooncake 可以控制 KV 数据放 DRAM 还是 SSD,但不能控制在线 Master 元数据只驻留 SSD。--memory_allocator=offset|cachelib 管理的是 KV payload 使用的 Segment allocator,也不作用于 metadata_shards_。
Bucket backend 采用“DRAM 索引 + SSD .meta”模式:启动时扫描 .meta 文件,随后重建完整的 object_bucket_map_ 和 buckets_。恢复代码见 storage_backend.cpp。
OffsetAllocator backend 也始终需要 DRAM 中的索引,不过可以选择是否把恢复 checkpoint 写到 SSD:
export MOONCAKE_OFFLOAD_STORAGE_BACKEND_DESCRIPTOR=offset_allocator_storage_backend
export MOONCAKE_OFFLOAD_FILE_STORAGE_PATH=/nvme/mooncake-offload
export MOONCAKE_OFFSET_PERSIST_MODE=relaxed # disabled | relaxed | strict
export MOONCAKE_OFFSET_PERSIST_INTERVAL_SECONDS=60
disabled:默认不保留可恢复索引。relaxed:周期生成 checkpoint。strict:每次BatchOffload建立持久化屏障。
环境变量解析见 storage_backend.cpp。即使启用持久化,重启后仍会重建内存 Map,而不是把在线查询改成 SSD 查询。
源码与文档存在时间差
当前部署文档仍写着 OffsetAllocator 不支持恢复,这只符合默认 disabled 模式。固定版本源码和仓内测试已经包含 relaxed/strict persistence,因此部署前应以实际版本源码和测试为准,而不能只看旧文档。
Snapshot 备份的到底是什么¶
既然 Master 元数据始终驻留 DRAM,自然会产生下一个问题:Master 挂掉以后,内存目录怎么办?这就是 Master snapshot 解决的问题。
Snapshot 不是在线 Metadata Store,也不是 Master 内存的逐字节镜像。它是一个周期性一致性 checkpoint,主要保存三类 payload:
metadata:对象目录、Replica、Lease、checksum、data type、hard pin、group、discarded replicas 和下一个 Replica ID。segments:内存 Segment、allocator 与 Local SSD Manager 的持久化状态。task_manager:任务管理状态。
此外还有描述格式和版本的 manifest.txt、记录序列边界与 view version 的 descriptor.txt,以及指向最新完整快照的 latest.txt。
Soft Pin、锁、指标等纯运行时状态不会原样持久化。最重要的是,snapshot 不包含 KV Cache payload 字节。它保存的是“数据在哪里”的控制面目录,而不是数据本身。
如何获得一致快照¶
Mooncake 采用 fork + Copy-on-Write:
sequenceDiagram
participant R as 请求线程
participant P as Master 父进程
participant C as Snapshot 子进程
participant S as Local SSD / S3
R->>P: Put/Get 持有 snapshot shared lock
P->>P: snapshot 线程申请 exclusive lock
P->>C: fork,一致状态停在该时刻
P-->>R: 释放锁,继续在线服务
C->>C: MessagePack 编码、Zstd 压缩
C->>S: 上传 payload 与 manifest
C->>S: 发布 descriptor,更新 latest
真正阻塞在线请求的主要是取得独占锁并完成 fork() 的短窗口。子进程获得 fork 时刻的内存视图,父进程释放锁后继续处理请求;序列化、压缩和上传都在子进程执行。代码入口见 master_snapshot_manager.cpp。
Copy-on-Write 也有代价:如果快照期间父进程持续修改大量元数据页,父子进程会保留各自版本,临时内存占用可能上升。因此“在线停顿短”不等于“快照没有资源成本”。
编码与发布¶
MetadataSerializer 遍历 1024 个 shard,只写入非空 shard。每个 shard 独立用 MessagePack 编码,再使用 Zstd level 3 压缩;segments 和 task manager 也分别编码成独立 payload。Codec 顺序见 master_snapshot_codec.cpp。
保存顺序是:
只有 payload 和 manifest 全部上传成功,repository 才发布 descriptor 并更新 latest。这样,不完整的中间文件不会被正常选作最新可恢复快照。默认保留最近两份,也为最新快照损坏提供了回退空间。发布逻辑见 master_snapshot_manager.cpp。
Snapshot payload 可以保存到本地文件系统或 S3。本地路径由环境变量指定:
Embedded catalog 下的目录大致为:
/nvme/mooncake-master-snapshots/
└── mooncake_master_snapshot/
└── <cluster_id>/
├── latest.txt
├── 20260829_103000_123/
│ ├── metadata
│ ├── segments
│ ├── task_manager
│ ├── manifest.txt
│ └── descriptor.txt
└── 20260829_102000_087/
└── ...
本地存储初始化见 local_file_snapshot_object_store.cpp。
Master 如何恢复¶
启动时设置 --enable_snapshot_restore=true,Master 会在初始化阶段执行完整恢复:
- 读取
latest,取得最新 Snapshot Descriptor。 - 列出较旧快照,作为 fallback 候选。
- 下载并校验 manifest、metadata、segments 和 task manager。
- 先恢复 segments 和 allocator,因为 MEMORY Replica 的地址必须绑定到已挂载 Segment。
- 解压各 shard,把对象重新
emplace到metadata_shards_。 - 恢复 task manager,重新路由旧格式对象并重建 Group 状态。
- 清理 Lease 已过期或副本状态不完整的对象,重建容量与指标。
- 某个候选损坏时清空中间状态,继续尝试上一份;全部失败则以空状态启动。
入口见 MasterService::RestoreState,恢复后清理见 ApplySnapshotState。
恢复相对直接,是因为它不扫描全部 KV payload,也不从第一条历史操作开始回放,而是从最近 checkpoint 直接重建内存目录。空 shard 不进入 snapshot,压缩又减少了读取量,latest 指针也避免了全目录搜索。
但这里的“快”有边界。当前实现是整份下载、整份解压和整份重建,并不是按需加载;从代码中也没有看到 shard 并行恢复。因此其复杂度仍近似为:
Key 达到很大规模时,下载、Zstd 解压、MessagePack 解析和 unordered_map 重建都会进入恢复关键路径。
怎样选择恢复方案¶
如果目标是单机冷恢复,可以把 snapshot 放到独立的本地 NVMe:
export MOONCAKE_SNAPSHOT_LOCAL_PATH=/nvme/mooncake-master-snapshots
mooncake_master \
--memory_allocator=offset \
--enable_snapshot=true \
--enable_snapshot_restore=true \
--snapshot_object_store_type=local \
--snapshot_catalog_store_type=embedded \
--snapshot_interval_seconds=600 \
--snapshot_child_timeout_seconds=300 \
--snapshot_retention_count=2
其中,snapshot interval 主要决定 RPO,即最多可能回退多少时间;Snapshot 大小、存储介质和 Key 数量主要决定冷恢复 RTO。
| 目标 | 更直接的手段 |
|---|---|
| 缩短单机冷恢复时间 | 本地 NVMe snapshot、控制元数据规模 |
| 防止整机或本地盘损坏 | S3 或远端持久化存储 |
| 降低快照损坏风险 | 至少保留两份 snapshot |
| 降低仅靠周期快照产生的 RPO | HA / OpLog |
| 缩短 Master 切换时间 | 已在内存中跟随状态的 Hot Standby |
当前经典周期 snapshot 还有几个代码边界:生成路径要求 memory_allocator=offset;启用 OpLog 时 Primary 会跳过这套周期生成路径,由 Standby 路径负责 snapshot;DFS 暂时不能与 snapshot/oplog recovery 同时使用;NoF Segment Manager 当前也没有被这套 snapshot 序列化。
恢复目录不等于恢复数据
如果一个 KV Cache 副本只存在于某台机器的 DRAM,而那台机器也已经掉电,snapshot 中保存的地址无法让数据复活。只有底层 MEMORY Segment 仍然有效,或 LOCAL_DISK payload 仍然存在并能重新注册,恢复后的目录才有真实数据可指向。
回到最初的问题¶
现在再问“KV Cache 元数据能否放在 memory 或 SSD”,答案不再只是一个简单的“不能”。
- PagedAttention Block Table 管当前推理实例中的逻辑块到 GPU Block,不属于 Mooncake SSD 索引。
- Master ObjectMetadata 管 Key 到节点、介质和 Replica,是全局路由,在线驻留 DRAM。
- SSD backend index 管 Key 到文件和 offset,是最接近“定位数据页”的元数据;它可以在 SSD 留恢复副本,但在线查询仍使用 DRAM 索引。
- Master snapshot 把控制面目录保存到本地文件或 S3,用于冷恢复;恢复后仍重建为内存 Map。
这也是整条源码链最终收束出的判断:Mooncake 的数据可以在 DRAM 和 SSD 之间分层,定位这些数据的在线索引却不是一个可以任意切换 residency 的统一对象。Snapshot 解决的是控制面失忆,不是把控制面改造成 SSD 查询系统,更不是替 KV payload 做备份。
参考文献¶
- Mooncake 固定源码版本
0518784d。 MasterService::ObjectMetadata。Replica与各类 Descriptor。StorageObjectMetadata。MasterSnapshotManager。MasterSnapshotCodec。MasterService::RestoreState。