在Cython中强制NumPy ndarray拥有其内存

15

按照这个回答中的方法 "Can I force a numpy ndarray to take ownership of its memory?",我尝试使用Python C API函数PyArray_ENABLEFLAGS通过Cython的NumPy包装器,并发现它没有暴露出来。

以下是手动尝试将其暴露的代码(这只是一个最小化的例子,以重现该失败)

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 *'

我的问题:在这种情况下,这是正确的方法吗?如果是,那么我做错了什么?如果不是,那么我如何在不降级到C扩展模块的情况下强制NumPy在Cython中拥有所有权?


我的回答对你有用吗? - Stefan
确实如此,谢谢! - kynan
3个回答

20
你的接口定义有一些小错误。以下是我使用的代码片段:
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 中。


1
这对我不起作用。它编译得很好,但导入模块会导致链接错误,抱怨PyArray_ENABLEFLAGS。这是使用numpy 1.9.1时出现的。 - amaurea
这个解决方案适用于numpy 1.7及以上版本。旧版本缺少PyArray_ENABLEFLAGS。 - marscher
这个出错了,显示为 ImportError: ./_owndata.so: undefined symbol: PyArray_ENABLEFLAGS - Korem
@Korem 你使用过 cdef extern from "numpy/arrayobject.h" 吗?也许你需要检查一下 "numpy" 是否在你的包含路径中。 - Syrtis Major
如果数据类型是用户定义的,例如 my_dtype = np.dtype([('t1', np.float32), ('t2', np.uint16)]),那该怎么办呢?我知道它有一个 type_num,但是我不知道如何在 Cython 中获取它。它被定义为 cdef np.ndarray[mytype_t] arr,而 mytype_t 包含了打包的浮点数和 uint16。 - dashesy
setup.py 中,对于我的情况,Extension 应该具有 include_dirs=[numpy.get_include()] 参数,以便找到 numpy 标头。 - rth

7

@Stefan的解决方案适用于大多数场景,但是有些脆弱。 Numpy使用PyDataMem_NEW / PyDataMem_FREE进行内存管理,并且这些调用映射到通常的malloc/free +某些内存跟踪(我不知道Stefan的解决方案对内存跟踪的影响,至少它似乎不会崩溃)。

还有更奇怪的情况可能出现,在这些情况下,numpy库中的free与cython代码中的malloc不使用相同的内存分配器(例如链接到不同的运行时,如此github问题或这个SO帖子)。

传递/管理数据所有权的正确工具是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.flags
...
OWNDATA : False
...
>>> m[0]
42.0
>>> del m
freeing ptr= 94339864945184

输出结果符合预期。

注意:生成的数组实际上并不拥有数据(即标志返回OWNDATA:False),因为内存由内存托管器所拥有,但结果是相同的:当数组被删除时(因为没有人再持有对内存托管器的引用),内存会被释放。


MemoryNanny 不一定要保护一个原始的C指针。它也可以是其他任何东西,例如 std::vector

%%cython -+
from libcpp.vector cimport vector
cdef class VectorNanny:
    #automatically default initialized/destructed by Cython:
    cdef vector[double] vec 
    @staticmethod
    cdef create(vector[double]& vec):
        cdef VectorNanny result = VectorNanny()
        result.vec.swap(vec) # swap and not copy
        return result
   
# for testing:
def create_vector(int N):
    cdef vector[double] vec;
    vec.resize(N, 2.0)
    return VectorNanny.create(vec)

以下测试显示保姆可以正常工作:
nanny=create_vector(10**8) # top shows additional 800MB memory are used
del nanny                  # top shows, this additional memory is no longer used.

6
最新的Cython版本允许您以最少的语法来完成此操作,尽管与建议的低级解决方案相比有稍微更高的开销。

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

https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html#coercion-to-numpy

< p > 这本身并不传递所有权。

值得注意的是,通过 array_cwrapper 调用生成Cython数组。

这将生成一个cython.array,而不分配内存。默认情况下,cython.array使用stdlib.h mallocfree,因此预期您使用默认的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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接