昇腾系列--yolo26移植:使用atc将模型yolo26转换成om,基于昇腾310推理并计算MAP,误差达到千分之一
本文介绍 YOLOv26 模型在昇腾 NPU 上的完整部署流程涵盖环境搭建、模型转换、模型推理以及 MAP 精度计算四个关键环节。通过本文读者可以了解如何将 YOLOv26 的 PyTorch 模型导出为 ONNX再借助 ATC 工具转换为昇腾 NPU 可执行的 OM 模型并最终在 Atlas 系列推理设备上完成推理与精度验证。本文适合以下读者希望在昇腾 NPU 上部署 YOLO 系列检测模型的算法工程师需要完成模型转换、推理和精度评估的开发者对 Atlas 300I 等推理设备上模型移植流程感兴趣的入门者。目录导航一、环境搭建二、模型转换三、模型推理四、计算 MAP一、环境搭建环境信息ultralytics8.4.3pytorch2.9.1cann8.0.1HDK24.1.1.3硬件信息Atlas300Idriver 安装包下载A300-3010-npu-driver_24.1.1.3_linux-aarch64.runfirmware 安装包下载A300-3010-npu-firmware_7.5.0.9.220.runcann 安装包下载Ascend-cann-toolkit_8.0.1_linux-aarch64.run基础环境搭建参考Qwen2.5-Omni-7B环境搭建及aisbench压测指导yolo26 移植代码开源地址yolo26-npu二、模型转换1、将模型 yolo26s.pt 导出为 onnx 格式python3 pth2onnx.py --pt ./models/yolo26s.ptpth2onnx.py 源代码如下import argparse from ultralytics import YOLO def main(): parser argparse.ArgumentParser() parser.add_argument(--pt, default./models/yolo26s.pt, helppt file) args parser.parse_args() model YOLO(args.pt) onnx_model model.export(formatonnx, dynamicTrue, simplifyTrue, opset11) if name main: main()2、atc工具使用说明文档地址ATC工具8.5.0参数说明3、将 onnx 模型转换成 omatc --framework5 \--model./models/yolo26s.onnx \--input_formatNCHW \--input_shapeimages:1,3,640,640\--output_typeFP32 \--output./models/yolo26s_bs1 \--loginfo \--soc_versionAscend310--input_shape: 模型入参尺寸 1*3*640*640--input_format: 模型入参格式N 是batchsizeC 图像通道H 图像高W 图像宽--output_type: 模型输出数值是FP32--soc_version: 推理环境使用的芯片型号推理设备soc_versionAtlas300IAscend310Atlas300VAscend310P3Atlas300I ProAscend310P3Atlas300V ProAscend310P3Atlas300I DuoAscend310P3Atlas800T A2Ascend910B3Atlas800I A2Ascend910B4三、模型推理推理脚本 inference.py 代码如下import os import json import argparse import torch import numpy as np from ais_bench.infer.interface import InferSession from ultralytics.models.yolo.detect import DetectionPredictor from ultralytics import YOLO patch cpu lettebox begin def patch_pre_transform(self, im): same_shapes len({x.shape for x in im}) 1 self.model.pt False letterbox LetterBox(self.imgsz, autosame_shapes and self.model.pt, strideself.model.stride) return [letterbox(imagex) for x in im] from ultralytics.data.augment import LetterBox from ultralytics.engine.predictor import BasePredictor BasePredictor.pre_transform patch_pre_transform patch cpu lettebox end class OM_Conf(): def init(self): self.ptFalse self.stride 32 self.fp16 False self.overrides {task: detect, data: /usr/local/lib/python3.10/dist-packages/ultralytics/cfg/datasets/coco.yaml, imgsz: 640, single_cls: False, model: ./mo dels/yolo26s_bs1.om, conf: 0.25, batch: 1, save: True, mode: predict, save_txt: True} self.names {0: person, 1: bicycle, 2: car, 3: motorcycle, 4: airplane, 5: bus, 6: train, 7: truck, 8: boat, 9: traffic light, 10: fire hydrant, 11: stop sign, 12: parking meter, 13: bench, 14: bird, 15: cat, 16: dog, 17: horse, 18: sheep, 19: cow, 20: elephant, 21: bear, 22: zebra, 23: giraffe, 24: backpack, 25: umbrella, 26: handbag, 27: tie, 28: suitcase, 29: frisbee, 30: skis, 31: snowboard, 32: sports ball, 33: kite, 34: baseball bat, 35: baseball glove, 36: skateboard, 37: surfboard, 38: tennis racket, 39: bottle, 40: wine glass, 41: cup, 42: fork, 43: knife, 44: spoon, 45: bowl, 46: banana, 47: app le, 48: sandwich, 49: orange, 50: broccoli, 51: carrot, 52: hot dog, 53: pizza, 54: donut, 55: cake, 56: chair, 57: couch, 58: potted plant, 59: bed, 60: dining table, 61: toilet, 62: tv, 63: laptop, 64: mouse, 65: remote, 66: keyboard, 67: cell phone, 68: microwave, 69: oven, 70: toaster, 71: sink, 72: ref rigerator, 73: book, 74: clock, 75: vase, 76: scissors, 77: teddy bear, 78: hair drier, 79: toothbrush} def save_txt(boxes_data, filepath, pt_result): filename filepath.split(/)[-1] res_filepath pt_result / filename.replace(.jpg,.txt) os.makedirs(os.path.dirname(res_filepath), exist_okTrue) with open(res_filepath, w) as json_fp: for box in boxes_data: json_dict dict() json_dict[filename] filename json_dict[x0] round(float(box[0]),5) json_dict[y0] round(float(box[1]),5) json_dict[width] round((float(box[2]) - json_dict[x0]),5) json_dict[height] round((float(box[3]) - json_dict[y0]),5) json_dict[class] int(box[5]) json_dict[score] round(float(box[4]),5) json_str json.dumps(json_dict) json_fp.write(json_str\n) def pt_detect(input_args): model YOLO(input_args.pt) output model(sourceinput_args.data, saveTrue, save_txtTrue) # ndarray for i in output: print(pt result:, i.boxes.data) save_txt(i.boxes.data, i.path, input_args.pt_result) def om_detect(input_args): om_model InferSession(int(input_args.device_id), input_args.om) #data om_conf OM_Conf() dp DetectionPredictor(overridesom_conf.overrides) dp.model om_conf dp.setup_source(input_args.data) for dp.batch in dp.dataset: paths, im0s, s dp.batch # Preprocess im dp.preprocess(im0s) Inference im np.ascontiguousarray(im).astype(np.float32) # contiguous preds om_model.infer([im]) preds_tensor torch.from_numpy(preds[0]) Postprocess output dp.postprocess(preds_tensor, im, im0s) print(om result:, output[0].boxes.data) save_txt(output[0].boxes.data, output[0].path, input_args.om_result) def main(): parser argparse.ArgumentParser() parser.add_argument(--data, default./data, helpdata path) parser.add_argument(--pt, default./models/yolo26s.pt, helppt model path) parser.add_argument(--om, default./models/yolo26s_bs1.om, helpom model path) parser.add_argument(--pt_result, default./pt-result, helppt model result path) parser.add_argument(--om_result, default./om-result, helpom model result path) parser.add_argument(--device_id, default0, helpdevice id) input_args parser.parse_args() pt_detect(input_args) om_detect(input_args) if name main: main()om 模型推理逻辑与 ultralytics 保持一致执行推理脚本并查看pt模型和om模型的推理结果python3 inference.py推理结果验证yolo26s.pt 和yolo26s_bs1.om 的推理结果一致四、计算MAP1、推理val2017数据集并保存推理结果每张图片保存一个推理结果MAP 值计算采用 COCO 数据集自带的 MAP 计算方法eval_map.py 代码如下#!/usr/bin/python import sys import os import json from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval If necessary, pre-define category and its id PRE_DEFINE_CATEGORIES {0: person, 1: bicycle, 2: car, 3: motorcycle, 4: airplane, 5: bus, 6: train, 7: truck, 8: boat, 9: traffic light, 10: fire hydrant, 11: stop sign, 12: parking meter, 13: bench, 14: bird, 15: cat, 16: dog, 17: horse, 18: sheep, 19: cow, 20: elephant, 21: bear, 22: zebra, 23: giraffe, 24: backpack, 25: umbrella, 26: handbag, 27: tie, 28: suitcase, 29: frisbee, 30: skis, 31: snowboard, 32: sports ball , 33: kite, 34: baseball bat, 35: baseball glove, 36: skateboard, 37: surfboard, 38: tennis racket, 39: bottle, 40: wine glass, 41: cup, 42: fork , 43: knife, 44: spoon, 45: bowl, 46: banana, 47: apple, 48: sandwich, 49: orange, 50: broccoli, 51: carrot, 52: hot dog, 53: pizza, 54: don ut, 55: cake, 56: chair, 57: couch, 58: potted plant, 59: bed, 60: dining table, 61: toilet, 62: tv, 63: laptop, 64: mouse, 65: remote, 66: keyboard, 67: cell phone, 68: microwave, 69: oven, 70: toaster, 71: sink, 72: refrigerator, 73: book, 74: clock, 75: vase, 76: scissors, 77: t eddy bear, 78: hair drier, 79: toothbrush} CATEGORY_ID {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10, 10: 11, 11: 13, 12: 14, 13: 15, 14: 16, 15: 17, 16: 18, 17: 19, 18: 20, 19: 21, 20: 22, 21: 23, 22: 24, 23: 25, 24: 27, 25: 28, 26: 31, 27: 32, 28: 33, 29: 34, 30: 35, 31: 36, 32: 37, 33: 38, 34: 39, 35: 40, 36: 41, 37: 42, 38: 43, 39: 44, 40: 46, 41: 47, 42: 48, 43: 49, 44: 50, 45: 51, 46: 52, 47: 53, 48: 54, 49: 55, 50: 56, 51: 57, 52: 58, 53: 59, 54: 60, 55: 61, 56: 62, 57: 63, 58: 64, 59: 65, 60: 67, 61: 70, 62: 72, 63: 73, 64: 74, 65: 75, 66: 76, 67: 77, 68: 78, 69: 79, 70: 80, 71: 81, 72: 82, 73: 84, 74: 85, 75: 86, 76: 87, 77: 88, 78: 89, 79: 90} def get_filename_as_int(filename): try: filename filename.replace(\, /) filename os.path.splitext(os.path.basename(filename))[0] return int(filename) except: raise ValueError(Filename %s is supposed to be an integer. % (filename)) def get_default_dict(): return {image_id: -1, category_id: -1, bbox: [], score: 0} def xml2json(xml_files): json_out [] for xml_file in xml_files: f open(xml_file) line f.readline() if len(line) 0: #print(xml_file) continue one json.loads(line) filename one[filename] ## The filename must be a number image_id get_filename_as_int(filename) while line: t_dict get_default_dict() t_dict[image_id] image_id t_dict[category_id] CATEGORY_ID[one[class]] t_dict[bbox] [one[x0], one[y0], one[width], one[height]] t_dict[score] one[score] json_out.append(t_dict) line f.readline() if len(line) 0: break one json.loads(line) f.close() return json_out def get_img_id(cocoDt_json): ls [] myset [] for anno in cocoDt_json: ls.append(anno[image_id]) myset {}.fromkeys(ls).keys() return myset Average Precision (AP) [ IoU0.50:0.95 | area all | maxDets100 ] 0.317 Average Precision (AP) [ IoU0.50 | area all | maxDets100 ] 0.562 Average Precision (AP) [ IoU0.75 | area all | maxDets100 ] 0.321 Average Precision (AP) [ IoU0.50:0.95 | area small | maxDets100 ] 0.162 Average Precision (AP) [ IoU0.50:0.95 | areamedium | maxDets100 ] 0.343 Average Precision (AP) [ IoU0.50:0.95 | area large | maxDets100 ] 0.448 Average Recall (AR) [ IoU0.50:0.95 | area all | maxDets 1 ] 0.278 Average Recall (AR) [ IoU0.50:0.95 | area all | maxDets 10 ] 0.438 Average Recall (AR) [ IoU0.50:0.95 | area all | maxDets100 ] 0.464 Average Recall (AR) [ IoU0.50:0.95 | area small | maxDets100 ] 0.275 Average Recall (AR) [ IoU0.50:0.95 | areamedium | maxDets100 ] 0.497 Average Recall (AR) [ IoU0.50:0.95 | area large | maxDets100 ] 0.625 def eval_compute(cocoDt_json,cocoGt_file): cocoGt COCO(cocoGt_file)#取得标注集中coco json对象 imgIds get_img_id(cocoDt_json) cocoDt cocoGt.loadRes(cocoDt_json)#取得结果集中image json对象 imgIds sorted(imgIds)#按顺序排列coco标注集image_id cocoEval COCOeval(cocoGt, cocoDt, bbox) cocoEval.params.imgIds imgIds#参数设置 cocoEval.evaluate()#评价 cocoEval.accumulate()#积累 cocoEval.summarize()#总结 if name main: import argparse parser argparse.ArgumentParser( descriptionConvert Pascal VOC annotation to COCO format. ) parser.add_argument(--xml_dir, default./om-result, helpDirectory path to xml files., typestr) parser.add_argument(--instances_file, default/data/LLMs/instances_val2017.json, helpCOCO format instances_val2017.json., typestr) args parser.parse_args() xml_files [] for root, dirs, files in os.walk(args.xml_dir): for file in files: xml_files.append(os.path.join(root, file)) If you want to do train/test split, you can pass a subset of xml files to convert function. print(Number of xml files: {}.format(len(xml_files))) detJson xml2json(xml_files) eval_compute(detJson, args.instances_file)/code/pre 注意yolo26s.pt 推理的标签与val2017数据集的标签需要映射 计算pt模型推理结果的MAP值 python3 eval_map.py --xml_dir pt-result/ 计算om模型推理结果的MAP值 python3 eval_map.py --xml_dir om-result/ yolo26模型的MAP值汇总精度误差控制在千分之一以内 模型 pt 格式 MAP(0.50:0.95) om 格式 MAP(0.50:0.95) 误差 yolo26n 0.33 0.33 0 yolo26s 0.406 0.405 0.001 yolo26m 0.455 0.454 0.001 yolo26l 0.469 0.469 0 yolo26x 0.499 0.498 0.0015、性能对比为便于评估 yolo26s 在 GPU 与昇腾 NPU 上的部署效果下表对比了两种平台的推理耗时、吞吐量FPS以及 MAP 精度。数据基于实际测试或合理估算仅供参考。对比项GPUNVIDIA T4昇腾 NPUAtlas 300I推理耗时单张 640×640约 12 ms约 15 ms吞吐量FPS约 83约 66MAP0.50:0.950.4060.405测试条件说明输入尺寸统一为 640×640batch size 为 1输出类型为 FP32。GPU 环境NVIDIA T4CUDA 11.8PyTorch 2.9.1ultralytics 8.4.3。昇腾 NPU 环境Atlas 300IAscend310CANN 8.0.1HDK 24.1.1.3OM 模型由 ATC 工具转换。推理耗时取多次运行的平均值未包含数据预处理和后处理时间。MAP 精度基于 COCO val2017 数据集计算pt 与 om 格式的精度误差控制在千分之一以内。五、常见问题与排查在环境搭建、模型转换和推理过程中可能会遇到一些典型问题。下面针对常见报错给出错误现象、原因分析和解决步骤供读者参考。1、ATC 转换报错错误现象执行 atc 命令时提示 E10001 或 E10002 等错误码例如模型解析失败、算子不支持或参数校验不通过转换过程中断。原因分析常见原因包括 ONNX 模型导出时算子版本与 ATC 不兼容、--soc_version 与推理设备不匹配、--input_shape 与模型实际输入不一致以及 CANN 环境变量未正确配置。解决步骤确认 --soc_version 与推理设备对应例如 Atlas 300I 使用 Ascend310可参考上文设备与 soc_version 对照表。核对 --input_shape 是否与 ONNX 模型输入一致建议先用 Netron 查看模型输入名称和维度。检查 ONNX 模型是否包含 ATC 不支持的算子可尝试在导出时设置 simplifyTrue 或升级 CANN 版本。执行 source /usr/local/Ascend/ascend-toolkit/set_env.sh 确保环境变量生效再重新运行 atc 命令。查看 atc 日志--loginfo定位具体报错算子必要时在昇腾社区搜索对应错误码。2、om 推理结果与 pt 不一致错误现象om 模型推理输出的检测框、类别或置信度与 pt 模型存在明显差异甚至出现漏检或误检。原因分析可能原因包括输入预处理不一致letterbox 填充方式不同、om 模型输出为 FP32 但后处理未对齐、--output_type 设置错误以及模型转换时精度损失。解决步骤确保 pt 和 om 推理使用相同的预处理逻辑尤其是 letterbox 的填充颜色和缩放方式。检查 om 模型输出是否为 FP32若为 FP16 需在 ATC 转换时指定 --output_typeFP32。对比 pt 和 om 的原始输出张量确认差异是否来自后处理如 NMS 阈值、类别映射。若差异集中在个别类别检查类别 ID 映射是否正确参考 eval_map.py 中的 CATEGORY_ID 映射。尝试使用 --precision_mode 参数调整精度策略或关闭算子融合后重新转换。3、驱动安装失败错误现象安装 driver 或 firmware 时提示依赖缺失、内核版本不匹配或安装中断npu-smi info 无法正常显示设备信息。原因分析常见原因包括操作系统内核版本与驱动不兼容、未安装 gcc 和 make 等编译依赖、安装包架构与系统架构不一致以及残留的旧版本驱动未清理。解决步骤确认系统架构为 aarch64并核对安装包名称是否包含 linux-aarch64。检查内核版本是否满足驱动要求必要时升级内核或选择对应版本的驱动安装包。安装前先卸载旧版本驱动./Ascend-hdk-*.run --uninstall并清理 /usr/local/Ascend 下的残留文件。安装编译依赖apt-get install -y gcc g make linux-headers-$(uname -r)。重新执行安装命令安装完成后运行 npu-smi info 验证设备是否正常识别。4、推理脚本报错找不到 InferSession错误现象运行 inference.py 时提示 ModuleNotFoundError: No module named ais_bench或无法导入 InferSession。原因分析ais_bench 推理工具未安装或安装后未正确配置 Python 环境路径。解决步骤确认已安装 ais_benchpip3 install ais_bench或从昇腾社区下载对应版本的推理工具包。检查 Python 环境是否与安装 ais_bench 时一致避免多个 Python 版本混用。若使用虚拟环境确保激活后再运行推理脚本。确认 CANN 环境变量已生效必要时重新执行 source /usr/local/Ascend/ascend-toolkit/set_env.sh。5、MAP 计算时标签映射错误错误现象eval_map.py 计算出的 MAP 值异常偏低或提示 category_id 超出范围。原因分析yolo26s.pt 推理输出的类别 ID 与 COCO val2017 数据集的类别 ID 不一致未进行正确映射。解决步骤核对 eval_map.py 中的 CATEGORY_ID 映射表确保 yolo 类别 ID 正确映射到 COCO 类别 ID。检查推理结果文件中的 class 字段是否为 0-79 的整数若超出范围需修正映射。确认 instances_val2017.json 路径正确且标注文件与推理数据集一致。对比 pt 和 om 的 MAP 值若两者误差在千分之一以内说明映射和计算流程正常。六、总结与参考资料1、总结本文完整介绍了 YOLOv26 模型在昇腾 NPU 上的部署流程核心要点可归纳为以下四点ATC 转换参数配置转换时需重点核对 --soc_version 与推理设备对应、--input_shape 与模型输入一致并指定 --output_typeFP32 以保证精度。推理脚本与 ultralytics 逻辑对齐om 推理通过复用 DetectionPredictor 的预处理与后处理流程确保与 pt 模型在 letterbox、NMS 等环节保持一致。MAP 精度误差控制通过 eval_map.py 完成类别 ID 映射并基于 COCO val2017 计算 MAPpt 与 om 格式的精度误差可控制在千分之一以内。环境与排障搭建环境时需保证驱动、固件与 CANN 版本匹配遇到 ATC 报错、推理结果不一致等问题时可参考上文排查步骤快速定位。2、参考资料ATC 工具8.5.0参数说明CANN 安装指南ultralytics 官方文档yolo26-npu 开源代码仓库