YOLOv11实战:从模型导出到昇腾Atlas 310P推理全流程
·
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_fp32out | FP16 输入 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的推理。
更多推荐

所有评论(0)