ONNX基础回顾:ONNX格式、计算图、节点与张量、静态形状与动态形状的区别
在深入处理动态形状之前,必须先厘清ONNX的几个核心概念。很多动态形状相关的报错,根源并不在于“动态”本身,而在于对ONNX格式的底层设计存在误解。本节将系统梳理ONNX的格式构成、计算图结构、节点与张量的关系,并重点辨析静态形状与动态形状的本质差异。
2.1 ONNX格式:不只是“一个文件”
ONNX(Open Neural Network Exchange)是一种开放式的模型交换格式,它定义了一套统一的计算图表示方法,使得模型可以在PyTorch、TensorFlow、MXNet等不同框架之间迁移。一个完整的ONNX模型文件(通常以.onnx为后缀)本质上是一个Protobuf(Protocol Buffers)序列化文件,内部包含以下关键信息:
- 模型元信息(ModelProto):包括ir_version(IR版本)、opset_import(算子集版本)、producer_name(导出框架名称)等。这些字段决定了后续算子语义的解释方式。
- 计算图(GraphProto):模型的核心逻辑载体,包含所有节点、初始器、输入输出张量描述。
- 初始器(Initializer):存放模型的权重参数(如卷积核、偏置项),它们以TensorProto形式内嵌在模型中,不随输入变化。
- 输入/输出描述(ValueInfoProto):声明模型对外接口的张量名称、数据类型和形状信息。
理解这一点很重要:ONNX文件并非像图片或文本那样直接“打开”就能看到内容,它需要借助解析工具(如onnxPython库)来读取和修改。动态形状问题往往就出在“输入/输出描述”与“实际推理时的张量形状”不一致上。
2.2 计算图:有向无环图(DAG)
ONNX的计算图是一个有向无环图(Directed Acyclic Graph, DAG)。图中的每个节点(Node)代表一个算子操作,边(Edge)代表张量(Tensor)在算子之间的流动。数据从输入张量开始,沿着图的拓扑方向依次经过各节点,最终产生输出张量。
计算图有三个核心要素:
- 节点(Node):每个节点包含
op_type(算子类型,如Conv、Relu、MatMul)、input(输入张量名列表)、output(输出张量名列表)以及attribute(算子属性,如卷积的kernel_shape、strides)。 - 张量(Tensor):在图中流动的数据载体,具有名称、数据类型(如float32、int64)和形状(shape)。张量可以是中间结果,也可以是模型的输入或输出。
- 拓扑顺序:图必须是无环的,且推理引擎按照拓扑序执行节点。这意味着形状推断(Shape Inference)也是沿着这个顺序进行的——如果某个节点的输入形状未知,后续所有依赖它的节点形状都无法确定。
下面这张SVG图展示了ONNX计算图的基本结构,以及静态形状与动态形状在“形状传播”上的关键差异:
2.3 节点与张量:形状信息的载体
在ONNX中,张量(Tensor)是数据的基本单位,而节点(Node)是操作的基本单位。每个节点的输入输出都通过张量名(tensor name)进行关联。张量的形状信息存储在ValueInfoProto中,包含dim_value(固定维度值)和dim_param(动态维度参数名)两种形式。
例如,一个动态批大小的输入可以表示为:
input_tensor:
name: "input"
type:
tensor_type:
elem_type: FLOAT
shape:
dim:
- dim_param: "batch_size" # 动态维度,用参数名表示
- dim_value: 3 # 固定维度,通道数
- dim_value: 224 # 固定高度
- dim_value: 224 # 固定宽度
当dim_param出现时,该维度即为动态维度。推理引擎在加载模型时,如果遇到dim_param,会将其标记为“待定”,直到实际输入数据到达时才绑定具体数值。
2.4 静态形状与动态形状的本质区别
静态形状与动态形状并非“好”与“坏”的二元对立,而是灵活性与性能之间的权衡。下表从多个维度对比了二者的差异:
| 对比维度 | 静态形状 | 动态形状 |
|---|---|---|
| 形状表示 | 所有维度均为dim_value,完全确定 |
至少一个维度为dim_param或None |
| 形状推断时机 | 模型导出/加载时即可完成 | 需在推理时根据实际输入动态推断 |
| 内存分配 | 可提前预分配,零运行时开销 | 需运行时计算并分配,可能引发碎片 |
| 算子优化 | 可使用融合、常量折叠等静态优化 | 部分优化无法应用,需保留通用路径 |
| 适用场景 | 固定分辨率、固定批大小的生产环境 | 多分辨率输入、可变批大小、视频流处理 |
| 导出难度 | 低,默认导出即为静态 | 高,需显式设置动态轴并处理算子兼容性 |
2.5 常见误区与复盘
误区一:动态形状 = 使用None就是动态
在PyTorch导出时,即使输入张量包含None维度,如果后续算子(如Reshape、Flatten)将形状“固化”了,那么导出的ONNX模型可能仍然是静态的。真正的动态形状要求从输入到输出的整个传播链中,所有相关算子的形状推断都支持动态维度。
误区二:动态形状一定能提升灵活性
某些算子(如Resize、NonMaxSuppression)在动态形状下可能触发不同的实现分支,导致推理速度显著下降。此外,动态形状会阻止部分图优化(如算子融合),实际吞吐量可能低于静态形状模型。
复盘建议:在导出模型前,先明确部署场景的输入变化范围。如果输入尺寸变化有限(如仅批大小变化),建议只将批大小维度设为动态,其余维度保持静态,以平衡灵活性与性能。
2.6 本节小结
ONNX模型的核心是计算图,节点定义操作,张量承载数据,而形状信息则决定了推理引擎如何分配资源和执行优化。静态形状在编译期即可完成所有形状推导,适合固定场景的高性能部署;动态形状则通过dim_param或None标记未知维度,将形状推断推迟到运行时,以换取输入灵活性。理解这两种模式的本质区别,是后续处理动态形状导出报错、算子兼容性问题的前提。