短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの
DESCRIPTION
短距離古典MDコードの設計で苦労したところ、妥協したところなど。TRANSCRIPT
![Page 1: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/1.jpg)
1/18
短距離ハイブリッド並列分子動力学コードの 設計思想と説明のようなもの
東大物性研 渡辺宙志
2014年7月30日
![Page 2: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/2.jpg)
2/18 概要
本資料の目的
・並列プログラム特有の「設計の難しさ」を共有したい ・説明とソースコードを公開することで「よりよいコード」 が生まれることを期待
設計思想
・C++で開発のしやすさと計算速度をなるべく両立 ・外部ライブラリになるべく依存しない ・設計と速度がぶつかったら速度を優先 「妥協の産物」なので、速度面、設計面両方で中途半端
文句があるなら自分でもっと良いコード作って公開してください
![Page 3: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/3.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/4.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/5.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/6.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/7.jpg)
7/18 複数のプロジェクトの扱い(1/4)
同じ計算カーネルを、異なる研究に使いたい → 液滴衝突、界面張力測定、気液共存線・・・
ありがちなパターン
こういうのは避けたい・・・ (あとはswitch文みたいなのもイヤ)
int main(void){ //ProjectA(); //ProjectB(); ProjectC(); }
![Page 8: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/8.jpg)
8/18 複数のプロジェクトの扱い(2/4)
・Singletonパターンを使う ・ProjectManagerクラスをSingletonに。 ・全てのプロジェクトはProjectクラスのサブクラスに ・コンストラクタで自分をProjectManagerに登録 ・実体をグローバル変数として宣言
プロジェクトの追加→Projectクラスの実装
やりたいこと
・研究テーマごとに「プロジェクト」として管理する ・プロジェクトを追加しても、main関数を含むコードは再コンパイルしない ・プロジェクトは文字列で指定する
実装
![Page 9: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/9.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/10.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/11.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/12.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/13.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/14.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/15.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/16.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/17.jpg)
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: 短距離ハイブリッド並列分子動力学コードの設計思想と説明のようなもの](https://reader037.vdocuments.pub/reader037/viewer/2022100319/54954e9bb47959076a8b45f6/html5/thumbnails/18.jpg)
18/18 まとめのようなもの
性能と拡張性・保守性の両立の難しさ ◯ プロジェクトクラスによるプロジェクト管理 ☓ 変数のアクセシビリティ、固定長配列、コメントアウトによるメソッド選択 C++を使っているのに、結局Fortranに毛が生えたようなコードに。 →これはある程度やむを得なかった。コードの抽象化は どうしてもコンパイラの最適化まわりとぶつかる。
並列化構造をなるべく隠蔽したかった。 ◯ Executorクラス+ExecuteAllである程度実装。 ☓ ちょっと複雑なことをやろうとすると、並列化構造を意識せざるを得ない。
どの情報を誰が持つべきか? ・変数はVariablesクラスに。プロセス番号などはMPIInfoクラスに、シミュレーション情報はSimula_onInfoクラスに分けた。 → このわけかたが良いかどうか自信がない。何か指針がほしい。
◯外部ライブラリに極力依存しない 「車輪の再開発」を行うことになるが、ライブラリ依存度が強いとスパコン環境で とてもとても苦労する。