【RDMA】20. RDMA之Pyverbs(Python Verbs)
转自: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主要有两种使用场景:
- 通过重写部分Python代码、翻译成C代码之后编译为二进制程序,来提升Python程序的性能。
- 将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-coregithub.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_testgithub.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 基本用法 - 知乎