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,765 views

Published on

短距離古典MDコードの設計で苦労したところ、妥協したところなど。

Published in: Engineering
  • Be the first to comment

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

  1. 1. 1/18 短距離ハイブリッド並列分子動力学コードの   設計思想と説明のようなもの 東大物性研   渡辺宙志 2014年7月30日
  2. 2. 2/18 概要 本資料の目的 ・並列プログラム特有の「設計の難しさ」を共有したい ・説明とソースコードを公開することで「よりよいコード」   が生まれることを期待   設計思想 ・C++で開発のしやすさと計算速度をなるべく両立   ・外部ライブラリになるべく依存しない   ・設計と速度がぶつかったら速度を優先   「妥協の産物」なので、速度面、設計面両方で中途半端   文句があるなら自分でもっと良いコード作って公開してください
  3. 3. 3/18 コードの概観 h-p://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/18 コードの動作イメージ MDManager MDUnit MDUnit MDManager MDUnit MDUnit ユーザ Project::Run Variables Variables Variables Variables MDManager:  プロセスの化身。ユーザはこのインスタンスから操作する   MDUnit:  スレッドの化身。計算領域を管理する。   Variables:  変数の実体を管理。MDUnitのメンバになっている。   Project:  このクラスのRunメソッドが事実上のmain関数。   ProjectManager:  インプットファイルから適切なProjectクラスを呼び出す Input  File ProjectManager Modeを調べる 適切なプロジェクトを実行する
  5. 5. 5/18 インプットパラメータの扱い  (1/2) 希望 ・なるべく汎用的に使いたい ・外部ライブラリに依存しない ・インプットファイルは手で編集する    →XMLは使わない 実装 Mode=Benchmark   Density=0.5   ThermalizeLoop=150   UnitLength=50   TimeStep=0.001   TotalLoop=1000   Ini_alVelocity=0.9 インプットファイル例 ・Parameterクラス(parameter.cc/parameter.h)   ・文字列ハッシュで管理する(std::map) ・名前=値の形式でずらずら並べる。 ・Parameterクラスが全て「文字列」として保存 ・値を取り出す時に文字列を解釈
  6. 6. 6/18 インプットパラメータの扱い  (2/2) 1.  実行可能ファイルにインプットファイル名を渡す 2.  main関数でファイル名を受け取り、MDManagerクラスに渡す 3.  MDManagerクラスが、受け取ったファイル名からParameterクラ スのインスタンスを作成 4.  Parameterクラスのインスタンスを通じて値を受け取る 流れ 値の受け取り方 ハッシュキーを渡し、値を受け取る   double  Parameter::GetDouble(string  key)   ハッシュキーが定義されていなかった場合、valueを値とする  (より安全)   double  Parameter::GetDoubleDef(string  key,  double  value) 特殊キー「Mode」 インプットファイルは必ずModeを定義しなければならない   →複数のプロジェクトの管理(後述)
  7. 7. 7/18 複数のプロジェクトの扱い(1/4) 同じ計算カーネルを、異なる研究に使いたい →  液滴衝突、界面張力測定、気液共存線・・・   ありがちなパターン こういうのは避けたい・・・    (あとはswitch文みたいなのもイヤ) int   main(void){      //ProjectA();      //ProjectB();      ProjectC();   }
  8. 8. 8/18 複数のプロジェクトの扱い(2/4) ・Singletonパターンを使う ・ProjectManagerクラスをSingletonに。 ・全てのプロジェクトはProjectクラスのサブクラスに ・コンストラクタで自分をProjectManagerに登録   ・実体をグローバル変数として宣言   プロジェクトの追加→Projectクラスの実装 やりたいこと ・研究テーマごとに「プロジェクト」として管理する ・プロジェクトを追加しても、main関数を含むコードは再コンパイ ルしない ・プロジェクトは文字列で指定する 実装
  9. 9. 9/18 複数のプロジェクトの扱い(3/4) class  Benchmark  :  public  Project  {   private:   public:      Benchmark(void)  {          //ここでプロジェクトマネージャクラスに自分を追加          ProjectManager::GetInstance().AddProject("Benchmark",  this);      };      void  Run(MDManager  *mdm);   };   BenchmarkクラスはProjectクラスのサブクラス ベンチマークプロジェクトの例   benchmark.h benchmark.cc Benchmark  bench;   ←ここで実体が作られコンストラクタが呼ばれる ハッシュキー
  10. 10. 10/18 複数のプロジェクトの扱い(4/4) int   main(int  argc,  char  **argv)  {      setvbuf(stdout,  NULL,  _IOLBF,  0);      MDManager  mdm(argc,  argv);      if  (mdm.IsValid())  {          ProjectManager::GetInstance().ExecuteProject(&mdm);      }  else  {          mout  <<  "Program  is  aborted."  <<  std::endl;      }   }   main関数  (main.cc) プロジェクトマネージャがインプットファイルを読み込み、文字列ハッシュから   該当するProjectクラスのインスタンスを取得。Project::Runを実行。 void   Benchmark::Run(MDManager  *mdm)  {   //ユーザはここを記述する   } 事実上のmain関数  (benchmark.cc) Mode=Benchmark   Density=0.5   ThermalizeLoop=150   UnitLength=50   TimeStep=0.001   TotalLoop=1000   Ini_alVelocity=0.9 インプットファイル ハッシュキー
  11. 11. 11/18 変数の管理  (1/2) 分子動力学法で必要なデータは、運動量(p)と座標(q)。 Nを粒子数、Dを次元として以下のデータを保持。 double  p[N][D];   double  q[N][D]; Q.  なぜstd::vectorを使わないの?   A.  遅いから 世の中にはstd::vectorと生配列で死ぬほど最適化が変わるコンパイラがあるのです・・・   同様の理由で、カットオフ長さもコンパイル時定数に mdconfig.hにて重要な定数を宣言 const  int  D  =  3;   const  int  N  =  1000000;   const  int  PAIRLIST_SIZE  =  N  *  50;   const  double  CUTOFF_LENGTH  =  2.5;
  12. 12. 12/18 変数の管理  (2/2) 誰のメンバにするか? 案1:    MDUnitクラスのメンバとする   案2:  Variablesクラスのメンバとして、Variablesクラスのインスタンスを MDUnitのメンバとする   →案2を採用。あとで観測ルーチンを作る際、Variablesクラスのインスタ ンスを渡すのが自然だと思ったから   アクセシビリティ 原則としてVariablesクラスのメンバは隠蔽したい   しかし、どうしても直接いじる必要が出てくる   →  double  q[N][D],  double  p[N][D]をpublicメンバに。   さらにMDUnit::GetVariablesでVariablesのインスタンスにもアクセス可能   (要するにフルオープン)   Variablesクラスはほぼ構造体のような使い方に
  13. 13. 13/18 力の計算  (1/2) 力の計算はForceCalculatorクラスが担当   ただし、中身は静的メソッドのみ   (ForceCalculatorはほぼ名前空間として使用) ただ力の計算をするクラスにインスタンスを与える意義を見いだせなかったので・・・ ForceCalculatorクラスがやること   ・座標の更新  (UpdatePosi_onHalf)   ・運動量の更新  (CalculateForce) 力の計算は、内部でそれぞれの機種に最適化されたルーチンを呼び出す ForceCalculator::CalculateForceの中身 CalculateForceNext(vars,  mesh,  sinfo);     //CalculateForceBruteforce(vars,sinfo);   //CalculateForceSorted(vars,mesh,sinfo);   //CalculateForcePair(vars,mesh,sinfo);   //CalculateForceUnroll(vars,mesh,sinfo); ←Q.  こういうの嫌じゃなかったんですか?   A.  コンパイル時に力計算ルーチンが確定して いないと、Inter-­‐file  op_miza_onが効かないこ とがあるんです。   設計思想はどうしてもコンパイラの仕様とぶつかる
  14. 14. 14/18 力の計算  (2/2) 座標の更新コード void   ForceCalculator::UpdatePosi_onHalf(Variables  *vars,  Simula_onInfo  *sinfo)  {      const  double  dt2  =  sinfo-­‐>TimeStep  *  0.5;      const  int  pn  =  vars-­‐>GetPar_cleNumber();      double  (*q)[D]  =  vars-­‐>q;      double  (*p)[D]  =  vars-­‐>p;      for  (int  i  =  0;  i  <  pn;  i++)  {          q[i][X]  +=  p[i][X]  *  dt2;          q[i][Y]  +=  p[i][Y]  *  dt2;          q[i][Z]  +=  p[i][Z]  *  dt2;      }   }   というセンテンスで、以後、ローカルな二次元配列っぽく使う   Variablesはクラスというより構造体として使っている   Simula_onInfoクラスはシミュレーション情報を管理   (システムサイズや時間刻みなど)  double  (*q)[D]  =  vars-­‐>q;  
  15. 15. 15/18 標準出力の扱い 並列コードでも標準出力を使いたい   しかし、適当に使うとすぐデッドロックする よくあるパターン   if  (0==rank){      std::cout  <<  Energy()  <<  std::endl;   }   Energy()が内部でAllReduceしているとデッドロックする。 std::coutのラッパ、MPIStreamを作成   MPIStream  mout;  ←グローバル変数として宣言   mout.SetRank(rank);  ←  MDManagerでランク番号を教えてもらう   mout  <<  で受け取った内容をostringstreamに保存しておく   std::endlを受け取った時、rank0のマスタースレッドのみ出力してクリア 関数内で通信していてもデッドロックしない mout  <<  Energy()  <<  std::endl;   使い方    
  16. 16. 16/18 全体処理(1/2) ・MDUnit全てに対して行いたい処理がある(初期条件作成、観測、etc...)   ・ユーザから見えるのはMDManagerであり、MDUnitを陽に扱いたくない   ・ユーザは知っている情報をMDUnitは知らない  (系の大きさ、時間刻み等)   →  Executorクラス+ExecuteAllメソッド+Executeメソッド   MDManagerはExecuteAllメソッドを持つ   MDManager::ExecuteAllはExecutorクラスのインスタンスを   支配下のMDUnit::Executeに渡すだけ void   MDManager::ExecuteAll(Executor  *ex)  {      #pragma  omp  parallel  for  schedule(sta_c)      for  (int  i  =  0;  i  <  num_threads;  i++)  {          mdv[i]-­‐>Execute(ex);      }   }  
  17. 17. 17/18 全体処理(2/2) MDUnit::ExecuteはExecutorクラスのExecuteに自分を入れて呼び出すだけ void  Execute(Executor  *ex)  {ex-­‐>Execute(this);}; mdunit.h ユーザはExecutorクラスを継承し、その中身を書く MDManager MDUnit MDUnit Executor ExecuteAll Execute Execute ユーザが定義したExecutorのインスタンスは   MDManagerを通じてMDUnitに配布される MDUnitは系のサイズや時間刻み等を知らないが、Executorクラスの   インスタンスを作るのはProject::Runの中なので、系のサイズなどの   情報を使える(コンストラクタで渡せる) Executor::ExecuteにはMDUnitのインスタンスが渡されるので、   それを通じてVariablesのインスタンスに好き放題できる 要するにコールバック関数
  18. 18. 18/18 まとめのようなもの 性能と拡張性・保守性の両立の難しさ   ◯ プロジェクトクラスによるプロジェクト管理   ☓ 変数のアクセシビリティ、固定長配列、コメントアウトによるメソッド選択   C++を使っているのに、結局Fortranに毛が生えたようなコードに。   →これはある程度やむを得なかった。コードの抽象化は   どうしてもコンパイラの最適化まわりとぶつかる。   並列化構造をなるべく隠蔽したかった。   ◯  Executorクラス+ExecuteAllである程度実装。   ☓  ちょっと複雑なことをやろうとすると、並列化構造を意識せざるを得ない。   どの情報を誰が持つべきか?   ・変数はVariablesクラスに。プロセス番号などはMPIInfoクラスに、シミュレーション情 報はSimula_onInfoクラスに分けた。   →  このわけかたが良いかどうか自信がない。何か指針がほしい。   ◯外部ライブラリに極力依存しない   「車輪の再開発」を行うことになるが、ライブラリ依存度が強いとスパコン環境で   とてもとても苦労する。  

×