はじめに
これはなんとなくMPI Advent Calendar 2017の8日目の記事ということにしました。別の場所に書いた内容の転載です。
MPIプロセスをgdbでデバッグする方針
MPIは複数プロセスが立ち上がるが、gdbは一度に一つのプロセスにしかアタッチできないため、なんらかの工夫が必要になる。Open MPIのFAQ: Debugging applications in parallelには、MPIプログラムをgdbでデバッグする方法として、
- 起動されたすべてのプロセスについてgdbをアタッチする
- 実行中の特定のプロセス一つだけにgdbをアタッチする
の二つの方法が紹介されているが、本稿では二番目の方法について紹介する。
方針
gdbは、プロセスIDを使って起動中のプロセスにアタッチする機能がある。そこで、まずMPIプログラムを実行し、その後で
gdbで特定のプロセスにアタッチする。しかし、gdbでアタッチするまで、MPIプログラムには特定の場所で待っていてほしい。
というわけで、
- 故意に無限ループに陥るコードを書いておく
- MPIプログラムを実行する
- gdbで特定のプロセスにアタッチする
- gdbで変数をいじって無限ループを脱出させる
- あとは好きなようにデバッグする
という方針を採用する。
動作例
こんなコードを書く。
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <mpi.h>
int main(int argc, char **argv) {
MPI_Init(&argc, &argv);
int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
printf("Rank %d: PID %d\n", rank, getpid());
fflush(stdout);
int i = 0;
int sum = 0;
while (i == rank) {
sleep(1);
}
MPI_Allreduce(&rank, &sum, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
printf("%d\n", sum);
MPI_Finalize();
}
このコードは、自分のPIDを出力してから、ランク0番のプロセスだけ無限ループに陥る。
このコードを-g
つきでコンパイルし、とりあえず4プロセスで実行してみよう。
$ mpic++ -g gdb_mpi.cpp
$ mpirun -np 4 ./a.out
Rank 2: PID 3646
Rank 0: PID 3644
Rank 1: PID 3645
Rank 3: PID 3647
4プロセス起動して、そこでランク0番だけ無限ループしているので、他のプロセスが待ちの状態になる。この状態でランク0番にアタッチしよう。もう一枚端末を開いてgdbを起動、ランク0のPID(実行の度に異なるが、今回は3644)にアタッチする。
$ gdb
(gdb) attach 3644
Attaching to process 3644
Reading symbols from /path/to/a.out...done.
(snip)
(gdb)
この状態で、バックトレースを表示してみる。
(gdb) bt
#0 0x00007fc229e2156d in nanosleep () from /lib64/libc.so.6
#1 0x00007fc229e21404 in sleep () from /lib64/libc.so.6
#2 0x0000000000400a04 in main (argc=1, argv=0x7ffe6cfd0d88) at gdb_mpi.cpp:15
sleep状態にあるので、main
関数からsleep
が、sleep
からnanosleep
が呼ばれていることがわかる。
ここからmain
に戻ろう。finish
を二回入力する。
(gdb) finish
Run till exit from #0 0x00007fc229e2156d in nanosleep () from /lib64/libc.so.6
0x00007fc229e21404 in sleep () from /lib64/libc.so.6
(gdb) finish
Run till exit from #0 0x00007fc229e21404 in sleep () from /lib64/libc.so.6
main (argc=1, argv=0x7ffe6cfd0d88) at gdb_mpi.cpp:14
14 while (i == rank) {
main
関数まで戻ってきた。この後、各ランク番号rank
の総和を、変数sum
に入力するので、sum
にウォッチポイントを設定しよう。
(gdb) watch sum
Hardware watchpoint 1: sum
現在は変数i
の値が0
で、このままでは無限ループするので、変数の値を書き換えてから続行(continue)してやる。
(gdb) set var i = 1
(gdb) c
Continuing.
Hardware watchpoint 1: sum
Old value = 0
New value = 1
0x00007fc229eaa676 in __memcpy_ssse3 () from /lib64/libc.so.6
ウォッチポイントにひっかかった。この状態でバックトレースを表示してみよう。
(gdb) bt
#0 0x00007fc229eaa676 in __memcpy_ssse3 () from /lib64/libc.so.6
#1 0x00007fc229820185 in opal_convertor_unpack ()
from /opt/openmpi-2.1.1_gcc-4.8.5/lib/libopen-pal.so.20
#2 0x00007fc21e9afbdf in mca_pml_ob1_recv_frag_callback_match ()
from /opt/openmpi-2.1.1_gcc-4.8.5/lib/openmpi/mca_pml_ob1.so
#3 0x00007fc21edca942 in mca_btl_vader_poll_handle_frag ()
from /opt/openmpi-2.1.1_gcc-4.8.5/lib/openmpi/mca_btl_vader.so
#4 0x00007fc21edcaba7 in mca_btl_vader_component_progress ()
from /opt/openmpi-2.1.1_gcc-4.8.5/lib/openmpi/mca_btl_vader.so
#5 0x00007fc229810b6c in opal_progress ()
from /opt/openmpi-2.1.1_gcc-4.8.5/lib/libopen-pal.so.20
#6 0x00007fc22ac244b5 in ompi_request_default_wait_all ()
from /opt/openmpi-2.1.1_gcc-4.8.5/lib/libmpi.so.20
#7 0x00007fc22ac68955 in ompi_coll_base_allreduce_intra_recursivedoubling ()
from /opt/openmpi-2.1.1_gcc-4.8.5/lib/libmpi.so.20
#8 0x00007fc22ac34633 in PMPI_Allreduce ()
from /opt/openmpi-2.1.1_gcc-4.8.5/lib/libmpi.so.20
#9 0x0000000000400a2c in main (argc=1, argv=0x7ffe6cfd0d88) at gdb_mpi.cpp:17
ごちゃごちゃっと関数呼び出しが連なってくる。MPIは規格であり、様々な実装があるが、今表示されているのはOpen MPIの実装である。内部でompi_coll_base_allreduce_intra_recursivedoubling
とか、それっぽい関数が呼ばれていることがわかるであろう。興味のある人は、OpenMPIのソースをダウンロードして、上記と突き合わせてみると楽しいかもしれない。
さて、続行してみよう。二回continueするとプログラムが終了する。
(gdb) c
Continuing.
Hardware watchpoint 1: sum
Old value = 1
New value = 6
0x00007fc229eaa676 in __memcpy_ssse3 () from /lib64/libc.so.6
(gdb) c
Continuing.
[Thread 0x7fc227481700 (LWP 3648) exited]
[Thread 0x7fc226c80700 (LWP 3649) exited]
Watchpoint 1 deleted because the program has left the block in
which its expression is valid.
0x00007fc229d7e445 in __libc_start_main () from /lib64/libc.so.6
mpirun
を実行していた端末も、以下のような表示をして終了するはずである。
$ mpic++ -g gdb_mpi.cpp
$ mpirun -np 4 ./a.out
Rank 2: PID 3646
Rank 0: PID 3644
Rank 1: PID 3645
Rank 3: PID 3647
6
6
6
6
まとめ
gdbで起動中のMPIプロセスの一つにアタッチして、デバッグする方法を紹介した。故意に無限ループを作っておき、gdbでその無限ループを解消するのがミソである。これでデバッグしたい場所の直前からgdbで好きないようにデバッグできる。ただし、私の経験では、並列プログラミングにおいてgdbを使ったデバッグは最終手段であり、できることならそうなる前にバグを潰しておきたい。できるだけ細かくきちんとテストを書いていって、そもそもバグが入らないようにしていくことが望ましい。