写点什么

一文掌握 Ascend C 孪生调试

  • 2023-12-12
    广东
  • 本文字数:6515 字

    阅读完需:约 21 分钟

一文掌握Ascend C孪生调试

本文分享自华为云社区《一文掌握Ascend C孪生调试》,作者:昇腾 CANN。

1 What,什么是孪生调试


Ascend C 提供孪生调试方法,即 CPU 域模拟 NPU 域的行为,相同的算子代码可以在 CPU 域调试精度,NPU 域调试性能。孪生调试的整体方案如下:开发者通过调用 Ascend C 类库编写 Ascend C 算子 kernel 侧源码,kernel 侧源码通过通用的 GCC 编译器进行编译,编译生成通用的 CPU 域的二进制,可以通过 gdb 通用调试工具等调试手段进行调试;kernel 侧源码通过毕昇编译器进行编译,编译生成 NPU 域的二进制文件,可以通过 msprof 工具进行性能数据采集等方式进行调试。



针对 NPU 域的调试来讲,根据依赖和调用动态库的不同,分为 NPU 域仿真调试和 NPU 域上板调试。NPU 域仿真调试,依赖和调用的是 model 仿真器指定的库文件,运行 model 仿真不需要使用真实的 NPU 环境;上板调试,依赖和调用的是真实 NPU 环境上的库文件,进行上板调试需要真实的 NPU 环境。


CPU 域调试用于定位逻辑错误、内存错误等功能问题; NPU 域调试不仅可以通过数据打印的方式定位功能问题,也可以用于定位性能问题、算子同步问题。


本文将从功能调试的角度出发,介绍 CPU 域和 NPU 域的调试方法,并通过具体的调试样例来帮助大家快速掌握;性能调试的方法将在后续的文章中介绍。

2 How,如何进行调试

2.1 CPU 域


本节介绍 CPU 域调试的两种方法:gdb 调试、使用 printf 打印命令打印。

2.1.1 gdb 调试


gdb 调试相信大家并不陌生,首先我们先来回顾几个常用的调试命令,更多内容可以前往https://sourceware.org/gdb/ 深入学习。



这里稍微复杂一点的是,因为我们程序是多核执行程序,cpu 调测将其转为多进程调试,每个核都会拉起独立的子进程,故 gdb 需要转换成子进程调试的方式。下面介绍子进程调试的方法。


• 调试单独一个子进程


在 gdb 启动后,首先设置跟踪子进程,之后再打断点,就会停留在子进程中,设置的命令为:


set follow-fork-mode child
复制代码


但是这种方式只会停留在遇到断点的第一个子进程中,其余子进程和主进程会继续执行直到退出。涉及到核间同步的算子无法使用这种方法进行调试。


如下是调试一个单独子进程的调试命令样例:


gdb --args add_custom_cpuset follow-fork-mode childbreak add_custom.cpp:45runlistbacktraceprint ibreak add_custom.cpp:56continuedisplay xLocalquit
复制代码


如果涉及到核间同步,那么需要能同时调试多个子进程。


在 gdb 启动后,首先设置调试模式为只调试一个进程,挂起其他进程。设置的命令如下:


(gdb) set detach-on-fork off
复制代码


查看当前调试模式的命令为:


​​​​​​(gdb) show detach-on-fork
复制代码


中断 gdb 程序的方式要使用捕捉事件的方式,即 gdb 程序监控 fork 这一事件并中断。这样在每一次起子进程时就可以中断 gdb 程序。设置的命令为:


(gdb) catch fork
复制代码


当执行 r 后,可以查看当前的进程信息:


(gdb) info inferiorsNum Description* 1 process 19613
复制代码


可以看到,当第一次执行 fork 的时候,程序断在了主进程 fork 的位置,子进程还未生成。


执行 c 后,再次查看 info inferiors,可以看到此时第一个子进程已经启动。


(gdb) info inferiorsNum Description* 1 process 196132 process 19626
复制代码


这个时候可以使用切换到第二个进程,也就是第一个子进程,再打上断点进行调试,此时主进程是暂停状态:


(gdb) inferior 2[Switching to inferior 2 [process 19626] ($HOME/demo)](gdb) info inferiorsNum Description1 process 19613* 2 process 19626
复制代码


请注意,inferior 后跟的数字是进程的序号,而不是进程号。


如果遇到同步阻塞,可以切换回主进程继续生成子进程,然后再切换到新的子进程进行调试,等到同步条件完成后,再切回第一个子进程继续执行。

2.1.2 printf 打印命令


printf 则更为简单,直接使用 printf 打印命令 printf(...)来观察数值的输出。需要注意的是:NPU 模式下目前不支持打印语句,所以需要添加内置宏__CCE_KT_TEST__予以区分。样例代码如下:


printf("xLocal size: %d\n", xLocal.GetSize());printf("tileLength: %d\n", tileLength);
复制代码

2.2 NPU 域

2.2.1 上板数据打印(DumpTensor、PRINTF)


功能包括 DumpTensor、PRINTF 两种,其中 DumpTensor 用于打印指定 Tensor 的数据,PRINTF 主要用于打印标量和字符串信息。


具体的使用方法如下:


1. 设置打开 DUMP 开关的环境变量


export ACL_DUMP_DATA=1


修改 Dump 信息配置文件 acl.json。单算子调用应用开发场景下,新建 acl.json 放在应用开发的工程目录下,在调用 aclInit 接口时传入;Pytorch 调用场景下,放在 Pytorch 脚本执行目录下,由框架自行读取。配置样例如下:其中 dump_path 表示 dump 数据文件存储到运行环境的目录,支持配置绝对路径或相对路径;dump_mode 表示 dump 的模式,配置成 input/output/all 有效模式均可以;dump_op_switch 表示单算子 dump 数据开关,需要配置成 on,开启单算子 dump 模式;dump_debug 为预留参数,开发者无需关注,直接配置成 off 即可。


{"dump":{"dump_path":"/dump","dump_mode": "all","dump_debug": "off","dump_op_switch": "on"}}
复制代码


2. 增加算子工程编译选项-DASCENDC_DUMP:修改算子工程 op_kernel 目录下的 CMakeLists.txt 文件,首行增加编译选项,打开 DUMP 开关,样例如下:


add_ops_compile_options(ALL OPTIONS -DASCENDC_DUMP)
复制代码


3. 在算子 kernel 侧实现代码中需要输出日志信息的地方调用 DumpTensor 接口或者 PRINTF 接口打印相关内容。


a) DumpTensor 示例,srcLocal 表示待打印的 Tensor;5 表示用户的自定义附加信息,比如当前的代码行号;dataLen 表示元素个数。


DumpTensor(srcLocal,5, dataLen);
复制代码


Dump 时,每个 block 核的 dump 信息前会增加对应信息头 DumpHead(32 字节大小),用于记录核号和资源使用信息;每次 Dump 的 Tensor 数据前也会添加信息头 DumpTensorHead(32 字节大小),用于记录 Tensor 的相关信息。如下图所示,展示了多核打印场景下的打印信息结构。



DumpHead 的具体信息如下:


• block_id:当前运行的核号;

• total_block_num:此次 dump 的核数;

• block_remain_len:当前核剩余可用的 dump 的空间;

• block_initial_space:当前核初始分配的 dump 空间;

• magic:内存校验魔术字。


DumpTensorHead 的具体信息如下:


• desc:用户自定义附加信息;

• addr:Tensor 的地址;

• data_type:Tensor 的数据类型;

• position:表示 Tensor 所在的物理存储位置。


打印结果的样例如下:


DumpHead: block_id=0, total_block_num=16, block_remain_len=1048448, block_initial_space=1048576, magic=5aa5bccdDumpTensor: desc=5, addr=0, data_type=DT_FLOAT16, positinotallow=UB[40, 82, 60, 11, 24, 55, 52, 60, 31, 86, 53, 61, 47, 54, 34, 62, 84, 29, 48, 95, 16, 0, 20, 77, 3, 55, 69, 73, 75, 40, 35, 13]DumpHead: block_id=1, total_block_num=16, block_remain_len=1048448, block_initial_space=1048576, magic=5aa5bccdDumpTensor: desc=5, addr=0, data_type=DT_FLOAT16, positinotallow=UB[58, 84, 22, 54, 41, 93, 1, 45, 50, 9, 72, 81, 23, 96, 86, 45, 36, 9, 36, 34, 78, 7, 2, 29, 47, 26, 13, 24, 27, 55, 90, 5]...DumpHead: block_id=7, total_block_num=16, block_remain_len=1048448, block_initial_space=1048576, magic=5aa5bccdDumpTensor: desc=5, addr=0, data_type=DT_FLOAT16, positinotallow=UB[28, 27, 79, 39, 86, 5, 23, 97, 89, 5, 65, 69, 59, 13, 49, 2, 34, 6, 52, 38, 4, 90, 11, 11, 61, 50, 71, 98, 19, 54, 54, 99]
复制代码


b) PRINTF 示例如下:


PRINTF("fmt string %d", 0x123);
复制代码

3 Example,调试样例


下面通过具体的样例来实战一下吧!

3.1 CPU 域调试样例


在进行调试之前我们需要获取一个精度有问题的算子样例


1. 通过如下的样例链接获取正确的样例。(CPU 调试当前仅适用于通过内核调用符调用算子的程序调试,所以这里我们获取 KernelLaunch 的代码样例)


https://gitee.com/ascend/samples/tree/master/operator/AddCustomSample/KernelLaunch


2. 将 AddKernelInvocation / add_custom.cpp 中的 Init 函数替换成如下有 bug 的代码。


__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z){xGm.SetGlobalBuffer((__gm__ half*)x + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);yGm.SetGlobalBuffer((__gm__ half*)y + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);zGm.SetGlobalBuffer((__gm__ half*)z + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);pipe.InitBuffer(inQueueX, BUFFER_NUM, TILE_LENGTH);pipe.InitBuffer(inQueueY, BUFFER_NUM, TILE_LENGTH);pipe.InitBuffer(outQueueZ, BUFFER_NUM, TILE_LENGTH);}
复制代码


3. 参考样例 readme,跑一下样例,样例出现如下报错:


[ERROR] result error


下面我们一起开始 debug 之旅吧。


1. 观察日志报错。


观察是否有打屏日志报错,可搜索关键词"failed"。下图的报错示例指示,错误出现在代码中调用 Add 接口的地方。


add_cpu: /usr/local/Ascend/ascend-toolkit/7.0.RC1.alpha003/x86_64-linux/tikcpp/tikcfw/interface/kernel_operator_vec_binary_intf.h:79: void AscendC::Add(const AscendC::LocalTensor<T>&, const AscendC::LocalTensor<T>&, const AscendC::LocalTensor<T>&, const int32_t&) [with T = float16::Fp16T; int32_t = int]: Assertion `false && "check vadd instr failed"' failed.
复制代码


通过上述报错日志,一般只能定位到框架报错的代码行,无法明确具体错误,接下来需要通过 gdb 调试的方式或者 printf 打印的方式进一步精确定位。


2. gdb 调试。下面的样例展示了拉起 Add 算子 CPU 侧运行程序的样例,该样例程序会直接抛出异常,直接 gdb 运行,查看调用栈信息分析定位即可。其他场景下您可以使用 gdb 打断点等基本操作进行调试。


1) 使用 gdb 拉起待调试程序,进入 gdb 界面进行 debug。


gdb add_cpu


2) 单独调试一个子进程。


(gdb) set follow-fork-mode child


3) 运行程序。


(gdb) r


4) 通过 bt 查看程序调用栈。


(gdb) bt


5) 查看具体层的堆栈信息,打印具体变量的值。本示例中,查看报错代码的上一层第 5 层的堆栈,打印了 TILE_LENGTH 为 128,该程序中表示需要处理 128 个 half 类型的数,大小为 128*sizeof(half)=256 字节;同时打印了输入 Tensor xLocal 的值,其中 dataLen 表示 LocalTensor 的 size 大小为 128 字节,只能计算 128 字节的数据。可以看出两者的长度不匹配,由此可以继续检查代码内存分配时传入长度是否有误,从而定位到 Init 函数中 InitBuffer 的问题。同理,大家可以自行打印 yLocal 和 zLocal 的值。


(gdb) f 5#5 0x0000555555560638 in KernelAdd::Compute (this=0x7fffffffd360, progress=0)at /samples-master/operator/AddCustomSample/KernelLaunch/AddKernelImpl/add.cpp:5454 Add(zLocal, xLocal, yLocal, TILE_LENGTH); (gdb) p TILE_LENGTH$1 = 128(gdb) p xLocal$3 = {<AscendC::BaseTensor<float16::Fp16T>> = {<No data fields>}, address_ = {logicPos = 9 '\t', bufferHandle = 0x7fffffffd460 "\003\005\377\377\200", dataLen = 128,bufferAddr = 0,absAddr = …}
复制代码


3. printf 打印。在调用报错代码行之前的位置增加变量打印。样例代码如下:


__aicore__ inline void Compute(int32_t progress){LocalTensor<half> xLocal = inQueueX.DeQue<half>();LocalTensor<half> yLocal = inQueueY.DeQue<half>();LocalTensor<half> zLocal = outQueueZ.AllocTensor<half>();#ifdef __CCE_KT_TEST__printf("xLocal size: %d\n", xLocal.GetSize());printf("tileLength: %d\n", TILE_LENGTH);#endifAdd(zLocal, xLocal, yLocal, TILE_LENGTH);outQueueZ.EnQue<half>(zLocal);inQueueX.FreeTensor(xLocal);inQueueY.FreeTensor(yLocal);}
复制代码


可以看到有如下打屏日志输出,打印了 tileLength 为 128,该程序中表示需要处理 128 个 half 类型的数;输入 Tensor xLocal 的 size 大小,为 64,表示只能计算 64 个 half 类型的数。可以看出两者的长度不匹配,由此可以继续检查代码内存分配时传入长度是否有误,从而定位到 Init 函数中 InitBuffer 的问题。


xLocal size: 64 


tileLength: 128

3.2 NPU 域调试样例


在进行调试之前我们需要获取一个精度有问题的算子样例


1. 通过如下的样例链接获取正确的样例。(上板数据打印调试当前适用于单算子调用程序(aclnn 接口)调试,所以这里我们获取算子工程和 aclnn 单算子调用的代码样例)


https://gitee.com/ascend/samples/tree/master/operator/AddCustomSample/FrameworkLaunch


2. 将 AddCustom / op_kernel / add_custom.cpp 中的 Init 函数替换成如下有 bug 的代码。


__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z, uint32_t totalLength, uint32_t tileNum){ASSERT(GetBlockNum() != 0 && "block dim can not be zero!");this->blockLength = totalLength / GetBlockNum();this->tileNum = tileNum;ASSERT(tileNum != 0 && "tile num can not be zero!");this->tileLength = this->blockLength / tileNum / BUFFER_NUM;xGm.SetGlobalBuffer((__gm__ half*)x + this->blockLength * GetBlockIdx(), this->blockLength);yGm.SetGlobalBuffer((__gm__ half*)y + this->blockLength * GetBlockIdx(), this->blockLength);zGm.SetGlobalBuffer((__gm__ half*)z + this->blockLength * GetBlockIdx(), this->blockLength);pipe.InitBuffer(inQueueX, BUFFER_NUM, this->tileLength);pipe.InitBuffer(inQueueY, BUFFER_NUM, this->tileLength);pipe.InitBuffer(outQueueZ, BUFFER_NUM, this->tileLength);}
复制代码


3. 参考样例 readme,跑一下算子的编译部署以及 aclnn 调用样例,aclnn 调用样例出现如下报错:


[ERROR] result error
复制代码


下面我们一起开始 debug 之旅吧。


在 AddCustom / op_kernel / add_custom.cpp 中关键的流程中增加数据打印

比如如下的示例代码分别在计算前和计算后增加对 zLocalTensor 的数据打印,同时打印参与计算的元素个数。


DumpTensor(zLocal, __LINE__, zLocal.GetSize());PRINTF("The data Length involved in calculation is %d.\n", this->tileLength);Add(zLocal, xLocal, yLocal, this->tileLength);DumpTensor(zLocal, __LINE__, zLocal.GetSize());
复制代码


参考上文的上板数据打印流程进行环境变量、cmake 文件等配置,运行 aclnn 单算子调用样例,可以看到屏幕有如下打印:


DumpTensor: desc=54, addr=512, data_type=DT_FLOAT16, positinotallow=UB[59.1875, 66.625, 53.4375, 52.0625, 80.3125, 10.5234, 21.3594, 43.2188, 1.15039, 50.625, 7.88672, 60.0625, 63.9062, 28.2812, 6.72266, 66.3125, 38.9062, 7.9375, 37.8125, 38.9062, 57.0938, 14.2266, 66.625, 62.3125, 5.17969, 1.63477, 45.2812, 68.1875, 59.9375, 23.8281, 71.8125,45.1875, 59.2188, 53.875, 77.25, 47.5625, 75, 38.1875, 61.2812, 63.125, 58.5312, 83.6875, 44.5312, 57.125, 2.74609, 32.8438, 27.1094, 20.9219, 70.3125, 62.7188, 20, 6.90625, 30.5312, 91.75, 17.3125, 29.8125, 68.9375, 83.125, 23.4375, 58.8125, 62.6875,69.9375, 19.9531, 46.25]The data Length involved in calculation is 128.DumpTensor: desc=57, addr=512, data_type=DT_FLOAT16, positinotallow=UB[151.75, 158, 48.125, 96.25, 158, 74.125, 101.375, 34, 36.2812, 112.375, 109.125, 95.25, 142.75, 44.75, 176.375, 130.125, 68.3125, 70.3125, 102.375, 92.75, 139.25, 55.4375, 87, 102.75, 177.375, 118.875, 113.375, 25.6562, 101.125, 107.75, 165.5,72.5, 17.5625, 52.4688, 167.125, 132.75, 171.375, 98.75, 133.375, 107.5, 119.375, 149.25, 161.625, 146.375, 162.25, 88.8125, 119.375, 88.9375, 170.5, 91.625, 72.9375, 131.875, 71.5625, 44.6562, 57.0625, 101.5, 150.125, 71.75, 126.625, 113.688, 139.75,107.938, 79.5625, 132.875]
复制代码


观察得到 tensor 的长度为 64,但是设置的计算长度为 128 两者不匹配,进一步定位可以确定问题为 Init 时初始化 buffer 的长度错误。


通过本篇内容 大家可以了解 Ascend C 孪生调试的概念,并可以参照实际样例进行实战练习。


更多相关内容请参考:《 Ascend C 官方教程


点击关注,第一时间了解华为云新鲜技术~

发布于: 1 小时前阅读数: 3
用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
一文掌握Ascend C孪生调试_人工智能_华为云开发者联盟_InfoQ写作社区