【RDMA】20. RDMA之Pyverbs(Python Verbs)

【RDMA】RDMA 学习资料总目录_bandaoyu的博客-CSDN博客SavirRDMA 分享1. RDMA概述https://blog.csdn.net/bandaoyu/article/details/112859853https://zhuanlan.zhihu.com/p/1388747382. 比较基于Socket与RDMA的通信https://blog.csdn.net/bandaoyu/article/details/1128613993. RDMA基本元素和编程基础https://blog.csdn.net/bandaoyu/article/de.https://blog.csdn.net/bandaoyu/article/details/120485737

转自:https://zhuanlan.zhihu.com/p/455174484作者:Savir

本文欢迎非商业转载,转载请注明出处。

Verbs是RDMA的编程接口,是所有上层应用程序/中间件的基础。在“RDMA之Verbs”一文中我们曾讲过,IB规范本身只对Verbs做出了定义和描述,包括有哪些用户接口,以及这些接口的参数和返回值等等,但是它并没有对软件如何实现Verbs API做出描述。前面的文章中我们着重介绍了基于C语言实现的Verbs API,但其实在符合IB规范的前提下,C语言并不是Verbs唯一的实现方式。

本文中,我将带大家了解近这几年间快速发展并趋于成熟的基于Cython语言(不是Python)的Verbs API——Pyverbs,包括其实现原理、特点以及使用方式等。Pyverbs的接口可以直接被Python程序调用,有了Pyverbs和Soft-RoCE的加持,大家就可以快速、低成本地编写一些基于Python的RDMA程序来进行学习和探索了。

概述

Verbs API有多种实现,最基础也是最常见的Verbs API是基于C语言实现的,这套接口被集成在rdma-core,也就是用户态RDMA协议栈中。这些API以ibv_为前缀,比如ibv_create_qp(),ibv_reg_mr()等等。

此外还有基于Java语言实现的jVerbs,底层是基于C语言的Verbs API实现的。

而本文要介绍的Pyverbs,是一套基于Cython实现的Verbs接口,可以直接被Python程序调用。其底层也是基于C语言的Verbs API实现的。

相比于C语言的API,Pyverbs有以下优点:

  • 用户不需要考虑资源申请和释放,可以快速实现稳定运行的RDMA应用程序
  • 可以结合python提供的各种库,灵活实现各种扩展功能,比如结合pyverbs实现RDMA测试框架

Pyverbs本身也是包含在rdma-core软件包中的,经过几年的发展,目前已经能够提供几乎所有C语言Verbs所实现的功能。

Cython

以下描述摘自维基百科:

Cython是结合了Python和C的语法的一种语言,可以简单的认为就是给Python加上了静态类型后的语法,用户可以维持大部分的Python语法,而不需要大幅度调整主要的程序逻辑与算法。但由于会直接编译为二进制程序,所以性能较Python会有很大提升。

Cython主要有两种使用场景:

  1. 通过重写部分Python代码、翻译成C代码之后编译为二进制程序,来提升Python程序的性能。
  2. 将C/C++代码封装为Python库,方便Python程序调用。

Pyverbs的实现利用的是Cython的第二个使用场景,将其作为C和Python之间的桥梁,即将C语言的Verbs API封装为Python API,从而可以大大降低编写RDMA程序的难度。Pyverbs、C Verbs以及用户应用的分层关系如下图所示:

Pyverbs和C Verbs等的关系

下载代码

Pyverbs代码位于OFED推动维护的RDMA用户态软件包rdma-core仓库:

rdma-core​github.com/linux-rdma/rdma-core

请将该仓库克隆到机器中:

git clone https://github.com/linux-rdma/rdma-core.git

部署

基于Pyverbs编写RDMA程序依赖于一系列基于Cython编写、gcc编译生成的动态链接库so文件。我们首先需要编译rdma-core工程,生成这些文件。

如果读者是基于本专栏"RoCE & Soft-RoCE"和"iWARP & Soft-iWARP"两篇文章中已经介绍过的和RXE(Soft-RoCE)或者SIW(Soft-iWARP)做实验,那么建议直接在之前配置好的虚拟机中进行下面的操作。

准备环境

请参考rdma-core仓库的README中的Building章节准备编译环境,除了常见的编译用的GCC、Cmake之外,还需要libnl-3等库,此外Pyverbs还依赖于Python3以及Cython。

编译工程

如果读者使用的是跟我一样的虚拟机镜像(见“RoCE & Soft-RoCE”一文),那么直接到rdma-core仓库根目录下执行下面的脚本就可以了。:

./build.sh

如果读者使用的是自己安装的其他Linux发行版,请观察有无报错并根据错误信息安装对应的软件包。如果无异常,也请注意执行脚本后的打印中是否有下面的“Found cython”一行:

检查Cython编译器是否成功被检测到

因为Pyverbs不是编译的必须项,如果未找到Cython,编译不会失败,但是也不会生成Pyverbs的动态链接库文件。

如果大家在编译过程中遇到问题,请在本文评论区提出。

代码结构

编译之后,将会生成build文件夹,我们简单介绍下rdma-core代码仓当中本文内容相关的目录:

.
├── build........................................................构建生成的文件目录
│   ├── bin...........................存放可执行文件的目录,主要是一些示例程序和小工具
│   │   ├── run_tests.py................................ 执行tests下面用例的入口脚本
│   ├── lib.............................................C verbs编译生成的动态链接库
│   ├── python
│   │   ├── cython...............................................Cython的可执行文件
│   │   └── pyverbs
│   │       ├── *.py....................................pyverbs用到的公共python文件
│   │       └── *.so..........................................pyverbs动态链接库文件
│   └── pyverbs
│       └── *.c.........................................Cython代码翻译成的c中间文件
├── Documentation
│   └── pyverbs.md..............................................pyverbs接口说明文档
├── libibverbs
│   └── *.c........................................................C Verbs API源码
├── librdmacm
│   └── *.c.........................................................CM建链 API源码
├── pyverbs........................................................Pyverbs源码目录
│   ├── examples
│   │   └── ib_devices.py...........................................Pyverbs示例程序
│   ├── *.pyx.........................................................Cython源文件
│   ├── *.pxd.........................................................Cython头文件
│   └── *.py.....................................................其他公共Python文件
└── tests
    └── *.py.............................................官方基于unittest的测试用例

配置环境变量*

因为我们编译产生的Pyverbs的so并没有在Python解释器查找动态链接库的目录中,如果不想每次都指定PYTHONPATH,可以通过set等命令设置环境变量,也可以将其加入.bashrc等环境配置文件中,比如将下列语句加入~/.bashrc

export PYTHONPATH=$PYTHONPATH:"/home/savir/repo/rdma-core/build/python"

然后执行下面的命令刷新环境变量:

source ~/.bashrc

如果大家使用venv虚拟环境(推荐),也可以在虚拟环境配置文件里面配置,请参考:

python - How do you set your pythonpath in an already-created virtualenv? - Stack Overflow

执行示例程序

为了示例程序的演示效果,首先请先加载RDMA设备(以RXE为例):

sudo modprobe rdma_rxe
sudo rdma link add rxe_0 type rxe netdev ens33

Pyverbs的示例程序位于rdma-core/pyverbs/example目录下,如果大家没有添加环境变量,这里我们先手动指定PYTHONPATH并执行该脚本。

PYTHONPATH=../../build/python ./ib_devices.py

如果已经添加环境变量的话前面的“PTYHONPATH=xxx”就不需要了。

执行效果如下图所示,可以看到该脚本的功能是扫描并列出本机的RDMA设备及其类型等信息:

示例程序执行结果

我将在下文以该程序为例解释Pyverbs的实现原理。

执行测试框架

除了上述示例程序之外,可以通过执行rdma-core/build/bin/run_tests.py来执行官方提供的基于Python的unittest框架编写的测试用例,比如:

./run_tests.py --dev rxe_0 -v test_qp

上面的命令会执行tests目录下对QP的测试用例,结果如下:

因为有一些测试用例RXE是不支持的,所以结果显示一些用例被跳过是正常的。

实现原理

下面我们以上一节中大家执行过的ib_devices.py为例讲一下Pyverbs的实现原理,本节需要大家有一点点Python的基础。

前文已经讲过Pyverbs最终还是调用的C Verbs API,那么这条通路是如何实现的呢?先来看pyverbs/example/ib_devices.py:

from pyverbs import device as d
import sys

lst = d.get_device_list()
dev = 'Device'
node = 'Node Type'
trans = 'Transport Type'
guid = 'Node GUID'
print_format = '{:^20}{:^20}{:^20}{:^20}'
print (print_format.format(dev, node, trans, guid))
print (print_format.format('-'*len(dev), '-'*len(node), '-'*len(trans),
       '-'*len(guid)))
for i in lst:
    print (print_format.format(i.name.decode(), d.translate_node_type(i.node_type),
           d.translate_transport_type(i.transport_type),
           d.guid_to_hex(i.guid))

除了前三条语句之外,后面全是打印相关的逻辑,因此我们只关注前几行即可。

首先,引入了pyverbs库中的device符号,那么这个pyverbs库在哪里呢?还记得我们之前执行该用例的语句吗:

PYTHONPATH=../../build/python ./ib_devices.py

这里的PYTHONPATH后面的目录就是其所在的位置,这个目录下几乎全是动态链接库文件,那么device符号对应的就是device.cpython-38-x86_64-linux-gnu.so这个文件了。

Pyverbs相关的so文件

这个so是通过GCC编译.c源文件生成的,对应的源文件是位于build/pyverbs下的device.c。如果打开这个.c文件,读者会发现很难看懂,它是Cython编译器根据Cython源文件编译而来的。

Cython语言编写的文件的后缀名是pyx和pxd,pyx类似于C语言的.c源文件,pyd类似于C语言的.h头文件。device.c对应的Cython源文件是pyverbs/device.pyx,对应的Cython头文件是pyverbs/device.pxd。

我们浏览一下源文件:

def get_device_list():
    """
    :return: list of IB_devices on current node
             each list element contains a Device with:
                 device name
                 device node type
                 device transport type
                 device guid
                 device index
    """
    cdef int count = 0;
    cdef v.ibv_device **dev_list;
    dev_list = v.ibv_get_device_list(&count)

        ...

    return devices

可以看到,最终调用了v.ibv_get_device_list,也就是C Verbs提供的API,这些API位于libibverbs.so动态链接库中。

所以,当用户在脚本中调用了d.get_device_list()之后,首先会被定位到device.cpython-38-x86_64-linux-gnu.so动态链接库文件中,然后调用libibverbs.so提供的ibv_get_device_list接口。

那么Pyverbs大致的实现原理就清楚了,我们来总结一下:

  • 编译的主要流程:
  • Cython静态编译器将Cython代码编译生成.c文件
  • GCC等编译器将.c文件编译成动态链接库.so文件

编译流程

  • 在用户执行Python程序之后:
  • Python解释器载入Pyverbs的动态链接库.so文件
  • 执行到对应的函数之后,会跳转到C Verbs的动态链接库.so文件(libibverbs.so)

调用关系

实验

IDE

目前比较流行的Python IDE有Pycharm和VS Code,关于使用哪个IDE来编写程序,不是本文所要讨论的内容。不过经过对比,我建议大家使用PyCharm Professional,因为其对Python的支持更全面,并且支持从Python语句到Cython源文件的跳转,学生还可以用学校的教育邮箱申请免费的Licences。

如果读者使用的是虚拟机,建议在宿主机上安装IDE,然后通过ssh远程连接到虚拟机的方式编写代码,VS Code上的Remote ssh插件和Pycharm 2021.3.1版本之后新加入的JetBrains Gateway插件都可以实现,读者也可以直接下载JetBrains Gateway安装包,然后根据安装程序的引导在虚拟机侧安装PyCharm的Server端。

至于如何配置这些IDE,大家自己百度吧~

编写程序

关于Pyverbs提供的Python接口及其使用方式,读者可以阅读官方提供的文档pyverbs.md或者直接阅读pyverbs目录下的Cython源码,因为跟Python语法很像,所以会写Python的同学很容易看懂。因为我们的目的是通过Pyverbs提供的Python库来编写RDMA代码,所以不必关心Cython的代码细节,只关注接口即可。

需要注意的是,如果代码和文档有不同步的情况,请以代码为准。这是因为有时候上传代码的人忘了同步修改造成的。(至于如何向开源社区提交代码来修改这些小问题,以后我再专门介绍)

例子

下面是我写的基于Pyverbs实现的双端通信的的示例程序的主体逻辑,大家可以参考它去编写自己的程序。

开源地址:

https://github.com/Li-Weihang/python_rdma_test​github.com/Li-Weihang/python_rdma_test

#!/usr/bin/python3.8
import sys

sys.path.append('..')

from utils.connection import SKT, CM
from utils.param_parser import parser

from pyverbs.addr import AH, AHAttr, GlobalRoute
from pyverbs.cq import CQ
from pyverbs.device import Context
from pyverbs.enums import *
from pyverbs.mr import MR
from pyverbs.pd import PD
from pyverbs.qp import QP, QPCap, QPInitAttr, QPAttr
from pyverbs.wr import SGE, RecvWR, SendWR

RECV_WR = 1
SEND_WR = 2
GRH_LENGTH = 40

args = parser.parse_args()

server = not bool(args['server_ip'])

if args['use_cm']:
    conn = CM(args['port'], args['server_ip'])
else:
    conn = SKT(args['port'], args['server_ip'])

print('-' * 80)
print(' ' * 25, "Python test for RDMA")

if server:
    print("Running as server...")
else:
    print("Running as client...")

print('-' * 80)

if args['qp_type'] == IBV_QPT_UD and args['operation_type'] != IBV_WR_SEND:
    print("UD QPs don't support RDMA operations.")
    conn.close()

conn.handshake()

ctx = Context(name=args['ib_dev'])
pd = PD(ctx)
cq = CQ(ctx, 100)

cap = QPCap(max_send_wr=args['tx_depth'], max_recv_wr=args['rx_depth'], max_send_sge=args['sg_depth'],
            max_recv_sge=args['sg_depth'], max_inline_data=args['inline_size'])
qp_init_attr = QPInitAttr(qp_type=args['qp_type'], scq=cq, rcq=cq, cap=cap, sq_sig_all=True)
qp = QP(pd, qp_init_attr)

gid = ctx.query_gid(port_num=1, index=args['gid_index'])

# Handshake to exchange information such as QP Number
remote_info = conn.handshake(gid=gid, qpn=qp.qp_num)

gr = GlobalRoute(dgid=remote_info['gid'], sgid_index=args['gid_index'])
ah_attr = AHAttr(gr=gr, is_global=1, port_num=1)

if args['qp_type'] == IBV_QPT_UD:
    ah = AH(pd, attr=ah_attr)
    qp.to_rts(QPAttr())
else:
    qa = QPAttr()
    qa.ah_attr = ah_attr
    qa.dest_qp_num = remote_info['qpn']
    qa.path_mtu = args['mtu']
    qa.max_rd_atomic = 1
    qa.max_dest_rd_atomic = 1
    qa.qp_access_flags = IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_LOCAL_WRITE
    if server:
        qp.to_rtr(qa)
    else:
        qp.to_rts(qa)

conn.handshake()

mr_size = args['size']
if server:
    if args['qp_type'] == IBV_QPT_UD:   # UD needs more space to store GRH when receiving.
        mr_size = mr_size + GRH_LENGTH
    content = 's' * mr_size
else:
    content = 'c' * mr_size

print("mr size = " + str(mr_size))

mr = MR(pd, mr_size, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ)
sgl = [SGE(mr.buf, mr.length, mr.lkey)]

if args['operation_type'] != IBV_WR_SEND:
    remote_info = conn.handshake(addr=mr.buf, rkey=mr.rkey)

def read_mr(mr):
    if args['qp_type'] == IBV_QPT_UD and server:
        return mr.read(mr.length - GRH_LENGTH, GRH_LENGTH).decode()
    else:
        return mr.read(mr.length, 0).decode()    

for i in range(args['iters']):
    print("Iter: " + f"{i + 1}/{args['iters']}")

    mr.write(content, len(content))
    print("MR Content before test:" + read_mr(mr))

    if server and args['operation_type'] == IBV_WR_SEND:
        wr = RecvWR(RECV_WR, len(sgl), sgl)
        qp.post_recv(wr)

    conn.handshake()

    if not server:
        wr = SendWR(SEND_WR, opcode=args['operation_type'], num_sge=1, sg=sgl)
        if args['qp_type'] == IBV_QPT_UD:
            wr.set_wr_ud(ah, remote_info['qpn'], 0)
        elif args['operation_type'] != IBV_WR_SEND:
            wr.set_wr_rdma(remote_info['rkey'], remote_info['addr'])

        qp.post_send(wr)

    conn.handshake() # delete

    if not server or args['operation_type'] == IBV_WR_SEND:
        wc_num, wc_list = cq.poll()

    print("MR Content after test:" + read_mr(mr))

conn.handshake()
conn.close()

print('-' * 80)

执行

如果已经配置了环境变量,直接执行即可。

查看帮助

大家可以通过-h或者--help查看支持的参数:

./rdma_test.py -h

测试

我们执行一个测试看看,参数选择的是RDMA Write操作,迭代5次。

  • Server端
./rdma_test.py -d rxe_0 -o write -n 5
  • Client端
./rdma_test.py -d rxe_0 -o write -n 5 192.168.31.109

执行结果

可以看到每次迭代,Server端的MR的内容都从一串’s’被Client端写成了一串’c’。

  • Server端

Server端执行结果

  • Client端

Client端执行结果

好了,Pyverbs就介绍到这里,文章有错误或者有疑惑的话请大家在评论区提出来。

以后本专栏的文章中会尽量在讲解之后附带一些基于Python的实验,以方便大家理解。

参考文档

[1] Cython的概念. Cython - 维基百科

[2] Cython 基本用法. 陈乐群. Cython 基本用法 - 知乎

posted on 2022-10-04 01:21  bdy  阅读(89)  评论(0编辑  收藏  举报

导航