【問題】 並列処理が途中で終了する
Python のモジュール multiprocessing を用いた並列処理が、意図せず途中で終了してしまいました。
実行環境
- Python 3.12
- Jupyterlab 4.0.11
- Mac M2 CPU
問題発生例
問題を単純化するため、乱数を出力する関数を定義し、並列処理により高速化する Python スクリプトを以下のように書いてみます。
import random
from multiprocessing import Pool, get_context
def random_output(n):
hoge = random.random()
print(hoge)
return hoge
with get_context("fork").Pool(processes = 3) as pool:
result = pool.imap(random_output, range(30))
next(result)
- x ∈ [0, 1] を満たす乱数を出力する関数 random_output を定義し、並列処理数を3に設定して、random_print 関数を30回実行させます。
- Pool の属性として imap 関数を用いています。
0.40398745671651450.89900968116753680.26495311715314007
0.11601183665445658
0.98591264346923340.09981477280742523
途中で出力が止まってしまいますが、エラーメッセージは何も出力されません。
- 並列処理数(変数 processes)を変えると、出力される乱数の個数も変化しますが、おおよそ並列処理数の2倍の個数の乱数が出力されて、処理が途中終了するようです。
- 関数の実行回数を 50 回より多く増やしても、状況はほぼ変わりません。
【解決策】 出力方法を工夫する
import random
from multiprocessing import Pool, get_context
def random_output(n):
hoge = random.random()
return hoge
with get_context("fork").Pool(processes = 3) as pool:
result = pool.imap(random_output, range(30))
for value in result:
print(value)
主な変更点
関数 random_output 内で print 関数を用いて出力すると混乱しやすいため、imap 関数の戻り値(イテレータ)から for ループで値を取得して出力する形式としました。
0.7218835018631663
0.3344170045104009
0.2780252849217204
0.24464348558677929
0.7824587594620069
0.4031033777635977
0.4004705738677147
0.47249736737995274
0.2829334318060327
0.1198023443666798
0.016142659436964912
0.29097633922228905
0.5351089611467076
0.978746281757676
0.33647064561693363
0.703743517087453
0.8197703208763935
0.9998776471460239
0.4161895275766967
0.17547474473294977
0.12662025058123794
0.41937914018030653
0.3755981546742474
0.3983354791382634
0.1799064461511578
0.3390259115731551
0.9256859596614425
0.44345468675582844
0.29497952238706626
0.7332903747559089
出力結果もかなり見やすくなりましたね。
途中終了の想定原因
imap 関数は遅延評価関数であり、プロセスごとに開始時刻がずれています。関数 random_output 中で print 文を定義したことによって、関数が実行されるたびに print 関数も実行され、その出力がプロセス間で競合した、と推定されます(print 関数はプログラムの実行速度を低めます)。
その結果、並列処理は完全に終了していないのに、あたかも並列処理を終了しきったかのような振る舞いを示しています。そこで、イテレータに全ての結果を格納した後に、for ループで結果を出力することで、イテレータを完全に消費する対策を取りました。
補足
上記のプログラムは非常に単純なため、imap 関数を map 関数に変更する等でも並列処理の途中終了を解決できますが、メモリを多用する複雑なプログラムの場合、map 関数よりも imap 関数や imap_unordered 関数を用いた方が、処理が速く終わるケースが多いようです。