10

我正在尝试使用 Cython 来并行化涉及生成中间多维数组的昂贵操作。

以下非常简化的代码说明了我正在尝试做的事情:

import numpy as np
cimport cython
cimport numpy as np
from cython.parallel cimport prange
from libc.stdlib cimport malloc, free


@cython.boundscheck(False)
@cython.wraparound(False)
def embarrasingly_parallel_example(char[:, :] A):

    cdef unsigned int m = A.shape[0]
    cdef unsigned int n = A.shape[1]
    cdef np.ndarray[np.float64_t, ndim = 2] out = np.empty((m, m), np.float64)
    cdef unsigned int ii, jj
    cdef double[:, :] tmp

    for ii in prange(m, nogil=True):
        for jj in range(m):

            # allocate a temporary array to hold the result of
            # expensive_function_1
            tmp_carray = <double * > malloc((n ** 2) * sizeof(double))

            # a 2D typed memoryview onto tmp_carray
            tmp = <double[:n, :n] > tmp_carray

            # shove the intermediate result in tmp
            expensive_function_1(A[ii, :], A[jj, :], tmp)

            # get the final (scalar) output for this ii, jj
            out[ii, jj] = expensive_function_2(tmp)

            # free the intermediate array
            free(tmp_carray)

    return out


# some silly examples - the actual operation I'm performing is a lot more
# involved
# ------------------------------------------------------------------------
@cython.boundscheck(False)
@cython.wraparound(False)
cdef void expensive_function_1(char[:] x, char[:] y, double[:, :] tmp):

    cdef unsigned int m = tmp.shape[0]
    cdef unsigned int n = x.shape[0]
    cdef unsigned int ii, jj

    for ii in range(m):
        for jj in range(m):
            tmp[ii, jj] = 0
            for kk in range(n):
                tmp[ii, jj] += (x[kk] + y[kk]) * (ii - jj)


@cython.boundscheck(False)
@cython.wraparound(False)
cdef double expensive_function_2(double[:, :] tmp):

    cdef unsigned int m = tmp.shape[0]
    cdef unsigned int ii, jj
    cdef double result = 0

    for ii in range(m):
        for jj in range(m):
            result += tmp[ii, jj]

    return result

编译失败似乎至少有两个原因:

  1. 基于 的输出cython -a,此处创建类型化内存视图:

    cdef double[:, :] tmp = <double[:n, :n] > tmp_carray
    

    似乎涉及 Python API 调用,因此我无法释放 GIL 以允许外部循环并行运行。

    我的印象是类型化的内存视图不是 Python 对象,因此子进程应该能够在不首先获取 GIL 的情况下创建它们。是这样吗?

2.即使我替换prange(m, nogil=True)为 normal range(m),Cython 似乎仍然不喜欢cdef内部循环中存在 a :

    Error compiling Cython file:
    ------------------------------------------------------------
    ...
                # allocate a temporary array to hold the result of
                # expensive_function_1
                tmp_carray = <double*> malloc((n ** 2) * sizeof(double))

                # a 2D typed memoryview onto tmp_carray
                cdef double[:, :] tmp = <double[:n, :n]> tmp_carray
                    ^
    ------------------------------------------------------------

    parallel_allocate.pyx:26:17: cdef statement not allowed here

更新

事实证明,第二个问题很容易通过移动来解决

 cdef double[:, :] tmp

for循环之外,只是分配

 tmp = <double[:n, :n] > tmp_carray

循环内。不过,我仍然不完全理解为什么这是必要的。

现在,如果我尝试使用prange我会遇到以下编译错误:

Error compiling Cython file:
------------------------------------------------------------
...
            # allocate a temporary array to hold the result of
            # expensive_function_1
            tmp_carray = <double*> malloc((n ** 2) * sizeof(double))

            # a 2D typed memoryview onto tmp_carray
            tmp = <double[:n, :n]> tmp_carray
               ^
------------------------------------------------------------

parallel_allocate.pyx:28:16: Memoryview slices can only be shared in parallel sections
4

2 回答 2

6

免责声明:这里的一切都需要一粒盐。我更多地猜测是知道的。您当然应该在Cython-User上提出问题。他们总是很友好,回答很快。

我同意 Cython 的文档不是很清楚:

[...] memoryviews 通常不需要 GIL:

cpdef int sum3d(int[:, :, :] arr) nogil: ...

特别是,您不需要 GIL 来进行 memoryview 索引、切片或转置。Memoryviews 需要 GIL 用于复制方法(C 和 Fortran 连续副本),或者当 dtype 是 object 并且读取或写入 object 元素时。

我认为这意味着传递内存视图参数或使用它进行切片或转置不需要 Python GIL。但是,创建内存视图或复制内存视图需要 GIL。

支持这一点的另一个论点是,Cython 函数有可能向 Python 返回内存视图。

from cython.view cimport array as cvarray
import numpy as np

def bla():
    narr = np.arange(27, dtype=np.dtype("i")).reshape((3, 3, 3))
    cdef int [:, :, :] narr_view = narr
    return narr_view

给出:

>>> import hello
>>> hello.bla()
<MemoryView of 'ndarray' at 0x1b03380>

这意味着 memoryview 是在 Python 的 GC 托管内存中分配的,因此需要创建 GIL。所以你不能在 nogil 部分创建内存视图


现在关于错误消息的问题

Memoryview 切片只能在并行部分中共享

我认为您应该将其阅读为“您不能拥有线程私有 memoryview 切片。它必须是线程共享 memoryview 切片”。

于 2014-03-06T22:59:39.597 回答
0

http://docs.cython.org/src/userguide/external_C_code.html#releasing-the-gil

"""

释放 GIL

您可以使用 with nogil 语句围绕一段代码释放 GIL:

 with nogil:
<code to be executed with the GIL released> Code in the body of the statement must not manipulate Python objects in any way, and must

在没有首先重新获取 GIL 的情况下,不要调用任何操作 Python 对象的东西。Cython 目前不检查这一点。

"""

于 2014-03-06T16:04:47.960 回答