原文请猛戳:http://galoisplusplus.coding.me/blog/2013/06/08/mpi-debug-tips/html
debug一个并行程序(parallel program)向来是件很麻烦的事情(Erlang
等functional programming language另当别论),
对于像MPI这种非shared memory的inter-process model来讲尤为如此。node
目前我所了解的商业调试器(debugger)有:linux
TotalViewshell
Allinea DDTbash
听说parallel debug的能力很屌,
本人没用过表示不知,说不定只是界面作得好看而已。
不过我想大部分人应该跟本屌同样是用不起这些商业产品的,高富帅们请无视。
如下我介绍下一些有用的open source工具:架构
首先推荐valgrind
的memcheck
。
大部分MPI标准的实现(implementation)(如openmpi、mpich)支持的是C、C++和Fortran语言。
Fortran语言我不了解,但C和C++以复杂的内存管理(memory management)见长但是出了名的XD。
有些时候所谓的MPI程序的bug,不过是通常sequential程序常见的内存错误罢了。
这个时候用memcheck检查就能够很容易找到bug的藏身之处。
你可能会争论说你用了RAII(Resource Allocation Is Initialization)等方式来管理内存,
不会有那些naive的问题,
但我仍是建议你使用memcheck检查你程序的可执行文件,
由于memcheck除了检查内存错误,
还能够检查message passing相关的错误,
例如:MPI_Send一块没有彻底初始化的buffer、
用来发送消息的buffer大小小于MPI_Send所指定的大小、
用来接受消息的buffer大小小于MPI_Recv所指定的大小等等,我想你的那些方法应该对这些无论用吧?。app
这里假设你已经安装并配置好了memcheck,例如若是你用的是openmpi,那么执行如下命令函数
ompi_info | grep memchecker
会获得相似工具
MCA memchecker: valgrind (MCA v2.0, API v2.0, Component v1.6.4)
的结果。
不然请参照Valgrind User Manual 4.9. Debugging MPI Parallel Programs with Valgrind进行配置。测试
使用memcheck须要在compile时下-g
参数。
运行memcheck用下面的命令:
mpirun [mpirun-args] valgrind [valgrind-args] <application> [app-args]
<!-- more -->
padb实际上是个job monitor,它能够显示MPI message queue的情况。
推荐padb的一大理由是它能够检查deadlock。
假设你没有parallel debugger,不用担忧,咱们还有gdb这种serial debugger大杀器。
首先说说mpirun/mpiexec/orterun所支持的打开gdb的方式。
openmpi支持:
mpirun [mpirun-args] xterm -e gdb <application>
执行这个命令会打开跟所指定的进程数目同样多的终端——一会儿蹦出这么多终端,神烦~——每一个终端都跑有gdb。
我试过这个方式,它不支持application带有参数的[app-args]状况,
并且进程跑在不一样机器上也没法正常跑起来——这一点openmpi的FAQ已经有比较复杂的解决方案。
mpich2支持:
mpirun -gdb <application>
但在mpich较新的版本中,该package的进程管理器(process manager)已经从MPD换为Hydra,这个-gdb
的选项随之消失。
详情请猛戳这个连接(http://trac.mpich.org/projects/mpich/ticket/1150)。
像我机器上的mpich版本是3.0.3,因此这个选项也就不能用了。
若是你想试试能够用包含MPD的旧版mpich。
好,如下假设咱们不用上述方式,只是像debug通常的程序同样,打开gdb,attach到相应进程,完事,detach,退出。
<!--- 使用gdb来debugMPI程序 --->
如今咱们要面对的一大问题实际上是怎么让MPI程序暂停下来。
由于绝大多数MPI程序其实执行得很是快——写并行程序的一大目的不就是加速么——不少时候来不及打开gdb,MPI程序就已经执行完了。
因此咱们须要让它先缓下来等待咱们打开gdb执行操做。
目前比较靠谱的方法是在MPI程序里加hook,这个方法我是在UCDavis的Professor Matloff的主页上看到的(猛戳这里:http://heather.cs.ucdavis.edu/~matloff/pardebug.html)。
不过我喜欢的方式跟Prof.Matloff所讲的稍有不一样:
#ifdef MPI_DEBUG int gdb_break = 1; while(gdb_break) {}; #endif
Prof. Matloff的方法没有一个相似MPI_DEBUG
的macro。
我加这个macro只是耍下小聪明,让程序能够经过不一样的编译方式生成debug模式和正常模式的可执行文件。
若是要生成debug模式的可执行文件,只需在编译时加入如下参数:
-DMPI_DEBUG
或
-DMPI_DEBUG=define
若是不加以上参数就是生成正常模式的可执行文件了,不会再有debug模式的反作用(例如在这里是陷入无限循环)。
不用这个macro的话,要生成正常模式的可执行文件还得回头改源代码,
这样一者可能代码很长,致使很难找到这个hook的位置;
两者若是你在「测试-发布-测试-...」的开发周期里,debug模式所加的代码常常要「加入-删掉-加入-...」非常蛋疼。
(
什么?你犯二了,在源代码中加了一句
#define MPI_DEBUG
好吧,你也能够不改动这一句,只需在编译时加入
-UMPI_DEBUG
就能够生成正常模式的可执行文件。
)
这样只需照常运行,MPI程序就会在while循环的地方卡住。
这时候打开gdb,执行
(gdb) shell ps aux | grep <process-name>
找到全部对应进程的pid,再用
(gdb) attach <pid>
attach到其中某一个进程。
Prof. Matloff用的是
gdb <process-name> <pid>
这也是能够的。
但我习惯的是开一个gdb,要跳转到别的进程就用detach
再attach
。
让MPI程序跳出while循环:
(gdb) set gdb_break = 0
如今就能够随行所欲的执行设breakpoint啊、查看register啊、print变量啊等操做了。
我猜你会这么吐嘈这种方法:每一个process都要set一遍来跳出无限循环,神烦啊有木有!
是的,你没有必要每一个process都加,能够只针对有表明性的process加上(例如你用到master-slave的架构那么就挑个master跟slave呗~)。
神马?「表明」很难选?!
咱们能够把while循环改为:
while(gdb_break) { // set the sleep time to pause the processes sleep(<time>); }
这样在<time>时间内打开gdb设好breakpoint便可,过了这段时间process就不会卡在while循环的地方。
神马?这个时间很难取?取短了来不及,取长了又猴急?
好吧你赢了......
相似的作法也被PKU的Jinlong Wu (King)博士写的调试并行程序说起到了。
他用的是:
setenv INITIAL_SLEEP_TIME 10 mpirun [mpirun-args] -x INITIAL_SLEEP_TIME <application> [app-args]
本人没有试过,不过看起来比改源代码的方法要优秀些XD。
假设你在打开gdb后会发现no debugging symbols found
,
这是由于你的MPI可执行程序没有用于debug的symbol。
正常状况下,你在compile时下-g
参数,
生成的可执行程序(例如在linux下是ELF格式,ELF可不是「精灵」,而是Executable and Linkable Format)中会加入DWARF(DWARF是对应于「精灵」的「矮人」Debugging With Attributed Record Format)信息。
若是你编译时加了-g
参数后仍然有一样的问题,我想那应该是你运行MPI的环境有些库没装上的缘故。
在这样的环境下,若是你不幸踩到了segmentation fault的雷区,想要debug,
但是上面的招数失效了,坑爹啊......
好在天无绝人之路,只要有程序运行的错误信息(有core dump更好),
依靠一些汇编(assmebly)语言的常识仍是能够帮助你debug的。
这里就简单以我碰到的一个悲剧为例吧,
BTW为了找到bug,我在编译时没有加优化参数。
如下是运行时吐出的一堆错误信息(555好长好长的):
$ mpirun -np 2 ./mandelbrot_mpi_static 10 -2 2 -2 2 100 100 disable [PP01:13214] *** Process received signal *** [PP01:13215] *** Process received signal *** [PP01:13215] Signal: Segmentation fault (11) [PP01:13215] Signal code: Address not mapped (1) [PP01:13215] Failing at address: 0x1123000 [PP01:13214] Signal: Segmentation fault (11) [PP01:13214] Signal code: Address not mapped (1) [PP01:13214] Failing at address: 0xbf7000 [PP01:13214] [ 0] /lib64/libpthread.so.0(+0xf500) [0x7f6917014500] [PP01:13215] [ 0] /lib64/libpthread.so.0(+0xf500) [0x7f41a45d9500] [PP01:13215] [ 1] /lib64/libc.so.6(memcpy+0x15b) [0x7f41a42c0bfb] [PP01:13215] [ 2] /opt/OPENMPI-1.4.4/lib/libmpi.so.0 (ompi_convertor_pack+0x14a) [0x7f41a557325a] [PP01:13215] [ 3] /opt/OPENMPI-1.4.4/lib/openmpi/mca_btl_sm.so (+0x1ccd) [0x7f41a1189ccd] [PP01:13215] [ 4] /opt/OPENMPI-1.4.4/lib/openmpi/mca_pml_ob1.so (+0xc51b) [0x7f41a19a651b] [PP01:13215] [ 5] /opt/OPENMPI-1.4.4/lib/openmpi/mca_pml_ob1.so (+0x7dd8) [0x7f41a19a1dd8] [PP01:13215] [ 6] /opt/OPENMPI-1.4.4/lib/openmpi/mca_btl_sm.so (+0x4078) [0x7f41a118c078] [PP01:13215] [ 7] /opt/OPENMPI-1.4.4/lib/libopen-pal.so.0 (opal_progress+0x5a) [0x7f41a509be8a] [PP01:13215] [ 8] /opt/OPENMPI-1.4.4/lib/openmpi/mca_pml_ob1.so (+0x552d) [0x7f41a199f52d] [PP01:13215] [ 9] /opt/OPENMPI-1.4.4/lib/openmpi/mca_coll_sync.so (+0x1742) [0x7f41a02e3742] [PP01:13215] [10] /opt/OPENMPI-1.4.4/lib/libmpi.so.0 (MPI_Gatherv+0x116) [0x7f41a5580906] [PP01:13215] [11] ./mandelbrot_mpi_static(main+0x68c) [0x401b16] [PP01:13215] [12] /lib64/libc.so.6(__libc_start_main+0xfd) [0x7f41a4256cdd] [PP01:13215] [13] ./mandelbrot_mpi_static() [0x4010c9] [PP01:13215] *** End of error message *** [PP01:13214] [ 1] /lib64/libc.so.6(memcpy+0x15b) [0x7f6916cfbbfb] [PP01:13214] [ 2] /opt/OPENMPI-1.4.4/lib/libmpi.so.0 (ompi_convertor_unpack+0xca) [0x7f6917fae04a] [PP01:13214] [ 3] /opt/OPENMPI-1.4.4/lib/openmpi/mca_pml_ob1.so (+0x9621) [0x7f69143de621] [PP01:13214] [ 4] /opt/OPENMPI-1.4.4/lib/openmpi/mca_btl_sm.so (+0x4078) [0x7f6913bc7078] [PP01:13214] [ 5] /opt/OPENMPI-1.4.4/lib/libopen-pal.so.0 (opal_progress+0x5a) [0x7f6917ad6e8a] [PP01:13214] [ 6] /opt/OPENMPI-1.4.4/lib/openmpi/mca_pml_ob1.so (+0x48b5) [0x7f69143d98b5] [PP01:13214] [ 7] /opt/OPENMPI-1.4.4/lib/openmpi/mca_coll_basic.so (+0x3a94) [0x7f6913732a94] [PP01:13214] [ 8] /opt/OPENMPI-1.4.4/lib/openmpi/mca_coll_sync.so (+0x1742) [0x7f6912d1e742] [PP01:13214] [ 9] /opt/OPENMPI-1.4.4/lib/libmpi.so.0 (MPI_Gatherv+0x116) [0x7f6917fbb906] [PP01:13214] [10] ./mandelbrot_mpi_static(main+0x68c) [0x401b16] [PP01:13214] [11] /lib64/libc.so.6(__libc_start_main+0xfd) [0x7f6916c91cdd] [PP01:13214] [12] ./mandelbrot_mpi_static() [0x4010c9] [PP01:13214] *** End of error message *** -------------------------------------------------------------------------- mpirun noticed that process rank 1 with PID 13215 on node PP01 exited on signal 11 (Segmentation fault). --------------------------------------------------------------------------
注意到这一行:
[PP01:13215] [10] /opt/OPENMPI-1.4.4/lib/libmpi.so.0 (MPI_Gatherv+0x116) [0x7f41a5580906]
经过(这跟在gdb中用disas指令是同样的)
objdump -D /opt/OPENMPI-1.4.4/lib/libmpi.so.0
找到MPI_Gatherv的入口:
00000000000527f0 <PMPI_Gatherv>:
找到(MPI_Gatherv+0x116)的位置(地址52906):
52906: 83 f8 00 cmp $0x0,%eax 52909: 74 26 je 52931 <PMPI_Gatherv+0x141> 5290b: 0f 8c 37 02 00 00 jl 52b48 <PMPI_Gatherv+0x358>
地址为52931的<PMPI_Gatherv+0x141>以后的code主要是return,%eax应该是判断是否要return的counter。
如今寄存器%eax就成了最大的嫌疑,有理由 相信 猜某个对该寄存器的不正确操做致使了segmentation fault。好吧,其实debug不少时候还得靠猜,
记得有这么个段子:
「师爷,写代码最重要的是什么?」
「淡定。」
「师爷,调试程序最重要的是什么?」
「运气。」
接下来找到了%eax被赋值的地方:
52ac2: 41 8b 00 mov (%r8),%eax
这里须要了解函数参数传递(function parameter passing)的调用约定(calling convention)机制:
对x64来讲:int和pointer类型的参数依次放在rdi
、rsi
、rdx
、rcx
、r8
、r9
寄存器中,float参数放在xmm
开头的寄存器中。
对x86(32bit)来讲:参数放在堆栈(stack)中。
此外GNU C支持:
__attribute__((regparm(<number>)))
其中<number>是一个0到3的整数,表示指定<number>个参数经过寄存器传递,因为寄存器传参要比堆栈传参快,于是这也被称为#fastcall#。
若是指定
__attribute__((regparm(3)))
则开头的三个参数会被依次放在eax
、edx
和ecx
中。
(关于__attribute__
的详细介绍请猛戳GCC的官方文档)。
若是是C++的member function,别忘了隐含的第一个参数实际上是object的this
指针(pointer)。
回到咱们的例子,
%r8正对应MPI_Gatherv的第五個参数。
如今终于能够从底层的汇编语言解脱出来了,让咱们一睹MPI_Gatherv原型的尊容:
int MPI_Gatherv(void *sendbuf, int sendcnt, MPI_Datatype sendtype, void *recvbuf, int *recvcnts, int *displs, MPI_Datatype recvtype, int root, MPI_Comm comm)
第五个参数是recvcnts
,因而就能够针对这个「罪魁祸首」去看源程序到底出了什么问题了。
这里我就不贴出代码了,
bug的来源就是我当时犯二了,觉得这个recvcnts
是byte number,而实际上官方文档写得明白(这里的recvcounts
就是recvcnts
):
recvcounts integer array (of length group size) containing the number of elements that are received from each process (significant only at root)
实际上是the number of elements
啊有木有!不仔细看文档的真心伤不起!
也由于这个错误,使个人recvcnts
比recvbuf
的size要大,于是发生了access在recvbuf
范围之外的内存的状况(也就是咱们从错误信息所看到的Address not mapped
)。
最后再提一点,我源代码中的recvbuf
实际上是malloc出来的内存,也就是在heap中,这种状况其实用valgrind
应该就能够检测出来(若是recvbuf
在stack中我可不能保证这一点)。因此,骚念们,编译完MPI程式先跑跑valgrind
看能不能通关吧,更重要的是,写代码要仔细看API文档减小bug。
1][Open MPI FAQ: Debugging applications in parallel
3][Valgrind User Manual 4. Memcheck: a memory error detector
4][stackoverflow: How do I debug an MPI program?
5][Hints for Debugging Parallel Programs