возврат массивов numpy через pybind11

У меня есть функция C ++, вычисляющая большой тензор, который я хотел бы вернуть в Python в виде массива NumPy через pybind11 .

Из документации pybind11 кажется, что использование STL unique_ptr желательно. В следующем примере закомментированная версия работает, тогда как данная версия компилируется, но не работает во время выполнения («Невозможно преобразовать возвращаемое значение функции в тип Python!»).

Почему не работает версия smartpointer? Каков канонический способ создания и возврата массива 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) );
}

person mrupp    schedule 20.06.2017    source источник


Ответы (1)


Несколько комментариев (потом рабочая реализация).

  • Обертки объектов C ++ в pybind11 вокруг типов Python (например, pybind11::object, pybind11::list и, в данном случае, pybind11::array_t<T>) на самом деле являются просто оболочками вокруг указателя на базовый объект Python. В этом отношении они уже берут на себя роль обертки общего указателя, и поэтому нет смысла заключать это в unique_ptr: возврат объекта py::array_t<T> напрямую уже по сути просто возвращает прославленный указатель.
  • pybind11::array_t может быть сконструирован непосредственно из указателя данных, поэтому вы можете пропустить py::buffer_info промежуточный шаг и просто передать форму и шаги непосредственно конструктору pybind11::array_t. Созданный таким образом массив numpy не будет владеть своими собственными данными, он будет просто ссылаться на них (то есть для флага numpy owndata будет установлено значение false).
  • Владение памятью может быть привязано к сроку службы объекта Python, но вы все еще находитесь на крючке, чтобы правильно освободить память. Pybind11 предоставляет класс py::capsule, который поможет вам в этом. Что вы хотите сделать, так это сделать так, чтобы массив numpy зависел от этой капсулы в качестве родительского класса, указав его в качестве аргумента base для array_t. Это заставит массив numpy ссылаться на него, сохраняя его живым, пока сам массив жив, и вызовет функцию очистки, когда на него больше не ссылаются.
  • Флаг c_style в более старых (до 2.2) выпусках влиял только на новые массивы, т.е. когда не передавался указатель значения. Это было исправлено в версии 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
person Jason Rhinelander    schedule 21.06.2017
comment
Для тех, кто сталкивается с этим сейчас, создание и назначение объекта-капсулы больше не требуется. - person Nimitz14; 24.01.2020
comment
@ Nimitz14 Что ты имеешь в виду под словом "больше не нужно"? Кажется, что по умолчанию array_t ctor теперь копирует данные, если вы не укажете базу. Получение семантики без копирования, похоже, все еще требует капсулы. - person cfh; 26.02.2020
comment
В этом случае нет необходимости определять политику возврата хода через return_value_policy::move? Интересно, будет ли возвращенный array_t скопирован или нет. - person glinka; 19.05.2021
comment
предоставление базового аргумента py::capsule конструктору py::array_t<float>, поскольку py::array_t<float>(fsv_size, fsv, capsule) не работает с Windows fatal exception: code 0xc0000374 (повреждение кучи) в Windows, но работает без проблем в unix и macos, подробнее здесь. Есть предложения, почему это так? - person Laurynas Tamulevičius; 24.05.2021