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

Mooncake TE 阅读手记-16-路径选择与 Peer NIC Path

团团虾导读:RDMA 传输的路径选择不是在单一位置完成的——TE 分别在 libfabric 请求下发的入口和 Worker Pool 的 submitPostSend 中做了两次 selectDevice。这篇梳理了整个路径选择流程,包括拓扑亲和策略(优先同 NUMA、同 switch)、Peer NIC Path 的构建,以及 Endpoint 缓存复用机制。

阅读版本: Mooncake v0.3.10.post2-104-geaf724ab (commit eaf724ab, 2026-05-20)

Mooncake Transfer Engine 路径选择:两级独立决策与 Peer NIC Path 建立

RDMA 传输中,路径选择是建立连接的基础环节。在 Mooncake Transfer Engine (TE) 中,路径选择不是在单一位置完成的——它分两次独立决策(本地侧和远端侧),分别选出自己一侧的 HCA,然后汇聚成一条唯一的 peer NIC path,最终建立或复用 RDMA Endpoint。

这篇文章深入分析整个路径选择流程,从 libfabric 请求下发到 RDMA 连接建立,涵盖两级的 selectDevice 调用和拓扑亲和策略。


1. 整体流程概览

一个 RDMA 传输请求的路径选择发生在两个调用位置:

submitTransfer (libfabric 入口)
  └─ submitTransferTask
       └─ selectDevice(local_segment_desc, request.source, ...)  ← 本地侧
            └─ 按 source 定位本地 Buffer → 解析 NUMA location → topology.selectDevice
       └─ slice 分发到 WorkerPool
            └─ WorkerPool::submitPostSend
                 └─ selectDevice(peer_segment_desc, slice->rdma.dest_addr, ...)  ← 远端侧
                      └─ 按 dest_addr 定位远端 Buffer → 远端 topology.selectDevice
                 └─ MakeNicPath(server_name, nic_name)  ← 汇聚为 peer NIC path

两边的选择结果共同决定了 (本地 HCA, 远端 NIC) 这个二元组,以此作为 Endpoint 的唯一键。


2. 请求结构:一个请求如何携带两侧信息

// transport.h:58-67
struct TransferRequest {
    enum OpCode { READ, WRITE };
    OpCode opcode;
    void *source;           // 本地虚拟地址
    SegmentID target_id;    // 远端 segment ID
    uint64_t target_offset; // 远端目标偏移
    size_t length;
    int advise_retry_cnt = 0;
};

关键点:


3. 本地侧路径选择:从 source 到本地 HCA

本地侧的选择发生在 RdmaTransport::submitTransferTask 中(rdma_transport.cpp:464-582)。

3.1 获取本地 SegmentDesc

// rdma_transport.cpp:468
auto local_segment_desc = metadata_->getSegmentDescByID(LOCAL_SEGMENT_ID);

LOCAL_SEGMENT_ID 是本地节点在 metadata 中的唯一标识。local_segment_desc 包含了本地所有已注册的 Buffer、设备列表(devices)和 topology 信息。

3.2 第一次 selectDevice —— 确定本地 HCA

// rdma_transport.cpp:483-489
auto request_buffer_id = -1, request_device_id = -1;
if (selectDevice(local_segment_desc.get(), (uint64_t)request.source,
                 request.length, request_buffer_id,
                 request_device_id)) {
    request_buffer_id = -1;
    request_device_id = -1;
}

这里使用 request.source(本地虚拟地址)和 request.length,在本地 SegmentDesc 中找到对应的 Buffer 和 Device。如果请求的整个地址范围落在同一个 Buffer 中(通过第一次 selectDevice 的 buffer_id >= 0 来验证),后续 slice 可以复用这个结果,避免逐 slice 重复查找。

3.3 Slice 切分与逐 Slice 的 find_device 循环

// rdma_transport.cpp:491-569
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;
    // ...

    int buffer_id = -1, device_id = -1, retry_cnt = request.advise_retry_cnt;
    bool found_device = false;
    if (request_buffer_id >= 0 && request_device_id >= 0) {
        found_device = true;
        buffer_id = request_buffer_id;
        device_id = request_device_id;
    }
    while (retry_cnt < kMaxRetryCount && !found_device) {
        if (selectDevice(local_segment_desc.get(),
                         (uint64_t)slice->source_addr, slice->length,
                         buffer_id, device_id, retry_cnt++))
            continue;
        // 验证 device 是否 active
        assert(device_id >= 0);
        auto &context = context_list_[device_id];
        if (!context->active()) continue;
        // 验证 buffer lkey 存在
        assert(local_segment_desc->buffers[buffer_id].lkey.size()
               == context_list_.size());
        found_device = true;
        break;
    }
    // 将 slice 的 source_lkey 设置为 buffer 在该 device 上的 lkey
    slice->rdma.source_lkey =
        local_segment_desc->buffers[buffer_id].lkey[device_id];
    slices_to_post[context].push_back(slice);
}

对于跨 Buffer 的请求,每个 slice 单独调用 selectDevice,并且 retry_count 递增——这会让拓扑轮询到 next preferred/available HCA。retry_count = 0 时随机/轮询选 preferred HCA;retry_count >= 1 时按顺序遍历 preferred + available 列表。

3.4 selectDevice 核心逻辑

// 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 范围内
        if (offset < buffer.addr || length > buffer.length ||
            offset - buffer.addr > buffer.length - length) {
            continue;
        }

        // 解析 NUMA 感知 location
        // 例如 "cpu:0" → 直接使用
        // 例如 "MT_mem_0gb_0~MT_mem_0gb_15" → 解析分段信息,计算子段位置
        std::string location = buffer.name;
        SegmentsLocationInfo seg_info;
        if (parseSegmentsLocation(buffer.name, seg_info)) {
            location = resolveSegmentsLocation(seg_info, buffer.length,
                                               offset - buffer.addr);
        }

        // 调用 topology.selectDevice
        device_id =
            hint.empty()
                ? desc->topology.selectDevice(location, retry_count)
                : desc->topology.selectDevice(location, hint, retry_count);
        if (device_id >= 0) return 0;

        // 回退:通配符 location "*" 匹配任意设备
        device_id = hint.empty()
            ? desc->topology.selectDevice(kWildcardLocation, retry_count)
            : desc->topology.selectDevice(kWildcardLocation, hint, retry_count);
        if (device_id >= 0) return 0;
    }
    return ERR_ADDRESS_NOT_REGISTERED;
}

这个函数的核心三步是:

  1. 定 Buffer:遍历 desc->buffers,检查 offset + length 是否完全落在某个 Buffer 内
  2. 解 Location:从 Buffer 的 name(注册时传入的 location 字符串)中解析出 NUMA 域标识。普通 location 如 cpu:0 直接使用;分段 location 如 MT_mem_0gb_0~MT_mem_0gb_15 则计算当前 offset 对应的子段位置
  3. 选 HCA:调用 desc->topology.selectDevice(location, retry_count) 从 topology matrix 中查出对应 location 的 preferred HCA

3.5 Topology.selectDevice —— 从 location 到设备索引

Topology 在发现阶段(topology.cpp 中的 discoverCpuTopologydiscoverGpuTopology 等)枚举所有 IB 设备并检查它们的 NUMA 亲和性。每个 NUMA 域生成一个 TopologyEntry:

// topology.cpp:293-296
TopologyEntry{
    .name = "cpu:" + std::to_string(node_id),
    .preferred_hca = /* 同 NUMA 域的 HCA */,
    .avail_hca = /* 非同 NUMA 域的 HCA */
}

selectDevice 的查找过程(topology.cpp:574-601):

retry_count == 0:
  - use_round_robin_ ? thread_local_counter++ % preferred.size()
                     : random() % preferred.size()

retry_count >= 1:
  - 按 (retry_count - 1) 遍历 preferred + available 列表
  - index < preferred.size() → preferred[index]
  - else → available[index - preferred.size()]

hint 非空时(即 enable_dest_device_affinity 开启),先通过 hint(本地设备名)在 preferred/available 列表中做名称匹配名字查找,命中则直接返回对应索引;否则回退到标准流程。


4. 远端侧路径选择:从 target_offset 到远端 HCA

本地的 slices 被分发到 context->submitPostSend(即 WorkerPool::submitPostSend),远端侧的选择发生在 worker_pool.cpp:111-148

4.1 获取远端 SegmentDesc

// worker_pool.cpp:111
auto &peer_segment_desc = segment_desc_map[slice->target_id];

segment_desc_map 缓存了已知的远端 SegmentDesc,这些是在连接建立阶段通过 metadata 同步获取的。远端 SegmentDesc 携带了:

4.2 selectDevice —— 使用远端数据

// worker_pool.cpp:112-137
auto hint = globalConfig().enable_dest_device_affinity
                ? context_.deviceName()
                : "";
if (RdmaTransport::selectDevice(peer_segment_desc.get(),
                                slice->rdma.dest_addr, slice->length,
                                hint, buffer_id, device_id)) {
    // 失败则重新拉取远端 SegmentDesc 并重试
    peer_segment_desc = context_.engine().meta()->getSegmentDescByID(
        slice->target_id, true);
    if (!peer_segment_desc) { /* 失败 */ }
    if (RdmaTransport::selectDevice(
            peer_segment_desc.get(), slice->rdma.dest_addr,
            slice->length, hint, buffer_id, device_id)) {
        slice->markFailed();
        continue;
    }
}

关键参数:

和本地侧调用的是同一个 selectDevice 函数,但操作的是不同的 SegmentDesc:本地的 selectDevice 用本地的 buffers + topology,远端的用远端的 buffers + topology。这就是”两级独立决策”的本质。

4.3 远端 HCA 选择的关键影响因素

  1. 远端 Buffer 的 NUMA location:远端内存可能分布在不同的 NUMA 域,不同域对应的 preferred HCA 不同
  2. 远端 topology:远端系统的 NIC-NUMA 拓扑与本地的拓扑完全独立——本地拓扑看不到远端的亲和关系
  3. Dest Device Affinity Hint:如果启用,传入本地 HCA 名称给远端选择逻辑,让远端在有多个可选 HCA 时,优先选择与本地 HCA 更匹配的那张卡(名称匹配)

4.4 获取远端 rkey

// worker_pool.cpp:143-144
slice->rdma.dest_rkey =
    peer_segment_desc->buffers[buffer_id].rkey[device_id];

远端的 rkey 是按 (buffer_id, device_id) 二维索引的——同一块内存注册到同一个 segment 的不同 HCA 上会得到不同的 rkey。选对了远端 device(HCA),才能拿到正确的 rkey 完成 RDMA 操作。


5. 两方向汇聚:建立 Peer NIC Path

两端设备都确定后,在 WorkerPool 中汇聚成 peer NIC path:

// worker_pool.cpp:145-148
auto peer_nic_path =
    MakeNicPath(peer_segment_desc->name,
                peer_segment_desc->devices[device_id].name);
slice->peer_nic_path = peer_nic_path;

MakeNicPath 定义在 common.h:470-473

static inline const std::string MakeNicPath(const std::string &server_name,
                                            const std::string &nic_name) {
    return server_name + NIC_PATH_DELIM + nic_name;
}

其中 NIC_PATH_DELIM@。结果格式为 "192.168.3.76@mlx5_3"——server 名 + @ + NIC 设备名。

5.1 Peer NIC Path 作为 Endpoint 的键

远端侧选择完成后,slices 按 peer_nic_path 分桶:

// worker_pool.cpp:154-161
for (int shard_id = 0; shard_id < kShardCount; ++shard_id) {
    // ...
    slice_queue_[shard_id][slice->peer_nic_path].push_back(slice);
}

传输级别的 (local_context, peer_nic_path) 对唯一标识一个 RDMA Endpoint:

5.2 NicPath 归一化

为了支持跨连接的 endpoint 复用,TE 还在 common.h:475-479 提供了 normalizeNicPath:去除 peer_nic_path 中的端口部分(每次 handshake 随机分配的端口),保留 server@nic 作为稳定的复用键。同一个物理 peer 即使重新连接分配了不同端口,也能复用已建立的 endpoint。


6. 完整路径选择决策表

方向发生位置输入参数查找的数据结构决定的设备输出给下游
本地submitTransferTaskrequest.source + request.lengthlocal_segment_desc.buffers + local_segment_desc.topologycontext_list_[device_id](本地 HCA/RdmaContext)slice->rdma.source_lkey
远端WorkerPool::submitPostSendslice->rdma.dest_addr + slice->lengthpeer_segment_desc.buffers + peer_segment_desc.topologypeer_segment_desc.devices[device_id](远端 NIC)slice->rdma.dest_rkey + peer_nic_path
汇聚WorkerPool 分桶peer_nic_path = server@nic(context, peer_nic_path)建立或复用 RDMA Endpoint

7. 关键数据结构一览

TransferRequest(请求入口)

// transport.h:58-67
struct TransferRequest {
    OpCode opcode;          // READ 或 WRITE
    void *source;           // 本地虚拟地址 → 本地 selectDevice 的输入
    SegmentID target_id;    // 远端 segment ID → 查找远端 SegmentDesc
    uint64_t target_offset; // 远端目标偏移 → 远端 selectDevice 的输入
    size_t length;
    int advise_retry_cnt;
};

SegmentDesc(节点级描述)

// transfer_metadata.h:88-108
struct SegmentDesc {
    std::string name;               // server 名称(如 IP 或 hostname)
    std::string protocol;           // "rdma" / "tcp" / ...
    std::vector<DeviceDesc> devices; // 该节点的 NIC 列表
    Topology topology;              // NUMA location → HCA 的映射
    std::vector<BufferDesc> buffers; // 已注册的内存 Buffer 列表
    // ...
};

DeviceDesc(NIC 描述)

// transfer_metadata.h:45-50
struct DeviceDesc {
    std::string name;    // NIC 设备名,如 "mlx5_0"
    uint16_t lid;        // InfiniBand LID
    std::string gid;     // RoCE GID
    std::string eid;     // for ub
};

BufferDesc(内存 Buffer 描述)

// transfer_metadata.h:52-65
struct BufferDesc {
    std::string name;               // location 字符串,如 "cpu:0"
    uint64_t addr;                  // 注册的虚拟地址
    uint64_t length;                // 长度
    std::vector<uint32_t> lkey;     // 本地 key,按 device_id 索引
    std::vector<uint32_t> rkey;     // 远端 key,按 device_id 索引
};

8. 代码验证

以下是对应的源代码位置和关键逻辑验证:

8.1 本地侧 selectDevice 调用

文件:mooncake-transfer-engine/src/transport/rdma_transport/rdma_transport.cpp

// Line 483-489: 第一次尝试,用 request.source 匹配本地 Buffer
auto request_buffer_id = -1, request_device_id = -1;
if (selectDevice(local_segment_desc.get(), (uint64_t)request.source,
                 request.length, request_buffer_id,
                 request_device_id)) {
    // 失败,后续逐 slice 重试
}

// Line 523-540: 逐 slice 的 find_device 循环
while (retry_cnt < kMaxRetryCount && !found_device) {
    if (selectDevice(local_segment_desc.get(),
                     (uint64_t)slice->source_addr, slice->length,
                     buffer_id, device_id, retry_cnt++))
        continue;
    // 验证 device active, buffer lkey 存在
    auto &context = context_list_[device_id];
    if (!context->active()) continue;
    found_device = true;
}

// Line 559-561: 设置 slice 的 source_lkey
slice->rdma.source_lkey =
    local_segment_desc->buffers[buffer_id].lkey[device_id];

8.2 selectDevice 核心实现

文件:mooncake-transfer-engine/src/transport/rdma_transport/rdma_transport.cpp:692-728

8.3 远端侧 selectDevice 调用

文件:mooncake-transfer-engine/src/transport/rdma_transport/worker_pool.cpp:111-148

auto &peer_segment_desc = segment_desc_map[slice->target_id];
auto hint = globalConfig().enable_dest_device_affinity
                ? context_.deviceName() : "";
RdmaTransport::selectDevice(peer_segment_desc.get(),
                            slice->rdma.dest_addr, slice->length,
                            hint, buffer_id, device_id);

slice->rdma.dest_rkey =
    peer_segment_desc->buffers[buffer_id].rkey[device_id];
auto peer_nic_path = MakeNicPath(peer_segment_desc->name,
                                 peer_segment_desc->devices[device_id].name);

8.4 Topology.selectDevice 实现

文件:mooncake-transfer-engine/src/topology.cpp:555-601

8.5 Topology 发现

文件:mooncake-transfer-engine/src/topology.cpp:303-338

8.6 MakeNicPath

文件:mooncake-transfer-engine/include/common.h:470-473

static inline const std::string MakeNicPath(const std::string &server_name,
                                            const std::string &nic_name) {
    return server_name + NIC_PATH_DELIM + nic_name;
}

8.7 数据结构


9. 总结

Mooncake TE 的路径选择是一个”两边各自决策、最终汇聚”的设计:

  1. 本地侧根据 request.source 在本地的 buffers + topology 中找出最合适的本地 HCA,设置 source_lkey
  2. 远端侧根据 target_offset 在远端的 buffers + topology 中找出最合适的远端 NIC,设置 dest_rkey
  3. 汇聚(本地 context, 远端 server@nic) 形成唯一的 peer NIC path,作为建立或复用 RDMA Endpoint 的键

这种设计使得:即使本地有 3 个 HCA、远端有 4 个 HCA,TE 也能为每个 (本地 HCA, 远端 HCA) 组合建立独立的 RDMA 连接,不会跨 NUMA 走”冤枉路”。同时,拓扑信息分布在各自的 SegmentDesc 中,两侧的决策完全解耦——每个节点只需要维护自己的 NIC-NUMA 映射,无需知道对端的拓扑细节。


修订说明


Share this post on:

Previous Post
Mooncake TE 阅读手记-17-元数据管理
Next Post
Mooncake TE 阅读手记-15-RDMA QP/CQ 与操作模式