Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

4,580 views

Published on

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

Published in: Engineering
  • Be the first to comment

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

  1. 1. 1/22 短距離ハイブリッド並列分子動力学コードの   設計思想と説明のようなもの〜並列編〜   東大物性研   渡辺宙志 2014年8月5日
  2. 2. 2/22 概要 本資料の目的 ・並列プログラム特有の「設計の難しさ」を共有したい   ・っていうか単に「MPIの気持ち悪さ」を共有したい   設計思想 ・クラスが肥大化しすぎないようにしたい   ・不必要なクラスを作り過ぎないようにしたい   ・なるべくややこしいこと(通信の隠蔽とか)をしない   開発の歴史 まずflat-­‐MPI版を作成(Ver.  1)   その後、ハイブリッド並列版をスクラッチから作成  (Ver.  2)   Ver.  1からVer.  2で設計思想が変化  
  3. 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
  4. 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の扱い、コミュニケータの隠蔽
  5. 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が呼ばれることを保証する) ために は例外処理をするのが自然だが、手抜きにより実装していない。
  6. 6. 6/22 通信をどう設計するか?  (1/3) とりあえず単純領域分割、flat-­‐MPIのみ考える   すると、領域更新を担当するクラスを作るのが自然   →  ここではMDUnitと名付ける MDUnit MDUnit MDUnit MDUnit 実空間 分割された領域それぞれをMDUnitのインスタンスが管理   → 通信まわりをどう設計すべきか?  
  7. 7. 7/22 通信をどう設計するか?  (2/3) 案1:  MDUnit同士が行う   MDUnit MDUnit ・「隣の領域に誰がいるか」をMDUnitが自分で知っている必要がある   ・「領域更新」という局所的な役割と、「全体把握」という大局的な   役割の同居がとても気持ち悪い   →  flat-­‐MPI版では案1を採用 MDUnit MDUnit
  8. 8. 8/22 通信をどう設計するか?  (3/3) 案2:  MDUnitを管理するMDManagerクラスを作る   MDUnit MDUnit MDUnit MDUnit MDManager ・MDUnitは自分が全体のどこに位置するか知らない   ・通信は全てMDManagerを通して行う   ・局所的役割と大局的役割の分離   → ハイブリッド版では案2を採用
  9. 9. 9/22 MPIの気持ち悪さ  (1/3) ユーザ こういう動作を期待 こいつらだけが   並列動作する MDManager MDUnit MDUnit MDUnit MDUnit こいつが管理 すくなくともこういうイメージでMDManagerを作った
  10. 10. 10/22 MPIの気持ち悪さ  (2/3) 実際にはこうなってる MDManager MDUnit MDManager MDUnit MDManager MDUnit MDManager MDUnit こいつらみんな   並列動作する 並列動作するインスタンスを管理する「ただひとつの管理インス タンス」が存在しない   →このようにクラスを分ける意味はあったのだろうか?   ユーザ プロセス数に関係なく「ユーザから見てただひとつのインスタンスに   見える」オブジェクトがあれば、少なくとも設計はスッキリする? ※  ハイブリッド版では、一つのMDManager(プロセス)が複数のMDUnit(スレッド)を管理す るという意味もあるが・・・
  11. 11. 11/22 MPIの気持ち悪さ  (3/3) MDManager 通信はMDManagerを通してのみ行いたい MDUnit MDUnit MDManager しかし実際には、ソースのどこからでもどこへでもMPI通信できる →  MPIには本質的に「スコープ」が存在しない MDUnitに隣接する領域のランクを教えないことで   擬似的に「スコープ」を導入
  12. 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ノード計算のプロセス配置例
  13. 13. 13/22 どの情報を誰が管理すべきか  (2/2) 案1:  MPIInfoクラスを作って、そこで地図を管理                    通信するクラスがMPIInfoクラスのインスタンスを持つ 案2:  MDManagerクラスが地図を直接管理してしまう flat-­‐MPIコードの開発では案1を採用したが、   ハイブリッドコードの開発では案2を採用   ハイブリッドコードでは、MDManagerのコンストラクタ、デストラクタで MPI_Init/Finalizeを呼び出しており、MPI関連の情報を分離できていないこと、 及び分離することのメリットがあまりないことによる
  14. 14. 14/22 通信まわりの実装  (1/4) アルゴリズム ・相互作用距離よりも遠い粒子をペアリストに登録し、 しばらくリストを使いまわす(Bookkeeping法)   ・端にある粒子の座標のみ通信(短距離相互作用)   ・もらった粒子をさらに転送することで、斜め方向の通 信を省く(詳細は論文参照)。 考えるべきこと ・自分の粒子と他から借りている粒子をどうやって 区別するか ・送られてくる粒子情報が「どこから来た」か保存す べきか
  15. 15. 15/22 通信まわりの実装  (2/4) 自分の粒子と他から借りている粒子の区別   →  配列を共有、粒子数を2つ用意した データ配列 自分が管理する粒子 送られて来た粒子 ParocleNumber  (PN) TotalParocleNumber  (TPN) ※この名前は良くなかった 実空間
  16. 16. 16/22 通信まわりの実装  (3/4) 送られてくる粒子情報が「どこから来た」か保存すべきか   →「どこへ何を送るか」を覚えることで不要に 一番最初に送るときに「誰にどの粒子を送るか」をテーブルに保存。   また、誰から何粒子もらうかも記憶しておく(MPI_Sendrecvの引数で必要だから)。   あとは同じ順番で送れば、同じ場所に同じ粒子の座標が送られてくるはず PN TPN 1.  通信前にTPNをPNに合わせる 2.  右から粒子をもらい、その数だけTPNをずらす PN TPN 3.  以上の手続きを左、前後、上下で繰り返す。
  17. 17. 17/22 通信まわりの実装  (4/4) 自分の粒子と他から借りている粒子の区別   →二体関数以上の計算で必要 ポテンシャルエネルギーや圧力など、二体の関数について、そのまま計算する と、重複する分だけダブルカウントしてしまう。   →「自分が管理する粒子」と「借りた粒子」の寄与は半分にする。   →「借りた粒子同士の寄与」は無視する   粒子番号のチェックだけでできる(原始的?) 三体以上の相互作用がある場合はどうするんだろう?
  18. 18. 18/22 main関数の引数を誰が受け取るか(1/4) ・MPI情報管理クラス:  MPI_Initはargc,  argvを要求   ・パラメータクラス:  ファイル名の取得にargvが必要 main関数の引数を要求するクラスが、少なくとも2つある 誰がどうやって受け取るべきか?
  19. 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版コードではこちらを採用 設計的にはこれがまっとうな気もする。  
  20. 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());     }   ハイブリッド版コードではこちらを採用
  21. 21. 21/22 main関数の引数を誰が受け取るか(4/4) Q.  なぜ全てMDManagerに詰め込んだのですか?   分けたほうが設計がきれいだと思いますが? A.  分けるご利益があまりないと考えたから その他雑多な感想   ・MPIInfo、Parameterクラスのインスタンスは、どちらもMDManagerのメンバに なっており、ライフタイムを共有している。MDManagerとライフタイムを共有する クラスのインスタンスを外で作って渡す、というのがどうにも気持ち悪かった。   ・プロセスの化身であるMDManagerが、自分のランクを自分で知らない、という のが気持ち悪い気がした。「プロセスの化身」は誰か?MDManagerか? MPIInfoか?   ・main関数はなるべく簡素化したい(これは単に趣味)。
  22. 22. 22/22 まとめのようなもの 「相互作用が全て同一」という条件を最大限に利用した設計   ◯粒子番号しかチェックしなくて済むのでシンプル。   ☓粒子番号に意味を付与するのは拡張性に欠ける。 いずれ追加情報の管理が 必要になりそう。   →  通信まわりを最適化してしまうと、相互作用の詳細に強く依存し、毎回作りな おしに近くなる? 通信をもう少し抽象的に扱いたい。   通信の隠蔽を考慮していない   ◯  原則としてMPI_Sendrecvしか使わないのでシンプル。デバッグが楽。   ☓  計算が比較的重いからできたこと。強スケーリングを追求すると破綻。   MPI、というかSPMDという設計思想に慣れるのに時間がかかった。   SPMDは「通信に関わる全体的な視点」と「送受信に関わるプロセスの局所的な 視点」の両方同時に要求する。「慣れろ」と言われればそれまでだが・・・   C++の言語仕様そのものに起因する問題で、設計にわりと苦しんだ   っていうかC++はダメだと思う。GCのない言語で参照渡しの多用はいろいろ問題 がある。  

×