4. 模型加载与保存:onnx.load、onnx.save 与版本兼容性

模型训练完了,怎么把它弄到生产环境去?

这一步,说白了就是模型的「进出口」管理。我见过不少团队,模型精度再高,最后卡在加载和保存的坑里出不来。今天咱们就把这块彻底捋清楚。

4.1 用 onnx.load 加载模型

加载一个 ONNX 模型,代码其实就一行:

import onnx

model = onnx.load("model.onnx")
print(model)

嗯,就这么简单。但这里有个细节——onnx.load 返回的是一个 ModelProto 对象。它本质上是一个 Protocol Buffers 的实例,里面包含了模型的计算图、权重、元数据等等。

我个人习惯在加载后立刻打印一下模型的基本信息,确认没加载错:

print(f"IR 版本: {model.ir_version}")
print(f"算子集版本: {model.opset_import[0].version}")
print(f"生产者: {model.producer_name}")

我在项目中遇到过一个问题:加载一个 2GB 的大模型时,内存直接爆了。后来发现是 onnx.load 默认会把所有权重加载到内存里。如果你的模型特别大,可以考虑用 onnx.load_model 配合 load_external_data=False 来延迟加载外部数据。

小技巧: 如果你只想检查模型结构而不关心权重,可以用 onnx.shape_inference.infer_shapes(model) 来快速获取张量形状信息,不用完整加载。

4.2 用 onnx.save 保存模型

保存模型同样简单:

onnx.save(model, "optimized_model.onnx")

但这里有个容易踩的坑——外部数据。当模型权重超过 2GB 时,ONNX 默认会把权重存成单独的文件(比如 model.onnx.data)。这时候你如果只拷贝了 .onnx 文件,到别的地方一加载就报错。

我曾经在部署时吃过这个亏。模型在本地跑得好好的,传到服务器上就报 ModelProto 解析错误。排查了半天,发现是外部数据文件没一起传过去。

解决办法有两个:

  • 方法一: 保存时指定 save_external_data=False,强制把所有权重内嵌到 .onnx 文件中。但要注意,文件大小会超过 2GB,有些平台不支持。
  • 方法二: 把外部数据文件和模型文件放在同一目录下,保持文件名一致。
# 方法一:内嵌权重
onnx.save(model, "small_model.onnx", save_external_data=False)

# 方法二:保持外部数据
onnx.save(model, "big_model.onnx")  # 会自动生成 big_model.onnx.data
注意: 如果你用 onnx.save 保存的是经过剪枝或量化后的模型,记得先调用 onnx.checker.check_model(model) 验证一下。我遇到过剪枝后模型结构不完整,保存时没报错,但加载时直接崩溃的情况。

4.3 模型结构检查

模型加载或保存后,第一件事就是做结构检查。这就像你写完代码要编译一下,看看有没有语法错误。

ONNX 提供了内置的检查器:

try:
    onnx.checker.check_model(model)
    print("模型结构检查通过 ✅")
except onnx.checker.ValidationError as e:
    print(f"模型结构有问题: {e}")

这个检查器会验证:

  • 节点输入输出是否匹配
  • 张量形状是否一致
  • 算子是否在当前 opset 中支持
  • 图的拓扑结构是否合法

你想想看,如果模型结构有问题,后面做推理优化全是白费功夫。我一般会在每个处理步骤后都跑一次 check_model,比如剪枝后、量化后、融合算子后。这样能第一时间发现问题。

另外,还有一个更细致的检查工具——onnx.inference_shapes。它能帮你推断出每个中间张量的形状:

from onnx import shape_inference

model_with_shapes = shape_inference.infer_shapes(model)
print(model_with_shapes.graph.value_info)  # 打印所有中间张量的形状信息

这个功能在调试模型时特别有用。比如你发现某个节点的输出形状不对,用这个就能快速定位。

4.4 模型版本兼容性

版本兼容性,说白了就是「你的模型是用哪个版本的 ONNX 导出的?目标环境支持吗?」

ONNX 的版本体系主要有三个维度:

版本维度 说明 常见问题
IR 版本 模型文件的格式版本 高版本 IR 在低版本 ONNX 中无法加载
Opset 版本 算子集的版本号 高版本算子可能在旧推理引擎中不支持
ONNX Runtime 版本 推理引擎的版本 不同版本对算子的支持程度不同

我遇到过最典型的问题:用 ONNX 1.12 导出的模型,部署到 ONNX Runtime 1.8 上,结果报 Unsupported operator。查了半天,发现是某个新算子(比如 LayerNormalization)在旧版本里还不支持。

怎么解决?

  • 方案一: 升级推理环境的 ONNX Runtime 版本。这是最直接的办法。
  • 方案二: 导出模型时指定较低的 opset 版本。比如用 opset_version=11 导出,兼容性会好很多。
  • 方案三: 手动替换不兼容的算子。比如把 LayerNormalization 拆成基础数学算子。
# 查看当前模型的 opset 版本
print(model.opset_import[0].version)

# 导出时指定 opset 版本(以 PyTorch 为例)
import torch.onnx
torch.onnx.export(model, dummy_input, "model.onnx", opset_version=11)
核心建议: 如果你不确定目标环境的 ONNX Runtime 版本,导出时尽量用 opset 11 或 12。这两个版本兼容性最好,覆盖了绝大多数常用算子。我个人的经验是,opset 11 基本能覆盖 90% 以上的部署场景。

还有一个容易被忽略的点——算子域(domain)。有些自定义算子会放在 ai.onnx.contriborg.pytorch 等域下。这些算子在不同推理引擎中的支持情况差异很大。我建议尽量使用标准 ONNX 算子,避免引入自定义域。

最后,检查版本兼容性可以用这个命令:

import onnx
from onnx import version_converter

# 检查模型是否可以在目标 opset 下运行
try:
    converted_model = version_converter.convert_version(model, target_version=11)
    print("可以转换到 opset 11 ✅")
except Exception as e:
    print(f"转换失败: {e}")

嗯,到这里,模型加载、保存、检查、版本兼容性这几个核心操作就讲完了。说白了,这些步骤就像你写代码前的「环境检查」——花几分钟做好,后面能省下几小时的排查时间。下一章咱们聊聊模型的可视化,看看怎么把计算图变成一张看得懂的图。