昇腾 NPU 上手:ResNet-50 从 ONNX 到 OM 的完整踩坑记录

环境:Ascend 910B2C + CANN 8.5.1
目标:把 resnet_50.onnx 转成 resnet_50.om,在 NPU 上跑起来,并和转换前的 ONNX 结果做对比。

最近在昇腾机器上跑了一次经典的 ResNet-50 转换流程。看起来只是「ONNX → OM → 推理」三步,实际踩了不少坑:Python 依赖、opset 版本、精度对比方式都不简单。本文把完整过程记下来,方便以后自己复用,也希望帮到同样入门的同学。


1. 最终产物

文件说明
resnet_50.onnxONNX Model Zoo 的 ResNet-50 v2(约 98MB)
resnet_50.omATC 转换后的昇腾离线模型(约 50MB)
run_resnet50_om.pyACL Python 推理脚本

模型 I/O:

  • 输入datafloat32[1, 3, 224, 224](NCHW)
  • 输出[1, 1000](ImageNet 1000 类 logits)

2. 环境确认

先确认 NPU 和 CANN 是否正常:

npu-smi info
which atc
source /usr/local/Ascend/ascend-toolkit/set_env.sh

本次机器信息:

  • NPU:910B2C
  • CANN:8.5.1
  • 转换时 soc_versionAscend910B2C

3. 下载 ONNX 模型

ONNX Model Zoo 下载 ResNet-50 v2:

cd /home/compiler_projects

curl -L -o resnet_50.onnx \
  "https://media.githubusercontent.com/media/onnx/models/main/validated/vision/classification/resnet/model/resnet50-v2-7.onnx"

这个模型有两个小特点,后面转换会碰到:

  1. opset = 7(比较老)
  2. 权重被写在 graph.input(老式 ONNX 写法),真正的网络输入只有 data

4. ONNX → OM(ATC)

4.1 标准命令

source /usr/local/Ascend/ascend-toolkit/set_env.sh

atc --model=resnet_50.onnx \
    --framework=5 \
    --output=resnet_50 \
    --soc_version=Ascend910B2C \
    --input_format=NCHW \
    --input_shape="data:1,3,224,224" \
    --log=error

参数含义:

  • --framework=5:ONNX
  • --soc_version:必须和芯片匹配
  • --input_shape:静态 shape;这里把动态 batch 固定成 1

成功后会生成 resnet_50.om

4.2 坑 1:No module named 'numpy'

第一次跑 ATC 直接失败:

Environment_Error_Import_Python_Module_Failed(EC0010):
ModuleNotFoundError: No module named 'numpy'

原因:ATC 编译算子时会走 TBE(Python),它用的是 /usr/bin/python3,不是你日常装包的那个 Python。
即使 /usr/local/python3.11.xxx 里有 numpy,ATC 也看不到。

解决:给 ATC 实际使用的 Python 装依赖:

/usr/bin/python3 -m ensurepip --upgrade
/usr/bin/python3 -m pip install 'numpy<2' decorator attrs cloudpickle \
    ml-dtypes psutil scipy tornado sympy

装完 numpy 后,往往还会继续缺 decoratorscipy 等,建议一次性装齐。

4.3 坑 2:所有算子都报 No operator plugin

依赖装好后,又出现:

Not_Supported_Operator(E13010):
No operator plugin is registered for Op: xxx, optype: ai.onnx::7::Conv

ConvRelu 都报不支持,看起来很吓人。查日志后发现:

  • ONNX 插件其实加载成功了
  • 但 CANN 8.5.1 注册的是 ai.onnx::8 及以上
  • 当前模型是 ai.onnx::7

所以不是「昇腾不支持卷积」,而是 opset 太旧,对不上注册表

4.4 处理方式:清洗模型 + 提升 opset

先把权重从 graph.input 里摘掉,只保留真正输入 data,并把 batch 固定为 1;再把 opset 提升到 11:

import onnx

m = onnx.load("resnet_50.onnx")

# 1) 权重 initializer 不再作为 graph input
name_to_input = {i.name: i for i in m.graph.input}
for init in m.graph.initializer:
    if init.name in name_to_input:
        m.graph.input.remove(name_to_input[init.name])

# 2) 动态 batch -> 1
for i in m.graph.input:
    if i.name == "data":
        for d in i.type.tensor_type.shape.dim:
            d.ClearField("dim_param")
            if d.dim_value == 0:
                d.dim_value = 1

# 3) 提升 opset(对本模型直接改 version 即可)
for o in m.opset_import:
    if o.domain in ("", "ai.onnx"):
        o.version = 11

onnx.save(m, "resnet_50_op11.onnx")

然后再转:

atc --model=resnet_50_op11.onnx \
    --framework=5 \
    --output=resnet_50 \
    --soc_version=Ascend910B2C \
    --input_format=NCHW \
    --input_shape="data:1,3,224,224" \
    --log=error

这次输出:

ATC run success, welcome to the next use.

5. 怎么跑 resnet_50.om

昇腾侧用 ACL(Ascend Computing Language)加载 OM。核心流程:

  1. acl.init() / set_device
  2. acl.mdl.load_from_file("resnet_50.om")
  3. Host → Device 拷贝输入
  4. acl.mdl.execute(...)
  5. Device → Host 取输出
  6. 释放资源

仓库里放了一个可直接跑的脚本 run_resnet50_om.py

source /usr/local/Ascend/ascend-toolkit/set_env.sh
cd /home/compiler_projects

# 随机输入冒烟
python3 run_resnet50_om.py

# 真实图片(需 Pillow,会做 ImageNet 预处理)
python3 run_resnet50_om.py --image cat.jpg

冒烟结果示例:

model=/home/compiler_projects/resnet_50.om
input dims=[1, 3, 224, 224], bytes=602112
output dims=[1, 1000], bytes=4000
top-5:
  #1: class=490, score=...
  ...

6. 和转换前的 ONNX 对比

转换完最关心的还是:结果有没有漂?

方法很简单:同一份输入,分别用

  • onnxruntime(CPU)跑 ONNX
  • ACL 跑 OM

然后比:

  • max_abs_diff / mean_abs_diff
  • 相对 L2
  • cosine 相似度
  • top1 / top5

对比结果(固定随机种子 seed=0)

指标ONNX vs OM
cosine 相似度0.999998
相对 L20.00175
max abs diff≈ 0.0097
mean abs diff≈ 0.0024
top1一致
top55/5 完全一致

top5 类别完全相同:[490, 904, 556, 794, 599],分数也非常接近。

这类微小数值差通常来自昇腾图编译后的 FP16 混合精度计算,对分类任务基本无感。只要 topk 稳定、cosine 接近 1,就可以认为转换是成功的。


7. 一张流程图总结

resnet50-v2-7.onnx (opset7)
        │
        ▼
 清洗 input / 固定 shape / 提升 opset→11
        │
        ▼
      ATC 转换
   (soc=Ascend910B2C)
        │
        ▼
   resnet_50.om
        │
        ├──────────────► ACL 推理(NPU)
        │
onnxruntime (CPU) ──────► 同输入对比精度

8. 经验清单

  1. source set_env.sh,否则 atc / ACL 环境变量容易缺。
  2. ATC 用的 Python ≠ 你终端默认 Python,缺包要装到 /usr/bin/python3 对应环境。
  3. 老 ONNX(opset 7)在新 CANN 上可能全员报不支持,先升到 11+。
  4. 转换前最好确认:
    • 真正输入名(这里是 data
    • 静态 shape
    • 权重不要混在 graph.input
  5. 精度验收别只看单个 logit,cosine + topk 更有参考价值。

9. 参考命令速查

# 环境
source /usr/local/Ascend/ascend-toolkit/set_env.sh

# 转换
atc --model=resnet_50_op11.onnx \
    --framework=5 \
    --output=resnet_50 \
    --soc_version=Ascend910B2C \
    --input_format=NCHW \
    --input_shape="data:1,3,224,224" \
    --log=error

# 推理
python3 run_resnet50_om.py
python3 run_resnet50_om.py --image your.jpg

结语

这次把 ResNet-50 在昇腾上跑通后,最大的感受是:

ATC 本身不难,难的是环境对齐和模型版本兼容。

只要把 Python 依赖、soc_version、ONNX opset 这三件事处理好,经典 CNN 转 OM 还是很顺的;再补一个同输入精度对比,心里就踏实了。

如果你也在做昇腾模型迁移,欢迎把你的 soc_version、CANN 版本和报错贴出来一起对一下。

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐