【网络】UCX(Unified Communication X )|统一抽象通信接口

86 篇文章 233 订阅

目录

UCX 的意义

UCX 通信接口简介

支持的传输(协议)

UCX社区

UCX 编程模型简介

建立连接

内存注册

异步任务处理(重点)

使用UCX

编译debug版本

构建RPM包

构建DEB 包

构建Doxygen文档

使用UCX安装OpenMPI和OpenSHMEM

使用UCX安装MPICH

MPICH安装与UCXUCX性能测试

运行内部单元测试

已知问题


UCX 的意义

随着DPU的普及、各类DSA芯片的广泛使用,加之之前的IP/TCP硬件资源( RDMA(InfiniBand 和 RoCE)、TCP、GPU、共享内存和网络原子操作)等,不同的硬件资源对应不同的通信编程接口(比如IP/TCP是socket api,RDMA是verbs api) ,给开发人员带来了很大的挑战。如何在这之上抽象出统一的内存访问语义和统一的通信方式是一个很有价值课题。

UCX 通信接口简介

UCX 的全称是 Unified Communication X。正如它名字所展示的,UCX 旨在提供一个统一的抽象通信接口,能够适配任何通信设备,并支持各种应用的需求。

下图是 UCX 官方提供的架构图:

组成部分RoleDescription
UCPProtocol

Implements high-level abstractions such as tag-matching, streams, connection negotiation and establishment, multi-rail, and handling different memory types

实现上层抽象,如标记匹配、流、连接协商和建立、多轨以及处理不同的内存类型

UCTTransport

Implements low-level communication primitives such as active messages, remote memory access, and atomic operations

实现底层通信原语,如active messages、远程内存访问和原子操作

UCSServices

A collection of data structures, algorithms, and system utilities for common use

常用的数据结构、算法和系统实用工具的集合

UCMMemory

Intercepts memory allocation and release events, used by the memory registration cache

截获内存注册缓存使用的内存分配和释放事件

支持的传输(协议)

UCX社区

可以看到,UCX 整体分为两层:上层的 UCP 接口和底层的 UCT 接口。

底层的 UCT 适配了各种通信设备:从单机的共享内存,到常用的 TCP Socket,以及数据中心常用的 RDMA 协议,甚至新兴的 GPU 上的通信,都有很好的支持。

上层的 UCP 则是在 UCT 不同设备的基础上,封装了更抽象的通信接口,以方便应用使用。具体来说有以下几类:

  • Active Message:最底层的接口,提供类似 RPC 的语义。每条 Active Message 会触发接收端进行一些操作。
  • RMA / Atomic:是对远程直接内存访问(RDMA)的抽象。通信双方可以直接读写远端的内存,但是需要有额外的内存注册过程。
  • Tag Matching:常用于高性能计算 MPI 程序中。每条消息都会附带一个 64 位整数作为 tag,接收方每次可以指定接收哪种 tag 的消息。
  • Stream:对字节流(TCP)的抽象。

一般来说,和底层通信设备模型最匹配的接口具有最高的性能,其它不匹配的接口都会有一次软件转换过程。另一方面,同一种 UCP 接口发送不同大小的消息可能也会使用不同的 UCT 方法。例如在 RDMA 网络中,由于内存注册也有不小的开销,因此对于小消息来说,拷贝到预注册好的缓冲区再发送的性能更高。这些策略默认是由 UCX 自己决定的,用户也可以通过设置环境变量的方式手动修改。

在我们的系统中,使用了 UCP Tag 接口并基于此实现了轻量级的 RPC。在 RPC 场景下,Tag 可以用于区分不同上下文的消息:每个链接双方首先随机生成一个 tag 作为请求的标识,对于每次请求再随机生成一个 tag 作为回复的标识。此外 Tag 接口还支持 IO Vector,即将不连续的多个内存段合并成一个消息发送。这个特性可以用来将用户提供的数据缓冲区和 RPC 请求打包在一起,一定程度上避免数据拷贝。

UCX 编程模型简介

UCX 采用了以异步 IO 为核心的编程模型。其中 UCP 层定义的核心对象有以下四种:

  • Context:全局资源的上下文,管理所有通信设备。一般每个进程创建一个即可。
  • Worker:任务的管理调度中心,以轮询方式执行任务。一般每个线程创建一个,会映射为网卡上的一个队列。
  • Listener:类似 TCP Listener,用来在 worker 之间创建连接。
  • Endpoint:表示一个已经建立的连接。在此之上提供了各种类型的通信接口。

它们之间的所属关系如下图所示:

建立连接

UCX 中双方首先要建立连接,拿到一个 Endpoint 之后才能进行通信。建立连接一般要通过 Listener,过程和 TCP 比较类似:

通信双方 A/B 首先建立各自的 Context 和 Worker,其中一方 A 在 Worker 上创建 Listener 监听连接请求,Listener 的地址会绑定到本机的一个端口上。用户需要通过某种方法将这个地址传递给另一方 B。B 拿到地址后在 Worker 上发起 connect 操作,此时 A 会收到新连接请求,它可以选择接受或拒绝。如果接受则需要在 Worker 上 accept 这个请求,将其转换为 Endpoint。之后 B 会收到 A 的回复,connect 操作完成,返回一个 Endpoint。此后双方就可以通过这对 Endpoint 进行通信了。

内存注册

对于常规的通信接口,用户可以直接在 Endpoint 上发起请求。但对于 RMA(远程内存访问)操作,需要被访问的一方首先在自己的 Context 上注册内存,同时指定访问权限,获得一个 Mem handle。然后将这个本地 handle 转化为其他节点可以访问的一串 token,称为 remote key(rkey)。最后想办法把 rkey 传给远端。远端拿着这个 rkey 进行远程内存访问操作。

异步任务处理(重点)

为了发挥最高的性能,整个 UCX 通信接口是全异步的。所谓异步指的是 IO 操作的执行不会阻塞当前线程,一次操作的发起和完成是独立的两个步骤。如此一来 CPU 就可以同时发起很多 IO 请求,并且在它们执行的过程中可以做别的事情。

不过接下来问题来了:程序如何知道一个异步任务是否完成了?常见的有两种做法:主动轮询,被动通知。前者还是需要占用 CPU 资源,所以一般都采用通知机制。在 C 这种传统过程式语言中,异步完成的通知一般通过 回调函数(callback)实现:每次发起异步操作时,用户都需要传入一个函数指针作为参数。当任务完成时,后台的运行时框架会调用这个函数来通知用户。下面是 UCX 中一个异步接收接口的定义:

ucs_status_ptr_t ucp_tag_recv_nb (
  ucp_worker_h worker,
  void ∗ buffer,
  size_t count,
  ucp_datatype_t datatype,
  ucp_tag_t tag,
  ucp_tag_t tag_mask,
  ucp_tag_recv_callback_t cb  // <-- 回调函数
);

// 回调函数接口的定义
typedef void(∗ ucp_tag_recv_callback_t) (
  void ∗request, 
  ucs_status_t status,        // 执行结果,错误码
  ucp_tag_recv_info_t ∗info   // 更多信息,如收到的消息长度等
);

这个接口的语义是:发起一个异步 Tag-Matching 接收操作,并立即返回。当真的收到 tag 匹配的消息时,UCX 后台会处理这个消息,将其放到用户提供的 buffer 中,最后调用用户传入的 callback,通知用户任务的执行结果。

这里有一个很重要的问题是:上面提到的“后台处理”到底是什么时候执行的?答案是 UCX 并不会自己创建后台线程去执行它们,所有异步任务的后续处理和回调都是在 worker.progress() 函数中,也就是用户主动向 worker 轮询的过程中完成的。这个函数的语义是:“看看你手头要处理的事情,有哪些是能做的?尽力去推动一下,做完的通知我。” 换句话说,Worker 正在处理的所有任务组成了一个状态机,progress 函数的作用就是用新事件推动整个状态机的演进。后面我们会看到,对应到 async Rust 世界中,所有异步 IO 任务组成了最基础的 Future,worker 对应 Runtime,而 progress 及其中的回调函数则充当了 Reactor 的角色。

回到传统的 C 语言,在这里异步 IO 的最大难点是编程复杂性:多个并发任务在同一个线程上交替执行,只能通过回调函数来描述下一步做什么,会使得原本连续的执行逻辑被打散到多个回调函数中。本来局部变量就可以维护的状态,到这里就需要额外的结构体来在多个回调函数之间传递。随着异步操作数量的增加,代码的维护难度将会迅速上升。下面的伪代码展示了在 UCX 中如何通过异步回调函数来实现最简单的 echo 服务:

// 由于 C 语言语法的限制,这段代码需要从下往上读

// 这里存放所有需要跨越函数的状态变量
struct CallbackContext {
  ucp_endpoint_h ep;
  void *buf;
} ctx;

void send_cb(void ∗request, ucs_status_t status) {
  //【4】发送完毕
  ucp_request_free(request);
  exit(0);
}

void recv_cb(void ∗request, ucs_status_t status, ucp_tag_recv_info_t ∗info) {
  //【3】收到消息,发起发送请求
  ucp_tag_send_nb(ctx->ep, ctx->buf, info->length, ..., send_cb);
  ucp_request_free(request);
}

int main() {
  // 省略 UCX 初始化部分
  //【0】初始化任务状态
  ctx->ep = ep;
  ctx->buf = malloc(0x1000);
  //【1】发起异步接收请求
  ucp_tag_recv_nb(worker, ctx->buf, 0x1000, ..., recv_cb);
  //【2】不断轮询,驱动后续任务完成
  while(true) {
    ucp_worker_progress(worker);
  }
}

作为对比,假如 UCX 提供的是同步接口,那么同样的逻辑只需要以下几行就够了:

int main() {
  // 省略 UCX 初始化部分
  void *buf = malloc(0x1000);
  int len;
  ucp_tag_recv(worker, buf, 0x1000, &len, ...);
  ucp_tag_send(ep, buf, len, ...);
  return 0;
}

面对传统异步编程带来的“回调地狱”,主流编程语言经过了十几年的持续探索,终于殊途同归,纷纷引入了控制异步的终极解决方案—— async-await 协程。它的杀手锏就是能让开发者用同步的风格编写异步的逻辑。经过我们的封装过后,在 Rust 中用 async 协程编写同样的逻辑是长这样的:

async fn main() {
  // 省略 UCX 初始化部分
  let mut buf = vec![0u8; 0x1000];
  let len = worker.tag_recv(&mut buf, ...).await.unwrap();
  ep.tag_send(&buf[..len], ...).await.unwrap();
}

(摘抄自:《async Rust 封装 UCX 通信库》 https://zhuanlan.zhihu.com/p/397199431) 

mellonx文档《Unified Communication X (UCX)》:https://www.hpcadvisorycouncil.com/events/2019/APAC-AI-HPC/uploads/2018/07/UCX_Accelerate_HPC_Application.pdf 

使用UCX

编译release版本
构建UCX通常是运行“configure”和“make”的组合。在根目录执行以下命令安装UCX系统:

$ ./autogen.sh
$ ./contrib/configure-release --prefix=/where/to/install
$ make -j8
$ make install

注意:在运行configure时,编译对各种网络或其他特定硬件的支持可能需要额外的命令行标志。

编译debug版本

$ ./autogen.sh
$ ./contrib/configure-devel --prefix=$PWD/install-debug

***注意:由于额外的调试代码,UCX的debug版本通常会在运行时带来很大的性能损失。

构建RPM包

$ contrib/buildrpm.sh -s -b

构建DEB 包

$ dpkg-buildpackage -us -uc

构建Doxygen文档

$ make docs

使用UCX安装OpenMPI和OpenSHMEM

Wiki page

使用UCX安装MPICH

Wiki page

MPICH安装与UCXUCX性能测试

Start server:

$ ./src/tools/perf/ucx_perftest -c 0

Connect client:

$ ./src/tools/perf/ucx_perftest <server-hostname> -t tag_lat -c 1

NOTE the -c flag sets CPU affinity. If running both >commands on same host, make sure you set the affinity to different CPU cores.

运行内部单元测试

$ make -C test/gtest test

已知问题

  • UCX version 1.8.0 has a bug that may cause data corruption when TCP transport is used in conjunction with shared memory transport. It is advised to upgrade to UCX version 1.9.0 and above. UCX versions released before 1.8.0 don't have this bug.

  • UCX may hang with glibc versions 2.25-2.29 due to known bugs in the pthread_rwlock functions. When such hangs occur, one of the UCX threads gets stuck in pthread_rwlock_rdlock (which is called by ucs_rcache_get), even though no other thread holds the lock. A related issue is reported in glibc Bug 23844. If this issue occurs, it is advised to use glibc version provided with your OS distribution or build glibc from source using versions less than 2.25 or greater than 2.29.

  • Due to compatibility flaw when using UCX with rdma-core v22 setting UCX_DC_MLX5_RX_INLINE=0 is unsupported and will make DC transport unavailable. This issue is fixed in rdma-core v24 and backported to rdma-core-22.4-2.el7 rpm. See ucx issue 5749 for more details.

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值