团团虾导读:为什么 TE 不直接复用 gRPC?因为 LLM 推理场景下 KV Cache 是 GPU VRAM 中的连续张量,序列化只会引入不必要的 CPU-GPU 搬运开销。这篇从传统 RPC 路径的三个根本问题出发,拆解 TE 的数据面三原语——target_id+target_offset(远端寻址)、Transport 选择、Batch/Task polling(完成监控)。
文章一:TE 接口设计 — 绕开 RPC 序列化,暴露数据面原语
传统 RPC 路径的问题
在分布式训练/推理框架中,数据传输通常走框架级 RPC 通道(gRPC、brpc 等),路径大致是:
数据 → 序列化(protobuf/flatbuffers) → Socket/TCP/RDMA → 反序列化 → 使用
对于 LLM 推理中的 KV cache 传输(单次传输可达几 GB),这套路径有三个根本问题:
序列化开销巨大:KV cache 是 GPU VRAM 中的一大块连续张量数据,不需要序列化,序列化只会引入不必要的 CPU-GPU 数据搬运和内存分配。
Collective 语义不匹配:RPC 调用通常隐含同步语义(请求-响应),而 KV cache 传输天然是异步的、单向的(节点 A 读节点 B 的某块内存)。硬塞进 RPC 语义需要额外的协调开销。
无法精细控制硬件资源:框架级 RPC 把 RDMA QP、MR、CQ 等硬件资源封装在黑盒里,上层无法针对传输模式做优化。
TE 的数据面三原语
Mooncake Transfer Engine 把数据传输抽象成三个明确的”数据面原语”,全部暴露给上层应用直接控制:
1. 远端哪个 Buffer 可访问 → target_id + target_offset(Segment 协议)
2. 用哪个 Transport → Transport 选择(基于 metadata 的 protocol 字段)
3. 如何监控完成 → Batch/Task 级别的 polling(getTransferStatus)
对应的 API 核心接口(mooncake-transfer-engine/include/transport/transport.h):
// 1. 提交传输:指定远端 Segment、偏移量、本地地址、长度
struct TransferRequest {
enum OpCode { READ, WRITE };
OpCode opcode;
void *source; // 本地 buffer 地址
SegmentID target_id; // 远端 Segment 标识
uint64_t target_offset; // 远端 buffer 内偏移
size_t length; // 传输长度
int advise_retry_cnt = 0; // 单请求粒度的重试次数建议
};
// 2. 批量提交到 Batch
Status submitTransfer(BatchID batch_id,
const std::vector<TransferRequest>& entries);
// 3. 轮询完成状态
Status getTransferStatus(BatchID batch_id, size_t task_id,
TransferStatus& status);
这套接口没有序列化参数,没有 protobuf message,没有同步返回。 它只是告诉引擎”从本地地址 A 读(或写入)远端 Segment B 的偏移 C,长度 D”。引擎直接操作注册过的内存区域。
对比:RPC 方式 vs TE 方式
| 维度 | 框架级 RPC | Transfer Engine |
|---|---|---|
| 序列化 | 必须经过 protobuf/序列化 | 零拷贝,直接 DMA |
| 同步语义 | Request-Response | 异步 Submit + Poll |
| 地址模型 | Hostname:Port | SegmentID + Offset |
| 传输选择 | 框架内建,黑盒 | 显式 Transport 安装 |
| 完成通知 | 自动返回 | 显式 polling / Notify |
代码验证
TransferRequest 只有地址和偏移,没有序列化参数:
mooncake-transfer-engine/include/transport/transport.h:58-66 定义了 TransferRequest 结构体,包含 opcode、source 地址、target_id、target_offset、length、advise_retry_cnt 六个字段。没有序列化缓冲区、没有 protobuf message 引用。其中 advise_retry_cnt 为单请求粒度的重试次数建议,被 RdmaTransport::submitTransferTask()(rdma_transport.cpp:507-508)消费。
submitTransfer 的调用链完全不走序列化:
TransferEngineAPI 声明在transfer_engine.h:100,实际转发给TransferEngineImpl的实现位于transfer_engine.cpp:90-93TransferEngineImpl::submitTransfer()(transfer_engine_impl.h:117)转发给MultiTransportMultiTransport::submitTransfer()(multi_transport.cpp:107)按请求的 target_id 查找 Segment 描述中的 protocol 字段,选择对应的 Transport- Transport 将请求拆成 Slice 列表,提交给 RdmaContext 的 WorkerPool
MultiTransport 中的 Transport 选择逻辑:
mooncake-transfer-engine/src/multi_transport.cpp:432-453 中 selectTransport() 方法通过 metadata_->getSegmentDescByID(entry.target_id) 获取目标 Segment 的 protocol 字段,然后从 transport_map_ 查找对应的 Transport 实例。整个选择过程没有序列化、没有 RPC 调用。
上层使用示例(benchmark 代码):
mooncake-transfer-engine/example/transfer_engine_bench.cpp:398-441 展示了标准使用模式:
// 1. 分配 Batch
auto batch_id = engine->allocateBatchID(FLAGS_batch_size);
// 2. 构造批量请求(纯地址 + 偏移)
std::vector<TransferRequest> requests;
for (int i = 0; i < FLAGS_batch_size; ++i) {
TransferRequest entry;
entry.opcode = opcode;
entry.source = (uint8_t*)(addr) + block_size * (i * threads + thread_id);
entry.target_id = segment_id;
entry.target_offset = remote_base + block_size * (i * threads + thread_id);
entry.length = block_size;
requests.emplace_back(entry);
}
// 3. 提交
engine->submitTransfer(batch_id, requests);
// 4. Poll 每个 task 直到完成
for (int task_id = 0; task_id < FLAGS_batch_size; ++task_id) {
TransferStatus status;
while (!completed) {
engine->getTransferStatus(batch_id, task_id, status);
if (status.s == TransferStatusEnum::COMPLETED) completed = true;
}
}
注意:整个过程中,数据传输直接在两个节点的注册内存间进行(RDMA Read/Write),CPU 完全不参与数据搬运。所谓的”远端哪个 Buffer 可访问”由 target_id + target_offset 唯一定位,“用哪个 Transport”由 metadata 中的 protocol 字段自动选择。