二、ONNX 基础与模型格式:从 IR 到 proto 的全面拆解

大家好,我是你们的讲师。今天我们来聊聊 ONNX 的基因——它的定义、结构,以及那些你迟早会碰上的细节。这部分是后面所有优化的基石,搞不清楚这些,后面可能会踩坑。

别急,我们一步步来。

2.1 ONNX 是什么?它为什么重要?

ONNX,全称 Open Neural Network Exchange。说白了,它就是一个开放的、跨框架的模型中间格式。你可以把它想象成深度学习世界的“通用语言”。

我个人习惯把 ONNX 比作“模型的二进制交换协议”。为什么这么说?因为它的设计思路和 protobuf 很像——都是一种序列化方案,只不过序列化的对象是神经网络的结构和权重。

目的只有一个:让你的模型从 PyTorch 训出来,转成 ONNX,然后扔到 TensorRT 上去跑。中间不用重写代码,不用手动对齐算子。

我记得早些年做项目,从 Caffe 转 PyTorch 简直要命,算子名对不上、维度顺序反了、权重读出来是反的……有了 ONNX 之后,至少有了一个标准接口。虽然坑依然不少,但至少方向清楚了。

它背后由微软、Facebook(Meta)、Amazon、华为等公司联合维护。目前生态已经非常成熟,几乎所有主流框架都支持导出或导入 ONNX。

2.2 模型格式的“骨架”:IR 与 proto 定义

ONNX 的模型文件(.onnx)本质上是一个protocol buffer(protobuf)序列化后的二进制文件。这个文件由 onnx.proto 文件定义,里面描述了一个深度学习模型的全部要素。

我建议你直接去看官方 repo 里的 onnx/onnx/onnx.proto3。这个文件就是整个 ONNX 世界的“宪法”。

它的核心结构是 IR(Intermediate Representation,中间表示)。IR 定义了一个图(Graph),图里有一堆节点(Node),节点之间通过张量(Tensor)连接。

// 简化后的核心结构
message ModelProto {
  int64 ir_version = 1;          // IR 版本号
  repeated OperatorSetIdProto opset_import = 2; // 算子集引入
  string producer_name = 3;      // 导出工具名(比如 pytorch)
  string producer_version = 4;   // 导出工具版本
  GraphProto graph = 7;          // 核心:计算图
  // ... 其他元数据
}

message GraphProto {
  repeated NodeProto node = 1;               // 图中的所有算子节点
  repeated TensorProto initializer = 5;      // 常量(权重、bias)
  repeated ValueInfoProto input = 2;         // 输入张量描述
  repeated ValueInfoProto output = 3;        // 输出张量描述
  string name = 4;
}

message NodeProto {
  repeated string input = 1;     // 输入张量名列表
  repeated string output = 2;    // 输出张量名列表
  string name = 3;               // 节点名(可选)
  string op_type = 4;            // 算子类型(如 Conv、Relu)
  string domain = 7;             // 域(标准算子留空,自定义算子填 ai.onnx.contrib)
  repeated AttributeProto attribute = 5; // 算子属性(如 kernel_shape、strides)
}

message TensorProto {
  enum DataType { FLOAT = 1; UINT8 = 2; INT8 = 3; ... }
  DataType data_type = 1;
  repeated int64 dims = 2;       // 形状
  bytes raw_data = 5;           // 原始权重数据(字节流)
}

我刚开始读这个 proto 时,觉得太啰嗦了。但后来发现,正是这种“啰嗦”保证了兼容性。每个字段都有版本号、都有清晰的定义,不同框架导出时不会因为字段缺失而崩溃。

2.3 模型结构:图、节点与张量的关系

一个 ONNX 模型,本质就是一个有向无环图。你可以用任何图可视化工具把它画出来,就是一堆框框(节点)和箭头(边)。

概念对应 proto 类型我理解的方式
图 (Graph)GraphProto整个神经网络的所有计算步骤
节点 (Node)NodeProto一个原子操作,比如卷积、激活、归一化
张量 (Tensor)ValueInfoProto / TensorProto流动的数据,包括输入、输出、权重

我画了一张图,帮你理清它们之间的关系。

ONNX 模型结构:Graph → Node → Tensor ModelProto GraphProto (计算图) Input (输入) Conv (卷积) Relu (激活) Output (输出) tensor_0 tensor_1 tensor_2 Initializer (权重/常量) conv_weight 说明:蓝色箭头表示数据流(张量),绿色框表示固化在模型中的权重,虚线箭头表示权重绑定到算子节点。

上面这张图你应该能看明白:

  • 输入张量从左侧进入,流过算子节点,每一步都输出一个新的张量。
  • 权重(initializer)是固化在 GraphProto 里的常量,比如卷积核的数值。它们在推理时不会改变。
  • 每个节点只依赖它的输入张量,独立计算。这给后面的图优化(比如算子融合)提供了理论支撑。

2.4 常用算子与数据类型

ONNX 定义了一套标准算子集,放在 ai.onnx 域下。版本号就是 opset(算子集版本)。从 opset 7 到 opset 21,新增了很多算子,也废弃了一些老算子。

我挑几个你一定会碰到的算子说说。

算子类别常用算子(op_type)我踩过的坑
张量操作Reshape, Transpose, Gather, UnsqueezeReshape 的 shape 参数必须是常量,否则转 TensorRT 会报错
数学运算Add, Mul, Gemm, MatMulMatMul 的精度问题,fp16 下容易溢出
激活函数Relu, Sigmoid, Tanh, LeakyReluLeakyRelu 的 alpha 属性有些框架导出不一样
归一化BatchNormalization, InstanceNormalizationBN 在推理时一定要 freeze,不然会多出训练相关的分支
池化MaxPool, AveragePoolceil_mode 参数默认不同框架不一致,导出前要检查
卷积Conv, ConvTranspose分组卷积(group)的导出优化很棘手
形状推理Shape, Size, Slice, Tile动态形状下,这些算子很难完全静态化

至于数据类型,ONNX 支持常见的浮点型和整型:

  • FLOAT(32位浮点,默认)
  • FLOAT16(16位浮点,TensorRT 的 FP16 推理要用)
  • DOUBLE(64位浮点,很少用,推理时太慢)
  • INT8 / UINT8(量化必备)
  • INT64(主要用于形状参数,比如 Reshape 的 target shape)
  • BOOL(用于条件判断)
避坑指南: 我经常看到有人把 Reshape 的 target shape 设成 INT64。这在 ONNX 本身没问题,但转 TensorRT 时,有些内部优化器只认 INT32。所以我建议:所有形状相关的张量,导出前手动转为 INT32。

2.5 算子集的版本演进:你不能忽视的兼容性

ONNX 的算子集(opset)版本号决定了你的模型能被哪些运行时引擎加载。opset 7(2018 年)和 opset 21(2024 年)之间差了很多东西。

举几个例子:

  • opset 9:引入了 Identity 算子,看起来没用,但导出时经常被当做占位符。
  • opset 11:强化了 ScatterElements、BatchNormalization 的定义,修复了很多歧义。
  • opset 15:增加了 DynamicQuantizeLinear,开始支持动态量化。
  • opset 18:引入了 GroupNormalization、LayerNormalization 等现代算子。

我曾经在项目里遇到过一个 bug:用 PyTorch 1.12 导出的 ONNX 模型(opset 13),在 TensorRT 8.2 上跑不通,报错说 BatchNormalization 算子不支持。后来查到,TensorRT 8.2 只支持 opset 11 的 BN 实现。最后还是降级 opset 解决的。

我的经验: 目前大部分推理引擎都稳定支持 opset 11 ~ 17。如果你不确定目标平台的兼容性,就选 opset 11。它是最安全的起点。需要最新算子时才往上提版本。

学习 ONNX 基础,你不需要背下所有的 proto 字段。但你必须理解:

  • 一个 .onnx 文件里,图、节点、张量是怎么组织的。
  • 算子的版本和兼容性是怎么影响你的推理流程的。
  • 那些你常常忽略的细节(比如数据类型、形状来源)可能成为未来优化的瓶颈。

嗯,这部分就讲到这里。下一小节,我们会深入 ONNX 的导出实践,手把手带你从 PyTorch 导出一个干净的 ONNX 模型。做好准备,我们马上动手。