原文請猛戳:http://galoisplusplus.coding.me/blog/2013/06/08/mpi-debug-tips/
debug一個并行程序(parallel program)向來是件很麻煩的事情(Erlang等functional programming language另當(dāng)別論),對于像MPI這種非shared memory的inter-process model來說尤其如此。
與調(diào)試并行程序相關(guān)的工具
非開源工具
目前我所了解的商業(yè)調(diào)試器(debugger)有:
據(jù)說parallel debug的能力很屌,本人沒用過表示不知,說不定只是界面做得好看而已。不過我想大部分人應(yīng)該跟本屌一樣是用不起這些商業(yè)產(chǎn)品的,高富帥們請無視。以下我介紹下一些有用的open source工具:
開源工具
- Valgrind Memcheck
首先推薦valgrind的memcheck。大部分MPI標(biāo)準(zhǔn)的實現(xiàn)(implementation)(如openmpi、mpich)支持的是C、C++和Fortran語言。Fortran語言我不了解,但C和C++以復(fù)雜的內(nèi)存管理(memory management)見長可是出了名的XD。有些時候所謂的MPI程序的bug,不過是一般sequential程序常見的內(nèi)存錯誤罷了。這個時候用memcheck檢查就可以很容易找到bug的藏身之處。你可能會爭論說你用了RAII(Resource Allocation Is Initialization)等方式來管理內(nèi)存,不會有那些naive的問題,但我還是建議你使用memcheck檢查你程序的可執(zhí)行文件,因為memcheck除了檢查內(nèi)存錯誤,還可以檢查message passing相關(guān)的錯誤,例如:MPI_Send一塊沒有完全初始化的buffer、用來發(fā)送消息的buffer大小小于MPI_Send所指定的大小、用來接受消息的buffer大小小于MPI_Recv所指定的大小等等,我想你的那些方法應(yīng)該對這些不管用吧?
這里假設(shè)你已經(jīng)安裝并配置好了memcheck,例如如果你用的是openmpi,那么執(zhí)行以下命令
ompi_info | grep memchecker
會得到類似
MCA memchecker: valgrind (MCA v2.0, API v2.0, Component v1.6.4)
的結(jié)果。否則請參照Valgrind User Manual 4.9. Debugging MPI Parallel Programs with Valgrind進行配置。
使用memcheck需要在compile時下-g參數(shù)。運行memcheck用下面的命令:
mpirun [mpirun-args] valgrind [valgrind-args] <application> [app-args]
- Parallel Application Debugger
padb其實是個job monitor,它可以顯示MPI message queue的狀況。推薦padb的一大理由是它可以檢查deadlock。
使用gdb
假設(shè)你沒有parallel debugger,不用擔(dān)心,我們還有g(shù)db這種serial debugger大殺器。首先說說mpirun/mpiexec/orterun所支持的打開gdb的方式。
openmpi支持:
mpirun [mpirun-args] xterm -e gdb <application>
執(zhí)行這個命令會打開跟所指定的進程數(shù)目一樣多的終端——一下子蹦出這么多終端,神煩~——每個終端都跑有g(shù)db。我試過這個方式,它不支持application帶有參數(shù)的[app-args]情況,而且進程跑在不同機器上也無法正常跑起來——這一點openmpi的FAQ已經(jīng)有比較復(fù)雜的解決方案。
mpich2支持:
mpirun -gdb <application>
但在mpich較新的版本中,該package的進程管理器(process manager)已經(jīng)從MPD換為Hydra,這個-gdb的選項隨之消失。詳情請猛戳這個鏈接(http://trac.mpich.org/projects/mpich/ticket/1150))。像我機器上的mpich版本是3.0.3,所以這個選項也就不能用了。如果你想試試可以用包含MPD的舊版mpich。
好,以下假設(shè)我們不用上述方式,只是像debug一般的程序一樣,打開gdb,attach到相應(yīng)進程,完事,detach,退出。
現(xiàn)在我們要面對的一大問題其實是怎么讓MPI程序暫停下來。因為絕大多數(shù)MPI程序其實執(zhí)行得非??臁獙懖⑿谐绦虻囊淮竽康牟痪褪羌铀倜础芏鄷r候來不及打開gdb,MPI程序就已經(jīng)執(zhí)行完了。所以我們需要讓它先緩下來等待我們打開gdb執(zhí)行操作。
目前比較靠譜的方法是在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模式和正常模式的可執(zhí)行文件。如果要生成debug模式的可執(zhí)行文件,只需在編譯時加入以下參數(shù):
-DMPI_DEBUG
或
-DMPI_DEBUG=define
如果不加以上參數(shù)就是生成正常模式的可執(zhí)行文件了,不會再有debug模式的副作用(例如在這里是陷入無限循環(huán))。不用這個macro的話,要生成正常模式的可執(zhí)行文件還得回頭改源代碼,這樣一者可能代碼很長,導(dǎo)致很難找到這個hook的位置;二者如果你在「測試-發(fā)布-測試-...」的開發(fā)周期里,debug模式所加的代碼經(jīng)常要「加入-刪掉-加入-...」很是蛋疼。
(什么?你犯二了,在源代碼中加了一句
#define MPI_DEBUG
好吧,你也可以不改動這一句,只需在編譯時加入
-UMPI_DEBUG
就可以生成正常模式的可執(zhí)行文件。)
這樣只需照常運行,MPI程序就會在while循環(huán)的地方卡住。這時候打開gdb,執(zhí)行
(gdb) shell ps aux | grep <process-name>
找到所有對應(yīng)進程的pid,再用
(gdb) attach <pid>
attach到其中某一個進程。Prof. Matloff用的是
gdb <process-name> <pid>
這也是可以的。但我習(xí)慣的是開一個gdb,要跳轉(zhuǎn)到別的進程就用detach再attach。讓MPI程序跳出while循環(huán):
(gdb) set gdb_break = 0
現(xiàn)在就可以隨行所欲的執(zhí)行設(shè)breakpoint啊、查看register啊、print變量啊等操作了。
我猜你會這么吐嘈這種方法:每個process都要set一遍來跳出無限循環(huán),神煩啊有木有!
是的,你沒有必要每個process都加,可以只針對有代表性的process加上(例如你用到master-slave的架構(gòu)那么就挑個master跟slave唄~)。
神馬?「代表」很難選?!我們可以把while循環(huán)改成:
while(gdb_break)
{
// set the sleep time to pause the processes
sleep(<time>);
}
這樣在時間內(nèi)打開gdb設(shè)好breakpoint即可,過了這段時間process就不會卡在while循環(huán)的地方。
神馬?這個時間很難?。咳《塘藖聿患?,取長了又猴急?好吧你贏了......
類似的做法也被PKU的Jinlong Wu (King)博士寫的調(diào)試并行程序提及到了。他用的是:
setenv INITIAL_SLEEP_TIME 10
mpirun [mpirun-args] -x INITIAL_SLEEP_TIME <application> [app-args]
本人沒有試過,不過看起來比改源代碼的方法要優(yōu)秀些XD。
其他
假設(shè)你在打開gdb后會發(fā)現(xiàn)no debugging symbols found,這是因為你的MPI可執(zhí)行程序沒有用于debug的symbol。正常情況下,你在compile時下-g參數(shù),生成的可執(zhí)行程序(例如在linux下是ELF格式,ELF可不是「精靈」,而是Executable and Linkable Format)中會加入DWARF(DWARF是對應(yīng)于「精靈」的「矮人」Debugging With Attributed Record Format)信息。如果你編譯時加了-g參數(shù)后仍然有同樣的問題,我想那應(yīng)該是你運行MPI的環(huán)境有些庫沒裝上的緣故。在這樣的環(huán)境下,如果你不幸踩到了segmentation fault的雷區(qū),想要debug,可是上面的招數(shù)失效了,坑爹啊......好在天無絕人之路,只要有程序運行的錯誤信息(有core dump更好),依靠一些匯編(assmebly)語言的常識還是可以幫助你debug的。
這里就簡單以我碰到的一個悲劇為例吧,BTW為了找到bug,我在編譯時沒有加優(yōu)化參數(shù)。以下是運行時吐出的一堆錯誤信息(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應(yīng)該是判斷是否要return的counter。現(xiàn)在寄存器%eax就成了最大的嫌疑,有理由 相信 猜某個對該寄存器的不正確操作導(dǎo)致了segmentation fault。好吧,其實debug很多時候還得靠猜,記得有這么個段子:「師爺,寫代碼最重要的是什么?」「淡定?!埂笌煚敚{(diào)試程序最重要的是什么?」「運氣。」
接下來找到了%eax被賦值的地方:
52ac2: 41 8b 00 mov (%r8),%eax
這里需要了解函數(shù)參數(shù)傳遞(function parameter passing)的調(diào)用約定(calling convention)機制:
- 對x64來說:int和pointer類型的參數(shù)依次放在
rdi、rsi、rdx、rcx、r8、r9寄存器中,float參數(shù)放在xmm開頭的寄存器中。 - 對x86(32bit)來說:參數(shù)放在堆棧(stack)中。 此外GNU C支持:
__attribute__((regparm(<number>)))
其中<number>是一個0到3的整數(shù),表示指定<number>個參數(shù)通過寄存器傳遞,由于寄存器傳參要比堆棧傳參快,因而這也被稱為#fastcall#。如果指定
__attribute__((regparm(3)))
則開頭的三個參數(shù)會被依次放在eax、edx和ecx中。(關(guān)于__attribute__的詳細介紹請猛戳GCC的官方文檔)。
- 如果是C++的member function,別忘了隱含的第一個參數(shù)其實是object的this指針(pointer)。
回到我們的例子,%r8正對應(yīng)MPI_Gatherv的第五個參數(shù)?,F(xiàn)在終于可以從底層的匯編語言解脫出來了,讓我們一睹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)
第五個參數(shù)是recvcnts,于是就可以針對這個「罪魁禍?zhǔn)住谷タ丛闯绦虻降壮隽耸裁磫栴}了。這里我就不貼出代碼了,bug的來源就是我當(dāng)時犯二了,以為這個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要大,因而發(fā)生了access在recvbuf范圍以外的內(nèi)存的情況(也就是我們從錯誤信息所看到的Address not mapped)。
最后再提一點,我源代碼中的recvbuf其實是malloc出來的內(nèi)存,也就是在heap中,這種情況其實用valgrind應(yīng)該就可以檢測出來(如果recvbuf在stack中我可不能保證這一點)。所以,騷念們,編譯完MPI程式先跑跑valgrind看能不能通關(guān)吧,更重要的是,寫代碼要仔細看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]調(diào)試并行程序