RDMA,即 Remote Direct Memory Access,是一种绕过远程主机 OS kernel 访问其内存中数据的技术,概念源自于 DMA 技术。在 DMA 技术中,外部设备(PCIe 设备)能够绕过 CPU 直接访问 host memory;而 RDMA 则是指外部设备能够绕过 CPU,不仅可以访问本地主机的内存,还能够访问另一台主机上的用户态内存。由于不经过操作系统,不仅节省了大量 CPU 资源,同样也提高了系统吞吐量、降低了系统的网络通信延迟,在高性能计算和深度学习训练中得到了广泛的应用。本文将介绍 RDMA 的架构与原理,并讲解 RDMA 网络使用方法,测试代码在 Github 上可以找到。
技术背景 计算机网络通信中最重要两个衡量指标主要是 带宽 和 延迟,通信延迟主要是指:
Transmission Delay:
The time taken to transmit a packet from the host to the transmission medium 计算方式:Delayt=L/BandwidthDelayt=L/Bandwidth,其中 L 是要传输的数据包 L bit,Bandwidth 为链路带宽 如果两端的带宽高,则传输时间短,传输延迟低 Propagation delay
After the packet is transmitted to the transmission medium, it has to go through the medium to reach the destination. Hence the time taken by the last bit of the packet to reach the destination is called propagation delay.
计算方法:Delayp=Distance/VelocityDelayp=Distance/Velocity,其中 Distance 是传输链路的距离,Velocity 是物理介质传输速度 Velocity =3 X 108 m/s (for air) Velocity= 2.1 X 108 m/s (for optical fibre) Queueing delay
Let the packet is received by the destination, the packet will not be processed by the destination immediately. It has to wait in queue in something called as buffer. So the amount of time it waits in queue before being processed is called queueing delay. In general we can’t calculate queueing delay because we don’t have any formula for that. Processing delay
message handling time at sending/receive ends buffer管理、在不同内存空间中消息复制、以及消息发送完成后的系统中断 现实计算机网络中的通信场景中,主要是以发送小消息为主,因此处理延迟是提升性能的关键。
传统的 TCP/IP 网络通信,数据需要通过用户空间发送到远程机器的用户空间,在这个过程中需要经历若干次内存拷贝: 数据发送方需要讲数据从用户空间 Buffer 复制到内核空间的 Socket Buffer 数据发送方要在内核空间中添加数据包头,进行数据封装 数据从内核空间的 Socket Buffer 复制到 NIC Buffer 进行网络传输 数据接受方接收到从远程机器发送的数据包后,要将数据包从 NIC Buffer 中复制到内核空间的 Socket Buffer 经过一系列的多层网络协议进行数据包的解析工作,解析后的数据从内核空间的 Socket Buffer 被复制到用户空间 Buffer 这个时候再进行系统上下文切换,用户应用程序才被调用 在高速网络条件下,传统的 TPC/IP 网络在主机侧数据移动和复制操作带来的高开销限制了可以在机器之间发送的带宽。为了提高数据传输带宽,人们提出了多种解决方案,这里主要介绍下面两种:
TCP Offloading Engine Remote Direct Memroy Access TCP Offloading Engine 在主机通过网络进行通信的过程中,CPU 需要耗费大量资源进行多层网络协议的数据包处理工作,包括数据复制、协议处理和中断处理。当主机收到网络数据包时,会引发大量的网络 I/O 中断,CPU 需要对 I/O 中断信号进行响应和确认。为了将 CPU 从这些操作中解放出来,人们发明了TOE(TCP/IP Offloading Engine)技术,将上述主机处理器的工作转移到网卡上。TOE 技术需要特定支持 Offloading 的网卡,这种特定网卡能够支持封装多层网络协议的数据包。 TOE 技术将原来在协议栈中进行的IP分片、TCP分段、重组、checksum校验等操作,转移到网卡硬件中进行,降低系统CPU的消耗,提高服务器处理性能。 普通网卡处理每个数据包都要触发一次中断,TOE 网卡则让每个应用程序完成一次完整的数据处理进程后才触发一次中断,显著减轻服务器对中断的响应负担。 TOE 网卡在接收数据时,在网卡内进行协议处理,因此,它不必将数据复制到内核空间缓冲区,而是直接复制到用户空间的缓冲区,这种“零拷贝”方式避免了网卡和服务器间的不必要的数据往复拷贝。 RDMA 为了消除传统网络通信带给计算任务的瓶颈,我们希望更快和更轻量级的网络通信,由此提出了RDMA技术。RDMA利用 Kernel Bypass 和 Zero Copy技术提供了低延迟的特性,同时减少了CPU占用,减少了内存带宽瓶颈,提供了很高的带宽利用率。RDMA提供了给基于 IO 的通道,这种通道允许一个应用程序通过RDMA设备对远程的虚拟内存进行直接的读写。
RDMA 技术有以下几个特点:
CPU Offload:无需CPU干预,应用程序可以访问远程主机内存而不消耗远程主机中的任何CPU。远程主机内存能够被读取而不需要远程主机上的进程(或CPU)参与。远程主机的CPU的缓存(cache)不会被访问的内存内容所填充 Kernel Bypass:RDMA 提供一个专有的 Verbs interface 而不是传统的TCP/IP Socket interface。应用程序可以直接在用户态执行数据传输,不需要在内核态与用户态之间做上下文切换 Zero Copy:每个应用程序都能直接访问集群中的设备的虚拟内存,这意味着应用程序能够直接执行数据传输,在不涉及到网络软件栈的情况下,数据能够被直接发送到缓冲区或者能够直接从缓冲区里接收,而不需要被复制到网络层。 下面是 RDMA 整体框架架构图,从图中可以看出,RDMA在应用程序用户空间,提供了一系列 Verbs 接口操作RDMA硬件。RDMA绕过内核直接从用户空间访问RDMA 网卡。RNIC网卡中包括 Cached Page Table Entry,用来将虚拟页面映射到相应的物理页面。 RDMA 详解 目前RDMA有三种不同的硬件实现,它们都可以使用同一套API来使用,但它们有着不同的物理层和链路层:
Infiniband:基于 InfiniBand 架构的 RDMA 技术,由 IBTA(InfiniBand Trade Association)提出。搭建基于 IB 技术的 RDMA 网络需要专用的 IB 网卡和 IB 交换机。从性能上,很明显Infiniband网络最好,但网卡和交换机是价格也很高,然而RoCEv2和iWARP仅需使用特殊的网卡就可以了,价格也相对便宜很多。 iWARP:Internet Wide Area RDMA Protocal,基于 TCP/IP 协议的 RDMA 技术,由 IETF 标 准定义。iWARP 支持在标准以太网基础设施上使用 RDMA 技术,而不需要交换机支持无损以太网传输,但服务器需要使用支持iWARP 的网卡。与此同时,受 TCP 影响,性能稍差。 RoCE:基于以太网的 RDMA 技术,也是由 IBTA 提出。RoCE支持在标准以太网基础设施上使用RDMA技术,但是需要交换机支持无损以太网传输,需要服务器使用 RoCE 网卡,性能与 IB 相当。 I/O 瓶颈 时间回退到二十世纪的最后一年,随着 CPU 性能的迅猛发展,早在 1992 年 Intel 提出的 PCI 技术已经满足不了人民群众日益增长的 I/O 需求,I/O 系统的性能已经成为制约服务器性能的主要矛盾。尽管在 1998 年,IBM 联合 HP 、Compaq 提出了 PCI-X 作为 PCI 技术的扩展升级,将通信带宽提升到 1066 MB/sec,人们认为 PCI-X 仍然无法满足高性能服务器性能的要求,要求构建下一代 I/O 架构的呼声此起彼伏。经过一系列角逐,Infiniband 融合了当时两个竞争的设计 Future I/O 和 Next Generation I/O,建立了 Infiniband 行业联盟,也即 BTA (InfiniBand Trade Association),包括了当时的各大厂商 Compaq、Dell、HP、IBM、Intel、Microsoft 和 Sun。在当时,InfiniBand 被视为替换 PCI 架构的下一代 I/O 架构,并在 2000 年发布了 1.0 版本的 Infiniband 架构 Specification,2001 年 Mellanox 公司推出了支持 10 Gbit/s 通信速率的设备。 然而好景不长,2000 年互联网泡沫被戳破,人们对于是否要投资技术上如此跨越的技术产生犹豫。Intel 转而宣布要开发自己的 PCIe 架构,微软也停止了 IB 的开发。尽管如此,Sun 和 日立等公司仍然坚持对 InfiniBand 技术的研发,并由于其强大的性能优势逐渐在集群互联、存储系统、超级计算机内部互联等场景得到广泛应用,其软件协议栈也得到标准化,Linux 也添加了对于 Infiniband 的支持。进入2010年代,随着大数据和人工智能的爆发,InfiniBand 的应用场景从原来的超算等场景逐步扩散,得到了更加广泛的应用,InfiniBand 市场领导者 Mellanox 被 NVIDIA 收购,另一个主要玩家 QLogic 被 Intel 收购,Oracle 也开始制造自己的 InfiniBand 互联芯片和交换单元。到了 2020 年代,Mellanox 最新发布的 NDR 理论有效带宽已经可以达到 单端口 400 Gb/s,为了运行 400 Gb/s 的 HCA 可以使用 PCIe Gen5x16 或者 PCIe Gen4x32。 架构组成 InfiniBand 架构为系统通信定义了多种设备:channel adapter、switch、router、subnet manager,它提供了一种基于通道的点对点消息队列转发模型,每个应用都可通过创建的虚拟通道直接获取本应用的数据消息,无需其他操作系统及协议栈的介入。 在一个子网中,必须有至少每个节点有一个 channel adapter,并且有一个 subnet manager 来管理 Link。 Channel Adapters
可安装在主机或者其他任何系统(如存储设备)上的网络适配器,这种组件为数据包的始发地或者目的地,支持 Infiniband 定义的所有软件 Verbs
Host Channel Adapter:HCA Target Channel Adapter:TCA Switch
Switch 包含多个 InfiniBand 端口,它根据每个数据包 LRH 里面的 LID,负责将一个端口上收到的数据包发送到另一个端口。除了 Management Packets,Switch 不产生或者消费任何 Packets。它包含有 Subnet Manager 配置的转发表,能够响应 Subnet Manager 的 Management Packets。 Router
Router 根据 L3 中的 GRH,负责将 Packet 从一个子网转发到另一个子网,当被转到到另一子网时,Router 会重建数据包中的 LID。
Subnet Manager
Subnet Manager 负责配置本地子网,使其保持工作:
发现子网的物理拓扑 给子网中的每个端口分配 LIC 和其他属性(如活动MTU、活动速度) 给子网交换机配置转发表 检测拓扑变化(如子网中节点的增删) 处理子网中的各种错误 分层设计 InfiniBand 有着自己的协议栈,从上到下依次包括传输层、网络层、数据链路层和物理层: 对应着不同的层,数据包的封装如下,下面将对每一层的封装详细介绍: Physical Layer
物理层定义了 InfiniBand 具有的电气和机械特性,InfiniBand 支持光纤和铜作为传输介质。在物理层支持不同的 Link 速度,每个 Link 由四根线组成(每个方向两条),Link 可以聚合以提高速率,目前绝大多数的系统采用 4 Link。 以 QDR 为例,线上的 Signalling Rate 为 10 Gb/s,由于采用 8b/10b 编码,实际有效带宽单 Link 为 10 Gb/s * 8/10 = 8 Gb/s,如果是 4 Link,则带宽可以达到 32 Gb/s。因为是双向的,所以 4 Link 全双工的速率可以达到 64 Gb/s。 Link Layer
Link Layer 是 InfiniBand 架构的核心,包含以下部分:
Packets:链路层由两种类型的Packets,Data Packet 和 Management Packet,数据包最大可以为 4KB,数据包传输的类型包括两种类型 Memory:RDMA read/write,atomic operation Channel:send/receive,multicast transmission Switching:在子网中,Packet 的转发和交换是在链路层完成的 一个子网内的每个设备有一个由 subnet manager分配的 16 bit Local ID (LID) 每个 Packet 中有一个 Local Route Header (LRH) 指定了要发送的目标 LID 在一个子网中通过 LID 来负责寻址 QoS:链路层提供了 QoS 保证,不需要数据缓冲 Virtual Lanes:一种在一条物理链路上创建多条虚拟链路的机制。虚拟通道表示端口的一组用于收发数据包的缓冲区。支持的 VL 数是端口的一个属性。 每个 Link 支持 15 个标准的 VL 和一个用于 Management 的 VL15,VL15 具有最高等级,VL0 具有最低等级 Service Level:InfiniBand 支持多达 16 个服务等级,但是并没有指定每个等级的策略。InfiniBand 通过将 SL 和 VL 映射支持 QoS Credit Based Flow Control Data Integrity:链路层通过 Packet 中的 CRC 字段来进行数据完整性校验,其组成包括 ICRC 和 VCRC。 Network Layer
网络层负责将 Packet 从一个子网路由到另一个子网:
在子网间传输的 Packet 都有一个 Gloabl Route Header (GRH)。在这个 Header 中包括了该 Packet 的128 bit 的 源 IPv6 地址和目的 IPv6 地址 每个设备都有一个全局的 UID (GUID),路由器通过每个Packet的 GUID 来实现在不同子网间的转发 下面是 GRH 报头的格式,长40字节,可选,用于组播数据包以及需要穿越多个子网的数据包。它使用 GID 描述了源端口和目标端口,其格式与 IPv6 报头相同。 Transport Layer
传输层负责 Packet 的按序传输、根据 MTU 分段和很多传输层的服务(reliable connection, reliable datagram, unreliable connection, unreliable datagram, raw datagram)。InfiniBand 的传输层提供了一个巨大的提升,因为所有的函数都是在硬件中实现的。 InfiniBand 支持的服务 按照连接和可靠两个标准,可以划分出下图四种不同的传输模式: 可靠连接(RC)一个QP只和另一个QP相连,消息通过一个QP的发送队列可靠地传输到另一个QP的接收队列。数据包按序交付,RC连接很类似于TCP连接。 不可靠连接(UC)一个QP只和另一个QP相连,连接是不可靠的,所以数据包可能有丢失。传输层出错的消息不会进行重传,错误处理必须由高层的协议来进行。 不可靠数据报(UD)一个 QP 可以和其它任意的 UD QP 进行数据传输和单包数据的接收。不保证按序性和交付性。交付的数据包可能被接收端丢弃。支持多播消息(一对多),UD连接很类似于UDP连接。 每种模式中可用的操作如下表所示,目前的RDMA硬件提供一种数据报传输:不可靠的数据报(UD),并且不支持memory verbs。 下面是传输层的 Base Transport Header 的结构,长度为 12 字节,指定了源 QP 和 目标 QP、操作、数据包序列号和分区。 Partition Key:InfiniBand 中每个端口 Device 都有一个由 SM 配置 P_Key 表,每个 QP 都与这个表中的一个 P_Key 索引相关联。只有当两个 QP 相关联的 P_Key 键值相同时,它们才能互相收发数据包。 Destination QP:24 bit 的目标 QP ID。 根据传输层的服务类别和操作,有不定长度的扩展传输报头(Extended Transport Header,ETH),比如下面是进行时候的 ETH:
下面是 RDMA ETH,面向于 RDMA 操作: RDMA ETH
下面是 Datagram ETH,面向与 UD 和 RD 类型的服务: Datagram ETH
Queue Key:仅当两个不可靠 QP 的 Q_Key 相同时,它们才能接受对方的单播或组播消息,用于授权访问目标 QP 的 Queue。 Source QP:24 bit 的source QP ID,用于回复数据包的Destination QP 下面是 Reliable Datagram ETH,面向于 RC 类型的服务,其中有 End2End Context 字段: RoCE InfiniBand 架构获得了极好的性能,但是其不仅要求在服务器上安装专门的 InfiniBand 网卡,还需要专门的交换机硬件,成本十分昂贵。而在企业界大量部署的是以太网络,为了复用现有的以太网,同时获得 InfiniBand 强大的性能,IBTA 组织推出了 RoCE(RDMA over Converged Ethernet)。RoCE 支持在以太网上承载 IB 协议,实现 RDMA over Ethernet,这样一来,仅需要在服务器上安装支持 RoCE 的网卡,而在交换机和路由器仍然使用标准的以太网基础设施。网络侧需要支持无损以太网络,这是由于 IB 的丢包处理机制中,任意一个报文的丢失都会造成大量的重传,严重影响数据传输性能。
RoCE 与 InfiniBand 技术有相同的软件应用层及传输控制层,仅网络层及以太网链路层存在差异,如下图所示: RoCE v1
RoCE 协议分为两个版本:
RoCE v1协议:基于以太网承载 RDMA,只能部署于二层网络,它的报文结构是在原有的 IB 架构的报文上增加二层以太网的报文头,通过 Ethertype 0x8915标识 RoCE 报文。 RoCE v2协议:基于 UDP/IP 协议承载 RDMA,可部署于三层网络,它的报文结构是在原有的 IB 架构的报文上增加 UDP 头、IP 头和二层以太网报文头,通过 UDP 目的端口号 4791 标 识 RoCE 报文。RoCE v2 支持基于源端口号 hash,采用 ECMP 实现负载分担,提高了网络的利用率。 iWARP iWARP 从以下几个方面降低了主机侧网络负载:
TCP/IP 处理流程从 CPU 卸载到 RDMA 网卡处理,降低了 CPU 负载。 消除内存拷贝:应用程序可以直接将数据传输到对端应用程序内存中,显著降低 CPU 负载。 减少应用程序上、下文切换:应用程序可以绕过操作系统,直接在用户空间对 RDMA 网卡下发命令,降低了开销,显著降低了应用程序上、下文切换造成的延迟。 由于 TCP 协议能够提供流量控制和拥塞管理,因此 iWARP 不需要以太网支持无损传输,仅通过普通以太网交换机和 iWARP 网卡即可实现,因此能够在广域网上应用,具有较好的扩展性。
RDMA 编程 传输模式 RDMA有两种基本操作,包括 Memory verbs 和 Messaging verbs:
Memory verbs:包括read、write和atomic操作,属于单边操作,只需要本端明确信息的源和目的地址,远端应用不必感知此次通信,数据的读或存都通过远端的DMA在RNIC与应用buffer之间完成,再由远端RNIC封装成消息返回到本端。 RDMA Read:从远程主机读取部分内存。调用者指定远程虚拟地址,像本地内存地址一样用来拷贝。在执行 RDMA 读操作之前,远程主机必须提供适当的权限来访问它的内存。一旦权限设置完成, RDMA 读操作就可以在对远程主机没有任何通知的条件下执行。不管是 RDMA 读还是 RDMA 写,远程主机都不会意识到操作正在执行 (除了权限和相关资源的准备操作)。 RDMA Write:与 RDMA Read 类似,只是数据写到远端主机中。RDMA写操作在执行时不通知远程主机。然而带即时数的RDMA写操作会将即时数通知给远程主机。 RDMA Atomic:包括原子取、原子加、原子比较和原子交换,属于RDMA原子操作的扩展。 Messaging verbs:包括send和receive操作,属于双边操作,即必须要远端的应用感知参与才能完成收发。 RDMA Send:发送操作允许你把数据发送到远程 QP 的接收队列里。接收端必须已经事先注册好了用来接收数据的缓冲 区。发送者无法控制数据在远程主机中的放置位置。可选择是否使用即时数,一个4位的即时数可以和数据缓冲一起被传送。这个即时数发送到接收端是作为接收的通知,不包含在数据缓冲之中。 RDMA Receive:这是与发送操作相对应的操作。接收主机被告知接收到数据缓冲,还可能附带一个即时数。接收端应用 程序负责接收缓冲区的维护和发布。 RDMA Consortium 和 IBTA 主导了RDMA,RDMAC是IETF的一个补充,它主要定义的是iWRAP和iSER,IBTA是infiniband的全部标准制定者,并补充了RoCE v1 v2的标准化。应用和RNIC之间的传输接口层(software transport interface)被称为Verbs。IBTA解释了RDMA传输过程中应具备的特性行为,而并没有规定Verbs的具体接口和数据结构原型。这部分工作由另一个组织OFA(Open Fabric Alliance)来完成,OFA提供了RDMA传输的一系列Verbs API。OFA开发出了OFED(Open Fabric Enterprise Distribution)协议栈,支持多种RDMA传输层协议。
OFED中除了提供向下与RNIC基本的队列消息服务,向上还提供了ULP(Upper Layer Protocols),通过ULPs,上层应用不需要直接到Verbs API对接,而是借助于ULP与应用对接,常见的应用不需要做修改,就可以跑在RDMA传输层上。
基本概念 Send Request
SR 定义了数据的发送量、从哪里、发送方式、是否通过 RDMA、到哪里。
结构 ibv_send_wr 用来描述 SR。 struct ibv_send_wr { uint64_t wr_id; struct ibv_send_wr *next; struct ibv_sge sg_list; int num_sge; enum ibv_wr_opcode opcode; unsigned int send_flags; / When opcode is *_WITH_IMM: Immediate data in network byte order.
- When opcode is *_INV: Stores the rkey to invalidate */ union { __be32 imm_data; uint32_t invalidate_rkey; }; union { struct { uint64_t remote_addr; uint32_t rkey; } 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; union { struct { uint32_t remote_srqn; } xrc; } qp_type; union { struct { struct ibv_mw *mw; uint32_t rkey; struct ibv_mw_bind_info bind_info; } bind_mw; struct { void *hdr; uint16_t hdr_sz; uint16_t mss; } tso; }; }; Receive Request
RR 定义用来放置通过 RDMA 操作接收到的数据的缓冲区。如没有定义缓冲区,并且有个传输者尝试执行一个发送操作或者一个带即时数的 RDMA 写操作,那么接收者将会发出接收未就绪的错误(RNR)。
结构 ibv_recv_wr 用来描述 RR。 struct ibv_recv_wr { uint64_t wr_id; struct ibv_recv_wr *next; struct ibv_sge *sg_list; int num_sge; }; Queue Pairs
RDMA提供了基于消息队列的点对点通信,每个应用都可以直接获取自己的消息,无需操作系统和协议栈的介入。消息服务建立在通信双方本端和远端应用之间创建的Channel-IO连接之上。当应用需要通信时,就会创建一条Channel连接,每条Channel的首尾端点是两对Queue Pairs(QP)。每对QP由Send Queue(SQ)和Receive Queue(RQ)构成,这些队列中管理着各种类型的消息。QP会被映射到应用的虚拟地址空间,使得应用直接通过它访问RNIC网卡。除了QP描述的两种基本队列之外,RDMA还提供一种队列Complete Queue(CQ),CQ用来知会用户WQ上的消息已经被处理完。 RDMA提供了一套软件传输接口,方便用户创建传输请求Work Request(WR),WR中描述了应用希望传输到Channel对端的消息内容,WR 通知QP中的某个队列Work Queue(WQ)。在 WQ 中,用户的 WR 被转化为Work Queue Element(WQE)的格式,等待RNIC的异步调度解析,并从WQE指向的Buffer中拿到真正的消息发送到 Channel 对端。 struct ibv_qp { struct ibv_context *context; void *qp_context; struct ibv_pd *pd; struct ibv_cq *send_cq; struct ibv_cq *recv_cq; struct ibv_srq *srq; uint32_t handle; uint32_t qp_num; enum ibv_qp_state state; enum ibv_qp_type qp_type; pthread_mutex_t mutex; pthread_cond_t cond; uint32_t events_completed; }; Completion Queue
发送到 SQ 和 RQ 的工作请求都被视为未完成,工作请求未完成期间,它指向的内存缓冲区的内容是不确定的。CQ 包含了发送到工作队列(WQ)中已完成的工作请求(WR)。每次完成表示一个特定的 WR 执行完毕(包括成功完成的 WR 和不成功完成的 WR)。完成队列是一个用来告知应用程序已经结束的工作请求的信息(状态、操作码、大小、来源)的机制。 CQ有n个完成队列实体(CQE),CQE 的数量在CQ创建时指定。当一个CQE被 轮询 到,它就从CQ中被删除。CQ是一个CQE的 FIFO 队列。CQ能服务于发送队列、接收队列或者同时服务于这两种队列。多个不同QP中的工作请求(WQ)可联系到同一个CQ上。
结构 ibv_cq 用来描述CQ。 struct ibv_cq { struct ibv_context *context; struct ibv_comp_channel *channel; void *cq_context; uint32_t handle; int cqe; pthread_mutex_t mutex; pthread_cond_t cond; uint32_t comp_events_completed; uint32_t async_events_completed; }; Memory Registration
RDMA 设备访问的每一个内存缓冲区都必须注册,在注册过程中,将对内存缓冲区执行如下操作:
将连续的内存缓冲区分成内存页,将这些内存空间提供给网络适配器作为虚拟的连续缓冲区,缓冲区使用虚拟地址 将虚拟内存映射到物理内存,注册进程将虚拟地址与物理地址的映射表写入网络适配器。 检查内存页权限,确保它们支持为 MR(Memory Region) 发出请求的权限 锁定内存页权限,以防它们被换出,确保虚拟内存到物理内存的映射不变 注册成功后,内存有两个键:
本地键 lkey:供本地工作请求用来访问内存的 key 远程键 rkey:供远程机器通过 RDMA 访问内存的 key 在工作请求中,将使用这些 key 来访问内存缓冲区,同一内存缓冲区可以被多次注册(甚至设置不同的操作权限),并且每次注册都会生成不同的 key。
结构 ibv_mr 用来描述内存注册。 struct ibv_mr { struct ibv_context *context; struct ibv_pd *pd; void *addr; size_t length; uint32_t handle; uint32_t lkey; uint32_t rkey; }; Memory Window
启用远程内存访问的方式有以下两种:
注册允许远程内存访问的内存缓冲区 注册内存区并将其绑定到内存窗口 这两种方式都将创建一个 rkey,可用来访问制定的内存。然而,如果想要这个rkey 无效,以禁止访问该内存时。采用注销内存区的方式实现起来比较繁琐。而使用内存窗口,并根据需要进行绑定和解除绑定,对于启动和禁用运城内存访问简单灵活得多。
内存窗口作用于以下场景:
动态地授予和回收已注册缓冲区的远程访问权限,这种方式相较于将缓冲区取消注册、再注册或者重注册,有更低的性能损耗代价。 想为不同的远程代理授予不同的远程访问方式,或者在一个已注册的缓冲区中不同范围授予哪些权限。 内存窗口和内存注册之间的关联操作叫做绑定。不同的MW可以做用于同一个MR,即使有不同的访问权限。
Address Vector
地址向量用来描述本地节点到远程节点的路由。在QP的每个UC/RC中,都有一个地址向量存在于QP的上下文中。在UD的QP中,每个提交的发送请求(SR)中都应该定义地址向量。
结构 ibv_ah用来描述地址向量。
Global Routing Header(GRH)
GRH用于子网之间的路由。当用到RoCE时,GRH用于子网内部的路由,并且是强制使用的,强制使用GRH是为了保证应用程序即支持IB又支持RoCE。当全局路由用在给予UD的QP时,在接受缓冲区的前40自己会包含有一个GRH。这个区域专门存储全局路由信息,为了回应接收到的数据包,会产生一个合适的地址向量。如果向量用在UD中,接收请求RR应该总是有额外的40字节用来GRH。
结构 ibv_grh 用来描述GRH。
Protection Domain
保护域是一种集合,它的内部元素只能与集合内部的其它元素相互作用。这些元素可以是AH、QP、MR、和SRQ。保护域用于QP与内存注册和内存窗口相关联,这是一种授权和管理网络适配器对主机系统内存的访问。PD也用于将给予不可靠数据报(UD)的QP关联到地址处理(AH),这是一种对UD目的端的访问控制。 struct ibv_pd { struct ibv_context context; uint32_t handle; }; 通信过程 获取设备列表 首先必须检查得到本机可用的IB设备列表,列表中的每个设备都包含一个名字和GUID。 / 1 获取设备列表 */ int num_devices; struct ibv_device **dev_list = ibv_get_device_list(&num_devices); if (!dev_list || !num_devices) { fprintf(stderr, "failed to get IB devices\n"); rc = 1; goto main_exit; } 打开要请求的设备
遍历设备列表,通过设备的GUID或者名字选择并打开它,获取一个上下文: /* 2 打开设备,获取设备上下文 */ struct ibv_device ib_dev = dev_list[0]; res.ib_ctx = ibv_open_device(ib_dev); if (!res.ib_ctx) { fprintf(stderr, "failed to open device \n"); rc = 1; goto main_exit; } 一般在这里需要释放设备列表占用的资源 / 3 释放设备列表占用的资源 */ ibv_free_device_list(dev_list); dev_list = NULL; ib_dev = NULL; 查询设备的工作能力
设备的工作能力能使用户了解已打开设备支持的特性和能力 ibv_port_attr。 /* 4 查询设备端口状态 */ if (ibv_query_port(res.ib_ctx, 1, &res.port_attr)) { fprintf(stderr, "ibv_query_port on port failed\n"); rc = 1; goto main_exit; } 分配保护域以及您的资源
保护域(PD)允许用户限制哪些组件只能相互交互。这个组件可以是AH、QP、MR、MW、和SRQ。 /* 5 创建PD(Protection Domain) */ res.pd = ibv_alloc_pd(res.ib_ctx); if (!res.pd) { fprintf(stderr, "ibv_alloc_pd failed\n"); rc = 1; goto main_exit; } 创建 CQ
一个CQ包含完成的工作请求(WR),每个WR将生成放置在CQ中的完成队列实体CQE,CQE将表明WR是否成功完成: /* 6 创建CQ(Complete Queue) */ int cq_size = 10; res.cq = ibv_create_cq(res.ib_ctx, cq_size, NULL, NULL, 0); if (!res.cq) { fprintf(stderr, "failed to create CQ with %u entries\n", cq_size); rc = 1; goto main_exit; } 注册一个内存区域
在注册过程中,用户设置内存权限并接收 lkey 和 rkey,稍后将使用这些秘钥来访问此内存缓冲区: /* 7 注册MR(Memory Region) */ int size = MSG_SIZE; res.buf = (char *)malloc(size); if (!res.buf) { fprintf(stderr, "failed to malloc %Zu bytes to memory buffer\n", size); rc = 1; goto main_exit; } memset(res.buf, 0, size); int mr_flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE; res.mr = ibv_reg_mr(res.pd, res.buf, size, mr_flags); if (!res.mr) { fprintf(stderr, "ibv_reg_mr failed with mr_flags=0x%x\n", mr_flags); rc = 1; goto main_exit; } fprintf(stdout, "MR was registered with addr=%p, lkey=0x%x, rkey=0x%x, flags=0x%x\n", res.buf, res.mr->lkey, res.mr->rkey, mr_flags); 创建 QP
创建 QP 还将创建关联的发送队列和接
收队列: /* 8 创建QP(Queue Pair) */ struct ibv_qp_init_attr qp_init_attr; memset(&qp_init_attr, 0, sizeof(qp_init_attr)); qp_init_attr.qp_type = IBV_QPT_RC; qp_init_attr.sq_sig_all = 1; qp_init_attr.send_cq = res.cq; qp_init_attr.recv_cq = res.cq; qp_init_attr.cap.max_send_wr = 1; qp_init_attr.cap.max_recv_wr = 1; qp_init_attr.cap.max_send_sge = 1; qp_init_attr.cap.max_recv_sge = 1; res.qp = ibv_create_qp(res.pd, &qp_init_attr); if (!res.qp) { fprintf(stderr, "failed to create QP\n"); rc = 1; goto main_exit; } fprintf(stdout, "QP was created, QP number=0x%x\n", res.qp->qp_num); 交换控制信息
可以通过 Socket 或者 RDMA_CM API 来交换控制信息,这里演示的是使用 Socket 交换信息: /* 9 交换控制信息 */ struct cm_con_data_t local_con_data; // 发送给远程主机的信息 struct cm_con_data_t remote_con_data; // 接收远程主机发送过来的信息 struct cm_con_data_t tmp_con_data; local_con_data.addr = htonll((uintptr_t)res.buf); local_con_data.rkey = htonl(res.mr->rkey); local_con_data.qp_num = htonl(res.qp->qp_num); local_con_data.lid = htons(res.port_attr.lid); if (sock_sync_data(server_ip, sizeof(struct cm_con_data_t), (char *)&local_con_data, (char )&tmp_con_data) < 0) { fprintf(stderr, "failed to exchange connection data between sides\n"); rc = 1; goto main_exit; } remote_con_data.addr = ntohll(tmp_con_data.addr); remote_con_data.rkey = ntohl(tmp_con_data.rkey); remote_con_data.qp_num = ntohl(tmp_con_data.qp_num); remote_con_data.lid = ntohs(tmp_con_data.lid); / save the remote side attributes, we will need it for the post SR */ res.remote_props = remote_con_data; fprintf(stdout, "Remote address = 0x%" PRIx64 "\n", remote_con_data.addr); fprintf(stdout, "Remote rkey = 0x%x\n", remote_con_data.rkey); fprintf(stdout, "Remote QP number = 0x%x\n", remote_con_data.qp_num); fprintf(stdout, "Remote LID = 0x%x\n", remote_con_data.lid); 转换 QP 状态
QP 有一个状态机,用于指定 QP 在各种状态下能够做什么:
RESET:重置状态,QP 刚创建时即处于 RESET 状态,此时不能在 QP 中添加发送请求或接收请求,所有入站消息都被默默丢弃 INIT:已初始化状态,此时不能添加发送请求,可以添加接收请求,但是请求不会被处理,所有入站消息都被默默丢弃。最好在QP处于这种状态时将接收请求加入到其中,再切换到 RTR 状态。这样可以避免发送消息的远程 QP 在需要使用接收请求时没有接收请求可用的情况发生。 RTR:Ready To Receive 状态,此时不能添加发送请求,但是可以添加并且处理接收请求,所有入站信息都将得到处理。在这种状态下收到的第一条消息,将触发异步事件「通信已建立」 RTS:Ready To Send 状态,此时可以添加和处理发送和接收请求,所有入站信息都将得到处理 SQD:Send Queue Drained 状态,此时 QP 将完成所有已进入处理程序的发送请求的处理工作 SQE:Send Queue Error 状态,传输类型为不可靠的 QP,当其发送队列出现错误时,RDMA 设备会自动将其切换到这个状态 ERROR:错误状态,此时所有未处理的工作请求都被删除 状态:RESET -> INIT -> RTR -> RTS 要严格按照顺序进行转换 INIT之后就可以调用 ibv_post_recv 提交一个receive buffer了 当 QP进入RTR(ready to receive)状态以后,便开始进行接收处理 RTR之后便可以转为RTS(ready to send),RTS状态下可以调用ibv_post_send
/* 10 转换QP状态 */ // RESET -> INIT struct ibv_qp_attr attr; int flags; memset(&attr, 0, sizeof(attr)); attr.qp_state = IBV_QPS_INIT; attr.port_num = 1; // IB 端口号 attr.pkey_index = 0; attr.qp_access_flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE; flags = IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS; rc = ibv_modify_qp(res.qp, &attr, flags); if (rc) fprintf(stderr, "failed to modify QP state to INIT\n"); //INIT -> RTR(Ready To Receive) memset(&attr, 0, sizeof(attr)); attr.qp_state = IBV_QPS_RTR; attr.path_mtu = IBV_MTU_256; attr.dest_qp_num = res.remote_props.qp_num; attr.rq_psn = 0; attr.max_dest_rd_atomic = 1; attr.min_rnr_timer = 0x12; attr.ah_attr.is_global = 0; attr.ah_attr.dlid = res.remote_props.lid; attr.ah_attr.sl = 0; attr.ah_attr.src_path_bits = 0; attr.ah_attr.port_num = 1; flags = 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; rc = ibv_modify_qp(res.qp, &attr, flags); if (rc) fprintf(stderr, "failed to modify QP state to RTR\n"); //RTR -> RTS(Ready To Send) memset(&attr, 0, sizeof(attr)); attr.qp_state = IBV_QPS_RTS; attr.timeout = 0x12; attr.retry_cnt = 6; attr.rnr_retry = 0; attr.sq_psn = 0; attr.max_rd_atomic = 1; flags = IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC; rc = ibv_modify_qp(res.qp, &attr, flags); if (rc) fprintf(stderr, "failed to modify QP state to RTS\n"); 创建发送/接收任务
ibv_send_wr(send work request) 该任务会被提交到QP中的SQ(Send Queue)中 发送任务有三种操作:Send,Read,Write Send操作需要对方执行相应的Receive操作 Read/Write直接操作对方内存,对方无感知 把要发送的数据的内存地址,大小,密钥告诉HCA Read/Write还需要告诉HCA远程的内存地址和密钥 /* 11 创建发送任务ibv_send_wr */ struct ibv_send_wr sr; struct ibv_sge sge; struct ibv_send_wr bad_wr = NULL; int rc; / prepare the scatter/gather entry / memset(&sge, 0, sizeof(sge)); sge.addr = (uintptr_t)res->buf; sge.length = MSG_SIZE; sge.lkey = res->mr->lkey; / prepare the send work request */ memset(&sr, 0, sizeof(sr)); sr.next = NULL; sr.wr_id = 0; sr.sg_list = &sge; sr.num_sge = 1; sr.opcode = opcode; sr.send_flags = IBV_SEND_SIGNALED; if (opcode != IBV_WR_SEND) { sr.wr.rdma.remote_addr = res->remote_props.addr; sr.wr.rdma.rkey = res->remote_props.rkey; } 提交发送/接收任务
发送 ibv_post_send 接收 ibv_post_recv rc = ibv_post_send(res->qp, &sr, &bad_wr); if (rc) fprintf(stderr, "failed to post SR\n"); return rc; 轮询任务完成信息 /* 13 轮询任务结果 */ struct ibv_wc wc; int poll_result; int rc = 0; do { poll_result = ibv_poll_cq(res->cq, 1, &wc); } while (poll_result == 0); RDMA 单边操作 单边操作传输方式是RDMA与传统网络传输的最大不同,提供直接访问远程的虚拟地址,无须远程应用的参与,这种方式适用于批量数据传输。
READ和WRITE是单边操作,只需要本端明确信息的源和目的地址,远端应用不必感知此次通信,数据的读或写都通过RDMA在RNIC与应用Buffer之间完成,再由远端RNIC封装成消息返回到本端。
RDMA Read
对于单边操作,以存储网络环境下的存储为例,数据的流程如下: 首先A、B建立连接,QP已经创建并且初始化。 数据被存档在 B 的 buffer地址 VB,注意VB应该提前注册到B的RNIC (并且它是一个Memory Region) ,并拿到返回的local key,相当于RDMA操作这块buffer的权限。 B 把数据地址 VB,key封装到专用的报文传送到A,这相当于B把数据buffer的操作权交给了A。同时B在它的WQ中注册进一个WR,以用于接收数据传输的A返回的状态。 A 在收到 B 的送过来的数据 VB 和 R_key 后,RNIC 会把它们连同自身存储地址 VA 到封装 RDMA READ 请求,将这个消息请求发送给B,这个过程A、B两端不需要任何软件参与,就可以将 B 的数据存储到 B 的 VA虚拟地址。 B在存储完成后,会向A返回整个数据传输的状态信息。 RDMA Write
对于单边操作,以存储网络环境下的存储为例,数据的流程如下: 首先A、B建立连接,QP已经创建并且初始化。 数据 remote目标存储buffer地址VB,注意VB应该提前注册到B的RNIC(并且它是一个Memory Region),并拿到返回的local key,相当于RDMA操作这块buffer的权限。 B把数据地址VB,key封装到专用的报文传送到A,这相当于B把数据buffer的操作权交给了A。同时B在它的WQ中注册进一个WR,以用于接收数据传输的A返回的状态。 A在收到B的送过来的数据VB和R_key后,RNIC会把它们连同自身发送地址VA到封装RDMA WRITE请求,这个过程A、B两端不需要任何软件参与,就可以将A的数据发送到B的VB虚拟地址。 A在发送数据完成后,会向B返回整个数据传输的状态信息。 RDMA 双边操作 双边操作与传统网络的底层buffer pool类似,收发双方的参与过程并无差别,区别在零拷贝、kernel bypass,实际上传统网络中一些高级的网络SOC 已经实现类似功能。对于RDMA,这是一种复杂的消息传输模式,多用于传输短的控制消息。
RDMA 中 SEND/RECEIVE 是双边操作,即必须要远端的应用感知参与才能完成收发。在实际中,SEND/RECEIVE多用于连接控制类报文,而数据报文多是通过READ/WRITE来完成的。对于双边操作为例,主机 A 向主机 B 发送数据的流程如下:
首先,A 和 B 都要创建并初始化好各自的QP、CQ,并且为 RDMA 注册了 Memory Region,A 想发送数据给 B A 和 B 分别向自己的WQ中注册WQE,对于A,WQ=SQ,WQE描述指向一个等到被发送的数据;对于B,WQ=RQ,WQE描述指向一块用于存储数据的Buffer A 的 HCA 作为硬件总是从 SQ 中取出 WQE,解析到这是一个SEND消息,将数据直接从 A 的 Buffer 中发给 B。数据流到达B的RNIC后,B 的 HCA 将会从 RQ 中取出 WQE,并把数据直接存储到 WQE 指向的存储位置。 AB 通信完成后,A的CQ中会产生一个完成消息 CQE 表示发送完成。与此同时,B 的 CQ 中也会产生一个完成消息表示接收完成。每个WQ中WQE的处理完成都会产生一个CQE。 即使传输发生错误,也会产生 CQE,CQE 中会有字段表明传输的状态。 工具使用 带宽测试 ib_read_bw ServerA:ib_read_bw -a -d mlx4_ 0ServerB: ib_read_bw -a -F <ServerAIP> -d mlx4_0 --report_gbits
Server A
这里 -q 指定 QP 数为 2,-x 指定 GID Index
$ ib_read_bw -q 2 -x 3 --report_g --run_infinitely
- Waiting for client to connect... *
RDMA_Read BW Test Dual-port : OFF Device : mlx5_1 Number of qps : 2 Transport type : IB Connection type : RC Using SRQ : OFF PCIe relax order: ON CQ Moderation : 1 Mtu : 1024[B] Link type : Ethernet GID index : 3 Outstand reads : 16 rdma_cm QPs : OFF Data ex. method : Ethernet
local address: LID 0000 QPN 0x02d9 PSN 0xf326ce OUT 0x10 RKey 0x060d13 VAddr 0x007fa3bccfc000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:00:12 local address: LID 0000 QPN 0x02da PSN 0x1403a0 OUT 0x10 RKey 0x060d13 VAddr 0x007fa3bcd0c000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:00:12 remote address: LID 0000 QPN 0x02c9 PSN 0x861fee OUT 0x10 RKey 0x050f13 VAddr 0x007f55afc0f000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:04:05 remote address: LID 0000 QPN 0x02ca PSN 0xfad640 OUT 0x10 RKey 0x050f13 VAddr 0x007f55afc1f000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:04:05
#bytes #iterations BW peak[Gb/sec] BW average[Gb/sec] MsgRate[Mpps]
Server B
$ ib_read_bw 172.18.0.237 -q 2 -x 3 --report_g --run_infinitely
RDMA_Read BW Test Dual-port : OFF Device : mlx5_3 Number of qps : 2 Transport type : IB Connection type : RC Using SRQ : OFF PCIe relax order: ON TX depth : 128 CQ Moderation : 1 Mtu : 1024[B] Link type : Ethernet GID index : 3 Outstand reads : 16 rdma_cm QPs : OFF Data ex. method : Ethernet
local address: LID 0000 QPN 0x02c9 PSN 0x861fee OUT 0x10 RKey 0x050f13 VAddr 0x007f55afc0f000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:04:05 local address: LID 0000 QPN 0x02ca PSN 0xfad640 OUT 0x10 RKey 0x050f13 VAddr 0x007f55afc1f000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:04:05 remote address: LID 0000 QPN 0x02d9 PSN 0xf326ce OUT 0x10 RKey 0x060d13 VAddr 0x007fa3bccfc000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:00:12 remote address: LID 0000 QPN 0x02da PSN 0x1403a0 OUT 0x10 RKey 0x060d13 VAddr 0x007fa3bcd0c000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:00:12
#bytes #iterations BW peak[Gb/sec] BW average[Gb/sec] MsgRate[Mpps] 65536 829982 0.00 87.06 0.166061 65536 828835 0.00 86.94 0.165832 65536 828849 0.00 86.95 0.165835 65536 828828 0.00 86.94 0.165831 65536 828801 0.00 86.94 0.165825 65536 828795 0.00 86.94 0.165824 65536 828852 0.00 86.95 0.165835
ib_write_bw ServerA: ib_write_bw -a -d mlx4_ 0ServerB: ib_write_bw -a -F <ServerAIP> -d mlx4_0 --report_gbits
ib_send_bw ServerA: ib_send_bw -a -d mlx4_ 0ServerB: ib_send_bw -a -F <ServerAIP> -d mlx4_0 --report_gbits
延迟测试 延迟测试也有三个命令,使用方法与上类似:
ib_read_lat ib_write_lat ib_send_lat 以 ib_read_lat 为例,测试结果如下:
Server A
$ ib_read_lat -x 3 --report_g
- Waiting for client to connect... *
RDMA_Read Latency Test Dual-port : OFF Device : mlx5_1 Number of qps : 1 Transport type : IB Connection type : RC Using SRQ : OFF PCIe relax order: ON Mtu : 1024[B] Link type : Ethernet GID index : 3 Outstand reads : 16 rdma_cm QPs : OFF Data ex. method : Ethernet
local address: LID 0000 QPN 0x02db PSN 0x144f6a OUT 0x10 RKey 0x060d14 VAddr 0x00000001849000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:00:12 remote address: LID 0000 QPN 0x02cb PSN 0x1c38a6 OUT 0x10 RKey 0x050f14 VAddr 0x00000000e59000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:04:05
Server B
$ ib_read_lat 172.18.0.237 -x 3 --report_g
RDMA_Read Latency Test Dual-port : OFF Device : mlx5_3 Number of qps : 1 Transport type : IB Connection type : RC Using SRQ : OFF PCIe relax order: ON TX depth : 1 Mtu : 1024[B] Link type : Ethernet GID index : 3 Outstand reads : 16 rdma_cm QPs : OFF Data ex. method : Ethernet
local address: LID 0000 QPN 0x02cb PSN 0x1c38a6 OUT 0x10 RKey 0x050f14 VAddr 0x00000000e59000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:04:05 remote address: LID 0000 QPN 0x02db PSN 0x144f6a OUT 0x10 RKey 0x060d14 VAddr 0x00000001849000 GID: 00:00:00:00:00:00:00:00:00:00:255:255:192:168:00:12
#bytes #iterations t_min[usec] t_max[usec] t_typical[usec] t_avg[usec] t_stdev[usec] 99% percentile[usec] 99.9% percentile[usec] 2 1000 10.87 14.62 11.40 11.48 0.34 12.74 14.62
参考资料 Introduction to InfiniBand White Paper InfiniBand Architecture Overview InfiniBand Architecture Specification Release 1.2.1 Introduction to RDMA RDMA Aware Network Programming User Manual RDMA-Tutorial RDMA Mojo Blog
标签:QP,00,架构,attr,qp,详解,RDMA,ibv From: https://blog.51cto.com/u_13721330/5874828