RDMA介绍
RDMA优势
- SOCKET: 通过网卡中断通知,在协议栈实现封包解包,需要内存拷贝才能到达用户空间
- DPDK:
通过cpu轮询获知报文到达,将协议栈上移到用户空间,报文由网卡直接DMA到用户空间,虽然实现了零拷贝,但仍然需要cpu做封包解包
RDMA:
将协议栈下移到网卡,内核bypass,零拷贝,低cpu消耗等
RDMA标准
RDMA标准有三种协议实现:IB, ROCE, IWARP
RDMA基本概念
- WQ: work queue,工作队列,应用程序是生成者,网卡是消费者
- WQE: work queue elment,工作队列元素
- QP: queue pair, 队列对,包括SQ和RQ
- SQ: receive queue, 发送队列,属于WQ
- RQ: receive queue, 接收队列,属于WQ
- CQ: completion queue, 完成队列,网卡是生产者,应用程序是消费者
- CQE: completion queue element, 完成队列元素
- WR: work request,工作请求,ibvers api中的参数类型,最终会转换成WQE
- WC: work completion, 工作完成,ibvers api中的参数类型,保存CQE信息
RDMA操作类型
send/recv双端操作(通信需两端cpu参与)
send端需指定本端buffer地址和lkey,网卡从此buffer读取数据封装后发送。不用指定远端buffer信息。
struct ibv_sge sg;
struct ibv_send_wr wr;
struct ibv_send_wr *bad_wr;
memset(&sg, 0, sizeof(sg));
sg.addr = (uintptr_t)buf_addr;
sg.length = buf_size;
sg.lkey = mr->lkey;
memset(&wr, 0, sizeof(wr));
wr.wr_id = 0;
wr.sg_list = &sg;
wr.num_sge = 1;
wr.opcode = IBV_WR_SEND;
wr.send_flags = IBV_SEND_SIGNALED;
if (ibv_post_send(qp, &wr, &bad_wr)) {
fprintf(stderr, "Error, ibv_post_send() failed\n");
return -1;
}
recv端指定本端buffer地址和lkey,网卡将报文写到此bufer
struct ibv_sge sg;
struct ibv_recv_wr wr;
struct ibv_recv_wr *bad_wr;
memset(&sg, 0, sizeof(sg));
sg.addr = (uintptr_t)buf_addr;
sg.length = buf_size;
sg.lkey = mr->lkey;
memset(&wr, 0, sizeof(wr));
wr.wr_id = 0;
wr.sg_list = &sg;
wr.num_sge = 1;
if (ibv_post_recv(qp, &wr, &bad_wr)) {
fprintf(stderr, "Error, ibv_post_recv() failed\n");
return -1;
}
send端和recv端都会生成CQE
write/read/atomic单端操作(通信不需要远端cpu参与)
不仅需指定本端buffer地址和lkey,也要指定远端buffer地址和rkey。
struct ibv_sge sg;
struct ibv_send_wr wr;
struct ibv_send_wr *bad_wr;
memset(&sg, 0, sizeof(sg));
sg.addr = (uintptr_t)buf_addr; //本端地址,长度和key
sg.length = buf_size;
sg.lkey = mr->lkey;
memset(&wr, 0, sizeof(wr));
wr.wr_id = 0;
wr.sg_list = &sg;
wr.num_sge = 1;
wr.opcode = IBV_WR_RDMA_WRITE;
wr.send_flags = IBV_SEND_SIGNALED;
wr.wr.rdma.remote_addr = remote_address //远端地址和key
wr.wr.rdma.rkey = remote_key;
if (ibv_post_send(qp, &wr, &bad_wr)) {
fprintf(stderr, "Error, ibv_post_send() failed\n");
return -1;
}
只有发送端会生成CQE,通信前需通过socket或者CM获取接收端buffer虚拟地址和rkey,通信后需告知接收端数据已发送
RDMA服务类型
根据可靠性,是否面向连接分为如下几种服务类型
- 可靠: 应答机制,数据校验和保序机制
- 面向连接: 每个QP在初始化时就和目的QP建立关联则称为连接,每个QP发送数据时才指定目的QP则称为数据报
RDMA MR注册
用户空间申请buffer后需要将对应的内存进行注册,注册MR的作用:
- 创建VA到PA的映射表,不管读本端数据还是远端网卡将数据写到远端,网卡都需要根据VA找到对应的PA
- 控制访问权限,注册时会生成lkey和rkey,控制本端和远端访问内存权限
- 避免换页,防止VA到PA映射关系变化
RDMA建连
面向连接的服务类型需要在本端和远端QP之间建立连接后才能通过RDMA进行数据交换,建连需要交换如下信息
- GID: global identifier, 每个网卡的全局唯一标识 (通过命令show_gids查看)
- QPN: queue paire number, 每个qp标识
- VA: virtual address,远端虚拟地址
- RKEY: remote key,远端虚拟地址的key
- PSN: packet sequence number, 初始序列号,接收端根据PSN判断是否乱序
建连两种方式
- 基于socket的带外建连方式。NCCL使用此方式
- 基于CM的带内建连方式,使用QP 1交互,有专属的报文格式,交互流程和用户接口
RDMA API
支持librdmacm和libibvers两种api,这里以libibvers api为例
控制面api(需陷入内核态执行)
获取RDMA设备列表: ibv_get_device_list
打开指定的RDMA设备: ibv_open_device
查询RDMA设备属性: ibv_query_device
申请pd: ibv_alloc_pd
注册MR: ibv_reg_mr
创建CQ: ibv_create_cq
创建QP: ibv_create_qp
修改QP属性: ibv_modify_qp
数据面api(不需要陷入内核态执行)
- 下发发送端wr(异步): int ibv_post_send(struct ibv_qp *qp, struct ibv_send_wr *wr, struct ibv_send_wr **bad_wr)
struct ibv_send_wr {
uint64_t wr_id;
struct ibv_send_wr *next;
struct ibv_sge *sg_list; //指定本端buffer地址
int num_sge;
enum ibv_wr_opcode opcode; //操作类型: write/read/atomic等
int send_flags;
uint32_t imm_data; /* in network byte order */
union {
struct {
uint64_t remote_addr; //远端buffer地址
uint32_t rkey; //远端buffer key
} rdma;
struct {
uint64_t remote_addr;
uint64_t compare_add;
uint64_t swap;
uint32_t rkey;
} atomic;
struct {
struct ibv_ah *ah;
uint32_t remote_qpn;
uint32_t remote_qkey;
} ud;
} wr;
...
};
struct ibv_sge {
uint64_t addr;
uint32_t length;
uint32_t lkey;
};
如何知道下发的wr已完成?
如果通过ibv_modify_qp修改qp时ibv_qp_init_attr.sq_sig_all设置为1,则每个wr都会生成CQE,如果为0,则根据wr.send_flags决定,如果设置了IBV_SEND_SIGNALED则此wr生成CQE,最后通过ibv_poll_cq获取CQE,CQE中包含wr完成情况
- 下发接收端recv wr(异步): int ibv_post_recv(struct ibv_qp *qp, struct ibv_recv_wr *wr, struct ibv_recv_wr **bad_wr)
struct ibv_recv_wr {
uint64_t wr_id;
struct ibv_recv_wr *next;
struct ibv_sge *sg_list; //指定本端buffer地址
int num_sge;
};
如何知道下发的rwr已完成?
发送端发送如下请求时,会触发接收端生成CQE,最后通过ibv_poll_cq获取CQE,CQE中包含rwr完成情况
Send
Send with Immediate
RDMA Write with immediate
- 获取wc: int ibv_poll_cq(struct ibv_cq *cq, int num_entries, struct ibv_wc *wc)
struct ibv_wc {
uint64_t wr_id; //wr_id标识
enum ibv_wc_status status; //成功或者失败
enum ibv_wc_opcode opcode;
uint32_t vendor_err;
uint32_t byte_len;
uint32_t imm_data; /* in network byte order */
uint32_t qp_num;
uint32_t src_qp;
int wc_flags;
uint16_t pkey_index;
uint16_t slid;
uint8_t sl;
uint8_t dlid_path_bits;
};
RDMA传输头
基础传输头
每个roce报文都包含此头部
- opcode: 操作类型,write first/write middle/write last/write only等
- destination QP: 目的qpn
- A: ackreq, 是否需要对端回复ack,如果报文分片只有最后一个报文设置为1,中间的报文不需要ack
- PSN: 报文序号,接收端根据PSN判断是否乱序
RETH扩展传输头
指定了远端虚拟地址,key和此报文传输的payload长度。write first/write only类型报文包含此头部
AETH扩展传输头
接收端响应报文,告知发送端数据是否正常
syndrome的bit5:6代表ACK或NAK
bit5:6为11表示NAK,说明有错误发送,bit4:4标识错误原因
MTU
MTU支持设置为256, 512, 1024, 2048和4096,发送数据长度超过MTU将被分片。以write 3072字节数据为例,网卡mtu为1024,报文被分成如下三种。
- write first报文包含BTH和RETH头,其中RETH指定了远端地址和key
- write middle报文只包含BTH头
- write last报文只包含BTH头,并且BTH的reqack设置为1,接收端只会对此报文响应ack
如果数据长度小于MTU则不分片,报文类型为write only,每个报文包含BTH和RETH头,其中BTH的reqack为1,RETH指定远端地址和key
自适应路由AR
打开AR功能
mlxreg -d 01:00.1 --reg_name ROCE_ACCL --set adaptive_routing_forced_en=1
查看AR状态
# mlxreg -d 01:00.1 --reg_name ROCE_ACCL --get
Sending access register...
Field Name | Data
============================================================
roce_adp_retrans_field_select | 0x00000001
roce_tx_window_field_select | 0x00000001
roce_slow_restart_field_select | 0x00000001
roce_slow_restart_idle_field_select | 0x00000001
min_ack_timeout_limit_disabled_field_select | 0x00000001
adaptive_routing_forced_en_field_select | 0x00000001
selective_repeat_forced_en_field_select | 0x00000001
dc_half_handshake_en_field_select | 0x00000000
ack_dscp_force_field_select | 0x00000000
roce_adp_retrans_en | 0x00000001
roce_tx_window_en | 0x00000000
roce_slow_restart_en | 0x00000001
roce_slow_restart_idle_en | 0x00000000
min_ack_timeout_limit_disabled | 0x00000000
adaptive_routing_forced_en | 0x00000001 -->使能
selective_repeat_forced_en | 0x00000000
dc_half_handshake_en | 0x00000000
ack_dscp_force | 0x00000000
ack_dscp | 0x00000000
============================================================
使能AR后,每个报文变化如下
- opcode为write only
- 包含RETH头,指定远端地址和key,让报文直接写到相应内存地址,无需做乱序重排(DDP技术实现乱序接收,按序提交)
- BTH头第9字节最高位置1,交换机可根据此标志区分ar报文
重传机制
没使能AR时,数据传输过程中,丢失其中一个分片,接收端根据报文中的PSN判断有乱序,回复NAK,指示哪个分片丢失(BTH头中的PSN),发送端收到NAK报文,重传分片及其后面所有报文。
例如网卡mtu为1024,write两次数据,每次write 3072字节数据,报文PSN为:0-5,丢失PSN1后,接收端发送NAK报文,BTH头中的PSN字段指示PSN1丢失,发送端重发PSN 1-5
使能AR后,乱序报文是预期的,接收端维护接收窗口,接收窗口内的乱序报文超时后才回复NAK报文。
超时重传
发送数据后,接收端未响应ack,或者ack丢失,发送端会重传最新ack后的所有报文,超时时间和重传次数由如下变量指定,在调用ibv_modify_qp时设置
struct ibv_qp_attr {
uint8_t timeout;
uint8_t retry_cnt;
}
timeout值和实际时间对应关系,参考https://www.rdmamojo.com/2013/01/12/ibv_modify_qp/
0 - infinite
1 - 8.192 usec (0.000008 sec)
2 - 16.384 usec (0.000016 sec)
3 - 32.768 usec (0.000032 sec)
4 - 65.536 usec (0.000065 sec)
5 - 131.072 usec (0.000131 sec)
6 - 262.144 usec (0.000262 sec)
7 - 524.288 usec (0.000524 sec)
8 - 1048.576 usec (0.00104 sec)
9 - 2097.152 usec (0.00209 sec)
10 - 4194.304 usec (0.00419 sec)
11 - 8388.608 usec (0.00838 sec)
12 - 16777.22 usec (0.01677 sec)
13 - 33554.43 usec (0.0335 sec)
14 - 67108.86 usec (0.0671 sec)
15 - 134217.7 usec (0.134 sec)
16 - 268435.5 usec (0.268 sec)
17 - 536870.9 usec (0.536 sec)
18 - 1073742 usec (1.07 sec)
19 - 2147484 usec (2.14 sec)
20 - 4294967 usec (4.29 sec)
21 - 8589935 usec (8.58 sec)
22 - 17179869 usec (17.1 sec)
23 - 34359738 usec (34.3 sec)
24 - 68719477 usec (68.7 sec)
25 - 137000000 usec (137 sec)
26 - 275000000 usec (275 sec)
27 - 550000000 usec (550 sec)
28 - 1100000000 usec (1100 sec)
29 - 2200000000 usec (2200 sec)
30 - 4400000000 usec (4400 sec)
31 - 8800000000 usec (8800 sec)
参考
RDMA杂谈:https://zhuanlan.zhihu.com/p/164908617
IB规范: https://www.afs.enea.it/asantoro/V1r1_2_1.Release_12062007.pdf
RDMA在NCCL中的应用
GPU channel个数及网卡qp个数
集合通信(初始化时确定channel个数,并创建qp)
每个rank的channel的个数:nChannels=ROCE网卡个数*2
每个网卡创建的qp个数:出现在channel中的次数
NCCL INFO 0 : NET/0 GPU/0 GPU/1 GPU/2 GPU/3 GPU/4 GPU/5 GPU/6 GPU/7 NET/0
NCCL INFO 1 : NET/1 GPU/1 GPU/7 GPU/6 GPU/5 GPU/4 GPU/0 GPU/3 GPU/2 NET/1
NCCL INFO 2 : NET/2 GPU/2 GPU/7 GPU/6 GPU/5 GPU/4 GPU/1 GPU/0 GPU/3 NET/2
NCCL INFO 3 : NET/3 GPU/3 GPU/7 GPU/6 GPU/5 GPU/4 GPU/2 GPU/1 GPU/0 NET/3
NCCL INFO 4 : NET/4 GPU/4 GPU/3 GPU/2 GPU/1 GPU/0 GPU/7 GPU/6 GPU/5 NET/4
NCCL INFO 5 : NET/5 GPU/5 GPU/3 GPU/2 GPU/1 GPU/0 GPU/4 GPU/7 GPU/6 NET/5
NCCL INFO 6 : NET/6 GPU/6 GPU/3 GPU/2 GPU/1 GPU/0 GPU/5 GPU/4 GPU/7 NET/6
NCCL INFO 7 : NET/7 GPU/7 GPU/3 GPU/2 GPU/1 GPU/0 GPU/6 GPU/5 GPU/4 NET/7
点对点通信(初始化时确定channel个数,首次调用send/recv时创建qp)
每个rank可用channel个数为:p2pnChannels=nChannels
每个rank和peer的channel个数为:p2pnChannelsPerPeer=2
每个网卡创建的qp个数:跨node peer个数p2pnChannelsPerPeer2,其中2指的是send/recv均需要创建qp
上述两种情况的qp创建个数默认为1,可通过环境变量NCCL_IB_QPS_PER_CONNECTION修改
RDMA初始化
RDMA初始化发生在创建transport时,具体流程如下(以rank0发送,rank1接收为例)
a. rank1创建proxy progress线程,用于后续数据交互
b. rank1创建socket,并将监听ip和端口发给rank0
c. rank0创建proxy progress线程,用于后续数据交互
d. rank0通过监听ip和端口连接rank1,执行RDMA初始化流程创建cq,qp,注册fifo,将本端gid/qpn等信息发给rank1
e. rank1接收rank0的gid/qpn等信息,执行RDMA初始化流程创建cq,qp,注册fifo和modify qp,将本端gid/qpn等信息发给rank0
f. rank0接收rank1的gid/qpn等信息,modify qp
g. rank0和rank申请通信buffer,并注册到网卡
RDMA数据处理流程
数据发送流程如下
a. GPU1将发送的数据放入通信buffer,并通过fifo1通知send proxy
b. rank0 proxy progress thread通过fifo2从远端获知remote_addr和remote_key,并调用ncclIbIsend通知NIC1发送数据
c. NIC1从GPU1的通信buffer读数据进行发送
d. NIC2收到数据后,将数据写到GPU2的通信buffer中
e. rank1 proxy progress thread通过ncclIbIrecv获知数据接收成功,通过fifo3通知GPU2接收到数据
例如发送4096字节数据报文,网卡会根据mtu 1024将数据分成四个报文发送,最后一个报文类型为write immediate(触发接收端生成CQE)
自适应路由AR
除了网卡使能ar,nccl还需要满足两个条件
- 设置环境变量NCCL_IB_ADAPTIVE_ROUTING
- 发送数据长度大于环境变量NCCL_IB_AR_THRESHOLD指定的值(默认8192)
使能ar后,除了post一个write类型的wr,还会post一个write immediate类型的wr,触发接收端生成CQE,以此来通知接收端
例如使能ar时发送4096字节数据报文(还会额外发送一个write immediateb报文,指定发送数据长度),网卡会根据mtu 1024将4096字节的数据分成四个报文(write only)发送(52 ack是对54报文的响应,抓包乱序了),最后一个报文类型为write immediate
重传机制
乱序重传
没使能AR时,接收端收到乱序报文就回复NAK通知发送端重传;
使能AR后,乱序报文是预期的,接收端维护接收窗口,接收窗口内的乱序报文超时后才回复NAK报文。
超时重传
nccl中通过如下环境变量设置超时时间和重试次数
NCCL_PARAM(IbTimeout, "IB_TIMEOUT", 18);
NCCL_PARAM(IbRetryCnt, "IB_RETRY_CNT", 7);