簡體   English   中英

在沒有 GIL 的情況下在 Cython 中並行化

[英]Parallelize in Cython without GIL

我正在嘗試計算numpy數組的一些列,使用cdef函數在 for 循環中對 python 對象( numpy數組)進行操作。

我想同時進行。 但不確定如何。

這是一個玩具示例,一個def函數使用prange在 for 循環中調用cdef函數,這是不允許的,因為np.ndarray是一個 python 對象。 在我的實際問題中,一個矩陣和一個向量是cdef函數的參數,並且執行了一些numpy矩陣操作,例如np.linalg.pinv() (我猜這實際上是瓶頸)。

%%cython
import numpy as np
cimport numpy as np
from cython.parallel import prange
from c_functions import estimate_coef_linear_regression

DTYPE = np.float
ctypedef np.float_t DTYPE_t

def transpose_example(np.ndarray[DTYPE_t, ndim=2] data):
    """
    Transposes a matrix. It does each row independently and parallel
    """

    cdef Py_ssize_t n = data.shape[0]
    cdef Py_ssize_t t = data.shape[1]

    cdef np.ndarray[DTYPE_t, ndim = 2] results = np.zeros((t, n))

    cdef Py_ssize_t i

    for i in prange(n, nogil=True):
        results[i, :] = transpose_vector(data[:, i])

    return results

cdef transpose_vector(np.ndarray[DTYPE_t, ndim=1] vector):
    """
    transposes a np vector
    """
    return vector.transpose()

a = np.random.rand(100, 20)
transpose_example(a)

產出

Converting to Python object not allowed without gil

並行執行此操作的最佳方法是什么?

您可以在沒有 GIL 的情況下傳遞類型化 memoryview 切片( cdef transpose_vector(DTYPE_t[:] vector) ) - 這是較新類型 memoryview 語法優於np.ndarray的主要優勢之一。

然而,

  • 您不能在內存視圖上調用 Numpy 成員函數(如轉置),除非您轉換回 Numpy 數組( np.asarray(vector) )。 這需要 GIL。
  • 調用任何類型的 Python 函數(例如transpose )都需要 GIL。 這可以在with gil:塊中完成,但是當該塊幾乎是您的整個循環時,就變得毫無意義了。
  • 您沒有為transpose_vector指定返回類型,因此它將默認為object ,這需要 GIL。 您可以指定 Cython 返回類型,但我懷疑即使返回 memoryview 切片也可能需要在某處進行一些引用計數。
  • 注意不要讓多個線程覆蓋傳遞的 memoryview 切片中的相同數據。

總結:memoryview slices,但請記住,如果沒有 GIL,您可以做的事情非常有限。 您當前的示例只是不可並行化的(但這可能主要是因為它是一個玩具示例)。

“並行執行此操作的最佳方法是什么?”
+
我故意使用np.transpose()來表明我必須使用 python 對象。

讓我從自由地轉述亨利福特 (Henry FORD) 的名言開始:汽車中缺陷最少的部分是完全不存在的部分——它永遠不會損壞。

那些知道numpy的內部數組對象表示如何工作的人肯定,
幾乎需要零時間

幾乎需要零內存 I/O (至少在過去十年左右確實如此)


為什么 ?

Numpy 很聰明
它根本不會為此移動任何數據。 它只是以大約15 [us]的成本調整索引


回答 :

執行np.transpose()的最佳方法根本不需要任何並行化。

任何這樣做的嘗試都會導致性能np.transpose() ,因為人為地強制執行了許多無用的內存 I/O,而原生np.transpose()從不這樣做——它只是交換索引方案,而不移動單元數據堆,所有這些都保留在它們原來的位置(包括保持任何和所有緩存一致性有效 - 所以任何下一次訪問都將從緩存中發生0.5 ~ 5 [ns] ,而不必再次支付150-350 [ns]倍的大量費用150-350 [ns]用於從/向物理 RAM 位置移動任何單元數據並破壞緩存線)


證據 :

______1x THE PROBLEM SIZE 100 x 20 x 8 [B] ( 16 kB RAM).transpose() ~ 17 [us]

>>> r =   100; c =   20; a = np.arange(r*c).reshape( (r,c) );a.itemsize*a.size/1E6
0.016
>>> aClk.start(); _ = a.transpose(); aClk.stop()  ####  16   [kB] RAM-footprint
17                                                ####  17   [us] !!! ZERO-COPY

____100x THE PROBLEM SIZE 1000 x 200 x 8 [B] (1.6 MB RAM).transpose() ~ 17 [us]

>>> r =  1000; c =  200; a = np.arange(r*c).reshape( (r,c) );a.itemsize*a.size/1E6
1.6
>>> aClk.start(); _ = a.transpose(); aClk.stop()  ####   1.6 [MB] RAM-footprint
17                                                ####  17   [us] !!! ZERO-COPY

__10000x THE PROBLEM SIZE 10000 x 2000 x 8 [B] (160 MB RAM).transpose() ~ 16 [us]

>>> r = 10000; c = 2000; a = np.arange(r*c).reshape( (r,c) );a.itemsize*a.size/1E6
160.0
>>> aClk.start(); _ = a.transpose(); aClk.stop()  #### 160.0 [MB] RAM-footprint
16                                                ####  16   [us] !!! ZERO-COPY

移動或 ALAP 分配和復制許多 RAM 存儲的兆字節的單元數據,就像任何prange -d 或任何其他代碼會做的那樣,將需要很prange ,而不是像智能 numpy 設計那樣智能16 [ns]


>>> a.flags
  C_CONTIGUOUS    : True <-------------------------ORIGINAL [indirect] RAM-indexing
  F_CONTIGUOUS    : False
  OWNDATA         : False
  WRITEABLE       : True
  ALIGNED         : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY    : False
>>>
>>> _.flags
  C_CONTIGUOUS    : False
  F_CONTIGUOUS    : True <------------------------TRANSPOSE'd
  OWNDATA         : False
  WRITEABLE       : True
  ALIGNED         : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY    : False

QED


結語:

現在是通知背后的重點。 希望如此。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM