Mooncake Transfer Engine:内存段、注册内存和批量传输

从内存段、内存注册、传输请求和批次编号出发,建立 Mooncake Transfer Engine 的数据搬运模型。

Code walkthroughsMooncake and HiCache internalsLLM servingKV cacheMooncakeTransfer Engine

Mooncake Transfer Engine 解决的问题很具体:给定本地缓冲区、远端 Segment、偏移量和长度,如何可靠地写入或读回一批数据。

它不理解对象键,不判断副本是否为 COMPLETE,也不知道这段数据是不是 KV 缓存。这些都是 Store 或上层框架的职责。

核心入口:

text
mooncake-transfer-engine/include/transfer_engine.h
mooncake-transfer-engine/include/transport/transport.h
mooncake-transfer-engine/src/transfer_engine_impl.cpp
mooncake-transfer-engine/src/multi_transport.cpp

layer view

Transfer Engine 只处理远端可寻址内存上的字节传输

Store 把对象和副本信息转换成 Segment、偏移量与长度;Transfer Engine 再把这些字段交给具体传输后端。

公开 API

调用者看到的统一入口

openSegmentregisterLocalMemoryallocateBatchIDsubmitTransfer

寻址模型

远端位置不是裸指针

SegmentHandletarget_idtarget_offsetlength

本地内存

本地缓冲区必须能被传输层使用

地址位置固定内存 / MR可否远端访问

批量执行

一批请求统一提交和轮询

BatchIDTransferRequest[]TransferStatus

TransferEngine 提供哪些接口

Transfer Engine 的公开接口可以分成几类:

text
init / freeEngine
installTransport / uninstallTransport / getTransport
openSegment / closeSegment / removeLocalSegment
registerLocalMemory / unregisterLocalMemory
allocateBatchID / freeBatchID
submitTransfer
getTransferStatus / getBatchTransferStatus

这些 API 说明它不是 KV 存储,而是负责内存注册、Segment 发现和批量传输任务。

Segment 表示远端可寻址空间

传输任务不能只给出一个“远端指针”。跨进程或跨机器时,本地进程不能直接使用远端虚拟地址。Transfer Engine 会先通过 openSegment(segment_name) 得到 SegmentHandle

Store 侧的 Segment 元数据会告诉 Client:

text
segment id
segment name
base
size
te_endpoint
protocol

Transfer Engine 侧拿到 Segment 后,传输任务才能表达:

text
target_id
target_offset
length

也就是说,Store 的副本描述最终会转换为 Transfer Engine 所需的 Segment、偏移量和长度。

registerLocalMemory 是传输数据的前置条件

许多传输后端不能直接使用任意一段由 malloc 分配的内存做高速传输。RDMA、GPU P2P、NVLink 和 EFA 等路径通常需要固定内存页、注册内存区域、生成访问键或建立映射。

因此 Transfer Engine 暴露:

text
registerLocalMemory(addr, length, location, remote_accessible, update_metadata)
unregisterLocalMemory(addr)
registerLocalMemoryBatch(...)

这一步的意义是告诉 Transfer Engine:这段本地缓冲区可以参与传输。

SGLang 接入 Mooncake Store 时,Mooncake 后端会注册主机 KV 池。之后 batch_get_intobatch_put_from 才能把主机 KV 缓存页指针直接交给 Store 和 Transfer Engine,而不需要把数据复制成 Python 字节串。

TransferRequest 描述一次基本传输

Transfer Engine 的核心请求可以简化为:

text
TransferRequest
  opcode: READ | WRITE
  source: 本地缓冲区地址
  target_id: 远端 Segment ID
  target_offset: 远端 Segment 内的偏移量
  length: 字节数

READ 和 WRITE 的区别在于数据方向:

text
READ:
  远端 Segment -> 本地 source 缓冲区

WRITE:
  本地 source 缓冲区 -> 远端 Segment

call path

同一个 TransferRequest,方向由 opcode 决定

`source` 在 Transfer Engine 中表示本地缓冲区地址。READ 时它是目的地;WRITE 时它是来源。

本地缓冲区TransferRequestTransfer Engine远端 Segment
  1. 1

    本地缓冲区

    TransferRequest

    WRITE source=addr

    本地切片或主机 KV 页作为数据来源

  2. 2

    TransferRequest

    远端 Segment

    target_id + target_offset + length

    把数据写到副本对应的位置

  3. 3

    远端 Segment

    TransferRequest

    READ target_id + target_offset

    从远端 Segment 读取数据

  4. 4

    Transfer Engine

    本地缓冲区

    填充 source=addr

    READ 的结果写回本地缓冲区

字段名 source 需要按 Transfer Engine 的语义理解:它是本地缓冲区地址,具体方向由操作码决定。

BatchID 与批量任务

Transfer Engine 不鼓励逐个同步传输小对象,而是使用 BatchID 表达一组传输请求:

text
BatchID id = allocateBatchID(batch_size)
submitTransfer(id, requests)
getBatchTransferStatus(id, status)
freeBatchID(id)

这对 KV 缓存很重要。一个前缀可能有大量缓存页,每个缓存页又可能拆成 K/V 对象。如果每个对象都独立提交,提交和轮询开销会非常高。

BatchTransfer 的目标是把许多小传输组织成一批,让传输层统一调度和查询状态。

TransferStatus 只表示传输状态

Transfer Engine 的状态只描述数据传输:

text
WAITING
PENDING
COMPLETED
TIMEOUT
FAILED
CANCELED

这些状态只说明传输任务是否完成,不说明 Store 对象是否可读。

例如在 Put 路径中,WRITE 传输成功以后,Store 仍然需要通过 PutEnd 把副本提交为 COMPLETE。反过来,在 Get 路径中,即使 Master 返回了 COMPLETE 副本,只要 READ 传输失败,上层仍然不能使用数据。

因此要区分:

text
TransferStatus:
  数据是否传输完成

ReplicaStatus:
  对象副本是否可读

Transfer Engine 在 Store Put/Get 中的位置

Put 时:

text
Store 副本
  -> TransferSubmitter
  -> TransferRequest(WRITE)
  -> TransferEngine.submitTransfer
  -> 传输后端

Get 时:

text
Store 副本
  -> TransferSubmitter
  -> TransferRequest(READ)
  -> TransferEngine.submitTransfer
  -> 填充本地切片

Store 负责对象、副本和租约;Transfer Engine 负责数据搬运。这个边界不应该混淆。

为什么这层要独立出来

如果 Store 自己直接实现 RDMA/TCP/NVLink/EFA,每增加一种后端都会影响对象语义层。

Transfer Engine 独立出来以后,Store 只需要表达:

text
把切片写入副本
从副本读入切片

底层到底采用哪种传输方式,由 Transfer Engine 和 MultiTransport 处理。

这让 Mooncake 能同时支持多种部署形态:本地开发可以使用 TCP,生产环境可能使用 RDMA 或 EFA,同机多卡可能使用 NVLink,异构硬件还可以接入其他后端。

和 Store 语义保持距离

Transfer Engine 的关键边界是:它只知道传输任务,不知道对象生命周期。TransferStatus.COMPLETED 表示字节传输完成;ReplicaStatus.COMPLETE 表示 Store 认为这个对象副本可以读取。写入路径中,前者成功后还需要 PutEnd;读取路径中,后者存在后还需要 READ 成功。

这层独立性是 Mooncake 能扩展多种传输方式的原因。Store 不需要知道 RDMA 内存区域、NVLink IPC 或 TCP 连接怎样管理;Transfer Engine 也不需要知道对象键、租约,以及 K/V 对象是否同属于一个逻辑页。两边只通过 Segment、偏移、长度和本地缓冲区地址交接。

读源码时如果觉得概念混在了一起,可以先问一句:眼前的状态是在描述“数据是否已经传输完成”,还是在描述“对象副本是否对读取方可见”。这两个问题分属不同层次。