330 likes | 431 Views
並列プログラミングの Practice 課題を解いてもらうための布教活動. 田浦. 並列問題解決のステップ. decomposition ( 並列性抽出): 問題のタスクへの分割 mapping ( 負荷分散): タスクを API レベルのスレッド/プロセスにマッピング coordination: タスク間の通信,依存関係を API または instruction レベルの通信・同期に翻訳する. 最も簡単な例.
E N D
並列プログラミングのPractice課題を解いてもらうための布教活動並列プログラミングのPractice課題を解いてもらうための布教活動 田浦
並列問題解決のステップ • decomposition (並列性抽出): • 問題のタスクへの分割 • mapping (負荷分散): • タスクをAPIレベルのスレッド/プロセスにマッピング • coordination: • タスク間の通信,依存関係をAPIまたはinstructionレベルの通信・同期に翻訳する
最も簡単な例 • for (i = 0; i < n; i++) { a[i] = i * i * d;}s = 0;for (i = 0; i < n; i++) { s += a[i]; }
a[0] = … a[1] = … a[2] = … a[n – 1] =… s = 0; for (i = 0; i < n; i++) s += a[i]; Decomposition • 順序制約(依存関係): • A B タスクAの終了後にタスクBを開始 • 関係のないものはどのような順番・タイミングで実行されるかわからない(という前提で正しく動くようにdecompositionを行う)
Mapping • タスクをAPIレベルの並列性にマッピング • MPI : • 並列性の源: プロセス(mpirun –np P …) • プロセス数は固定 • Pthreads : • 並列性の源: スレッド(pthread_create) • スレッド数は可変だが,多数作りすぎると性能ロスが大きい • もちろんどの道CPUは固定である
本例題のタスク スレッドmapping • P個のスレッド(t = 0, …, P – 1) • スレッドtはi[nt / P, n(t + 1) / P)の更新を行えばよい(等分) スレッド0 スレッド0 スレッド1 スレッド2 スレッドP – 1 … スレッド0
Coordination • 通信 • 実行するタスクが必要とするデータを送る(分散メモリ) • 同期 • 順序制約(依存関係)を満たすための調停 • i.e., 順序制約が満たされたタスクを実行 • 並行アクセスの制御 • 順序制約のない複数のタスクによる,同一メモリロケーションへのアクセスを調停(共有メモリ) • 原子的な更新 • 排他制御
本例題におけるCoordination(共有メモリ) • initially: run = done = 0; • master() { /* thread 0 */ run = 1; work(0); atomic_increment_n_done(); while (n_done < P); calc_sum();} • worker(t) { /* thread t */ while (run == 0); work(t); atomic_increment_n_done(); }
本例題におけるCoordination(分散メモリ) • master() { /* processor 0 */for p = 1, …, P – 1 send(p, run); work(0); n_done++; while (n_done < P) { received(done) => n_done++; } calc_sum();} • worker(t) { /* thread t */ receive(run()); work(t); send(master, done); }
より一般的な場合 • タスク間の仕事量がばらつく • 負荷 != タスクの「数」 • タスクの数や形が,プログラムを実行しながら判明する • タスクの数? 負荷? • 対処法: • タスクを細かく分散する(大数の法則) • 動的にmappingをする(動的負荷分散)
例: Quicksort • sort(a, l, h) { … sort(a, c, h); sort(a, l, c);} l c h
Qucksortのtask sort(0, 100) sort(0, 37) sort(37, 100) sort(0, 10) sort(10, 37) sort(37, 64) sort(64, 100) … … … …
例2: fib • fib(n) { if (n < 2) return 1; else return fib(n – 1) + fib(n – 2);}
fibのtask fib(5) fib(4) fib(3) fib(3) fib(2) fib(2) fib(1) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) + + + + + +
スレッド/プロセス タスクmappingの二つのスタイル • 1スレッド/プロセス 1タスク (細粒度スレッド) • プログラムを単純に記述可能 • スレッド/プロセス CPUのmappingは不確定(OSそのほかに依存) • 1CPU 1スレッド (SPMD) • タスク スレッド/プロセスのmappingを明示的に記述(プログラムが複雑) • タスク数 >> CPU数のときは唯一の選択肢 • プロセス数固定のモデル(e.g., MPI)でも唯一の選択肢
一般的なSPMDテンプレート • タスクをあらわす構造体 • タスクの状態: 実行可・不可 • 「実行可能」なタスクを保持する構造体(Runnable Queue, Task Queue, Scheduling Queue) Task Queue
一般的なSPMDテンプレートの動作 • 各スレッド(= プロセッサ)の動作 • while (!finishsed) {t = get_task(); execute(t);} 新たに{作られた・実行可能になった}task t execute task queue(実行可能task) t t
各スレッドの動作 • execute(t) {アプリ固有の動作 including … create_task:t’ = a new task created by executing t; mark t’ runnable (e.g., enqueue(t’)); … enable_task:t’ = task enabled by t’; mark t’ runnable (e.g., enqueue(t’));}
Task QueueのDesign Choice • Shared: 全体でひとつ.共有 • Private: スレッドごとにひとつ
Shared Task Queue • 自然に空いているプロセッサにtaskが実行される • ハードウェア共有メモリ以外では非現実的 • ハードウェア共有メモリでも台数が多く,タスク粒度が小さいときはボトルネックになる
Private (per Thread) Task Queue • スレッド(プロセッサ)にひとつのtask queue • スケーラブル • 複雑化する問題: 負荷分散 • Static • Dynamic
Private Task Queue + Static Load Balancing • 全スレッドがofflineに合意できる方法でtaskがどのlocal task queueに入るかを決めておく • e.g., Quicksort : 木のノードの位置を符号化,それをハッシュする • task生成 : 適切なスレッドのqueueにenqueue • 各スレッドは自分のlocal queueからのみtaskを取り出して実行 • taskが細かい粒度でスレッドに分配される
Private Task Queue + Dynamic Load Balancing • task生成: 任意のqueue (ほとんどの場合自分のprivate queue)にtaskをenqueue • 何らかの方針で,private queueの間でtaskを移動 • PULL : 自分のprivate queueにタスクがない場合,他の(e.g., random)スレッドのqueueからtaskを奪う • PUSH : 自分のprivate queueのtaskを時折他のスレッドに移す
Taxonomy Shared Static L.B. Private Lazy (PULL) Dynamic L.B. Proactive (PUSH)
Lazy Task Creation • Private queue + dynamic L.B. + lazy (PULL) load distribution • Task queueはdeque (両端から出し入れ可能なqueue) ローカル実行 負荷分散
Lazy Task Creationの動作 • task生成 先頭にpush; 直ちに実行 • task終了 先頭からpop; 次を実行 • task enabled 末尾に挿入 • 負荷分散 末尾の要素を移動
Lazy Task Creationの効果 … … …
まともな台数効果を得るためのtips (1) • tips以前のtips (Solarisのみ) • pthread_setconcurrency(台数) • thr_setconcurrency(台数) • いずれかを用いて実際の並列度(利用したいCPU数)を指定 • 指定しないと1 (いくらスレッドを作っても無駄)
まともな台数効果を得るためのtips (2) • 世の中はボトルネックだらけ • mutex lockは極力使わない • compare & swapなど,lockを用いないatomic update • どうしてもlockが必要なら,critical sectionを極力短く(< 数十命令)した上でspin lock • 系: libc defaultのmallocを使わない • もっとスケーラブルなライブラリを使う • http://www.yl.is.s.u-tokyo.ac.jp/gc/ • 系: mallocを使ってそうなライブラリ(e.g., STL)を使わない
すでに動的負荷分散機能を持つ言語 • StackThreads/MP, MTCAMP • C/C++のライブラリ, 拡張言語 • http://www.yl.is.s.u-tokyo.ac.jp/mtcamp/ • http://www.yl.is.s.u-tokyo.ac.jp/sthreads/ • Cilk • Cの拡張言語 • http://supertech.lcs.mit.edu/cilk/ • KLIC • 並列論理型言語 • http://www.klic.org/
MTCAMP • Cの拡張 • 情報理工のSunFire15K (istsun0)にインストール済み(利用法は講義HPに近日掲載) • 特徴:多数(>> CPU数)のスレッド生成を許容する
構文 • 3つの拡張: mtc_sync, mtc_fork, --- • mtc_sync { … mtc_fork [shared(変数名,…)] <文>; … ---;}
例 • int fib(int n){ if (n < 2) return 1; else { int a, b; ST_POLLING(); mtc_sync { mtc_fork shared(a) a = fib(n - 1); b = fib(n - 2); ---; ST_POLLING(); return a + b; }}}