MindStudio异常检测工具
在昇腾设备上进行算子开发,需要涉及多核调度、多流水执行以及多层次内存间的数据交互,任何一条指令对内存的错误访问、同步指令的错误使用,都可能引起内存访问越界、数据竞争,导致算子精度或执行异常。
一个复杂算子的生命周期内可能执行十万乃至上百万条指令,想要准确定位异常的指令非常困难,对于数据竞争问题,我们想要找到存在竞争风险的两条指令更是"大海捞针"。
此时MindStudio Sanitizer(msSanitizer)异常检测工具来啦!借助竞争检测功能,让你完成分钟级的异常定位,告别手工排查。
msSanitizer异常检测工具主要包括内存检测、竞争检测、初始化检测、同步检测等功能,为昇腾算子开发者提供了检测算子异常的手段,快速定位算子内存和竞争问题。本文将以一个实际的算子竞争问题为例,让读者对竞争异常的现象、异常检测工具的使用方法、竞争问题的分析方法有个直观的感受。
什么是算子竞争?
在算子开发过程中,我们可能会遇到算子偶现精度异常的问题。一般现象就是多次执行同一算子,算子输出不一致。
此时我们就怀疑算子很可能出现了竞争问题。那什么是竞争问题呢?竞争(race hazard)是一个通用概念,一般是指程序中多个线程或进程同时访问了相同的共享内存单元,导致的不确定行为。在昇腾设备架构上,竞争可能发生在核间,主要涉及GM(global memory);可能发生在流水间,涉及 GM 和UB、L1、L0A、L0B、L0C等内存;也可能发生在同一条流水内相邻的指令之间。
1. GM 为global memory 全局内存,由多个 core 共享
2. UB、L1、L0A、L0B、L0C为core内的内存单元,但仍可能被多个流水访问
下面以线程进行举例,两个线程访问相同的内存单元时,根据两个线程间的数据依赖关系,有以下三种情况:
第一种情况是线程2期望读取线程1写入的数据,此时应保证线程2的读事件发生在线程1的写事件之后。第二种情况是期望线程1读取数据后线程2再写入,防止线程1读取到错误的数据,此时应保证线程2的写事件发生在线程1的读事件之后。第三种情况是期望线程2写入的数据覆盖线程1写入的数据,此时应保证线程2的写事件发生在线程1的写事件之后。
为了保证两个线程上的事件能按照预期的顺序执行,需要做好正确的线程间同步。对应到昇腾设备上,我们需要处理好核间、流水间、流水内指令的同步关系。如果同步指令缺失或者错误使用,就可能导致竞争问题。msSanitizer会检测所有可能同时发生,并且访问了相同内存单元的指令,帮助开发者识别潜在的竞争问题。
工具使用方式
算子编译
在算子编译时,需要增加检测编译选项以支持算子检测功能。
算子执行
在原始的算子执行命令前增加 mssanitizer 来拉起算子,即可完成检测:
mssanitizer -t racecheck -- <kernel_prog>
检测完成后,算子异常信息会直接输出到终端:
异常检测异常报告会包含以下关键信息:
-
• 竞争类型:支持显示竞争事件类型,包括WAR(write-after-read)、RAW(read-after-write)或WAW(write-after-write)信息,具体可参考《算子竞争问题》章节;
-
• 内存类型:支持显示竞争发生的内存类型,包括GM、UB、L1、L0A、L0B和L0C等内存单元;
-
• 算子名:显示竞争发生的 kernel name 方便多算子场景下问题定位;
-
• 竞争流水类型:显示产生竞争异常的流水类型;
-
• 调用栈展示:通过调用栈展示竞争事件对应的代码行信息;
经典案例------Add算子同步指令错位导致算子精度问题
问题背景
下面通过一个基于真实案例构造的简化样例,对msSanitizer的竞争检测功能的开启方法和分析思路进行介绍。
如上图所示,我们实现了一个简单的加法算子,将输入 x 和 y 相加后输出到 z中。实际实现时,我们需要搬入->计算->搬出三个步骤完成算子逻辑,分别对应算子代码的57和 58 行、64 行以及 66 和 67 行。此处的搬入指令发生在PIPE_MTE2流水上,计算指令发生在 PIPE_V 流水上,搬出指令发生在 PIPE_MTE3流水上。各流水上的指令并行执行,为避免竞争问题,我们分别在 60 行和66行插入了从 PIPE_MTE2 到 PIPE_V 流水的同步,以及从 PIPE_V 到 PIPE_MTE3流水的同步,来保证指令执行的时序。
从以上代码来看,我们的同步逻辑没有问题,算子应输出如下结果:
但实际执行下来,我们发现算子精度异常,并且每次执行得到的输出都不同:
问题定位
首先我们需要为算子增加检测编译选项来支持竞争检测,此处以Makefile为例:
重新编译算子后,我们得到了支持检测的算子。下一步就是用msSanitizer拉起算子进行执行:
mssanitizer -t racecheck ./add.fatbin
得到了如下检测结果:
异常分析
从msSanitizer的检测结果来看,算子确实存在竞争问题,我们需要通过分析检测报告来定位竞争发生的具体位置。
检测报告中提示共有两处竞争问题,第一处发生在 UB 上的 0x0 地址,提示PIPE_MTE2 流水写入和 PIPE_V 流水读取发生竞争;另中一种发生在 UB 上的0x100 地址,同样也是PIPE_MTE2 流水写入和 PIPE_V流水读取存在竞争。从代码位置来看两处竞争分别发生在57、64行以及58、64行之间。结合上面的算子源码,我们在PIPE_MTE2 流水和 PIPE_V 流水间做了正确的同步,为什么还是会有竞争问题呢?
此时我们可以结合工具输出的指令记录日志来辅助分析。打开日志,我们可以找到如下内容:
聚焦指令记录中关键的搬运、计算和同步事件,我们可以分析得到指令间的同步关系如下:
流水间的连线表示 SetFlag/WaitFlag 流水间同步配对关系,由上图可以发现PIPE_MTE2 上的搬运指令(DMA_MOV)和 PIPE_V上的计算指令(BINARY_OP)确实没有正确同步,会导致竞争问题。正确的同步配对关系应如下图表示:
我们可以根据日志中提示的代码行号确认多余的同步指令的位置,发现是在类的构造和析构函数中误插同步导致了同步指令配对错误,进而导致了竞争问题:
删除此处错误的同步指令后,重新编译执行算子,发现算子精度正常,并且工具也不再报告竞争异常:
总结
算子竞争问题由于偶现的特性,相对比较隐蔽,并且需要分析指令间的时序关系,一般比较难以定位。在大算子场景,手工分析竞争问题更加困难。使用msSanitizer对算子进行竞争检测,可以快速定位到竞争指令位置,帮助开发者定位竞争问题。
msSanitizer 链接:gitcode.com/Ascend/mssa…
扫描二维码,关注昇腾MindStudio微信公众号查看前沿昇腾科技!