4. ONNX模型加载与推理:使用ONNX Runtime加载模型、创建Session、执行推理

好,咱们进入正题。前面几章我们把ONNX模型的结构、转换、可视化都聊了一遍,现在终于到了最核心的一步——让模型真正跑起来

说白了,加载模型、创建Session、执行推理,这三步就是ONNX Runtime的“三板斧”。我刚开始接触ONNX Runtime时,觉得这玩意儿不就是个推理引擎嘛,能有多复杂?结果在实际项目里踩了不少坑。嗯,今天我把这些经验都揉碎了讲给你听。

4.1 加载模型:从文件到内存

加载模型,就是把.onnx文件读进内存。这一步看似简单,但有个细节很多人会忽略——模型文件的完整性校验

我个人习惯在加载前先检查文件大小,至少确保文件不是0字节。你想想看,如果模型文件在传输过程中损坏了,你后面花再多时间调优都是白搭。

# 加载模型前的安全检查
import os
import onnxruntime as ort

model_path = "resnet50.onnx"
if not os.path.exists(model_path):
    raise FileNotFoundError(f"模型文件 {model_path} 不存在")
if os.path.getsize(model_path) == 0:
    raise ValueError("模型文件为空,请检查文件完整性")

# 正式加载
session = ort.InferenceSession(model_path)
print("模型加载成功!")
小技巧:在ARM平台上,我建议用os.path.getsize()检查文件大小。曾经有个项目,模型文件从服务器下载时网络中断,生成了一个只有几百字节的残损文件,排查了半天才发现是文件没下完整。

4.2 创建Session:配置推理环境

Session是ONNX Runtime的核心对象。它负责管理模型的计算图、内存分配、执行引擎等。创建Session时,你可以指定运行在CPU还是GPU上,以及各种优化选项。

在ARM平台上,我们通常用CPU推理。但别小看CPU推理,配置得当的话,性能差距能到30%以上。

# 创建Session的基本方式
session = ort.InferenceSession("resnet50.onnx")

# 带配置的Session创建
import onnxruntime as ort

# 设置Session选项
sess_options = ort.SessionOptions()

# 设置线程数(ARM平台建议设为物理核心数)
sess_options.intra_op_num_threads = 4
sess_options.inter_op_num_threads = 2

# 启用内存优化
sess_options.enable_mem_pattern = True
sess_options.enable_cpu_mem_arena = True

# 设置日志级别(生产环境建议设为WARNING)
sess_options.log_severity_level = 2  # 0:VERBOSE, 1:INFO, 2:WARNING, 3:ERROR

# 创建Session
session = ort.InferenceSession("resnet50.onnx", sess_options)
关键参数说明:
  • intra_op_num_threads:单个算子内部的并行线程数。我建议设为CPU物理核心数,比如树莓派4B是4核,就设4。
  • inter_op_num_threads:算子之间的并行线程数。通常设2就够了,设太多反而会因为线程切换导致性能下降。
  • enable_mem_pattern:内存预分配模式。开启后能减少推理时的内存分配开销,ARM平台建议开启。

4.3 获取模型输入输出信息

创建Session后,第一件事不是急着推理,而是搞清楚模型的输入输出长什么样。这一步我吃过亏——有一次模型输入是NCHW格式,我传了NHWC的数据,结果推理结果全错,排查了整整一天。

# 获取模型输入输出信息
input_name = session.get_inputs()[0].name
input_shape = session.get_inputs()[0].shape
input_type = session.get_inputs()[0].type

output_name = session.get_outputs()[0].name
output_shape = session.get_outputs()[0].shape

print(f"输入名称: {input_name}")
print(f"输入形状: {input_shape}")
print(f"输入类型: {input_type}")
print(f"输出名称: {output_name}")
print(f"输出形状: {output_shape}")
属性 说明 常见值
name 输入/输出张量的名称 "input", "output", "data"
shape 张量的形状,可能包含动态维度 [1, 3, 224, 224]
type 张量的数据类型 "tensor(float)", "tensor(int64)"
注意:如果shape中包含-1,表示该维度是动态的。比如[-1, 3, 224, 224]表示batch size可以变化。在ARM平台上,动态维度会带来额外的内存分配开销,我建议尽量固定batch size。

4.4 执行推理:让模型跑起来

终于到了最激动人心的环节——执行推理。在ONNX Runtime中,推理就是调用run()方法,传入输出名称和输入数据。

# 准备输入数据(假设是图像分类任务)
import numpy as np

# 模拟一张224x224的RGB图像
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)

# 执行推理
outputs = session.run([output_name], {input_name: input_data})

# 获取结果
result = outputs[0]
print(f"推理结果形状: {result.shape}")
print(f"推理结果: {result}")

这里有个细节:run()的第一个参数是输出名称列表,如果你传None,会返回所有输出。我个人习惯明确指定输出名称,这样代码更清晰,也避免不必要的计算。

4.5 完整推理流程示例

把上面的步骤串起来,就是一个完整的推理流程。我在项目中通常封装成一个函数,方便复用。

import numpy as np
import onnxruntime as ort

def onnx_inference(model_path, input_data):
    """
    ONNX模型推理函数
    :param model_path: 模型文件路径
    :param input_data: 输入数据,numpy数组
    :return: 推理结果
    """
    # 1. 创建Session
    sess_options = ort.SessionOptions()
    sess_options.intra_op_num_threads = 4
    sess_options.log_severity_level = 2
    
    session = ort.InferenceSession(model_path, sess_options)
    
    # 2. 获取输入输出信息
    input_name = session.get_inputs()[0].name
    output_name = session.get_outputs()[0].name
    
    # 3. 执行推理
    outputs = session.run([output_name], {input_name: input_data})
    
    return outputs[0]

# 使用示例
if __name__ == "__main__":
    # 加载模型
    model_path = "resnet50.onnx"
    
    # 准备输入数据
    input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
    
    # 执行推理
    result = onnx_inference(model_path, input_data)
    print("推理完成,结果形状:", result.shape)
避坑指南:我曾经在树莓派上跑模型,发现推理速度特别慢。排查后发现是input_data的数据类型是float64,而模型期望的是float32。ONNX Runtime不会自动做类型转换,类型不匹配时会报错或者默默做隐式转换,导致性能骤降。所以,务必确保输入数据的dtype和模型要求一致

4.6 性能调优小贴士

在ARM平台上,推理性能是重中之重。这里分享几个我实测有效的优化点:

  • 预热推理:第一次推理通常较慢,因为要分配内存、加载缓存。建议先跑一次“假推理”做预热。
  • 批量推理:如果有多张图片要处理,尽量合并成一个batch,能显著提升吞吐量。
  • 内存复用:如果反复推理相同形状的数据,可以复用输入输出缓冲区,减少内存分配开销。
# 预热推理示例
def warm_up(session, input_name, output_name, input_shape):
    """预热推理,减少首次推理的延迟"""
    dummy_input = np.random.randn(*input_shape).astype(np.float32)
    for _ in range(3):
        session.run([output_name], {input_name: dummy_input})
    print("预热完成")

# 使用
warm_up(session, input_name, output_name, [1, 3, 224, 224])

好了,这一章的内容就到这里。加载模型、创建Session、执行推理,这三步看起来简单,但每个环节都有值得深挖的细节。下一章我们会聊如何对ONNX模型进行优化,让它在ARM平台上跑得更快。到时候见!