ONNX模型基础:ONNX格式、模型结构、节点与张量、动态轴
核心原理:ONNX作为模型交换的“通用语言”
ONNX(Open Neural Network Exchange)本质上是一种中间表示格式,它不直接执行推理,而是定义了一套跨框架的计算图描述规范。当我们将PyTorch模型导出为ONNX时,实际上是在做一次“翻译”:把PyTorch的动态图执行逻辑,转换为一张静态的计算图,图中每个节点代表一个算子(如Conv、Relu、Gemm),每条边代表张量(Tensor)的流动方向。
理解ONNX的关键在于区分三个层次:模型级(ModelProto)、图级(GraphProto)和节点级(NodeProto)。一个ONNX文件就是一个序列化的ModelProto,它包含一个GraphProto;GraphProto由多个NodeProto组成节点列表,同时维护了图的输入输出张量信息。这种嵌套结构使得ONNX既能表达简单的线性网络,也能表达带有分支、循环的复杂拓扑。
核心要点:ONNX文件不是“模型权重包”,而是“计算图+权重”的复合体。权重以初始化器(Initializer)的形式存放在图中,作为节点的常量输入。因此,ONNX文件的大小主要由权重张量决定,而计算图本身通常只占很小体积。
模型结构:从ProtoBuf到可视化计算图
ONNX基于Google Protobuf序列化,因此文件后缀为.onnx。我们可以用Python API快速解析其结构:
import onnx
model = onnx.load("resnet18.onnx")
graph = model.graph
print("节点数量:", len(graph.node))
print("输入:", [(i.name, [d.dim_value for d in i.type.tensor_type.shape.dim]) for i in graph.input])
print("输出:", [(o.name, [d.dim_value for d in o.type.tensor_type.shape.dim]) for o in graph.output])
print("初始化器数量:", len(graph.initializer))
一个典型的ONNX图包含以下核心字段:
| 字段 | 作用 | 示例 |
|---|---|---|
node | 算子节点列表,按拓扑序排列 | Conv、Relu、MaxPool |
input | 图的输入张量定义(含形状与类型) | input: float32[1,3,224,224] |
output | 图的输出张量定义 | output: float32[1,1000] |
initializer | 权重与常量张量(不参与梯度) | conv1.weight: float32[64,3,7,7] |
value_info | 中间张量的形状推断结果 | layer1_out: float32[1,64,56,56] |
每个节点本身又包含op_type(算子类型)、input(输入张量名列表)、output(输出张量名列表)以及attribute(算子属性,如卷积的kernel_shape、strides)。张量在图中通过名称字符串进行关联,而非指针或索引,这保证了图的序列化友好性。
节点与张量:理解数据流的关键
ONNX中的张量(Tensor)是不可变的数据载体,它只有名称、数据类型和形状三个核心属性。节点则是计算单元,它消费输入张量并产生输出张量。这种设计使得ONNX图天然适合静态分析——我们可以不执行计算,仅通过遍历节点和张量名就能还原完整的数据流路径。
以ResNet中的一个BasicBlock为例,其ONNX子图结构如下:
节点序列(简化):
Conv_0: input[input, res1.weight] → output[conv1_out]
Relu_1: input[conv1_out] → output[relu1_out]
Conv_2: input[relu1_out, res2.weight] → output[conv2_out]
Add_3: input[conv2_out, input] → output[add_out] # 残差连接
Relu_4: input[add_out] → output[block_out]
注意Add_3节点直接引用了图的原始输入input,这就是残差连接在ONNX中的表达方式——通过张量名实现跨层引用。理解这一点对后续TensorRT优化至关重要,因为TensorRT的层融合(如将Conv+BN+Relu融合为单个算子)正是基于这种数据流依赖关系进行重写的。
实操建议:使用onnx.graphsurgeon或netron可视化工具查看模型时,重点关注张量名的命名规律。PyTorch导出的模型张量名通常为"/layer1.0/conv1/Conv_output_0"这种带路径前缀的格式,便于溯源。
动态轴:从固定形状到可变批量
ONNX默认使用静态形状描述张量,即每个维度都是固定整数。但实际部署中,我们往往需要支持不同的batch size或输入分辨率。为此,ONNX引入了动态轴(Dynamic Axes)机制,在导出时通过dynamic_axes参数指定哪些维度是可变的:
import torch
# 导出时允许batch维度和宽高维度动态变化
torch.onnx.export(
model, dummy_input, "model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size", 2: "height", 3: "width"},
"output": {0: "batch_size"}
},
opset_version=17
)
导出后,ONNX图中对应维度的dim_param字段会变为字符串(如"batch_size"),而非整数。TensorRT在解析这类模型时,会将其转换为优化配置文件(Optimization Profile)中的动态范围。需要注意的是,并非所有算子都支持动态形状——例如某些全局池化或reshape操作可能要求输入形状完全确定。
常见误区:动态轴并非越多越好。每个动态维度都会增加TensorRT的优化搜索空间,导致构建时间变长。如果业务场景中batch size固定为1,建议导出时保持静态,仅在需要弹性伸缩时才启用动态轴。
案例:将PyTorch模型导出为ONNX并检查动态轴
下面演示一个完整的导出与验证流程。假设我们有一个简单的CNN分类模型:
import torch
import torch.nn as nn
import onnx
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(3, 16, 3, padding=1)
self.fc = nn.Linear(16*8*8, 10)
def forward(self, x):
x = torch.relu(self.conv(x))
x = x.view(x.size(0), -1)
return self.fc(x)
model = SimpleCNN().eval()
dummy = torch.randn(1, 3, 8, 8)
# 导出:batch维度动态,图像尺寸固定
torch.onnx.export(model, dummy, "simple_cnn.onnx",
input_names=["input"], output_names=["output"],
dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}},
opset_version=17)
# 验证动态轴
onnx_model = onnx.load("simple_cnn.onnx")
for inp in onnx_model.graph.input:
for dim in inp.type.tensor_type.shape.dim:
print(f"输入维度: dim_value={dim.dim_value}, dim_param={dim.dim_param}")
输出结果中,batch维度的dim_param为"batch",而其他维度保持dim_value为固定值。这表示TensorRT在转换时,需要为batch维度设置最小/最优/最大三个值(如1/8/32),而图像尺寸则固定为8×8。
误区与复盘:ONNX模型优化的常见陷阱
- 误区一:忽略opset版本。不同opset版本支持的算子集合不同,TensorRT对高版本opset的支持可能滞后。建议使用opset 13~17之间的版本,兼顾算子覆盖率和兼容性。
- 误区二:将动态轴用于所有维度。例如将通道数也设为动态,这会导致TensorRT无法进行通道维度的内存布局优化,性能大幅下降。
- 误区三:不检查中间张量的精度。ONNX默认使用float32,但某些框架导出时可能混入float64算子,导致TensorRT拒绝转换。导出后应使用
onnx.checker.check_model和onnx.shape_inference.infer_shapes进行完整性验证。 - 误区四:忽视initializer的存储布局。ONNX中权重默认按行主序(row-major)存储,而TensorRT内部可能使用列主序或特定向量化布局。转换器会自动处理重排,但若手动修改ONNX权重,则需格外小心。
复盘总结:一个健壮的ONNX导出流程应包含以下步骤:① 使用torch.onnx.export并显式指定opset;② 用onnx.checker验证模型合法性;③ 用onnx.shape_inference推断中间张量形状;④ 用netron可视化检查数据流;⑤ 在TensorRT转换前,先用onnxruntime跑一遍推理,确认输出与PyTorch一致。这五步能过滤掉绝大多数导出问题。
小结与自查清单
本章我们梳理了ONNX格式的四个核心概念:ProtoBuf序列化结构、图与节点的层次关系、张量名称驱动的数据流以及动态轴的声明与传播规则。在进入TensorRT优化之前,请确保你能够回答以下问题:
- ONNX文件中的
initializer和node.input有什么区别?权重存储在哪一层? - 如果两个节点都引用了名为
"res2_out"的张量,这代表什么含义? - 动态轴在ONNX图中是如何标记的?TensorRT的Optimization Profile需要哪些参数?
- 为什么说“动态轴越多,TensorRT构建越慢”?
掌握这些基础后,下一阶段我们将聚焦于TensorRT的层融合策略与精度校准方法,届时你会更深刻地理解ONNX计算图结构如何直接影响底层优化决策。