MPIでhelloを作成してみる.

2003-08-23T16:00+09:00 matsu
helloを出力する簡単なサンプルをとおしてMPIプログラミングを開始する.
まずサンプル
どの辺がMPI特有なんだ?
MPI関数を呼び出せる範囲
ランク
MPI_SendとMPI_Recv
MPIデータ型
MPI_Comm_size
まとめ

まずサンプル

あんまりグダグダ言っても面白くないので,何はともあれサンプルを示してみる.

 1: #include <stdio.h>
  2: #include <string.h>
  3: #include <stdlib.h>
  4: #include "mpi.h"

  6: #define MSG_LEN 100

  8: int main(int argc, char* argv[]){
  9:   int my_rank;
 10:   int tag = 0;
 11:   char message[MSG_LEN];

 13:   /* ���Ƥ�MPI�ؿ���ƤӽФ����˰������ƤӽФ� */
 14:   MPI_Init(&argc, &argv);

 16:   /* ��ʬ�Υ�󥯤�Ĵ�٤� */
 17:   MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);

 19:   /* �����˽�����ʬ�� */
 20:   /* ���0�ʳ��Υץ������ν��� */
 21:   if(my_rank != 0){
 22:     int dest = 0; /* ������ */

 24:     sprintf(message, "Hello, my process rank is %d", my_rank);

 26:     /* ���0�Υץ������˥�å��������� */
 27:     MPI_Send(message, strlen(message)+1, MPI_CHAR, dest, tag, MPI_COMM_WORLD);
 28:   }
 29:   /* ���0�Υץ������ν��� */
 30:   else{
 31:     int process_num; /* �ץ������� */
 32:     int source;      /* ������ */
 33:     MPI_Status recv_status; /* �������Υ��ơ����� */

 35:     /* �ץ���������Ĵ�٤� */
 36:     MPI_Comm_size(MPI_COMM_WORLD, &process_num);

 38:     for(source = 1; source < process_num; source++){
 39:       /* ���1��process_num�Υץ����������å���������� */
 40:       MPI_Recv(message, MSG_LEN, MPI_CHAR, source, tag, MPI_COMM_WORLD,
 41:                &recv_status);
 42:       fprintf(stdout, "%s\n", message);
 43:     }
 44:   }

 46:   /* MPI�ؿ���ƤӽФ�����,�Ǹ�˰������ƤӽФ� */
 47:   MPI_Finalize();
 48:   exit(EXIT_SUCCESS);
 49: }
このプログラムは
cc hello.c -lmpi
とか
mpicc hello.c
でコンパイルできる.後者はlamやmpichをインストールすると使用できるコマンドで,MPI特有の処理を施しつつccを呼び出す.このプログラムは
mpirun -np 8 a.out
などとして実行できる.上記は-npでプロセス数を8としている.実行結果を以下に示す.
$ mpirun -np 4 a.out 
Hello, my process rank is 1
Greetings from process 2!
Greetings from process 3!
次節以降にサンプルの解説を記す.

どの辺がMPI特有なんだ?

サンプルのMPI特有の部分は”MPI_”で始まる識別子を含む文である.MPI識別子には命名規則がある.MPIの識別子は”MPI_”で始まる.MPI定数はMPI_CHARなどのように”MPI_”の後に大文字が続く.MPI関数はMPI_Initなどのように”MPI_”の後に最初に大文字一文字,その後に小文字が続く.

MPI関数を呼び出せる範囲

全てのMPI関数は
#include "mpi.h"
int MPI_Init(int *argc, char ***argv)
を一回だけ実行した後で呼び出す(※).MPI_Initにはmain関数の引数のポインタを渡す.これによってMPI_Initは起動時のコマンドラインオプションを取得し,MPIのための環境を準備する.実はMPI自体ではコマンドライン引数を指定していないのだが,MPI実装では使用できるオプションのいくつかを以下に示す.
-mpiqueue
MPI_Finalizeが呼び出されたときに,メッセージキューの状態を出力する.デバッグ用である.
-mpiversion
MPI実装のバージョン(MPIのバージョンではない)を出力する.この値はconfigureスクリプトを実行したときに取り込まれた値である.
-mpinice nn
nice値を値nn分増やす.値nnはroot以外は正でなければならない.全てのシステムがこの引数をサポートしているわけではないが,ignoreはしない.
-mpimem(MPICH用)
もしMPICHが-DMPIR_DEBUG_MEMを指定してビルドsれていれば,全てのmallocとfreeをチェックして,メモリ空間に損害を与える兆候がないか見る.
また,MPI関数を呼び出したあとは最後に一回だけ
#include "mpi.h"
int MPI_Finalize()
を実行する.MPI_Initを実行する前に他のMPI関数を呼び出すことはできないし,MPI_Finalizeを実行したあとに他のMPI関数を呼び出すこともできない.
※こうしたMPI関数ヘッダのドキュメントはネット上にいくつかある.また,
apt-get install mpi-doc
すると
/usr/share/doc/mpi-doc/www/index.html 
からも参照できる.

ランク

MPIプログラムの各実行プロセスにはそれぞれランクという値が設定される.SPMD(※)であるMPIプログラムはこのランク毎に処理を分岐させる.プロセスのランクは以下で取得する.
#include "mpi.h"
int MPI_Comm_rank ( MPI_Comm comm, int *rank )
commはコミュニケータという通信集団,すなわち通信しあえるプロセスの集合を指定する.コミュニケータについては次節でもう少し述べる.commはMPI_Comm型の定数を指定する.この定数には以下がある.
MPI_COMM_WORLD
すべてのプロセス
MPI_COMM_SELF
その定数を呼び出しているプロセス
MPI_Comm_rankを呼び出すと第二引数rankにランクが設定される.
※Single Program Multi Data

MPI_SendとMPI_Recv

MPI_Sendはメッセージを送信する関数である.
#include "mpi.h"
int MPI_Send( void *buf, int count, MPI_Datatype datatype, int dest, 
              int tag, MPI_Comm comm )
MPI_Sendのパラメータは全て入力パラメータである.各パラメータの説明を以下に示す.
buf
送信バッファの先頭アドレス.
count
送信バッファの要素数.
datatype
送信バッファの要素の型.
dest
宛先プロセスのランク.
tag
メッセージのタグ.
comm
コミュニケータ.
MPI_Recvはメッセージを受信する関数である.
#include "mpi.h"
int MPI_Recv( void *buf, int count, MPI_Datatype datatype, int source, 
              int tag, MPI_Comm comm, MPI_Status *status )
MPI_Recvの出力パラメータについて以下に示す.
buf
受信バッファの先頭アドレス
status
受信結果のステータス.
MPI_Recvの入力パラメータについて以下に示す.
count
受信バッファのサイズ.これは実際に受信するメッセージよりも大きければ,同じでなくても構わない.足りなければ溢れる.
datatype
受信バッファの要素の型.
source
送信元プロセスのランク.
tag
メッセージのタグ.
comm
コミュニケータ
datatypeには,MPIで定義されたMPIデータ型を指定する.これについては次節で述べる.MPI_SendとMPI_Recvには少々謎めいたパラメータタグ(tag)とコミュニケータ(comm)がある.受信したメッセージはどうすべきだろうか.プリントアウトすべきか,あるいは配列に格納して計算用データとすべきか,あるいは他の処理に必要かもしれない.同じプロセスから受信したデータでもこうした迷いが生じる.そこで,タグ(※)という一つの整数を使用して,メッセージに情報を付加する.受信側はタグの値を参照して,受信メッセージに対してどういった処理を施すべきかを判断できるようになる.また,MPIによる計算ライブラリを使用した場合はどうだろうか.ライブラリでもプロセス間で通信を行ない,ライブラリの呼び出し側でもプロセス間で通信を行なう.このとき,タグだけでは,ライブラリによる通信とライブラリの呼び出し側の通信とを区別できない.ライブラリがタグ0のメッセージに対してなんらかの処理を行なうように実装されていて,ライブラリの呼び出し側でもタグ0に対して別の処理を行なうようにコーディングしていると,動作が保証されない.この問題はコミュニケータの導入によって解決される.ライブラリとその呼び出し側で別々のコミィニケータを設定し,メッセージの送受信時にコミュニケータを指定する.これによって予期しない送信ポイントからのッセージを受信することのないようにすることができる.送信側で指定したコミュニケータが受信側で指定しているコミュニケータと異なれば,メッセージは受信されないので,ライブラリとその呼び出し側では別々のコミュニケータを使用すれば問題は解決される.以上のようにタグとコミュニケータによってメッセージの送受信の論理的空間を広げる(あるいは詳細化する)ことで,受信したメッセージに対する処理の選択,メッセージを送受信するプログラムのモジュールやライブラリの区別が可能になる.
※タグはメッセージタイプとも呼ばれる.

MPIデータ型

MPIデータ型には,C,Fortranそれぞれに対応したものがある.以下にCに対応するMPIデータ型を示す.

MPIデータ型Cの型
MPI_CHARchar
MPI_BYTEunsigned charと同様
MPI_SHORTshort
MPI_INTint
MPI_LONGlong
MPI_FLOATfloat
MPI_DOUBLEdouble
MPI_UNSIGNED_CHARunsigned char
MPI_UNSIGNED_SHORTunsigned short
MPI_UNSIGNEDunsigned int
MPI_UNSIGNED_LONGunsigned long
MPI_LONG_DOUBLElong double(いくつかのシステムでは実装されていないかもしれない)

以下はMPI関数MPI_MAXLOCとMPI_MINLOC(謎)用である.

MPI_FLOAT_INTstruct{float, int}
MPI_LONG_INTstruct{long, int}
MPI_DOUBLE_INTstruct{double, int}
MPI_SHORT_INTstruct{short, int}
MPI_2INTstruct{int, int}
MPI_LONG_DOUBLE_INTstruct{long double, int}(これはオプションで,多分NULLにセットされる)
MPI_LONG_LONG_INTstruct{long long, int}(これはオプションで,多分NULLにセットされる)

以下はC,Fortranの両方で使用できるMPI型である.

MPI_PACKEDMPI関数MPI_PackとMPI_Unpack用
MPI_UB関数MPI_Type_struct用.upper-boundの場合に使用(?)
MPI_LB関数MPI_Type_struct用.lower-boundの場合に使用(?)

MPI_Comm_size

サンプルのランク0のプロセスでは,自分以外の全プロセスからメッセージを受信してそれを出力する.すなわちプロセスの総数を知る必要がある場合がある(総数 - 1回MPI_Recvする必要がある).プロセス数の取得には以下を使用する.
#include "mpi.h"
int MPI_Comm_size ( MPI_Comm comm, int *size )
入力パラメータにcommはコミュニケータである.プロセス数は第二引数sizeに格納される.

まとめ

いきなり細部について書きすぎた感があるが,細部は後で必要に応じて参照することにすれば,MPIプログラミングの概要は掴めたと思う.MPI_Initしてメッセージの送受信を堪能してMPI_Finalize,これが基本である.タグとコミュニケータの応用については,別の記事でも特に取り上げたいと思う.

This article was written by Fujiko