並行性:並列プロセスを管理する
プロセスとプロセッサ
プロセスとは評価されるWolfram言語式のことである.プロセッサとは,このような評価を行う並列カーネルのことである.
「並列評価」で取り上げたコマンドParallelEvaluateは,明示的に与えられたプロセッサに評価を送るため,使用者自身で利用可能なプロセッサとプロセスを把握しておくことが要求される.ここで取り上げるスケジュール関数は,このような機能を行う関数である.利用できるプロセッサよりずっと多くの任意の数のプロセスを作ることが可能である.プロセッサの数より多いプロセスが作成されたら,残りのプロセスはキューに入り,プロセッサが利用できるようになったときに開始される.
プロセスの開始と待機
任意の利用可能なプロセッサでの評価のために式を並べるためのParallelSubmit[expr]と,与えられたプロセスが終了するのを待つWaitAll[pid]が基本の2つのコマンドである.
キューの中の各プロセスは固有の評価IDであるEIDで識別される.
ParallelSubmit[cmd] | cmd を送信してリモートカーネルで評価し,キューのジョブのEIDを返す |
ParallelSubmit[{vars …},cmd] | cmd をリモートカーネルに送る前に,与えられた変数の局所値のためのクロージャを構築する |
WaitAll[eid] | 与えられたプロセスが終了するのを待ち,その結果を返す |
WaitAll[{eid1,eid2,…}] | 与えられた全プロセスを待ち,結果のリストを返す |
WaitAll[expr] | expr に含まれているすべての評価IDの終了を待ち,それぞれのプロセスの結果と置き換える |
WaitNext[{eid1,eid2,…}] | 与えられたプロセスの一つが終了するのを待ち,{res,id,ids}を返す(id は終了したプロセスのEID,res はその結果,ids は残りのEIDのリスト) |
WaitNextは非決定的である.これは終了した任意のプロセスを返す.終了したプロセスがない場合は,結果が得られるまで待つ.WaitNextの結果の3つ目の要素は,別のWaitNextを呼び出す際の引数に適している.
関数ParallelSubmitおよびWaitAllにより「並行性」が実装されるので,いくつでもプロセスを開始することができる.それらは最終的には利用可能ないずれかのリモートプロセッサで評価される.特定の結果が必要なら,特定のEIDを待つことも,WaitNextを繰返し呼び出してすべての結果を待つこともできる.
基本的な使用法
以下の例題を試すために,並列カーネルをいくつか開始しておく.
リモートカーネルで処理するように1+1という評価をキューに入れる.キューに入れる前に式が評価されるのを避けるために,QueueにはHoldAllという属性がある.Queueが返す値は,キューに入れられたプロセスの評価ID(EID)である.
プロセスをキューに入れた後,他の計算を行って最終的には結果を得ることができる.結果がまだ出ていない場合,WaitAllはその結果を待つ.
複数のプロセスをキューに入れることができる.次の例では式12,22,…,52が評価のためにキューに入れられる.
残りの評価IDのリストへのpidsの再割当てにご注目いただきたい.pidsリストが空になるまで前の評価を繰り返すと,キューが空になる.
変数を使用する際の注意点
ParallelSubmit[e]の中の式 e が値が割り当てられている変数を使うとき,リモートカーネルでも同じ変数値が定義されているようにする必要がある.DistributeDefinitionsまたは共有変数を使わない限り,ローカルで定義された変数はリモートカーネルでは利用できない.詳細は「並列評価」のチュートリアルの「変数の値」を詳細のこと.
リモートカーネルで式Head[a]を評価したいとする.これはローカル上にあるもので,リモートカーネルではシンボルaは値を持っていないため,結果はIntegerではない.
ローカルの定数を使うことも,さらにはそれをaとしてParallelSubmitの引数にaの値をテキスト的に挿入することもできる.
これはよくある例であるが,より簡単にするために,ParallelSubmitのオプションである第1引数を使って変数を宣言することもできる.その場合,変数は第2引数の式に挿入される.
シンタックスParallelSubmit[{vars …},expr]はFunction[{vars …},expr]に似るように設計されている.どちらも変数の値を閉じ込む「囲い」を形成する.
反復子変数も同様である.以下の2つの出力において,並列評価によって得られる結果は正しくない.
反復子変数を定数として挿入するか,囲いを宣言するかして,以下のコマンドのように正しい結果が得られるようにする.
ParallelTable[]は反復子変数を正しく扱う.
低レベル関数
Parallel`Developer`コンテキストには,プロセスのキューを制御する関数が含まれている.
$Queue | ParallelSubmitで送信されたが利用可能なカーネルにまだ割り当てられていない評価のリスト |
$QueueLength | 入力キュー$Queueの長さを与える |
ResetQueues[] | 実行中の全プロセスを待ち,キューのプロセスを破棄する |
QueueRun[] | 終了した評価を全カーネルから集め,キューから新しい評価を割り当てる |
QueueRun[] は少なくとも1つの評価がカーネルに送信された場合,またはカーネルから結果を1つ受け取った場合は True を,それ以外は False を返す.通常 QueueRun を自分で実行する必要はない. QueueRun は WaitAll その他の関数内の適切な場所で呼ばれる. QueueRun は並行プログラムのために独自の「メインループ」を実装した場合にのみ必要となる.
評価IDを使う
WaitAll[{pid1,pid2,…}]は並列計算の一般メカニズムの簡単な形式である.WaitAllは引数に評価IDを含むどのような式でも取ることができ,関連付けられたプロセスがすべて終了するのを待つ.その後,PIDはそれぞれのプロセスの結果と置き換えられる.
WaitAllはParallelSubmitの逆であるとみなすことができる.つまり,WaitAll[ParallelSubmit[expr]]はリモートカーネルで評価されて expr を返す.これは丁度 expr 自体をローカルで評価したときと同じである.さらに,WaitAll[… ParallelSubmit[e1]… ParallelSubmit[en]…]は…e1…en…(各 ei は並列に評価される)に等しい.ここで,省略記号は周りの任意の計算を表す.
ParallelSubmitが使われる際に生成されたPIDはそのままの状態で維持されなければならず,WaitAllがタスクを行う前に破壊されたり複製されたりしてはならない.なぜなら,各PIDは実行中の並列計算を表しており,それぞれの結果が回収されるのは厳密に一度だけでなければならないからである.
- PIDはPlusの影響を受けない記号オブジェクトである.順番が入れ替わるかもしれないが,それは問題ない.ほとんどの算術操作は安全である.
- ParallelSubmitをリストに使用するマッピング関数は,結果にPIDのリストが含まれるので,安全である.
- TableはPIDのリストを返すため,安全である.
PIDが破壊または複製された計算から回復するためには,コマンドAbortKernels[]AbortKernels[]
ProcessID[pid] | プロセスを識別する固有の整数 |
Process[pid] | プロセスを表す式 |
Scheduling[pid] | プロセスに割り当てられた優先度 |
ProcessState[pid] | プロセスの状態(キューで待機中,実行中,終了) |
プロセスIDの特性(Developerコンテキスト内における)
これで,プロセスのいくつかが利用可能なプロセッサで実行中となった.すでに終了しているものもあるかもしれない.この情報の中には評価のデフォルトの出力形式で利用できるものもある.
WaitAll[]はすべてのプロセスが終了して結果が返されるまでスケジューラを呼び出す.デフォルトのキュータイプでは優先度は使用されない.
例
無限計算
多項式 () がすべて既約であること(不確定の推測)を証明したい場合,IrreduciblePolynomialQを使って検証できる.
この計算は永久に続く.中止するには,.を押すか,「評価」 ▶ 「評価を放棄」を選ぶかしてローカルの評価を放棄する.放棄したら,以下のようにして待機中の結果を収集する.
自動プロセス生成
種々の計算を並列化する一般的な方法は,関数操作で発生する関数 g をComposition[ParallelSubmit,g]で置き換えるというものである.この新しい操作により,すべての関数 g の呼出しのインスタンスが並列評価のキューに入る.この合成の結果はプロセスID(PID)で,g が発生した外側の計算で構成される構造の中に現れる.g の計算の結果を戻すには,式全体をWaitAllで囲む.これで式の中のすべてのPIDが,対応するプロセスが返した結果に置き換えられる.
並列マッピング
Mapの並列バージョンは簡単に作れる.連続的なMapは関数 fでリストの全要素を囲む.
すべてのマッピングを並列に実行するためには,fの代りにComposition[ParallelSubmit,f]を使う.結果としてPIDのリストが得られる.
最後にプロセスが終了するのを待つ.すべてのPIDは関連付けられたプロセスの結果に置き換えられる.
並列内積
並列化が記号内積にどのように作用するのかを見るために, が共通のaの最後次元,およびbの最初の次元であるような一般化された内積を考える.pはPlus,tはTimesであると考える.
前の式のpの代りにComposition[ParallelSubmit,p]を使って,pのすべての計算を並行実行するためにキューに入れることができる.結果としてプロセスIDのテンソルが得られる.
並列の表と総和
次のコードは55の乱数行列を生成する.各行は並列に計算される.
要素が並列に計算された総和である.総和の各要素は数値積分である.
並列化との比較
並列マッピング,表,内積は「並列評価」ですでに取り上げた.これらの関数はメソッドオプションの制御のもと,タスクをそれぞれいくつかの部分問題に分割する.このセクションの関数は各部分問題につき1つの評価を生成する.この分割はMethod->"FinestGrained"という設定に等しい.
すべての部分問題が同じだけの時間を必要とするなら,ParallelMap[],ParallelTable[]等の関数は速くなる.しかし,部分問題の計算時間が異なり,事前に推定することが容易でない場合は,このセクションで述べたようにWaitAll[… ParallelSubmit[]…]を使うか,それと同等のメソッドオプション設定を使った方がよいこともある.生成されたプロセスの数がリモートカーネル数より多い場合は,このメソッドは自動負荷調整を行う.カーネルには,前のジョブの終了後すぐに次のジョブが割り当てられるため,すべてのカーネルが常にビジーであるからである.
トレース
プロセスがどのようにスケジュールされるのかを見るためには,トレースが使用できる.この機能を使うためには,ツールキットをロードしていくつかのカーネルを起動する前に,デバッグパッケージをロードする必要がある.
いくつかのプロセスがキューに入っており,キューがどのように大きくなるのかが分かる.
WaitAll[]はスケジューラを呼び出す.スケジューラはキューに入ったジョブをアイドルプロセッサに送り,結果を回収してそれをアプリケーションに戻す.