[轉(zhuǎn)]MPI并行程序的調(diào)試技巧

原文請猛戳: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)到別的進程就用detachattach。讓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、rsirdx、rcxr8、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、edxecx中。(關(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啊有木有!不仔細看文檔的真心傷不起!也因為這個錯誤,使我的recvcntsrecvbuf的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)試并行程序

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容