RDMA_verbs详解-修改队列对属性(ibv_modify_qp)-ceph-ucx-nccl-intel rdma驱动源码解析

2024-02-24 17:14:16 浏览数 (2)

简介

QP上可变化的属性描述了QP的发送和接收属性。 在 UC 和 RC QP 中,这意味着将 QP 与远程 QP 连接。 在 Infiniband 中,应向子网管理员 (SA) 执行路径查询,以确定 QP 应配置哪些属性或作为最佳解决方案,使用通信管理器 (CM) 或通用 RDMA CM 代理 (CMA) 连接 QP。 然而,有些应用程序(如ceph)更喜欢自行连接 QP,并通过套接字交换数据来决定使用哪些 QP 属性。 在 RoCE 中,应在连接的 QP 的 QP 属性中配置 GRH,或在 UD QP 的地址句柄(Address Handle)中配置 GRH。 在 iWARP 中,应仅使用通用 RDMA CM 代理 (CMA) 连接 QP。 结构体: struct ibv_qp_attr 描述了队列对QP的属性:

代码语言:javascript复制
struct ibv_qp_attr {
    enum ibv_qp_state   qp_state;
    enum ibv_qp_state   cur_qp_state;
    enum ibv_mtu        path_mtu;
    enum ibv_mig_state  path_mig_state;
    uint32_t        qkey;
    uint32_t        rq_psn;
    uint32_t        sq_psn;
    uint32_t        dest_qp_num;
    unsigned int        qp_access_flags;
    struct ibv_qp_cap   cap;
    struct ibv_ah_attr  ah_attr;
    struct ibv_ah_attr  alt_ah_attr;
    uint16_t        pkey_index;
    uint16_t        alt_pkey_index;
    uint8_t         en_sqd_async_notify;
    uint8_t         sq_draining;
    uint8_t         max_rd_atomic;
    uint8_t         max_dest_rd_atomic;
    uint8_t         min_rnr_timer;
    uint8_t         port_num;
    uint8_t         timeout;
    uint8_t         retry_cnt;
    uint8_t         rnr_retry;
    uint8_t         alt_port_num;
    uint8_t         alt_timeout;
    uint32_t        rate_limit;
};

详细状态机及参数请参考mojo博客: https://www.rdmamojo.com/2013/01/12/ibv_modify_qp/

https://www.rdmamojo.com/2012/05/05/qp-state-machine/

ceph建连

代码语言:javascript复制
...
handle_connection
    activate()
        modify_qp_to_rtr
            qpa.qp_state = IBV_QPS_RTR
            ibv_modify_qp
        modify_qp_to_rts
            qpa.qp_state = IBV_QPS_RTS
            ibv_modify_qp

RTR属性如下:

代码语言:javascript复制
int Infiniband::QueuePair::modify_qp_to_rtr(void)
{
  // move from INIT to RTR state
  ibv_qp_attr qpa;
  // FIPS zeroization audit 20191115: this memset is not security related.
  memset(&qpa, 0, sizeof(qpa));
  qpa.qp_state = IBV_QPS_RTR;
  qpa.path_mtu = IBV_MTU_1024;
  qpa.dest_qp_num = peer_cm_meta.local_qpn;
  qpa.rq_psn = peer_cm_meta.psn;
  qpa.max_dest_rd_atomic = 1;
  qpa.min_rnr_timer = 0x12;
  qpa.ah_attr.is_global = 1;
  qpa.ah_attr.grh.hop_limit = 6;
  qpa.ah_attr.grh.dgid = peer_cm_meta.gid;
  qpa.ah_attr.grh.sgid_index = infiniband.get_device()->get_gid_idx();
  qpa.ah_attr.grh.traffic_class = cct->_conf->ms_async_rdma_dscp;
  //qpa.ah_attr.grh.flow_label = 0;
​
  qpa.ah_attr.dlid = peer_cm_meta.lid;
  qpa.ah_attr.sl = cct->_conf->ms_async_rdma_sl;
  qpa.ah_attr.src_path_bits = 0;
  qpa.ah_attr.port_num = (uint8_t)(ib_physical_port);
​
  ldout(cct, 20) << __FFL__ << " Choosing gid_index " << (int)qpa.ah_attr.grh.sgid_index << ", sl " << (int)qpa.ah_attr.sl << dendl;
​
  int attr_mask = IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSN | IBV_QP_MIN_RNR_TIMER | IBV_QP_MAX_DEST_RD_ATOMIC;
​
  int r = ibv_modify_qp(qp, &qpa, attr_mask);
  ...
}

RTS配置如下:

代码语言:javascript复制
int Infiniband::QueuePair::modify_qp_to_rts(void)
{
  // move from RTR state RTS
  ibv_qp_attr qpa;
  // FIPS zeroization audit 20191115: this memset is not security related.
  memset(&qpa, 0, sizeof(qpa));
  qpa.qp_state = IBV_QPS_RTS;
  /*
   * How long to wait before retrying if packet lost or server dead.
   * Supposedly the timeout is 4.096us*2^timeout.  However, the actual
   * timeout appears to be 4.096us*2^(timeout 1), so the setting
   * below creates a 135ms timeout.
   */
  qpa.timeout = 0x12;
  // How many times to retry after timeouts before giving up.
  qpa.retry_cnt = 7;
  /*
   * How many times to retry after RNR (receiver not ready) condition
   * before giving up. Occurs when the remote side has not yet posted
   * a receive request.
   */
  qpa.rnr_retry = 7; // 7 is infinite retry.
  qpa.sq_psn = local_cm_meta.psn;
  qpa.max_rd_atomic = 1;
​
  int attr_mask = IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC;
  int r = ibv_modify_qp(qp, &qpa, attr_mask);
  ...
}

ucx建连

代码语言:javascript复制
static ucs_status_t
uct_ud_iface_create_qp(uct_ud_iface_t *self, const uct_ud_iface_config_t *config)
{
    uct_ud_iface_ops_t *ops = ucs_derived_of(self->super.ops, uct_ud_iface_ops_t);
    uct_ib_qp_attr_t qp_init_attr = {};
    struct ibv_qp_attr qp_attr;
    static ucs_status_t status;
    int ret;
​
    qp_init_attr.qp_type             = IBV_QPT_UD;
    qp_init_attr.sq_sig_all          = 0;
    qp_init_attr.cap.max_send_wr     = config->super.tx.queue_len;
    qp_init_attr.cap.max_recv_wr     = config->super.rx.queue_len;
    qp_init_attr.cap.max_send_sge    = config->super.tx.min_sge   1;
    qp_init_attr.cap.max_recv_sge    = 1;
    qp_init_attr.cap.max_inline_data = config->super.tx.min_inline;
​
    status = ops->create_qp(&self->super, &qp_init_attr, &self->qp);
    if (status != UCS_OK) {
        return status;
    }
​
    self->config.max_inline = qp_init_attr.cap.max_inline_data;
​
    memset(&qp_attr, 0, sizeof(qp_attr));
    /* Modify QP to INIT state */
    qp_attr.qp_state   = IBV_QPS_INIT;
    qp_attr.pkey_index = self->super.pkey_index;
    qp_attr.port_num   = self->super.config.port_num;
    qp_attr.qkey       = UCT_IB_KEY;
    ret = ibv_modify_qp(self->qp, &qp_attr,
                        IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_QKEY);
    if (ret) {
        ucs_error("Failed to modify UD QP to INIT: %m");
        goto err_destroy_qp;
    }
​
    /* Modify to RTR */
    qp_attr.qp_state = IBV_QPS_RTR;
    ret = ibv_modify_qp(self->qp, &qp_attr, IBV_QP_STATE);
    if (ret) {
        ucs_error("Failed to modify UD QP to RTR: %m");
        goto err_destroy_qp;
    }
​
    /* Modify to RTS */
    qp_attr.qp_state = IBV_QPS_RTS;
    qp_attr.sq_psn = 0;
    ret = ibv_modify_qp(self->qp, &qp_attr, IBV_QP_STATE | IBV_QP_SQ_PSN);
    ...
}

nccl建连

代码语言:javascript复制
​
ncclResult_t ncclIbConnect(int dev, void* opaqueHandle, void** sendComm)
    if (stage->state == ncclIbCommStateSend)       goto ib_send;
    NCCLCHECK(ncclIbInitVerbs(dev, ctx, &comm->verbs))
    ...
    ncclIbCreateQp
rdma:
ncclResult_t ncclIbCreateQp
    qpInitAttr.qp_type = IBV_QPT_RC
    qpInitAttr.cap.max_send_wr = 2*MAX_REQUESTS -> 128
    qpInitAttr.cap.max_recv_wr = MAX_REQUESTS -> 64
    wrap_ibv_modify_qp
    ...
    
ncclResult_t ncclIbCreateQp(uint8_t ib_port, struct ncclIbVerbs* verbs, int access_flags, struct ibv_qp** qp) {
  struct ibv_qp_init_attr qpInitAttr;
  memset(&qpInitAttr, 0, sizeof(struct ibv_qp_init_attr));
  qpInitAttr.send_cq = verbs->cq;
  qpInitAttr.recv_cq = verbs->cq;
  qpInitAttr.qp_type = IBV_QPT_RC;
  // We might send 2 messages per send (RDMA and RDMA_WITH_IMM)
  qpInitAttr.cap.max_send_wr = 2*MAX_REQUESTS;
  qpInitAttr.cap.max_recv_wr = MAX_REQUESTS;
  qpInitAttr.cap.max_send_sge = 1;
  qpInitAttr.cap.max_recv_sge = 1;
  qpInitAttr.cap.max_inline_data = ncclParamIbUseInline() ? sizeof(struct ncclIbSendFifo) : 0;
  NCCLCHECK(wrap_ibv_create_qp(qp, verbs->pd, &qpInitAttr));
  struct ibv_qp_attr qpAttr;
  memset(&qpAttr, 0, sizeof(struct ibv_qp_attr));
  qpAttr.qp_state = IBV_QPS_INIT;
  qpAttr.pkey_index = ncclParamIbPkey();
  qpAttr.port_num = ib_port;
  qpAttr.qp_access_flags = access_flags;
  NCCLCHECK(wrap_ibv_modify_qp(*qp, &qpAttr, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS));
  return ncclSuccess;
}

RTR:

代码语言:javascript复制
ncclResult_t ncclIbRtrQp(struct ibv_qp* qp, uint32_t qpn, struct ncclIbQpInfo* info) {
  struct ibv_qp_attr qpAttr;
  memset(&qpAttr, 0, sizeof(struct ibv_qp_attr));
  qpAttr.qp_state = IBV_QPS_RTR;
  qpAttr.path_mtu = info->mtu;
  qpAttr.dest_qp_num = qpn;
  qpAttr.rq_psn = 0;
  qpAttr.max_dest_rd_atomic = 1;
  qpAttr.min_rnr_timer = 12;
  if (info->link_layer == IBV_LINK_LAYER_ETHERNET) {
    qpAttr.ah_attr.is_global = 1;
    qpAttr.ah_attr.grh.dgid.global.subnet_prefix = info->spn;
    qpAttr.ah_attr.grh.dgid.global.interface_id = info->iid;
    qpAttr.ah_attr.grh.flow_label = 0;
    qpAttr.ah_attr.grh.sgid_index = ncclParamIbGidIndex();
    qpAttr.ah_attr.grh.hop_limit = 255;
    qpAttr.ah_attr.grh.traffic_class = ncclParamIbTc();
  } else {
    qpAttr.ah_attr.is_global = 0;
    qpAttr.ah_attr.dlid = info->lid;
  }
  qpAttr.ah_attr.sl = ncclParamIbSl();
  qpAttr.ah_attr.src_path_bits = 0;
  qpAttr.ah_attr.port_num = info->ib_port;
  NCCLCHECK(wrap_ibv_modify_qp(qp, &qpAttr, IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER));
  return ncclSuccess;
}

RTS:

代码语言:javascript复制
ncclResult_t ncclIbRtsQp(struct ibv_qp* qp) {
  struct ibv_qp_attr qpAttr;
  memset(&qpAttr, 0, sizeof(struct ibv_qp_attr));
  qpAttr.qp_state = IBV_QPS_RTS;
  qpAttr.timeout = ncclParamIbTimeout();
  qpAttr.retry_cnt = ncclParamIbRetryCnt();
  qpAttr.rnr_retry = 7;
  qpAttr.sq_psn = 0;
  qpAttr.max_rd_atomic = 1;
  NCCLCHECK(wrap_ibv_modify_qp(qp, &qpAttr, IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC));
  return ncclSuccess;
}

还有其他的项目如:spdk, kernel也可作为参考

intel e810 rdma-core驱动

代码语言:javascript复制
ibv_modify_qp
.modify_qp = irdma_umodify_qp,
    ibv_cmd_modify_qp_ex
        copy_modify_qp_fields
            ...
            cmd->retry_cnt = attr->retry_cnt
            ...
        execute_cmd_write_ex IB_USER_VERBS_EX_CMD_MODIFY_QP -> 通过ABI接口转到内核处理
    irdma_mmap
    or ibv_cmd_modify_qp

内核处理

代码语言:javascript复制
ibv_modify_qp -> IB_USER_VERBS_EX_CMD_MODIFY_QP -> ib_uverbs_ex_modify_qp
    modify_qp
        uobj_get_obj_read UVERBS_OBJECT_QP
        校验参数
        ...
        ib_modify_qp_with_udata
            _ib_modify_qp -> IB/core:将 DMAC 解析限制为用户空间 QP,当前 ah_attr 由 ib_cm 层针对基于 rdma_cm 的应用程序进行初始化。 对于 RoCE 传输,ah_attr.roce.dmac 已由 ib_cm、rdma_cm 从 wc、路径记录、路由解析、显式路径记录设置(取决于主动方或被动方 QP)初始化。 因此避免为内核消费者的 QP 解析 DMAC
                attr_mask & IB_QP_AV -> IBV_QP_AV,主要用来指示内核做地址解析,对于RoCE,则进行L3到MAC地址的转换
                rdma_fill_sgid_attr
                    rdma_check_ah_attr
                    rdma_ah_retrieve_grh
                    rdma_get_gid_attr
                        rdma_gid_table
                        get_gid_entry
                ib_resolve_eth_dmac -> 调用ib_resolve_eth_dmac解析remote gid对应的MAC地址
                    ...
                    rdma_resolve_ip resolve_cb
                        complete -> IB/core:verbs/cm 结构中的以太网 L2 属性 ,此补丁添加了对 verbs/cm/cma 结构中的以太网 L2 属性的支持。 在处理 L2 以太网时,我们应该以与使用 IB L2(和 L4 PKEY)属性类似的方式使用 smac、dmac、vlan ID 和优先级。 因此,这些属性被添加到以下结构中: * ib_ah_attr - 添加了 dmac * ib_qp_attr - 添加了 smac 和 vlan_id,(sl 保留 vlan 优先级) * ib_wc - 添加了 smac、vlan_id * ib_sa_path_rec - 添加了 smac、dmac、vlan_id * cm_av - 添加了 smac 和 vlan_id 对于路径记录结构,在将其打包为有线格式时特别注意避免新字段,因此我们不会破坏 IB CM 和 SA 有线协议。 在主动侧,CM 被填充。 其内部结构来自 ULP 提供的路径。 我们添加了 ETH L2 属性并将它们放入 CM 地址句柄(struct cm_av)中。 在被动侧,CM 从与 REQ 消息关联的 WC 中填充其内部结构。 我们添加了从 WC 获取 ETH L2 属性的内容。 当硬件驱动程序在 WC 中提供所需的 ETH L2 属性时,它们会设置 IB_WC_WITH_SMAC 和 IB_WC_WITH_VLAN 标志。 IB 核心代码检查这些标志是否存在,如果没有,则从 ib_init_ah_from_wc() 辅助函数进行地址解析。 ib_modify_qp_is_ok 也被更新以考虑链路层。 有些参数对于以太网链路层是必需的,而对于IB来说则无关。 修改供应商驱动程序以支持新的函数签名
                rdma_lag_get_ah_roce_slave
                    rdma_read_gid_attr_ndev_rcu
                    rdma_get_xmit_slave_udp
                        rdma_build_skb
                        netdev_get_xmit_slave RDMA_LAG_FLAGS_HASH_ALL_SLAVES
                rdma_counter_bind_qp_auto
                ib_security_modify_qp -> IB/核心:在 QP 上强制执行 PKey 安全性,添加新的 LSM 挂钩以分配和释放安全上下文并检查访问 PKey 的权限。 创建和销毁 QP 时分配和释放安全上下文。 此上下文用于控制对 PKey 的访问。 当请求修改 QP 来更改端口、PKey 索引或备用路径时,请检查 QP 是否具有对该端口子网前缀上的 PKey 表索引中的 PKey 的权限。 如果 QP 是共享的,请确保 QP 的所有句柄也具有访问权限。 存储 QP 正在使用的端口和 PKey 索引。 重置到初始化转换后,用户可以独立修改端口、PKey 索引和备用路径。 因此,端口和 PKey 设置更改可以是先前设置和新设置的合并。 为了在 PKey 表或子网前缀更改时维持访问控制,请保留每个端口上使用每个 PKey 索引的所有 QP 的列表。 如果发生更改,则使用该设备和端口的所有 QP 都必须强制执行新缓存设置的访问权限。 这些更改将事务添加到 QP 修改过程中。 如果修改失败,则必须保持与旧端口和 PKey 索引的关联;如果修改成功,则必须将其删除。 必须在修改之前建立与新端口和 PKey 索引的关联,如果修改失败则将其删除。 1. 当 QP 被修改为特定端口时,PKey 索引或备用路径将该 QP 插入到适当的列表中。 2. 检查访问新设置的权限。 3. 如果步骤 2 授予访问权限,则尝试修改 QP。 4a. 如果步骤 2 和 3 成功,则删除任何先前的关联。 4b. 如果以太失败,请删除新的设置关联。 如果 PKey 表或子网前缀发生更改,则遍历 QP 列表并检查它们是否具有权限。 如果没有,则将 QP 发送到错误状态并引发致命错误事件。 如果它是共享 QP,请确保共享 real_qp 的所有 QP 也具有权限。 如果拥有安全结构的 QP 被拒绝访问,则安全结构将被标记为此类,并且 QP 将被添加到 error_list 中。 一旦将 QP 移至错误完成,安全结构标记就会被清除。 正确维护列表会将 QP 销毁转变为事务。 设备的硬件驱动程序释放 ib_qp 结构,因此当销毁正在进行时,ib_qp_security 结构中的 ib_qp 指针未定义。 当销毁过程开始时,ib_qp_security 结构被标记为正在销毁。 这可以防止对 QP 指针采取任何操作。 QP 成功销毁后,它仍然可以列在 error_list 上,等待该流处理它,然后再清理结构。 如果销毁失败,则 QP 端口和 PKey 设置将重新插入到适当的列表中,销毁标志将被清除,并强制执行访问控制,以防在销毁流程期间发生任何缓存更改。 为了保持安全更改隔离,使用新文件来保存与安全相关的功能
                    port_pkey_list_insert -> 在检查权限之前,将此 QP 添加到新端口和 pkey 设置的列表中,以防发生并发缓存更新。 遍历列表进行缓存更改不会获取安全互斥体,除非将 QP 发送到错误
                    check_qp_port_pkey_settings
                        get_pkey_and_subnet_prefix
                            ib_get_cached_pkey
                            ib_get_cached_subnet_prefix
                        enforce_qp_pkey_security
                    modify_qp -> .modify_qp = irdma_modify_qp_roce
                rdma_lag_put_ah_roce_slave
        release_qp:
        rdma_lookup_put_uobject
...
irdma_modify_qp_roce
    irdma_query_pkey
    rdma_get_udp_sport -> 根据 grh.flow_label 或 lqpn/rqrpn 获取 QP 的源 udp 端口号。 这可以更好地跨 NIC RX 队列传播流量
    irdma_qp_rem_qos
    dev->ws_remove
    rdma_read_gid_l2_fields
    irdma_roce_get_vlan_prio
        vlan_dev_get_egress_qos_mask
    irdma_qp_add_qos
        list_add(&qp->list, &vsi->qos[qp->user_pri].qplist)
    ib_modify_qp_is_ok
        qp_state_table[cur_state][next_state].valid) -> IB:添加 ib_modify_qp_is_ok() 库函数,内核中的 mthca 驱动程序包含一个表,其中的属性对于每个队列对状态转换都有效。 事实证明,正在准备合并的其他两个 IB 驱动程序(ipath 和 ehca)都复制了该表、错误等。 为了防止代码重复,请将此表和用于检查参数的代码移动到中间层库函数 ib_modify_qp_is_ok() 中
        qp状态表及参数: qp_state_table[IB_QPS_ERR   1][IB_QPS_ERR   1]
    switch (attr->qp_state) -> QP状态机
    ...
    irdma_flush_wqes
        irdma_hw_flush_wqes
            cqp_request->callback_fcn = irdma_hw_flush_wqes_callback
            cqp_info->cqp_cmd = IRDMA_OP_QP_FLUSH_WQES
            irdma_handle_cqp_op -> cqp 控制QP
                irdma_process_cqp_cmd
                    irdma_exec_cqp_cmd
                        switch (pcmdinfo->cqp_cmd) -> 判断控制QP命令类型
                        case IRDMA_OP_CEQ_CREATE -> 驱动程序将特权命令发布到硬件管理队列(控制 QP 或 CQP),以请求硬件执行管理操作。 实现 CQP 的创建/销毁以及支持函数、数据结构和标头以处理不同的 CQP 命令
                            irdma_sc_cq_create
                                irdma_sc_cqp_get_next_send_wqe
                                set_64bit_val(wqe, 0, cq->cq_uk.cq_size) -> 按位设置其值(每8字节)
                                dma_wmb()
                                irdma_sc_cqp_post_sq
                                    writel(IRDMA_RING_CURRENT_HEAD(cqp->sq_ring), cqp->dev->cqp_db) -> 写寄存器(往内存映射芯片之后的I/O 空间上写4字节数据)
                        ...
    irdma_sc_qp_setctx_roce
void irdma_sc_qp_setctx_roce(struct irdma_sc_qp *qp, __le64 *qp_ctx,
    set_64bit_val rq_wqe_size
    ...

总结

  • 业务层ceph/ucx/nccl/spdk/kernel等调用ibv_modify_qp, 修改QP状态
  • intel驱动完成参数校验, 封装命令, 解析命令, QP状态机转换, 最后写入芯片寄存器(逻辑电路开始工作)
  • 通过研究rdma驱动代码, 可以拓宽技术面, 学习编程技巧, 以及加深对IB规范的理解, 先理解和跟随, 然后结合需求做微创新, 这也是一条很好的技术路线

参考

mojo ibv_modify_qp: https://www.rdmamojo.com/2013/01/12/ibv_modify_qp/

QP状态机: https://www.rdmamojo.com/2012/05/05/qp-state-machine/

带外建连: https://zhuanlan.zhihu.com/p/655663006

翻译版本: https://blog.csdn.net/bandaoyu/article/details/115764839

IBM ibv_modify_qp: https://www.ibm.com/docs/zh-tw/aix/7.3?topic=management-ibv-modify-qp

RDMA中的QP: https://www.cnblogs.com/zafu/p/11587457.html

晓兵(ssbandjl)

博客: https://cloud.tencent.com/developer/user/5060293/articles | https://logread.cn | https://blog.csdn.net/ssbandjl | https://www.zhihu.com/people/ssbandjl/posts

DPU专栏

https://cloud.tencent.com/developer/column/101987

0 人点赞