C++でOpenMPI入門7 コミュニケータ1

前回に続いて, 一対一のブロッキング通信を用いてランク(int型)の送受信を行う. 今度はSendrecvによって, ひとつ隣のプロセスに自分のランクを送信する. 0は1へ, 1は2へ, 2は3への要領. 4プロセス並列の場合, 3はまた0へ送信することで周期的な関係を用いた.

#include <iostream>
#include <mpi.h>

int main(int argc, char **argv)
{
    MPI::Init(argc, argv);

    int rank = MPI::COMM_WORLD.Get_rank();
    int procs = MPI::COMM_WORLD.Get_size();

    int send_value = rank;
    int recv_value = -1;

    int dest_rank = (rank + 1) % procs;
    int src_rank = (rank - 1 + procs) % procs;

    const int TAG = 0;
    MPI::COMM_WORLD.Sendrecv(&send_value, 1, MPI::INT, dest_rank, TAG,
                             &recv_value, 1, MPI::INT, src_rank , TAG);
    std::cout << "rank = " << rank
              << " : recv_value = " << recv_value << std::endl;

    MPI_Finalize();
}

ご覧の通り, 前回と違って, rankによる条件分岐がなくなっている. この状態が並列では色々な意味で理想的. 今回は, dest_rankとsrc_rankを剰余を用いて計算している以外は前回から新しいことは出てきていない.

実行結果はこのようになるはず.

rank = 1 : recv_value = 0
rank = 2 : recv_value = 1
rank = 0 : recv_value = 3
rank = 3 : recv_value = 2

これは言わば, プロセスを周期的な1次元上に配置し, 右隣のプロセスへ通信せよ, という命令だと考えるとわかりやすい. より一般的に言えば, プロセス同士をあるトポロジーに基づいて関係づけている. OpenMPIではよく用いられるトポロジーについてこうした関係づけを効率よくサポートするコミュニケータという概念がある. 実を言えば, これまで使ってきたMPI::COMM_WORLDというのもコミュニケータの一つで, 暗黙のうちに組み立てられている.

つまり, 今回の例は1次元のデカルト座標系(Cartesian coordinate system)を表すコミュニケータを生成して用いてもできる.

#include <iostream>
#include <mpi.h>

int main(int argc, char **argv)
{
    MPI::Init(argc, argv);

    int procs = MPI::COMM_WORLD.Get_size();

    int const ndims = 1;
    int dims[ndims] = {0};
    MPI::Compute_dims(procs, ndims, dims);

    bool periods[ndims] = {true};
    MPI::Cartcomm COMM_CART = MPI::COMM_WORLD.Create_cart(
        ndims, dims, periods, true);

    int rank = COMM_CART.Get_rank();
    int send_value = rank;
    int recv_value = -1;

    int src_rank, dest_rank;
    COMM_CART.Shift(0, 1, src_rank, dest_rank);

    const int TAG = 0;
    COMM_CART.Sendrecv(&send_value, 1, MPI::INT, dest_rank, TAG,
                       &recv_value, 1, MPI::INT, src_rank , TAG);
    std::cout << "rank = " << rank
              << " : recv_value = " << recv_value << std::endl;

    MPI::Finalize();
}

実行結果は前述のコードと同じ. コードで上記の例と違う部分は, src_rankとdest_rankを決める方法が剰余からMPI::Cartcommを用いる方法へと変わった点. 先に言った通り, COMM_CARTにCreate_cart関数によって1次元のデカルト系のトポロジーをもったコミュニケータを作成して, これによって隣のランクを得ている.

新しく登場した関数は, Compute_dims, Create_cart, Shiftの3つ.

Compute_dimsは各次元毎にわりあてられるプロセス数を決めてくれるものだが, 自分で考えているトポロジーがあるなら不要. この1次元の場合は, 1通りしかありえないので明らかに不要. dimsはあらかじめ初期化してある必要があり, 0より大きい数値が入っていた場合は, 変更せず, 0であった場合のみCompute_dimsで更新され得る. よって, すべて自動で決めたければ0で初期化しておく. まあ, 呼ばなくて良い気がする. 結果としてdims中の数値を全てかけ合わせると総プロセス数(procs)になっているはず.

Create_cartはMPI::Cartcommを作成する. 4つの引数は順に, 次元数, 各次元あたりのプロセス数, 周期性の有無, 最後はランクの再配置を許すかどうか. 周期性というのは前述の例でランク3の隣が0であると定義するかどうかを各次元について示している. ランクの再配置とはトポロジーによってはハードウェア上の構成上効率の良い並びというものがあり得るため, それに基づいてCOMM_CARTのランクをつけ直すことみたいです. ようするにCOMM_CARTとMPI::COMM_WORLDでは同じプロセスが異なるランクを持つ可能性があるということなのかな.

Shiftは自分の座標を元にして周囲のランクをとってくる. 最初の引数はどの次元をみて隣をとってくるかを指定する(0はじまりであることに注意). 今回は1次元なので0しか指定できない. 次の引数は, いくつ隣をみるかを指定している(displacement). 今回は1つ隣をみている. 3, 4番目で指定した変数に, 1, 2番で指定した位置に相当する左右のランクが代入される. 今回は単純に両隣.

この例だとプロセス・トポロジーの良さがいまいち伝わらなさそうなので次回は2次元のデカルト系を使ってみます.

http://labo.nshimizu.com/documents/mpiprimj.pdf
http://publib.boulder.ibm.com/infocenter/zos/v1r11/index.jsp?topic=/com.ibm.zos.r11.fomp200/ipezps0047.htm