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计算图的基本结构,以及静态形状与动态形状在“形状传播”上的关键差异:

静态形状(Static Shape) input shape: [1,3,224,224] Conv kernel=3x3 Relu [1,3,224,224] [1,64,112,112] [1,64,112,112] ✅ 所有中间张量的形状在导出时已完全确定 ✅ 推理引擎可提前分配内存,无需动态判断 动态形状(Dynamic Shape) input shape: [None,3,None,None] Resize scale=2.0 out ⚠️ 输入形状含None(未知维度) ⚠️ 中间张量形状需在推理时动态推断 形状传播(Shape Propagation)对比 静态: [1,3,224,224] [1,64,112,112] [1,64,112,112] → 编译期即可确定所有形状 动态: [None,3,None,None] [None,64,?,?] [None,64,?,?] → 运行时才能确定,需动态推断 关键区别:静态形状在<tspan font-weight="bold" fill="#c0392b">图优化阶段</tspan>即可完成形状推导; 动态形状则需在<tspan font-weight="bold" fill="#c0392b">推理引擎运行时</tspan>根据实际输入张量重新推导。

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标记未知维度,将形状推断推迟到运行时,以换取输入灵活性。理解这两种模式的本质区别,是后续处理动态形状导出报错、算子兼容性问题的前提。