3. 模型格式与中间表示:ONNX 详解

聊到推理引擎,绕不开的一个话题就是模型格式。说白了,模型训练出来是个啥样,推理引擎能不能读懂,这中间需要一个“翻译官”。我个人觉得,ONNX 就是这个翻译官里最成功的一个。

3.1 为什么需要中间表示?

你想想看,PyTorch 训练出来的模型,和 TensorFlow 训练出来的模型,底层数据结构完全不一样。推理引擎如果直接去对接每一个框架,那工作量就太大了。我早年做嵌入式部署时,就吃过这个亏——客户给了一个 Caffe 模型,我们引擎只支持 TensorFlow,硬生生花了两周写转换脚本,还一堆 bug。

中间表示(IR)就是为了解决这个问题的。它定义了一套统一的、与框架无关的模型描述方式。ONNX 就是其中最著名的一个。

核心价值:ONNX 让模型从“训练框架”到“推理引擎”的迁移,变成了一条标准化的流水线。你只需要把模型导出成 ONNX 格式,任何支持 ONNX 的引擎都能加载它。

3.2 ONNX 是什么?

ONNX,全称 Open Neural Network Exchange。它由微软和 Facebook 在 2017 年联合推出。我刚开始接触它时,觉得这名字起得挺直白——就是想让神经网络模型能自由交换。

ONNX 定义了一套计算图的标准。它包含两个核心部分:

  • 算子集(Operator Set):定义了各种神经网络操作,比如 Conv、Relu、MatMul。每个算子都有明确的输入、输出和属性定义。
  • 类型系统(Type System):定义了张量的数据类型、形状等信息。

嗯,这里要注意:ONNX 不是运行时,它只是一个模型描述格式。你没法直接拿 ONNX 文件去跑推理,得先把它加载到推理引擎里。

3.3 Protobuf 与序列化

ONNX 模型文件长什么样?它其实是一个 .onnx 文件,本质上是一个二进制文件。这个二进制文件是用 Protobuf(Protocol Buffers) 序列化出来的。

Protobuf 是 Google 开发的一种数据序列化协议。它比 XML、JSON 更紧凑,解析速度也更快。我曾在项目中对比过,同样一个模型,用 JSON 存要 200MB,用 Protobuf 只有 80MB,加载时间从 5 秒降到了 1 秒。

ONNX 的 Protobuf 定义文件在官方仓库里可以找到。核心结构大概是这样:

// 简化后的 ONNX 模型结构
message ModelProto {
  int64 ir_version = 1;          // IR 版本号
  repeated OperatorSetIdProto opset_import = 2;  // 引用的算子集
  string producer_name = 3;      // 导出工具名称
  string producer_version = 4;   // 导出工具版本
  GraphProto graph = 5;          // 计算图
}

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

message NodeProto {
  repeated string input = 1;     // 输入张量名称列表
  repeated string output = 2;    // 输出张量名称列表
  string op_type = 3;            // 算子类型,如 "Conv"
  string name = 4;               // 节点名称
  repeated AttributeProto attribute = 5;  // 算子属性
}

你看,整个模型就是一个 ModelProto,里面包含一个 GraphProto,图里又包含一系列 NodeProto。每个节点就是一个算子,节点之间通过张量名称连接。

小技巧:如果你想查看一个 ONNX 文件的内容,可以用 onnx.load() 加载,然后打印 model.graph。我曾经用这个方式排查过一个算子属性不对的问题,省了不少时间。

3.4 ONNX 算子集与版本

ONNX 的算子集是不断演进的。每个版本会新增一些算子,或者修改已有算子的行为。比如 ONNX opset 11 引入了 ScatterND,opset 13 改进了 Softmax 的 axis 行为。

我建议你在导出模型时,尽量使用较新的 opset 版本。但也要注意,推理引擎可能只支持到某个特定版本。我曾经遇到过,导出的模型用了 opset 15 的 Resize,结果部署的引擎只支持到 opset 13,跑起来直接报错。

ONNX 版本 默认 opset 主要变化
1.6 11 新增 ScatterND、Pad 改进
1.8 13 Softmax axis 行为变更
1.10 15 新增 Resize 的 coordinate_transformation_mode
1.12 17 新增 SequenceMap、SequenceInsert 等序列算子

3.5 模型导出实战

以 PyTorch 为例,导出 ONNX 模型其实就几行代码:

import torch
import torchvision.models as models

# 加载一个预训练模型
model = models.resnet18(pretrained=True)
model.eval()

# 创建一个 dummy 输入
dummy_input = torch.randn(1, 3, 224, 224)

# 导出 ONNX
torch.onnx.export(
    model,               # 模型
    dummy_input,         # 示例输入
    "resnet18.onnx",     # 输出文件名
    export_params=True,  # 导出权重
    opset_version=17,    # 算子集版本
    input_names=['input'],
    output_names=['output'],
    dynamic_axes={
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)

print("模型导出成功!")

这里有个坑:dynamic_axes 参数。如果你不设置,导出的模型输入形状是固定的(比如 batch size 固定为 1)。我刚开始做部署时没注意这个,结果线上请求的 batch size 变了,模型直接报 shape 不匹配。后来我养成了习惯,只要有可能,就设置动态 batch size。

避坑指南:我曾经遇到过一个 case,PyTorch 里的 nn.Upsample 导出成 ONNX 后,在推理引擎上跑出来的结果和 PyTorch 不一致。排查了半天,发现是 ONNX 的 Resize 算子的坐标模式(coordinate_transformation_mode)默认值和 PyTorch 不一样。解决方案是手动指定 coordinate_transformation_mode='pytorch_half_pixel'

3.6 ONNX 的局限性

ONNX 虽然好,但也不是万能的。我总结了几点:

  • 算子覆盖不全:有些框架特有的算子,ONNX 可能没有对应实现。比如 PyTorch 的 torch.einsum,导出时可能会 fallback 成多个基础算子,影响性能。
  • 动态控制流支持有限:ONNX 对 if-else、循环等动态控制流的支持比较弱。如果你的模型里有 torch.where 或者循环,导出可能会失败。
  • 版本兼容性问题:不同版本的 ONNX 和不同版本的推理引擎之间,可能存在兼容性问题。我建议你在导出前,先确认目标引擎支持的 ONNX opset 版本。

3.7 知识体系总览

下面这张图,是我梳理的 ONNX 核心知识体系。你可以把它当作一个快速索引:

ONNX 核心知识体系 ONNX 模型格式 序列化:Protobuf(Protocol Buffers) 核心结构:ModelProto → GraphProto → NodeProto 算子集(Operator Set):版本演进 导出流程:PyTorch/TF → ONNX → 推理引擎 二进制序列化,比 JSON 更紧凑 计算图 + 权重 + 输入输出信息 opset 版本决定算子行为 注意 dynamic_axes 和算子兼容性

这张图把 ONNX 的四个核心维度串起来了:序列化方式、内部结构、算子版本、导出流程。你每次遇到 ONNX 相关的问题,都可以先对照这张图,定位问题出在哪个环节。

个人习惯:我一般在项目初期,就会把 ONNX 导出脚本写好,并且用 onnxruntime 做一次前向对齐测试。确保 PyTorch 输出和 ONNX 输出的误差在 1e-5 以内。这一步虽然简单,但能避免后期很多莫名其妙的 bug。

好了,关于 ONNX 和 Protobuf 的内容就聊到这里。模型格式是推理引擎生态的基石,理解它,你就能更好地理解后续的模型优化和部署流程。

专注资料整理