Mooncake Store 读取路径:GetReplicaList、租约和本地热缓存

沿着 GetReplicaList、副本选择和本地热缓存,梳理一次 Store 读取如何从元数据查询进入数据传输。

Code walkthroughsMooncake and HiCache internalsLLM servingKV cacheMooncakeDistributed systems

Mooncake Store 的 Get 不是“向 Master 要一个值”。Master 只返回可读副本和租约,数据由 Client 自己搬运。

核心链路:

text
Client::Get / Client::BatchGet
  -> Query / GetReplicaList
  -> Master 返回 COMPLETE 副本和租约
  -> Client 选择优先副本
  -> 本地热缓存或 TransferSubmitter
  -> 数据写入调用方切片

call path

Mooncake Store Get:Master 返回可读位置,Client 自己搬数据

Get 的数据不经过 Master。Master 给出 COMPLETE 副本和租约;真正的数据由本地热缓存、内存复制或 Transfer Engine 写入调用方切片。

调用方 / HiCacheStore ClientMaster本地热缓存Transfer Engine调用方切片
  1. 1

    调用方 / HiCache

    Store Client

    Get / BatchGet(键、目标切片)

    目标缓冲区通常是主机 KV 缓存页指针

  2. 2

    Store Client

    Master

    Query / GetReplicaList

    查询对象元数据和可读副本

  3. 3

    Master

    Store Client

    COMPLETE 副本 + 租约

    元数据有时间边界,不能无限复用

  4. 4

    Store Client

    本地热缓存

    尝试读取本地热缓存

    命中则少一次远端读取

  5. 5

    Store Client

    Transfer Engine

    READ Segment

    未命中本地缓存时,从副本所在的 Segment 读取

  6. 6

    Transfer Engine

    调用方切片

    填充本地缓冲区

    Mooncake 写到上层传入的目标地址

Get 先查询元数据

Get 的第一步是向 Master 查询对象元数据。Master 需要判断:

  • 对象键是否存在
  • 是否有处于 COMPLETE 状态的副本
  • 副本所在的 Segment 是否仍然可用
  • 是否可以发放租约
  • 租户和分组语义是否影响本次查询

返回给 Client 的不是对象数据,而是 QueryResult 一类的元数据:

text
副本描述
租约有效期
对象元数据

Client 拿到元数据以后,才进入数据面。

为什么只读取 COMPLETE 状态的副本

Put 路径中的副本会经历 PROCESSING 状态。Get 不能读取这种副本,因为数据可能还没有写完。

正确的读路径应该只把 COMPLETE 副本当作候选:

text
PROCESSING:
  写入中,不可读

COMPLETE:
  PutEnd 已提交,可读

REMOVED / FAILED:
  不可读

这是 Store 保证读取正确性的底线。如果读到了 PROCESSING 副本,就可能拿到只写了一半的数据。

租约限制元数据的有效期

Client 拿到副本列表以后,不能无限期复用,因为 Master 侧的状态可能变化:

  • Segment 被卸载
  • 副本被清理
  • 对象被更新或删除
  • Client 与 Master 看到的状态发生变化

租约为这次元数据使用划定了时间边界。Client 应该在租约有效期内使用副本描述符,过期后重新查询。

因此 Get 的正确性不是“元数据查到一次就永久有效”,而是:

text
元数据查询成功
  + 副本状态为 COMPLETE
  + 租约有效
  + 传输成功

首选副本与数据本地性

Client 拿到多个副本后,需要从中选择一个。选择时会考虑:

  • 是否在同一节点
  • 是否能走本地 memcpy
  • 是否已经进入本地热缓存
  • Segment 的协议是否适合当前 Client
  • 是否配置了优先选择同节点副本

优先选择本地副本可以减少网络传输。对于 KV 缓存这类大对象,本地性带来的差异可能远大于元数据查询成本。

本地热缓存只负责加速读取,不代表全局状态

本地热缓存保存常用对象的数据,可以减少重复远端读取,但不能代替 Master 元数据。

更准确地说:

text
Master 元数据:
  决定哪些副本可读

本地热缓存:
  加速已经判定可读的数据访问

本地热缓存也要借助写入令牌或版本号,防止异步 Put 把过期内容写回缓存。它只是本地优化层,不是分布式一致性的依据。

数据面读取如何执行

如果最终选择的是远端内存副本,Client 会把副本描述转换成 Transfer Engine READ 请求:

text
TransferRequest
  opcode = READ
  source = 本地目标地址
  target_id = 远端 Segment ID
  target_offset = 副本偏移量
  length = 切片大小

这里的 source 字段在 Transfer Engine 看来是本地缓冲区地址。READ 表示从远端 Segment 读取到本地地址。

如果是本地副本,可能直接复制内存;如果是文件或卸载副本,则交给对应后端处理。

BatchGet 与 HiCache

HiCache 从 Mooncake 预取时,会批量读取许多缓存页对象。BatchGet 的价值和 BatchPut 类似:减少单个对象的元数据与传输开销。

Mooncake 的 Python 零拷贝 API 最后会暴露为:

text
batch_get_into(keys, ptrs, sizes)
batch_get_into_multi_buffers(keys, ptrs, sizes)

SGLang 后端把主机内存池缓存页的目标指针传给 Mooncake。Mooncake Store 把对象数据直接写入这些主机缓冲区,随后 HiCache 再把主机缓存页回载到 GPU。

读取失败如何影响上层

Get 失败可以发生在几层:

  • Master 查不到对象键
  • 没有 COMPLETE 副本
  • 租约已过期
  • 选中的副本所对应的 Segment 不可用
  • Transfer Engine READ 失败
  • 批次中某个 K/V 对象读取失败

对于 SGLang HiCache,这些对象读取结果最终要按缓存页汇总。如果某个缓存页的 K 对象成功而 V 对象失败,这一页就不能算命中。

因此,后端需要按缓存页汇总对象状态,再计算连续命中的页数。

从 HiCache 视角看读取

HiCache 读取 Mooncake 时,目标不是把对象放进 Python,而是把远端 KV 缓存页直接填回主机 KV 池。batch_get_into 中的指针指向主机内存池缓存页;Mooncake Store 负责把远端副本的字节写到这些地址;之后 HiCache 还要执行从主机内存到 GPU 的回载。

因此,一次远端命中至少跨过三道边界:Master 元数据必须返回可读副本,数据面必须把对象读回主机缓冲区,HiCache 必须把主机缓存页提交回 GPU KV 池。前两步成功只能说明 Mooncake Store 工作正常,不能自动说明注意力计算已经能够复用这段 KV。

对象读取结果和缓存页命中结果也要分清。Mooncake 只能告诉你某个对象读取成功;SGLang 还要汇总 K/V 对象、按注意力头拆分的对象和辅助内存池对象,判断整个缓存页是否读取成功。只要这组对象中有一个失败,这一页就应该按未命中处理。