团团虾导读:这篇回答 Mooncake 阅读中最容易踩的坑——target_offset 到底是相对偏移还是远端虚拟地址?通过五步代码追踪给出了明确结论:target_offset 就是远端真实虚拟地址,上层仍需要从 SegmentDesc 获取。同时梳理了 TE 到底帮上层隐藏了什么(rkey/lkey/QP 管理),以及 cpu:0 作为 NUMA location 标签的作用。
第三篇:RDMA 寻址的深度解析
RDMA 基本原理:rkey + addr 缺一不可
对于 RDMA RC(Reliable Connection),要执行远端内存读写,rkey + 远端虚拟地址是最小必要信息集合:
- 远端虚拟地址 (addr) — 目标进程中的虚拟地址(对应
ibv_sge.addr或ibv_send_wr.wr.rdma.remote_addr) - 远端内存密钥 (rkey) — 内存区域访问权限令牌(对应
ibv_send_wr.wr.rdma.rkey)
缺少任何一个,RDMA 操作都无法完成。rkey 由远端通过 ibv_reg_mr 获得(与 lkey 同时分配),然后通过等带外机制(etcd)传递给发起方。
在 Mooncake 中,这个传递路径是:
target 节点:
registerLocalMemory(buf, len, "cpu:0")
-> ibv_reg_mr() 产生 lkey + rkey
-> BufferDesc {addr, length, lkey[], rkey[], name} 写入 etcd
initiator 节点:
openSegment(target_name)
-> etcd 拉取 SegmentDesc {buffers: [{addr, length, rkey[], lkey[], name}]}
-> 本地缓存 segment_id -> SegmentDesc
target_offset 的真实含义
理解 target_offset 在 Transfer Engine 中到底代表什么,是最大的一道坎。结论:target_offset 就是远端的真实虚拟地址。
关键代码路径:
第一步:Initiator 侧设置 target_offset(minimal_example.cpp:108-117)
// 文件: mooncake-transfer-engine/example/minimal_example.cpp:105-117
auto seg_desc = engine->getMetadata()->getSegmentDescByID(segment_id);
uint64_t remote_addr = (uint64_t)seg_desc->buffers[0].addr; // 远端的真实虚拟地址
TransferRequest req;
req.opcode = TransferRequest::WRITE;
req.source = buffer;
req.target_id = segment_id;
req.target_offset = remote_addr; // target_offset = 远端虚拟地址
req.length = kBlockSize;
第二步:RDMA transport 将 target_offset 用作 dest_addr(rdma_transport.cpp:506)
// 文件: mooncake-transfer-engine/src/transport/rdma_transport/rdma_transport.cpp:491-506
for (uint64_t offset = 0; offset < request.length;
offset += kBlockSize) {
Slice *slice = getSliceCache().allocate();
// ...
slice->source_addr = (char *)request.source + offset;
slice->rdma.dest_addr = request.target_offset + offset; // 远端地址 = target_offset + 本地偏移
slice->target_id = request.target_id;
// ...
}
第三步:Worker pool 中解析 rkey,含元数据刷新重试(worker_pool.cpp:98-151)
// 文件: mooncake-transfer-engine/src/transport/rdma_transport/worker_pool.cpp:111-148
auto &peer_segment_desc = segment_desc_map[slice->target_id];
int buffer_id, device_id;
auto hint = globalConfig().enable_dest_device_affinity
? context_.deviceName()
: "";
// selectDevice 用 dest_addr 查找目标缓冲区(比较 addr <= dest_addr <= addr+length)
if (RdmaTransport::selectDevice(peer_segment_desc.get(),
slice->rdma.dest_addr, slice->length,
hint, buffer_id, device_id)) {
// 查找失败则从 etcd 强制刷新 metadata 再重试
peer_segment_desc = context_.engine().meta()->getSegmentDescByID(
slice->target_id, true);
if (!peer_segment_desc) {
LOG(ERROR) << "Cannot reload target segment #"
<< slice->target_id;
slice->markFailed();
failed_target_ids[slice->target_id] = getCurrentTimeInNano();
continue;
}
// 刷新后用新数据再次尝试 selectDevice
if (RdmaTransport::selectDevice(
peer_segment_desc.get(), slice->rdma.dest_addr,
slice->length, hint, buffer_id, device_id)) {
slice->markFailed();
context_.engine().meta()->dumpMetadataContent(
peer_segment_desc->name, slice->rdma.dest_addr,
slice->length);
continue;
}
}
// 从远端 BufferDesc 中取出对应设备索引的 rkey
slice->rdma.dest_rkey =
peer_segment_desc->buffers[buffer_id].rkey[device_id];
第四步:selectDevice 用地址直接比较(rdma_transport.cpp:692-728)
// 文件: mooncake-transfer-engine/src/transport/rdma_transport/rdma_transport.cpp:692-728
int RdmaTransport::selectDevice(SegmentDesc *desc, uint64_t offset,
size_t length, std::string_view hint,
int &buffer_id, int &device_id,
int retry_count) {
if (desc == nullptr) return ERR_ADDRESS_NOT_REGISTERED;
const auto &buffers = desc->buffers;
for (buffer_id = 0; buffer_id < static_cast<int>(buffers.size());
++buffer_id) {
const auto &buffer = buffers[buffer_id];
// offset 被当作绝对地址,与 buffer.addr 直接比较!
if (offset < buffer.addr || length > buffer.length ||
offset - buffer.addr > buffer.length - length) {
continue;
}
// 根据 buffer.name (如 "cpu:0") 选择设备
// ...
}
return ERR_ADDRESS_NOT_REGISTERED;
}
第五步:最终 RDMA 操作(rdma_endpoint.cpp:620-631)
// 文件: mooncake-transfer-engine/src/transport/rdma_transport/rdma_endpoint.cpp:620-631
auto &wr = wr_list[i];
memset(&wr, 0, sizeof(ibv_send_wr));
wr.wr_id = (uint64_t)slice;
wr.opcode = slice->opcode == Transport::TransferRequest::READ
? IBV_WR_RDMA_READ
: IBV_WR_RDMA_WRITE;
wr.num_sge = 1;
wr.sg_list = &sge;
wr.send_flags = IBV_SEND_SIGNALED;
wr.wr.rdma.remote_addr = slice->rdma.dest_addr; // 远端的真实虚拟地址
wr.wr.rdma.rkey = slice->rdma.dest_rkey; // 远端 rkey
// ibv_post_send(qp, wr_list, &bad_wr);
架构总览图
下面这张 ASCII 图总结了从用户层 API 到 RDMA 硬件的完整数据流:
用户层 API Transfer Engine RDMA Transport
==================== ==================== ====================
TransferRequest {
target_id: 3 --> 通过 etcd 查找
target_offset: 0xA000 --> 远端虚拟地址 -----------> slice->rdma.dest_addr
source: local_buf --> 本地 lkey 解析 --------> slice->rdma.source_lkey
length: 65536 --> 按 kBlockSize 切片
opcode: WRITE
}
resolveRkey():
selectDevice(dest_addr)
-> buffers[buf_id].rkey[dev]
||
slice->rdma.dest_rkey
||
ibv_post_send(qp, wr)
wr.wr.rdma.remote_addr = dest_addr
wr.wr.rdma.rkey = dest_rkey
TE 到底帮上层隐藏了什么
回到用户的问题六:
“TE 把目标写成 target_id + target_offset,让上层不需要知道远端真实虚拟地址;远端地址由 SegmentDesc 和 BufferDesc 解析。RDMA 等后端再使用 rkey 和目标地址提交操作”
这个描述正确的是:TE 确实隐藏了 rkey 的细节。上层用户完全不需要操作 rkey。
但这个描述对 target_offset 的理解需要修正:target_offset 并不是相对偏移,上层仍然需要从 SegmentDesc 中获取远端真实虚拟地址。完整流程是:
| 被 TE 隐藏的 | 仍需要用户操作的 |
|---|---|
| rkey(远端内存密钥) | 从 SegmentDesc 获取远端 buffer.addr |
| lkey(本地内存密钥) | 调用 registerLocalMemory 注册内存 |
| Device selection | 设置 cpu:0 等 location 标签 |
| QP 管理、WR posting | 构造 TransferRequest |
| Slice 分块、重试 | 处理 TransferStatus |
| RDMA 握手(QP 建立) | 调用 init + installTransport |
| Metadata 缓存、刷新 | 调用 openSegment |
cpu:0 的作用
"cpu:0" 是一个 location 标签,用于指定内存在 NUMA 拓扑中的位置。它影响:
- 内存分配:
numa_alloc_onnode(size, 0)在 NUMA node 0 上分配内存 - 设备选择:
selectDevice调用时,buffer.name(等于"cpu:0")作为参数传给desc->topology.selectDevice("cpu:0", ...),帮助选择与 NUMA node 0 物理相连的 RDMA 设备(HCA),以最小化 PCIe 延迟
如果 buffer 跨越多个 NUMA 节点,可以使用 segments: 格式(来自 memory_location.h):
格式: "segments:<page_size>:<numa0>,<numa1>,..."
示例: "segments:4096:1,3,5,7"
代码验证
以下是对关键断言的源代码逐项验证:
验证 1:target_offset 是远端虚拟地址,不是相对偏移
文件: mooncake-transfer-engine/src/transport/rdma_transport/rdma_transport.cpp, line 506
slice->rdma.dest_addr = request.target_offset + offset;
这里的 offset 是 for 循环内按 kBlockSize 递增的本地切片偏移(0, kBlockSize, 2*kBlockSize, …),不是与 buffer 基准地址的相对偏移。dest_addr 就是最终写入 ibv_send_wr.wr.rdma.remote_addr 的值。
在 selectDevice 中(同文件 line 703),offset 参数就是 slice->rdma.dest_addr,被直接与 buffer.addr 比较:
if (offset < buffer.addr || ...)
这只能意味着 target_offset 必须等于远端 buffer 的某地址(通常是 buffers[0].addr)。
文件: mooncake-transfer-engine/example/minimal_example.cpp, line 108
uint64_t remote_addr = (uint64_t)seg_desc->buffers[0].addr;
确认:用户需要从 metadata 获取远端 buffer 地址填入 target_offset。
验证 2:rkey 来自远端 SegmentDesc 的 BufferDesc
文件: mooncake-transfer-engine/src/transport/rdma_transport/rdma_transport.cpp, lines 280-281
buffer_desc.lkey.push_back(context->lkey(addr));
buffer_desc.rkey.push_back(context->rkey(addr));
rkey 在 registerLocalMemory 时收集并写入 metadata。
文件: mooncake-transfer-engine/src/transport/rdma_transport/worker_pool.cpp, lines 143-144
slice->rdma.dest_rkey =
peer_segment_desc->buffers[buffer_id].rkey[device_id];
rkey 在 worker pool 中从缓存或刷新后的 SegmentDesc 提取。
文件: mooncake-transfer-engine/src/transport/rdma_transport/rdma_endpoint.cpp, lines 630-631
wr.wr.rdma.remote_addr = slice->rdma.dest_addr;
wr.wr.rdma.rkey = slice->rdma.dest_rkey;
最终写入 ibv_send_wr,由 ibv_post_send 提交到 RDMA QP。
验证 3:registerLocalMemory 同时产生 lkey 和 rkey
文件: mooncake-transfer-engine/src/transport/rdma_transport/rdma_transport.cpp, lines 189-192
const int kBaseAccessRights = IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_REMOTE_READ;
内存注册时同时指定了本地写、远端读、远端写权限。ibv_reg_mr 返回的 Memory Region 包含 lkey(用于本地 SGE)和 rkey(用于远端 WR),两者被分别存入 buffer_desc.lkey[] 和 buffer_desc.rkey[]。
验证 4:openSegment 本质上是元数据查询
文件: mooncake-transfer-engine/src/transfer_engine_impl.cpp, lines 453-476
Transport::SegmentHandle TransferEngineImpl::openSegment(
const std::string& segment_name) {
// ...
SegmentID sid = metadata_->getSegmentID(trimmed_segment_name);
// ...
return sid;
}
openSegment 只调用 getSegmentID,不建立任何网络连接。
文件: mooncake-transfer-engine/src/transfer_metadata.cpp, lines 1004-1024
getSegmentID 先在本地缓存查找(segment_name_to_id_map_),未命中则调用 getSegmentDesc(segment_name) 从 etcd 拉取完整 SegmentDesc,再分配数字 ID 并缓存。
验证 5:BufferDesc 写入 metadata 的完整链路
registerLocalMemory(addr, len, "cpu:0")
-> TransferEngineImpl::registerLocalMemory [transfer_engine_impl.cpp:511]
-> for each transport: transport->registerLocalMemory [line 525-528]
-> RdmaTransport::registerLocalMemoryInternal [rdma_transport.cpp:183]
-> ibv_reg_mr() for each RNIC context [line 235/253]
-> buffer_desc.lkey/rkey.push_back(...) [line 280-281]
-> buffer_desc.addr = (uint64_t)addr [line 296]
-> buffer_desc.length = length [line 297]
-> buffer_desc.name = "cpu:0" [line 293]
-> metadata_->addLocalMemoryBuffer(...) [line 301]
-> segment_desc->buffers.push_back(buffer_desc) [transfer_metadata.cpp:1067]
-> updateLocalSegmentDesc() [line 1069]
-> encodeSegmentDesc(desc, segmentJSON) [transfer_metadata.cpp:466]
-> storage_plugin_->set(key, segmentJSON) [line 471]
验证 6:rdma_endpoint.cpp 中的 ibv_post_send 调用
文件: mooncake-transfer-engine/src/transport/rdma_transport/rdma_endpoint.cpp, lines 623-640
wr.opcode = slice->opcode == Transport::TransferRequest::READ
? IBV_WR_RDMA_READ
: IBV_WR_RDMA_WRITE;
wr.num_sge = 1;
wr.sg_list = &sge;
wr.send_flags = IBV_SEND_SIGNALED;
// ...
wr.wr.rdma.remote_addr = slice->rdma.dest_addr; // 远端地址
wr.wr.rdma.rkey = slice->rdma.dest_rkey; // 远端 rkey
// ...
int rc = ibv_post_send(qp_list_[qp_index], wr_list.data(), &bad_wr);
这是 RDMA 操作的最终发起点,确认了 rkey + dest_addr 是 libibverbs 必需的两个参数。
总结:
| 问题 | 答案 |
|---|---|
| target_offset 是什么 | 远端真实虚拟地址,通常从 SegmentDesc.buffers[].addr 获取 |
| target_name 怎么填 | 远端节点的 local_server_name (ip:port 格式) |
| Segment 是什么 | 节点元数据容器,包含 registered buffers 列表、RDMA 设备信息、NUMA 拓扑 |
| openSegment 做什么 | 从 etcd 拉取远端 SegmentDesc,分配本地数字 ID 并缓存 |
| rkey 从哪来 | 远端 registerLocalMemory 时写入 etcd 的 BufferDesc.rkey[] |
| initiator 为什么需要 registerLocalMemory | 获取自己 buffer 的 lkey,RDMA 提交时需要 |
| cpu:0 的作用 | NUMA location 标签,用于选择最优 RDMA 设备和内存分配 |