C++
MPI

誰にも共感してもらえないけどMPIのスコープが気持ち悪い

はじめに

これは多分MPI Advent Calendar 2017の2日目の記事です。

MPIというのはMessage Passing Interfaceの略で、異なるプロセス間でデータをやりとりするものなんだけれども、ライブラリという形で提供されている。これはつまり、もともとプロセス間通信という概念を含まない言語に、なんか関数を呼ぶと通信できるようにする追加機能という感じで、そのために特にスコープまわりが居心地が悪いものになっている。

筆者はそんな「MPIの気持ち悪さ」をたびたび口にしているんだけれど、いまのところ誰からも共感されたことがない。

本稿はそんな、いまのところ僕だけが気持ち悪いと思っているMPIのスコープ関連の話をしてみる。

サンプル

とりあえずこんなコードを見てほしい。

test.cpp
#include <cstdio>
#include <mpi.h>

int
main(int argc, char **argv) {
  int rank = 0;
  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
  if (0 == rank) {
    int send_value = 12345;
    MPI_Send(&send_value, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);
  } else {
    class Hoge {
    private:
      int private_value;
    public:
      void recv() {
        MPI_Status st;
        MPI_Recv(&private_value, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &st);
      }
      void show() {
        printf("my private value is %d\n", private_value);
      }
    };
    Hoge h;
    h.recv();
    h.show();
  }
  MPI_Finalize();
}

ちょっとごちゃごちゃしているけれど、2プロセスで実行するものと思ってほしい。実行結果はこんな感じになる。

$ mpic++ test.cpp
$ mpirun -np 2 ./a.out 
my private value is 12345

これは、プロセス0番から、プロセス1番に「12345」という整数を一つ送り、プロセス1番がそれを表示する、それだけのコードである。プロセス番号で条件分岐しているので、それぞれバラしてみよう。

まずプロセス0番から見るとこういうコードになっている。

int send_value = 12345;
MPI_Send(&send_value, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);

なんの変哲もないコードである。送る変数を定義して、プロセス1番に向けて整数を一つ送っているだけ。

さて、プロセス1番はこうなっている。

class Hoge {
  private:
    int private_value;
  public:             
    void recv() {       
      MPI_Status st;              
      MPI_Recv(&private_value, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &st);
    }
    void show() {                               
      printf("my private value is %d\n", private_value);
    }                                   
};
Hoge h;                                       
h.recv();                         
h.show();

まず、クラスHogeを定義している。そのHogeの中で、pubicなメンバ関数recvを定義し、そこでMPI通信によりデータを受け取っているのだが、その受け取りもとはHogeのプライベートメンバ変数private_valueである。

しかも、このクラスはプロセス0番では定義すらされない(プロセス0番のスコープにはHogeは定義されていない)。つまり、プロセス0番から送られたデータは、プロセス0番からは見えないクラスの、しかもプライベートメンバ変数に直接叩き込まれていることになる。

なぜこういうことができるかというと、プロセス間通信において、送る側は「送る先のプロセス番号とタグ番号」という、一種グローバル変数を使って送信しており、受け取る側がそれをどう処理するかは受け取り側の問題としているから。

つまり何が言いたいかというと、プロセス番号(rank)とタグ(tag)により通信をするシステムは、C/C++の持つスコープという概念を完全に破壊する1

これ、僕はとても気持ち悪いと思うんだけれど、誰も共感してくれないんですよね〜。

じゃあどうすればいいのさ?

僕は通信において、受信先を指定していないのが気持ち悪さの根源だと思う。

まず、プロセスそれぞれにたいして何かオブジェクトを作って欲しい。例えばこんな感じ。

class Hoge: pubic MPIProcess {
  public:
    int recv;
    int send;
};

int
main(void){
  Hoge hoges[] = MPI_Init(Hoge);
}

MPI_Initは、クラス定義を渡すと、仮想的にプロセスの数だけオブジェクトを作る。ただし、実体を作るのは一つだけ。あとは「別のプロセスが実体を作った」ということを前提に話をすすめる。

そして、例えば0番から1番への通信はこんな感じに書く。

MPI_send(hoges[0]->send, hoges[1]->recv);

この場合はまだプロセス番号というグローバル変数は残っているけれど、少なくとも送信先は、プロセス0番からも「見えて」いないといけない。

ここで、このコードを実行する際、プロセス0番にとっては、hoges[1]というオブジェクトは「知ってはいる」けれど、実体はないことに注意。

で、もっと言えば、MPIのプロセス配置って一次元的だったり、二次元的だったりすることが多いじゃない。なので、それを使うとプロセス番号書かなくていいようになって欲しいよね。例えば12プロセスあるとして、

hoges->reshape(12);

とかすると、一次元的に並んで、

class Hoge: pubic MPIProcess {
  public:
    int recvdata;
    int senddata;
    void hogehoge(){
      send(senddata, right->recvdata);
    }
};

みたいに、「自分の右にあるHogeオブジェクトのrecvdataに、自分のsenddataを送れ」みたいにできるといいね。

後はもちろん

hoges->reshape((3,4));

とかすると、3✗4の二次元的にreshapeされたことになって、left, right, forward, backが使えるようになって、

hoges->reshape((3,2,2));

とかすると、さらにupとdownが使えるようになる、みたいな。

要するに、

  • プロセスごとに、対応する通信用オブジェクトが生成されて欲しい
  • あるプロセスから見ると、通信用オブジェクトは自分だけが実体化されているが、他のプロセスを管理するオブジェクトも「見える」
  • 「見えているオブジェクト」の公開メソッドは呼ぶことができるし、公開メンバにデータを送り込むこともできる。ただし、公開メンバを読み出すことはできない(実体が自分が管理するメモリ上にないから)

みたいな感じでMPIプログラムを組みたいなぁ・・・と。これ、伝わってますかね?

まとめ

rankとtagはグローバル変数であることによる、MPIのスコープ破壊の気持ち悪さをつらつら書いてみました。「自分でラップしろ」って言われたらそれまでなんで、だからどうだってことはないんですけどね。もっとこう、プロセス間通信をライブラリじゃなくて、言語仕様に最初から組み込んだ言語があるといいな、と思うわけです。そうでなくてもプロセス並列はMPI(ライブラリ)、スレッド並列はOpenMP(ディレクティブ)、SIMDベクトル化は組み込みって、全部違うのなんか嫌でしょ。最初からそういうものを言語仕様に取り込んでいて、素直に書けるプログラミング言語が欲しいな、と思う毎日です。


  1. 「破壊する」は言い過ぎかもしれないが、とりあえずスコープを飛び越えていることは間違いない。