MPI並行程序的調試技巧

原文請猛戳: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

聽說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所指定的大小等等,
我想你的那些方法應該對這些無論用吧?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 -->

- Parallel Application Debugger

padb實際上是個job monitor,它能夠顯示MPI message queue的情況。
推薦padb的一大理由是它能夠檢查deadlock。

使用gdb

假設你沒有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,要跳轉到別的進程就用detachattach

讓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類型的參數依次放在rdirsirdxrcxr8r9寄存器中,float參數放在xmm開頭的寄存器中。

  • 對x86(32bit)來講:參數放在堆棧(stack)中。
    此外GNU C支持:

__attribute__((regparm(<number>)))

其中<number>是一個0到3的整數,表示指定<number>個參數經過寄存器傳遞,因爲寄存器傳參要比堆棧傳參快,於是這也被稱爲#fastcall#。
若是指定

__attribute__((regparm(3)))

則開頭的三個參數會被依次放在eaxedxecx中。
(關於__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啊有木有!不仔細看文檔的真心傷不起!
也由於這個錯誤,使個人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][調試並行程序

相關文章
相關標籤/搜索