跳转至

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。相关路径见 MetadataAccessorROGetReplicaList

对于 MEMORY Replica,MemoryDescriptor 最终携带:

uint64_t size_;
uintptr_t buffer_address_;
std::string protocol_;
std::string transport_endpoint_;

所以查询结果已经能够表达“通过哪个传输端点,从哪个远端内存地址读取多少字节”。字段定义见 allocator.h

SSD backend 保存本地精确位置

LocalDiskDescriptor 不包含文件路径和 offset,只保存:

UUID client_id;
uint64_t object_size;
std::string transport_endpoint;

它回答“哪个 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.hstorage_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:

  1. metadata:对象目录、Replica、Lease、checksum、data type、hard pin、group、discarded replicas 和下一个 Replica ID。
  2. segments:内存 Segment、allocator 与 Local SSD Manager 的持久化状态。
  3. 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

保存顺序是:

metadata
  → segments
  → task_manager
  → manifest
  → descriptor
  → latest
  → retention cleanup

只有 payload 和 manifest 全部上传成功,repository 才发布 descriptor 并更新 latest。这样,不完整的中间文件不会被正常选作最新可恢复快照。默认保留最近两份,也为最新快照损坏提供了回退空间。发布逻辑见 master_snapshot_manager.cpp

Snapshot payload 可以保存到本地文件系统或 S3。本地路径由环境变量指定:

export MOONCAKE_SNAPSHOT_LOCAL_PATH=/nvme/mooncake-master-snapshots

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 会在初始化阶段执行完整恢复:

  1. 读取 latest,取得最新 Snapshot Descriptor。
  2. 列出较旧快照,作为 fallback 候选。
  3. 下载并校验 manifest、metadata、segments 和 task manager。
  4. 先恢复 segments 和 allocator,因为 MEMORY Replica 的地址必须绑定到已挂载 Segment。
  5. 解压各 shard,把对象重新 emplacemetadata_shards_
  6. 恢复 task manager,重新路由旧格式对象并重建 Group 状态。
  7. 清理 Lease 已过期或副本状态不完整的对象,重建容量与指标。
  8. 某个候选损坏时清空中间状态,继续尝试上一份;全部失败则以空状态启动。

入口见 MasterService::RestoreState,恢复后清理见 ApplySnapshotState

恢复相对直接,是因为它不扫描全部 KV payload,也不从第一条历史操作开始回放,而是从最近 checkpoint 直接重建内存目录。空 shard 不进入 snapshot,压缩又减少了读取量,latest 指针也避免了全目录搜索。

但这里的“快”有边界。当前实现是整份下载、整份解压和整份重建,并不是按需加载;从代码中也没有看到 shard 并行恢复。因此其复杂度仍近似为:

\[ T_{restore}=O(N_{keys}+N_{replicas}+S_{snapshot}) \]

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 做备份。

参考文献

  1. Mooncake 固定源码版本 0518784d
  2. MasterService::ObjectMetadata
  3. Replica 与各类 Descriptor
  4. StorageObjectMetadata
  5. MasterSnapshotManager
  6. MasterSnapshotCodec
  7. MasterService::RestoreState

评论