【问题标题】:Return Array of Eigen::Matrix from C++ to Python without copying将 Eigen::Matrix 的数组从 C++ 返回到 Python 而不复制
【发布时间】:2020-11-19 09:50:24
【问题描述】:

我有一些 C++ 代码可以生成和操作 Eigen 矩阵的数组。 最后我想在 python 中使用这些矩阵,并认为这可能是pybind11 的工作。

基本上我想要在 python 中返回的是两个嵌套列表/numpy 数组 mat_a(I, 4, 4)mat_b(J, K, 4, 4)。 因为我必须在 C++ 中做很多线性代数的事情,所以我想使用 Eigen,我使用的数据结构是 std::array<std::array<Eigen::Matrix4f, 2>, 3>>> mat_b // for J=3, K=2。 现在的问题是如何有效地将它传递给python?

此外,我想对多个输入 x = [x_0, x_1, ..., x_N] 执行这些计算,结果是 mat_a(N, I, 4, 4)mat_b(N, J, K, 4, 4)。每个 x_i 的计算都是独立的,但我认为在 C++ 中通过 x_i 编写这个循环可能会更快。另一方面,如果我们在 C++ 中只有固定大小的数组,任务会变得更容易,这个循环也可以转移到 python。

这是我的问题的一些虚拟代码(I=5,J=3,K=2):

// example.cpp
#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
#include <pybind11/stl.h>
#include <pybind11/functional.h>
#include <pybind11/stl_bind.h>

#include <array>
#include <vector>
#include <Eigen/Dense>


Eigen::Matrix4f get_dummy(){
    Eigen::Matrix4f mat_a;
    mat_a << 1, 2, 3, 4,
             5, 6, 7, 8,
             9, 8, 7, 6,
             5, 4, 3, 2;
    return mat_a;
}

std::pair< std::vector<std::array<Eigen::Matrix4f, 5> >,
           std::vector<std::array<std::array<Eigen::Matrix4f, 2>, 3> > >  get_matrices(std::vector<float> & x){

    std::vector<std::array<Eigen::Matrix4f, 5> > mat_a(x.size());
    std::vector< std::array< std::array< Eigen::Matrix4f, 2>, 3> > mat_b(x.size());

    //    for (u_int i=0; i< x.size(); i++)
    //        do_stuff(x[i], mat_a[i], mat_b[i]);
    mat_a[0][0] = get_dummy();

    return std::make_pair(mat_a, mat_b);
    }


PYBIND11_MODULE(example, m) {
    m.def("get_dummy", &get_dummy, pybind11::return_value_policy::reference_internal);
    m.def("get_matrices", &get_matrices, pybind11::return_value_policy::reference_internal);
}

我通过以下方式编译代码:

c++ -O3 -Wall -shared -std=c++14 -fPIC `python3 -m pybind11 --includes` example.cpp -o example`python3-config --extension-suffix`

比在 python 中使用它:

import numpy as np
import example

x = np.zeros(1000)

mat_a, mat_b = get_matrices(x)

print(np.shape(mat_a))
print(np.shape(mat_b))
print(mat_a[0][0])

如果我只想返回一个Eigen::Matrix,它会运行得很快,而且据我所知无需复制。但是当我尝试将Eigen:Matricesstd::array/std::vector 嵌套时,pybind 返回一个嵌套的numpy 数组列表,而不是一个多维数组。 这正如预期的那样,实际上我对它的效果印象深刻,但对我来说似乎相当慢,尤其是随着数组尺寸的增长。

问题是如何改进这一点以获得多维 numpy 数组而无需不必要的复制。

我尝试了一些道路但没有奏效(对我来说,这并不意味着它们通常不起作用;我只是想不通):

  • 使用Eigen::Tensor 代替Eigen:Matrix 的数组
  • 在 python 中创建矩阵并通过引用将其传递给 C++
  • 为数组构建自定义包装器, J>

【问题讨论】:

  • 一个numpy 多维数组有一个一维数据缓冲区,并使用shapestrides 来处理多维性。如果您的 c++ 结构没有兼容的布局,则无法避免复制。例如,我相信c 矩阵通常是指向数组的指针数组。这更像是 Python 列表布局。
  • 一种可能的解决方法是在 Python 端(nd-array)分配一块内存,而不是为这个同质的内存块(具有相应的偏移量)创建多个视图/映射(您的矩阵)在 Eigen/C++ 方面并在那里与他们合作。 eigen.tuxfamily.org/dox/group__TutorialMapClass.html

标签: python c++ numpy eigen pybind11


【解决方案1】:

如果您不太依赖于 Eigen,另一种可能性是 xtensor ( found here)。我已经使用了他们的 python 绑定,之前给出了一个直接与 python 通信的示例(found here)。这将具有能够处理更大的多维数组的优势。线性代数不会那么光滑(在那里很难击败 Eigen),但会类似于你在 numpy 中所做的(例如np.dot(A,B)

如果您想坚持使用 Eigen,请注意使用 STL 的一些技术细节。由于您的 std::array 不再能够包含固定数量的矩阵,当您移动到 ​​std::vector 时,您将遇到对齐问题(诚然,我并不完全理解)。很快就会为您提供一个有效的 xtensor 实现。

【讨论】:

    【解决方案2】:

    您最好的选择可能是在 python 端创建数据,以便对其进行重新计数并收集垃圾。

    test.py

    import example
    import numpy as np
    
    array = np.zeros((3, 2, 4, 4), 'f4')
    
    example.do_math(array, 3, 2)
    print(array[0, 0])
    

    example.cpp

    #define PY_SSIZE_T_CLEAN
    #include <Python.h>
    
    #include <Eigen/Dense>
    
    Eigen::Matrix4f get_dummy() {
        Eigen::Matrix4f mat_a;
        mat_a << 1, 2, 3, 4,
                 5, 6, 7, 8,
                 9, 8, 7, 6,
                 5, 4, 3, 2;
        return mat_a;
    }
    
    PyObject * example_meth_do_math(PyObject * self, PyObject * args, PyObject * kwargs) {
        static char * keywords[] = {"array", "rows", "cols", NULL};
    
        PyObject * array;
        int rows, cols;
    
        if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Oii", keywords, &array, &rows, &cols)) {
            return NULL;
        }
    
        Py_buffer view = {};
        if (PyObject_GetBuffer(array, &view, PyBUF_SIMPLE)) {
            return NULL;
        }
    
        Eigen::Matrix4f * ptr = (Eigen::Matrix4f *)view.buf;
    
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                ptr[i * cols + j] = get_dummy();
            }
        }
    
        PyBuffer_Release(&view);
        Py_RETURN_NONE;
    }
    
    PyMethodDef module_methods[] = {
        {"do_math", (PyCFunction)example_meth_do_math, METH_VARARGS | METH_KEYWORDS, NULL},
        {},
    };
    
    PyModuleDef module_def = {PyModuleDef_HEAD_INIT, "example", NULL, -1, module_methods};
    
    extern "C" PyObject * PyInit_example() {
        PyObject * module = PyModule_Create(&module_def);
        return module;
    }
    

    setup.py

    from setuptools import Extension, setup
    
    ext = Extension(
        name='example',
        sources=['./example.cpp'],
        extra_compile_args=['-fpermissive'],
        include_dirs=['.'], # add the path of Eigen
        library_dirs=[],
        libraries=[],
    )
    
    setup(
        name='example',
        version='0.1.0',
        ext_modules=[ext],
    )
    

    从这里添加第二个参数并使用两个数组进行计算应该很简单。

    您可以使用python setup.py develop 构建它。

    如果您想分发它,您可以使用python setup.py bdist_wheel 创建一个 Wheel 文件。

    我使用numpy创建数据,这样可以确保数据的底层内存是C连续的。

    这个例子很简单,它使用一个 Matrix4f 指针来迭代一个 3x2 矩阵数组。随意将ptr 转换为Eigen::Array&lt;Eigen::Matrix4f&gt;, 3, 2&gt;。您不能将其转换为 std::vector,因为 std::vector 的内部数据包含一个指针。

    请注意std::vector&lt;std::array&lt;...&gt;&gt; 在内存中没有一个连续的数组。请改用Eigen::Array

    编辑:

    这是一个使用EigenArrayMap的函数:

    PyObject * example_meth_do_math(PyObject * self, PyObject * args, PyObject * kwargs) {
        static char * keywords[] = {"array", NULL};
    
        PyObject * array;
    
        if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &array)) {
            return NULL;
        }
    
        Py_buffer view = {};
        if (PyObject_GetBuffer(array, &view, PyBUF_SIMPLE)) {
            return NULL;
        }
    
        Eigen::Map<Eigen::Array<Eigen::Matrix4f, 2, 3>> array_map((Eigen::Matrix4f *)view.buf, 2, 3);
    
        for (int i = 0; i < 2; ++i) {
            for (int j = 0; j < 3; ++j) {
                array_map(i, j) = get_dummy();
            }
        }
    
        PyBuffer_Release(&view);
        Py_RETURN_NONE;
    }
    

    【讨论】:

    • 可以从 C++ 创建一个包含您在 C++ 中定义的数据类型的新类型并使用它,但是由于您类型的向量结构中的数组中的特征,您永远不会在单个 numpy 对象。即使您设法定义了一个在内存中调用PyMemoryView_FromMemory 的连续类型,那么np.frombuffer 也不会简单。
    • 非常感谢!这很好用,添加第二个数组很简单。我想问两件事。首先,您能否将演员阵容从ptr 拼写为Eigen::Array。是否有必要使用setup.py 文件,或者我可以使用正确的编译器标志集来实现相同的目的吗?或者一般来说,您会为更大的项目推荐(多个)setup.py 文件,因为它可以更好地自动化/有优势吗?
    • 我将很快编辑答案以使用Eigen::Array,因为我从不使用 Eigen,这将需要一些时间。我更喜欢glm。当您调用python setup.py develop 时,编译和链接步骤将打印到控制台。
    • 您可以去掉 setup.py 并在 makefile 中使用这些命令。我建议根据您的大项目的独立部分保留 setup.py。当较小的部分独立编译并且也可以进行单元测试时,它真的很方便。如果您将它们保留为单独的存储库,则相应地标记版本并将 requirements.txt 添加到您的主项目中。另一种选择是 git 子模块。也可以有一个 setup.py 和许多 Extensions。
    • 您不能在运行时更改模板参数。您可以使用的是“步幅”,我已经看到 Eigen 支持 Map 的自定义步幅。寻找 Array 和 Map 的替代品。对我来说,特征源是大麦可读的。创建有关 Map 和 Array 的新问题。 Eigen 专家可能会为您提供帮助。
    猜你喜欢
    • 1970-01-01
    • 2022-12-03
    • 2021-06-08
    • 2021-12-19
    • 1970-01-01
    • 2019-07-08
    • 1970-01-01
    • 1970-01-01
    • 2022-11-17
    相关资源
    最近更新 更多