短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの...

22
1/22 短距離ハイブリッド並列分子動力学コードの 設計思想と説明のようなもの〜並列編〜 東大物性研 渡辺宙志 201485

description

短距離古典MDコード、主に通信まわりの設計で苦労したところ、悩んだところなど。

Transcript of 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの...

Page 1: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

1/22

短距離ハイブリッド並列分子動力学コードの  設計思想と説明のようなもの〜並列編〜  

東大物性研  渡辺宙志

2014年8月5日

Page 2: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

2/22 概要

本資料の目的

・並列プログラム特有の「設計の難しさ」を共有したい  ・っていうか単に「MPIの気持ち悪さ」を共有したい  

設計思想

・クラスが肥大化しすぎないようにしたい  ・不必要なクラスを作り過ぎないようにしたい  ・なるべくややこしいこと(通信の隠蔽とか)をしない  

開発の歴史

まずflat-­‐MPI版を作成(Ver.  1)  その後、ハイブリッド並列版をスクラッチから作成  (Ver.  2)  Ver.  1からVer.  2で設計思想が変化  

Page 3: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

3/22 コードの概観

h6p://mdacp.sourceforge.net/ ファイルの置き場所 言語:C++  ライセンス:  修正BSD  ファイル数:50ファイル  (*.ccと*.hがほぼ半数ずつ)  ファイル行数:  5000  lines  (ぎりぎり読める程度?)  

計算の概要  ・短距離古典分子動力学法  (カットオフ付きLJポテンシャル)  ・相互作用、カットオフ距離は全粒子で固定  ・MPI+OpenMPによるハイブリッド並列化   プロセス/スレッドの両方で領域分割(pseudo-­‐flat-­‐MPI)  ・アルゴリズムの解説  

Prog.  Theor.  Phys.  126  203-­‐235  (2011)  arXiv:1012.2677

Comput.  Phys.  Commun.  184  2775-­‐2784  (2013)  arXiv:1210.3450

Page 4: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

4/22 MPIラッパークラス  (1/2)

とりあえずMPIのラッパークラスは作って置きたくなる  

Communicatorクラス  (communicator.cc/.h)  静的メソッドのみ含む、事実上の名前空間  

void  Communicator::SendInteger(int  &number,  int  dest_rank){      MPI_Send(&number,  1,  MPI_INT,  dest_rank,  0,  MPI_COMM_WORLD);  }

例:MPI_Sendのラッパー  

例:std::vectorをやりとりするためのラッパー  

void  Communicator::SendRecvIntegerVector(          std::vector<int>  &send_buffer,  int  send_number,  int  dest_rank,          std::vector<int>  &recv_buffer,  int  recv_number,  int  src_rank);  

ラッパークラスの役割:  型の明示、std::vectorの扱い、コミュニケータの隠蔽

Page 5: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

5/22 MPIラッパークラス  (2/2)

MPI_InitとMPI_Finalizeの隠蔽もすぐに思いつく  ・main関数とライフタイムを共有する適当なクラス(ここではMDManager)を用意する  ・そのコンストラクタでMPI_Initを、デストラクタでMPI_Finalizeを呼び出す  

int  main(int  argc,  char  **argv)  {      MDManager  mdm(argc,  argv);      if  (mdm.IsValid())  {          ProjectManager::GetInstance().ExecuteProject(&mdm);      }  else  {          mout  <<  "Program  is  aborted."  <<  std::endl;      }  }

←  ここでMPI_Initが呼ばれている  

←  関数を抜けるときにMPI_Finalizeが呼ばれる  

main.cc

※  このコードは異常終了処理を考慮していない。正しく異常終了させる(=ユーザの都合で異常終了する際にMPI_Finalizeが呼ばれることを保証する) ためには例外処理をするのが自然だが、手抜きにより実装していない。

Page 6: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

6/22 通信をどう設計するか?  (1/3)

とりあえず単純領域分割、flat-­‐MPIのみ考える  すると、領域更新を担当するクラスを作るのが自然  →  ここではMDUnitと名付ける

MDUnit MDUnit MDUnit MDUnit

実空間

分割された領域それぞれをMDUnitのインスタンスが管理  → 通信まわりをどう設計すべきか?  

Page 7: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

7/22 通信をどう設計するか?  (2/3)

案1:  MDUnit同士が行う  

MDUnit MDUnit

・「隣の領域に誰がいるか」をMDUnitが自分で知っている必要がある  ・「領域更新」という局所的な役割と、「全体把握」という大局的な  役割の同居がとても気持ち悪い  →  flat-­‐MPI版では案1を採用

MDUnit MDUnit

Page 8: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

8/22 通信をどう設計するか?  (3/3)

案2:  MDUnitを管理するMDManagerクラスを作る  

MDUnit MDUnit MDUnit MDUnit

MDManager

・MDUnitは自分が全体のどこに位置するか知らない  ・通信は全てMDManagerを通して行う  ・局所的役割と大局的役割の分離  → ハイブリッド版では案2を採用

Page 9: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

9/22 MPIの気持ち悪さ  (1/3)

ユーザ

こういう動作を期待

こいつらだけが  並列動作する

MDManager

MDUnit

MDUnit

MDUnit

MDUnit

こいつが管理

すくなくともこういうイメージでMDManagerを作った

Page 10: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

10/22 MPIの気持ち悪さ  (2/3)

実際にはこうなってる

MDManager MDUnit

MDManager MDUnit

MDManager MDUnit

MDManager MDUnit

こいつらみんな  並列動作する

並列動作するインスタンスを管理する「ただひとつの管理インスタンス」が存在しない  →このようにクラスを分ける意味はあったのだろうか?  

ユーザ

プロセス数に関係なく「ユーザから見てただひとつのインスタンスに  見える」オブジェクトがあれば、少なくとも設計はスッキリする?

※  ハイブリッド版では、一つのMDManager(プロセス)が複数のMDUnit(スレッド)を管理するという意味もあるが・・・

Page 11: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

11/22 MPIの気持ち悪さ  (3/3)

MDManager

通信はMDManagerを通してのみ行いたい

MDUnit MDUnit

MDManager

しかし実際には、ソースのどこからでもどこへでもMPI通信できる

→  MPIには本質的に「スコープ」が存在しない

MDUnitに隣接する領域のランクを教えないことで  擬似的に「スコープ」を導入

Page 12: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

12/22 どの情報を誰が管理すべきか  (1/2)

MPIでは、ノードをまたぐ通信量をなるべく減らすように  プロセスを配置する  

0 1 4 5

2 3 6 7

8 9 11 12

10 11 13 14

ハイブリッドだとさらにややこしくなる。  →  どの領域に誰がいるかの「地図」の管理が必要

1ノード4プロセス、4ノード計算のプロセス配置例

Page 13: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

13/22 どの情報を誰が管理すべきか  (2/2)

案1:  MPIInfoクラスを作って、そこで地図を管理                    通信するクラスがMPIInfoクラスのインスタンスを持つ

案2:  MDManagerクラスが地図を直接管理してしまう

flat-­‐MPIコードの開発では案1を採用したが、  ハイブリッドコードの開発では案2を採用  

ハイブリッドコードでは、MDManagerのコンストラクタ、デストラクタでMPI_Init/Finalizeを呼び出しており、MPI関連の情報を分離できていないこと、及び分離することのメリットがあまりないことによる

Page 14: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

14/22 通信まわりの実装  (1/4)

アルゴリズム

・相互作用距離よりも遠い粒子をペアリストに登録し、しばらくリストを使いまわす(Bookkeeping法)  ・端にある粒子の座標のみ通信(短距離相互作用)  ・もらった粒子をさらに転送することで、斜め方向の通信を省く(詳細は論文参照)。

考えるべきこと

・自分の粒子と他から借りている粒子をどうやって区別するか ・送られてくる粒子情報が「どこから来た」か保存すべきか

Page 15: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

15/22 通信まわりの実装  (2/4)

自分の粒子と他から借りている粒子の区別  →  配列を共有、粒子数を2つ用意した

データ配列

自分が管理する粒子 送られて来た粒子

ParocleNumber  (PN)

TotalParocleNumber  (TPN)

※この名前は良くなかった 実空間

Page 16: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

16/22 通信まわりの実装  (3/4)

送られてくる粒子情報が「どこから来た」か保存すべきか  →「どこへ何を送るか」を覚えることで不要に

一番最初に送るときに「誰にどの粒子を送るか」をテーブルに保存。  また、誰から何粒子もらうかも記憶しておく(MPI_Sendrecvの引数で必要だから)。  あとは同じ順番で送れば、同じ場所に同じ粒子の座標が送られてくるはず

PN

TPN

1.  通信前にTPNをPNに合わせる

2.  右から粒子をもらい、その数だけTPNをずらす PN

TPN 3.  以上の手続きを左、前後、上下で繰り返す。

Page 17: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

17/22 通信まわりの実装  (4/4)

自分の粒子と他から借りている粒子の区別  →二体関数以上の計算で必要

ポテンシャルエネルギーや圧力など、二体の関数について、そのまま計算すると、重複する分だけダブルカウントしてしまう。  

→「自分が管理する粒子」と「借りた粒子」の寄与は半分にする。  →「借りた粒子同士の寄与」は無視する  

粒子番号のチェックだけでできる(原始的?)

三体以上の相互作用がある場合はどうするんだろう?

Page 18: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

18/22 main関数の引数を誰が受け取るか(1/4)

・MPI情報管理クラス:  MPI_Initはargc,  argvを要求  ・パラメータクラス:  ファイル名の取得にargvが必要

main関数の引数を要求するクラスが、少なくとも2つある

誰がどうやって受け取るべきか?

Page 19: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

19/22 main関数の引数を誰が受け取るか(2/4)

案1:  argc,  argvをMPI管理クラス(MPIInfo)とパラメータ管理クラス(Parameterクラス)それぞれに渡し、それらのポインタを管理クラスに渡す。

int  main(int  argc,  char  *argv[]){      MPIInfo  minfo(argc,  argv);      Parameter  param(argc,  argv);      MDUnit  mdu(&minfo,  &param);      //なにか処理  }

flat-­‐MPI版コードではこちらを採用 設計的にはこれがまっとうな気もする。  

Page 20: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

20/22 main関数の引数を誰が受け取るか(3/4)

案2:  MDManagerにのargc,  argvを渡し、コンストラクタでMPI_Initの処理やParameterのインスタンスを作る

int  main(int  argc,  char  *argv[]){      MDManager  mdm(argc,  argv);  }  

MDManager::MDManager(int  &argc,  char  **  &argv)  {      MPI_Init(&argc,  &argv);      MPI_Comm_size(MPI_COMM_WORLD,  &num_procs);      MPI_Comm_rank(MPI_COMM_WORLD,  &rank);      std::string  inpurile;      if  (argc  >  1)  {          inpurile  =  argv[1];      }  else  {          mout  <<  "#  Input  file  is  not  specified.  input.cfg  is  used."  <<  std::endl;          inpurile  =  "input.cfg";      }      param.LoadFromFile(inpurile.c_str());    }  

ハイブリッド版コードではこちらを採用

Page 21: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

21/22 main関数の引数を誰が受け取るか(4/4)

Q.  なぜ全てMDManagerに詰め込んだのですか?  分けたほうが設計がきれいだと思いますが?

A.  分けるご利益があまりないと考えたから

その他雑多な感想  ・MPIInfo、Parameterクラスのインスタンスは、どちらもMDManagerのメンバになっており、ライフタイムを共有している。MDManagerとライフタイムを共有するクラスのインスタンスを外で作って渡す、というのがどうにも気持ち悪かった。  ・プロセスの化身であるMDManagerが、自分のランクを自分で知らない、というのが気持ち悪い気がした。「プロセスの化身」は誰か?MDManagerか?MPIInfoか?  ・main関数はなるべく簡素化したい(これは単に趣味)。

Page 22: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの 〜並列編〜

22/22 まとめのようなもの

「相互作用が全て同一」という条件を最大限に利用した設計  ◯粒子番号しかチェックしなくて済むのでシンプル。  ☓粒子番号に意味を付与するのは拡張性に欠ける。 いずれ追加情報の管理が必要になりそう。  →  通信まわりを最適化してしまうと、相互作用の詳細に強く依存し、毎回作りなおしに近くなる? 通信をもう少し抽象的に扱いたい。  

通信の隠蔽を考慮していない  ◯  原則としてMPI_Sendrecvしか使わないのでシンプル。デバッグが楽。  ☓  計算が比較的重いからできたこと。強スケーリングを追求すると破綻。  

MPI、というかSPMDという設計思想に慣れるのに時間がかかった。  SPMDは「通信に関わる全体的な視点」と「送受信に関わるプロセスの局所的な視点」の両方同時に要求する。「慣れろ」と言われればそれまでだが・・・  

C++の言語仕様そのものに起因する問題で、設計にわりと苦しんだ  っていうかC++はダメだと思う。GCのない言語で参照渡しの多用はいろいろ問題がある。