【问题标题】:returning numpy arrays via pybind11通过 pybind11 返回 numpy 数组
【发布时间】:2017-11-23 09:40:20
【问题描述】:

我有一个 C++ 函数计算一个大张量,我想通过 pybind11 将它作为 NumPy 数组返回给 Python。

从 pybind11 的文档来看,使用 STL unique_ptr 似乎是可取的。 在下面的示例中,注释掉的版本有效,而给定的版本编译但在运行时失败(“无法将函数返回值转换为 Python 类型!”)。

为什么智能指针版本失败?创建和返回 NumPy 数组的规范方法是什么?

PS:由于程序结构和数组的大小,最好不要复制内存,而是从给定的指针创建数组。内存所有权应由 Python 取得。

typedef typename py::array_t<double, py::array::c_style | py::array::forcecast> py_cdarray_t;

// py_cd_array_t _test()
std::unique_ptr<py_cdarray_t> _test()
{
    double * memory = new double[3]; memory[0] = 11; memory[1] = 12; memory[2] = 13;
    py::buffer_info bufinfo (
        memory,                                   // pointer to memory buffer
        sizeof(double),                           // size of underlying scalar type
        py::format_descriptor<double>::format(),  // python struct-style format descriptor
        1,                                        // number of dimensions
        { 3 },                                    // buffer dimensions
        { sizeof(double) }                        // strides (in bytes) for each index
    );

    //return py_cdarray_t(bufinfo);
    return std::unique_ptr<py_cdarray_t>( new py_cdarray_t(bufinfo) );
}

【问题讨论】:

    标签: python c++ arrays numpy pybind11


    【解决方案1】:

    一些 cmets(然后是一个有效的实现)。

    • pybind11 围绕 Python 类型(如 pybind11::objectpybind11::list 以及在本例中为 pybind11::array_t&lt;T&gt;)的 C++ 对象包装器实际上只是底层 Python 对象指针的包装器。在这方面,已经承担了共享指针包装器的角色,因此将其包装在 unique_ptr 中是没有意义的:直接返回 py::array_t&lt;T&gt; 对象本质上只是返回了一个美化的指针。
    • pybind11::array_t 可以直接从数据指针构造,因此您可以跳过py::buffer_info 中间步骤,只需将形状和步幅直接提供给pybind11::array_t 构造函数。以这种方式构造的 numpy 数组不会拥有自己的数据,它只会引用它(也就是说,numpy owndata 标志将设置为 false)。
    • 内存所有权可以与 Python 对象的生命周期相关联,但您仍然需要正确地进行释放。 Pybind11 提供了一个py::capsule 类来帮助你做到这一点。您要做的是通过将其指定为array_tbase 参数,使numpy 数组依赖于这个胶囊作为它的父类。这将使 numpy 数组引用它,只要数组本身还活着,它就会保持活动状态,并在不再引用它时调用清理函数。
    • 旧版本(2.2 之前)中的c_style 标志仅对新数组有影响,即在不传递值指针时。这在 2.2 版本中已修复,如果您仅指定形状而不指定步幅,也会影响自动步幅。如果您自己直接指定步幅(如下例所示),则完全没有效果。

    因此,将各个部分放在一起,这段代码是一个完整的 pybind11 模块,它演示了如何完成您正在寻找的内容(并包含一些 C++ 输出以证明确实可以正常工作):

    #include <iostream>
    #include <pybind11/pybind11.h>
    #include <pybind11/numpy.h>
    
    namespace py = pybind11;
    
    PYBIND11_PLUGIN(numpywrap) {
        py::module m("numpywrap");
        m.def("f", []() {
            // Allocate and initialize some data; make this big so
            // we can see the impact on the process memory use:
            constexpr size_t size = 100*1000*1000;
            double *foo = new double[size];
            for (size_t i = 0; i < size; i++) {
                foo[i] = (double) i;
            }
    
            // Create a Python object that will free the allocated
            // memory when destroyed:
            py::capsule free_when_done(foo, [](void *f) {
                double *foo = reinterpret_cast<double *>(f);
                std::cerr << "Element [0] = " << foo[0] << "\n";
                std::cerr << "freeing memory @ " << f << "\n";
                delete[] foo;
            });
    
            return py::array_t<double>(
                {100, 1000, 1000}, // shape
                {1000*1000*8, 1000*8, 8}, // C-style contiguous strides for double
                foo, // the data pointer
                free_when_done); // numpy array references this parent
        });
        return m.ptr();
    }
    

    编译它并从 Python 调用它表明它可以工作:

    >>> import numpywrap
    >>> z = numpywrap.f()
    >>> # the python process is now taking up a bit more than 800MB memory
    >>> z[1,1,1]
    1001001.0
    >>> z[0,0,100]
    100.0
    >>> z[99,999,999]
    99999999.0
    >>> z[0,0,0] = 3.141592
    >>> del z
    Element [0] = 3.14159
    freeing memory @ 0x7fd769f12010
    >>> # python process memory size has dropped back down
    

    【讨论】:

    • 对于现在遇到这种情况的任何人,不再需要创建和分配胶囊对象。
    • @Nimitz14 “不再需要”是什么意思?默认情况下,array_t ctor 现在似乎会复制数据,除非您指定基数。获得非复制语义似乎仍然需要一个胶囊。
    • 这种情况下是否不需要通过return_value_policy::move定义搬家退货政策?想知道返回的array_t 是否会被复制。
    • 将基本参数py::capsule 提供给py::array_t&lt;float&gt; 构造函数作为py::array_t&lt;float&gt;(fsv_size, fsv, capsule) 在Windows 上失败并出现Windows fatal exception: code 0xc0000374(堆损坏),而在unix 和macos 上工作没有问题,更多细节here。有什么建议为什么会这样?
    • @Nimitz14 我知道这是很久以前的事了,但是知道当前推荐的返回 NumPy 数组的方法是什么吗?
    【解决方案2】:

    我建议使用ndarray。一个基本原则是,除非明确要求,否则永远不会复制基础数据(或者您很快就会以极大的低效率告终)。下面是它的使用示例,但还有其他功能我没有展示,包括转换为 Eigen 数组 (ndarray::asEigen(array)),这使它非常强大。

    标题:

    #ifndef MYTENSORCODE_H
    #define MYTENSORCODE_H
    
    #include "ndarray_fwd.h"
    
    namespace myTensorNamespace {
    
    ndarray::Array<double, 2, 1> myTensorFunction(int param1, double param2);
    
    }  // namespace myTensorNamespace
    
    #endif  // include guard
    

    库:

    #include "ndarray.h"
    #include "myTensorCode.h"
    
    namespace myTensorNamespace {
    
    ndarray::Array<double, 2, 1> myTensorFunction(int param1, double param2) {
        std::size_t const size = calculateSize();
        ndarray::Array<double, 2, 1> array = ndarray::allocate(size, size);
        array.deep() = 0;  // initialise
        for (std::size_t ii = 0; ii < size; ++ii) {
            array[ii][ndarray::view(ii, ii + 1)] = 1.0;
        }
        return array;
    }
    
    }  // namespace myTensorNamespace
    

    包装器:

    #include "pybind11/pybind11.h"
    #include "ndarray.h"
    #include "ndarray/pybind11.h"
    
    #include "myTensorCode.h"
    
    namespace py = pybind11;
    using namespace pybind11::literals;
    
    namespace myTensorNamespace {
    namespace {
    
    PYBIND11_MODULE(myTensorModule, mod) {
        mod.def("myTensorFunction", &myTensorFunction, "param1"_a, "param2"_a);
    }
    
    }  // anonymous namespace
    }  // namespace myTensorNamespace
    

    【讨论】:

      猜你喜欢
      • 2022-06-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-29
      • 1970-01-01
      • 2021-12-31
      • 2011-09-23
      相关资源
      最近更新 更多