YOLOv11实战:从模型导出到昇腾Atlas 310P推理全流程

在昇腾Atlas 310P上部署YOLOv11,核心流程其实就三步:导出ONNX → 转换OM模型 → 编写推理代码。本文不讲原理,直接上操作步骤和代码,助你快速跑通推理。

环境准备(避坑指南)

在开始之前,请确保你的服务器或开发板环境满足以下基本条件:

  • 硬件:Atlas 310P系列(如310P3)。
  • 软件:CANN工具包(建议6.0+,需与驱动匹配)、Python 3.9+。
  • 关键依赖:
    • ultralytics:用于加载和导出YOLOv11模型。
    • aclruntime 或 ais_bench:华为提供的推理运行时库。

环境变量检查
在~/.bashrc中确认是否添加了CANN的路径,否则后续命令会报错:

export PYTHONPATH=/usr/local/Ascend/ascend-toolkit/latest/python/site-packages:$PYTHONPATH
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/latest/lib64:$LD_LIBRARY_PATH
第一步:模型导出(PyTorch → ONNX)

YOLOv11模型(.pt格式)不能直接在NPU上跑,必须先转为ONNX格式。

注意事项:

  • 动态轴:ATC工具对动态Shape支持有限,建议导出时设为dynamic=False,固定输入尺寸。
  • 算子简化:开启simplify=True以减少转换出错概率。

Python导出脚本:

from ultralytics import YOLO

# 1. 加载模型 (支持yolov11n.pt, yolov11s.pt等)
model = YOLO("yolov11n.pt")

# 2. 导出为ONNX
# imgsz: 必须与训练时或推理时的尺寸一致,如640或1024
# opset: 建议11或12
# dynamic: 设为False,固定Batch和尺寸,方便ATC转换
model.export(
    format="onnx", 
    imgsz=640, 
    opset=12, 
    dynamic=False, 
    simplify=True
)

运行后,你将获得一个yolov11n.onnx文件。

第二步:模型转换(ONNX → OM)

这是最关键的一步,使用华为的ATC(Ascend Tensor Compiler)工具将ONNX转为昇腾专用的OM离线模型。

ATC转换命令:
请根据你的实际芯片型号修改soc_version参数(310P3通常对应Ascend310P3)。

atc --model=yolov11n.onnx \
    --framework=5 \
    --output=yolov11n \
    --input_format=NCHW \
    --input_shape="images:1,3,640,640" \
    --soc_version=Ascend310P3 \
    --precision_mode=force_fp32

参数详解:

  • --framework=5:代表输入是ONNX模型。
  • --input_shape:必须与导出ONNX时的尺寸一致。格式为节点名:batch,channel,height,width。YOLOv11的输入节点名通常是images。
  • --soc_version:务必准确,310P3写Ascend310P3,910写Ascend910。
模式说明
force_fp32强制全部计算和输出为 FP32,这是最安全的 FP32 模式,推荐用于调试 Python 推理段错误问题
force_fp16强制全部计算和输出为 FP16
allow_fp32_to_fp16允许 FP32 转 FP16(原来你用的模式)
allow_fp32_to_bf16允许 FP32 转 BF16
allow_mix_precision混合精度模式
allow_mix_precision_fp16混合精度 + FP16
cube_fp16in_fp32outFP16 输入 FP32 输出
must_keep_origin_dtype保留原始 ONNX 数据类型

如果执行成功,目录下会生成yolov11n.om文件,这就是我们要部署的模型。

第三步:编写推理代码(Python ACL)

有了OM模型,我们就可以使用Python的aclruntime进行推理。这里提供一个精简的推理逻辑框架。

推理代码示例:

import threading
import time
import cv2
import numpy as np
import acl

ACL_MEMCPY_HOST_TO_DEVICE = 1
ACL_MEMCPY_DEVICE_TO_HOST = 2

class OMDetector:
    def __init__(
        self,
        om_path,
        class_names,
        input_size=(640, 640),
        score_thres=0.6,
        nms_thres=0.5,
        device_id=0,
    ):
        self.om_path = om_path
        self.class_names = class_names
        self.num_classes = len(class_names)
        self.input_size = input_size
        self.score_thres = score_thres
        self.nms_thres = nms_thres
        self.device_id = device_id

        self.model_id = None
        self.model_desc = None
        self.context = None
        self.stream = None

        self.lock = threading.Lock()

        # ===== 预分配资源 =====
        self.input_buffer = None
        self.input_dataset = None

        self.output_buffers = []
        self.output_dataset = None

        self.host_output_buffers = []

    # ================= ACL init =================
    def init_acl(self):
        acl.init()
        acl.rt.set_device(self.device_id)
        self.context, _ = acl.rt.create_context(self.device_id)
        self.stream, _ = acl.rt.create_stream()

    # ================= Load model & alloc =================
    def load_model(self):
        # ---- load model ----
        self.model_id, ret = acl.mdl.load_from_file(self.om_path)
        if ret != 0:
            raise RuntimeError("load model failed")

        self.model_desc = acl.mdl.create_desc()
        ret = acl.mdl.get_desc(self.model_desc, self.model_id)
        if ret != 0:
            raise RuntimeError("get model desc failed")

        # ---- input ----
        input_size = acl.mdl.get_input_size_by_index(self.model_desc, 0)
        self.input_buffer, _ = acl.rt.malloc(input_size, 0)

        self.input_dataset = acl.mdl.create_dataset()
        acl.mdl.add_dataset_buffer(
            self.input_dataset,
            acl.create_data_buffer(self.input_buffer, input_size),
        )

        # ---- output ----
        self.output_dataset = acl.mdl.create_dataset()
        out_num = acl.mdl.get_num_outputs(self.model_desc)

        for i in range(out_num):
            size = acl.mdl.get_output_size_by_index(self.model_desc, i)

            dev_buf, _ = acl.rt.malloc(size, 0)
            self.output_buffers.append(dev_buf)

            acl.mdl.add_dataset_buffer(
                self.output_dataset,
                acl.create_data_buffer(dev_buf, size),
            )

            # host buffer(也复用)
            host_buf, _ = acl.rt.malloc_host(size)
            self.host_output_buffers.append((host_buf, size))

        print("[INFO] OM model loaded & buffers allocated")

    # ================= Release =================
    def release_acl(self):
        for buf in self.output_buffers:
            acl.rt.free(buf)

        if self.input_buffer:
            acl.rt.free(self.input_buffer)

        for host_buf, _ in self.host_output_buffers:
            acl.rt.free_host(host_buf)

        if self.output_dataset:
            acl.mdl.destroy_dataset(self.output_dataset)
        if self.input_dataset:
            acl.mdl.destroy_dataset(self.input_dataset)

        if self.model_id:
            acl.mdl.unload(self.model_id)
        if self.model_desc:
            acl.mdl.destroy_desc(self.model_desc)

        if self.stream:
            acl.rt.destroy_stream(self.stream)
        if self.context:
            acl.rt.destroy_context(self.context)

        acl.rt.reset_device(self.device_id)
        acl.finalize()

        print("[INFO] ACL released")

    # ================= Letterbox =================
    @staticmethod
    def letterbox(img, new_shape=(640, 640), color=(114, 114, 114)):
        h, w = img.shape[:2]
        r = min(new_shape[0] / h, new_shape[1] / w)
        new_w, new_h = int(w * r), int(h * r)

        resized = cv2.resize(img, (new_w, new_h))
        dw, dh = new_shape[1] - new_w, new_shape[0] - new_h
        top, bottom = dh // 2, dh - dh // 2
        left, right = dw // 2, dw - dw // 2

        padded = cv2.copyMakeBorder(
            resized, top, bottom, left, right,
            cv2.BORDER_CONSTANT, value=color
        )
        return padded, r, (left, top)

    # ================= Infer =================
    def infer(self, image_bgr):
        with self.lock:
            return self._infer_internal(image_bgr)

    def _infer_internal(self, img_bgr):
        acl.rt.set_context(self.context)

        orig_h, orig_w = img_bgr.shape[:2]

        # ---- preprocess ----
        img, ratio, pad = self.letterbox(img_bgr, self.input_size)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        img = img.transpose(2, 0, 1).astype(np.float32) / 255.0
        img = np.expand_dims(img, axis=0)

        # ---- copy input ----
        acl.rt.memcpy(
            self.input_buffer,
            img.nbytes,
            acl.util.bytes_to_ptr(img.tobytes()),
            img.nbytes,
            ACL_MEMCPY_HOST_TO_DEVICE,
        )

        # ---- execute ----
        t0 = time.perf_counter()
        ret = acl.mdl.execute(self.model_id, self.input_dataset, self.output_dataset)
        if ret != 0:
            raise RuntimeError("model execute failed")
        acl.rt.synchronize_stream(self.stream)
        t1 = time.perf_counter()

        print(f"[INFO] infer {(t1 - t0)*1000:.2f} ms")

        # ---- fetch output ----
        outputs = []
        for i, (host_buf, size) in enumerate(self.host_output_buffers):
            dev_buf = self.output_buffers[i]
            acl.rt.memcpy(
                host_buf,
                size,
                dev_buf,
                size,
                ACL_MEMCPY_DEVICE_TO_HOST,
            )

            data = acl.util.ptr_to_bytes(host_buf, size)
            outputs.append(np.frombuffer(data, dtype=np.float32).copy())

        return self.postprocess_detect(outputs, ratio, pad, (orig_h, orig_w))

    # ================= Postprocess =================
    def postprocess_detect(self, outputs, ratio, pad, orig_shape):
        raw = outputs[0]
        num_classes = self.num_classes
        no = 4 + num_classes
        num_preds = raw.size // no

        output = raw.reshape(1, no, num_preds)
        preds = np.transpose(output[0], (1, 0))

        boxes = []
        scores = []
        class_ids = []

        preds[:, 0] -= pad[0]
        preds[:, 1] -= pad[1]

        for i in range(preds.shape[0]):
            cls_scores = preds[i, 4:]
            score = np.max(cls_scores)
            if score < self.score_thres:
                continue

            cls_id = int(np.argmax(cls_scores))
            x, y, w, h = preds[i, :4]

            left = int((x - w / 2) / ratio)
            top = int((y - h / 2) / ratio)
            width = int(w / ratio)
            height = int(h / ratio)

            boxes.append([left, top, width, height])
            scores.append(float(score))
            class_ids.append(cls_id)

        indices = cv2.dnn.NMSBoxes(
            boxes, scores, self.score_thres, self.nms_thres
        )

        results = []
        for i in indices:
            i = i[0] if isinstance(i, (list, tuple, np.ndarray)) else i
            x, y, w, h = boxes[i]
            results.append({
                "box": [x, y, x + w, y + h],
                "cls_id": class_ids[i],
                "cls_name": self.class_names[class_ids[i]],
                "confidence": scores[i],
            })

        return results

def draw_and_save(image, results, save_path):
    """
    image: 原始 BGR 图
    results: detector.infer 返回的结果
    save_path: 保存路径
    """
    img = image.copy()

    for det in results:
        x1, y1, x2, y2 = det["box"]
        cls_name = det["cls_name"]
        score = det["confidence"]

        # 画框
        cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)

        # 文本
        label = f"{cls_name} {score:.2f}"
        (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)

        cv2.rectangle(
            img,
            (x1, y1 - th - 6),
            (x1 + tw, y1),
            (0, 255, 0),
            -1
        )
        cv2.putText(
            img,
            label,
            (x1, y1 - 4),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.6,
            (0, 0, 0),
            2
        )

    cv2.imwrite(save_path, img)
    print(f"[INFO] result saved to {save_path}")



常见问题排查
  • ATC转换报错ModuleNotFoundError: No module named 'attr':这是CANN环境缺依赖,尝试安装pip install attrs或检查CANN安装是否完整。
  • 推理报错The resources are busy:说明NPU设备被占用。使用npu-smi info查看占用进程,杀掉无关进程后再试。
  • 输出结果不对:检查ONNX导出时的imgsz是否与ATC的input_shape以及代码中的预处理尺寸完全一致。

通过以上步骤,你应该能在Atlas 310P上成功跑通YOLOv11的推理。

更多推荐