マルチプロセッシングのために共有メモリでnumpy配列を使用する


111

マルチプロセッシングモジュールで使用するために、共有メモリでnumpy配列を使用したいと思います。問題は、それをctypes配列としてだけでなく、numpy配列のように使用することです。

from multiprocessing import Process, Array
import scipy

def f(a):
    a[0] = -a[0]

if __name__ == '__main__':
    # Create the array
    N = int(10)
    unshared_arr = scipy.rand(N)
    arr = Array('d', unshared_arr)
    print "Originally, the first two elements of arr = %s"%(arr[:2])

    # Create, start, and finish the child processes
    p = Process(target=f, args=(arr,))
    p.start()
    p.join()

    # Printing out the changed values
    print "Now, the first two elements of arr = %s"%arr[:2]

これにより、次のような出力が生成されます。

Originally, the first two elements of arr = [0.3518653236697369, 0.517794725524976]
Now, the first two elements of arr = [-0.3518653236697369, 0.517794725524976]

配列にはctypesの方法でアクセスできますarr[i]。しかし、それは派手な配列ではなく-1*arr、または、などの操作を実行できませんarr.sum()。解決策はctypes配列をnumpy配列に変換することだと思います。ただし、(この作業を行うことができないことを除いて)それが共有されるとは思わない。

よくある問題の標準的な解決策があるようです。


1
これと同じじゃないの?stackoverflow.com/questions/5033799/...
pygabriel

1
それは全く同じ質問ではありません。リンクされた質問は、subprocessではなくについて尋ねていmultiprocessingます。
Andrew

回答:


82

@unutbuの(もう利用できません)と@Henry Gomersallの回答に追加します。shared_arr.get_lock()必要なときにアクセスを同期するために使用できます。

shared_arr = mp.Array(ctypes.c_double, N)
# ...
def f(i): # could be anything numpy accepts as an index such another numpy array
    with shared_arr.get_lock(): # synchronize access
        arr = np.frombuffer(shared_arr.get_obj()) # no data copying
        arr[i] = -arr[i]

import ctypes
import logging
import multiprocessing as mp

from contextlib import closing

import numpy as np

info = mp.get_logger().info

def main():
    logger = mp.log_to_stderr()
    logger.setLevel(logging.INFO)

    # create shared array
    N, M = 100, 11
    shared_arr = mp.Array(ctypes.c_double, N)
    arr = tonumpyarray(shared_arr)

    # fill with random values
    arr[:] = np.random.uniform(size=N)
    arr_orig = arr.copy()

    # write to arr from different processes
    with closing(mp.Pool(initializer=init, initargs=(shared_arr,))) as p:
        # many processes access the same slice
        stop_f = N // 10
        p.map_async(f, [slice(stop_f)]*M)

        # many processes access different slices of the same array
        assert M % 2 # odd
        step = N // 10
        p.map_async(g, [slice(i, i + step) for i in range(stop_f, N, step)])
    p.join()
    assert np.allclose(((-1)**M)*tonumpyarray(shared_arr), arr_orig)

def init(shared_arr_):
    global shared_arr
    shared_arr = shared_arr_ # must be inherited, not passed as an argument

def tonumpyarray(mp_arr):
    return np.frombuffer(mp_arr.get_obj())

def f(i):
    """synchronized."""
    with shared_arr.get_lock(): # synchronize access
        g(i)

def g(i):
    """no synchronization."""
    info("start %s" % (i,))
    arr = tonumpyarray(shared_arr)
    arr[i] = -1 * arr[i]
    info("end   %s" % (i,))

if __name__ == '__main__':
    mp.freeze_support()
    main()

同期アクセスが必要ない場合、または独自のロックを作成する場合mp.Array()は不要です。mp.sharedctypes.RawArrayこの場合に使用できます。


2
美しい答え!複数の共有配列が必要で、それぞれ個別にロック可能であるが、実行時に配列の数が決定されている場合、ここで行ったことの単純な拡張ですか?
Andrew

3
@Andrew:子プロセスが生成される前に共有配列を作成する必要があります。
jfs

操作の順序についての良い点。しかし、それは私が念頭に置いていたものです。ユーザーが指定した数の共有配列を作成してから、いくつかの子プロセスを生成します。それは簡単ですか?
Andrew

1
@Chicony:配列のサイズを変更することはできません。子プロセスを開始する前に割り当てる必要があったメモリの共有ブロックと考えてください。あなたは、あなたが渡すことができ、すべてのメモリなどを使用する必要はありませんcountnumpy.frombuffer()。あなたは下の使用レベルでそれを行うために試みることができるmmapかのようなものposix_ipc(リサイズしながら、コピー伴うかもしれません)RawArrayアナログ(または既存のライブラリを探してください)直接サイズ変更可能を実装します。または、タスクで許可されている場合:データを部分的にコピーします(一度にすべてが必要ない場合)。「共有メモリのサイズを変更する方法」は別の質問です。
jfs 16

1
@umopapisdn:Pool()プロセスの数を定義します(使用可能なCPUコアの数がデフォルトで使用されます)。関数が呼び出されMた回数ですf()
jfs 2018

21

Arrayオブジェクトが有するget_obj()バッファインタフェースを提示ctypesの配列を返し、それに関連する方法を、。私は以下がうまくいくと思います...

from multiprocessing import Process, Array
import scipy
import numpy

def f(a):
    a[0] = -a[0]

if __name__ == '__main__':
    # Create the array
    N = int(10)
    unshared_arr = scipy.rand(N)
    a = Array('d', unshared_arr)
    print "Originally, the first two elements of arr = %s"%(a[:2])

    # Create, start, and finish the child process
    p = Process(target=f, args=(a,))
    p.start()
    p.join()

    # Print out the changed values
    print "Now, the first two elements of arr = %s"%a[:2]

    b = numpy.frombuffer(a.get_obj())

    b[0] = 10.0
    print a[0]

実行すると、この最初の要素うち版画a今、10.0であることを示すaとはb同じメモリにちょうど2つのビューがあります。

それがマルチプロセッサに対して安全であることを確認するために、オブジェクト、およびその組み込みロックに存在するacquireおよびreleaseメソッドを使用して、すべてが安全にアクセスされることを確認する必要があると思います(私はの専門家ではありません)マルチプロセッサモジュール)。Arraya


@unutbuが彼の(現在は削除されている)回答で示したように、同期がなければ機能しません。
jfs

1
おそらく、配列の後処理にアクセスしたいだけなら、同時実行の問題やロックを心配することなく、きれいに実行できますか?
Henry Gomersall、2011年

この場合は必要ありませんmp.Array
jfs

1
処理コードはロックされた配列を必要とする場合がありますが、データの後処理の解釈は必ずしも必要ではありません。これは問題が正確に何であるかを理解することから来ていると思います。明らかに、共有データに同時にアクセスするには、ある程度の保護が必要になります。
Henry Gomersall、2011年

16

すでに与えられた答えは良いですが、2つの条件が満たされていれば、この問題のはるかに簡単な解決策があります。

  1. あなたは上にあるPOSIX準拠のオペレーティングシステム(例えばLinuxでは、マックOSX)。そして
  2. 子プロセスには、共有配列への読み取り専用アクセスが必要です。

この場合、子プロセスはforkを使用して作成されるため、変数を明示的に共有する必要はありません。分岐した子は自動的に親のメモリ空間を共有します。Pythonマルチプロセッシングのコンテキストでは、これはすべてのモジュールレベルの変数を共有することを意味します。これ、子プロセスまたは明示的に呼び出す関数に明示的に渡す引数には適用されないことに注意してくださいmultiprocessing.Pool

簡単な例:

import multiprocessing
import numpy as np

# will hold the (implicitly mem-shared) data
data_array = None

# child worker function
def job_handler(num):
    # built-in id() returns unique memory ID of a variable
    return id(data_array), np.sum(data_array)

def launch_jobs(data, num_jobs=5, num_worker=4):
    global data_array
    data_array = data

    pool = multiprocessing.Pool(num_worker)
    return pool.map(job_handler, range(num_jobs))

# create some random data and execute the child jobs
mem_ids, sumvals = zip(*launch_jobs(np.random.rand(10)))

# this will print 'True' on POSIX OS, since the data was shared
print(np.all(np.asarray(mem_ids) == id(data_array)))

3
+1本当に貴重な情報。共有されるのがモジュールレベルの変数のみである理由を説明できますか?ローカル変数が親のメモリ空間の一部ではないのはなぜですか?たとえば、ローカル変数Vを持つ関数Fと、Vを参照するF内の関数Gがある場合、なぜこれが機能しないのですか?
Coffee_Table 2017年

5
警告:この回答は少し誤解を招くものです。子プロセスは、フォーク時に、グローバル変数を含む親プロセスの状態のコピーを受け取ります。状態は決して同期されておらず、その瞬間から分岐します。この手法は一部のシナリオ(例:親プロセスのスナップショットを処理して終了するアドホックな子プロセスの分岐)で役立つ場合がありますが、他のシナリオ(例:共有と親プロセスとデータを同期します)。
デビッドスタイン

4
@EelkeSpaak:「フォークされた子が自動的に親のメモリ空間を共有する」というステートメントは正しくありません。厳密に読み取り専用の方法で親プロセスの状態を監視する必要がある子プロセスがある場合、フォーキングはそこに到達しません。子はフォーキングの瞬間に親状態のスナップショットのみを表示します。実際、私がこの制限を発見したとき、それはまさに私が(あなたの答えに従って)やろうとしていたことです。したがって、回答の追記。簡単に言うと、親の状態は「共有」されず、単に子にコピーされます。通常の意味での「共有」ではありません。
David Stein

2
少なくともposixシステムでは、これがコピーオンライトの状況であると誤解していますか?つまり、フォークの後、新しいデータが書き込まれるまでメモリが共有され、その時点でコピーが作成されます。つまり、データが正確に「共有」されないのは事実ですが、パフォーマンスが大幅に向上する可能性があります。プロセスが読み取り専用の場合、コピーのオーバーヘッドはありません。ポイントを正しく理解しましたか?
センダーレ2018年

2
@senderleはい、それはまさに私が意味したことです!したがって、読み取り専用アクセスに関する回答の私のポイント(2)。
EelkeSpaak

11

POSIX共有メモリを使用してpythonインタープリター間でnumpy配列を共有する小さなpythonモジュールを作成しました。多分それは便利でしょう。

https://pypi.python.org/pypi/SharedArray

仕組みは次のとおりです。

import numpy as np
import SharedArray as sa

# Create an array in shared memory
a = sa.create("test1", 10)

# Attach it as a different array. This can be done from another
# python interpreter as long as it runs on the same computer.
b = sa.attach("test1")

# See how they are actually sharing the same memory block
a[0] = 42
print(b[0])

# Destroying a does not affect b.
del a
print(b[0])

# See how "test1" is still present in shared memory even though we
# destroyed the array a.
sa.list()

# Now destroy the array "test1" from memory.
sa.delete("test1")

# The array b is not affected, but once you destroy it then the
# data are lost.
print(b[0])

8

次のsharedmemモジュールを使用できます:https : //bitbucket.org/cleemesser/numpy-sharedmem

次に、元のコードを示します。今回は、NumPy配列のように動作する共有メモリを使用します(NumPy sum()関数を呼び出す最後のステートメントに注意してください)。

from multiprocessing import Process
import sharedmem
import scipy

def f(a):
    a[0] = -a[0]

if __name__ == '__main__':
    # Create the array
    N = int(10)
    unshared_arr = scipy.rand(N)
    arr = sharedmem.empty(N)
    arr[:] = unshared_arr.copy()
    print "Originally, the first two elements of arr = %s"%(arr[:2])

    # Create, start, and finish the child process
    p = Process(target=f, args=(arr,))
    p.start()
    p.join()

    # Print out the changed values
    print "Now, the first two elements of arr = %s"%arr[:2]

    # Perform some NumPy operation
    print arr.sum()

1
注:これはもはや開発されていないと、Linux上で動作していないようgithub.com/sturlamolden/sharedmem-numpy/issues/4
AD

numpy-sharedmemは開発中でない可能性がありますが、Linuxでも機能します。github.com/vmlaker/benchmark-sharedmemを確認してください
Velimir Mlaker 16年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.