目录
前言
上周我在华为昇腾Atlas200DK中部署nanodet和yolov5时,出现了目标检测结果不对(没有检测到任何物体),困扰了整整三天,最后找到原因并圆满解决,在这里记录一下debug过程。
以上测试均在python中完成。
问题描述
第一天上午,我首先将nanodet由pytorch的.pth模型转换为.onnx模型,并通过cv2.dnn模块实现仅依赖于opencv的nanodet推理部署main_nanodet.py,该部署文件能够得到正确的检测结果。
随后,我通过atlas_utils(上一篇博客中提到的CANN官方封装接口)将main_nanodet.py改造成在CANN上推理部署的acl_nanodet.py,并且仔细梳理了该acl实现的流程和细节,如下图所示:
其中预处理模块如下:
def pre_process(self, image):
"""
preprocess
"""
image_resized = self.padding_resize_image(image)
image_norm = self._normalize(image_resized.astype(np.float32))
image_inp = np.transpose(image_norm, (2, 0, 1))
return image_inp
然后通过CANN atc工具将.onnx模型转换成为.om模型,并运行acl_nanodet.py,运行中无任何报错,后处理模块将输出结果保存在了outputs文件夹中
随后我通过ssh scp工具将结果拷贝到主机上,发现没有任何检测框,开始查错(踩坑)。
问题排查1.后处理模块
我将acl_nanodet.py中模型推理execute()的结果打印出来,发现模型推理有输出,而后处理没有检测出目标,合理怀疑时后处理出了问题,于是我仔细排查了acl_nanodet.py的后处理代码,发现没啥问题,因此我将main_nanodet.py中onnx模型推理的输出,通过np.save()保存下来,直接喂给acl_nanodet.py的后处理,结果outputs文件夹中得到了正确的检测结果,表明后处理没问题,bug在其他地方。
问题排查2.预处理模块
将main_nanodet.py预处理后的数据和acl_nanodet.py预处理后的数据通过np.save()保存后,进行比对,发现几乎没有区别,表明预处理没问题,bug在其他地方。
问题排查3.模型
将main_nanodet.py于acl_nanodet.py模型推理得到的输出通过np.save()保存,发现两者模型推理结果有很大差异,表明是模型推理出现问题。
合理怀疑atc模型转换出现问题,根据官方转换方法重新将.onnx模型转换成.om模型,然后测试,发现推理结果仍然不对,我十分疑惑。
此时我自己debug的经验已经吃不住了,到昇腾官方论坛寻找类似结果,未果。到华为atlas官方文档寻找资料,未果。
然后我将nanodet的onnx模型更换为nanodet_m的onnx模型,这两个模型只在层数和输出尺寸上有区别,因此acl_nanodet.py可以共用,修改超参数就行。
结果仍然不能获得目标检测结果。
问题排查4.算法bug
由于nanodet是较新的算法,可能有一些比较新的算子导致AI芯片计算出现问题,因此第二天我又将yolov5算法移植到atlas200dk中,发现仍然无法检测到目标????人比较懵了
问题排查5.AI板卡
由于后处理、预处理和模型都没问题,因此怀疑该AI板卡芯片出现bug。因此我把Ascend samples中的yolov4例程拷贝下来并进行测试,得到正确的检测结果,表明板卡没问题,此时心里是真的????不知所措
问题排查6.模型精度
根据我的嵌入式模型部署经验,一般模型精度从fp32到fp16不大影响检测结果,而fp16到int8则会造成检测精度掉点,但也不会完全检测不出来东西。
死马当活马医,使用atc模型,将精度设定flag为不改变原模型精度,但仍然没有检测结果,没啥心里感受(因为知道大概不是精度问题)
问题排查6.模型推理
经过了一整天的debug,仍然未找到问题,但由于预处理和后处理没问题,板卡没问题,因此一定是模型推理的问题。
第三日晚,偶然看到官方samples的issue里其他开发者提到无检测结果的问题,官方回复是检查预处理、模型和后处理。
想了想既然检查了预处理,那有没有可能输入模型的数据根本没有预处理,或者说只是”假预处理“?
于是将模型输入数据用np.save()保存下来,发现真的和main_nanodet.py的模型输入数据不同!
问题解决:模型输入数据与预处理不同
我是通过函数的方法,将imread读入的image通过pre_process(): return预处理结果image_inp,然后将预处理结果进行execute([image_inp, ]),但是execute的输入却与image_inp不同
然后我又查阅了pyACL的原生C接口,发现execute()这个函数封装的acl模型推理接口是直接读取内存中指定位置的数据,而execute()封装的实现是把image_inp这个数据的内存地址拷贝到模型读数据的内存地址,也就是说image_inp的内存地址储存的数据并没有进行预处理。
回到pre_process()的实现中,有一句
image_inp = np.transpose(image_norm, (2, 0, 1))
numpy.transpose这个函数是个”lazy“函数,不对内存中的数据进行重排,仅仅改变读取方式(也就是读取轴顺序),这就导致了传入execute()中的image_inp实际上是没有真正transpose的。
解决方法很简单,为transpose后的image_inp开辟一块新内存就行了,比如numpy.copy()深拷贝:
def pre_process(self, image):
"""
preprocess
"""
image_resized = self.padding_resize_image(image)
image_norm = self._normalize(image_resized.astype(np.float32))
image_inp = np.transpose(image_norm, (2, 0, 1))
return image_inp.copy()
后记
踩坑很痛苦,解决很快乐~