使用numpy/scipy最小化Python多处理池中的开销
我花了好几个小时尝试并行化我的数字运算代码,但当我这样做的时候,它只会变得更慢。不幸的是,当我尝试将其简化为下面的示例时,问题就消失了,我真的不想在这里发布整个程序。所以问题是:在这种类型的程序中,我应该避免哪些陷阱 (注:Unutbu回答后的跟进在底部。) 情况如下:使用numpy/scipy最小化Python多处理池中的开销,python,numpy,parallel-processing,multiprocessing,pool,Python,Numpy,Parallel Processing,Multiprocessing,Pool,我花了好几个小时尝试并行化我的数字运算代码,但当我这样做的时候,它只会变得更慢。不幸的是,当我尝试将其简化为下面的示例时,问题就消失了,我真的不想在这里发布整个程序。所以问题是:在这种类型的程序中,我应该避免哪些陷阱 (注:Unutbu回答后的跟进在底部。) 情况如下: 它是关于一个模块,它定义了一个包含大量内部数据的类BigData。在该示例中,有一个插值函数列表ff;在实际的程序中,有更多的,例如,ffA[k],ffB[k],ffC[k] 计算将被归类为“令人尴尬的并行”:每次可以在较小的
- 它是关于一个模块,它定义了一个包含大量内部数据的类
。在该示例中,有一个插值函数列表BigData
;在实际的程序中,有更多的,例如,ff
,ffA[k]
,ffB[k]
ffC[k]
- 计算将被归类为“令人尴尬的并行”:每次可以在较小的数据块上完成工作。在本例中,这是
do\u chunk()
- 在我的实际程序中,示例中显示的方法将导致最差的性能:每个块大约1秒(在单个线程中完成时,实际计算时间大约为0.1秒)。因此,对于n=50,
将在5秒内运行,do_single()
将在55秒内运行do_multi()
- 我还试图通过将
和xi
数组分割成连续的块,并迭代每个块中的所有yi
值来分割工作。那就好一点了。现在,无论我使用1、2、3或4个线程,总执行时间都没有差别。但当然,我希望看到实际的加速李>k
- 这可能与:。然而,在程序的其他地方,我使用了一个多处理池来进行更为孤立的计算:一个函数(未绑定到类),看起来像
,只对该数组进行numpy计算。在那里,有一个显著的速度提升def do_chunk(array1,array2,array3)
- CPU使用率随着预期的并行进程数而变化(三个线程的CPU使用率为300%)
self
被pickle到一个37到140 MB的对象中,该对象需要传递给工作进程。更糟糕的是,Python的酸洗速度非常慢;酸洗本身花费了几秒钟的时间,这发生在传递给工作进程的每一块工作上。除了清理和传递大数据对象,Linux中的apply\u async
开销非常小;对于一个小函数(添加几个整数参数),每个apply\u async
/get
对只需要0.2毫秒。因此,将工作分成非常小的部分本身并不是问题。所以,我将所有大数组参数作为索引传输到全局变量。为了优化CPU缓存,我将块大小保持在较小的位置
全局变量存储在全局dict
中;在设置工作池后,将立即在父进程中删除这些条目。只有dict
的键被传输到工作进程。酸洗/IPC的唯一大数据是工人创建的新数据
#!/usr/bin/python2.7
import numpy as np, sys
from multiprocessing import Pool
_mproc_data = {} # global storage for objects during multiprocessing.
class BigData:
def __init__(self, size):
self.blah = np.random.uniform(0, 1, size=size)
def do_chunk(self, k, xi, yi):
# do the work and return an array of the same shape as xi, yi
zi = k*np.ones_like(xi)
return zi
def do_all_work(self, xi, yi, num_proc):
global _mproc_data
mp_key = str(id(self))
_mproc_data['bd'+mp_key] = self # BigData
_mproc_data['xi'+mp_key] = xi
_mproc_data['yi'+mp_key] = yi
pool = Pool(processes=num_proc)
# processes have now inherited the global variabele; clean up in the parent process
for v in ['bd', 'xi', 'yi']:
del _mproc_data[v+mp_key]
# setup indices for the worker processes (placeholder)
n_chunks = 45
n = len(xi)
chunk_len = n//n_chunks
i1list = np.arange(0,n,chunk_len)
i2list = i1list + chunk_len
i2list[-1] = n
klist = range(n_chunks) # placeholder
procs = []
for i in range(n_chunks):
p = pool.apply_async( _do_chunk_wrapper, (mp_key, i1list[i], i2list[i], klist[i]) )
sys.stderr.write(".")
procs.append(p)
sys.stderr.write("\n")
# allocate space for combined results
zi = np.zeros_like(xi)
# get data from workers and finish
for i, p in enumerate(procs):
zi[i1list[i]:i2list[i]] = p.get(timeout=30) # timeout allows ctrl-C handling
pool.close()
pool.join()
return zi
def _do_chunk_wrapper(key, i1, i2, k):
"""All arguments are small objects."""
global _mproc_data
bd = _mproc_data['bd'+key]
xi = _mproc_data['xi'+key][i1:i2]
yi = _mproc_data['yi'+key][i1:i2]
return bd.do_chunk(k, xi, yi)
if __name__ == "__main__":
xi, yi = np.linspace(1, 100, 100001), np.linspace(1, 100, 100001)
bd = BigData(int(1e7))
bd.do_all_work(xi, yi, 4)
下面是速度测试的结果(同样,2个内核,4个线程),它改变了工作进程的数量和块中的内存量(数组片的xi
,yi
,zi
的总字节数)。这些数字是以“每秒一百万个结果值”为单位的,但这对于比较来说并不重要。“1进程”的行是使用完整的输入数据直接调用do_chunk
,而不使用任何子进程
#过程125K 250K 500K 1000K无限
1 0.82
2 4.28 1.96 1.3 1.31
3 2.69 1.06 1.06 1.07
4 2.17 1.27 1.23 1.28
内存中数据大小的影响非常显著。CPU有3 MB共享三级缓存,每个核心加上256 KB二级缓存。请注意,计算还需要访问
BigData
对象的几MB内部数据。因此,我们从中了解到,进行这种速度测试是有用的。对于这个程序,2个进程最快,其次是4个,3个进程最慢。尽量减少进程间的通信。
在多处理
模块中,所有(单台计算机)进程间通信通过队列完成。通过队列传递的对象
它们是腌制的。因此,尝试通过队列发送更少和/或更小的对象
- 不要通过队列发送
的实例BigData
。它相当大,并且随着self
中数据量的增加而增大:self
每一个 时间>代码>池。Apple yasyc(O-DojCujkWraseR,(自我,K,席,彝))< /Cord>被称为In [6]: import pickle In [14]: len(pickle.dumps(BigData(50))) Out[14]: 1052187
在主进程中酸洗,在辅助进程中取消酸洗。这个self
增加len的大小(pickle.dumps(BigData(N))
增加N
- 让数据从全局变量中读取。在Linux上,您可以利用写时拷贝。作为:
在fork()之后,父级和子级处于等效状态。将父级的整个内存复制到RAM中的另一个位置是愚蠢的。这就是“写上拷贝”原则的由来。只要子对象不改变其内存状态,它实际上就会访问父对象的内存。只有在修改后,相应的位和段才会复制到子级的内存空间中
因此,您可以避免通过队列传递
的实例 简单地说BigData
#!/usr/bin/python2.7 import numpy as np, sys from multiprocessing import Pool _mproc_data = {} # global storage for objects during multiprocessing. class BigData: def __init__(self, size): self.blah = np.random.uniform(0, 1, size=size) def do_chunk(self, k, xi, yi): # do the work and return an array of the same shape as xi, yi zi = k*np.ones_like(xi) return zi def do_all_work(self, xi, yi, num_proc): global _mproc_data mp_key = str(id(self)) _mproc_data['bd'+mp_key] = self # BigData _mproc_data['xi'+mp_key] = xi _mproc_data['yi'+mp_key] = yi pool = Pool(processes=num_proc) # processes have now inherited the global variabele; clean up in the parent process for v in ['bd', 'xi', 'yi']: del _mproc_data[v+mp_key] # setup indices for the worker processes (placeholder) n_chunks = 45 n = len(xi) chunk_len = n//n_chunks i1list = np.arange(0,n,chunk_len) i2list = i1list + chunk_len i2list[-1] = n klist = range(n_chunks) # placeholder procs = [] for i in range(n_chunks): p = pool.apply_async( _do_chunk_wrapper, (mp_key, i1list[i], i2list[i], klist[i]) ) sys.stderr.write(".") procs.append(p) sys.stderr.write("\n") # allocate space for combined results zi = np.zeros_like(xi) # get data from workers and finish for i, p in enumerate(procs): zi[i1list[i]:i2list[i]] = p.get(timeout=30) # timeout allows ctrl-C handling pool.close() pool.join() return zi def _do_chunk_wrapper(key, i1, i2, k): """All arguments are small objects.""" global _mproc_data bd = _mproc_data['bd'+key] xi = _mproc_data['xi'+key][i1:i2] yi = _mproc_data['yi'+key][i1:i2] return bd.do_chunk(k, xi, yi) if __name__ == "__main__": xi, yi = np.linspace(1, 100, 100001), np.linspace(1, 100, 100001) bd = BigData(int(1e7)) bd.do_all_work(xi, yi, 4)
In [6]: import pickle In [14]: len(pickle.dumps(BigData(50))) Out[14]: 1052187
p = pool.apply_async(_do_chunk_wrapper, (k_start, k_end, xi, yi))
import math import numpy as np import time import sys import multiprocessing as mp import scipy.interpolate as interpolate _tm=0 def stopwatch(msg=''): tm = time.time() global _tm if _tm==0: _tm = tm; return print("%s: %.2f seconds" % (msg, tm-_tm)) _tm = tm class BigData: def __init__(self, n): z = np.random.uniform(size=n*n*n).reshape((n,n,n)) self.ff = [] for i in range(n): f = interpolate.RectBivariateSpline( np.arange(n), np.arange(n), z[i], kx=1, ky=1) self.ff.append(f) self.n = n def do_chunk(self, k, xi, yi): n = self.n s = np.sum(np.exp(self.ff[k].ev(xi, yi))) sys.stderr.write(".") return s def do_chunk_of_chunks(self, k_start, k_end, xi, yi): s = sum(np.sum(np.exp(self.ff[k].ev(xi, yi))) for k in range(k_start, k_end)) sys.stderr.write(".") return s def do_multi(self, numproc, xi, yi): procs = [] pool = mp.Pool(numproc) stopwatch('\nPool setup') ks = list(map(int, np.linspace(0, self.n, numproc+1))) for i in range(len(ks)-1): k_start, k_end = ks[i:i+2] p = pool.apply_async(_do_chunk_wrapper, (k_start, k_end, xi, yi)) procs.append(p) stopwatch('Jobs queued (%d processes)' % numproc) total = 0.0 for k, p in enumerate(procs): total += np.sum(p.get(timeout=30)) # timeout allows ctrl-C interrupt if k == 0: stopwatch("\nFirst get() done") print(total) stopwatch('Jobs done') pool.close() pool.join() return total def do_single(self, xi, yi): total = 0.0 for k in range(self.n): total += self.do_chunk(k, xi, yi) stopwatch('\nAll in single process') return total def _do_chunk_wrapper(k_start, k_end, xi, yi): return bd.do_chunk_of_chunks(k_start, k_end, xi, yi) if __name__ == "__main__": stopwatch() n = 50 bd = BigData(n) m = 1000*1000 xi, yi = np.random.uniform(0, n, size=m*2).reshape((2,m)) stopwatch('Initialized') bd.do_multi(2, xi, yi) bd.do_multi(3, xi, yi) bd.do_single(xi, yi)
Initialized: 0.15 seconds Pool setup: 0.06 seconds Jobs queued (2 processes): 0.00 seconds First get() done: 6.56 seconds 83963796.0404 Jobs done: 0.55 seconds .. Pool setup: 0.08 seconds Jobs queued (3 processes): 0.00 seconds First get() done: 5.19 seconds 83963796.0404 Jobs done: 1.57 seconds ... All in single process: 12.13 seconds
Initialized: 0.10 seconds Pool setup: 0.03 seconds Jobs queued (2 processes): 0.00 seconds First get() done: 10.47 seconds Jobs done: 0.00 seconds .................................................. Pool setup: 0.12 seconds Jobs queued (3 processes): 0.00 seconds First get() done: 9.21 seconds Jobs done: 0.00 seconds .................................................. All in single process: 12.12 seconds