MPI Debug Tips

debug一个并行程序(parallel program)向来是件很麻烦的事情(Erlang等functional
programming language另当别论), 对于像MPI这种非shared memory的inter-process model来说尤其如此。

与调试并行程序相关的工具


非开源工具

目前我所了解的商业调试器(debugger)有:

据说parallel debug的能力很屌, 本人没用过表示不知,说不定只是界面做得好看而已
不过我想大部分人应该跟本屌一样是用不起这些商业产品的,高富帅们请无视。 以下我介绍下一些有用的open source工具:

开源工具


- Valgrind Memcheck

首先推荐valgrindmemcheck
大部分MPI标准的实现(implementation)(如openmpimpich)支持的是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所指定的大小等等,我想你的那些方法应该对这些不管用吧?

这里假设你已经安装并配置好了memcheck,例如如果你用的是openmpi,那么执行以下命令






1
ompi_info | grep memchecker

会得到类似






1
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用下面的命令:






1
mpirun [mpirun-args] valgrind [valgrind-args] <application> [app-args]


- Parallel Application Debugger

padb其实是个job monitor,它可以显示MPI message queue的状况。 推荐padb的一大理由是它可以检查deadlock。

使用gdb

假设你没有parallel debugger,不用担心,我们还有gdb这种serial debugger大杀器。

首先说说mpirun/mpiexec/orterun所支持的打开gdb的方式。

openmpi支持:






1
mpirun [mpirun-args] xterm -e gdb <application>

执行这个命令会打开跟所指定的进程数目一样多的终端——一下子蹦出这么多终端,神烦~——每个终端都跑有gdb。
我试过这个方式,它不支持application带有参数的[app-args]情况, 而且进程跑在不同机器上也无法正常跑起来——这一点openmpi的FAQ已经有比较复杂的解决方案。

mpich2支持:






1
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,退出。

现在我们要面对的一大问题其实是怎么让MPI程序暂停下来。
因为绝大多数MPI程序其实执行得非常快——写并行程序的一大目的不就是加速么——很多时候来不及打开gdb,MPI程序就已经执行完了。
所以我们需要让它先缓下来等待我们打开gdb执行操作。

目前比较靠谱的方法是在MPI程序里加hook,这个方法我是在UCDavis的Professor Matloff的主页上看到的(猛戳这里:http://heather.cs.ucdavis.edu/~matloff/pardebug.html)。
不过我喜欢的方式跟Prof.Matloff所讲的稍有不同:






1
2
3
4
#ifdef MPI_DEBUG
int gdb_break = 1;
while(gdb_break) {};
#endif

Prof. Matloff的方法没有一个类似MPI_DEBUG的macro。
我加这个macro只是耍下小聪明,让程序可以通过不同的编译方式生成debug模式和正常模式的可执行文件。
如果要生成debug模式的可执行文件,只需在编译时加入以下参数:






1
-DMPI_DEBUG






1
-DMPI_DEBUG=define

如果不加以上参数就是生成正常模式的可执行文件了,不会再有debug模式的副作用(例如在这里是陷入无限循环)。
不用这个macro的话,要生成正常模式的可执行文件还得回头改源代码, 这样一者可能代码很长,导致很难找到这个hook的位置;
二者如果你在「测试-发布-测试-…」的开发周期里,debug模式所加的代码经常要「加入-删掉-加入-…」很是蛋疼。

( 什么?你犯二了,在源代码中加了一句






1
#define MPI_DEBUG

好吧,你也可以不改动这一句,只需在编译时加入






1
-UMPI_DEBUG

就可以生成正常模式的可执行文件。 )

这样只需照常运行,MPI程序就会在while循环的地方卡住。 这时候打开gdb,执行






1
(gdb) shell ps aux | grep <process-name>

找到所有对应进程的pid,再用






1
(gdb) attach <pid>

attach到其中某一个进程。

Prof. Matloff用的是






1
gdb <process-name> <pid>

这也是可以的。 但我习惯的是开一个gdb,要跳转到别的进程就用detachattach

让MPI程序跳出while循环:






1
(gdb) set gdb_break = 0

现在就可以随行所欲的执行设breakpoint啊、查看register啊、print变量啊等操作了。

我猜你会这么吐嘈这种方法:每个process都要set一遍来跳出无限循环,神烦啊有木有!
是的,你没有必要每个process都加,可以只针对有代表性的process加上(例如你用到master-slave的架构那么就挑个master跟slave呗~)。

神马?「代表」很难选?! 我们可以把while循环改成:






1
2
3
4
5
while(gdb_break)
{
// set the sleep time to pause the processes
sleep(<time>);
}

这样在时间内打开gdb设好breakpoint即可,过了这段时间process就不会卡在while循环的地方。

神马?这个时间很难取?取短了来不及,取长了又猴急? 好吧你赢了……

类似的做法也被PKU的Jinlong Wu (King)博士写的调试并行程序提及到了。
他用的是:






1
2
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好长好长的):






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$ 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).
--------------------------------------------------------------------------

注意到这一行:






1
2
[PP01:13215] [10] /opt/OPENMPI-1.4.4/lib/libmpi.so.0
(MPI_Gatherv+0x116) [0x7f41a5580906]

通过(这跟在gdb中用disas指令是一样的)






1
objdump -D /opt/OPENMPI-1.4.4/lib/libmpi.so.0

找到MPI_Gatherv的入口:






1
00000000000527f0 <PMPI_Gatherv>:

找到(MPI_Gatherv+0x116)的位置(地址52906):






1
2
3
   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被赋值的地方:






1
   52ac2:       41 8b 00                mov    (%r8),%eax

这里需要了解函数参数传递(function parameter passing)的调用约定(calling convention)机制:

  • 对x64来说:int和pointer类型的参数依次放在rdirsirdxrcxr8r9寄存器中,float参数放在xmm开头的寄存器中。

  • 对x86(32bit)来说:参数放在堆栈(stack)中。 此外GNU C支持:






1
__attribute__((regparm(<number>)))

其中是一个0到3的整数,表示指定个参数通过寄存器传递,由于寄存器传参要比堆栈传参快,因而这也被称为#fastcall#。
如果指定






1
__attribute__((regparm(3)))

则开头的三个参数会被依次放在eaxedxecx中。
(关于__attribute__的详细介绍请猛戳GCC的官方文档)。

  • 如果是C++的member
    function,别忘了隐含的第一个参数其实是object的this指针(pointer)。

回到我们的例子,%r8正对应MPI_Gatherv的第五個参数。
现在终于可以从底层的汇编语言解脱出来了,让我们一睹MPI_Gatherv原型的尊容:






1
2
3
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):






1
2
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啊有木有!不仔细看文档的真心伤不起!
也因为这个错误,使我的recvcntsrecvbuf的size要大,因而发生了access在recvbuf范围以外的内存的情况(也就是我们从错误信息所看到的Address
not mapped
)。

最后再提一点,我源代码中的recvbuf其实是malloc出来的内存,也就是在heap中,这种情况其实用valgrind应该就可以检测出来(如果recvbuf在stack中我可不能保证这一点)。所以,骚念们,编译完MPI程式先跑跑valgrind看能不能通关吧,更重要的是,写代码要仔细看API文档减少bug。

参考资料

[1]Open MPI FAQ:
Debugging applications in parallel

[2]Using Valgrind’s
Memcheck Tool to Find Memory Errors and Leaks in MPI and Serial Applications on
Linux

[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

[6]Compiling
and Running with MPICH2 and the gdb Debugger

[7]调试并行程序

MPI Debug Tips,布布扣,bubuko.com

时间: 2024-11-05 18:53:31

MPI Debug Tips的相关文章

Gradle学习目录总结

如果是你想干的事情,在别人看起来可能是很难的一件事,不过你自己很喜欢,你不会觉得很苦.我开始创业那会是28岁.对我来讲,我创业的目的不是为了自己当老板,我希望有一个平台有一个环境,我可以控制一些资源,让我去创造一个新的产品和服务: -- 周鸿祎 Gradle是一种依赖管理工具,基于Groovy语言,面向Java应用为主,它抛弃了基于XML的各种繁琐配置,取而代之的是一种基于Groovy的领域特定(DSL)语言. 当然,我们现在最多都是在Android Studio的项目中,和我一样没有接触过的就

usbip install

# README for usbip-utils## Copyright (C) 2011 matt mooney <[email protected]>#               2005-2008 Takahiro Hirofuchi [Requirements]    - USB/IP device drivers    Found in the staging directory of the Linux kernel. - sysfsutils >= 2.0.0    sy

iOS 小知识-tips

--->1<--- arc的项目中使用非arc代码,则添加-fno-objc-arc: 非arc项目中使用arc代码,则添加-fobjc-arc. --->2<--- 实用的类 NSKeyedArchiver [UIScreen mainScreen] [UIDevice currentDevice] [UIFont familyNames] [UIApplication sharedApplication] [NSUserDefaults standardUserDefaults

NDK开发,如何配置 debug环境

刚开始做NDK 开发的时候,Android Studio 还没提供了 native C/C++ 设置断点 调试,我们都是通过输出 日志来调试,这样费时耗力.Android Studio 应该是在 2.2 版本才提供的设置断点 debug 功能,同时在该版本也提供了 cmake 编译.     我目前在做 NDK 开发的时候,还是习惯用 NDK-Build(也就是设置 Android.mk) 来开发,我先简单说一下怎么用输出日志来调试: 1.首先在 Android.mk 设置MODULE 添加日志

Windows7驱动调试小Tips

v\:* {behavior:url(#default#VML);} o\:* {behavior:url(#default#VML);} w\:* {behavior:url(#default#VML);} .shape {behavior:url(#default#VML);}/* Style Definitions */ table.MsoNormalTable {mso-style-name:普通表格; mso-tstyle-rowband-size:0; mso-tstyle-colb

10 Tips for Writing Better Code (阅读理解)

出发点 http://www.tuicool.com/articles/A7VrE33 阅读中文版本<编写质优代码的十个技巧>,对于我编码十年的经验,也有相同感受, 太多的坑趟过,太多的经历走过,对良好编码的技巧,只能说更加心有灵犀. 下面从英文原版阅读,结合自己的理解,尝试注解下作者的观点. 注解 -- 原文见下网址 https://cdiggins.github.io/blog/programming-tips.html 10 Tips for Writing Better Code 注:

基于Debug模式windows应用程序app.config设置

介绍 当我们开发Windows窗体应用程序,我们肯定会利用App.config的.我们可以添加,修改和删除键/值对管理用户首选项.在这里,将会节省你的时间. 演示 这里有一个简单的Windows窗体应用程序来展示如何添加一个密钥对到App.config文件.我建立的Visual Studio 2015年这里面的Windows窗体应用程序. using System; using System.Collections.Generic; using System.ComponentModel; us

VS2012下配置MPI

1.先下载安装mpich,下载地址为: http://www.mpich.org/downloads/ 完成后的目录如下图所示: 2.打开VS,创建如下工程 3.选择项目属性,添加头文件和库文件 4.引入mpi头文件和库 (1)找到"VC++ Directions->Include Directories",加上MPICH2的include文件路径,例如:"D:\software\MPICH2\include" (2)左边还是"VC++ Direct

基于MPI的并行计算—矩阵向量乘

以前没接触过MPI编程,对并行计算也没什么了解.朋友的期末课程作业让我帮忙写一写,哎,实现结果很一般啊.最终也没完整完成任务,惭愧惭愧. 问题大概是利用MPI完成矩阵和向量相乘.输入:Am×n,Bn×1  ,输出:Cm×1 附:程序中定义m=400,n=100,矩阵和向量的取值为随意整型数,为了便于显示并行效果,循环完成该计算任务100000次. 实现过程 1.实验环境:WINDOWS8.1 64位+ MPICH + VS2013   / kubuntu 14.04 + mpich 2.解题思路