前序
关于RDMA带外建联过程,博客[1]已经分析的很好了。本篇结合nccl的源码,分析RDMA的建链过程。
发送端和接收端的qp状态迁移流程:
text
发送端 QP 迁移流程:
RESET --[create]--> RESET --[modify INIT]--> INIT --[modify RTR]--> RTR --[modify RTS]--> RTS
^ ^ ^
| | |
设置本地端口 提供远端地址、QPN 设置发送序列号、重试
P_Key、访问权限 MTU、GID/LID
接收端 QP 迁移流程(类似,但角色相反):
RESET --[create]--> RESET --[modify INIT]--> INIT --[modify RTR]--> RTR --[modify RTS]--> RTS
^ ^ ^
| | |
设置本地端口 使用发送端提供的 设置发送序列号
P_Key、访问权限 远端 QPN、地址 (准备发送 CTS)
发送端处理流程
QP 状态迁移总览(结合 NCCL 步骤)
text
ncclIbQpCreate()
│
▼
┌──────────┐
│ RESET │
└──────────┘
│
│ ncclIbQpInit()
│ (set port, pkey, access)
▼
┌──────────┐
│ INIT │
└──────────┘
│
│ ncclIbQpRtr()
│ (提供 dest_qpn, ah_attr, MTU, ...)
▼
┌──────────┐
│ RTR │ ← 可接收消息
└──────────┘
│
│ ncclIbQpRts()
│ (设置 psn, timeout, retry)
▼
┌──────────┐
│ RTS │ ← 可发送/接收
└──────────┘
NCCL 流程中:
- RESET → INIT 在 ncclIbSenderQpsCreate 中完成(发送端)
- INIT → RTR → RTS 在 ncclIbSenderQpsToRts 中完成(收到对端元数据后)
ncclIbConnect 总体流程(发送端视角)
text
ncclIbConnect()
│
├─ 1. 解析句柄,获取远端监听地址
│ handle->connectAddr → ncclSocketInit()
│
├─ 2. 异步连接 Socket
│ ncclSocketConnect() → 等待 ncclSocketReady()
│
├─ 3. 交换设备虚拟属性
│ ├─ 发送本地 ncclNetVDeviceProps_t + ncclIbDevExtraProps
│ ├─ 接收远端相同结构
│ └─ 计算最终 ndevs / nqps = max(local, remote)
│
├─ 4. 为每个物理设备初始化基础资源 (ncclIbInitCommDevBase)
│ ├─ 分配 PD (wrap_ibv_alloc_pd)
│ ├─ 创建 CQ (wrap_ibv_create_cq)
│ └─ 注册 MR: ctsFifoMr, cmplsRecordsMr, putSignalScratchpadMr
│
├─ 5. 创建并初始化 QP 至 INIT 状态
│ └─ ncclIbSenderQpsCreate() → 见图2
│
├─ 6. 交换连接元数据 (ncclIbConnectionMetadata)
│ ├─ 发送本地元数据(QPN, GID, LID, MTU, ECE...)
│ └─ 接收远端元数据
│
├─ 7. 将 QP 迁移至 RTR → RTS 状态
│ └─ ncclIbSenderQpsToRts() → 见图3
│
├─ 8. 最终握手 (发送 ready 标志)
│
└─ 9. 返回 sendComm
QP 创建与初始化至 INIT
text
ncclIbSenderQpsCreate()
│
├─ for each qpIndex in [0 .. nqps-1]
│ │
│ ├─ devIndex = qpIndex % ndevs // 条带化分配
│ ├─ ibDev = ncclIbDevs[ devIndex ]
│ │
│ ├─ 准备创建属性 qpCreateAttrs
│ │ ├─ type = IBV_QPT_RC
│ │ ├─ max_send_wr = 2*NET_IB_MAX_REQUESTS
│ │ ├─ max_recv_wr = 0
│ │ ├─ cq, pd, qp_context
│ │ └─ oooRq = (localOooRq && remOooRq && MLX5)
│ │
│ ├─ ncclIbQpCreate()
│ │ └─ wrap_ibv_create_qp() → QP 状态 = RESET
│ │
│ ├─ 填充元数据:qpInfo[qpIndex].qpn, devIndex
│ │
│ ├─ ncclIbQpInit() // RESET → INIT
│ │ ├─ ibv_qp_attr:
│ │ │ ├─ qp_state = IBV_QPS_INIT
│ │ │ ├─ pkey_index = ncclParamIbPkey()
│ │ │ ├─ port_num = ibDev->portNum
│ │ │ └─ qp_access_flags = IBV_ACCESS_REMOTE_WRITE
│ │ └─ wrap_ibv_modify_qp( , IBV_QP_STATE|IBV_QP_PKEY_INDEX|IBV_QP_PORT|IBV_QP_ACCESS_FLAGS)
│ │
│ └─ 若启用 ECE:
│ wrap_ibv_query_ece() → 保存到本地 qpInfo[].ece, ece_supported
│
└─ 所有 QP 处于 INIT 状态,等待远端地址信息
QP 迁移至 RTR 与 RTS
text
ncclIbSenderQpsToRts(remMeta)
│
├─ for each qpIndex in [0 .. nqps-1]
│ │
│ ├─ 获取远端 QP 信息:remQpInfo = &remMeta->qpInfo[qpIndex]
│ ├─ 获取远端设备信息:remDevInfo = &remMeta->devs[remQpInfo->devIndex]
│ │
│ ├─ 若双方支持 ECE:
│ │ wrap_ibv_set_ece( , &remQpInfo->ece) // 协商 ECE 参数
│ │
│ ├─ 1. 迁移至 RTR (ncclIbQpRtr)
│ │ ├─ 填充 ibv_qp_attr:
│ │ │ ├─ qp_state = IBV_QPS_RTR
│ │ │ ├─ path_mtu = min(本地MTU, 远端MTU)
│ │ │ ├─ dest_qp_num = remQpInfo->qpn
│ │ │ ├─ rq_psn = 0
│ │ │ ├─ max_dest_rd_atomic = 1
│ │ │ ├─ min_rnr_timer = 12
│ │ │ └─ ah_attr (地址属性) → 见图4
│ │ └─ wrap_ibv_modify_qp( , IBV_QP_STATE|IBV_QP_AV|IBV_QP_PATH_MTU|IBV_QP_DEST_QPN|IBV_QP_RQ_PSN|... )
│ │
│ └─ 2. 迁移至 RTS (ncclIbQpRts)
│ ├─ 填充 ibv_qp_attr:
│ │ ├─ qp_state = IBV_QPS_RTS
│ │ ├─ sq_psn = 0
│ │ ├─ timeout = ncclParamIbTimeout()
│ │ ├─ retry_cnt = ncclParamIbRetryCnt()
│ │ ├─ rnr_retry = 7
│ │ └─ max_rd_atomic = 1
│ └─ wrap_ibv_modify_qp( , IBV_QP_STATE|IBV_QP_SQ_PSN|IBV_QP_TIMEOUT|IBV_QP_RETRY_CNT|IBV_QP_RNR_RETRY|IBV_QP_MAX_QP_RD_ATOMIC)
│
└─ 所有 QP 进入 RTS 状态,可以发送/接收数据
地址关联 -- ah_attr 填充细节(RTR 阶段)
text
构建 ah_attr (struct ibv_ah_attr)
│
├─ 基础设置
│ ├─ ah_attr.sl = remMeta->sl // 服务等级
│ ├─ ah_attr.src_path_bits = 0
│ └─ ah_attr.port_num = rtrAttr->localIbPort
│
├─ 判断链路类型
│ │
│ ├─ 如果是 RoCE (linkLayer == IBV_LINK_LAYER_ETHERNET)
│ │ ├─ ah_attr.is_global = 1
│ │ ├─ ah_attr.grh.dgid = rtrAttr->remoteGid
│ │ ├─ ah_attr.grh.sgid_index = rtrAttr->localGidIndex
│ │ ├─ ah_attr.grh.hop_limit = 255
│ │ └─ ah_attr.grh.traffic_class = rtrAttr->tc (DSCP)
│ │
│ └─ 如果是 InfiniBand
│ ├─ 比较子网前缀:
│ │ local_subnet = extract_subnet(localGid)
│ │ remote_subnet = extract_subnet(remoteGid)
│ │
│ ├─ 若 local_subnet == remote_subnet (同一子网)
│ │ ├─ ah_attr.is_global = 0
│ │ └─ ah_attr.dlid = rtrAttr->remoteLid
│ │
│ └─ 若 local_subnet != remote_subnet (跨子网)
│ ├─ ah_attr.is_global = 1
│ ├─ ah_attr.dlid = ncclIbExtractFlid(&remoteGid) // FLID
│ ├─ ah_attr.grh.dgid = remoteGid
│ └─ ah_attr.grh.sgid_index = rtrAttr->localGidIndex
│
└─ 最终该 ah_attr 传递给 ibv_modify_qp 的 IBV_QP_AV 掩码
接收端处理流程
接收端 QP 状态迁移遵循 IB 规范:RESET → INIT → RTR → RTS。
ncclIbAccept 总体流程
text
ncclIbAccept(listenComm, &recvComm, ...)
│
├─ 1. 从 listenComm 获取监听 socket 和 stage
│ lComm->stage 用于保存握手状态
│
├─ 2. 接受 socket 连接 (异步)
│ ncclSocketInit() → ncclSocketAccept()
│ 等待 ncclSocketReady()
│
├─ 3. 接收发送端设备虚拟属性
│ ├─ 接收 ncclNetVDeviceProps_t + ncclIbDevExtraProps
│ ├─ 本地虚拟设备与远端做交集 (ncclIbCheckVProps)
│ └─ 计算最终 ndevs / nqps = max(local, remote)
│
├─ 4. 回发本地设备虚拟属性
│ └─ 发送本地 ncclNetVDeviceProps_t + ncclIbDevExtraProps
│
├─ 5. 接收发送端连接元数据 (ncclIbConnectionMetadata)
│ └─ 获得远端 QPN、GID、LID、MTU、ECE、RKey 等
│
├─ 6. 初始化本地 IB 资源 (为每个物理设备)
│ ├─ 分配 PD (wrap_ibv_alloc_pd)
│ ├─ 创建 CQ (wrap_ibv_create_cq)
│ └─ 注册 MR: ctsFifoMr, cmplsRecordsMr
│
├─ 7. 创建并配置 QP 至 RTS 状态
│ └─ ncclIbReceiverQpsCreateToRts()
│
├─ 8. 预投递接收 Work Request (可选)
│ └─ ncclIbReceiverPrePostReceiveWorkRequests()
│
├─ 9. 填充本地元数据并发送回发送端
│ ├─ meta.ndevs, meta.devs[].rkey, lid, gid, mtu...
│ └─ ncclSocketProgress() 发送
│
├─ 10. 等待发送端的 ready 标志
│
└─ 11. 返回 recvComm
接收端 QP 创建与配置
text
ncclIbReceiverQpsCreateToRts(remMeta, &meta)
│
├─ for each qpIndex in [0 .. nqps-1]
│ │
│ ├─ devIndex = qpIndex % ndevs // 条带化分配
│ ├─ ibDev = ncclIbDevs[ devIndex ]
│ ├─ remQpInfo = &remMeta->qpInfo[qpIndex]
│ │
│ ├─ 1. 创建 QP (ncclIbQpCreate)
│ │ ├─ type = IBV_QPT_RC
│ │ ├─ max_recv_wr = NET_IB_MAX_REQUESTS
│ │ ├─ max_send_wr = 2*NET_IB_MAX_REQUESTS
│ │ └─ wrap_ibv_create_qp() → QP 状态 RESET
│ │
│ ├─ 2. 迁移至 INIT 状态 (ncclIbQpInit)
│ │ └─ 设置 pkey_index, port_num, qp_access_flags
│ │
│ ├─ 3. 若远端支持 ECE,设置 ECE (wrap_ibv_set_ece)
│ │
│ ├─ 4. 迁移至 RTR 状态 (ncclIbQpRtr)
│ │ ├─ 填充 ah_attr:
│ │ │ ├─ 根据链路类型 (RoCE/IB) 设置 dgid/lid/sgid_index
│ │ │ └─ sl, port_num
│ │ ├─ dest_qp_num = remQpInfo->qpn
│ │ ├─ path_mtu = min(localMTU, remDevInfo->mtu)
│ │ └─ wrap_ibv_modify_qp()
│ │
│ ├─ 5. 迁移至 RTS 状态 (ncclIbQpRts)
│ │ ├─ timeout, retry_cnt, rnr_retry
│ │ └─ wrap_ibv_modify_qp()
│ │
│ ├─ 6. 查询协商后的 ECE (wrap_ibv_query_ece)
│ │ └─ 填充 meta.qpInfo[qpIndex].ece
│ │
│ └─ 7. 记录 QPN 到 meta
│
├─ 若 flushEnabled:
│ └─ 为每个设备额外创建用于 GPU Direct Flush 的 QP
│
└─ 所有 QP 进入 RTS 状态
总结
发送端在 RTR 阶段使用从接收端元数据中获得的 dest_qp_num 和 ah_attr。
接收端在 RTR 阶段使用从发送端元数据中获得的 dest_qp_num 和 ah_attr。
双方都需要知晓对端的 QPN 和 GID/LID 才能完成地址关联。
Reference
1\] [RDMA带外建联过程](https://zhuanlan.zhihu.com/p/655663006)