科学技術計算などで使われる並列計算 Message Passing Interface (MPI) でデータをやりとりするには通常 MPI_Send と MPI_Recv などを送り手と受取手の両方で MPI 関数呼び出す。データを受けとる側は何回データが送られてくるのかわかっていないといけない。しかし、MPI_Put と MPI_Get という、一方的にデータを送りつけたり、取得する方法がある。この方法ならいくつのノードからデータが送られてくるかわからくても大丈夫。ネットにあまり具体例がないので、ここにひとつ例を載せておく。
int rank, n;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &n);
const int target= 0;
const int nsend=1;
float send= rank + 1;
この例では、簡単のため各MPIノードが1個のデータ send
をノード0に MPI_Put する。(もちろん、これなら MPI_Gather すれば良いわけだけども、実際の応用ではどこに何回でも送ってもよく、かつ AlltoAll のようにあらゆる場所に送るわけではない場合などに柔軟性のあるデータ転送ができる。)
MPI Window
MPI_Win win_header, win_data;
まずは他のプロセスから書き込めるメモリ領域「ウィンドウ」を確保する。既にメモリを確保している場合は MPI_Win_create
、これを期に確保する場合は MPI_Win_allocate
がある。
int MPI_Win_create(void *base, MPI_Aint size, int disp_unit,
MPI_Info info, MPI_Comm comm, MPI_Win *win)
int MPI_Win_allocate(MPI_Aint size, int disp_unit, MPI_info info,
MPI_Comm comm, void* baseptr, MPI_Win* win)
ここでは、何個のデータが既に書き込まれているか記録する int ndat
とバッファ float* dat
のために二つのウィンドウを用意している。
int ndat= 0;
MPI_Win_create(&ndat, sizeof(int), sizeof(int), MPI_INFO_NULL,
MPI_COMM_WORLD, &win_header);
float* dat;
MPI_Win_allocate(sizeof(float)*n, sizeof(float), MPI_INFO_NULL,
MPI_COMM_WORLD, &dat, &win_data);
disp_unit = sizeof(float)
とはデータの送る位置を指定する場合の単位で、MPI_Put のところで説明する。
データの送受信をはじめるには、大雑把な MPI_Win_fence
と他から同時に読み書きされるのを防ぐ MPI_Win_lock
の2種類を使う。
MPI_Win_fence
MPI_Win_fence(0, win_data);
「win_data には好きに書き込んでください」と MPI_Put の受け入れを開始する。複数のプロセスが書き込む順番は何も保証されていないし、複数のプロセスが同じ場所に書き込んではいけない。
MPI_Win_lock
もし、データを書き込みたい場所があらかじめわかっているなら、win_data と MPI_Win_fence だけで十分だけれども、ここではデータを上書きしてしまわないように、適当にずらして書き込んでいく;すなわち、ノード0の変数 ndat
を読んで、送り先の offset
を決め、後で送られてくる場合に備えて ndat
の値を増やしておく。
// offset = target::ndat
// target::ndat += nsend
これは MPI_Get_accumulate
でできるが、並列計算の常として、ndat の値をとってきて、ndat の値を増やすまでの間に他のプロセスも同時に ndat の値を読んでいる危険がある。なので、この更新は同時にひとつのノードしか ndat を読み書きできないようにロックをかける必要がある。
int MPI_Win_lock(int lock_type, int rank, int assert, MPI_Win win)
int MPI_Win_unlock(int rank, MPI_Win win)
「ノード rank のウィンドウ win にこれからアクセスしますが、他のノードは unlock するまで待っててくださいね」
MPI_Win_lock(MPI_LOCK_EXCLUSIVE, target, 0, win_header);
int offset;
MPI_Get_accumulate(&nsend, 1, MPI_INT, &offset, 1, MPI_INT, target,
0, 1, MPI_INT, MPI_SUM, win_header);
MPI_Win_unlock(target, win_header);
注意すべきことは MPI_Win_fence
は「ご自由に書き込みください」という宣言なので、Win_fence を win_header に対して呼んでしまうと、 MPI_Win_lock
しても、ロックされず、自由に読み書きされてしまう。この例では win_data
は MPI_Win_fence で自由に読み書きできる dat を提供し、win_header
は MPI_Win_lock で一度にひとつのプロセスしか読み書きできない ndat を管理している。
MPI_Put
int MPI_Put(const void *origin_addr, int origin_count,
MPI_Datatype origin_datatype, int target_rank,
MPI_Aint target_disp, int target_count,
MPI_Datatype target_datatype, MPI_Win win)
1個のデータ float send
を win_data
の先頭から offset のところに書き込む:
MPI_Put(&send, 1, MPI_FLOAT, target, offset, 1, MPI_FLOAT, win_data);
ここで、ウィンドウを作った時の disp_unit = sizeof(float)
が関係してくる。target_disp = offset
により dat の位置から disp_unit * offset バイト後に書き込んでいる。単位はなんでもよくて、disp_unit=1 にしてバイト単位にしても良いし、struct Particle を読み書きするなら、disp_unit=sizeof(struct Particle) が便利かもしれない。
MPI_Win_fence(0, win_data);
二度目の MPI_Win_fence で自由書き込み期間がおわりを宣言し、これ以降は MPI_Put のデータ書き込みが完了したことが保証される。逆に2度目の MPI_Win_fence までは dat がどうなっているかはわからないし、MPI_Get に関しても同様。
最後に結果を出力する。
if(rank == target) {
printf("ndat after communication: %d\n", ndat);
for(int i=0; i<ndat; ++i)
printf("received data[%d] = %f\n", i, dat[i]);
}
後始末
MPI_Win_free(&win_data);
MPI_Win_free(&win_header);
MPI_Finalize();
結果は
$ mpirun -n 4 mpi_oneside_ex
ndat at the end: 4
received dat[0] = 1.000000
received dat[1] = 4.000000
received dat[2] = 3.000000
received dat[3] = 2.000000
など。書き込む順番はランダムだけれども、上書きしたりすることなく、各ノードがノード0の dat
に send = rank+1
を書き込んでいる。