MPI_Comm_spawnによる動的プロセス生成

通常のよくあるMPIプログラムは実行開始時にプロセス数を決めて、そのプロセスが最初から最後まで存在する。
一方で、MPIプログラムを実行中に動的にプロセスを生成する方法として`MPI_Comm_spawn`という関数がある。fork (あるいはsystem()) のようなものでプロセスを追加で起動することができる。この時、呼ばれた側の子プロセスもMPIプログラムである必要がある。

ここでは`MPI_Comm_spawn`について調べたことをまとめておく。

参考資料:

- https://stackoverflow.com/questions/38411583/send-and-receive-operations-between-communicators-in-mpi
- https://www.mpi-forum.org/docs/mpi-2.2/mpi22-report/node143.htm#Node143
- https://www.hpci-office.jp/materials/MPI-2017-03-23.pdf

# サンプルコード

ここではspawnを呼ぶ側のプロセスを`master`、呼ばれる側を`worker`と呼ぶことにする。
master, workerそれぞれのソースコードは以下のようになる。まずはサンプルコードを示し、その後解説を行っていく。

```master.cpp
#include <iostream>
#include "mpi.h"

int main(int argc, char *argv[]) {
  int rank, size;
  MPI_Init(&argc, &argv);
  MPI_Comm_size(MPI_COMM_WORLD, &size);  // the number given to `mpiexec -n <size>`
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);  // 0...size

  MPI_Comm intercomm;
  MPI_Comm comm = MPI_COMM_SELF;
  // MPI_Comm comm = MPI_COMM_WORLD;
  int maxprocs = 3, root = 0;
  char *cmdargv[] = {"foo", "bar", NULL};  // the first argument is NOT the command itself
  MPI_Comm_spawn("worker", cmdargv, maxprocs, MPI_INFO_NULL, root,
                 comm, &intercomm, MPI_ERRCODES_IGNORE);
  // blocking operation. MPI_Comm_spawn return after MPI_Init is called in the spawned process

  int wrank, wsize, rsize;
  MPI_Comm_size(intercomm,
                &wsize);  // => 1, local group of intercomm is identical to the communicator given as the fifth argument
  MPI_Comm_rank(intercomm, &wrank);  // => 0
  MPI_Comm_remote_size(intercomm, &rsize);   // => 3 (== maxprocs)
  int is_inter;
  MPI_Comm_test_inter(intercomm, &is_inter);
  std::cout << size << ' ' << rank << ' ' << wsize << ' ' << wrank << ' ' << rsize << ' ' << is_inter << std::endl;
  int received;
  // communication between local group is specified
  MPI_Recv(&received, 1, MPI_INT, 2, 0, intercomm, MPI_STATUS_IGNORE);
  std::cout << "received" << received << std::endl;

  MPI_Comm_disconnect(&intercomm);
  MPI_Finalize();
  std::cout << "after finalize" << std::endl;
  return 0;
}
```

```worker.cpp
#include <iostream>
#include "mpi.h"

int main(int argc, char *argv[]) {
  MPI_Comm parent;
  int rank, size, rsize;
  MPI_Init(&argc, &argv);
  std::cout << "worker started" << ' ' << argc << std::endl;
  for(int i = 0; i < argc; i++) {
    std::cout << argv[i] << ' ';
  }
  std::cout << std::endl;
  MPI_Comm_get_parent(&parent);
  MPI_Comm_remote_size(parent, &rsize);
  MPI_Comm_size(parent, &size);
  MPI_Comm_rank(parent, &rank);
  std::cout << rsize << ' ' << size << ' ' << rank << std::endl;
  int data = 123456;
  if (rank == 2) {
    MPI_Send(&data, 1, MPI_INT, 0, 0, parent);
    std::cout << "sent" << std::endl;
  }
  MPI_Comm_disconnect(&parent);
  MPI_Finalize();
  return 0;
}
```

master, workerをそれぞれコンパイルし、mpiexecコマンドで実行する。

```shell-session
mpicxx -o master master.cpp
mpicxx -o worker worker.cpp
mpiexec -n 2 master.cpp
```

ここで`-n 2`と指定している数は静的に起動されるプロセス数、つまりmasterが２並列で起動する。
上記のコードでは、この２プロセスに加えてworkerが６プロセス（masterそれぞれに対して３プロセスずつ）起動する。

実行結果はいかのようになる。

```
2 0 1 0 3 1
worker started 3
worker foo bar
1 3 0
worker started 3
worker foo bar
1 3 1
worker started 3
worker foo bar
1 3 2
sent
received123456
2 1 1 0 3 1
worker started 3
worker foo bar
1 3 0
worker started 3
worker foo bar
1 3 1
worker started 3
worker foo bar
1 3 2
sent
received123456
after finalize
after finalize
```

# 解説

まず、子プロセスを起動する関数が`MPI_Comm_spawn`になる。引数に見慣れないものがいくつかあるので順に解説していく。
参考 : https://www.open-mpi.org/doc/v3.0/man3/MPI_Comm_spawn.3.php

MPI_Comm_spawnの仕様は以下の通りである。

```cpp
int MPI_Comm_spawn(const char *command, char *argv[], int maxprocs,
    MPI_Info info, int root, MPI_Comm comm,
    MPI_Comm *intercomm, int array_of_errcodes[])
```

サンプルの該当部分を抜粋すると以下のようになる。

```master.cpp
  MPI_Comm intercomm;
  MPI_Comm comm = MPI_COMM_SELF;
  // MPI_Comm comm = MPI_COMM_WORLD;
  int maxprocs = 3, root = 0;
  char *cmdargv[] = {"foo", "bar", NULL};  // the first argument is NOT the command itself
  MPI_Comm_spawn("worker", cmdargv, maxprocs, MPI_INFO_NULL, root,
                 comm, &intercomm, MPI_ERRCODES_IGNORE);
```

まず、第一引数の`"worker"`という文字列は起動するプログラムのパスである。

第二引数はworkerプログラムに渡す引数の配列になっており、配列の最後の要素にnullptrを与える必要があり、これで配列のサイズを判定している。
注意したいのが`cmdargv[0]`はプログラムの名前ではないということ。CやC++ではargv[0]には実行しているプログラム自体の名前が入るが、そのような慣例とは異なり引数自体が入る。
引数が何もなければ`MPI_ARGV_NULL`を指定しても良い。
（ところでなぜconst char** ではなくchar**なのだろうか...？）

第三引数の`maxprocs`は起動する際のMPIプロセス数である。
つまり、`mpiexec -n 3 worker foo bar` というコマンド実行しているのと同等である。

第４引数には特別な場合以外、`MPI_INFO_NULL`を指定しておけば良い。（実装依存のオプションを指定できるらしい）

第５引数の`root`は上記の変数を評価するrankを指定する。つまり、自分のrank == root である場合に子プロセスを起動すると考えていれば良い。

第６引数は`comm`はcommunicatorを指定する。さて、ここで`MPI_COMM_SELF`という見慣れないcommunicatorを指定している。
そもそもcommunicatorは何かというと、MPIプロセスの集合のようなものである。よく使う`MPI_COMM_WORLD`はすべてのrank、すなわち [0,1,..., (静的process数-1)] というrankの集合になっている。
一方で`MPI_COMM_SELF`というのは自分自身のみを含むcommunicatorになっていて、rank 0のみが存在する。（それぞれのMPIプロセスはcommunicatorごとに異なるrankで表される。例えば、あるプロセスは`MPI_COMM_WORLD`においてはrank 3だが、別のcommunicatorにおいてはrankが0になるといったことも起こる。）

さて、今`MPI_COMM_SELF`のrank = 0で子プロセスが起動されるので、親の２プロセスそれぞれで子のMPIプログラムが起動する。
これを試しに`MPI_COMM_WORLD`にすると、全体で一回だけ子のMPIプログラムが起動するようになる。

第７引数`intercomm`もcommunicator (MPI_Comm)になるが、spawnされたプロセスと自分を含むinter communicatorになる。inter communicatorとは何かというのは以下でまた説明するが、これを使って子プロセスと親プロセスの間で通信をすることができる。

第８引数はエラーの情報。とりあえず`MPI_ERRCODES_IGNORE`を指定しておけば良いだろう。

MPI_Comm_spawnはblocking操作である。子プロセスの中で`MPI_Init`が呼ばれて始めて返ってくる。（つまり子プロセスはかならずMPIプログラムでなければならない。）

## inter communicatorとは何か。

（これが自分が理解するのに一番苦労した部分）

先ほどcommunicatorはMPIプロセスの集合を表すといったが、MPIのcommunicatorには"intra communicator", "inter communicator"の２種類が存在する。ほとんどの場合、"intra communicator"しか使うことはない。
先ほど述べた`MPI_COMM_WORLD`も`MPI_COMM_SELF`もどちらもintra communicatorである。

```
MPI_COMM_WORLD     => [0, 1, 2]       # 3 process で実行している場合のMPI_COMM_WORLD
MPI_COMM_SELF      => [0]          # MPI_COMM_SELFは自分自身のプロセスのみを含む
```

一方でinter communicatorというのは、local group, remote groupというrankの集合を２つ持つcommunicatorである。ざっくりといえば、普通のcommunicato２つ分あるようなもの。local groupとremote groupは重複を持たない集合になっており、自身のプロセスはlocal groupに含まれる。模式的に図で表すと以下のような感じ

```
intercomm = {  localgroup: [ 0, 1, 2], remotegroup: [0, 1, 2, 3] }
```

上記のように local group でも remote group でも rankの番号 0,1,2 は重複しているが、それぞれが指しているプロセスは別のものである。local group と remote group に属するプロセスに重複があってはいけない。

`MPI_Comm_spawn` を読んだ時に作られるinter communicator は local group に親プロセスのcommunicator、remote groupに子プロセスのcommunicatorが作られる。この時にlocal groupは`MPI_Comm_spawn`を第６引数で指定したものと同じものが、remote group には子プログラムの`MPI_COMM_WORLD`と同じものが入る。

inter communicator に対して、`MPI_Comm_size` `MPI_Comm_rank` を呼ぶと local group のサイズとランクが取得される。
remote groupのサイズを取得するには `MPI_Comm_remote_size` を使う。
また、intra communicator か inter communicator かを判別するには `MPI_Comm_test_inter` を呼べばよい。

さて、この inter communicator に対して`MPI_Send`, `MPI_recv` を呼ぶとどうなるか？
その場合、通信相手（Sendの場合は送信先、Recvの場合は受信元）はremote groupになる。例えば、`MPI_Send`の場合、remote groupの指定したrankの相手に送信される。

## 親子間での通信

先ほどのinter communicatorを使って親子間で通信ができる。

master側のサンプルの中の該当部分を抜粋すると以下のようになる。

```cpp:master.cpp
MPI_Recv(&received, 1, MPI_INT, 2, 0, intercomm, MPI_STATUS_IGNORE);
// inter communicatorのremote group（子MPIプログラム）のrank 2 から受信する
```

子から親の送信先を指定するには、`MPI_Comm_get_parent` を使う。
これを呼ぶとremote groupに親が入ったinter communicatorが得られる。

```cpp:worker.cpp
MPI_Comm parent;
MPI_Comm_get_parent(&parent);
```

これを使うと親と`MPI_Send`や`MPI_Recv`などを使って送受信ができる。

```cpp:worker.cpp
if (rank == 2) {
    MPI_Send(&data, 1, MPI_INT, 0, 0, parent);
  }
```

ここでは子プログラムのrank 2のプロセスと親プログラムの間でやりとりをしている。

通信が終わり必要のなくなったcommunicatorを開放するには`MPI_Comm_disconnect`関数を使う。