私はメモリ内に大きなnumpy配列を持っていると仮定し、この巨大な配列を入力として(他のパラメータと一緒に)受け取る関数func
を持っています。 func
は、異なるパラメーターを使用して並行して実行できます。例えば:
def func(arr, param):
# do stuff to arr, param
# build array arr
pool = Pool(processes = 6)
results = [pool.apply_async(func, [arr, param]) for param in all_params]
output = [res.get() for res in results]
マルチプロセッシングライブラリを使用すると、その巨大な配列が異なるプロセスに複数回コピーされます。
異なるプロセスに同じ配列を共有させる方法はありますか?この配列オブジェクトは読み取り専用であり、変更されることはありません。
Arrが配列ではなく、任意のpythonオブジェクトである場合、それを共有する方法はありますか?
[編集済み]
答えを読みましたが、まだ少し混乱しています。 fork()はコピーオンライトであるため、pythonマルチプロセッシングライブラリで新しいプロセスを生成するときに追加コストを呼び出さないでください。しかし、次のコードは、大きなオーバーヘッドがあることを示唆しています。
from multiprocessing import Pool, Manager
import numpy as np;
import time
def f(arr):
return len(arr)
t = time.time()
arr = np.arange(10000000)
print "construct array = ", time.time() - t;
pool = Pool(processes = 6)
t = time.time()
res = pool.apply_async(f, [arr,])
res.get()
print "multiprocessing overhead = ", time.time() - t;
出力(そして、ちなみに、配列のサイズが大きくなるとコストが増加するため、メモリコピーに関連するオーバーヘッドがまだあると思われます):
construct array = 0.0178790092468
multiprocessing overhead = 0.252444982529
配列をコピーしなかったのに、なぜそんなに大きなオーバーヘッドがあるのですか?そして、共有メモリはどの部分を節約しますか?
コピーオンライトfork()
セマンティクスを使用するオペレーティングシステム(一般的なUNIXなど)を使用する場合、データ構造を変更しない限り、追加のメモリを使用せずにすべての子プロセスで使用できます。特別なことをする必要はありません(オブジェクトを絶対に変更しないことを絶対に確認してください)。
最も効率的なことyoはあなたの問題に対してできることです配列を効率的な配列構造にパックすることです(numpy
または array
)、共有メモリに配置し、multiprocessing.Array
でラップして、関数に渡します。 この回答はその方法を示しています 。
writeable共有オブジェクトが必要な場合は、何らかの同期またはロックでラップする必要があります。 multiprocessing
が提供する これを行う2つの方法 :1つは共有メモリ(単純な値、配列、またはctypesに適しています)またはManager
プロキシを使用します。プロセス(ネットワーク経由でも)。
Manager
アプローチは任意のPythonオブジェクトで使用できますが、オブジェクトをプロセス間でシリアル化/非シリアル化して送信する必要があるため、共有メモリを使用した同等のオブジェクトよりも遅くなります。
Pythonで利用可能な並列処理ライブラリとアプローチの豊富さ があります。 multiprocessing
は優れた、丸みのあるライブラリですが、特別なニーズがある場合は、おそらく他のアプローチのいずれかが優れている可能性があります。
私は同じ問題にぶつかり、それを回避するために小さな共有メモリユーティリティクラスを作成しました。
私はmultiprocessing.RawArray
(ロックフリー)を使用しています。また、配列へのアクセスはまったく同期されていません(ロックフリー)。自分の足を撃たないように注意してください。
このソリューションを使用すると、クアッドコアi7で約3倍に高速化されます。
コードは次のとおりです。自由に使用および改善してください。バグを報告してください。
'''
Created on 14.05.2013
@author: martin
'''
import multiprocessing
import ctypes
import numpy as np
class SharedNumpyMemManagerError(Exception):
pass
'''
Singleton Pattern
'''
class SharedNumpyMemManager:
_initSize = 1024
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(SharedNumpyMemManager, cls).__new__(
cls, *args, **kwargs)
return cls._instance
def __init__(self):
self.lock = multiprocessing.Lock()
self.cur = 0
self.cnt = 0
self.shared_arrays = [None] * SharedNumpyMemManager._initSize
def __createArray(self, dimensions, ctype=ctypes.c_double):
self.lock.acquire()
# double size if necessary
if (self.cnt >= len(self.shared_arrays)):
self.shared_arrays = self.shared_arrays + [None] * len(self.shared_arrays)
# next handle
self.__getNextFreeHdl()
# create array in shared memory segment
shared_array_base = multiprocessing.RawArray(ctype, np.prod(dimensions))
# convert to numpy array vie ctypeslib
self.shared_arrays[self.cur] = np.ctypeslib.as_array(shared_array_base)
# do a reshape for correct dimensions
# Returns a masked array containing the same data, but with a new shape.
# The result is a view on the original array
self.shared_arrays[self.cur] = self.shared_arrays[self.cnt].reshape(dimensions)
# update cnt
self.cnt += 1
self.lock.release()
# return handle to the shared memory numpy array
return self.cur
def __getNextFreeHdl(self):
orgCur = self.cur
while self.shared_arrays[self.cur] is not None:
self.cur = (self.cur + 1) % len(self.shared_arrays)
if orgCur == self.cur:
raise SharedNumpyMemManagerError('Max Number of Shared Numpy Arrays Exceeded!')
def __freeArray(self, hdl):
self.lock.acquire()
# set reference to None
if self.shared_arrays[hdl] is not None: # consider multiple calls to free
self.shared_arrays[hdl] = None
self.cnt -= 1
self.lock.release()
def __getArray(self, i):
return self.shared_arrays[i]
@staticmethod
def getInstance():
if not SharedNumpyMemManager._instance:
SharedNumpyMemManager._instance = SharedNumpyMemManager()
return SharedNumpyMemManager._instance
@staticmethod
def createArray(*args, **kwargs):
return SharedNumpyMemManager.getInstance().__createArray(*args, **kwargs)
@staticmethod
def getArray(*args, **kwargs):
return SharedNumpyMemManager.getInstance().__getArray(*args, **kwargs)
@staticmethod
def freeArray(*args, **kwargs):
return SharedNumpyMemManager.getInstance().__freeArray(*args, **kwargs)
# Init Singleton on module load
SharedNumpyMemManager.getInstance()
if __== '__main__':
import timeit
N_PROC = 8
INNER_LOOP = 10000
N = 1000
def propagate(t):
i, shm_hdl, evidence = t
a = SharedNumpyMemManager.getArray(shm_hdl)
for j in range(INNER_LOOP):
a[i] = i
class Parallel_Dummy_PF:
def __init__(self, N):
self.N = N
self.arrayHdl = SharedNumpyMemManager.createArray(self.N, ctype=ctypes.c_double)
self.pool = multiprocessing.Pool(processes=N_PROC)
def update_par(self, evidence):
self.pool.map(propagate, Zip(range(self.N), [self.arrayHdl] * self.N, [evidence] * self.N))
def update_seq(self, evidence):
for i in range(self.N):
propagate((i, self.arrayHdl, evidence))
def getArray(self):
return SharedNumpyMemManager.getArray(self.arrayHdl)
def parallelExec():
pf = Parallel_Dummy_PF(N)
print(pf.getArray())
pf.update_par(5)
print(pf.getArray())
def sequentialExec():
pf = Parallel_Dummy_PF(N)
print(pf.getArray())
pf.update_seq(5)
print(pf.getArray())
t1 = timeit.Timer("sequentialExec()", "from __main__ import sequentialExec")
t2 = timeit.Timer("parallelExec()", "from __main__ import parallelExec")
print("Sequential: ", t1.timeit(number=1))
print("Parallel: ", t2.timeit(number=1))
これは、 Ray の使用目的です。これは、並列および分散Python用のライブラリです。内部では、 Apache Arrow データレイアウト(これはゼロコピー形式)を使用してオブジェクトをシリアル化し、 共有メモリオブジェクトストア に格納します。コピーを作成せずに複数のプロセスからアクセスされます。
コードは次のようになります。
import numpy as np
import ray
ray.init()
@ray.remote
def func(array, param):
# Do stuff.
return 1
array = np.ones(10**6)
# Store the array in the shared memory object store once
# so it is not copied multiple times.
array_id = ray.put(array)
result_ids = [func.remote(array_id, i) for i in range(4)]
output = ray.get(result_ids)
ray.put
を呼び出さない場合、配列は引き続き共有メモリに格納されますが、func
の呼び出しごとに1回行われますが、これは望みのものではありません。
これは、配列だけでなく配列を含むオブジェクト(たとえば、以下のようにintを配列にマッピングする辞書)でも機能することに注意してください。
IPythonで次のコマンドを実行することで、Rayとpickleのシリアル化のパフォーマンスを比較できます。
import numpy as np
import pickle
import ray
ray.init()
x = {i: np.ones(10**7) for i in range(20)}
# Time Ray.
%time x_id = ray.put(x) # 2.4s
%time new_x = ray.get(x_id) # 0.00073s
# Time pickle.
%time serialized = pickle.dumps(x) # 2.6s
%time deserialized = pickle.loads(serialized) # 1.9s
Rayを使用したシリアル化は、pickleよりもわずかに高速ですが、共有メモリを使用するため、逆シリアル化は1000倍高速になります(もちろん、この数はオブジェクトによって異なります)。
Ray documentation を参照してください。 Ray and Arrowを使用した高速シリアル化 について詳しく読むことができます。注:私はRay開発者の1人です。
ロバート・ニシハラが述べたように、Apache Arrowはこれを簡単にします。具体的には、Plasmaのメモリ内オブジェクト・ストアを使用して、Rayを構築します。
Flaskアプリでのビッグオブジェクトの高速な読み込みと再読み込みのために、brain-plasma
を作成しました。これは、pickle.dumps(...)によって生成されたpickle
'dバイト文字列を含む、Apache Arrowシリアル化可能オブジェクトの共有メモリオブジェクト名前空間です。
Apache RayとPlasmaの主な違いは、オブジェクトIDを追跡することです。ローカルで実行されているプロセス、スレッド、またはプログラムは、brain
オブジェクトから名前を呼び出すことにより、変数の値を共有できます。
$ pip install brain-plasma
$ plasma_store -m 10000000 -s /tmp/plasma
from brain_plasma import Brain
brain = Brain(path='/tmp/plasma/)
brain['a'] = [1]*10000
brain['a']
# >>> [1,1,1,1,...]