Python 在numpy块上并行运行一个重循环

Python 在numpy块上并行运行一个重循环,python,numpy,multiprocessing,Python,Numpy,Multiprocessing,我需要迭代一个巨大的numpy数组来构建三个列表,这取决于一个昂贵的C库调用的结果,它接受标量值,并且无法矢量化(或者至少我不知道如何实现)。 这个循环可能需要几个小时到几天的时间,我可以看到性能随着时间的推移而降低(我记录了进度,我可以看到在接近结束时会慢得多),这可能是由于列表的大小不断增加(?) 代码如下所示(我省略了与打印进度和一些微优化相关的代码): 将numpy导入为np 导入swig_c_库 def构建索引(大数组1、大数组2): xs=[] ys=[] idxs=[] 对于(x,

我需要迭代一个巨大的numpy数组来构建三个列表,这取决于一个昂贵的C库调用的结果,它接受标量值,并且无法矢量化(或者至少我不知道如何实现)。 这个循环可能需要几个小时到几天的时间,我可以看到性能随着时间的推移而降低(我记录了进度,我可以看到在接近结束时会慢得多),这可能是由于列表的大小不断增加(?)

代码如下所示(我省略了与打印进度和一些微优化相关的代码):

将numpy导入为np
导入swig_c_库
def构建索引(大数组1、大数组2):
xs=[]
ys=[]
idxs=[]
对于(x,y),以np.ndenumerate(大数组)表示的值:

如果不是(值这是模块可以帮助解决的问题

让我们先创建两个数组
a1
a2
。它们可以有任意形状,但在本例中,我们将通过
n
使它们成为
n
,其中
n=30
。我们将数组展平并将它们堆叠在一起,形成一个大的形状数组(2900).沿
轴=1
尺寸的每一对都是位于
a1
a2
上相同位置的一对项目:

In[1]:
import numpy as np
n = 30
a1 = np.random.rand(n, n)
a2 = np.random.rand(n, n)
a = np.stack((a1.flat, a2.flat))
a.shape

Out[1]:
(2, 900)
然后,我们继续将数组拆分为块。我们选择250个块:

In[2]:
chunks = np.array_split(a, 250, axis=1)
len(chunks)

Out[2]:
250


In[3]:
chunks[0]

Out[3]:
array([[ 0.54631022,  0.8428954 ,  0.11835531,  0.59720379],
   [ 0.51184696,  0.64365038,  0.74471553,  0.67035977]])
现在我们将定义一个
slow_函数
,它将扮演问题中描述的慢速计算的角色。我们还定义了一种使用
numpy
在其中一个块上应用慢速函数的方法

In[4]:
def slow_function(pair):
    return np.asscalar(pair[0]) + np.asscalar(pair[1])

def apply_on_chunk(chunk):
    return np.apply_along_axis(slow_function, 0, chunk)

apply_on_chunk(chunks[0])

Out[4]:
array([ 1.05815717,  1.48654578,  0.86307085,  1.26756356])
在上面,请注意,
apply_on_chunk
无论块中
axis=1
的大小如何都可以工作。换句话说,我们可以继续调用
apply_on_chunk(a)
来计算整个初始数组的结果


dask.bag并行
我们现在展示如何使用
dask.bag
对象的
map
方法沿块并行计算:

In[5]:
import dask.bag as db
mybag = db.from_sequence(chunks)

In[6]:
%time myresult = mybag.map(apply_on_chunk)

Out[6]:
CPU times: user 4 ms, sys: 0 ns, total: 4 ms
Wall time: 1.62 ms
此时,我们还没有计算任何内容。我们已经向
dask
描述了我们希望如何计算结果。这一步骤相对较快,大约需要1.6毫秒


要继续并触发实际计算,我们调用
myresult
上的
compute
方法:

In[7]:
%time myresult = myresult.compute()


Out[7]:
CPU times: user 256 ms, sys: 24 ms, total: 280 ms
Wall time: 362 ms
上面的操作需要1/3秒多一点的时间。我们得到的是一个数组列表,对应于调用
dask.bag
中每个元素的
apply\u on\u chunk
的结果。我们检查了其中的前五个:

In[8]:
myresult[:5]

Out[8]:
[array([ 1.05815717,  1.48654578,  0.86307085,  1.26756356]),
 array([ 1.48913909,  1.25028145,  1.36707112,  1.04826167]),
 array([ 0.90069768,  1.24921559,  1.23146726,  0.84963409]),
 array([ 0.72292347,  0.87069598,  1.35893143,  1.02451637]),
 array([ 1.16422966,  1.35559156,  0.9071381 ,  1.17786002])]
如果我们真的想要最终形式的结果,我们必须调用
np.concatenate
,将所有块的结果放在一起。我们在下面这样做,并展示计算的性能:

In[9]:
%time myresult = np.concatenate(\
    db.from_sequence(\
        np.array_split(np.stack((a1.flat, a2.flat)), 250, axis=1)\
    ).map(apply_on_chunk).compute())

Out[9]:
CPU times: user 232 ms, sys: 40 ms, total: 272 ms
Wall time: 342 ms
完整的计算,它为我们提供了一个带有结果的单个数组,运行时间略多于1/3秒


如果我们直接在整个阵列上进行计算,而不使用任何并行化,会怎么样

In[10]:
%time myresult_ = np.apply_along_axis(slow_function, 0, np.stack((a1.flat, a2.flat)))

Out[10]:
CPU times: user 12 ms, sys: 0 ns, total: 12 ms
Wall time: 12.9 ms
直接向上的计算速度要快得多。但原因是
slow_function
目前并没有那么慢。它只是两个元素的总和,根本不需要花费太多时间。我们在
dask.bag
计算中看到的缓慢是rom并行化


让我们继续并重试,但这一次使用了一个非常慢的功能,每次调用大约需要20毫秒:

In[11]:
n = 30
a1 = np.random.rand(n, n)
a2 = np.random.rand(n, n)

import time

def slow_function(pair):
    time.sleep(0.02)
    return np.asscalar(pair[0]) + np.asscalar(pair[1])

def apply_on_chunk(chunk):
    return np.apply_along_axis(slow_function, 0, chunk)
让我们比较一下
dask
可以做什么,以及直接在整个阵列上运行计算:

In[12]:
%time myresult = np.concatenate(\
    db.from_sequence(\
        np.array_split(np.stack((a1.flat, a2.flat)), 250, axis=1)\
    ).map(apply_on_chunk).compute())

Out[12]:
CPU times: user 236 ms, sys: 20 ms, total: 256 ms
Wall time: 4.75 s


In[13]:
%time myresult_ = np.apply_along_axis(slow_function, 0, np.stack((a1.flat, a2.flat)))


Out[13]:
CPU times: user 72 ms, sys: 16 ms, total: 88 ms
Wall time: 18.2 s
可以看出,
dask
正在利用多处理来加速计算。我们得到了大约4倍的加速

为了完整性,我们证明了
dask
和直接计算的结果彼此一致:

In[14]:
np.testing.assert_array_equal(myresult, myresult_)
print("success")

Out[14]:
success

请注意,问题中的函数返回一个元组

np.asarray(xs), np.asarray(ys), np.asarray(idxs)

我们所描述的只涉及
np.asarray(idxs)
的计算。如果知道原始
a1
a2
的形状,则可以很容易地获得返回元组中的前两项。如果知道大数组的大小,则可以预设xs和ys的大小:
xs=[None]*大数组的大小。然后使用索引赋值:
xs[i]=x
。这将显著提高大型数组的速度。感谢您的出色回答。它应该作为示例包含在dask文档中。我已经实现了它,并且它工作正常,至少加快了3倍的过程。无论如何,我有一个小问题,无法从块高效地重建结果数组。现在我使用它,假设250作为ch未知长度:
xs=np.concatenate([result[i][0]表示xrange(250)中的i)ys=np.concatenate([result[i][1]表示xrange(250)中的i)idxs=np.concatenate([result[i][2]表示xrange(250)])
有没有一种简单的方法来实现这一点?
np.asarray(xs), np.asarray(ys), np.asarray(idxs)