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
pybindUpdated 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::MatrixEigen::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
  1. 直接返回 RowMatrixXd
    1. 返回RowMatrixXd &
      1. 返回打包的 RowMatrixXd
        1. 返回打包(引用)的RowMatrixXd

          Hint

          避免 copy

          1. 使用 py::arg().noconvert()
            1. 使用这个参数的话,会在尝试进行隐式复制的时候报错。如果函数接收的参数为 Eigen::Ref<MatrixXd>(非const) 则不需要显式指定此参数。

          Limit

          1. Eigen::Ref<MatrixType> 是默认列优先存储,也可以手动指定为行优先存储(Eigen::RowMajor才能与numpy一致)
            1. 解决方法一: 使用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 提供evalexeceval_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
           

          © Lazurite 2021 - 2024