Pybind11 实践
date
Apr 22, 2022
Last edited time
Mar 27, 2023 08:47 AM
status
Published
slug
Pybind11实践
tags
Programing
summary
type
Post
Field
Plat
pybind11 绑定代码测试绑定 Numpy 数组Eigen 数组接口(常用)Hint避免 copyLimitOpencv接口(常用)高维数组TensorEigen直接使用 Numpy 和 Python 功能opaque typesPytorch Extension添加链接和头文件
pybind11
pybind • Updated Apr 25, 2022
pybind11 绑定代码测试
通过编写
PYBIND11_MODULE(ModuleName,m)
模块,可以绑定 C++ 编写的函数,详细的入门教程及语法可参考官方文档,这里我们简单演示一些基本功能,如这里 m.doc 即是 python 中要显示的模块信息,通过 m.def 绑定了两个函数 “add” 和“add_c”,分别实现整数相加与数组相加。
编译得到 example1.so.xxx 文件,注意文件名与
PYBIND11_MODULE(ModuleName,m)
中的 ModuleName 要一致,否则无法导入模块。把. pyd 文件放在运行目录下启动 python,接下来在 python 中使用如下绑定 Numpy 数组
操作 Numpy 数组,需要引入头文件
<pybind11/numpy.h>
,通过 pybind11::array_t
类可以接收 numpy.ndarray 数组。数组本质上在底层是一块一维的连续内存区,通过 pybind11 中的 request() 函数可以把数组解析成 py::buffer_info
结构体,buffer_info 类型可以公开一个缓冲区视图,它提供对内部数据的快速直接访问。别的类型都比较直观清楚,需要特别注意 strides 这个元素代表的是每个维度相邻元素的字节间隔,numpy 和 C++ 的数组默认都是行优先存储的,那么对于一个 row 行 cols 列的二维 float 数组 (矩阵) 来说,每行 “相邻” 元素(如 array(0,N)和 array(1,N))在行优先的内存上就相隔了一行元素大小的数量,即
sizeof(float) * cols
,每列 “相邻” 元素(如 array(N,0)和 array(N,1))在内存中是紧挨着的,即元素开头的位置相隔了一个元素类型的字节大小 sizeof(float)
,那么这个数组和缓冲区的 strides 就是 { sizeof(float) * array.cols, sizeof(float) }
,这在解析和重包装数组对象时经常用到。py::array_tadd_c(py::array_t arr1, py::array_t arr2)
重新实现数组相加,arr.request() 解析获得 buffer_info 对象,通过 py::array_t(buffer_info)
传入整个 buffer_info 可以重新构造一个完全相同的数组对象,即实现深拷贝,也可以传入一个 buffer_info.shape
构造形状相同的新数组,都是开辟了新的内存空间。获取缓冲对象元素的指针 buffer.ptr 就可以操作元素完成运算,这里 buffer.size 是元素的总数,不管数组是多少维度的,其底层表示均是一维数组,可以用一个大循环直接遍历所有元素,实现数组元素相加。这里采用 openMP 进行简单并行加速,实测运算速度可以达到 numpy 的 90% 以上。
Eigen 数组接口(常用)
py::array_t 的类函数通常比较有限(仅有访问元素 dims等基础功能),对标 numpy 中丰富的线性代数操作难以满足,通常需要转换成 C++ 的数组和矩阵类型来完成相应功能,其中最广泛使用的的矩阵库就是 Eigen,pybind11 也实现了对 Eigen 一维和二维数组的直接转换支持,需要包含头文件
<pybind11/eigen.h>
。下面是一个简单例子,函数参数和返回值都可以直接使用
Eigen::Matrix
和 Eigen::Array
的类型,pybind11 会自动帮你转换。不过这样使用会发现函数运行速度比直接传入
py::array_t
要慢很多,尤其是大型矩阵。实际上,当我们普通的 Eigen::Matrix
对象作为参数和返回值时,为了保证内存安全,pybind11 接受 numpy.ndarray 的输入值,将其值复制到适当的临时数组变量,然后用这个临时变量调用 C++ 函数。即默认情况下是多进行了一次数组内存拷贝的,对于计算量很小的矩阵四则运算等操作,这会显著增加函数运行的总时间!那么有没有不进行复制而是引用传递的方法呢?答案是使用 Eigen::Ref 和 Eigen::Map,这样 pybind11 默认会简单地引用返回的数据,但是须注意确保这些数据保持有效 (不能在返回前被销毁)。特别注意,由于 numpy 和 Eigen 对数据的默认存储顺序不同 (Numpy 行优先,Eigen 列优先),有时会遇到限制,需要在创建
Eigen::Matrix
对象使用 Eigen::Rowmajor
参数指定为行优先数组,否则转换时有可能会发生内存泄漏导致函数崩溃。如果自定义函数中没有使用
Eigen::Ref
和 Eigen::Map 返回参数,为了避免数组被复制,可以在绑定函数中使用 pybind11 的返回值策略 py::return_value_policy::reference_internal
来返回引用值稀疏矩阵类型 scipy.sparse.csr_matrix/scipy.sparse.csc_matrix 不支持按引用传递,它们总是被复制按值传递的。
Example
- 直接返回
RowMatrixXd
- 返回
RowMatrixXd &
- 返回打包的
RowMatrixXd
- 返回打包(引用)的
RowMatrixXd
Hint
避免 copy
- 使用
py::arg().noconvert()
使用这个参数的话,会在尝试进行隐式复制的时候报错。如果函数接收的参数为
Eigen::Ref<MatrixXd>
(非const) 则不需要显式指定此参数。Limit
Eigen::Ref<MatrixType>
是默认列优先存储,也可以手动指定为行优先存储(Eigen::RowMajor
才能与numpy一致)
解决方法一: 使用
Eigen::Ref<RowMatrixXd>。
这是通用的解决方式,但是在性能上不如第二种。但可以使用数组切片解决方法二:更改默认的Eigen存储方式,但在
a2=a.transpose()
时有问题,因为a, a2
共用相同的数据。并且不能使用切片。Opencv接口(常用)
Example
高维数组
Tensor
如果直接使用Tensor,那么直接返回tuple即可,不会进行复制
Eigen
直接使用 Numpy 和 Python 功能
通过 Eigen::Tensor 可以部分处理高维数组数据,但是 Eigen::Tensor 的特征方法较少且文档不健全,很多 Numpy 功能没有对应的函数,实际使用来说还是很不方便。为了保证 Numpy 的完整数组功能,我们希望可以在 C++ 中直接使用 Numpy 的函数功能。
通过
py::moudle::attr()
就可以实现链接到当前激活的 python 环境,直接调用 python 中相应类型的函数,需要 #include <pybind11/embed.h>
。py::module::import()
可以将 python 标准库或当前 python 环境中定义的对象到 C++ 环境下共同使用,这真正意义上的 “混合编程”。py::object np = py::module::import(“numpy”)
等价于 python 中的 import numpy as np
,之后就可以使用 np 的函数和属性了,使用格式为 (py::object 变量).attr(“python 中的函数名”)(参数)。_a 是
py::literals
中的迭代器别名,用来输入 python 中的 Keyword 参数,如 “dtype”_a = “double”
。仍然有一些 python 特性无法通过
py::moudle::attr()
和 C++ 方法给出(attr() 中只能输入函数名,非函数特性则不行),比如数组的切片和列表索引特性、布尔数组等。pybind11 提供eval
exec
和eval_file
函数来直接运行 Python 表达式和语句,如下所示。local = py::dict()
用来接收和转换 python++ 中对应的一组参数,py::dict()[“python 变量名”] = C++ 变量
py::dict()
传入 eval, exec, eval_file 时可以接收 Python 表达式产生的所有变量,保存变量名为 key 值,通过下面的语句可以用 C++ 变量接收之。C++ 变量 = py::dict()[“python 变量名”]
这样基本上能够在 C++ 中完全使用 python 的模块属性了。如果有用 c++ 来编写 python 模块的需求,pybind11 真的是一个利器,在本文我只是介绍了 pybind11 很小一部分常用的功能,还有很多功能是需要参考官方文档去实践的
opaque types
pybind11 严重依赖模板匹配机制来转换从 STL 数据类型(如向量、链表、哈希表等)构造的参数和返回值。这甚至以递归方式工作,例如处理哈希映射列表成对的基本类型和自定义类型等。
这意味着参数的传递经过了类型转换以及复制,因此C++端的引用的应用将不会修改python端的数据。
并且,如果复制处理非常大的列表时,操作可能会很昂贵。为了处理上述所有情况,pybind11 提供了一个名为的宏
PYBIND11_MAKE_OPAQUE(T)
,它禁用基于模板的类型转换机制,从而使它们变得不透明。不透明对象的内容永远不会被检查或提取,因此它们可以通过引用传递。例如,要变成std::vector<int>
不透明类型,请添加声明:Pytorch Extension
由于Pytorch给的编译方式简单,因此无法像CMAKE一样配置。这就需要记录一下,如何添加链接和头文件等功能。
配置参考如下
添加链接和头文件
例如需要使用
Eigen
编程,则需要在这里列举一下两者等效的命令
添加头文件目录 | include_directories | -I |
添加需要链接的库文件目录 | LINK_DIRECTORIES | -L |
链接库的名称 | TARGET_LINK_LIBRARIES | -l |