大规模并行中的从核exception定位方案
-
背景:
在运行CESM中遇到了1800进程中有 <5 个进程从核UNKOWN EXPT。需要定位到具体expt现场才能修复bug。
困难之处:
一开始使用在所有spawn插桩的方案,但是由于EXPT的进程号会随着调试代码的链接而变化,无法通过rank判断输出部分线程。那么直接的处理办法就是所有进程都输出到屏幕或者每个进程都输出到独立文件的方案。后来采用了后者,但是大规模I/O性能十分低下,原本一个小时出结果的程序预计需要一周,不是一个可靠的方案;前者也有类似问题。
处理方案总体:
使用wrap在所有的athread_spawn插桩,记录本次athread_spawn调用时的栈帧情况。为了能把记录的最后一次spawn的栈帧信息正确的输出,需要在出错进程退出前输出获得的栈帧信息。这个出错后的处理通过在__expt_handler插桩实现。
“如何通过wrap来插桩”
这是一个由gcc编译器(链接器)提供的功能。具体操作流程:
通过在编译的时候加入-Wl,-wrap=symbol(Wl的意思是之后的参数传递给链接器),来告诉链接器不去链接名为symbol的函数的时候,链接名为__wrap_symbol的函数。
__wrap_symbol是用户自己定义的函数,可以插入用户自己想运行的插桩代码。__wrap_symbol中通过__real_symbol调用原函数。更多wrap的资料可以见http://www.bubuko.com/infodetail-1328284.html等网页。
“如何看函数原型”
grep -r "athread_spawn" /usr/sw-mpp/
可以在神威的所有头文件中查找一个函数的定义。
从中可以看到athread_spawn是一个define,实际上__real_athread_spawn才是函数实体(其原因估计是为了通过define在函数名前加入slave_)。即我们需要wrap的函数不是athread_spawn,而是__real_athread_spawn。“如何获取足够深度的栈帧”
通过阅读几个函数的汇编,可以解析神威栈帧的一些基本信息。sp是栈帧的指针,指向栈底的位置。对于当前函数,0(sp)存的就是我们需要的return address。
但是由于神威fortran对于函数特殊的处理方案(据我所知x86的交叉编译方案不同),对于athread_spawn这种函数,fortran会先调用athread_spawn_作为外壳,由外壳调用原来的__real_athread_spawn。这样就需要至少寻找到backtrace(2)。
此外,由于CESM代码还在fortran中使用open ACC,open ACC的调用栈和直接athread库不一样。具体来说,是acc_parallel_start_(fortran外壳)调用了ACC_parallel_start(c的ACC开始函数),再调用了__real_athread_spawn这个c的athread库入口。一共需要寻找到backtrace(3)。
在技术上,我们通过0(sp)可以获得当前函数的返回地址,对于更深的栈帧,则需要一些技巧。和x86架构不同,不少RISC架构如MIPS、alpha没有bp(当前函数的栈顶)指针,无法通过寄存器(即bp-sp)得到栈的深度。但是由于这种任务下我们需要前跳的栈空间大小比较固定,可以通过阅读汇编直接得到栈深度,并hard-coding到代码的方法解决。具体来说:
call __real_athread_spawn Address: asm volatile("ldl %0, 0($sp)":"=r"(stack_frame));
call ACC_parallel_start Address: asm volatile("ldl %0, 32($sp)":"=r"(stack_frame));
call acc_parallel_start_ Address: asm volatile("ldl %0, 96($sp)":"=r"(stack_frame));
其中32是__real_athread_spawn的栈深度,64是ACC_parallel_start的栈深度。调用acc_start的地址即位于产生问题的CESM函数。(实际上,可以通过分析指令流,寻找ldi sp,*(sp)这种指令来获得栈长度,但是由于有动态分配栈内存的部分,该方法并不具有通用性。同时动态变化长度的指令依赖比较复杂,解析有一定的困难)
“如何捕获从核exception”
我们找了很久,没有现成的资料可以在程序退出前插桩。通过readelf -s a.out | grep -E "exception|expt"可以发现一些类似的函数。通过小case的实验我们最终定位到了__expt_handler这个函数,可以在UNKOWN EXPT前捕获到这个事件。并在其中把栈信息按进程号输出到对应文件,就可以获得需要的调试结果。此外,为了wrap __expt_handler这个函数,需要获得__expt_handler的参数列表。我们不能通过在头文件寻找其定义,因为__expt_handler不存在于任何头文件中。一个可行的方案是从1个参数试起,逐个往上试,直到试到链接器通过为止。
-
手动点赞! 膜拜!
-
棒!!!!!!!!
-
可以用这种比较简单点的方法获取到从核的PC以及SDLB和DMA Exception的报错现场.
#include <athread.h> #include <signal.h> long cgid; #define IO_GET(addr) (*(long*)(addr)) #define CPE_BASE_ADDR(cpe_id) (0x8003000000L | cgid << 36L | cpe_id << 16L) #define CPE_PC_ADDR 0x2000 inline long get_cpe_pc(int cpe_id){ //return *(long*)(0x8003002000L | cgid << 36L | cpe_id << 16L); //return *(long*) CPE_BASE_ADDR(cpe_id) | CPE_PC_ADDR; return IO_GET(CPE_BASE_ADDR(cpe_id) | CPE_PC_ADDR); } #define TC_BASE_ADDR (0x8004000000L | cgid << 36L) #define TC_SDLB_ERR_PEVEC 0x308000L #define TC_SDLB_ERR_SPOT 0x308080L char *SDLB_REQ_TYPE_STR[] = {"read", "write", "faa", "updt+", "updt-"}; char *SDLB_REQ_SRC_STR[] = {"cpe", "ibox", "dma"}; char *BOOL_STR[] = {"no", "yes"}; inline void decode_sdlb_err(){ puts("==============DECODE OF SDLB ERROR SPOT=============="); long spot = IO_GET(TC_BASE_ADDR | TC_SDLB_ERR_SPOT); printf("TC_SDLB_ERR_SPOT: %lx\n", spot); long reqtype = spot & 7; printf("REQ_TYPE: %s\n", SDLB_REQ_TYPE_STR[reqtype]); long src_pe = spot >> 40 & 63; printf("SRC_PE: %d\n", src_pe); long grain = spot >> 46 & 2; printf("GRAIN: %d\n", 1 << grain); long src_id = spot >> 59 & 3; printf("SRC_TYPE: %s\n", SDLB_REQ_SRC_STR[src_id - 1]); long oor = (spot >> 61) & 1; printf("OUT_OF_RANGE: %s\n", BOOL_STR[oor]); long oop = (spot >> 62) & 1; printf("OUT_OF_PERM: %s\n", BOOL_STR[oop]); } #define GC_BASE_ADDR (0x8005000000L | cgid << 36L) #define GC_DMACHK_TYPE 0x200700L char *DMA_ERR_STR[] = { "LDM unaligned", "MEM unaligned", "size unaligned", "bsize unaligned", "stride unaligned", "GET_P/PUT_P occur", "size non positive", "", "invalid OP", "invalid MODE", "invalid OP+MODE", "reply overflow/unaligned", "LDM overflow", "bcast vec=0", "not connected" }; void decode_dma_chk(){ puts("================DECODE OF DMACHK_TYPE================"); long chk = IO_GET(GC_BASE_ADDR | GC_DMACHK_TYPE); long type; puts("CHK_TYPE:"); for (type = 0; type <= 14; type ++) if ((1 << type) & chk){ printf("\t%s\n", DMA_ERR_STR[type]); } long src_pe = chk >> 16 & 63; printf("SRC_PE: %d\n", src_pe); } void expt_decoder(int sig){ printf("writing cpe PCs on signal %d\n", sig); int i, j; for (i = 0; i < 8; i ++) { for (j = 0; j < 8; j ++) printf("%3d: %12llx ", i * 8 + j, get_cpe_pc(i * 8 + j)); puts(""); } decode_sdlb_err(); decode_dma_chk(); } extern void slave_gid(long *); void sig_init(int rank){ if (!athread_idle()) athread_init(); long cid; athread_spawn(gid, &cid); athread_join(); cgid = cid; }
上面这段代码里面expt_decoder是解析从核错误并打印到屏幕的函数, sig_init是初始化核组号的函数. 里面调用了一个叫gid的从核函数用于获取核组号, 实现如下:
#include <slave.h> #include <simd.h> void gid(long *_id){ long cid; asm ("rcsr %0, 0\n\t": "=r"(cid)); if (!_MYID){ *_id = cid >> 6; } } #endif
可以配合wrap expt_handler使用目测效果拔群.
函数原型我的印象是__expt_handler(void *a, void *b)
能够兼容, 可以这么写:void __wrap___expt_handler(void *a, void *b){ //大神的解析栈代码. expt_decoder(0); __real___expt_handler(a, b); }
另外, wrap只需要在最后链接的时候加.
-
666666 膜拜