web-dev-qa-db-ja.com

python joblibで共有変数に書き込む方法

次のコードは、forループを並列化します。

import networkx as nx;
import numpy as np;
from joblib import Parallel, delayed;
import multiprocessing;

def core_func(repeat_index, G, numpy_arrary_2D):
  for u in G.nodes():
    numpy_arrary_2D[repeat_index][u] = 2;
  return;

if __name__ == "__main__":
  G = nx.erdos_renyi_graph(100000,0.99);
  nRepeat = 5000;
  numpy_array = np.zeros([nRepeat,G.number_of_nodes()]);
  Parallel(n_jobs=4)(delayed(core_func)(repeat_index, G, numpy_array) for repeat_index in range(nRepeat));
  print(np.mean(numpy_array));

ご覧のとおり、出力される期待値は2です。ただし、クラスター(マルチコア、共有メモリ)でコードを実行すると、0.0が返されます。

問題は、各ワーカーがnumpy_arrayオブジェクトの独自のコピーを作成し、メイン関数で作成されたものが更新されないことだと思います。 numpy配列numpy_arrayを更新できるようにコードを変更するにはどうすればよいですか?

18
user3813057

joblibは、 its manual のように、デフォルトでprocessesのマルチプロセッシングプールを使用します。

内部的には、ParallelオブジェクトがPythonインタープリターをフォークしてリストの各項目を実行するマルチプロセッシングプールを作成します。遅延関数は、関数呼び出し構文を持つタプル(関数、引数、kwargs)。

つまり、すべてのプロセスは配列の元の状態を継承しますが、内部に書き込んだものはすべて、プロセスが終了すると失われます。関数の結果のみが呼び出し側(メイン)プロセスに返されます。ただし、何も返さないため、Noneが返されます。

共有配列を変更可能にするには、スレッドを使用する方法と共有メモリを使用する方法の2つがあります。


プロセスとは異なり、スレッドはメモリを共有します。したがって、配列に書き込むことができ、すべてのジョブがこの変更を確認できます。 joblibマニュアルによると、これは次のように行われます:

  Parallel(n_jobs=4, backend="threading")(delayed(core_func)(repeat_index, G, numpy_array) for repeat_index in range(nRepeat));

実行すると:

$ python r1.py 
2.0

ただし、複雑なものを配列に書き込む場合は、データまたはデータピースの周りのロックを適切に処理する必要があります。そうしないと、競合状態に陥ります(グーグル)。

Pythonの計算マルチスレッドは制限されているため、I/Oマルチスレッドとは異なり)、GILについても注意深く読んでください。


それでもプロセスが必要な場合(GILなどが原因で)、その配列を共有メモリに配置できます。

これはもう少し複雑なトピックですが、 joblib + numpy shared memory examplejoblibマニュアルにも示されています。

9
Sergey Vasilyev

セルゲイが彼の答えで書いたように、プロセスは状態とメモリを共有しません。これが、期待される答えが表示されない理由です。

スレッド同じプロセスで実行されるため、状態とメモリ空間を共有します。これは、多くのI/O操作がある場合に役立ちます。 [〜#〜] gil [〜#〜]のため、処理能力(CPU数)は増えません

プロセス間で通信するための1つの手法はManagerを使用したプロキシオブジェクトです。プロセス間でリソースを同期するマネージャーオブジェクトを作成します。

Manager()によって返されるマネージャーオブジェクトは、Pythonオブジェクトを保持し、他のプロセスがプロキシを使用してそれらを操作できるようにするサーバープロセスを制御します。

私はこのコードをテストしていません(使用するモジュールがすべてあるわけではありません)。コードをさらに変更する必要があるかもしれませんが、Managerオブジェクトを使用すると、次のようになります。

if __name__ == "__main__":
    G = nx.erdos_renyi_graph(100000,0.99);
    nRepeat = 5000;

    manager = multiprocessing.Manager()
    numpys = manager.list(np.zeros([nRepeat, G.number_of_nodes()])

    Parallel(n_jobs=4)(delayed(core_func)(repeat_index, G, numpys, que) for repeat_index in range(nRepeat));
    print(np.mean(numpys));
1
Chen A.