やかんです。

今回は、何はともあれMPIのAPIについてMPIのAPIについて理解していきたいというそういうメモになってます。

東大生やかんのブログ
やかん

※内容は僕のパブリックめも。

MPIのAPIについて

主要な、というか、Cannon、Fox、SUMMAに関係していそうなAPIは多分以下の11個。

  • MPI_Init
  • MPI_Comm_rank
  • MPI_Comm_size
  • MPI_Scatter
  • MPI_Bcast
  • MPI_Cart_create
  • MPI_Cart_coords
  • MPI_Cart_shift
  • MPI_Sendrecv_replace
  • MPI_Gather
  • MPI_Finalize

順に見ていきます。ちなみにこの辺は、OSの授業で扱われたシステムコールやpthread APIなどを思い出すと理解が捗る気がします。

東大生やかんのブログ
やかん

まじで神授業であった。。

MPI_Init

MPI_Init(&argc, &argv);

MPIの初期化関数です。引数は2つありますが、この二つは基本的に気にしなくていいと思います。main関数でコマンドラインから引数を受け取ることができますが、それを横流しするイメージです。

int main(int argc, char *argv[])
{
    MPI_Init(&argc, &argv);
}

↑こんな感じ。このAPIはMPIを使うときの呪文だと思って、MPIを使う場合はとりあえず一番上に書いておけばOKです。

MPI_Comm_rank

int MPI_Comm_rank(MPI_Comm comm, int *rank);

自分のプロセスIDを取得するAPIです。実際に使う場合は、

MPI_Comm_rank(MPI_COMM_WORLD, &rank);

こうなると思います。MPI_COMM_WORLDもとりあえず呪文でいいと思いますが、MPI_Comm_rankの第一引数は「どの範囲のプロセスにおけるIDを取得する?」みたいな意味合いです。MPI_COMM_WORLDはmpiのヘッダーファイル内に定義されている値で、「全プロセスの中からIDを取得したいです」という意味です。

また、mpiにおいては「プロセスのID」というと語弊があるので、「プロセスのランク」と言う方が適切です。

MPI_Comm_size

int MPI_Comm_size(MPI_Comm comm, int *size);

指定された範囲内に存在するプロセス数を取得するAPIです。第一引数はMPI_Comm_rankと同様、「どの範囲内に存在するプロセス数を数える?」みたいな意味合いです。

てな訳なので、通常以下のような記述になると思われます。

// MPI_COMM_WORLD内のプロセス数を取得
MPI_Comm_size(MPI_COMM_WORLD, &size);

MPI_Scatter

int MPI_Scatter(const void *sendbuf, int sendcount, MPI_Datatype sendtype,
                void *recvbuf, int recvcount, MPI_Datatype recvtype,
                int root, MPI_Comm comm);

急にゴツくなりました。これは、指定されたデータを、指定されたプロセスから指定された範囲内の全てのプロセスに分散する(共有する)APIです。引数が多いので一つ一つ見ていくと、

  • 第1引数(sendbuf):送信したいデータのバッファ。ルートプロセス(後述)のみ有効。
  • 第2引数(sendcount):第3引数で指定するデータ型に応じた、送信したいデータの要素数。
  • 第3引数(sendtype):送信するデータのデータ型。これはMPIが提供しているデータ型だから、例えばMPI_INTとかMPI_DOUBLEとかになる。
  • 第4引数(recvbuf):受信するデータのバッファ。ルートプロセスに限らず全てのプロセスで有効。
  • 第5引数(recvcount):第6引数で指定するデータ型に応じた、受信するデータの要素数。
  • 第6引数(recvtype):受信するデータのデータ型。sendtype同様、MPI_INTなど。
  • 第7引数(root):ルートプロセスのランク。ここで指定したランクを持つプロセスから、データが各プロセスに送信される。
  • 第8引数(comm):MPI_Scatterに関係するプロセスの範囲を指定する。通常、既出のMPI_COMM_WORLDを指定する。

といったところです。

引数は多いですが噛み砕いてみるとシンプルで、例えばsendtypeとrecvtypeは基本的に同じだったり、sendcountとrecvcountも基本的に同じだったり。

使用例としてはこんな感じ↓

MPI_Scatter(data, 4, MPI_INT, recv_data, 4, MPI_INT, root, MPI_COMM_WORLD);

MPI_Bcast

int MPI_Bcast(void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm);

MPI_Scatterに非常に似ていますが、異なるAPIです。

  • 第1引数(buffer):ブロードキャストしたいデータのバッファ。ルートプロセスでは送信データが、それ以外のプロセスでは受信データが格納される。
  • 第2引数(count):データ型に応じた、データの要素数。
  • 第3引数(datatype):データのデータ型。これはMPIが提供しているデータ型だから、例えばMPI_INTとかMPI_DOUBLEとかになる。
  • 第4引数(root):ルートプロセスのランク。
  • 第5引数(comm):MPI_Scatterに関係するプロセスの範囲を指定する。通常、既出のMPI_COMM_WORLDを指定する。

「MPI_Scatterと何が違うの?てか、MPI_Bcastの方が便利じゃね?」と思いますよね。僕もそう思います。これについてはのちに扱うとして、とりあえずいかに使用例を示します。

MPI_Bcast(&data, 1, MPI_INT, root, MPI_COMM_WORLD);

MPI_Cart_create

int MPI_Cart_create(MPI_Comm old_comm, int ndims, const int dims[],
                    const int periods[], int reorder, MPI_Comm *comm_cart);

こいつもゴツいですね。プロセスをグリッド状に配置するAPIです。「グリッド状に配置」というのは、物理的に配置するのではなく論理的に配置することです。

また、ここから「コミュニケータ」という言葉を用いて説明します。コミュニケータとは、プロセス間の通信を管理するために導入される概念です。例えば、MPI_COMM_WORLDというのは「全てのプロセスを対象とするコミュニケータ」であると言えます。コミュニケータは、通信の範囲を限定し、各コミュニケータ間で通信は独立しているものとして理解すれば良さそうです。

コミュニケータという言葉を用いてMPI_Cart_createを説明すると、プロセスをグリッド状に配置したコミュニケータを作成するAPI、と説明ができます。

さて、MPI_Cart_createについてもMPI_Scatter同様、引数を一つずつ見ていきます。

  • 第1引数(old_comm):MPI_Cart_createでは新たなコミュニケータを作成しますが、これは指定したコミュニケータをもとに作成される。その、元となるコミュニケータを指定するのがこの引数。通常はMPI_COMM_WORLDが指定される。
  • 第2引数(ndims):グリッドの次元数。
  • 第3引数(dims[]):各次元におけるプロセス数を指定する配列。
  • 第4引数(periods[]):各次元のグリッドがトーラス構造かどうかを指定するフラグの配列。トーラス構造かどうか、という点で言えばグリッドと言うよりもデカルトトポロジーと言った方が適切だと思われる。0の場合は非周期的(端が繋がっていない)、1の場合は周期的(端が繋がっている)。
  • 第5引数(reorder):プロセスランクの再割り当てが可能かどうか示すフラグ。1なら可能、0なら不可能。
  • 第6引数(comm_cart):新しいコミュニケータに割り当てるポインタ。

面白いAPIですよね。大学3年のときに位相空間をほんのちょっとかじっておいてよかったと感じる一方で、もっと勉強してトポロジーとかまで深めておけばよかったとやや後悔する気もあります。今から勉強しますか。

東大生やかんのブログ
やかん

夏休みの課題だ。

MPI_Cart_createの使用例としてはこんな感じだと思われる↓

// デカルトトポロジーのコミュニケータを作成
MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 1, &cart_comm);

MPI_Cart_coords

int MPI_Cart_coords(MPI_Comm comm, int rank, int maxdims, int coords[]);

これは比較的シンプルで、指定されたプロセスランクのデカルトトポロジーにおける座標を取得します。

  • 第1引数(comm):おなじみのcommだが、MPI_Cart_createで作成したコミュニケータを渡すのが通例だと思われる。
  • 第2引数(rank):座標を取得したいプロセスのランク。
  • 第3引数(maxdims):これは、とりあえず「デカルトトポロジーの次元数」を指定すると思って良いという理解。
  • 第4引数(coords):座標が取得されると、ここに指定した配列に座標が格納される。

第4引数のcoordsについては具体例見ると一目瞭然です。デカルトトポロジーも言ってしまえばグリッドなので、例えば以下のようになります。

[0, 0]
[0, 1]
[1, 0]
[1, 1]
東大生やかんのブログ
やかん

次元が上がったときのことはちょっとわかりません。

MPI_Cart_shift

int MPI_Cart_shift(MPI_Comm comm, int direction, int disp, int *rank_source, int *rank_dest);

これはちょっとよくわかっていないので、勉強して理解したのち改めて内容を更新していこうと思います。一旦、わかっていることだけ書きます。

東大生やかんのブログ
やかん

すみません。

これはデカルトトポロジーにおいて隣接するプロセスランクを取得します。引数を見ていくと特にわかりやすくて、

  • 第1引数(comm):MPI_Cart_coords同様、MPI_Cart_createで作成したコミュニケータであることが通例。
  • 第2引数(direction):「どの方向に隣接したプロセスを対象とするか」を指定する。2次元の場合、0が水平方向、1が垂直方向。
  • 第3引数(disp):シフトする距離。符号付きの値。
  • 第4引数(rank_source):よくわからん。
  • 第5引数(rank_dest):よくわからん。

MPI_Sendrecv_replace

int MPI_Sendrecv_replace(void *buf, int count, MPI_Datatype datatype,
                         int dest, int sendtag, int source, int recvtag,
                         MPI_Comm comm, MPI_Status *status);

メモリ利用効率向上のためのAPIだと理解しています。データを送信して受信する時に、送信と受信が同時に行われるのであれば、送信 → 受信の順番で同じバッファを利用してもメモリが違法に書き潰されることはありません。

東大生やかんのブログ
やかん

レジスタリネーミングにおけるWARに似てます。偽の依存関係ですね。

送信用バッファ、受信用バッファをそれぞれ用意するとメモリが勿体無いからまとめてしまおう、という発想だと理解しています。

こちらも同様に1つずつ引数を見ていきます。

  • 第1引数(buf):送信データのバッファで、受信データもここに格納される。
  • 第2引数(count):送受信するデータの要素数。
  • 第3引数(datatype):データの型。MPI_Scatterなどと同様、MPI_INTなど。
  • 第4引数(dest):送信先のプロセスランク。
  • 第5引数(sendtag):送信データ(メッセージ)に付与するタグ。通信の識別に利用される。
  • 第6引数(source):受信元のプロセスランク。MPI_ANY_SOURCEを指定すると任意のプロセスから受信できる。
  • 第7引数(recvtag):受信データ(メッセージ)に対するタグ。MPI_ANY_TAGを指定すると任意のタグのメッセージが受信できる。
  • 第8引数(comm):通信に使用するコミュニケータ。通常はMPI_COMM_WORLDが指定される。
  • 第9引数(status):受信におけるステータスを格納するバッファ。

使用例としてこんな感じ↓

// 隣接プロセスとデータを交換
MPI_Status status;
MPI_Sendrecv_replace(&data, 1, MPI_INT, right, 0, left, 0, MPI_COMM_WORLD, &status);

データの送受信を実際に行う場合、すごく便利そうなAPIですね。

MPI_Gather

int MPI_Gather(const void *sendbuf, int sendcount, MPI_Datatype sendtype,
               void *recvbuf, int recvcount, MPI_Datatype recvtype,
               int root, MPI_Comm comm);

これはMPI_Scatterの逆、といったイメージです。各プロセスからデータを1つのプロセス(ルートプロセス)に集めるためのAPI。

引数を順に見ていくと、

  • 第1引数(sendbuf):各プロセスが送信するデータを格納するバッファ。
  • 第2引数(sendcount):MPI_Scatterに同じ。
  • 第3引数(sendtype):MPI_Scatterに同じ。
  • 第4引数(recvbuf):受信するデータを格納するバッファ。ルートプロセスにおいてのみ有効。
  • 第5引数(recvcount):MPI_Sendrecv_replaceに同じ。
  • 第6引数(recvtype):MPI_Sendrecv_replaceに同じ。
  • 第7引数(root):MPI_Scatterに同じ。
  • 第8引数(comm):通信に使用するコミュニケータ。通常はMPI_COMM_WORLDが指定される。

ここで、recvbufの容量は、sendbufの容量にコミュニケータのプロセス数を乗じたものになります。

MPI_Finalize

int MPI_Finalize(void);

これは特に説明不要かと思います。全てのMPIコミュニケーションが完了したら呼び出されるAPIです。リソースの解放など、クリーンアップ処理を行ってくれます。MPI_Initとセットで、「MPIを使用するときの呪文」と思って問題ないと思います。

MPI_ScatterとMPI_Bcastの違いについて

これは、APIを見比べても正直ピンとこないです。端的に述べると、

  • MPI_Scatter:各プロセスは、ルートプロセスから異なるデータを受け取る(つまりデータが分散される)
  • MPI_Bcast:各プロセスは、ルートプロセスから同じデータを受け取る

です。使用用途としては、MPI_Scatterはシンプルに計算の効率化、MPI_Bcastは全プロセスで共有したい初期データなどの共有に用います。

となると、

東大生やかんのブログ
やかん

え、MPI_Scatterはどの辺でデータを分割して送信してるの?

と思いますよね。これは、ルートプロセスにおいて送信データが、データそのものではなくバッファとして与えられている点、加えてデータの容量も指定されている点に着目すれば理解可能です。

まあ、すごいシンプルな話で、1つのプロセスに、指定された容量分のデータが共有されたら、そのデータ容量分バッファのアドレスを更新する、って話ですね。だから、関係式としては

sendbufの容量 >> sendcount * sendtypeの容量

てな感じです。

次は何したいか。

CannonとFoxを実装したいです。。難しそうだけどなー、できるかなあ、、あと、こういうの見てみるとSUMMAめちゃむずそうなんだけど大丈夫かな。とりあえず、できるところまで頑張ります。

ということで、こちらのメモ終了。最後までお読みいただき、ありがとうございます。