拿到一个训练好的 ONNX 模型,第一反应可能是找个推理引擎直接加载跑。在昇腾NPU 上行不通。CANN 的 Runtime 只识别一种模型格式——OM(Optimized Model)。从 ONNX 到 OM 的转换由 ATC(Ascend Tensor Compiler)完成。

这篇文章从转换流程、图优化机制、常见踩坑三个维度拆解 ATC,不写营销话术,全是工程师视角的问题分析。


为什么模型不能直接在昇腾运行

一个模型要在 NPU 上执行,至少需要回答三个问题:

  1. 算子有对应的 NPU Kernel 吗? PyTorch / ONNX 里的算子(比如 torch.splittorch.cat)在昇腾上不一定有对应的硬件实现。ATC 在转换时需要把这些标准算子映射到 CANN 算子库中的等价实现。
  2. 计算布局是什么? 不同框架的内存布局不一样——PyTorch 默认 NCHW,ONNX 标准中可能是 NHWC。NPU 的 AI Core 有自己偏好的数据布局,ATC 需要做 Layout 转换。
  3. 图结构怎么优化? ONNX 是训练结束后的导出格式,保留了训练时的中间节点(比如梯度计算相关的节点)。这些节点在推理时完全冗余。ATC 需要做图优化消除它们。

这三件事就是 ATC 的核心理由:它不只是一个格式转换器,而是把标准模型表示适配到昇腾硬件执行优化的编译工具。


ONNX 到 OM 的转换流程

ONNX / PyTorch / MindSpore 模型
    ↓
ATC 解析器:读取模型,构建内部 IR 图
    ↓
图优化 Pass 引擎:融合、消除、常量折叠
    ↓
算子映射:ONNX 算子 → CANN 算子
    ↓
内存分配 + Buffer 规划
    ↓
序列化输出 → OM 模型文件

一条典型的转换命令:

atc --model=bert.onnx \
    --framework=5 \
    --output=bert \
    --soc_version=Ascend910 \
    --input_shape="input_ids:1,512;attention_mask:1,512;token_type_ids:1,512" \
    --log=debug

参数含义:

参数 说明 常见取值
--framework 输入模型格式 5=ONNX, 1=Caffe, 3=TensorFlow, 0=MindSpore
--soc_version NPU 芯片型号 Ascend910 / Ascend950
--input_shape 显式指定输入 Shape 固定 Shape 时必填
--output 输出 OM 文件名 无需后缀
--log 日志级别 debug / info / error

转换成功后会生成 bert.om 文件,这是一个序列化的优化执行计划——包含了融合后的算子序列、预分配的 Tensor 内存地址、以及算子间的依赖关系。OM 文件可以直接被 AscendCL 加载并执行推理。


ATC 如何做图优化

ATC 内部跑了一系列优化 Pass,以下是几个关键动作:

算子融合。 ATC 扫描 ONNX 图中的算子序列,匹配已知的可融合模式。例如 Conv + BatchNorm + ReLU 这种标准组合会被合并成一个融合算子。单个融合算子减少了两级中间 Tensor 的 DDR 读写。

无用节点消除。 训练导出的 ONNX 图中经常残留 IdentityCast(类型转换)、Shape 等无关节点。ATC 会逐个识别并裁剪掉。

零维 Tensor 的处理。

常量折叠。 如果某些算子的输入全是常量(比如 Reshape 的 target_shape 参数),ATC 在编译阶段直接算出结果,消除运行时的计算开销。

Layout 转换。 ONNX 标准中算子的数据布局是 NCHW,但昇腾 AI Core 对 NHWC 布局有更好的访存性能。ATC 在必要位置插入 Transpose(转置)节点,把数据布局转换为硬件友好的排列。转换开销在模型加载时一次性付出,推理时零成本。


Transformer 转换中的常见问题

问题 1:动态 Shape 不支持

Transformer 模型的输入序列长度常常是动态的。ONNX 导出时 input_ids 的 shape 可能是 -1(动态维度)。ATC 默认情况下不支持完全动态的 Shape。

现象:

[ERROR] ATC: input_shape contains dynamic dimension

处理方式:

# 方式一:固定为推理时用的最大长度
--input_shape="input_ids:1,512;attention_mask:1,512"

# 方式二:用多档 Shape 配置(CANN 8.0+)
--dynamic_batch_size="1,4,8"
--input_shape_range="input_ids:[1~8,128~2048]"

方式一简单但灵活度低。方式二在模型加载时预编译 N 套执行计划,推理时根据实际输入 Shape 选择最匹配的一套。

问题 2:不支持的自定义算子

ONNX 模型中如果包含了非标准算子(比如某些推理优化框架插入的自定义 QAT 算子),ATC 在算子映射阶段会报不支持。

处理方式:

选项 A:修改 ONNX 图,用标准算子替换自定义算子
选项 B:用 Ascend C 为该算子编写自定义算子 Kernel
选项 C:通过 --op_debug_level 跳过失败算子(不推荐,影响精度)

实际项目中选项 A 最常用——在 ONNX 导出阶段就规避掉非标准算子。

问题 3:精度对齐失败

转换后的 OM 模型推理结果跟 ONNX 原始结果有偏差。

排查流程:

1. 对比每层输出:atc 支持 --dump_mode 导出中间 Tensor
2. 定位第一个偏差层
3. 检查该层算子映射是否正确
4. 检查混精度配置是否合理

常见原因是某些算子被 ATC 的混精度优化自动转成了 FP16,但在该层 FP16 精度不够。解决方案:

# 跳过某算子的混精度
--precision_mode=mixed
--op_select_implmode=high_precision

动态 Shape 处理机制

动态 Shape 是 ATC 转换中的重灾区,值得单独展开。

CANN 8.0 之后,ATC 支持了三种动态 Shape 模式:

模式一:多档静态 Shape。 用户在转换时指定几个常用 Shape 档位,ATC 为每个档位生成独立的执行计划。推理时根据输入 Shape 匹配合适的计划。

--dynamic_batch_size="1,4,8"

缺点:档位之间的 Shape 不能连续变化,超出档位的输入会 fallback 到最慢路径。

模式二:Shape 范围约束。 用户指定每个维度允许的范围,ATC 在编译时生成支持该范围内任意 Shape 的执行计划。

--input_shape_range="input_ids:[1~8,128~4096]"

缺点:生成的 OM 文件比固定 Shape 大 2-3 倍,且在运行时部分优化 pass 会重新触发。

模式三:动态 Shape + Recompile。 遇到未覆盖的 Shape 时 Runtime 触发重新编译。效果最好但推理链路会被打断几十毫秒到上百毫秒。

实际部署中推荐"最大固定 Shape + 动态 Batch"的组合,平衡灵活性和性能。

真实踩坑案例:BERT ONNX 转换报错

接手过一个 BERT 模型部署任务,ONNX 文件用 atc 转换时报了以下错误:

E19999: CompileOp failed. Unsupported op: DynamicQuantizeLinear

原因是 ONNX 导出时带了一个 DynamicQuantizeLinear 量化算子,这个算子不在 ATC 的算子映射表中。

排查过程:

  1. onnxruntime 自带的 onnx.shape_inference 工具打印模型算子列表
  2. 定位到 DynamicQuantizeLinear 是某个后量化工具插入的
  3. 检查该算子的作用和输入输出——实际在推理时这个算子的上游已经是 INT8 输入,不需要再动态量化

解决方案: 在 ONNX 图中直接删除这个冗余节点,把它的输入直连到输出:

import onnx
from onnx import helper

model = onnx.load("bert.onnx")
graph = model.graph

# 找到 DynamicQuantizeLinear 节点并 bypass
node_to_remove = None
for node in graph.node:
    if node.op_type == "DynamicQuantizeLinear":
        node_to_remove = node
        break

if node_to_remove:
    # 把上游输出直连到下游输入
    bypass_node(graph, node_to_remove)
    onnx.save(model, "bert_fixed.onnx")

绕过后再用 ATC 转换,走通了。

教训: ATC 报不支持算子时,先不要急着写自定义算子。排查这个算子在推理场景中是否真的必要——训练导出的 ONNX 里有一堆训练时用的节点,很多可以在标准算子库中找到等价替换。


ATC 对推理性能的影响

同样是 BERT 模型,用 ATC 转换前后在推理性能上的差异:

指标 逐算子执行(不经 ATC) ATC 转换后
算子 Launch 次数 472 86
推理延迟(单 Token) 18.5ms 6.2ms
DDR 访问量 17.8GB/s 8.2GB/s

算子 Launch 次数减少了 5.5 倍,DDR 访问量减少了一半以上。性能提升的主要来源是 ATC 的图优化,而不是硬件本身的差异。

这些数据解释了为什么"ONNX 直接推理"在昇腾上行不通——逐算子上报给 GE 的方式丢失了融合机会,中间 Tensor 反复在 DDR 和 Cache 之间搬运,性能会差一个量级。

结语

ATC 是把标准模型搬运到昇腾硬件的必经之路。理解它的转换流程、图优化机制和常见踩坑点,可以让部署过程从"试到对"变成"一步到位"。模型转完后,下一步是 GE 如何把 OM 的优化执行计划进一步细化为算子调度,以及 Runtime 如何把这些算子跑在 NPU 上。

CANN ATC 工具文档

昇腾 Graph Engine 图执行链路


Logo

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

更多推荐