掘金 人工智能 06月23日 14:17
离线推理精度问题分析
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文档针对将传统模型迁移至昇腾设备时可能出现的精度问题,提供了详细的定位方法和解决方案。主要聚焦于Pytorch module到onnx再到om模型的转换过程,通过对比在线和离线推理结果,分析可能导致精度问题的环节,并给出针对性的调试策略和优化建议。

🔍 精度问题定义:当离线推理结果与在线推理结果的差异超过1%时,即可认为存在精度问题,需要进行深入分析。

🛠️ 精度问题定界定位:通过对比在线和离线推理的输入、输出数据,判断问题是出在预处理、后处理还是模型推理部分,为后续定位提供方向。

🤔 模型推理精度定位:针对Pytorch到onnx到om的流程,分别分析onnx模型和om模型可能存在的精度问题,包括模型配置、权重加载、训练模式、算子精度等。

💡 ONNX模型精度问题常见原因:模型配置/权重加载不一致、未关闭training模式、导出时forward函数中输入参数不一致等。

⚙️ OM模型精度问题分析:重点关注ATC命令参数设置,如融合规则、精度模式、AIPP功能、量化以及Onnx改图等,并针对数据溢出、内存踩踏和算子精度问题提供具体解决方案和建议。

背景

本文档适用的场景是传统模型迁移到昇腾设备上(见下图),出现了精度问题,介绍精度问题的定位方法和解决方案。本文档只介绍“Pytorch module -> onnx -> om”场景。

什么是精度问题

离线推理(om模型在昇腾acl推理)的结果和在线推理(如:对原始模型通过pytorch/onnxruntime框架在gpu/cpu的推理)相差比较大,如在开源数据集的精度差距>1%,则认为离线推理精度不达标。这里是以在线推理的结果作为基准参考。

精度问题定界定位

精度问题定界

模型推理精度定位

确认了是在模型存在精度问题后,先来分析模型推理的过程:Pytorch module -> onnx -> OM

onnx模型精度问题常见原因

om模型精度问题分析

1.确认ATC命令参数设置

如果使用ONNX直接转成的原始OM存在精度问题,首先确认ATC命令的下列参数设置:--fusion_switch_file:融合规则(包括图融合和UB融合)开关配置文件路径以及文件名,修改融合规则,可能引起精度及性能变化。--precision_mode:设置整个网络模型的精度模式,若精度异常时,可尝试整网设置为FP32计算,从而判断是否为计算精度引起的误差。--op_precision_mode:指定算子内部处理时的精度模式--modify_mixlist:在内置优化策略基础上进行调整,自行指定哪些算子允许降精度,哪些算子不允许降精度。--optypelist_for_implmode:设置optype列表中算子的实现模式,算子实现模式包括high_precision、high_performance两种--customize_dtypes:模型编译时自定义某个或某些算子的计算精度。--keep_dtype:保持原始网络模型编译时个别算子的计算精度不变。

建议尝试设置--precision_mode=force_fp32,其他参数不设置。
2.是否开启AIPP功能

如果使用了--insert_op_conf参数,建议使用无AIPP的OM进行精度比对,以保证输入数据一致

3.是否使用量化

如果使用了AMCT/ModelSlim/--compression_optimize_conf参数做量化,建议使用原始模型确认精度,排查量化引入的精度问题

4.是否使用Onnx改图

建议使用改图前的原始onnx转om来确认精度,排查由于改图引入的精度问题

5.常见精度问题

精度问题分析实践

以Reset50为样例, 提供在线推理,离线推理、结果比对:参考ACL_PyTorch docs:01~04步骤

import numpy as npimport torchfrom torchvision.io import read_imagefrom torchvision.models import resnet50, ResNet50_Weightsimport onnxruntime as ortfrom ais_bench.infer.interface import InferSession# pytorch在线推理class PytorchInferencer:    def __init__(self):        weights = ResNet50_Weights.DEFAULT        self.model = resnet50(weights=weights)        self.model.eval()        self.transforms = weights.transforms()        self.categories = weights.meta["categories"]    def preprocess(self, image_path):        """预处理"""        # print(self.transforms)        img = read_image(image_path)        model_input = self.transforms(img).unsqueeze(0)        return model_input    def model_inference(self, model_input):        """执行推理"""        with torch.no_grad():            model_output = self.model(model_input)        return model_output    def postprocess(self, model_output):        """后处理"""        model_output = model_output.squeeze(0).softmax(0)        class_id = model_output.argmax().item()        score = model_output[class_id].item()        category_name = self.categories[class_id]        return dict(category=category_name, class_id=class_id, score=score)    def e2e_inference(self, image_path):        """端到端推理"""        model_input = self.preprocess(image_path)        model_output = self.model_inference(model_input)        prediction = self.postprocess(model_output)        return prediction        def export_to_onnx(self, image_path):        torch.onnx.export(            self.model,                # pytorch网络模型            self.preprocess(image_path),          # 随机的模拟输入            "resnet.onnx",        # 导出的onnx文件位置            export_params=True,   # 导出训练好的模型参数            verbose=True,         # verbose=True,支持打印onnx节点和对应的PyTorch代码行            training=torch.onnx.TrainingMode.EVAL,  # 导出模型调整到推理状态,将dropout,BatchNorm等涉及的超参数固定            input_names=["input_data"],    # 为静态网络图中的输入节点设置别名,在进行onnx推理时,将input_names字段与输入数据绑定            output_names=["output_data"],  # 为输出节点设置别名            # 如果不设置dynamic_axes,那么对于输入形状为[1, 3, 224, 224],在以后使用onnx进行推理时也必须输入[1, 3, 224, 224]            # 下面设置了输入的第0维是动态的,以后推理时batch_size的大小可以是其他动态值            #dynamic_axes={            #    "input_data": {0: "-1"},            #    "output_data": {0: "-1"}            #},            keep_initializers_as_inputs=None,  #是否将模型参数作为输入数据的一部分进行导出            opset_version=17                  # ONNX 运算符的版本号        )        print("export onnx model successfully!!")# onnx在线推理class OnnxInferencer(PytorchInferencer):    def __init__(self, onnx_path):        super(OnnxInferencer, self).__init__()        # 可以构造预处理不一样,如修改归一化均值        # self.transforms.__dict__["mean"] = [0.1, 0.1, 0.1]        self.session = ort.InferenceSession(onnx_path)    def model_inference(self, model_input):        input_data = {"input_data": model_input.numpy()}        model_output = self.session.run([], input_data)        print(f"onnx output type: {type(model_output)}, onnx output[0] type: {type(model_output[0])}")        model_output = torch.from_numpy(model_output[0])        return model_output# om离线推理class OmInferencer(PytorchInferencer):    def __init__(self, om_path, device_id=0):        super(OmInferencer, self).__init__()        self.session = InferSession(device_id=device_id, model_path=om_path)    def model_inference(self, model_input):        # 可以取值'static'(静态模型)、'dymbatch'(动态batch模型)、'dymhw'(动态分辨率模型)、'dymdims'(动态dims模型)、'dymshape'(动态shape模型)        mode = "static"         # om模型推理输入输出都是numpy.array格式,而在线推理的模型输入输出是torch.Tensor,注意转换。        model_output = torch.from_numpy(self.session.infer(feeds=[model_input.numpy()], mode=mode)[0])        return model_outputinferencer = PytorchInferencer()print(inferencer.e2e_inference("ILSVRC2012_val_00006083.jpeg"))# {'category': 'Yorkshire terrier', 'score': 0.2925560474395752}# export onnx modelinferencer.export_to_onnx("ILSVRC2012_val_00006083.jpeg")# 在shell执行atc模型转换"""atc --framework=5 --model=resnet.onnx --output=resnet50_bs1 --input_format=NCHW --input_shape=input_data:1,3,224,224 --log=error --soc_version=Ascend910B4"""inferencer = OmInferencer('./resnet50_bs1.om')print(inferencer.e2e_inference("./ILSVRC2012_val_00006083.jpeg"))# {'category': 'Yorkshire terrier', 'score': 0.2925560474395752}# 在线推理和离线推理结果对比def precision_compare(pth_output, om_output):    pth_output1 = pth_output.flatten().astype(np.float64)    om_output1 = om_output.flatten().astype(np.float64)    cosine_similarity = np.dot(pth_output1, om_output1) \                        / (np.linalg.norm(om_output1) * np.linalg.norm(om_output1))    absolute_errors = np.abs(om_output1 - pth_output1)    relative_errors = absolute_errors / pth_output1 * 100    print('余弦相似度:', cosine_similarity)    print('最大绝对误差:', absolute_errors.max())    print('最大相对误差:', relative_errors.max() )def run_precision():    pth_inferencer = PytorchInferencer()    om_inferencer = OmInferencer('./resnet50_bs1.om')    model_input = pth_inferencer.preprocess('./ILSVRC2012_val_00006083.jpeg')    pth_output = pth_inferencer.model_inference(model_input).numpy()    om_output = om_inferencer.model_inference(model_input).numpy()    precision_compare(pth_output, om_output)

模型输入输出导出

# 输入输出统一转成numpy.array格式,保存为.npy文件# 1.pytorch在线推理,模型输入输出格式为torch.Tensorimport numpy as npnp.save("input_pth.npy", pth_input.numpy())np.save("output_pth.npy", pth_output.numpy())# 2.onnx在线推理,模型输入格式为np.array,输出为list,取索引0,格式为np.arraynp.save("input_onnx.npy", onnx_input)np.save("output_onnx.npy", onnx_output[0])# 3. om离线推理,输入输出格式为np.arraynp.save("input_om.npy", om_input)np.save("output_om.npy", om_output)# 也可以通过ais_bench工具dump 输出'''python -m ais_bench --model ./resnet50_bs1.om --input ./input_pth.npy --output ./ais_bench_out --outfmt NPY'''

模型输入输出比对

import numpy as npfrom scipy import spatialdef compare(path1, path2):    # 1.直接比较是否完全一致    print("是否完全一致:", np.array_equal(path1, path2))    # 2.浮点数的容差比较    print("是否近似相同:", np.allclose(path1, path2))    # 3.余弦相似度(适合向量/特征),常用的判断标准为余弦相似度大于0.99    cos_sim = 1 - spatial.distance.cosine(path1.reshape(-1), path2.reshape(-1))    print("余弦相似度:", cos_sim)def input_output_compare(ref1, ref2):    print(f"{ref1} vs {ref2} input compare: ")    input1 = np.load(f"input_{ref1}.npy")    input2 = np.load(f"input_{ref2}.npy")    compare(input1, input2)        print("---" * 30)    print(f"{ref1} vs {ref2} output compare: ")    input1 = np.load(f"output_{ref1}.npy")    input2 = np.load(f"output_{ref2}.npy")    compare(input1, input2)    print("---" * 30)for values in [("pth", "onnx"), ("pth", "om"), ("onnx", "om")]:    input_output_compare(*values)

OM模型精度定位

Mindstudio精度调试工具:链接

1.定位问题算子

详细说明参考:result_analyse

精度分析工具:msit debug compare使用msit debug compare功能:使用指导

msit debug compare -gm ./resnet.onnx -om ./resnet50_bs1.om -i ./input_onnx.npy -o ./msit_compare

查看result_{timestamp}.csvresult各字段说明及分析说明:链接

使用专家建议:

msit debug compare -gm ./resnet.onnx -om ./resnet50_bs1.om -i ./input_onnx.bin -o ./msit_compare/advisor --advisor

由于我测试的onnx和om无精度差异问题,故专家建议也是认为没问题。

2.验证单算子精度

构造单算子模型,使用相同的输入,验证输出精度

提取单算子:方式1:使用msit debug surgeon工具提取算子:链接

msit debug surgeon extract --input resnet.onnx --output-file sub_ops.onnx --start-node-names "/layer2/layer2.3/conv1/Conv" --end-node-names "/layer2/layer2.3/conv1/Conv"

方式2:使用改图工具构造单算子

from auto_optimizer import OnnxGraph# 创建单算子ONNXop_model = OnnxGraph('conv.onnx')# 读取原ONNX模型model = OnnxGraph.parse('resnet50.onnx')# 找到问题算子node = model['Conv_1']# 复制算子至单算子模型op_model.add_node(node.name, node.op_type, inputs=node.inputs, outputs=node.outputs, attrs=node.attrs)init = [node.name for node in model.initializers]for inp in node.inputs:    if inp in init:        op_model.add_initializer(inp, model[inp].value)    else:        op_model.add_input(inp, dtype='float32', shape=[]) # shape若为空,则转OM时需指定input_shapefor out in node.outputs:    op_model.add_output(out, dtype='float32', shape=[])# 保存单算子模型op_model.save('conv.onnx')

3.累计误差问题定位

如果问题算子的单算子精度正常,则说明算子输入有问题,或可能是累积误差导致的精度下降,需要前向二分定位关键算子。

    将ONNX模型截断为两部分,第一个ONNX模型转为OM,并使用OM做第一步推理,OM的输出作为输入使用ONNX做第二步推理。因为ONNX模型为精度的标杆,可认为第二步推理的精度完全正确。

    如果ONNX的最终输出有问题,则说明OM的输出已有累积误差问题,OM中包含问题算子,截断位置变为输入与当前截断位置的中间;如果ONNX输出结果正常,说明OM的输出正常,问题算子在ONNX中,截断位置变为当前截断位置与精度下降位置的中间。

    重复步骤1、2直到定位出最小问题区间,确认单算子精度,如果单算子精度正常,则说明数据对算子精度比较敏感,可使用FP32或不使用fusion pass融合算子等方法提高算子精度,使用方法参考ONNX转OM。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

昇腾 精度问题 模型迁移 OM模型 ONNX
相关文章