2、ONNX与ONNX Runtime基础:ONNX模型格式、计算图概念、ONNX Runtime架构与执行模式
在深入探讨图优化技术之前,必须先厘清ONNX与ONNX Runtime的基本概念。ONNX(Open Neural Network Exchange)是一个开放的模型表示标准,它定义了跨框架的模型交换格式;而ONNX Runtime则是微软推出的高性能推理引擎,负责将ONNX模型高效地映射到各类硬件上执行。理解这两者的关系,是掌握后续图优化选项的前提——因为所有优化策略都作用于ONNX计算图,并由Runtime的架构组件来执行。
上图展示了从ONNX模型文件到Runtime执行的整体链路。左侧是模型定义层,中间是图优化层,右侧是执行层。ONNX Runtime的核心工作流程是:加载ONNX模型 → 解析为内存中的计算图 → 应用一系列图优化(即本课程后续章节的重点) → 将优化后的图分配给指定的Execution Provider(EP)执行。下面我们分层展开。
2.1 ONNX模型格式与计算图核心概念
核心原理:Protobuf序列化与图结构
ONNX模型文件本质是一个基于Protobuf(Protocol Buffers)序列化的二进制文件,其顶层结构为ModelProto。一个完整的ONNX模型包含以下关键部分:
- GraphProto:计算图主体,包含所有节点(Node)、初始值(Initializer)、输入输出描述(ValueInfo)。
- NodeProto:图中的每个算子节点,包含算子类型(op_type)、输入输出张量名称、属性(attribute)。
- Initializer:权重、偏置等常量张量,直接嵌入模型文件,不参与计算。
- Opset Version:算子集版本号,决定了算子语义和属性定义,不同版本间可能存在差异。
计算图是一种有向无环图(DAG),节点代表算子,边代表张量数据流。ONNX采用静态图设计,即图结构在推理前完全确定,这为编译期优化提供了基础。例如,PyTorch模型导出为ONNX时,会通过torch.onnx.export将动态的Python执行流程固化为静态图。
判断方法:如何快速解析ONNX模型结构
在实际工程中,我们经常需要检查一个ONNX模型的结构。推荐使用Python API进行快速解析:
import onnx
model = onnx.load("model.onnx")
graph = model.graph
print(f"节点数: {len(graph.node)}")
print(f"初始值(权重)数: {len(graph.initializer)}")
print(f"输入: {[(i.name, [d.dim_value for d in i.type.tensor_type.shape.dim]) for i in graph.input]}")
print(f"输出: {[(o.name, [d.dim_value for d in o.type.tensor_type.shape.dim]) for o in graph.output]}")
# 打印前5个节点
for node in graph.node[:5]:
print(f"Op: {node.op_type}, 输入: {list(node.input)}, 输出: {list(node.output)}")
通过上述代码,可以快速了解模型的算子类型分布、输入输出维度、权重数量等信息。这有助于判断模型是否适合进行某些图优化(例如,若模型中大量存在Conv+BN+ReLU组合,则算子融合优化收益显著)。
案例拆解:一个简单的ONNX图结构
假设我们有一个非常简单的线性模型:y = W*x + b,其ONNX图包含两个节点:MatMul和Add。初始值(Initializer)包含W和b。图结构如下:
| 节点序号 | 算子类型 | 输入 | 输出 | 属性 |
|---|---|---|---|---|
| 1 | MatMul | x, W | matmul_out | 无 |
| 2 | Add | matmul_out, b | y | 无 |
在这个图中,x是输入,W和b是Initializer。ONNX Runtime在加载此图后,会先进行常量折叠(如果W和b是常量,且MatMul的输入之一为常量,则可能被折叠),但通常权重在推理时是固定的,因此MatMul+Add可以融合为一个Gemm算子(如果满足特定条件),从而减少一次内存读写。
常见误区与总结
误区2:忽略Opset版本。不同Opset版本中,同一算子(如Split、Slice)的属性和行为可能不同,导致推理结果不一致。
误区3:混淆Initializer与输入。Initializer是模型内部的常量,不占用推理时的输入端口。
理解ONNX图结构是后续所有优化的基础。只有清楚每个节点的输入输出关系,才能安全地进行节点删除、融合等操作。
2.2 ONNX Runtime架构与执行模式
核心原理:会话(Session)与执行提供程序(EP)
ONNX Runtime采用分层架构,最上层是语言绑定(Python/C#/C++等),中间是核心引擎(Core),底层是各种Execution Provider。核心引擎负责模型加载、图优化、执行计划编排;EP则负责将算子实际运行在特定硬件上。常见的EP包括:
- CPUExecutionProvider:默认EP,使用MLAS(Microsoft Linear Algebra Subprograms)库,针对x86/ARM做了深度优化。
- CUDAExecutionProvider:NVIDIA GPU加速,支持Tensor Core。
- TensorrtExecutionProvider:集成NVIDIA TensorRT,实现层融合和精度校准。
- OpenVINOExecutionProvider:针对Intel CPU/GPU/VPU优化。
执行模式分为同步(Sync)和异步(Async)两种。同步模式下,Session.Run()会阻塞直到推理完成;异步模式则通过回调或Future获取结果,适合高吞吐场景。
操作流程:配置Session与选择EP
在Python中创建推理会话的典型流程如下:
import onnxruntime as ort
# 创建会话选项
so = ort.SessionOptions()
# 设置图优化级别(后续章节详解)
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
# 设置线程数
so.intra_op_num_threads = 4
# 指定EP及其优先级
providers = [
('CUDAExecutionProvider', {'device_id': 0}),
'CPUExecutionProvider' # 备选
]
session = ort.InferenceSession("model.onnx", sess_options=so, providers=providers)
# 执行推理
outputs = session.run(["output_name"], {"input_name": input_data})
注意:EP的注册顺序决定了优先级。如果CUDA EP无法处理某个算子(例如不支持的自定义算子),Runtime会自动回退到下一个EP(如CPU)。这种“部分回退”机制保证了模型的可用性,但会引入额外的数据拷贝开销(GPU→CPU→GPU),应尽量避免。
案例拆解:CPU与GPU执行性能对比
我们以ResNet-50模型为例,分别使用CPU EP和CUDA EP进行推理,观察性能差异。测试环境:Intel Xeon Gold 6248 CPU,NVIDIA V100 GPU,batch size=1。
| EP配置 | 平均延迟(ms) | 吞吐量(images/s) | 备注 |
|---|---|---|---|
| CPU (ORT_ENABLE_ALL) | 18.5 | 54 | 8线程 |
| CUDA (默认) | 4.2 | 238 | FP32 |
| CUDA + TensorRT | 2.8 | 357 | FP16 |
从结果可见,GPU EP显著降低延迟。但值得注意的是,当模型较小时(如MobileNet),CPU与GPU的差距会缩小,因为GPU的启动开销和数据传输开销占比增大。此时,使用CPU EP可能更优。
常见误区与总结
session.get_providers()确认实际启用的EP,避免因EP注册失败而静默回退到CPU。
理解ONNX Runtime的架构,有助于我们针对性地选择优化策略。例如,如果发现模型在GPU上存在大量小算子调用,则图优化中的算子融合(如Conv+BN融合)将大幅减少kernel launch次数,从而提升性能。
2.3 从模型到执行:图优化的作用位置
图优化发生在模型加载之后、执行之前。ONNX Runtime的优化器(Graph Optimizer)会遍历计算图,应用一系列规则。这些规则分为多个级别(Level),用户可以通过SessionOptions控制优化级别。理解优化器的工作位置,有助于我们判断何时需要手动干预(例如,关闭某些优化以排查问题)。
优化器的主要输入是原始GraphProto,输出是优化后的GraphProto。优化过程是确定性的,即相同的模型和优化级别必然产生相同的优化结果。这为调试提供了便利——如果推理结果异常,可以逐步降低优化级别,对比输出差异。