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

18

Click here to load reader

Upload: kaityo256

Post on 20-Dec-2014

1.371 views

Category:

Engineering


3 download

DESCRIPTION

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

TRANSCRIPT

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

1/18

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

東大物性研  渡辺宙志

2014年7月30日

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

2/18 概要

本資料の目的

・並列プログラム特有の「設計の難しさ」を共有したい ・説明とソースコードを公開することで「よりよいコード」  が生まれることを期待  

設計思想

・C++で開発のしやすさと計算速度をなるべく両立  ・外部ライブラリになるべく依存しない  ・設計と速度がぶつかったら速度を優先  「妥協の産物」なので、速度面、設計面両方で中途半端  

文句があるなら自分でもっと良いコード作って公開してください

Page 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

Page 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を調べる

適切なプロジェクトを実行する

Page 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クラスが全て「文字列」として保存 ・値を取り出す時に文字列を解釈

Page 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を定義しなければならない  →複数のプロジェクトの管理(後述)

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

7/18 複数のプロジェクトの扱い(1/4)

同じ計算カーネルを、異なる研究に使いたい →  液滴衝突、界面張力測定、気液共存線・・・  

ありがちなパターン

こういうのは避けたい・・・    (あとはswitch文みたいなのもイヤ)

int  main(void){      //ProjectA();      //ProjectB();      ProjectC();  }

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

8/18 複数のプロジェクトの扱い(2/4)

・Singletonパターンを使う ・ProjectManagerクラスをSingletonに。 ・全てのプロジェクトはProjectクラスのサブクラスに ・コンストラクタで自分をProjectManagerに登録  ・実体をグローバル変数として宣言  

プロジェクトの追加→Projectクラスの実装

やりたいこと

・研究テーマごとに「プロジェクト」として管理する ・プロジェクトを追加しても、main関数を含むコードは再コンパイルしない ・プロジェクトは文字列で指定する

実装

Page 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;   ←ここで実体が作られコンストラクタが呼ばれる

ハッシュキー

Page 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

インプットファイル

ハッシュキー

Page 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;

Page 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クラスはほぼ構造体のような使い方に

Page 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が効かないことがあるんです。  

設計思想はどうしてもコンパイラの仕様とぶつかる

Page 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;  

Page 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;  

使い方    

Page 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);      }  }  

Page 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のインスタンスに好き放題できる

要するにコールバック関数

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

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

性能と拡張性・保守性の両立の難しさ  ◯ プロジェクトクラスによるプロジェクト管理  ☓ 変数のアクセシビリティ、固定長配列、コメントアウトによるメソッド選択  C++を使っているのに、結局Fortranに毛が生えたようなコードに。  →これはある程度やむを得なかった。コードの抽象化は  どうしてもコンパイラの最適化まわりとぶつかる。  

並列化構造をなるべく隠蔽したかった。  ◯  Executorクラス+ExecuteAllである程度実装。  ☓  ちょっと複雑なことをやろうとすると、並列化構造を意識せざるを得ない。  

どの情報を誰が持つべきか?  ・変数はVariablesクラスに。プロセス番号などはMPIInfoクラスに、シミュレーション情報はSimula_onInfoクラスに分けた。  →  このわけかたが良いかどうか自信がない。何か指針がほしい。  

◯外部ライブラリに極力依存しない  「車輪の再開発」を行うことになるが、ライブラリ依存度が強いとスパコン環境で  とてもとても苦労する。