RDMA-In-Practice[1] MTU&SL&ODP&CC

可以看到,在libverbs的pingpong示例中,还有如下图所示许多可供选择与调整的选项,这篇博客在上一篇的基础上探究与实现一下未涉及到的选项。

image-20231201204810040

MTU

在建立QP间的链接时可以通过设定ibv_qp_attr.path_mtu来控制QP间一次通信最大可以传输的数据包大小,这个大小包含了协议头、元数据以及实际载荷。

此处设定的MTU不能超过链路支持的最大的MTU,后者可以通过ibv_devinfo命令进行查询。如果是在程序中,下图这些信息可以通过函数ibv_query_port查询得到。

image-20231201213527815

要在RDMAEndpoint程序中支持变更MTU只需从命令行参数读取并在连接QP时设定即可。

Service Level

关于SL的详细内容可以参考InfiniBandFAQ_FQ_100中的第16个问题的回答:

image-20231204174316599

Service Level是用来指定数据包的QoS等级。InfiniBand通过创建多条虚拟路径Virtual Lanes来支持QoS,每条VL即为一条独立的逻辑通信链路,所有VL共享同一条物理链路。每个物理链路最多支持15个标准的VL(VL0到VL14)和一条管理路径(VL15),优先级从VL15到VL0递减。

通过设定Service Level可以为链路指定通信优先级,最终会由通信路径上的交换机或路由器将SL转换为VL。

SL在创建QP时设定,这对高性能的程序可能不太有用,但也先加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ibv_qp_attr attr = {
.qp_state = IBV_QPS_RTR,
.path_mtu = this->mtu,
.rq_psn = this->remoteIBAddr.psn,
.dest_qp_num = this->remoteIBAddr.qpn,
.ah_attr = {
.dlid = this->remoteIBAddr.lid,
.sl = this->ibSL,
.src_path_bits = 0,
.is_global = 0,
.port_num = this->ibPort
},
.max_dest_rd_atomic = 1,
.min_rnr_timer = 12
};

...

if (ibv_modify_qp(this->ibQP, &attr, ...)) {
...
}

On Demand Paging

ODP也有一篇原Mellanox的技术博客可以参考:UNDERSTANDING ON DEMAND PAGING (ODP)。ODP技术被用来简化内存注册,这里是一个介绍ODP出现的幻灯片。借助ODP技术,应用程序无需确定用户注册的虚拟地址位于物理页的实际位置,在RDMA杂谈对MR的介绍中我们直到传统情况下本地的HCA是需要维护VA->PA的映射表的,如下图所示,且注册MR时需要通过锁页保证VA->PA的映射不会更改。而使用ODP后HCA不再需要维护映射表,而是当页面不存在时向操作系统请求新的VA->PA的转化。

image-20231204221222350

在使用中,On Demand Paging可以有显示和隐式两种使用方式,显示方式是针对对特定缓冲区,而隐式方式则是注册程序的整个虚地址空间:

1
2
3
4
5
6
7
access_flags |= IBV_ACCESS_ON_DEMAND;

// 隐式
ctx->mr = ibv_reg_mr(ctx->pd, NULL, SIZE_MAX, access_flags);

// 显示
ibv_reg_mr(ctx->pd, ctx->buf, size, access_flags);

我本地环境中似乎不支持隐式使用过程中的一些环节(跟libverbs的官方代码相比),因此支持实现了普通的On Demand Paging。

Completion Channel

completion channel的概念可以参考RDMAmojo的一篇文章,它是由libibverbs引入的一个抽象。使用完成事件通道可以将改变编程模式,替代原本基于轮询的查询completion的模式。同时借助完成事件通道还可以把一个完成事件发送给多个线程或赋予不同完成事件不同的优先级。

使用Completion Event Channel首先要初始化出一个通道:

1
2
3
4
5
6
7
8
9
if (this->useEvent) {
this->ibCompChannel = ibv_create_comp_channel(this->ibCtx);
if (this->ibCompChannel == nullptr) {
ibv_close_device(this->ibCtx);
ibv_free_device_list(devList);
this->endpointStatus = EndpointStatus::FAIL;
throw std::runtime_error("Fail to create completion channel");
}
}

在提交了request后通过ibv_req_notify_cq通知完成队列的频道,当请求完成后会有一个事件被写入该队列:

1
2
3
4
5
if (this->useEvent) {
if (ibv_req_notify_cq(this->ibCQ, 0)) {
throw std::runtime_error("Couldn't request CQ notification");
}
}

调用ibv_get_cq_event可以阻塞等待cq到对应的cq中work request的完成,然后即可正常通过ibv_poll_cq拉取出来。ibv_ack_cq_events可以告知completion channel n个事件已被处理。

1
2
3
4
5
6
7
8
9
10
11
if (this->useEvent) {
ibv_cq *ev_cq;
void *ev_ctx;
if (ibv_get_cq_event(this->ibCompChannel, &ev_cq, &ev_ctx)) {
throw std::runtime_error("Fail to get CQ event.");
}
ibv_ack_cq_events(ev_cq, 1);
if (ibv_req_notify_cq(ev_cq, 0)) {
throw std::runtime_error("Fail to request CQ notification.");
}
}

最终在finalize的时候释放即可:

1
if (this->useEvent) ibv_destroy_comp_channel(this->ibCompChannel);