团团虾导读: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;
};
关键点:
source是本地虚拟地址,用于本地侧路径选择——找到内存所在的 NUMA 域,选出最接近该域的 HCA。target_id和target_offset用于远端侧路径选择——通过target_id查找远端 SegmentDesc,用target_offset找到远端 Buffer,再根据远端 topology 选出路上的 HCA。- 无论是 READ(本地是 sink)还是 WRITE(本地是 source),
source总是指向本地地址,target_offset指向远端地址。
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;
}
这个函数的核心三步是:
- 定 Buffer:遍历
desc->buffers,检查offset + length是否完全落在某个 Buffer 内 - 解 Location:从 Buffer 的
name(注册时传入的 location 字符串)中解析出 NUMA 域标识。普通 location 如cpu:0直接使用;分段 location 如MT_mem_0gb_0~MT_mem_0gb_15则计算当前 offset 对应的子段位置 - 选 HCA:调用
desc->topology.selectDevice(location, retry_count)从 topology matrix 中查出对应 location 的 preferred HCA
3.5 Topology.selectDevice —— 从 location 到设备索引
Topology 在发现阶段(topology.cpp 中的 discoverCpuTopology、discoverGpuTopology 等)枚举所有 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 携带了:
- 远端的 buffer 列表(含地址、长度、rkey)
- 远端的 device 列表(NIC 名称、LID、GID)
- 远端的 topology matrix(远端 NUMA 域 → 远端 HCA 映射)
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;
}
}
关键参数:
desc=peer_segment_desc—— 远端的 SegmentDescoffset=slice->rdma.dest_addr—— 远端目标地址(request.target_offset + offset)hint= 本地 context 的设备名(如果enable_dest_device_affinity为 true)
和本地侧调用的是同一个 selectDevice 函数,但操作的是不同的 SegmentDesc:本地的 selectDevice 用本地的 buffers + topology,远端的用远端的 buffers + topology。这就是”两级独立决策”的本质。
4.3 远端 HCA 选择的关键影响因素
- 远端 Buffer 的 NUMA location:远端内存可能分布在不同的 NUMA 域,不同域对应的 preferred HCA 不同
- 远端 topology:远端系统的 NIC-NUMA 拓扑与本地的拓扑完全独立——本地拓扑看不到远端的亲和关系
- 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:
- 每个本地 HCA(对应一个
RdmaContext)有自己的一组 QP - 同一远端 server 上的不同 NIC(如
mlx5_0和mlx5_1)产生不同的peer_nic_path,即不同的 Endpoint - 因此,对于 N 个本地 HCA × M 个远端 NIC 的场景,最多可以有 N × M 个不同的 RDMA Endpoint
5.2 NicPath 归一化
为了支持跨连接的 endpoint 复用,TE 还在 common.h:475-479 提供了 normalizeNicPath:去除 peer_nic_path 中的端口部分(每次 handshake 随机分配的端口),保留 server@nic 作为稳定的复用键。同一个物理 peer 即使重新连接分配了不同端口,也能复用已建立的 endpoint。
6. 完整路径选择决策表
| 方向 | 发生位置 | 输入参数 | 查找的数据结构 | 决定的设备 | 输出给下游 |
|---|---|---|---|---|---|
| 本地 | submitTransferTask | request.source + request.length | local_segment_desc.buffers + local_segment_desc.topology | context_list_[device_id](本地 HCA/RdmaContext) | slice->rdma.source_lkey |
| 远端 | WorkerPool::submitPostSend | slice->rdma.dest_addr + slice->length | peer_segment_desc.buffers + peer_segment_desc.topology | peer_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
- Line 698-706:遍历
desc->buffers,通过地址范围检查找到命中 Buffer - Line 708-714:从 Buffer
name解析 location,支持普通和分段格式 - Line 716-724:调用
desc->topology.selectDevice(location)或带 hint 的版本;失败则用kWildcardLocation回退
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
- hint 版本(line 555-572):有 hint 时先做名称匹配,命中返回;未命中回退到无 hint 版本
- 无 hint 版本(line 574-601):retry_count=0 时随机/轮询选 preferred HCA;retry_count>=1 时按序遍历 preferred+available 列表
8.5 Topology 发现
文件:mooncake-transfer-engine/src/topology.cpp:303-338
discoverCpuTopology:扫描/sys/devices/system/node/node*,枚举每个 NUMA 域的 IB HCA- 与 NUMA 同节点的 HCA 归入
preferred_hca,不同节点的归入avail_hca - 每个 NUMA 域生成
TopologyEntry{name="cpu:N", ...}
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 数据结构
TransferRequest:mooncake-transfer-engine/include/transport/transport.h:58-67SegmentDesc:mooncake-transfer-engine/include/transfer_metadata.h:88-108DeviceDesc:mooncake-transfer-engine/include/transfer_metadata.h:45-50BufferDesc:mooncake-transfer-engine/include/transfer_metadata.h:52-65
9. 总结
Mooncake TE 的路径选择是一个”两边各自决策、最终汇聚”的设计:
- 本地侧根据
request.source在本地的 buffers + topology 中找出最合适的本地 HCA,设置source_lkey - 远端侧根据
target_offset在远端的 buffers + topology 中找出最合适的远端 NIC,设置dest_rkey - 汇聚:
(本地 context, 远端 server@nic)形成唯一的 peer NIC path,作为建立或复用 RDMA Endpoint 的键
这种设计使得:即使本地有 3 个 HCA、远端有 4 个 HCA,TE 也能为每个 (本地 HCA, 远端 HCA) 组合建立独立的 RDMA 连接,不会跨 NUMA 走”冤枉路”。同时,拓扑信息分布在各自的 SegmentDesc 中,两侧的决策完全解耦——每个节点只需要维护自己的 NIC-NUMA 映射,无需知道对端的拓扑细节。
修订说明
- 2026-05-26: 修订两处行号精度问题(基于源代码审查结果):
- 第 3 节:
submitTransferTask函数行号范围从464-569更正为464-582(函数体实际结束于 582 行) - 第 4 节:远端侧选择代码行号范围从
111-151更正为111-148(与第 8.3 节的引述保持一致;核心选择逻辑 + rkey + NicPath 构建在第 148 行结束,149 行起为 shard 分桶逻辑)
- 第 3 节: