これは多分MPI Advent Calendar 2017の13日目の記事です。なんとなく乗っ取った感じになっていますが、最後まで乗っ取る気は(いまのところ)ありません。
はじめに
MPIプログラムでファイルIOを真面目にやるのはいろいろ面倒です。特に、複数プロセスからファイルを吐きに行く時、性能を出すためには、裏の並列ファイルシステムの特性を理解しなければなりません。また、ステージングとかしているとさらに話がややこしくなります。ここでは、性能を全く気にせずにファイル出力を行ったらどうなるかだけ試してみます。
複数プロセスからのファイル出力
複数プロセスからファイルを吐く時、一番簡単なのはそれぞれのプロセスから別ファイルに吐き出すことです。しかし、シミュレーションとかで時間発展とかしている時に、ステップごとに全てのプロセスがファイルを吐いているとファイル数が膨大になります。そこで、各ステップごとに、各プロセスが持っているデータを一つのファイルにまとめて吐くことを考えます。
一番単純には、rank 0がファイルを作成し、プロセス数だけループを回して毎回バリアし、自分の番になったらファイルに自分のデータを追記していく方法でしょう。ナイーブに実装するならこんな感じでしょうか。
void
write_seq(int rank, int np) {
char filename[256];
static int index = 0;
sprintf(filename, "data%02d.seq", index);
index++;
if (rank == 0) {
FILE *fp = fopen(filename, "w");
fclose(fp);
}
for (int r = 0; r < np; r++) {
MPI_Barrier(MPI_COMM_WORLD);
if (rank != r)continue;
FILE *fp = fopen(filename, "ab");
fwrite((void *)buffer, sizeof(double), N, fp);
fclose(fp);
}
}
各プロセスがbuffer
の内容を同じファイルに順番に追記していくコードです。static int index
があり、呼ばれるたびにインクリメントされるので、ステップごとに異なるファイルを吐きます。
MPI-IO
さて、先程の例では、どのプロセスがファイルのどの位置からどの位置まで書き込むか予めわかっていました。こういう時にはMPI-IOが使えます。MPI-IOは、並列ファイルシステムをMPIの枠組みで抽象化したものです。高速化をよしなにやってくれるわけではなく、あくまでも並列ファイルシステムをうまく使うための仕組みをMPIの枠組みで提供するだけのものですので、高速化関連のチューニングは自分でやる必要があります。
とにかく使ってみましょう。詳細な説明は省略しますが、こんな感じです。
- ファイルハンドルは
MPI_File
で宣言 -
MPI_File_open
ファイルを開く -
MPI_File_set_view
これが何をやってるか僕は知りません -
MPI_File_write_at
指定されたオフセットに内容を書き込む -
MPI_File_close
ファイルを閉じる
先程の、各プロセスが持っているbuffer
をプロセス順に並べて書き込むコードはこんな感じになるでしょう。
int
write_mpi(int rank) {
char filename[256];
static int index = 0;
sprintf(filename, "data%02d.mpi", index);
index++;
MPI_File fh;
MPI_File_open(MPI_COMM_WORLD, filename, MPI_MODE_WRONLY | MPI_MODE_CREATE, MPI_INFO_NULL, &fh);
MPI_File_set_view(fh, 0, MPI_INT, MPI_INT, (char *)"native", MPI_INFO_NULL);
MPI_Status st;
MPI_File_write_at(fh, rank * 2 * N, buffer, N, MPI_DOUBLE, &st);
MPI_File_close(&fh);
return MPI_SUCCESS;
}
ベンチマーク
コード
ベンチマークを取ってみます。各プロセスが10万個の倍精度実数の乱数を一つのファイルに保存する、という処理を100回繰り返すコードです。
#include <cstdio>
#include <random>
#include <algorithm>
#include <time.h>
#include <mpi.h>
//----------------------------------------------------------------------
const int N = 100000;
const int LOOP = 100;
double buffer[N];
std::mt19937 mt;
//----------------------------------------------------------------------
double
myrand(void) {
std::uniform_real_distribution<double> ud(0.0, 1.0);
return ud(mt);
}
//----------------------------------------------------------------------
int
write_mpi(int rank) {
char filename[256];
static int index = 0;
sprintf(filename, "data%02d.mpi", index);
index++;
MPI_File fh;
MPI_File_open(MPI_COMM_WORLD, filename, MPI_MODE_WRONLY | MPI_MODE_CREATE, MPI_INFO_NULL, &fh);
MPI_File_set_view(fh, 0, MPI_INT, MPI_INT, (char *)"native", MPI_INFO_NULL);
MPI_Status st;
MPI_File_write_at(fh, rank * 2 * N, buffer, N, MPI_DOUBLE, &st);
MPI_File_close(&fh);
return MPI_SUCCESS;
}
//----------------------------------------------------------------------
void
write_seq(int rank, int np) {
char filename[256];
static int index = 0;
sprintf(filename, "data%02d.seq", index);
index++;
if (rank == 0) {
FILE *fp = fopen(filename, "w");
fclose(fp);
}
for (int r = 0; r < np; r++) {
MPI_Barrier(MPI_COMM_WORLD);
if (rank != r)continue;
FILE *fp = fopen(filename, "ab");
fwrite((void *)buffer, sizeof(double), N, fp);
fclose(fp);
}
}
//----------------------------------------------------------------------
void
makebuf(void) {
for (int i = 0; i < N; i++) {
buffer[i] = myrand();
}
}
//----------------------------------------------------------------------
int
main(int argc, char **argv) {
int rank, np;
double t1, t2;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &np);
mt.seed(rank);
MPI_Barrier(MPI_COMM_WORLD);
t1 = MPI_Wtime();
for (int i = 0; i < LOOP; i++) {
makebuf();
write_mpi(rank);
}
MPI_Barrier(MPI_COMM_WORLD);
t2 = MPI_Wtime();
double time_mpi = t2 - t1;
mt.seed(rank);
MPI_Barrier(MPI_COMM_WORLD);
t1 = MPI_Wtime();
for (int i = 0; i < LOOP; i++) {
makebuf();
write_seq(rank, np);
}
MPI_Barrier(MPI_COMM_WORLD);
t2 = MPI_Wtime();
double time_seq = t2 - t1;
if (0 == rank) {
printf("%f #MPI\n", time_mpi);
printf("%f #SEQ\n", time_seq);
}
MPI_Finalize();
}
//----------------------------------------------------------------------
実行環境
- Intel(R) Xeon(R) CPU E5-2680 v3 @ 2.50GHz
- 1ソケット12コア、1ノード2ソケット、合計24コア/ノード
- 1ノード24プロセス実行
- ファイルシステムはLustre
- SGI MPT 2.14
- Intel MPI 2018.1.163
実行結果
- SGI MPT
$ mpirun -np 24 ./a.out
4.810011 #MPI
2.725134 #SEQ
$ for file in *.mpi; do diff $file ${file/mpi/seq};done
- Intel MPI
$ mpirun -np 24 ./a.out
4.748924 #MPI
2.741026 #SEQ
$ for file in *.mpi; do diff $file ${file/mpi/seq};done
それぞれdata00.mpi, data00.seq
...data99.mpi, data99.seq
というファイルができて、拡張子がmpi
なのはMPI-IOを使ったもの、seq
はナイーブに実装したものです。diffをとって同じステップのファイルがMPI-IOとシーケンシャル番で同じ内容であることを確認しています。
で、1ノードから出力しているのでMPI-IOは遅いだろうなと思っていましたが、やっぱり遅いですね。
まとめ
MPI-IOを使ってみました。1ノードで、複数プロセスから同じファイルにシーケンシャルにファイルを出力した場合と、MPI-IOで一度に書き込みにいった場合を比較して、MPI-IOを使ったほうが倍近く遅いことがわかりました。高並列時にちゃんと使えば早くなるのでしょうが1、様々な資料を見る限り、かなりのノウハウが必要そうな気がします。なによりサイト依存が激しそうです。
とりあえず頭を使わずナイーブに吐いちゃって、それで遅くて仕方なかったら、吐くデータ量や吐く頻度を削減できないか考えた方がいろいろ良い気がします。
参考
- MPI-IO - 理化学研究所 計算科学研究機構 理研の堀敦史さんの講義資料
- MPI_File_write_at MPICHのドキュメント。基本的にMPICHのドキュメント見ながら適当に書いたので、コードが正しい自信があまりない2。