multiprocessing.Pool.map()
などのメソッドに最適なchunksize
引数を決定する要因は何ですか? .map()
メソッドは、デフォルトのチャンクサイズ(以下で説明)に任意のヒューリスティックを使用しているようです。その選択の動機は何ですか?また、特定の状況/設定に基づいたより思慮深いアプローチがありますか?
例-私は:
iterable
を約1500万個の要素を持つ.map()
に渡します。processes = os.cpu_count()
内でデフォルトの multiprocessing.Pool()
を使用します。私の素朴な考え方は、24人のワーカーそれぞれに等しいサイズのチャンク、つまり15_000_000 / 24
または625,000を割り当てることです。大きなチャンクは、すべてのワーカーを完全に活用しながら、離職/オーバーヘッドを削減する必要があります。しかし、これは各ワーカーに大きなバッチを与えることの潜在的な欠点をいくつか逃しているようです。これは不完全な画像ですか、何が欠けていますか?
私の質問の一部は、chunksize=None
の場合のデフォルトロジックに由来します:.map()
と.starmap()
の両方が .map_async()
を呼び出します。
def _map_async(self, func, iterable, mapper, chunksize=None, callback=None,
error_callback=None):
# ... (materialize `iterable` to list if it's an iterator)
if chunksize is None:
chunksize, extra = divmod(len(iterable), len(self._pool) * 4) # ????
if extra:
chunksize += 1
if len(iterable) == 0:
chunksize = 0
divmod(len(iterable), len(self._pool) * 4)
の背後にあるロジックは何ですか?これは、チャンクサイズが15_000_000 / (24 * 4) == 156_250
に近いことを意味します。 len(self._pool)
に4を掛ける意図は何ですか?
これにより、結果のチャンクサイズが4倍小さいになります。これは、pool._pool
の反復子の長さをワーカー数で除算するだけで構成される、上記の「単純なロジック」よりも小さくなります。
最後に、これもあります スニペット.imap()
のPythonドキュメントから、私の好奇心をさらに高めます:
chunksize
引数は、map()
メソッドで使用されるものと同じです。chunksize
に大きな値を使用する非常に長いイテラブルでは、デフォルト値1を使用するよりも高速にジョブを完了することができますmuch。
関連する答えですが、少し高すぎます: Pythonマルチプロセッシング:大きなチャンクサイズが遅いのはなぜですか? 。
この回答について
この回答は、受け入れられた回答 上記 のパートIIです。
詳細に入る前に、以下の2つのgifを検討してください。さまざまなiterable
長さの範囲に対して、2つの比較されたアルゴリズムが渡されたiterable
(それまでにシーケンスになります)をチャンクする方法と、結果のタスクが分散される方法を示します。ワーカーの順序はランダムであり、実際のワーカーごとの分散タスクの数は、軽量タスクルまたはワイドシナリオのタスクルのこの画像とは異なる場合があります。前述のように、オーバーヘッドもここには含まれていません。ただし、送信可能なデータサイズが無視できるほど密集したシナリオで十分に重いタスクセルの場合、実際の計算では非常によく似た絵が描かれます。
「5。Pool's Chunksize-Algorithm」の章に示されているように、Poolのchunksize-algorithmを使用すると、チャンクの数はn_chunks == n_workers * 4
で安定し、十分な反復可能性が得られます。 n_chunks == n_workers
とn_chunks == n_workers + 1
は、素朴なアプローチで。単純なアルゴリズムが適用される場合:n_chunks % n_workers == 1
はn_chunks == n_workers + 1
のTrue
であるため、単一のワーカーのみが使用される新しいセクションが作成されます。
単純なチャンクサイズアルゴリズム:
同じ数のワーカーでタスクを作成したと思うかもしれませんが、これは
len_iterable / n_workers
に残りがない場合にのみ当てはまります。 is余りがある場合、単一のワーカーに対して1つのタスクのみを持つ新しいセクションがあります。その時点で、計算はもはや並列ではなくなります。
以下に、第5章に示されている図に似た図を示しますが、チャンクの数ではなくセクションの数を表示しています。プールのフルチャンクサイズアルゴリズム(n_pool2
)の場合、n_sections
は、悪名高いハードコーディングされた要因4
で安定します。単純なアルゴリズムの場合、n_sections
は1と2を交互に使用します。
プールのチャンクサイズアルゴリズムの場合、前述のextra-treatmentによるn_chunks = n_workers * 4
での安定化により、ここに新しいセクションが作成されず、が維持されます。 /アイドリングシェア反復可能な十分な長さの1つのワーカーに制限されます。それだけでなく、アルゴリズムはIdling Shareの相対サイズを縮小し続けるため、RDE値は100%に収束します。
n_workers=4
の「十分な長さ」は、たとえばlen_iterable=210
です。それ以上の反復可能要素の場合、Idling Shareは1人のワーカーに制限されます。最初のチャンクサイズアルゴリズム内の4
-- multiplicationにより元々失われた特性場所。
単純なchunksize-algorithmも100%に収束しますが、非常に遅くなります。収束効果は、2つのセクションがある場合にテールの相対的な部分が縮小するという事実のみに依存します。採用されたワーカーが1つだけのこのテールは、n_workers - 1
の最大残余であるx軸の長さlen_iterable / n_workers
に制限されます。
実際のRDE値は、単純とプールのチャンクサイズアルゴリズムでどのように異なりますか?
以下に、2から100までのすべてのワーカー数について、5000までのすべての反復可能な長さのRDE値を示す2つのヒートマップを見つけます。カラースケールは0.5から1(50%-100%)。左のヒートマップのナイーブアルゴリズムでは、より暗い領域(RDE値が低い)に気付くでしょう。対照的に、右側のプールのチャンクサイズアルゴリズムは、はるかに明るい絵を描きます。
左下の暗いコーナーと右上の明るいコーナーの対角線上の勾配は、「長い反復可能」と呼ばれるもののワーカー数への依存を再び示しています。
各アルゴリズムでどれほど悪いことがありますか?
Poolのchunksize-algorithmの場合、RDEの値81.25%は、上記で指定したワーカーと反復可能な長さの範囲の最小値です。
素朴なチャンクサイズアルゴリズムでは、事態はさらに悪化する可能性があります。計算された最小のRDEは50.72%です。この場合、計算時間のほぼ半分で、単一のワーカーが実行されています! ナイツランディング の所有者であることに誇りを持ってください。 ;)
前の章では、最初はマルチプロセッシングをこのような厄介なトピックにする本質的な詳細から取り除かれた、純粋に数学的な分布問題の単純化されたモデルを検討しました。分布モデル(DM)aloneが実際に観察されるワーカーの使用率を説明するのにどれだけ貢献できるかをよりよく理解するために、real計算によって描かれる並列スケジュールを見てみましょう。
次のプロットはすべて、単純なcpuにバインドされたダミー関数の並列実行を処理します。これはさまざまな引数で呼び出されるため、入力値に応じて描画された並列スケジュールがどのように変化するかを確認できます。この関数内の「作業」は、範囲オブジェクトの反復のみで構成されます。膨大な数を渡すので、これはすでにコアをビジー状態に保つのに十分です。オプションで、この関数は変更されずに返されるtaskel固有の余分なdata
を取ります。すべてのtaskelはまったく同じ量の作業で構成されているため、ここではまだ高密度シナリオを扱っています。
この関数は、ns-resolutionでタイムスタンプを取得するラッパーで装飾されています(Python 3.7以降)。タイムスタンプは、taskelのタイムスパンを計算するために使用されるため、経験的な並列スケジュールの描画を可能にします。
@stamp_taskel
def busy_foo(i, it, data=None):
"""Dummy function for CPU-bound work."""
for _ in range(int(it)):
pass
return i, data
def stamp_taskel(func):
"""Decorator for taking timestamps on start and end of decorated
function execution.
"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time_ns()
result = func(*args, **kwargs)
end_time = time_ns()
return (current_process().name, (start_time, end_time)), result
return wrapper
また、プールのスターマップメソッドは、スターマップコール自体のみがタイミングをとるように装飾されています。この呼び出しの「開始」と「終了」により、生成された並列スケジュールのx軸の最小値と最大値が決まります。
Python 3.7.1、Ubuntu 18.04.2、Intel®Core™i7-2600K CPU @ 3.40GHz×8のスペックのマシン上の4つのワーカープロセスで40タスクの計算を観察します。
変化する入力値は、forループの反復回数(30k、30M、600M)と追加送信データサイズ(taskel、numpy-ndarrayごと:0 MiB、50 MiB)です。
...
N_WORKERS = 4
LEN_ITERABLE = 40
ITERATIONS = 30e3 # 30e6, 600e6
DATA_MiB = 0 # 50
iterable = [
# extra created data per taskel
(i, ITERATIONS, np.arange(int(DATA_MiB * 2**20 / 8))) # taskel args
for i in range(LEN_ITERABLE)
]
with Pool(N_WORKERS) as pool:
results = pool.starmap(busy_foo, iterable)
以下に示す実行は、チャンクの順序が同じになるように厳選されているため、分散モデルの並列スケジュールと比較してより良い違いを見つけることができますが、ワーカーがタスクを取得する順序が非決定的であることを忘れないでください。
繰り返しますが、分散モデルは、6.2章で既に見たように、並列スケジュールを「予測」します。
ここでの最初の実行は非常に短く、タスクは非常に「軽量」です。 pool.starmap()
- call全体は、合計で14.5ミリ秒かかりました。DMに反して、アイドリングはテールセクションに限定されず、タスク間やタスクセル間でも発生します。なぜなら、ここでの実際のスケジュールには、当然あらゆる種類のオーバーヘッドが含まれているからです。ここでのアイドリングとは、タスクテルのすべて外側を意味します。可能なrealアイドリングduring前述のように、taskelはキャプチャされません。
さらに、すべてのワーカーが同時にタスクを取得するわけではないことがわかります。これは、すべてのワーカーが共有inqueue
を介してフィードされており、一度に1人のワーカーのみがそれを読み取ることができるためです。 outqueue
にも同じことが当てはまります。これにより、後で説明するように、限界サイズ以外のサイズのデータを送信するとすぐに、大きな混乱を引き起こす可能性があります。
さらに、すべてのtaskelが同じ量の作業で構成されているという事実にもかかわらず、taskelの実際に測定されたタイムスパンは大きく異なることがわかります。 worker-3とworker-4に配布されたタスクは、最初の2人のワーカーによって処理されたタスクよりも時間がかかります。今回の実行では、その時点でworker-3/4のコアで turbo boost が使用できなくなったためと思われるため、より低いクロックレートでタスクを処理しました。
計算全体が非常に軽いため、ハードウェアまたはOSによって導入されたカオスファクターがPSを大幅に歪める可能性があります。計算は「風上の葉」であり、理論的に適切なシナリオであっても、DM-predictionはほとんど意味を持ちません。
Forループの反復回数を30,000から3,000万に増やすと、_ dm _によって提供されるデータによって予測されるものと完全に一致する実際の並列スケジュールになります。、やったー! taskelごとの計算は、開始時と中間でアイドリング部分をマージナリングできるほど十分に重いため、DMが予測した大きなアイドリングシェアのみが表示されます。
30Mの反復を維持しながら、タスクごとに50 MiBを前後に追加送信すると、状況が再びゆがみます。ここでは、キューイング効果がよく見えます。 Worker-4は、Worker-1よりも2番目のタスクを長く待つ必要があります。 70人の従業員がいるこのスケジュールを想像してみてください!
タスクセルが計算上非常に軽いが、ペイロードとして顕著な量のデータを提供する場合、単一の共有キューのボトルネックにより、物理的なコアに裏打ちされている場合でも、プールにワーカーを追加することによる追加の利点を防ぐことができます。このような場合、Worker-1が最初のタスクを実行し、Worker-40が最初のタスクを取得する前であっても新しいタスクを待機できます。
Pool
の計算時間が常にワーカーの数に比例して減少しない理由が明らかになります。 canに沿って比較的大量のデータを送信すると、ワーカーのアドレス空間にデータがコピーされるのを待つことにほとんどの時間が費やされ、一度に1人のワーカーしかフィードできないシナリオにつながります。
ここで50 MiBを再度送信しますが、反復回数を30Mから600Mに増やします。これにより、合計計算時間が10秒から152秒になります。描かれた並列スケジュールagainは、予測されたものと完全に一致しており、データコピーのオーバーヘッドは無視されます。
説明した4
による乗算は、スケジューリングの柔軟性を高めますが、taskel分布の不均一性も活用します。この乗算を使用しない場合、アイドリングシェアは、短い反復可能オブジェクト(DMfor Dense Scenario)でも1人のワーカーに制限されます。プールのchunksize-algorithmは、その特性を回復するために特定のサイズのinput-iterableを必要とします。
この回答が示すように、プールのチャンクサイズアルゴリズムは、少なくとも平均的な場合と長いオーバーヘッドが考慮されていないため、単純なアプローチと比較して平均でコア使用率が向上します。ここでのナイーブアルゴリズムの分布効率(DE)は〜51%になりますが、プールのチャンクサイズアルゴリズムの低さは〜81%になります。DEただし、IPCのような並列化オーバーヘッド(PO)は含まれません。第8章では、DEが、オーバーヘッドがわずかに抑えられた高密度シナリオの予測能力を引き続き発揮できることを示しました。
Poolのchunksize-algorithmは、単純なアプローチと比較して、より高いDEを達成するという事実にもかかわらず、は最適なtaskel分布を提供しません。すべての入力コンステレーションに対して。単純な静的チャンキングアルゴリズムは(オーバーヘッドを含む)並列化効率(PE)を最適化できませんが、alwaysを提供できない固有の理由はありません。相対分布効率(RDE)は100%です。つまり、chunksize=1
と同じDEです。単純なチャンクサイズアルゴリズムは基本的な数学のみで構成され、「ケーキをスライスする」ことは自由です。
「等サイズチャンク」アルゴリズムのプールの実装とは異なり、「偶数サイズチャンク」アルゴリズムは、len_iterable
ごとに100%のRDEを提供します。 n_workers
の組み合わせ。偶数サイズのチャンクアルゴリズムは、Poolのソースに実装するのがやや複雑ですが、タスクを外部にパッケージ化するだけで既存のアルゴリズムの上に変調できます(Q/Aをドロップする場合は、ここからリンクします)どうやってするか)。
あなたが不足していることの一部は、あなたの素朴な見積もりは、各作業単位が同じ時間を要すると仮定していることだと思います。ただし、一部のジョブが他のジョブよりも早く終了すると、一部のコアがアイドル状態になり、遅いジョブが終了するのを待つ場合があります。
したがって、チャンクを4倍に分割することにより、1つのチャンクが早期に終了した場合、そのコアは次のチャンクを開始できます(他のコアは低速のチャンクで作業を続けます)。
なぜ彼らがファクター4を正確に選んだのかわかりませんが、マップコードのオーバーヘッドを最小限に抑えること(可能な限り最大のチャンクが必要です)と、異なる時間を取るチャンクをバランスさせること(可能な限り最小のチャンクが必要です)とのトレードオフになります)。