Заставить NumPy ndarray стать владельцем памяти в Cython

После этого ответа на вопрос "Могу ли я заставить numpy ndarray завладеть его памятью?" Я пытался использовать функцию API Python C PyArray_ENABLEFLAGS через оболочку NumPy Cython и обнаружил, что он не выставлен.

Следующая попытка выставить его вручную (это всего лишь минимальный пример воспроизведения ошибки)

from libc.stdlib cimport malloc
import numpy as np
cimport numpy as np

np.import_array()

ctypedef np.int32_t DTYPE_t

cdef extern from "numpy/ndarraytypes.h":
    void PyArray_ENABLEFLAGS(np.PyArrayObject *arr, int flags)

def test():
    cdef int N = 1000

    cdef DTYPE_t *data = <DTYPE_t *>malloc(N * sizeof(DTYPE_t))
    cdef np.ndarray[DTYPE_t, ndim=1] arr = np.PyArray_SimpleNewFromData(1, &N, np.NPY_INT32, data)
    PyArray_ENABLEFLAGS(arr, np.NPY_ARRAY_OWNDATA)

завершается с ошибкой компиляции:

Error compiling Cython file:
------------------------------------------------------------
...
def test():
    cdef int N = 1000

    cdef DTYPE_t *data = <DTYPE_t *>malloc(N * sizeof(DTYPE_t))
    cdef np.ndarray[DTYPE_t, ndim=1] arr = np.PyArray_SimpleNewFromData(1, &N, np.NPY_INT32, data)
    PyArray_ENABLEFLAGS(arr, np.NPY_ARRAY_OWNDATA)
                          ^
------------------------------------------------------------

/tmp/test.pyx:19:27: Cannot convert Python object to 'PyArrayObject *'

Мой вопрос: правильный ли это подход в этом случае? Если так, что я делаю не так? Если нет, то как заставить NumPy стать владельцем Cython, не переходя на модуль расширения C?

1 ответ

Решение

У вас просто есть небольшие ошибки в определении интерфейса. Следующее работало для меня:

from libc.stdlib cimport malloc
import numpy as np
cimport numpy as np

np.import_array()

ctypedef np.int32_t DTYPE_t

cdef extern from "numpy/arrayobject.h":
    void PyArray_ENABLEFLAGS(np.ndarray arr, int flags)

cdef data_to_numpy_array_with_spec(void * ptr, np.npy_intp N, int t):
    cdef np.ndarray[DTYPE_t, ndim=1] arr = np.PyArray_SimpleNewFromData(1, &N, t, ptr)
    PyArray_ENABLEFLAGS(arr, np.NPY_OWNDATA)
    return arr

def test():
    N = 1000

    cdef DTYPE_t *data = <DTYPE_t *>malloc(N * sizeof(DTYPE_t))
    arr = data_to_numpy_array_with_spec(data, N, np.NPY_INT32)
    return arr

Это мое setup.py файл:

from distutils.core import setup, Extension
from Cython.Distutils import build_ext
ext_modules = [Extension("_owndata", ["owndata.pyx"])]
setup(cmdclass={'build_ext': build_ext}, ext_modules=ext_modules)

Построить с python setup.py build_ext --inplace, Затем убедитесь, что данные действительно принадлежат:

import _owndata
arr = _owndata.test()
print arr.flags

Среди прочего, вы должны увидеть OWNDATA : True,

И да, это определенно правильный способ борьбы с этим, так как numpy.pxd делает то же самое, чтобы экспортировать все остальные функции в Cython.

Решение @Stefan работает для большинства сценариев, но несколько хрупко. Numpy использует PyDataMem_NEW/PyDataMem_FREE для управления памятью, и это деталь реализации, что эти вызовы отображаются на обычные malloc/free + некоторая трассировка памяти (я не знаю, какое влияние она оказывает на трассировку памяти, по крайней мере, кажется, что она не падает).

Возможны и более эзотерические случаи, когда free из numpy-library не использует тот же распределитель памяти, что и malloc в коде Cython (например, связан с различными временами выполнения).

Правильный инструмент для передачи / управления владением данными PyArray_SetBaseObject ,

Для начала нам нужен объект python, который отвечает за освобождение памяти. Я использую самодельный класс cdef (в основном из-за регистрации / демострации), но, очевидно, есть и другие возможности:

%%cython
from libc.stdlib cimport free

cdef class MemoryNanny:
    cdef void* ptr # set to NULL by "constructor"
    def __dealloc__(self):
        print("freeing ptr=", <unsigned long long>(self.ptr)) #just for debugging
        free(self.ptr)

    @staticmethod
    cdef create(void* ptr):
        cdef MemoryNanny result = MemoryNanny()
        result.ptr = ptr
        print("nanny for ptr=", <unsigned long long>(result.ptr)) #just for debugging
        return result

 ...

Теперь мы используем MemoryNanny -объект в качестве сторожа для памяти, которая освобождается, как только уничтожается родительский-numpy-массив. Код немного неловкий, потому что PyArray_SetBaseObject крадет ссылку, которая не обрабатывается Cython автоматически:

%%cython
...
from cpython.object cimport PyObject
from cpython.ref cimport Py_INCREF

cimport numpy as np

#needed to initialize PyArray_API in order to be able to use it
np.import_array()


cdef extern from "numpy/arrayobject.h":
    # a little bit awkward: the reference to obj will be stolen
    # using PyObject*  to signal that Cython cannot handle it automatically
    int PyArray_SetBaseObject(np.ndarray arr, PyObject *obj) except -1 # -1 means there was an error

cdef array_from_ptr(void * ptr, np.npy_intp N, int np_type):
    cdef np.ndarray arr = np.PyArray_SimpleNewFromData(1, &N, np_type, ptr)
    nanny = MemoryNanny.create(ptr)
    Py_INCREF(nanny) # a reference will get stolen, so prepare nanny
    PyArray_SetBaseObject(arr, <PyObject*>nanny) 
    return arr
...

И вот пример, как можно назвать эту функциональность:

%%cython
...
from libc.stdlib cimport malloc
def create():
    cdef double *ptr=<double*>malloc(sizeof(double)*8);
    ptr[0]=42.0
    return array_from_ptr(ptr, 8, np.NPY_FLOAT64)

который можно использовать следующим образом:

>>> m =  create()
nanny for ptr= 94339864945184
>>> m[0]
42.0
>>> del m
freeing ptr= 94339864945184

с результатами / выходом, как и ожидалось.

Последняя версия Cython позволяет вам работать с минимальным синтаксисом, хотя и с немного большими накладными расходами, чем предложенные решения более низкого уровня.

numpy_array = np.asarray(<np.int32_t[:10, :10]> my_pointer)

https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html

Одно это не передает права собственности.

Примечательно, что с помощью этого вызова создается массив Cython через array_cwrapper.

Это порождает cython.array, без выделения памяти. Вcython.array использует stdlib.h malloc а также free по умолчанию, поэтому можно было бы ожидать, что вы также будете использовать malloc по умолчанию вместо каких-либо специальных распределителей CPython/Numpy.

free вызывается, только если для этого установлено право собственности cython.array, что по умолчанию, только если он распределяет данные. В нашем случае мы можем установить его вручную через:

my_cyarr.free_data = True


Итак, чтобы вернуть одномерный массив, это будет так же просто, как:

from cython.view cimport array as cvarray

# ...
    cdef cvarray cvarr = <np.int32_t[:N]> data
    cvarr.free_data = True
    return np.asarray(cvarr)
Другие вопросы по тегам