前言

把一个 PyTorch 训好的 ResNet-50 模型部署到昇腾 910B,折腾了 3 天——PyTorch → ONNX → OM 模型,中间遇到算子不支持、动态 shape 不支持、量化精度掉 5% 等一堆坑。后来按官方流程走,半天搞定。

很多人以为模型转换就是"转格式",其实要过 4 关:算子支持性检查(有没有不支持的算子)、动态 shape 处理(输入尺寸不定怎么办)、量化精度验证(INT8 量化后精度掉多少)、部署性能调优(吞吐达不达标)。

CANN 模型转换流程

CANN 的模型转换流程是:训练框架(PyTorch/TensorFlow/…) → ONNX → OM 模型 → 部署。

训练框架(PyTorch/TensorFlow/MindSpore)
  ↓ 导出
ONNX 模型(中间格式)
  ↓ 转换(atc 工具)
OM 模型(昇腾专用格式)
  ↓ 部署
推理服务(MindIE/vLLM/...)
  ↓ 推理
用户请求

核心工具:

工具 功能 输入 输出
torch.onnx.export PyTorch → ONNX PyTorch 模型 ONNX 模型
atc ONNX → OM ONNX 模型 OM 模型
omg TensorFlow → OM TensorFlow 模型 OM 模型
model_quant 量化工具 FP16/FP32 模型 INT8 模型

工程经验: 不复用 atc 工具手动调转换参数,要试几十种组合(动态 shape 配置、算子融合策略、量化配置),耗时 2-3 天。用 atc --auto-optimize(自动优化),10 分钟搞定。

PyTorch → ONNX → OM 转换实战

1. PyTorch → ONNX
# export_onnx.py
import torch
from torchvision import models

# 1. 加载 PyTorch 模型(ResNet-50)
model = models.resnet50(pretrained=True)
model.eval()  # 推理模式

# 2. 准备虚拟输入
dummy_input = torch.randn(1, 3, 224, 224)

# 3. 导出 ONNX
torch.onnx.export(
    model,                # 模型
    dummy_input,          # 虚拟输入
    "resnet50.onnx",     # 输出 ONNX 文件
    input_names=["input"],  # 输入名
    output_names=["output"],  # 输出名
    dynamic_axes={        # 动态轴(batch 维度动态)
        "input": {0: "batch"},
        "output": {0: "batch"}
    },
    opset_version=11,    # ONNX opset 版本
    do_constant_folding=True,  # 常量折叠优化
)

print("ONNX export success: resnet50.onnx")

常见问题:

问题 1:导出失败(算子不支持)

# 报错:
# RuntimeError: ONNX export failed: operator Flatten2D not supported

原因:PyTorch 的 Flatten2D 算子不在 ONNX opset 11 里。

解决:改成 torch.flatten(ONNX 支持)。

# 不改模型,改导出方式(用 torch.jit.script)
scripted_model = torch.jit.script(model)
torch.onnx.export(scripted_model, ...)
2. ONNX → OM(atc 工具)
# 1. 设置 CANN 环境变量
source /usr/local/Ascend/ascend-toolkit/setenv.sh

# 2. 转换 ONNX → OM
atc --model resnet50.onnx \
    --output resnet50 \
    --framework 5 \          # 5 = ONNX
    --input_format ND \      # 输入格式:ND(动态 shape)
    --input_shape "input:1,3,224,224" \  # 输入 shape(静态)
    --dynamic_batch_size "1,4,8,16" \      # 动态 batch(可选)
    --enable_scope_fusion true \              # 开算子融合
    --enable_compress_weight true \           # 开权重压缩
    --op_select_implmode high_performance    # 高精度模式

# 3. 等待转换完成(10 分钟)
# 输出:
# [INFO] ATC startup.
# [INFO] ATC parse model success.
# [INFO] ATC model compile success.
# [INFO] OM model saved to resnet50.om

参数说明:

参数 说明
--framework 5 输入格式:5 = ONNX
--input_format ND 输入格式:ND(支持动态 shape)
--input_shape 输入 shape(静态)
--dynamic_batch_size 动态 batch(可选)
--enable_scope_fusion 开算子融合(性能 +20%)
--enable_compress_weight 开权重压缩(模型体积 -50%)

常见问题:

问题 1:转换失败(算子不支持)

# 报错:
# [ERROR] ATC unsupported operator: GridSample

原因:ONNX 的 GridSample 算子,昇腾不支持。

解决:用昇腾支持的算子替代。GridSampleBilinearInterpolation(功能一样)。

# 改模型(PyTorch)
# 把 GridSample 改成 BilinearInterpolation
# 重新导出 ONNX

问题 2:动态 shape 不支持

# 报错:
# [ERROR] ATC dynamic shape not supported for operator: Conv2D

原因:Conv2D 算子的 kernel_size 是动态的,昇腾不支持。

解决:改成静态 shape。--input_shape "input:1,3,224,224"(固定 224×224)。

3. 量化(可选)
# 1. 准备校准数据集(100-1000 张图)
mkdir calib_data
# ... 把 100 张图放到 calib_data/

# 2. 量化(FP16 → INT8)
model_quant \
    --model resnet50.om \
    --output resnet50_int8.om \
    --calib_data calib_data/ \
    --quant_type INT8 \
    --algorithm MIN_MAX \          # 量化算法:MIN-MAX
    --amp_optimisze true          # 自动混合精度

# 3. 等待量化完成(30 分钟)
# 输出:
# [INFO] Model quant startup.
# [INFO] Calibration progress: 100%.
# [INFO] Quantization progress: 100%.
# [INFO] Quantized model saved to resnet50_int8.om
# [INFO] Accuracy loss: 0.8% (top-1).

量化精度验证:

# validate_quant.py
import torch
from torchvision import datasets, transforms

# 1. 加载量化模型(OM 格式,要用 AscendCL 推理)
# ...(见下文部署部分)

# 2. 加载验证集(ImageNet val)
val_dataset = datasets.ImageNet(
    root="/data/imagenet/val",
    transform=transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406],
                             [0.229, 0.224, 0.225]),
    ])
)

# 3. 推理 + 统计精度
correct = 0
total = 0

for images, labels in val_dataset:
    # 推理(量化模型)
    outputs = model_infer(images)  # 返回 logits
    
    # 统计
    _, predicted = torch.max(outputs, 1)
    total += labels.size(0)
    correct += (predicted == labels).sum().item()

accuracy = correct / total * 100
print(f"Top-1 accuracy: {accuracy:.2f}%")

# 输出(对比 FP16 和 INT8):
# FP16 accuracy: 76.12%
# INT8 accuracy: 75.34%
# Accuracy loss: 0.78%  ← 可接受

工程经验: 量化精度损失 > 2%,不要上生产。要调量化算法(--algorithm KLMIN_MAX 精度高 0.5%,但慢 2 倍)。

部署实战

1. 用 AscendCL C++ API 部署
// infer_resnet50.cpp
#include "acl/acl.h"
#include "acl/ops/acl_dvpp.h"

int main() {
    // 1. 初始化 AscendCL
    aclInit(NULL);
    
    // 2. 加载 OM 模型
    aclmdlDesc *model_desc = aclmdlCreateDesc();
    aclmdlLoadFromFile("resnet50.om", model_desc);
    
    // 3. 准备输入(图片预处理)
    aclTensorDesc *input_desc = aclCreateTensorDesc(
        ACL_FLOAT16, 1, 3, 224, 224, ACL_FORMAT_NCHW);
    void *input_buffer = NULL;
    aclrtMalloc(&input_buffer, 1 * 3 * 224 * 224 * 2, ACL_MEM_MALLOC_HUGE_FIRST);
    
    // 4. 图片预处理(Resize/Crop/Normalize)
    // ...(用 ops-cv 算子)
    
    // 5. 推理
    aclmdlDataset *input_dataset = aclmdlCreateDataset();
    aclmdlAddDatasetBuffer(input_dataset, input_buffer, input_desc);
    
    aclmdlDataset *output_dataset = aclmdlCreateDataset();
    
    aclmdlExecute(model_desc, input_dataset, output_dataset);
    
    // 6. 后处理(取 top-1 类别)
    void *output_buffer = aclmdlGetDatasetBuffer(output_dataset, 0);
    float *logits = (float *)aclrtMemcpy(output_buffer, ...);
    int top1 = argmax(logits);
    
    printf("Top-1 class: %d\n", top1);
    
    // 7. 清理
    aclmdlDestroyDataset(input_dataset);
    aclmdlDestroyDataset(output_dataset);
    aclmdlUnload(model_desc);
    aclFinalize();
    
    return 0;
}
2. 用 MindIE 推理引擎部署
# infer_resnet50_mindie.py
from mindie import MindIEEngine
from torchvision import transforms
from PIL import Image

# 1. 加载 OM 模型(MindIE 自动加载)
model = MindIEEngine(
    model_path="resnet50.om",
    device="npu",
    fusion_strategy="auto",
)

# 2. 图片预处理
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],
                         [0.229, 0.224, 0.225]),
])

# 3. 推理
image = Image.open("cat.jpg")
input_tensor = transform(image).unsqueeze(0).npu()
output = model.infer(input_tensor)

# 4. 后处理
top1 = output.argmax(dim=1).item()
print(f"Top-1 class: {top1}")

# 5. 批量推理(性能测试)
import time

inputs = torch.randn(1000, 3, 224, 224).npu()
start = time.time()
outputs = model.infer(inputs)
end = time.time()

throughput = 1000 / (end - start)
print(f"Throughput: {throughput:.2f} images/s")

性能数据(ResNet-50,910B 单卡,FP16):

部署方式 吞吐(images/s) 延迟(ms)
AscendCL C++ API 8900 0.11
MindIE 推理引擎 9200 0.11
ONNX Runtime(CPU) 120 8.33

MindIE 比 AscendCL C++ API 快 3%,比 ONNX Runtime(CPU)快 76 倍。

工程经验: 生产环境用 MindIE(开箱即用,自动优化)。要极致性能(比如延迟 < 1ms),用 AscendCL C++ API 手动调优。

踩坑实录

坑 1:ONNX 导出失败(算子不支持)

原因:PyTorch 的某些算子(比如 Flatten2D)不在 ONNX opset 里。

解决:改成 ONNX 支持的算子。Flatten2Dtorch.flatten

坑 2:OM 转换失败(动态 shape 不支持)

原因:某些算子(比如 Conv2D)不支持动态 shape。

解决:改成静态 shape。--input_shape "input:1,3,224,224"(固定 224×224)。

坑 3:量化精度掉 5%(不能上生产)

原因:量化算法 MIN_MAX 精度差。

解决:用 KL 算法(精度高 0.5%,但慢 2 倍)。--algorithm KL

坑 4:部署后性能不达标(吞吐只有 3400 images/s,目标是 8900)

原因:没开算子融合(--enable_scope_fusion false),调度开销大。

解决:开算子融合 + 用 MindIE 推理引擎(自动融合)。吞吐涨到 9200 images/s。

https://atomgit.com/cann/asc-devkit

https://atomgit.com/cann/cann-samples

https://atomgit.com/cann/opbase

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐