マルチプロセッシングプロセス間で大規模な読み取り専用のNumpy配列を共有する


88

60GBのSciPyアレイ(マトリックス)があり、5つ以上のmultiprocessing Processオブジェクト間で共有する必要があります。私はnumpy-sharedmemを見て、SciPyリストでこの議論を読みました。2つのアプローチがあるようです-numpy-sharedmemとを使用してmultiprocessing.RawArray()NumPyをdtypesにマッピングしctypeます。さて、これnumpy-sharedmemが進むべき道のようですが、良い参考例はまだ見ていません。配列(実際には行列)は読み取り専用になるため、ロックは必要ありません。さて、サイズが大きいので、コピーは避けたいと思います。それはのように聞こえる正しい方法で作成することであるだけとして配列のコピーをsharedmem配列し、その後にそれを渡すProcessオブジェクト?いくつかの具体的な質問:

  1. sharedmemハンドルを実際にサブに渡すための最良の方法は何Process()ですか?1つの配列を渡すためだけにキューが必要ですか?パイプの方がいいでしょうか?Process()サブクラスのinit(pickle化されていると想定している)への引数として渡すことはできますか?

  2. 上でリンクしたディスカッションでnumpy-sharedmemは、64ビットセーフではないという言及がありますか?私は間違いなく32ビットアドレス指定できないいくつかの構造を使用しています。

  3. このRawArray()アプローチにはトレードオフがありますか?遅い、バギー?

  4. numpy-sharedmemメソッドにctypeからdtypeへのマッピングが必要ですか?

  5. 誰かがこれを行ういくつかのオープンソースコードの例を持っていますか?私は非常に実践的な知識を持っており、良い例がなければ、これを機能させるのは困難です。

他の人にこれを明確にするために私が提供できる追加情報がある場合は、コメントしてください。追加します。ありがとう!

これはUbuntuLinuxと多分MacOSで実行する必要がありますが、移植性は大きな問題ではありません。


1
異なるプロセスがその配列に書き込む場合は、プロセスmultiprocessingごとに全体のコピーを作成することを期待してください。
tiago 2013

3
@tiago:「配列(実際には行列)は読み取り専用であるため、どのような種類のロックも必要ありません」
Jan-Philip Gehrcke博士2013

1
@tiago:また、(への引数を介してtarget_function)明示的に指示されていない限り、マルチプロセッシングはコピーを作成しません。オペレーティングシステムは、変更があった場合にのみ、親のメモリの一部を子のメモリスペースにコピーします。
Jan-Philip Gehrcke博士2013


私は尋ねたいくつか 質問する前にこのことについては。私の解決策はここにあります:github.com/david-hoffman/peaks/blob/…(申し訳ありませんが、コードは災害です)。
デビッドホフマン

回答:


30

@VelimirMlakerは素晴らしい答えを出しました。コメントと小さな例を少し追加できると思いました。

(sharedmemに関するドキュメントはあまり見つかりませんでした。これらは私自身の実験の結果です。)

  1. サブプロセスの開始時、または開始後にハンドルを渡す必要がありますか?前者の場合は、targetargs引数を使用できますProcess。これは、グローバル変数を使用するよりも優れている可能性があります。
  2. リンクしたディスカッションページから、64ビットLinuxのサポートがしばらく前にsharedmemに追加されたようですので、問題ではない可能性があります。
  3. これはわかりません。
  4. いいえ。以下の例を参照してください。

#!/usr/bin/env python
from multiprocessing import Process
import sharedmem
import numpy

def do_work(data, start):
    data[start] = 0;

def split_work(num):
    n = 20
    width  = n/num
    shared = sharedmem.empty(n)
    shared[:] = numpy.random.rand(1, n)[0]
    print "values are %s" % shared

    processes = [Process(target=do_work, args=(shared, i*width)) for i in xrange(num)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()

    print "values are %s" % shared
    print "type is %s" % type(shared[0])

if __name__ == '__main__':
    split_work(4)

出力

values are [ 0.81397784  0.59667692  0.10761908  0.6736734   0.46349645  0.98340718
  0.44056863  0.10701816  0.67167752  0.29158274  0.22242552  0.14273156
  0.34912309  0.43812636  0.58484507  0.81697513  0.57758441  0.4284959
  0.7292129   0.06063283]
values are [ 0.          0.59667692  0.10761908  0.6736734   0.46349645  0.
  0.44056863  0.10701816  0.67167752  0.29158274  0.          0.14273156
  0.34912309  0.43812636  0.58484507  0.          0.57758441  0.4284959
  0.7292129   0.06063283]
type is <type 'numpy.float64'>

この関連する質問は役に立つかもしれません。


37

Linux(またはPOSIX準拠のシステム)を使用している場合は、この配列をグローバル変数として定義できます。Linuxで、新しい子プロセスを開始するときにmultiprocessing使用fork()しています。新しく生成された子プロセスは、変更しない限り、メモリを親と自動的に共有します(コピーオンライトメカニズム)。

「配列(実際には行列)は読み取り専用なので、どのような種類のロックも必要ありません」と言っているので、この動作を利用することは非常に単純でありながら非常に効率的なアプローチです。すべての子プロセスがアクセスします。この大きなnumpy配列を読み取るとき、物理メモリ内の同じデータ。

配列をProcess()コンストラクターに渡さないでください。これmultiprocessingによりpickle、データが子に指示されます。これは、非常に非効率的であるか、不可能です。Linuxでは、fork()子の直後に同じ物理メモリを使用する親の正確なコピーがあるため、必要なのは、マトリックスを含むPython変数に、target渡した関数内からアクセスできることを確認することだけですProcess()。これは通常、「グローバル」変数を使用して実現できます。

コード例:

from multiprocessing import Process
from numpy import random


global_array = random.random(10**4)


def child():
    print sum(global_array)


def main():
    processes = [Process(target=child) for _ in xrange(10)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()


if __name__ == "__main__":
    main()

サポートしていません- Windows上fork()-multiprocessingのWin32 API呼び出しを使用していますCreateProcess。特定の実行可能ファイルからまったく新しいプロセスを作成します。なぜ、Windowsのいずれかであるということです必要な親の実行時に作成されたものでニーズのデータならば子供にデータを酸洗いします。


3
コピーオンライトは、参照カウンターを含むページをコピーします(したがって、フォークされた各Pythonには独自の参照カウンターがあります)が、データ配列全体はコピーされません。
ロビンス2013

1
私は、グローバル変数よりもモジュールレベルの変数でより多くの成功を収めたことを付け加えます...つまり、フォークの前にグローバルスコープのモジュールに変数を追加します
robince 2013

5
この質問/回答に出くわした人への注意:マルチスレッド操作にOpenBLASにリンクされたNumpyを使用している場合は、使用時にマルチスレッドを無効にしてください(export OPENBLAS_NUM_THREADS = 1)。multiprocessingそうしないと、子プロセスがハングする可能性があります(通常、共有グローバル配列/行列で線形代数演算を実行するときに、n個のプロセッサではなく1個のプロセッサの1 / nを使用します。OpenBLASとの既知のマルチスレッドの競合は、Pythonにまで及ぶようですmultiprocessing
Dologan 2013

1
Pythonが、fork与えられたパラメーターProcessをシリアル化する代わりに、OSを使用してパラメーターを渡すだけではない理由を誰かが説明できますか?それはできなかった、あるforkだけの親プロセスに適用される前に、 childパラメータ値は、OSからまだ利用可能であるように、と呼ばれていますか?シリアル化するよりも効率的だと思いますか?
最大

2
fork()これはWindowsでは利用できないことを私たちは皆知っています。それは私の回答とコメントで何度も述べられています。私は、これはあなたの最初の質問だったことを知っている、と私は上記の4件のコメントがそれに答え、この「妥協は、より良い保守用と同じ動作を確保するため、デフォルトでは、両方のプラットフォーム上でパラメータ転送の同じ方法を使用することです。」。どちらの方法にも長所と短所があります。そのため、Python 3では、ユーザーが方法を選択する際の柔軟性が高くなっています。この議論は、ここで行うべきではない詳細を話さずに生産的ではありません。
Dr. Jan-Philip Gehrcke 2015年

24

あなたは私が書いた小さなコードに興味があるかもしれません:github.com/vmlaker/benchmark-sharedmem

関心のある唯一のファイルはですmain.py。これはnumpy-sharedmemのベンチマークです。コードは、パイプを介して、生成されたプロセスに配列(numpyまたはのいずれかsharedmem)を渡すだけです。労働者はただsum()データを要求します。2つの実装間のデータ通信時間を比較することにのみ興味がありました。

また、別のより複雑なコードgithub.com/vmlaker/sherlockも作成しました

ここでは、OpenCVでのリアルタイム画像処理にnumpy-sharedmemモジュールを使用しています。OpenCVの新しいcv2APIによると、画像はNumPy配列です。画像(実際にはその参照)は、multiprocessing.Manager(キューやパイプを使用するのではなく)作成されたディクショナリオブジェクトを介してプロセス間で共有されます。プレーンなNumPy配列を使用する場合と比較して、パフォーマンスが大幅に向上しています。

パイプ対キュー

私の経験では、パイプを使用したIPCはキューよりも高速です。キューはロックを追加して複数のプロデューサー/コンシューマーにとって安全にするため、これは理にかなっています。パイプはしません。ただし、2つのプロセスしかやり取りしない場合は、Pipeを使用するのが安全です。または、ドキュメントにあるように、次のようになります。

...パイプの異なる端を同時に使用するプロセスによる破損のリスクはありません。

sharedmem安全性

sharedmemモジュールの主な問題は、プログラムが正常に終了しないときにメモリリークが発生する可能性があることです。これについては、ここでの長い議論説明されています。2011年4月10日、Sturlaはメモリリークの修正について言及していますが、それ以来、GitHubのSturla Molden(github.com/sturlamolden/sharedmem-numpy)とBitbucketのChris Lee-Messer ()の両方のリポジトリを使用して、リークが発生しています。bitbucket.org/cleemesser/numpy-sharedmem)。


ありがとう、非常に有益です。sharedmemただし、メモリリークは大したことのように聞こえます。それを解決するためのリードはありますか?
2013

1
リークに気付くだけでなく、コードでそれを探していません。上記の「sharedmemsafety」の下sharedmemに、参考のために、モジュールの2つのオープンソースリポジトリのキーパーを追加しました。
Velimir Mlaker 2013

14

配列がそれほど大きい場合は、を使用できますnumpy.memmap。たとえば、ディスクに配列が格納され'test.array'ている場合、「書き込み」モードでも同時プロセスを使用してその配列のデータにアクセスできますが、必要なのは「読み取り」モードのみであるため、ケースは単純です。

アレイの作成:

a = np.memmap('test.array', dtype='float32', mode='w+', shape=(100000,1000))

その後、通常の配列と同じ方法でこの配列を埋めることができます。例えば:

a[:10,:100]=1.
a[10:,100:]=2.

変数を削除すると、データはディスクに保存されますa

後で、次のデータにアクセスする複数のプロセスを使用できますtest.array

# read-only mode
b = np.memmap('test.array', dtype='float32', mode='r', shape=(100000,1000))

# read and writing mode
c = np.memmap('test.array', dtype='float32', mode='r+', shape=(100000,1000))

関連する回答:


3

また、タスクを適切に分割できるかのように、pyroのドキュメントを確認すると、同じマシンのさまざまなコアだけでなく、さまざまなマシンでさまざまなセクションを実行できるので便利です。


0

マルチスレッドを使用してみませんか?メインプロセスのリソースはそのスレッドによってネイティブに共有できるため、マルチスレッドは明らかにメインプロセスが所有するオブジェクトを共有するためのより良い方法です。

あなたがPythonのGILメカニズムを心配している場合、多分あなたはに頼ることができますnogilnumba

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