Previous ToC Next

22. MPIの全てを5分で理解する(嘘)

学生C:今回これなんですか?

赤木 :何かそういうものがあったほうがいいという神の声が聞こえたとか。

学生C:はあ。

赤木 :まあ全てっていうのはもちろん嘘で、考え方と、FDPSとか使うのに必要な最低限くらいね。

学生C:まあそれなら、、、

赤木 :君MPIでプログラム書いたことある?

学生C:ないです。

赤木 :じゃあまあちょうどいい機会だから、付き合って。

学生C:はい、、、

22.1. MPI の考え方

赤木 :まず考え方ね。MPI って、基本的な考え方として、ネットワークでつ ながった沢山の計算機でそれぞれ勝手にプログラムが走って、それらがお互い に通信できる、というだけなの。例えば、メイルを送るプログラムと受け取る プログラムは通信してるし、ブラウザとウェブサーバーも通信しているわけね、 MPI でも、基本的にはそれと同じで、いくつかの計算機で走ってるプログラム 同士が通信するだけ。

学生C: でもなんか色々な、MPIの関数呼んで初期設定したり、通信もMPIの関 数使うんじゃないですか?

赤木 :それはそう。普通のネットワークでの通信だと、相手はIPアドレスと か名前で指定するじゃない?でも並列プログラムって、スパコンで実行したり、 研究室のサーバーで実行したり、あるいは手元のノートで実行したりするわけ で、その時にIPアドレスとか名前とかをどう変更するかを自分で管理するのは 面倒なわけで、MPIの初期化関数はその辺の面倒をみてくれるわけ。

初期化というか、最初のプログラムの実行開始の時ね。普通だと

 mpirun -np 16 your_program [options of your program]
みたいな感じで、スパコンセンターだと色々センター固有のコマンドとかあるけど、ここでは -np 16 で 16個の your_program を走らせる、と指定してるわけ。ここでは16個が全部同じ your_program ね。色々すると例えば 8個はprogram1 で残りは program2 なんてこともできるけど、普通しないと思う。

で、この16個がどの計算機でどう走るかは、MPI のインストールがちゃんとできていればどっかにある設定ファイルを使ってなんかするのね。基本的は特に指定しなければ 1台の計算機の上で複数プロセスが走るはず。スパコンセンターとかだと「ジョブスクリプト」というのを書いて、それで使う計算機の台数とか、1台の中でプロセス何個動かすかとか指定するの。

で、そうすると、MPI のプログラムの中では、プロセスに番組がふられて、これランクっていうのがMPIの中でのきまりだけど、自分のランクが何番かわかる(そういう関数がある)し、全部で何プロセスあるかとかもわかる、あと、通信も相手をランクで指定できるわけ。

なので、MPI 使って書いておけば、研究室のサーバーでも富岳とかのスパコンでも同じように使えるわけ。

学生C:そういわれるとなんか素晴らしいものみたいに聞こえますが、そのわりには赤木さんいつも「MPIはクソ」「MPIは滅びるべきである」とかいってませんか?

赤木 :そうね、それは、MPIのやってくれることが本質的にわりと今いったこと、つまり、名前のかわりに番号で通信できるのと、あと自分の番号と全プロセスの数がわかる、とこれだけで、あとは全部自分でしないといけないからなの。

学生C:え、でも、並列プログラムって、何台かの計算機とか、複数のプロセスとかで1つの計算するわけだから、どうしてもそうなるんじゃないですか?それぞれのプロセスが自分がk計算するところを計算して、必要なデータを他からもらってくる必要があれば通信するわけですよね?

赤木 :20年くらい前までは並列プログラムってそうじゃなかったのよ。例えば、High Performance Fortran っていう言語があって、これは Fortran 90の配列操作をまあ自動的に並列実行してくれる、というものだったわけ。大きな配列を宣言して、それをどう分割するかくらいを指定すれば、あとは自分の分担から外れた隣にある配列要素のアクセスとかしてもコンパイラが面倒みてくれたの。

学生C:それは確かに素晴らしいように聞こえますが、、、でもそんなのが動く計算機がもうないからみんなMPI使ってるんですよね?あればそっち使いますよね?

赤木 :まああるっちゃあるんだけど、、、、NEC の SX とか、、、ただ、配列操作を並列化、というのは最近のキャッシュがある計算機では絶対性能でないから、そういう計算機で性能だそうと思うとなんでもできるMPI使うわけ。並列計算って大抵は速くしたいからするわけだから、性能でないと話にならないのね。

学生C:なんか話が愚痴になってません?本題はMPIだったような気も。

赤木 :そうね、じゃあまず初期設定から

22.2. 初期設定

赤木 :言語は C ね。C++ でも他の言語でも普通 C のインターフェース使う から。

   #include "mpi.h"
はしたとするとと。
     int myid, numprocs;
     MPI_Init(&argc,&argv);                  //初期化
     MPI_Comm_size(MPI_COMM_WORLD,&numprocs);//プロセス数を numprocs に入れる
     MPI_Comm_rank(MPI_COMM_WORLD,&myid);    //自分の番号を myid に入れる
みたいな感じ。このあと色々やって、あとプログラムの終了の時には

     MPI_Finalize
を呼ぶんだったような気がするわ。

学生C: MPI_COMM_WORLD ってなんですか?

赤木 :これ、「コミュニケータ」というもので、通信する範囲を指定するのに使えるの。MPI_COMM_WORLD はデフォルトで、起動したMPIプロセス全部。

学生C:通信相手番号で指定するなら、それだけでよくないですか?何に使うんですかこれ?

赤木 :あ、相手が1つならいいけど、MPIでは 放送とか、逆に総和とかできるのね。あるプロセスのデータを他のプロセスに放送とか、逆に全プロセスのある変数の値の合計をあるプロセスにとか、あるいは全プロセスにとか。で、これ、全プロセスじゃなくて、グループに分けられると便利なこともある、というか、分ける必要があるプログラムがあるので、そのための機能ね。まああんまり使わないんだけど、、、

学生C:わけるって、どんなふうにですか?

赤木 :例えば LINPACK っていう連立1次方程式を解くプログラムだと、プロセスを2次元格子に並べて行列を分割してもたせるわけね。そうすると、縦方向の放送とか、横方向の放送とかがあると便利なの。

学生C:なるほど。粒子法とかだと MPI_COMM_WORLD そのままですか?

赤木 :大抵はそうだと思うわ。あと、1対1通信と集合通信の話をすればいいんじゃないかな。というわけでまずは1対1通信ね。

22.3. 1対1通信

学生C:えーと、今までの話だと、単に送るほうは送り先指定して送って、受け取るほうはどこからきたかを指定して受け取るだけですよね?

赤木 :と思うわけだけど、実際はそうでもないのね。送る関数と受け取る関数はそれぞれこんな感じ:

  int MPI_Send(const void *buf,
               int count, MPI_Datatype datatype,
               int dest,
               int tag,
               MPI_Comm comm);

  int MPI_Recv(void *buf,
               int count,
               MPI_Datatype datatype,
               int source,
               int tag,
               MPI_Comm comm,
               MPI_Status * status)       
               
学生C:buf, count, datatype は、、、これなぜ単純にバイト数渡すんじゃなくて、さらには C のいろんな関数みたいに sizeof を渡すんでもなくて型そのものを渡すんですか?

赤木 :まあ send/receive なら絶対これいらないわね。話だけでてきたけど総和とかしたいと型がいるから、そういう関数に合わせたんじゃないかしら?これ設計者のセンスが悪いと私は思うわ。

学生C: dest, source はそれぞれ送り先と送信元のランクですね? tag ってな んですか?

赤木 :これが一致してないと受け取らないの。

学生C:何故そんな面倒なことを?

赤木 :これは、ネットワークとかネットワークコントローラの性質を考えたからだと思うわ。ネットワークのメッセージって、送った準備に届く、という保証がないの。処理のなんかの都合で順番変わったりするわけ。そうすると、単純に順番でみてるととデータが入れ替わっても分からないから、それが起きないようにアプリケーションの側でちゃんと番号かなんかつけてね、としてるの。

学生C:それ、MPI のライブラリ側でもできますよね?送るほうは順番数えて送る時にそれつけて送って、受け取るほうもちゃんと今の呼び出しが何回目か憶えてればいいだけじゃないですか?

赤木 :ヨイトコロニキガツキマシタネ。私もそう思うわ。プロセスがマルチスレッドでプロセス a のスレッド i からプロセス b のスレッド j に送る、みたい場合だとそれでは上手くいかなく、、、ないか。これも別にスレッドが全部プロセスみたいなものと思って数えればいいわね。

この辺が MPI のアレなところなわけ。なんかユーザー側でしなくてもいいことをしないといけなくてそれが間違いの元になりえる、という。あと、受け取る側で受け取る数を指定しないといけないのもわりとウザいところね。Cのいろんな入力関数みたいに、ちゃんと十分なバッファサイズを用意して、あふれたらあふれたと返す、とかなぜできないの?みたいな。

文句ばっかりいっててもしょうがないわね。1対1通信であと知っておいて欲しいのは、 Send/Recv の他に Isend/Irecv というのがあって、こっちは非同期に動く、つまり、この関数呼んでもすぐに戻ってきて別の処理にうつることができるんだけど、いつ通信が終わるかはわからなくて終わるのを待つ関数が別にあるの。逆にいうと Send/Recv は関数から戻ってきたら処理が終わってるのね。でも、 Send はユーザープログラムが送り終わった、というだけでネットワークのどこまでメッセージがいってるかはわからないの。

これ、2つのプロセスがメッセージを交換するのも実は簡単ではない、ということなのね。例えば両方が MPI_Send したら、相手が受け取らないのでSend が終わらなくてプログラムがハングアップするかもしれないわけ。で、なのでその時には MPI_Sendrecv を使うか、 Isend/Irecv 使うかどっちかにしないといけないの。

学生C: Isend と Recv の組合せではいけないんですか?

赤木 : あ、そうね。理屈ではいいような気がするわ。ちゃんと仕様調べて実験もしてみて?

学生C: えー、なんか大変そう、、、

赤木 : まあ気がむいたらでいいわ。あと集団通信ね。

22.4. 集団通信

赤木 :一杯あるんだけど基本的なものだけ。まず MPI_Barrier.

  int MPI_Barrier( MPI_Comm comm );
これは、全プロセスがこれ呼ぶまで先に呼んだものは待つ、というだけ。こういうのバリア同期というのね。これだけが必要なことはあんまりないはずだけど、デバッグの時にはすごく使うかも。これもコミュニケータ引数なので、もしも COMM_WORLDでないのを使うとその範囲だけが止まるわけ。

学生C:これはタグとかないんですか?

赤木 :これはいらない、といってもさっきの話だと他のもいらないんだけど、これはとにかくいらないということみたい。2つのバリアの順番が入れ替るとかはないから。

学生C:なるほど。

赤木 :で、次、

  int MPI_Bcast(void *buffer,
                int count, MPI_Datatype datatype,
                int root,
                MPI_Comm comm);
これは放送ね。ランク root のプロセスが同じデータを他の全プロセスに送る。

  int MPI_Reduce(const void *sendbuf,
                 void *recvbuf,
                 int count,
                 MPI_Datatype datatype,
                 MPI_Op op,
                 int root,
                 MPI_Comm comm);

こっちは総和とか。これ、型と操作と両方指定するのなんか変な気がするけどまあ仕様はそうなってると。あと Allreduce というのもあってこっちは結果を全プロセスが受け取るの。

あとまだすごく一杯色々な関数あるけどそういうのが必要になったらあるかどうかは調べればいいと思うわ。それぞれが複数のところに一度に送るとかそれをまた合計するとかそういうの。

学生C:FDPS 使うプログラムの中ではこの辺の MPI の関数直接呼ぶのはどうするんですか?

赤木 : COMM_WORLD 使ってれば、というか普通使ってるしそうでない時はPS::CommInfo::setCommunicator とか使って設定しているはずだからそのコミュニケータ使って直接よべばいいけど、プロセス数とか自分の番号とか総和とか放送とかは良く使うので関数準備してあるの。例えば総和は

  template <class T>
  T PS::Comm::getSum(const T val);
ね。C++ のテンプレート使ってるから、MPIが知っている型ならMPI_Allreduce みたいに6個も引数並べなくてもよしなにやってくれるみたい。もちろん中では MPI_Allreduce 呼んでるだけ。

学生C:配列の各要素の和とかはどうすればいいんですか?

赤木 :そこですごく遅くなるとかでなければ要素毎に getSum 呼んでもいいし、性能が気になるなら直接 MPI_Allreduce 呼んでもいいし、なんなら配列版の getSum 作ってプルリクだしてくれれば、、、

学生C: あー、そういうことですね。わかりました、、、

22.5. 課題

  1. 適当な1変数関数の台形公式による数値積分を区間分割して並列に行なう MPI プログラムを作って、ちゃんと動いていること、台形公式の刻みが十分小さいならちゃんと並列化で高速になることを確認してみよ。

  2. 本文にあった、2プロセスがメッセージ交換するプログラムをいくつか作ってみて、メッセージサイズが非常に大きい時の動作を確認せよ。

22.6. まとめ

  1. MPI の概要を学んだ

  2. MPI では、それぞれのプロセスは普通のプログラムで、それがMPIの関数を使って通信する。

  3. 通信の基本的関数は Send/Recv (Isend/Irecv) である。

  4. バリア同期、総和、放送のための関数もある。

  5. FDPS から直接これらの関数を呼べる。また、同期、総和、放送は便利な関数が用意されている。

22.7. 参考資料

特になし。
Previous ToC Next