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)
还有一个容易被忽略的点——算子域(domain)。有些自定义算子会放在 ai.onnx.contrib 或 org.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}")
嗯,到这里,模型加载、保存、检查、版本兼容性这几个核心操作就讲完了。说白了,这些步骤就像你写代码前的「环境检查」——花几分钟做好,后面能省下几小时的排查时间。下一章咱们聊聊模型的可视化,看看怎么把计算图变成一张看得懂的图。