华为昇腾NPU开发实战:YOLOv11从GPU到Atlas 300I Duo全流程部署

当YOLOv11遇到国产NPU,部署链路里藏着不少容易踩的坑
很多人用GPU跑完YOLOv11目标检测模型,想把它部署到边缘设备上,第一反应是:换个推理框架不就行了?
真到动手时才发现,事情没那么简单。从x86+GPU迁移到华为昇腾Atlas 300I Duo,你要面对的是一套完全不同的软件栈:驱动、固件、CANN工具包、ATC模型转换、OM格式推理……任何一个环节版本不匹配,模型就跑不起来。
这篇文章不讲概念,只讲从环境搭建到生产部署的完整实操链路。基于openEuler 24.03 + Atlas 300I Duo 96G + CANN 8.5.0的真实环境,把每一步的命令、参数、坑点和验证方法全部拆清楚。
01 先搞清楚:你要部署的硬件和软件到底是什么
动手装环境之前,先确认版本对应关系。昇腾生态最容易出问题的地方,就是驱动、固件、CANN三者版本不匹配。
本次实操环境一览:
|
项目 |
版本/型号 |
说明 |
|
操作系统 |
openEuler 24.03 LTS-SP3 |
aarch64架构,国产操作系统 |
|
加速卡 |
Atlas 300I Duo 96G |
两张310P3芯片,共96GB显存 |
|
NPU驱动版本 |
25.5.1 |
固件与驱动必须配套 |
|
CANN版本 |
8.5.0 |
包含Toolkit + Ops算子包 |
|
模型 |
YOLOv11m |
ultralytics导出,转OM后推理 |
有一个快速验证命令可以确认当前SOC型号:
python3 -c "import acl;print(acl.get_soc_name())"
输出Ascend310P3,说明环境识别正确。如果报找不到acl模块,说明CANN环境变量还没生效。
另外,用npu-smi info可以查看所有NPU芯片的健康状态、温度和显存占用。正常情况下每张卡的Health列都应该显示OK。
文档已创建。后续按公众号专业解读类写法和知识卡片风排版写入完整技术教程内容。
02 第一步:驱动与固件安装,顺序错了全盘重来
这一步是整个部署链路里最容易翻车的环节。两个核心原则:
原则一:首次安装先固件后驱动;版本升级先固件后驱动。顺序反了,驱动会识别不到固件版本,直接报错。
原则二:装完驱动绝对不能让系统自动更新内核。openEuler的yum update会升级内核版本,一旦内核变了,已安装的驱动就会失效,这是很多人踩过的大坑。
2.1 关闭内核自动更新
在yum配置里把kernel加入排除列表:
sudo sed -e "/\*/exclude=/s/$/ kernel*/" -i /etc/yum.conf
或者直接编辑dnf.conf文件,手动添加exclude=kernel*。
2.2 安装编译依赖
驱动安装需要一系列编译工具和库文件,一条命令装齐:
sudo dnf install -y gcc gcc-c++ make cmake unzip zip-devel libffi-devel openssl-devel pciutils net-tools sqlite-devel lapack-devel gcc-gfortran python3-devel dkms
2.3 创建运行用户
NPU驱动不建议用root用户日常运行,需要创建一个专用用户组和用户:
sudo groupadd HwHiAiUsersudo useradd -g HwHiAiUser -d /home/HwHiAiUser -m HwHiAiUser -s /bin/bashsudo usermod -aG HwHiAiUser $USER
2.4 安装驱动和固件
https://www.hiascend.com/hardware/firmware-drivers/old/community?product=2&model=17&cann=8.5.0&driver=Ascend+HDK+25.5.1
驱动与CANN需要匹配,可以从下面界面匹配



下载好对应版本的.run安装包后,先校验完整性:
./Ascend-hdk-310p-npu-driver_25.5.1_linux-aarch64.run --check./Ascend-hdk-310p-npu-firmware_7.8.0.6.201.run --check
校验通过后,先装驱动,再装固件:
sudo ./Ascend-hdk-310p-npu-driver_25.5.1_linux-aarch64.run --full --install-for-allsudo ./Ascend-hdk-310p-npu-firmware_7.8.0.6.201.run --full
装完重启系统,再用npu-smi info验证,能看到芯片列表和温度信息,就说明驱动固件装对了。
版本下载页面要注意选对三个条件:产品系列选"加速卡"、产品型号选"Atlas 300I Duo 推理卡"、CANN版本选8.5.0。CPU架构必须选AArch64,不要选x86。
03 第二步:CANN Toolkit安装,别漏了OPS算子包
CANN是昇腾的计算架构栈,相当于GPU生态里的CUDA。安装的时候有两个包必须装:Toolkit开发包和Ops算子包。只装Toolkit不装Ops,模型转换时会报算子不支持的错误。
3.1 配置用户组和依赖
和驱动安装时一样,先确认HwHiAiUser用户组存在,再装Python编译依赖:
sudo yum install -y gcc python3 python3-pip kernel-headers-$(uname -r) kernel-devel-$(uname -r)
3.2 下载并安装CANN
从昇腾官网下载对应版本的.run包,先装Toolkit合一包:
bash ./Ascend-cann_8.5.0_linux-aarch64.run --install
再装310P专用的Ops算子包:
bash ./Ascend-cann-310p-ops_8.5.0_linux-aarch64.run --install
3.3 验证安装
安装完成后,必须配置环境变量才能生效:
source /usr/local/Ascend/set_env.sh
建议把这行加到~/.bashrc里,每次登录自动加载。
验证命令:
python3 -c "import acl;print(acl.get_soc_name())"
输出Ascend310P3,说明CANN环境配置正确。
|
关键提醒:下载CANN时,产品系列要选"310P系列产品",CPU架构选"AArch64",操作系统选"openEuler",安装方式选"离线安装(Runfile)"。选错任何一个条件,下载的包都可能装不上。 |
04 第三步:导出ONNX,这几个参数直接决定转换成败
ONNX是模型格式的中间桥梁。PyTorch的.pt模型不能直接在NPU上跑,必须先转成ONNX,再用ATC工具转成昇腾专属的OM格式。
导出操作不需要在NPU机器上做,在任意x86开发机上完成即可。
4.1 导出命令
先安装依赖:
pip install ultralytics onnx onnxslim onnxruntime -i https://pypi.tuna.tsinghua.edu.cn/simple
然后写一段Python代码导出:
from ultralytics import YOLOmodel = YOLO("./model/best.pt")model.export( format="onnx", imgsz=(416,736), # 高416,宽736 opset=11, # ★ 昇腾ATC兼容性最好;opset>=17常报算子不支持 simplify=True, # 做常量折叠,显著提升ATC成功率 nms=False, # ★ 不要把NMS导进图里,后处理放CPU dynamic=True, # ★ 不固定shape,ACT转换批处理)
4.2 四个关键参数的解释
|
参数 |
推荐值 |
为什么这么设 |
|
opset |
11 |
昇腾ATC对opset 11兼容性最好,17以上的新算子经常不支持 |
|
simplify |
True |
常量折叠可以消除冗余节点,减少ATC转换失败的概率 |
|
nms |
False |
NMS算子在NPU上性能差,放CPU做后处理更快 |
|
dynamic |
True |
动态shape方便后续做批处理,ACT转换时更灵活 |
4.3 再做一次简化优化
导出ONNX后,建议再用onnxslim跑一次简化:
python -m onnxslim best.onnx best_sim.onnx
这一步能进一步消除模型里的冗余节点,让后续ATC转换更顺畅。
05 第四步:ATC转OM,310P3的精度选择很特殊
ATC是昇腾的模型转换工具,把ONNX格式转成NPU专属的OM格式。这一步有一个很多人不知道的坑:310P3不支持BF16精度。
5.1 安装ATC依赖
ATC工具依赖一些Python包:
python3 -m pip install "numpy<2" decorator sympy psutil protobuf attrs cffi scipy -i https://pypi.tuna.tsinghua.edu.cn/simple
5.2 转换命令
完整的ATC转换命令如下:
atc --model=best_sim.onnx --framework=5 --output=best_bs1 --input_format=NCHW --input_shape="images:1,3,416,736" --soc_version=Ascend310P3 --precision_mode=v2=mixed_float16 --output_type=FP16 --op_select_implmode=high_performance --log=error
5.3 关键参数说明
|
参数 |
值 |
说明 |
|
--framework |
5 |
5表示ONNX格式 |
|
--input_shape |
images:1,3,416,736 |
NCHW格式,batch=1 |
|
--soc_version |
Ascend310P3 |
必须和实际芯片型号一致 |
|
--precision_mode |
mixed_float16 |
310P3不支持BF16,用mixed_float16 |
|
--output_type |
FP16 |
输出张量用FP16精度 |
|
--op_select_implmode |
high_performance |
算子优先选高性能实现 |
转换成功后会生成best_bs1.om文件,这就是可以直接在NPU上运行的模型文件。

从PyTorch到昇腾NPU,模型要经过四次格式转换
06 第五步:ais_bench快速验证,先跑通再优化
模型转好了,先别急着写生产代码。用昇腾官方的ais_bench工具做一轮快速验证,确认性能和精度都没问题,再投入开发。
6.1 安装ais_bench和aclruntime
310P和910用的是同一套ais_bench和aclruntime包,不需要按芯片型号区分。但安装顺序有讲究:必须先装aclruntime,再装ais_bench。因为ais_bench依赖aclruntime这个底层ACL的Python绑定。
自动检测Python版本和架构,下载对应whl包:
PY=$(python3 -c "import sys;print(fcp{sys.version_info.major}{sys.version_info.minor})")ARCH=$(uname -m)BASE=https://aisbench.obs.myhuaweicloud.com/packet/ais_bench_infer/0.0.2/aitpip3 install "${BASE}/aclruntime-0.0.2-${PY}-linux_${ARCH}.whl"pip3 install "${BASE}/ais_bench-0.0.2-py3-none-any.whl"
6.2 纯推理性能测试(0数据,看吞吐/时延)
先跑空数据测试,看模型本身的推理速度:
python3 -m ais_bench --model ./best_bs1.om --output ./output/ --outfmt BIN --loop 5
这个命令会连续推理5次,输出平均时延、P99时延和吞吐率。如果时延在可接受范围内,再喂真实数据。
6.3 喂真实数据测试(.npy或.bin目录)
准备好预处理后的输入数据(.npy格式),跑真实推理:
python3 -m ais_bench --model ./best_bs1.om --input ./input_npy --output ./ --output_dirname results_bs1
输出结果里会包含每张图的推理耗时、检测数量和置信度。对比一下和GPU上的结果,确认精度没有明显下降。
07 生产级推理代码
import os
import time
import sys
import numpy as np
import cv2
sys.path.append('/yolo11Adapt/offer_package/ais_bench_src')
BASE_DIR = '/yolo11Adapt'
OM_PATH = os.path.join(BASE_DIR, 'model', 'yolo11m_310P3.om')
DATA_DIR = os.path.join(BASE_DIR, 'dataset')
OUTPUT_DIR = os.path.join(BASE_DIR, 'output', 'om_infer')
IMAGES = [
os.path.join(DATA_DIR, 'pic001.jpg'),
os.path.join(DATA_DIR, 'pic002.jpg'),
os.path.join(DATA_DIR, 'pic003.jpg'),
]
IMG_SIZE = 640
CONF_THRESH = 0.25
IOU_THRESH = 0.45
# COCO 80 类名称
COCO_NAMES = [
'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
'hair drier', 'toothbrush'
]
os.makedirs(OUTPUT_DIR, exist_ok=True)
print("=" * 60)
print("YOLOv11 OM 模型推理 (ais_bench - 最终完美修复版)")
print("=" * 60)
print(f"OM模型: {OM_PATH}")
if not os.path.exists(OM_PATH):
print(f"✗ OM 模型不存在: {OM_PATH}")
sys.exit(1)
# 安全加固:确保 OM 模型文件权限符合 ais_bench 要求
os.chmod(OM_PATH, 0o644)
# 加载 OM 模型
from ais_bench.infer.interface import InferSession
session = InferSession(0, OM_PATH)
print("✓ OM 模型加载成功")
print()
def preprocess(img_path):
"""标准的 Letterbox 预处理,保持宽高比,Padding 加在四周"""
img = cv2.imread(img_path)
if img is None:
raise FileNotFoundError(f"Cannot read {img_path}")
h0, w0 = img.shape[:2]
# 保持宽高比,计算缩放比例
r = min(IMG_SIZE / h0, IMG_SIZE / w0)
h, w = int(h0 * r), int(w0 * r)
img_resized = cv2.resize(img, (w, h), interpolation=cv2.INTER_LINEAR)
# 计算四周的 padding(核心修复:将padding 均匀分配到上下左右)
pad_top = (IMG_SIZE - h) // 2
pad_bottom = IMG_SIZE - h - pad_top
pad_left = (IMG_SIZE - w) // 2
pad_right = IMG_SIZE - w - pad_left
# 四周填充灰色
img_padded = cv2.copyMakeBorder(img_resized, pad_top, pad_bottom, pad_left, pad_right,
cv2.BORDER_CONSTANT, value=(114, 114, 114))
# 归一化并转为 CHW
img_chw = img_padded.transpose(2, 0, 1).astype(np.float32) / 255.0
img_input = np.expand_dims(img_chw, axis=0)
# 返回预处理结果以及还原所需的参数
return img_input, (h0, w0), (h, w), img, (pad_top, pad_left)
def postprocess(outputs, orig_shape, infer_shape, pad_offset):
h0, w0 = orig_shape # 原始图像宽高
h, w = infer_shape # 缩放后(未padding)的宽高
pad_top, pad_left = pad_offset # 上方和左方的 padding 偏移量
raw_pred = outputs[0]
# 维度转置: (1, 84, 8400) -> (8400, 84)
if raw_pred.ndim == 3:
pred = np.squeeze(raw_pred).transpose(1, 0)
else:
pred = raw_pred.transpose(1, 0)
boxes = pred[:, :4] # cx, cy, w, h (相对于640x640 的绝对坐标)
scores = pred[:, 4:]
# 获取最大类别置信度
class_ids = np.argmax(scores, axis=1)
confs = scores[np.arange(len(scores)), class_ids]
# 置信度过滤
mask = confs > CONF_THRESH
boxes = boxes[mask]
class_ids = class_ids[mask]
confs = confs[mask]
if len(boxes) == 0:
return []
# 核心坐标还原逻辑
cx = boxes[:, 0]
cy = boxes[:, 1]
bw = boxes[:, 2]
bh = boxes[:, 3]
# 1. 减去 padding 偏移量(现在减去的是 pad_top 和 pad_left,保证结果为正)
cx = cx - pad_left
cy = cy - pad_top
# 2. 缩放回原始尺寸
scale_x = w0 / w
scale_y = h0 / h
cx = cx * scale_x
cy = cy * scale_y
bw = bw * scale_x
bh = bh * scale_y
# 3. xywh 转 xyxy
x1 = cx - bw / 2
y1 = cy - bh / 2
x2 = cx + bw / 2
y2 = cy + bh / 2
# 4. 裁剪到图像合法范围
x1 = np.clip(x1, 0, w0)
y1 = np.clip(y1, 0, h0)
x2 = np.clip(x2, 0, w0)
y2 = np.clip(y2, 0, h0)
xyxy = np.stack([x1, y1, x2, y2], axis=1).astype(np.float32)
# 5. 使用 OpenCV NMS
indices = cv2.dnn.NMSBoxes(
bboxes=xyxy,
scores=confs.astype(np.float32),
score_threshold=CONF_THRESH,
nms_threshold=IOU_THRESH
)
results = []
if len(indices) > 0:
indices = indices.flatten()
for idx in indices:
results.append({
'class_id': int(class_ids[idx]),
'confidence': float(confs[idx]),
'bbox': [float(v) for v in xyxy[idx]],
})
return results
for i, img_path in enumerate(IMAGES):
fname = os.path.basename(img_path)
print(f"[{i+1}/{len(IMAGES)}] Processing {fname}...")
if not os.path.exists(img_path):
print(f" ✗图片不存在: {img_path}")
continue
# 使用标准的 Letterbox 预处理
img_input, orig_shape, infer_shape, img_orig, pad_offset = preprocess(img_path)
t0 = time.time()
outputs = session.infer([img_input], mode='static')
t1 = time.time()
results = postprocess(outputs, orig_shape, infer_shape, pad_offset)
print(f" 推理耗时: {t1 - t0:.3f}s")
print(f" 检测数量: {len(results)}")
for r in results:
cname = COCO_NAMES[r['class_id']] if r['class_id'] < len(COCO_NAMES) else f'class_{r["class_id"]}'
bbox = r['bbox']
print(f" {cname} conf={r['confidence']:.3f} bbox=[{bbox[0]:.1f},{bbox[1]:.1f},{bbox[2]:.1f},{bbox[3]:.1f}]")
# 画框
annotated = img_orig.copy()
for r in results:
bbox = r['bbox']
cname = COCO_NAMES[r['class_id']] if r['class_id'] < len(COCO_NAMES) else f'class_{r["class_id"]}'
cv2.rectangle(annotated, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), (0, 255, 255), 2)
cv2.putText(annotated, f'{cname} {r["confidence"]:.2f}',
(int(bbox[0]), int(bbox[1]) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)
save_path = os.path.join(OUTPUT_DIR, fname)
cv2.imwrite(save_path, annotated)
print(f" 保存到: {save_path}")
print()
session.free_resource()
print("=" * 60)
print("OM推理完成!")
print(f"结果保存目录: {OUTPUT_DIR}")
print("=" * 60)
更多推荐




所有评论(0)