3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

はじめに

これはなんとなく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で変数をいじって無限ループを脱出させる
  • あとは好きなようにデバッグする

という方針を採用する。

動作例

こんなコードを書く。

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

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?