在python

时间:2018-01-02 22:15:51

标签: python arrays parallel-processing joblib parallel-for

我试图在python中使用joblib库并行化二维数组上的操作。这是我的代码

from joblib import Parallel, delayed
import multiprocessing
import numpy as np

# The code below just aggregates the base_array to form a new two dimensional array
base_array = np.ones((2**12, 2**12), dtype=np.uint8)
def compute_average(i, j):
    return np.uint8(np.mean(base_array[i*4: (i+1)*4, j*4: (j+1)*4]))

num_cores = multiprocessing.cpu_count()
new_array = np.array(Parallel(n_jobs=num_cores)(delayed(compute_average)(i, j) 
                                        for i in xrange(0,1024) for j in xrange(0,1024)), dtype=np.uint8)

上面的代码比下面的基本嵌套for循环花费更多的时间。

new_array_nested = np.ones((2**10, 2**10), dtype=np.uint8)
for i in xrange(0,1024):
    for j in xrange(0,1024):
         new_array_nested[i,j] = compute_average(i,j)

为什么并行操作需要更多时间?如何提高上述代码的效率?

1 个答案:

答案 0 :(得分:3)

我们可以轻松地到达77 [ms] 下的,但需要掌握一些步骤才能实现目标,所以让我们开始吧: < / p>

问:为什么并行操作需要更多时间?

因为使用 joblib 的建议步骤创建了许多全面的流程副本 - 以便逃避GIL步骤pure-[SERIAL]跳舞(一个接一个) )但是(!)这包括所有变量和整个python解释器及其内部状态的所有内存传输的附加成本(对于确实很大的 numpy 数组非常昂贵/敏感) ,在它开始在&#34;有用的&#34;处理你的&#34;有效载荷&#34; - 计算策略,
所以
所有这些实例化开销的总和很容易变得更大,而不是与反比例1 / N因子的开销不可知的期望
在哪里设置N ~ num_cores

For details, read the mathematical formulation in the tail part of Amdahl's Law re-formulation here.

问:可以帮助提高上述代码的效率吗?

尽可能节省所有间接费用:
- 尽可能:
  - on process spawn-side ,尝试使用 n_jobs = ( num_cores - 1 ) 为&#34; main&#34;提供更多空间如果业绩上升,过程将继续进行并进行基准测试 - on process termination-side ,避免从返回值中收集和构造一个新的(可能很大的)对象,而是预先分配一个足够大的进程本地数据结构和返回一些有效的,序列化的,以方便和无阻塞地合并每个部分返回的结果&#39;比对。

这两个&#34;隐藏&#34;成本是您的主要设计敌人,因为它们线性地添加到整个问题解决方案的计算路径的纯 [SERIAL] 部分(ref.: the effects of both of these in the overhead-strict Amdahl's Law formula

实验&amp;结果:

>>> from zmq import Stopwatch; aClk = Stopwatch()
>>> base_array = np.ones( (2**12, 2**12), dtype = np.uint8 )
>>> base_array.flags
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA      : True
  WRITEABLE    : True
  ALIGNED      : True
  UPDATEIFCOPY : False
>>> def compute_average_per_TILE(               TILE_i,   TILE_j ): // NAIVE MODE
...     return np.uint8( np.mean( base_array[ 4*TILE_i:4*(TILE_i+1),
...                                           4*TILE_j:4*(TILE_j+1)
...                                           ]
...                               )
...                      )
... 
>>> aClk.start(); _ = compute_average_per_TILE( 12,13 ); aClk.stop()
25110
  102
  109
   93

每次拍摄需要 93 [us] 。期望大约1024*1024*93 ~ 97,517,568 [us]涵盖整个base_array的平均处理。

在实验上,这里可以很好地看到不太好处理的开销的影响,天真嵌套的实验花了:

>>> aClk.start(); _ = [ compute_average_per_TILE( i, j )
                                              for i    in xrange(1024)
                                              for    j in xrange(1024)
                        ]; aClk.stop()
26310594
^^...... 
26310594 / 1024. / 1024. == 25.09 [us/cell]

约为3.7倍(由于未发生&#34;尾部&#34; -part(分配单个返回值)开销2 ** 20次,但仅一次,在终端分配。

然而,更多的惊喜即将到来。

这里有什么合适的工具?

从来没有一个普遍的规则,没有一刀切。

<强>鉴于
每个调用只需要一个4x4矩阵磁贴就可以进行处理(按照建议的 25 [us] 生成的 joblib 生成 2**20 .cpu_count()次调用,通过原始提案分发到 ...( joblib.Parallel( n_jobs = num_cores )( joblib.delayed( compute_average )( i, j ) for i in xrange( 1024 ) for j in xrange( 1024 ) ) 完全实例化的流程

~ 25 [us/cell]

确实存在改善表现的空间。

对于这些小规模矩阵(并非所有问题都在这个意义上都是如此开心),人们可以期待更智能的内存访问模式和减少python GIL起源的弱点的最佳结果。

由于每次呼叫跨度只是4x4微型计算,更好的方法是利用智能矢量化(所有数据都适合缓存,因此缓存计算是寻求最佳性能的假期旅程)

最好的(仍然非常天真的矢量化代码)
能够从~ 74 [ns/cell]小于~ 4.6 [ns] (因为它需要 base_array / a { {1}}单元格处理),如果缓存中优化的矢量化代码能够正确制作,那么期望另一个加速级别。

77 [ms]?中!值得这么做,不是吗?

不是97秒,
不是25秒,
如果能够更好地优化呼叫签名,只需键入一下键盘就可以但小于77 [ms] ,并且可以更多地将其缩小:

>>> import numba
>>> @numba.jit( nogil = True, nopython = True )
... def jit_avg2( base_IN, ret_OUT ):  // all pre-allocated memory for these data-structures
...     for i in np.arange( 1024 ):    // vectorised-code ready numpy iterator
...         for j in np.arange( 1024 ):// vectorised-code ready numpy iterator 
...             ret_OUT[i,j] = np.uint8( np.mean( base_IN[4*i:4*(i+1),
...                                                       4*j:4*(j+1)
...                                                       ]
...                                               )
...                                      )
...     return                         // avoid terminal assignment costs
... 

>>> aClk.start(); _ = jit_avg2( base_array, mean_array ); aClk.stop()
1586182 (even with all the jit-compilation circus, it was FASTER than GIL-stepped nested fors ...)
  76935
  77337