GDBによるMPIプログラムのデバッグ

はじめに

これはなんとなく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を使ったデバッグは最終手段であり、できることならそうなる前にバグを潰しておきたい。できるだけ細かくきちんとテストを書いていって、そもそもバグが入らないようにしていくことが望ましい。