本文介绍 YOLOv26 模型在昇腾 NPU 上的完整部署流程,涵盖环境搭建、模型转换、模型推理以及 MAP 精度计算四个关键环节。通过本文,读者可以了解如何将 YOLOv26 的 PyTorch 模型导出为 ONNX,再借助 ATC 工具转换为昇腾 NPU 可执行的 OM 模型,并最终在 Atlas 系列推理设备上完成推理与精度验证。
本文适合以下读者:
- 希望在昇腾 NPU 上部署 YOLO 系列检测模型的算法工程师;
- 需要完成模型转换、推理和精度评估的开发者;
- 对 Atlas 300I 等推理设备上模型移植流程感兴趣的入门者。
目录导航
- 一、环境搭建
- 二、模型转换
- 三、模型推理
- 四、计算 MAP
一、环境搭建
环境信息
ultralytics | 8.4.3 |
| pytorch | 2.9.1 |
| cann | 8.0.1 |
| HDK | 24.1.1.3 |
| 硬件信息 | Atlas300I |
driver 安装包下载:A300-3010-npu-driver_24.1.1.3_linux-aarch64.run
firmware 安装包下载:A300-3010-npu-firmware_7.5.0.9.220.run
cann 安装包下载: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.pt
pth2onnx.py 源代码如下:
import argparse from ultralytics import YOLO def main(): parser = argparse.ArgumentParser() parser.add_argument('--pt', default="./models/yolo26s.pt", help='pt file') args = parser.parse_args() model = YOLO(args.pt) onnx_model = model.export(format="onnx", dynamic=True, simplify=True, opset=11) if name == 'main': main()2、atc工具使用说明
文档地址:ATC工具(8.5.0)参数说明
3、将 onnx 模型转换成 om
atc --framework=5 \
--model=./models/yolo26s.onnx \
--input_format=NCHW \
--input_shape='images:1,3,640,640'\
--output_type=FP32 \
--output=./models/yolo26s_bs1 \
--log=info \
--soc_version=Ascend310
--input_shape: 模型入参尺寸 1*3*640*640
--input_format: 模型入参格式,N 是batchsize,C 图像通道,H 图像高,W 图像宽
--output_type: 模型输出数值是FP32
--soc_version: 推理环境使用的芯片型号
| 推理设备 | soc_version |
| Atlas300I | Ascend310 |
Atlas300V | Ascend310P3 |
Atlas300I Pro | Ascend310P3 |
| Atlas300V Pro | Ascend310P3 |
| Atlas300I Duo | Ascend310P3 |
| Atlas800T A2 | Ascend910B3 |
| Atlas800I A2 | Ascend910B4 |
三、模型推理
推理脚本 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, auto=same_shapes and self.model.pt, stride=self.model.stride) return [letterbox(image=x) 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.pt=False 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_ok=True) 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(source=input_args.data, save=True, save_txt=True) # 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(overrides=om_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', help='data path') parser.add_argument('--pt', default='./models/yolo26s.pt', help='pt model path') parser.add_argument('--om', default='./models/yolo26s_bs1.om', help='om model path') parser.add_argument('--pt_result', default='./pt-result', help='pt model result path') parser.add_argument('--om_result', default='./om-result', help='om model result path') parser.add_argument('--device_id', default='0', help='device 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 的推理结果一致
四、计算MAP
1、推理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) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.317 Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.562 Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.321 Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.162 Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.343 Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.448 Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 1 ] = 0.278 Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 10 ] = 0.438 Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.464 Average Recall (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.275 Average Recall (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.497 Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 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( description="Convert Pascal VOC annotation to COCO format." ) parser.add_argument("--xml_dir", default='./om-result', help="Directory path to xml files.", type=str) parser.add_argument("--instances_file", default='/data/LLMs/instances_val2017.json', help="COCO format instances_val2017.json.", type=str) 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 精度。数据基于实际测试或合理估算,仅供参考。
| 对比项 | GPU(NVIDIA T4) | 昇腾 NPU(Atlas 300I) |
| 推理耗时(单张 640×640) | 约 12 ms | 约 15 ms |
| 吞吐量(FPS) | 约 83 | 约 66 |
| MAP(0.50:0.95) | 0.406 | 0.405 |
测试条件说明:
- 输入尺寸统一为 640×640,batch size 为 1,输出类型为 FP32。
- GPU 环境:NVIDIA T4,CUDA 11.8,PyTorch 2.9.1,ultralytics 8.4.3。
- 昇腾 NPU 环境:Atlas 300I(Ascend310),CANN 8.0.1,HDK 24.1.1.3,OM 模型由 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 不支持的算子,可尝试在导出时设置 simplify=True 或升级 CANN 版本。
- 执行 source /usr/local/Ascend/ascend-toolkit/set_env.sh 确保环境变量生效,再重新运行 atc 命令。
- 查看 atc 日志(--log=info)定位具体报错算子,必要时在昇腾社区搜索对应错误码。
2、om 推理结果与 pt 不一致
错误现象:om 模型推理输出的检测框、类别或置信度与 pt 模型存在明显差异,甚至出现漏检或误检。
原因分析:可能原因包括输入预处理不一致(letterbox 填充方式不同)、om 模型输出为 FP32 但后处理未对齐、--output_type 设置错误,以及模型转换时精度损失。
解决步骤:
- 确保 pt 和 om 推理使用相同的预处理逻辑,尤其是 letterbox 的填充颜色和缩放方式。
- 检查 om 模型输出是否为 FP32,若为 FP16 需在 ATC 转换时指定 --output_type=FP32。
- 对比 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_bench:pip3 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_type=FP32 以保证精度。
- 推理脚本与 ultralytics 逻辑对齐:om 推理通过复用 DetectionPredictor 的预处理与后处理流程,确保与 pt 模型在 letterbox、NMS 等环节保持一致。
- MAP 精度误差控制:通过 eval_map.py 完成类别 ID 映射并基于 COCO val2017 计算 MAP,pt 与 om 格式的精度误差可控制在千分之一以内。
- 环境与排障:搭建环境时需保证驱动、固件与 CANN 版本匹配,遇到 ATC 报错、推理结果不一致等问题时可参考上文排查步骤快速定位。
2、参考资料
- ATC 工具(8.5.0)参数说明
- CANN 安装指南
- ultralytics 官方文档
- yolo26-npu 开源代码仓库