作者​:昇腾实战派
知识地图​:https://blog.csdn.net/Lumos_Lovegood/article/details/161601003

背景概述

随着AI应用场景的多样化发展,开发者常需将已在GPU或CPU上训练好的小模型迁移至昇腾NPU上进行推理部署。然而,由于不同硬件平台在算子实现、数据类型支持等方面的差异,模型迁移后可能出现精度下降的问题。本文系统性地介绍了一套从整体输出比对到逐算子排查的精度定位方法论,并提供了常见精度问题的解决方案,帮助开发者高效完成模型迁移与精度调优。

1. 定位方法论

小模型精度的定位流程如下图所示。首先,在相同输入下对比GPU/CPU与NPU的输出tensor,若余弦相似度≥0.999且最大绝对误差≤0.001,则认为精度达标;否则进入精度排查步骤。排查时,使用msprobe工具一键dump并比对ONNX模型与OM模型中各算子的输出tensor,定位到第一个精度异常的算子,最后对该算子进行深入分析。

在这里插入图片描述

1.1. 模型输出tensor对比

以下脚本实现了Pth模型(CPU)、ONNX模型(CPU)和OM模型(NPU)的输出精度比对。若需使用GPU作为标杆,只需将device和providers改为cuda相关配置即可。

import argparse
import numpy as np
import torch
import onnxruntime
from ais_bench.infer.interface import InferSession

def preprocess_image(img):
    if img is None:
        img = np.random.randint(0, 255, size=(112, 112, 3), dtype=np.uint8)
    else:
        img = cv2.imread(img)
        img = cv2.resize(img, (112, 112))
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img = np.transpose(img, (2, 0, 1))
    img = torch.from_numpy(img).unsqueeze(0).float()
    img.div_(255).sub_(0.5).div_(0.5)
    return img

def onnx_infer(onnx_model_path, img):
    session = onnxruntime.InferenceSession(onnx_model_path, providers=['CPUExecutionProvider'])
    input_name = session.get_inputs()[0].name
    model_output = session.run(None, {input_name: img.numpy()})[0]
    return model_output

def om_infer(model, img, device_id):
    session = InferSession(device_id, model_path=model)
    model_output = session.infer(feeds=[img.numpy()])[0]
    return model_output

@torch.no_grad()
def pytorch_infer(weight, name, img):
    device = torch.device('cpu')
    model = ModelClass(...)
    model.load_state_dict(torch.load('model_weight.pth'), map_location=device)
    model.eval()
    feat = model(img).numpy()
    return feat

def precision_compare(output1, output2, label1="Output1", label2="Output2"):
    output1_flat = output1.flatten().astype(np.float64)
    output2_flat = output2.flatten().astype(np.float64)
    cosine_similarity = np.dot(output1_flat, output2_flat) / (np.linalg.norm(output1_flat) * np.linalg.norm(output2_flat))
    absolute_errors = np.abs(output2_flat - output1_flat)
    relative_errors = absolute_errors / (np.abs(output1_flat) + 1e-10) * 100
    print(f'{label1} vs {label2}:')
    print(f'  余弦相似度: {cosine_similarity:.10f}')
    print(f'  最大绝对误差: {absolute_errors.max():.10e}')
    print(f'  最大相对误差(%): {relative_errors.max():.10e}')

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='ArcFace Model Precision Compare')
    parser.add_argument('--network', type=str, default='r50', help='backbone network')
    parser.add_argument('--weight', type=str, default='', help='PyTorch权重路径')
    parser.add_argument('--onnx', type=str, default=None, help='ONNX模型路径')
    parser.add_argument('--img', type=str, default=None, help='图片路径')
    parser.add_argument('--model', type=str, default=None, help='OM模型路径')
    parser.add_argument('--device', type=int, default=0, help='NPU device ID')
    args = parser.parse_args()

    img = preprocess_image(args.img)
    outputs = {}
    if args.weight:
        outputs['PyTorch'] = pytorch_infer(args.weight, args.network, img)
    if args.onnx:
        outputs['ONNX'] = onnx_infer(args.onnx, img)
        print(f"ONNX output shape: {outputs['ONNX'].shape}")
    if args.model:
        outputs['OM'] = om_infer(args.model, img, args.device)
        print(f"OM output shape: {outputs['OM'].shape}")

    if 'PyTorch' in outputs and 'ONNX' in outputs:
        precision_compare(outputs['PyTorch'], outputs['ONNX'], 'PyTorch', 'ONNX')
    if 'ONNX' in outputs and 'OM' in outputs:
        precision_compare(outputs['ONNX'], outputs['OM'], 'ONNX', 'OM')
    if 'PyTorch' in outputs and 'OM' in outputs:
        precision_compare(outputs['PyTorch'], outputs['OM'], 'PyTorch', 'OM')

使用步骤:

  1. 数据预处理:可使用numpy生成固定随机值,或对同一张图片进行预处理得到tensor。

  2. 分别加载Pth、ONNX和OM模型进行推理,保存输出结果。

  3. 两两对比输出tensor,输出余弦相似度、最大绝对误差和最大相对误差,如下图所示:
    在这里插入图片描述

  4. 若余弦相似度≥0.999,则认为精度达标;否则进入后续排查步骤。

1.2. 精度数据dump和对比

使用msprobe工具可一键dump并对比ONNX模型与OM模型中各算子的输出精度(工具下载地址)。命令如下:

msprobe compare -m offline_model -gp /golden_path/golden_model.onnx -tp /target_path/target_path.om -o ./compare_output --input_shape "input_name:1,3,112,112"

参数说明:

  • -m:比对模式,需指定为offline_model。
  • -gp:标杆模型文件(.onnx)路径。
  • -tp:对比的昇腾AI处理器离线模型(.om)路径。
  • -o:结果输出路径,默认为当前路径的output文件夹。
  • -input_shape:模型输入为静态shape时使用,例如"input_name1:1,224,224,3;input_name2:3,300",节点间用英文分号隔开。

更多用法详见链接

1.3. 比对结果说明及分析

输出结果文件目录结构如下:

{output_path}/{timestamp}/{input_name-input_shape}  # {input_name-input_shape}用来区分动态shape时不同的模型实际输入,静态shape时没有该层
├-- dump_data
│   ├-- npu                          # npu dump数据目录
│   │   ├-- {timestamp}              # 模型所有npu dump的算子输出,dump为False情况下没有该目录
│   │   │   └-- 0                    # Rank设备ID号
│   │   │       └-- {om_model_name}  # 模型名称
│   │   │           └-- 1            # 模型ID号
│   │   │               ├-- 0        # 针对每个Task ID执行的次数维护一个序号,从0开始计数,该Task每dump一次数据,序号递增1
│   │   │               │   ├-- Add.8.5.1682067845380164
│   │   │               │   ├-- ...
│   │   │               │   └-- Transpose.4.1682148295048447
│   │   │               └-- 1
│   │   │                   ├-- Add.11.4.1682148323212422
│   │   │                   ├-- ...
│   │   │                   └-- Transpose.4.1682148327390978
│   │   ├-- {timestamp}
│   │   │   ├-- output_0.bin
│   │   │   └-- output_0.npy
│   │   └-- {timestamp}_summary.json
│   └-- {onnx} # 原模型dump数据存放路径,onnx对应ONNX模型
│       ├-- Add_100.0.1682148256368588.npy
│       ├-- input_Add_100.0.1682148256368588.npy  # 如果是ONNX模型,则会dump输入数据,并增加对应的input前缀
│       ├-- ...
│       └-- Where_22.0.1682148253575249.npy
├-- input
│   └-- input_0.bin                          # 随机输入数据,若指定了输入数据,则该文件不存在
├-- model
│   ├-- {om_model_name}.json                    # 离线模型OM模型(.om)通过atc工具转换后的json文件
│   └-- new_{onnx_model_name}.onnx              # 把每个算子作为输出节点后新生成的ONNX模型
└-- result_{timestamp}.csv                   # 比对结果文件

比对结果位于result_{timestamp}.csv中,示例如下:
在这里插入图片描述

需关注以下字段:

  • DateType:NPU侧与CPU/GPU侧的数据类型,若不同可能存在精度损失。
  • IsNpuOps:是否为NPU独有节点,无需关注。
  • IsPrecisionError:是否为精度异常节点,需重点关注第一个精度异常的节点。
  • CosineSimilarity…MeanRelativeError:各类误差比对结果,若某项超过阈值则需重点关注。各算法说明见链接

精度指标参考标准如下:

误差对比算法 正常参考标准
CosineSimilarity > 0.99
RelativeEuclideanDistance < 0.05
KullbackLeiblerDivergence < 0.005
RootMeanSquareError < 1.0
MeanRelativeError < 1.0

根据比对结果找到第一个精度异常的算子,进行进一步分析。对于小模型,精度问题通常由数据类型强制转换导致的数值溢出引起,因为ATC在转换模型时可能强制转换某些算子的数据类型。

2. 常见精度问题解决方法

2.1. 设置精度模式

对于Atlas 800I A2或Atlas 800I A3产品,因其支持fp32数据格式,可通过ATC转换命令中的--precision_mode参数设置精度模式,避免数值溢出。例如,must_keep_origin_dtype表示保持原图精度,转换原则如下:

  • 若AI Core支持原图中算子的精度,则采用该算子。
  • 若原图算子为float16,AI Core不支持float16但支持float32和bfloat16,则自动采用高精度float32。
  • 若原图算子为float16,AI Core仅支持bfloat16,则使用float16的AI CPU算子;若AI CPU也不支持,则报错。
  • 若原图算子为float32,AI Core不支持float32仅支持float16,则使用float32的AI CPU算子;若AI CPU也不支持,则报错。

其他参数取值介绍见链接

用法示例:

--precision_mode=must_keep_origin_dtype

不同精度模式的精度与性能排序如下:

  • 精度高低:force_fp32 > must_keep_origin_dtype > allow_fp32_to_fp16 > allow_mix_precision > force_fp16
  • 性能优劣:force_fp16 >= allow_mix_precision > allow_fp32_to_fp16 > must_keep_origin_dtype > force_fp32

2.2. 算子修改

对于部分产品,许多算子的输入输出仅支持fp16,而原始ONNX算子通常为fp32,转换过程中存在fp32到fp16的转换,容易导致数值溢出。针对此类算子精度问题,需在算子侧进行修改,具体方法因算子而异。

以conv2d算子为例,若其输入输出仅支持fp16,而原始模型基于fp32训练,卷积计算所得数值可能超出fp16范围。可通过缩放方式解决:将卷积权重(或输入)缩小x倍,由于卷积为线性操作,输出结果相应缩小x倍,从而将结果限制在fp16范围内。卷积计算完成后,将输出cast回fp32,再添加一个Mul算子将结果放大x倍。x的具体数值需根据模型实际情况确定

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐