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

Mooncake TE 阅读手记-14-RDMA 内存注册与 lkey/rkey

团团虾导读:RDMA 编程中最容易搞混的概念就是 lkey 和 rkey——它们都来自同一次 ibv_reg_mr() 调用,但一个留在本地,一个通过 etcd 传给对端。这篇从 registerLocalMemory 的源码链路出发,把内存注册→密钥分发→RDMA WR 构造→Worker Pool 消费这一整条路径完整走了一遍。最后讨论了 rkey 能不能独立映射内存这个常见误区。

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

RDMA 内存注册与 lkey/rkey 三元组

在阅读 Mooncake Transfer Engine 的 registerLocalMemory() 代码时,我们会看到这样一条链路:TransferEngineImpl 遍历所有已安装的 Transport,把用户传入的同一块内存注册到每一个 Transport 上。对于 RDMA Transport 而言,“注册”就是调用 ibv_reg_mr() 创建 Memory Region(MR),并从中拿到 lkey(Local Key)rkey(Remote Key)

本文从 Mooncake 源码出发,先理清这三个概念,然后完整追踪从内存注册到 RDMA 数据传输的全链路,最后讨论 “rkey 能不能独立映射内存” 这个问题。


一、lkey / rkey / MR 概念速览

概念英文全称属于谁用途
MRMemory Region哪一方注册就属于哪一方一块被 HCA 注册过的内存区域,只有 MR 内的地址才能被 RDMA 直接访问
lkeyLocal Key注册方自己的本地 HCA 在工作请求(WR)中读写本地 MR 时出示的凭证
rkeyRemote Key注册方自己,但交给远端远端 HCA 在 RDMA READ/WRITE 时访问本地 MR 需要出示的凭证
PDProtection Domain本地QP 和 MR 必须属于同一个 PD 才能互访,提供安全隔离

核心事实:每次 ibv_reg_mr() 同时产生一个 lkey 和一个 rkey,它们出自同一个 struct ibv_mr

// struct ibv_mr 的关键字段
mr->addr   // 映射后的虚拟地址(iova-based MR 下不一定等于注册地址)
mr->lkey   // 本地密钥
mr->rkey   // 远程密钥

lkey 留给自己用,rkey 通过 metadata server 交换给对方用。


二、完整路径:从注册到传输

2.1 入口:TransferEngineImpl 遍历所有 Transport

// src/transfer_engine_impl.cpp:511-534
int TransferEngineImpl::registerLocalMemory(void* addr, size_t length,
                                            const std::string& location,
                                            bool remote_accessible,
                                            bool update_metadata) {
    // 1. 禁止重叠注册
    if (checkOverlap(addr, length)) {
        LOG(ERROR)
            << "Transfer Engine does not support overlapped memory region";
        return ERR_ADDRESS_OVERLAPPED;
    }
    // 2. 零长度检查
    if (length == 0) {
        LOG(ERROR)
            << "Transfer Engine does not support zero length memory region";
        return ERR_INVALID_ARGUMENT;
    }

    // 3. 遍历所有已安装的 Transport,逐个注册
    for (auto transport : multi_transports_->listTransports()) {
        int ret = transport->registerLocalMemory(
            addr, length, location, remote_accessible, update_metadata);
        if (ret < 0) return ret;
    }

    // 4. 记录到本地 map,防止重复注册
    std::unique_lock<std::shared_mutex> lock(mutex_);
    insertMemoryRegionLocked({addr, length, location, remote_accessible});
    return 0;
}

listTransports() 就一行:遍历 transport_map_ 返回所有已安装 Transport 的裸指针。

// src/multi_transport.cpp:506-511
std::vector<Transport*> MultiTransport::listTransports() {
    std::vector<Transport*> transport_list;
    for (auto& entry : transport_map_)
        transport_list.push_back(entry.second.get());
    return transport_list;
}

所以如果同时安装了 rdmatcpcxl 三个 Transport,同一块内存会被注册三次——每个 Transport 各一次。


2.2 RdmaTransport:在每个 Context 上注册 MR 并收集密钥

// src/transport/rdma_transport/rdma_transport.cpp:183-304
int RdmaTransport::registerLocalMemoryInternal(void *addr, size_t length,
                                                const std::string &name,
                                                bool remote_accessible,
                                                bool update_metadata,
                                                bool force_sequential) {
    BufferDesc buffer_desc;

    // 设置访问权限
    const int kBaseAccessRights = IBV_ACCESS_LOCAL_WRITE |
                                  IBV_ACCESS_REMOTE_WRITE |
                                  IBV_ACCESS_REMOTE_READ;

    int access_rights = kBaseAccessRights;
    if (MCIbRelaxedOrderingEnabled) {
        access_rights |= IBV_ACCESS_RELAXED_ORDERING;  // IBVERBS_1.8+
    }

    // 预触碰需同时满足三个条件:至少一个 Context、至少 4 个 CPU 核心、
    // 且内存大小 >= 4GB。预触碰目的是加速后续 MR 注册。
    bool do_pre_touch = context_list_.size() > 0 &&
                        std::thread::hardware_concurrency() >= 4 &&
                        length >= (size_t)4 * 1024 * 1024 * 1024;
    if (do_pre_touch) {
        int ret = preTouchMemory(addr, length);
        if (ret != 0) return ret;
    }

    // 在每个 RdmaContext(每块物理网卡一个)上注册 MR
    // 注:源码存在并行注册分支(MC_ENABLE_PARALLEL_REG_MR),
    // 启用后为每个 Context 创建独立线程并发注册以加速。此处展示串行路径。
    for (size_t i = 0; i < context_list_.size(); ++i) {
        int ret = context_list_[i]->registerMemoryRegion(addr, length,
                                                          access_rights);
        if (ret) return ret;
    }

    // 收集每个 Context 的 lkey 和 rkey
    for (auto &context : context_list_) {
        buffer_desc.lkey.push_back(context->lkey(addr));
        buffer_desc.rkey.push_back(context->rkey(addr));
    }
    // buffer_desc.lkey[0] = NIC-0 的本地密钥
    // buffer_desc.lkey[1] = NIC-1 的本地密钥
    // buffer_desc.rkey[0] = NIC-0 的远程密钥
    // buffer_desc.rkey[1] = NIC-1 的远程密钥

    // 写入 metadata server,供远端节点查询
    buffer_desc.addr = (uint64_t)addr;
    buffer_desc.length = length;
    metadata_->addLocalMemoryBuffer(buffer_desc, update_metadata);
}

关键点:lkeyrkey按 NIC 维度存储的 vector,每个 NIC(即每个 RdmaContext)都独立为同一块内存注册 MR 并生成一对 key。远端发起传输时,需要知道目标地址落在哪个 buffer 上、用哪个 NIC,然后取对应的 rkey[device_id]


2.3 RdmaContext:真正调用 Verbs API

// src/transport/rdma_transport/rdma_context.cpp:225-338
int RdmaContext::registerMemoryRegionInternal(void *addr, size_t length,
                                               int access,
                                               MemoryRegionMeta &mrMeta) {
    // CPU 内存 -- 直接注册
    mrMeta.addr = addr;
    mrMeta.mr = ibv_reg_mr(pd_, addr, length, access);

    // GPU 内存 + nvidia-peermem -- 同上
    // GPU 内存 + 无 nvidia-peermem -- 通过 dma-buf 注册
    //   cuMemGetAddressRange → cuMemGetHandleForAddressRange
    //   → ibv_reg_dmabuf_mr(pd_, dmabuf_offset, length, addr, dmabuf_fd, access)
}

成功后 mrMeta 存储 void *addr(原始虚拟地址)和 struct ibv_mr *mr


2.4 lkey/rkey 的读取

// src/transport/rdma_transport/rdma_context.cpp:375-391
uint32_t RdmaContext::rkey(void *addr) {
    RWSpinlock::ReadGuard guard(memory_regions_lock_);
    auto iter = findMemoryRegionContaining(reinterpret_cast<uintptr_t>(addr));
    if (iter != memory_region_map_.end()) return iter->second.mr->rkey;
    LOG(ERROR) << "Address " << addr << " rkey not found for " << deviceName();
    return 0;
}

uint32_t RdmaContext::lkey(void *addr) {
    RWSpinlock::ReadGuard guard(memory_regions_lock_);
    auto iter = findMemoryRegionContaining(reinterpret_cast<uintptr_t>(addr));
    if (iter != memory_region_map_.end()) return iter->second.mr->lkey;
    LOG(ERROR) << "Address " << addr << " lkey not found for " << deviceName();
    return 0;
}

findMemoryRegionContaining()std::map<uintptr_t, MemoryRegionMeta> 做 upper_bound 二分查找,支持从一段大 MR 内的任意子地址反查所属 MR。


2.5 BufferDesc 的数据结构

// include/transfer_metadata.h:52-65
struct BufferDesc {
    std::string name;                // NUMA 位置,如 "CPU_0"
    uint64_t addr;                   // 起始地址
    uint64_t length;
    std::vector<uint32_t> lkey;      // 每 NIC 一个本地密钥
    std::vector<uint32_t> rkey;      // 每 NIC 一个远程密钥
    // ... 其他协议字段
};

2.6 传输阶段:组装 WR 并投递

Step 1 — 切片并设置 source_lkey (rdma_transport.cpp:464-582):

// submitTransferTask() 中
slice->rdma.dest_addr = request.target_offset + offset;
slice->rdma.source_lkey =
    local_segment_desc->buffers[buffer_id].lkey[device_id];

Step 2 — 解析远端 rkey (worker_pool.cpp:143-144):

// WorkerPool::submitPostSend() 中,从远端 peer 的段描述中找到 rkey
slice->rdma.dest_rkey =
    peer_segment_desc->buffers[buffer_id].rkey[device_id];

Step 3 — 组装 SGE + WR,调用 ibv_post_send (rdma_endpoint.cpp:613-640):

// SGE 描述本地内存(数据从哪里读 / 写到哪里)
sge.addr   = (uint64_t)slice->source_addr;
sge.length = slice->length;
sge.lkey   = slice->rdma.source_lkey;   // ← 本地 lkey

// WR 描述远端操作
// 注:完整 WR 还含 wr_id、num_sge、send_flags、wr.next 等字段,
// 此处略去以聚焦 lkey/rkey 的两个三元组关系
wr.opcode              = (slice->opcode == READ)
                         ? IBV_WR_RDMA_READ
                         : IBV_WR_RDMA_WRITE;
wr.sg_list             = &sge;
wr.wr.rdma.remote_addr = slice->rdma.dest_addr;  // ← 远端地址
wr.wr.rdma.rkey        = slice->rdma.dest_rkey;  // ← 远端 rkey

// 投递到 QP
ibv_post_send(qp_list_[qp_index], wr_list.data(), &bad_wr);

三、RDMA 操作的两个三元组

上面的代码清楚地展示了一个事实:一次 RDMA READ 或 WRITE 需要两组参数,而不是一个三元组。

3.1 两个三元组

方位三元组对应代码语义
本地(local_addr, length, lkey)sge.addr / sge.length / sge.lkey描述本地内存位置和访问凭证
远端(remote_addr, length, rkey)wr.wr.rdma.remote_addr / slice->length / wr.wr.rdma.rkey描述远端内存位置和访问凭证

以 RDMA READ 为例:

以 RDMA WRITE 为例:

3.2 Slice 中的 RDMA 字段

// include/transport/transport.h:117-127
union {
    struct {
        uint64_t dest_addr;      // 远端地址
        uint32_t source_lkey;    // 本地 lkey
        uint32_t dest_rkey;      // 远端 rkey
        int lkey_index;
        int rkey_index;
        volatile int *qp_depth;
        uint32_t retry_cnt;
        uint32_t max_retry_cnt;
    } rdma;
    // ... 其他传输协议的字段
};

可以看到 dest_addrsource_lkeydest_rkey 都在同一 struct 里,而 source_addr 在外层的 Slice 结构体中,length 也在外层。这五个字段正好构成了两个三元组。

3.3 为什么 rkey 不能单独映射地址

rkey 的设计哲学是权限和地址分离

这三者是一一绑定在 Work Request 里的:

wr.wr.rdma.remote_addr = xxx;  // 访问哪个地址(偏移)
wr.wr.rdma.rkey        = yyy;  // 出示哪个凭证(权限)
// 范围由 sge.length 指定

IB Verbs 规范没有 “用 rkey 解析出地址” 的 API。远端地址必须由应用层显式传递。在 Mooncake 中,这个地址就是 TransferRequest::target_offset,在切片时填入 slice->rdma.dest_addr


四、端到端数据流

[本地进程]                              [远端进程]
    |                                       |
    | registerLocalMemory(buf)              | registerLocalMemory(buf)
    |  → ibv_reg_mr()                       |  → ibv_reg_mr()
    |  → lkey=0x123, rkey=0xABC             |  → lkey=0x456, rkey=0xDEF
    |  → 存入 metadata server(两端都能读)   |  → 存入 metadata server
    |                                       |
    | submitTransfer(WRITE)                 |
    |  从 metadata 查询远端 SegmentDesc:     |
    |    remote_addr = 远端 buf 地址          |
    |    dest_rkey   = 0xDEF                |
    |                                       |
    |  组装 SGE:                             |
    |    {本地 addr, len, lkey=0x123}        |
    |  组装 WR:                              |
    |    {remote_addr, rkey=0xDEF}           |
    |                                       |
    |  ibv_post_send(QP) ─────────────────→  │ HCA 直接 DMA 写入远端内存
    |                                       │ (远端 CPU 全程不参与)
    |  ← ibv_poll_cq() = COMPLETED ──────── │

核心文件索引

文件行号内容
mooncake-transfer-engine/src/transfer_engine_impl.cpp511-534遍历所有 Transport 注册内存
mooncake-transfer-engine/src/multi_transport.cpp506-511listTransports() 实现
mooncake-transfer-engine/src/transport/rdma_transport/rdma_transport.cpp175-304RDMA 内存注册 + 密钥收集
mooncake-transfer-engine/src/transport/rdma_transport/rdma_context.cpp225-338ibv_reg_mr() / ibv_reg_dmabuf_mr()
mooncake-transfer-engine/src/transport/rdma_transport/rdma_context.cpp375-391lkey() / rkey() 查询方法
mooncake-transfer-engine/src/transport/rdma_transport/rdma_context.cpp393-413findMemoryRegionContaining()
mooncake-transfer-engine/src/transport/rdma_transport/rdma_transport.cpp464-582submitTransferTask() 切片 + source_lkey
mooncake-transfer-engine/src/transport/rdma_transport/worker_pool.cpp143-144dest_rkey 从 peer 解析
mooncake-transfer-engine/src/transport/rdma_transport/rdma_endpoint.cpp613-640SGE/WR 组装 + ibv_post_send()
mooncake-transfer-engine/include/transport/transport.h58-67TransferRequest 定义
mooncake-transfer-engine/include/transport/transport.h117-127Slice::rdma union 成员
mooncake-transfer-engine/include/transfer_metadata.h52-65BufferDesclkey[]rkey[] vector

五、总结

  1. MR 注册一次,产出一对 keyibv_reg_mr() 对一段内存地址范围创建 MR,返回的 ibv_mr 里同时有 lkeyrkey

  2. lkey 留给自己,rkey 交给别人:本地 HCA 在 WR 中用 lkey 访问本地内存;远端 HCA 用 rkey 访问这块内存。Mooncake 通过 metadata server 交换 BufferDesc(含 lkey[]/rkey[] vector)。

  3. 一次 RDMA 操作需要两个三元组

    • 本地侧:(local_addr, length, lkey) — 通过 SGE 描述
    • 远端侧:(remote_addr, length, rkey) — 通过 wr.rdma 描述
  4. rkey 不能独立映射地址:地址和权限分离是 RDMA 的核心设计。rkey 只提供访问凭证,remote_addrlength 显式指定访问范围,三者共同完整描述一次远端内存访问。


修订说明

2026-05-26 修订,基于 review-notes.md 对 7 处简化进行修正:

  1. Section 2.1:补充 length == 0 零长度检查及 checkOverlapLOG(ERROR) 调用。
  2. Section 2.2:修正 pre-touch 触发条件,从仅 >= 4GB 改为源码中的三条件(NIC 数、CPU 核心数、内存大小)。
  3. Section 2.2:标注源码存在 MC_ENABLE_PARALLEL_REG_MR 并行注册分支。
  4. Section 2.2:补充 IBV_ACCESS_RELAXED_ORDERING 标志位(IBVERBS_1.8+)。
  5. Section 2.4:恢复 lkey()/rkey() 方法中的 LOG(ERROR) 调用。
  6. Section 2.6:标注 WR 省略字段(wr_idnum_sgesend_flagswr.next)。
  7. Section 2.2:修正多 NIC 系统的中文表述,消除歧义。

Share this post on:

Previous Post
Mooncake TE 阅读手记-15-RDMA QP/CQ 与操作模式
Next Post
Mooncake TE 阅读手记-13-高性能编程线程模型