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