マルチプロセッシングの共有メモリオブジェクト


123

私はメモリに大きな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

配列をコピーしなかったのに、なぜこんなに大きなオーバーヘッドがあるのですか?そして、共有メモリは私をどのように節約しますか?



あなたはドキュメントを見ましよね?
Lev Levitsky

@FrancisAvilaは、配列だけでなく任意のPythonオブジェクトを共有する方法はありますか?
ヴェンデッタ2012年

1
@LevLevitsky私は尋ねなければなりません、配列だけでなく任意のpythonオブジェクトを共有する方法はありますか?
ヴェンデッタ2012年

2
この答えは、任意のPythonオブジェクトを共有できない理由をうまく説明しています。
ジャンヌカリラ

回答:


121

コピーオンライトfork()セマンティクス(一般的なunixなど)を使用するオペレーティングシステムを使用している場合、データ構造を変更しない限り、追加のメモリを消費せずにすべての子プロセスで使用できます。特別なことをする必要はありません(ただし、オブジェクトを変更しないようにしてください)。

問題に対して実行できる最も効率的なこと、配列を(numpyまたはを使用してarray)効率的な配列構造にパックし、それを共有メモリに配置してでラップしmultiprocessing.Array、それを関数に渡すことです。この回答は、その方法を示しています

あなたがしたい場合は、書き込み可能な共有オブジェクトを、あなたは同期やロックのいくつかの種類でそれをラップする必要があります。multiprocessingこれを行う2つの方法を提供します。1つは共有メモリ(単純な値、配列、またはctypesに適しています)またはManagerプロキシを使用し、1つのプロセスがメモリを保持し、マネージャが他のプロセスからのアクセスを(ネットワーク経由でも)調停します。

このManagerアプローチは任意のPythonオブジェクトで使用できますが、オブジェクトをシリアル化/非シリアル化してプロセス間で送信する必要があるため、共有メモリを使用する同等のものよりも遅くなります。

Pythonでは豊富な並列処理ライブラリとアプローチを利用できますmultiprocessingは優れた丸みを帯びたライブラリですが、特別なニーズがある場合は、おそらく他のアプローチのいずれかがより良いでしょう。


25
ただ注意してください。Pythonでは、fork()は実際にはコピーオンアクセスを意味します(オブジェクトにアクセスするだけでその参照カウントが変更されるため)。
Fabio Zadrozny

3
@FabioZadrozny実際にオブジェクト全体をコピーしますか、それとも参照カウントを含むメモリページだけをコピーしますか?
zigg

5
AFAIK、refcountを含むメモリページのみ(つまり、各オブジェクトアクセスで4kb)。
Fabio Zadrozny

1
@maxクロージャーを使用します。に指定された関数apply_asyncは、引数を介するのではなく、スコープ内の共有オブジェクトを直接参照する必要があります。
フランシスアビラ

3
@FrancisAvilaどうやってクロージャーを使うの?apply_asyncに与える関数は選択可能にすべきではありませんか?それともmap_asyncの制限だけですか?
GermanK 2015年

17

私は同じ問題に遭遇し、それを回避するために小さな共有メモリユーティリティクラスを書きました。

私は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 __name__ == '__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))

マルチプロセッシングプールを作成する前に共有メモリ配列を設定する必要があることを理解したばかりですが、その理由はまだわかりませんが、逆の方法では確実に機能しません。
martin.preinfalk 2013年

その理由は、マルチプロセッシングプールはプールがインスタンス化されるときにfork()を呼び出すため、その後に作成された共有メモリへのポインターにアクセスできなくなるためです。
Xiv

py35でこのコードを試したところ、multiprocessing.sharedctypes.pyで例外が発生したので、このコードはpy2専用だと思います。
HillierDániel博士、

11

これは、並列および分散Python用のライブラリであるRayの使用目的です。内部的には、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を配列にマッピングする辞書)。

Rayとpickleのシリアル化のパフォーマンスを比較するには、IPythonで次のコマンドを実行します。

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を使用したシリアライゼーションはピクルよりもわずかに高速ですが、共有メモリを使用しているため、デシリアライゼーションは1000倍高速です(もちろん、この数はオブジェクトによって異なります)。

Rayのドキュメントを参照してください。RayとArrowを使用し高速シリアル化の詳細を読むことができます。注:私はRay開発者の1人です。


1
レイ、いいね!しかし、以前にこのライブラリを使用してみましたが、残念ながら、Rayはウィンドウをサポートしていないことに気づきました。できるだけ早くWindowsをサポートできることを願っています。開発者、ありがとう!
Hzzkygcs

5

Robert Nishiharaが述べたように、Apache Arrowは、特にRayが構築されているPlasmaインメモリオブジェクトストアでこれを簡単にします。

Flaskアプリで大きなオブジェクトの読み込みと再読み込みを高速化するために、特に脳プラズマを作成しました。これは、pickleによって生成された 'dバイト文字列を含む、Apache Arrow-serializableオブジェクトの共有メモリオブジェクト名前空間ですpickle.dumps(...)

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,...]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.