RDMA介绍及其在NCCL中的使用

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);
RDMA(Remote Direct Memory Access)是一种网络技术,允许一台计算机直接读取或写入远程计算机的内存,而无需远程计算机的CPU参与。这种技术广泛应用于高性能计算和大规模分布式系统中,以减少延迟并提高数据传输效率。在RDMA传输层中,ACK/NAK协议是用于确保数据可靠传输的关键机制。 ### ACK/NAK协议的基本原理 #### 1. **确认机制(ACK)** 在RDMA传输过程中,每当发送方发送一个数据包后,接收方会在成功接收到该数据包后发送一个确认信号(ACK)。这个确认信号告诉发送方数据包已经正确接收,发送方可以继续发送下一个数据包。如果发送方在一定时间内没有收到ACK,它会重新发送该数据包以确保数据的可靠性。 #### 2. **否定确认机制(NAK)** 除了ACK之外,RDMA传输层还支持否定确认(NAK)机制。当接收方检测到数据包丢失、损坏或顺序错误时,它会发送一个NAK信号给发送方。NAK信号通常包含丢失或损坏的数据包的序列号,以便发送方能够快速定位并重新发送这些数据包。 #### 3. **重传机制** 当发送方收到NAK信号或在超时时间内未收到ACK信号时,它会启动重传机制。发送方将重新发送未被确认的数据包。为了确保高效的数据传输,RDMA协议通常会使用滑动窗口机制来管理数据包的发送和确认过程。滑动窗口允许发送方在等待确认的同时继续发送多个数据包,从而提高网络利用率。 #### 4. **序列号和确认号** 每个数据包都会被分配一个唯一的序列号,接收方通过确认号来告知发送方已经成功接收的数据包的最高序列号。这样,发送方可以根据确认号来确定哪些数据包已经被成功接收,哪些需要重新发送。 #### 5. **流量控制和拥塞控制** RDMA传输层还实现了流量控制和拥塞控制机制,以防止发送方发送数据的速度超过接收方的处理能力或网络的承载能力。流量控制通常通过调整滑动窗口的大小来实现,而拥塞控制则通过监测网络状况并动态调整发送速率来避免网络拥塞。 ### 示例代码:简单的ACK/NAK模拟 以下是一个简单的Python代码示例,模拟了RDMA传输层中的ACK/NAK协议工作机制。请注意,这只是一个简化的模型,实际的RDMA实现会更加复杂。 ```python import time import random class RDMAConnection: def __init__(self): self.sent_packets = {} self.next_seq_num = 0 self.timeout = 2 # seconds def send_packet(self, data): seq_num = self.next_seq_num self.sent_packets[seq_num] = {'data': data, 'time_sent': time.time()} print(f"Sent packet {seq_num}: {data}") self.next_seq_num += 1 return seq_num def receive_ack(self, seq_num): if seq_num in self.sent_packets: del self.sent_packets[seq_num] print(f"Received ACK for packet {seq_num}") else: print(f"Received ACK for unknown packet {seq_num}") def receive_nak(self, seq_num): if seq_num in self.sent_packets: print(f"Received NAK for packet {seq_num}, retransmitting...") self.retransmit_packet(seq_num) else: print(f"Received NAK for unknown packet {seq_num}") def retransmit_packet(self, seq_num): packet = self.sent_packets[seq_num] packet['time_sent'] = time.time() print(f"Retransmitting packet {seq_num}: {packet['data']}") def check_timeouts(self): current_time = time.time() for seq_num, packet in list(self.sent_packets.items()): if current_time - packet['time_sent'] > self.timeout: print(f"Timeout for packet {seq_num}, retransmitting...") self.retransmit_packet(seq_num) # 模拟接收方 class Receiver: def __init__(self): self.expected_seq_num = 0 def receive_packet(self, seq_num, data, connection): if seq_num == self.expected_seq_num: print(f"Received packet {seq_num}: {data}") connection.receive_ack(seq_num) self.expected_seq_num += 1 else: print(f"Out of order packet {seq_num}, sending NAK...") connection.receive_nak(seq_num) # 模拟RDMA传输 def simulate_rdma(): connection = RDMAConnection() receiver = Receiver() # 模拟发送数据包 for i in range(5): seq_num = connection.send_packet(f"Data {i}") # 模拟网络延迟 time.sleep(0.5) # 模拟接收方处理数据包 for i in range(5): # 模拟数据包丢失 if random.random() < 0.2: print(f"Packet {i} lost") continue receiver.receive_packet(i, f"Data {i}", connection) # 检查超时 time.sleep(3) connection.check_timeouts() simulate_rdma() ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

分享放大价值

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值