やかんです。

消極的なタイトルで恐縮ですが、そもそも3次元以上には対応できてないですし、「このくらいのタイトルが妥当かな、、」ってことでタイトルつけてます。

ただ、個人的に理解が深まってきているのを感じるので、メモとして綴ります。

例の如く、内容は僕のパブリックメモです。

MPIのカートグラフィ機能とは。

MPIということで、文脈としては行列演算の並列化です。行列演算は並列化の際、部分行列として扱うと効率よく計算できます。

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

Cannon, Fox, SUMMA, PUMMA,,,

部分行列ごとにプロセスを割り当て、それぞれ並列化して計算を進め、最後に計算結果を統合する、という流れです。このとき、プロセスも「まるで部分行列みたいな形をしているかのように」トポロジカルに扱ってあげたいよね、という話だと理解しています。

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

トポロジーについてはまじの無知です

手書きの図で恐縮ですが、こんなイメージです↓

プロセスの配置のみに着目すると、

こんな感じ。(0, 0)とかは後述しますが、「座標」だと思って大丈夫なはず。次元が増えてくると、座標というより「行列における行と列」みたいに捉えた方がいいんですかね。

このように、プロセスに「配置」という概念を与えてあげると、特に行列演算については扱いが便利になるわけです。

具体的なAPIを通じて理解する。

MPIのカートグラフィ機能として、以下の3つを通じて理解を試みたいです。

  • MPI_Cart_create
  • MPI_Cart_coords
  • MPI_Cart_shift
東大生やかんのブログ
やかん

使用方法などは前回記事(こちら)を参照いただけると。

ざーっと概観したのち、具体的なサンプルプログラムの中で色々いじります。

MPI_Cart_create

これは、「プロセスに『配置』という概念を与えるためのAPI」と理解するのが良いと思います。MPIっぽく表現すると、「『配置』という概念を持つ新たなコミュニケータを作成するAPI」みたいになるんですかね。

MPI_Cart_coords

めでたくMPI_Cart_createによって「配置」という概念が与えられたプロセス達ですが、「わい、どこに位置しとるん?」という「座標」を取得するためのAPIです。

MPI_Cart_shift

「配置」が与えられたプロセス達ですが、お隣さん(隣接するプロセス)が誰なのかわからないと、正直意味がないです。てなわけで、お隣さんがどのプロセスなのかを取得するのがこのAPI。

これらのAPIで遊んでみる。

まず、MPI_Cart_createでプロセスを配置し、MPI_Cart_coordsでどのプロセスがどこに配置されているかを取得してみましょう。

#include <stdio.h>
#include <mpi.h>

int main(int argc, char *argv[])
{
  int rank, size, ndims = 2;
  int dims[2] = {2, 2};
  int periods[2] = {0, 0};
  int coords[2];
  MPI_Comm cart_comm;

  // --- MPIを初期化 ---
  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
  MPI_Comm_size(MPI_COMM_WORLD, &size);

  // --- 2次元グリッドを作成 ---
  MPI_Cart_create(MPI_COMM_WORLD, ndims, dims, periods, 0, &cart_comm);

  // --- 各プロセスの座標を取得 ---
  MPI_Cart_coords(cart_comm, rank, ndims, coords);

  // --- 各プロセスのランクと座標を表示 ---
  printf("Rank %d の座標 (%d, %d)\n", rank, coords[0], coords[1]);

  MPI_Finalize();
  return 0;
}

printfによる出力は以下。

Rank 0 の座標 (0, 0)
Rank 3 の座標 (1, 1)
Rank 2 の座標 (1, 0)
Rank 1 の座標 (0, 1)

ここで、rankというのは、MPIで扱われるプロセスの識別子です。以下の図のようなイメージ。

この例で本質的なのはMPI_Cart_createだと思うので、ちょっとだけ踏み込んでみます。

MPI_Cart_createのとこだけ抽出すると以下。

  int rank, size, ndims = 2;  
  int dims[2] = {2, 2};
  int periods[2] = {0, 0};
  MPI_Comm cart_comm;

  ...

  // --- 2次元グリッドを作成 ---
  MPI_Cart_create(MPI_COMM_WORLD, ndims, dims, periods, 0, &cart_comm);

これは、「MPIが管理するすべてのプロセスを、ndims次元(今回は2次元)に配置し(つまりグリッドみたいなもの)、各次元にはdims[0](今回は2)、dims[1](今回は2)個のプロセスを割り振ろう」みたいな意味合いです。

で、こうして作成された新たなコミュニケータをcart_commが受け取るわけですね。

詳細というほどでもないですが、もうちょい詳しい話は前回記事(こちら)に書いたと思います。

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

ずいぶん雑なんですが許してください。。

では次に、MPI_Cart_shiftを使ってお隣さんを取得してみましょう。

#include <stdio.h>
#include <mpi.h>

int main(int argc, char *argv[])
{
  int rank, size, ndims = 2;
  int dims[2] = {2, 2};
  int periods[2] = {0, 0};
  int coords[2];
  int left, right, up, down;
  MPI_Comm cart_comm;

  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
  MPI_Comm_size(MPI_COMM_WORLD, &size);

  // --- 2次元グリッドを作成 ---
  MPI_Cart_create(MPI_COMM_WORLD, ndims, dims, periods, 0, &cart_comm);

  // --- 各プロセスの座標を取得 ---
  MPI_Cart_coords(cart_comm, rank, ndims, coords);

  // --- 左右(水平方向)の隣接プロセスを取得 ---
  MPI_Cart_shift(cart_comm, 1, 1, &left, &right);
  // --- 上下(垂直方向)の隣接プロセスを取得 ---
  MPI_Cart_shift(cart_comm, 0, 1, &up, &down);

  // 各プロセスのランク、座標、および隣接プロセスのランクを表示
  printf("Rank %d の座標 (%d, %d), 左隣 = %d, 右隣 = %d, 上 = %d, 下 = %d\n",
         rank, coords[0], coords[1], left, right, up, down);

  MPI_Finalize();
  return 0;
}

お隣さんてか、上下左右なんで近隣住民っすね。出力は

Rank 0 の座標 (0, 0), 左隣 = -2, 右隣 = 1, 上 = -2, 下 = 2
Rank 2 の座標 (1, 0), 左隣 = -2, 右隣 = 3, 上 = 0, 下 = -2
Rank 3 の座標 (1, 1), 左隣 = 2, 右隣 = -2, 上 = 1, 下 = -2
Rank 1 の座標 (0, 1), 左隣 = 0, 右隣 = -2, 上 = -2, 下 = 3
東大生やかんのブログ
やかん

おやおや???

プロセスのランクは0から3までしかないのに、なぜか-2とかのプロセスが近隣住民として表示されている箇所がありますね。これはいわゆる「トーラス構造」に関わってくる話だと思いますが、小難しい話は抜きにコードをまず修正して再度実行してみます。

#include <stdio.h>
#include <mpi.h>

int main(int argc, char *argv[])
{
  int rank, size, ndims = 2;
  int dims[2] = {2, 2};
  int periods[2] = {1, 1}; // <- ここを変えたよ!
  int coords[2];
  int left, right, up, down;
  MPI_Comm cart_comm;

  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
  MPI_Comm_size(MPI_COMM_WORLD, &size);

  // --- 2次元グリッドを作成 ---
  MPI_Cart_create(MPI_COMM_WORLD, ndims, dims, periods, 0, &cart_comm);

  // --- 各プロセスの座標を取得 ---
  MPI_Cart_coords(cart_comm, rank, ndims, coords);

  // --- 左右(水平方向)の隣接プロセスを取得 ---
  MPI_Cart_shift(cart_comm, 1, 1, &left, &right);
  // --- 上下(垂直方向)の隣接プロセスを取得 ---
  MPI_Cart_shift(cart_comm, 0, 1, &up, &down);

  // 各プロセスのランク、座標、および隣接プロセスのランクを表示
  printf("Rank %d の座標 (%d, %d), 左隣 = %d, 右隣 = %d, 上 = %d, 下 = %d\n",
         rank, coords[0], coords[1], left, right, up, down);

  MPI_Finalize();
  return 0;
}

periodsを{0, 0}から{1, 1}に変えました。出力は

Rank 2 の座標 (1, 0), 左隣 = 3, 右隣 = 3, 上 = 0, 下 = 0
Rank 0 の座標 (0, 0), 左隣 = 1, 右隣 = 1, 上 = 2, 下 = 2
Rank 3 の座標 (1, 1), 左隣 = 2, 右隣 = 2, 上 = 1, 下 = 1
Rank 1 の座標 (0, 1), 左隣 = 0, 右隣 = 0, 上 = 3, 下 = 3

おー、なんだか良さそうですね。先ほどの図を再掲しますが、

たしかに、例えばrankが0のプロセスに着目すると、右隣がrank1のプロセス、左隣もぐるっと回ってrank1のプロセス、上がrank2のプロセス、下もぐるっと回ってrank2のプロセスになっていますね。

この、「ぐるっと回って」というのはいわゆるトーラス構造です。

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

すみません、トーラスについても理解していないに等しいくらい浅いことしか知りません。

periodsの値を0, 1で変えることによって、各次元においてトーラス構造を持たせるか否かを設定することができます。

というわけで、今回のメモを終えようと思います。まだまだ理解したいことは山積みですが、一個一個、焦らず理解を試みたいです。

最後までお読みいただき、ありがとうございます。