在这里插入图片描述

前言

上个月帮同事迁一个目标检测模型,从 GPU 转到昇腾 NPU。本来以为半天搞定,结果算子不支持、精度对不上、性能还不如原来,前后搞了一周。

后来发现昇腾工具包里有个 msadvisor,能在迁移前把问题都扫出来。如果一开始就用它,那一周的坑能省掉大半。


先说痛点

模型迁移最怕的不是代码改不动,而是改完之后才发现问题。

常见的坑有这几个:

算子不支持。 模型里用了 HardSigmoid 或者 Swish,导出 ONNX 没问题,但 ATC 编译的时候报错——“Op not supported”。这时候模型已经改了一大半,回头去换算子很费时间。

精度对不上。 编译通过了,推理也能跑,但输出的结果和 GPU 版本对不上。余弦相似度只有 0.95,差了 0.04。这时候要逐层排查,看是哪一层出了问题,又是大半天。

性能不达标。 终于跑通了,结果一测性能,比 GPU 还慢 20%。老板问为什么,你说不上来——是算子没融合?还是 batch size 太小?还是某个算子选了 Vector 实现而不是 Cube?

这三个坑,msadvisor 都能在迁移前帮你扫出来。


算子支持性检查

这是最基础的功能,但也最实用。

怎么用

把 ONNX 模型丢给它就行:

msadvisor check-onnx \
    --model=yolov5s.onnx \
    --output=./check_result

跑完之后,./check_result 目录下会生成几个文件。最重要的是 support_matrix.csv,里面列出了模型里所有算子,以及它们的支持情况。

用 Pandas 读一下:

import pandas as pd

df = pd.read_csv("./check_result/support_matrix.csv")
unsupported = df[df["Support Status"] == "Unsupported"]

print(f"总算子数:{len(df)}")
print(f"不支持的算子:{len(unsupported)}")
print(unsupported[["Op Type", "Alternative"]])

输出大概是这样:

总算子数:73
不支持的算子:2

Op Type         Alternative
HardSigmoid     Clip+Add+Mul
Swish           Sigmoid+Mul

看到没?它不只告诉你哪些算子不支持,还告诉你替代方案。HardSigmoid 可以用 Clip+Add+Mul 组合来替代,数学上完全等价,精度不会有损失。

自动生成替换代码

手动去改模型代码很麻烦,尤其是模型大的时候。msadvisor 可以自动生成替换后的代码:

msadvisor auto-fix \
    --model=yolov5s.py \
    --check_result=./check_result \
    --output=./fixed_model

它会生成一个 yolov5s_fixed.py,里面的 HardSigmoid 已经被替换成了标准算子组合。你可以直接对比修改后的代码:

# 原始代码(yolov5s.py)
class HardSigmoid(nn.Module):
    def forward(self, x):
        return torch.nn.functional.hardsigmoid(x)

# 替换后代码(yolov5s_fixed.py)
class HardSigmoidFixed(nn.Module):
    def forward(self, x):
        # HardSigmoid(x) = clip(x * 1/6 + 0.5, 0, 1)
        return torch.clamp(x * (1.0 / 6.0) + 0.5, 0.0, 1.0)

这种替换是数学等价的,你可以放心用,精度不会有问题。


精度对比

算子都支持之后,下一步是确认精度。你肯定不想迁完了才发现输出对不上。

msadvisor 可以自动做 GPU 和 NPU 的精度对比。当然,这需要你同时有 GPU 和 NPU 环境。

启动对比

msadvisor accuracy-compare \
    --model=yolov5s_fixed.onnx \
    --gpu_device=0 \
    --npu_device=0 \
    --input_data=./calibration_images \
    --output=./accuracy_result

--input_data 指定校准图片的路径,随便挑 50-100 张就行,不需要完整的验证集。

看结果

对比结果会保存在 ./accuracy_result/accuracy_report.json 里。用 Python 读一下:

import json

with open("./accuracy_result/accuracy_report.json") as f:
    report = json.load(f)

print(f"余弦相似度:{report['cosine_similarity']:.4f}")
print(f"最大绝对误差:{report['max_abs_error']:.6f}")
print(f"平均绝对误差:{report['mean_abs_error']:.6f}")
print(f"是否通过:{report['pass']}")

输出:

余弦相似度:0.9991
最大绝对误差:0.0087
平均绝对误差:0.0009
是否通过:True

余弦相似度大于 0.99 算通过。如果没通过,报告里会列出哪些层的误差最大:

for layer in report["failed_layers"]:
    print(f"层名:{layer['name']}")
    print(f"  余弦相似度:{layer['cosine']:.4f}")
    print(f"  建议:{layer['suggestion']}")

常见的建议有两个:

  1. 把该层改成 FP32 计算。 有些算子用 FP16 精度不够,改成 FP32 就好了。
  2. 检查输入数据是否对齐。 GPU 和 NPU 的输入必须是同一份数据,不能一个是 BGR 一个是 RGB。

性能预估

精度没问题了,接下来关心性能。msadvisor 可以根据算子类型估算迁移后的性能。

生成性能报告

msadvisor performance-estimate \
    --model=yolov5s_fixed.onnx \
    --input_shape="images:1,3,640,640" \
    --output=./perf_result

看报告

性能报告在 ./perf_result/performance_report.json

import json

with open("./perf_result/performance_report.json") as f:
    perf = json.load(f)

print(f"预估推理延迟:{perf['latency_ms']:.1f} ms")
print(f"预估 AI Core 利用率:{perf['aicore_util']}%")
print(f"预估内存占用:{perf['memory_mb']} MB")
print(f"预估吞吐量:{perf['throughput_fps']:.0f} FPS")

输出:

预估推理延迟:13.2 ms
预估 AI Core 利用率:68%
预估内存占用:890 MB
预估吞吐量:76 FPS

报告里还有瓶颈分析:

for bottleneck in perf["bottlenecks"]:
    print(f"瓶颈算子:{bottleneck['op']}")
    print(f"  原因:{bottleneck['reason']}")
    print(f"  建议:{bottleneck['suggestion']}")

输出:

瓶颈算子:Conv_18
  原因:Cube Unit 利用率低,batch size 太小
  建议:增大 batch size 到 4 以上

瓶颈算子:Softmax_21
  原因:Vector Unit 瓶颈,没有和前一个算子融合
  建议:开启算子融合(--enable_fusion=true)

这些建议很实用。按照建议改了之后,再重新预估一遍,通常能看到明显的性能提升。


集成到开发流程

msadvisor 可以集成到 CI/CD 里,每次提交模型代码都自动检查。

我们用的是 GitHub Actions,配置文件大概是这样:

# .github/workflows/model-migration-check.yml
name: Model Migration Check

on: [push, pull_request]

jobs:
  migration-check:
    runs-on: self-hosted-npu
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Export ONNX
      run: |
        python export_onnx.py
    
    - name: Run msadvisor check
      run: |
        msadvisor check-onnx \
            --model=model.onnx \
            --output=./check_result
    
    - name: Check unsupported ops
      run: |
        if [ -s ./check_result/unsupported_ops.txt ]; then
          echo "❌ Unsupported ops found!"
          cat ./check_result/unsupported_ops.txt
          exit 1
        else
          echo "✅ All ops supported"
        fi

这样每次提交代码,都会自动检查算子支持性。如果有不支持的算子,CI 就会失败,不让合并。这比迁到一半才发现问题的效率高多了。


几个常见问题的处理方式

动态 shape 不支持

YOLOv5 的输入尺寸是动态的(640×640 或 1280×1280)。导出 ONNX 的时候,要指定动态维度:

# 错误写法:固定 shape
torch.onnx.export(
    model,
    torch.randn(1, 3, 640, 640),
    "yolov5s.onnx"
)

# 正确写法:指定动态维度
torch.onnx.export(
    model,
    torch.randn(1, 3, 640, 640),
    "yolov5s.onnx",
    dynamic_axes={
        "images": {2: "height", 3: "width"},
        "output": {2: "height", 3: "width"}
    }
)

然后用 ATC 编译的时候,也要指定动态范围:

atc --model=yolov5s.onnx \
    --framework=5 \
    --output=yolov5s \
    --input_shape_range="images:[1,3,320,320~1280,1280]"

自定义算子

如果模型里用了自定义算子(比如 torch.autograd.Function),ONNX 导出后会变成 custom_op,ATC 编译会报错。

解决方式是用 Ascend C 重写这个算子。msadvisor 的 migration_suggestions.json 里会给出算子的输入输出的 shape 和 dtype,照着写。

精度不达标

如果余弦相似度小于 0.99,先查报告里的 failed_layers,找到问题层,然后:

  1. 把该层改成 FP32(在模型代码里加 .float()
  2. 重新导出 ONNX 并编译
  3. 再次跑精度对比

如果还不行,可能是数据预处理的问题。确认 GPU 和 NPU 用的预处理代码是完全一样的,特别是 BGR/RGB 这种细节。


msadvisor 的局限

msadvisor 能发现大部分问题,但不是万能的:

  1. 它只能检查算子支持性,不能保证性能。 性能跟具体输入数据、batch size、内存布局都有关系,预估会有偏差。
  2. 精度对比需要 GPU 环境。 如果手上没有 GPU,这一步做不了。可以考虑用朋友的 GPU 机器,或者在云上租一台。
  3. 自定义算子的替代方案不一定最优。 auto-fix 生成的代码是数学等价替换,但不一定是最优实现。比如性能可能不是最好的,需要你手动优化。

所以 msadvisor 的定位是「迁移前的检查清单」,不是「迁移后的性能保证」。它帮你把明显的坑填上,但深层次的性能优化还是要你自己做。


参考资源

  • msadvisor 用户指南:https://www.hiascend.com/document/detail/zh/CANN/
  • 模型迁移最佳实践:https://www.hiascend.com/document/detail/zh/CANN/
  • 算子支持列表查询:https://www.hiascend.com/document/detail/zh/CANN/
  • YOLOv5 昇腾迁移样例:https://atomgit.com/cann/models

总结

msadvisor 的核心价值是把迁移问题提前暴露。算子不支持的、精度可能有问题的、性能可能有瓶颈的,都在迁移前列出来。完整的检查流程是:算子支持性检查、自动生成替换代码、精度对比、性能预估。集成到 CI/CD 之后,每次提交模型代码都会自动检查,避免迁移到一半才发现问题。遇到不支持的算子,优先用数学等价替换,实在不行再用 Ascend C 重写。精度对比的余弦相似度要大于 0.99,否则要逐层排查哪一层出了问题。性能预估会有偏差,但瓶颈分析的建议通常是有用的。

Logo

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

更多推荐