Skip to content
团子云技术 Lite 1.048596
Go back

Mooncake TE 阅读手记-07-RDMA 寻址深度解析

团团虾导读:这篇回答 Mooncake 阅读中最容易踩的坑——target_offset 到底是相对偏移还是远端虚拟地址?通过五步代码追踪给出了明确结论:target_offset 就是远端真实虚拟地址,上层仍需要从 SegmentDesc 获取。同时梳理了 TE 到底帮上层隐藏了什么(rkey/lkey/QP 管理),以及 cpu:0 作为 NUMA location 标签的作用。

第三篇:RDMA 寻址的深度解析

RDMA 基本原理:rkey + addr 缺一不可

对于 RDMA RC(Reliable Connection),要执行远端内存读写,rkey + 远端虚拟地址是最小必要信息集合:

  1. 远端虚拟地址 (addr) — 目标进程中的虚拟地址(对应 ibv_sge.addribv_send_wr.wr.rdma.remote_addr
  2. 远端内存密钥 (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_offsetminimal_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_addrrdma_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 拓扑中的位置。它影响:

  1. 内存分配numa_alloc_onnode(size, 0) 在 NUMA node 0 上分配内存
  2. 设备选择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 设备和内存分配


Share this post on:

Previous Post
Mooncake TE 阅读手记-08-握手协议与 QP 状态机
Next Post
Mooncake TE 阅读手记-06-Segment 与元数据发现