4、ONNX模型调试与验证:ONNX Runtime推理、onnxruntime-gpu与onnxruntime-silicon、ONNX模型正确性验证

模型导出成 ONNX 之后,别急着往 TensorRT 里塞。我见过太多人,导出来就直接转,结果跑出来的结果全是错的,还以为是 TensorRT 的锅。

其实,ONNX 这一步是个很好的「中间检查点」。你得先确认 ONNX 本身是对的,再去谈优化。说白了,这一步就是给模型做个「体检」。

4.1 为什么要在 ONNX 阶段做验证?

你想想看,从 PyTorch 到 TensorRT,中间隔了好几层转换。如果 ONNX 这一步就有问题,后面排查起来会非常痛苦。

我个人习惯是,导出 ONNX 后,先用 ONNX Runtime 跑一遍推理,跟原始框架(比如 PyTorch)的输出做对比。误差在 1e-5 以内,我才敢往下走。

核心原则: 原始框架输出 → ONNX Runtime 输出 → TensorRT 输出,每一步都要做数值比对。跳过任何一步,都可能掉坑里。

4.2 ONNX Runtime 推理:CPU 下的快速验证

ONNX Runtime(简称 ORT)是微软开源的推理引擎。它最大的好处是:不需要 GPU,装个 pip 包就能跑。非常适合做快速验证。

4.2.1 安装与基础用法

pip install onnxruntime

装完之后,写个简单的推理脚本:

import onnxruntime as ort
import numpy as np

# 创建推理会话
session = ort.InferenceSession("model.onnx")

# 获取输入输出信息
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name

# 构造随机输入(注意 shape 和 dtype 要匹配)
dummy_input = np.random.randn(1, 3, 224, 224).astype(np.float32)

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

print(outputs[0].shape)

嗯,这里要注意:session.run 的第一个参数是输出名称列表,传 None 会返回所有输出。但我建议显式指定,避免不必要的计算。

4.2.2 数值比对脚本

我一般会写一个比对函数,专门用来验证 ONNX 和原始框架的输出:

def compare_outputs(torch_output, onnx_output, rtol=1e-5, atol=1e-5):
    """
    比对 PyTorch 和 ONNX Runtime 的输出
    """
    torch_np = torch_output.detach().cpu().numpy()
    onnx_np = onnx_output

    max_diff = np.max(np.abs(torch_np - onnx_np))
    mean_diff = np.mean(np.abs(torch_np - onnx_np))

    print(f"最大误差: {max_diff:.2e}")
    print(f"平均误差: {mean_diff:.2e}")

    if np.allclose(torch_np, onnx_np, rtol=rtol, atol=atol):
        print("✅ 验证通过!")
    else:
        print("❌ 验证失败!")

    return max_diff, mean_diff
我的经验: 对于分类模型,误差在 1e-5 以内基本没问题。但对于检测或分割模型,某些算子(比如 Resize)在不同框架下的实现可能有细微差异,误差放宽到 1e-4 也是可以接受的。

4.3 onnxruntime-gpu 与 onnxruntime-silicon

CPU 验证通过后,你可能想看看 ONNX 在 GPU 上的表现。这时候就需要 onnxruntime-gpu 了。

4.3.1 版本选择

包名 适用硬件 CUDA 依赖 典型场景
onnxruntime CPU 无 快速验证、CI 测试
onnxruntime-gpu NVIDIA GPU 需要 CUDA + cuDNN GPU 推理验证、性能摸底
onnxruntime-silicon Apple Silicon (M1/M2/M3) 无 Mac 本地验证

安装 GPU 版本时,版本号一定要跟你的 CUDA 版本匹配。我曾经因为装了不匹配的版本,折腾了一下午才发现是版本问题。

# CUDA 11.x 用户
pip install onnxruntime-gpu

# CUDA 12.x 用户
pip install onnxruntime-gpu==1.17.0  # 具体版本请查官方文档

4.3.2 GPU 推理代码

跟 CPU 版本几乎一样,只是创建 session 时指定 provider:

import onnxruntime as ort

# 创建 GPU 会话
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
session = ort.InferenceSession("model.onnx", providers=providers)

# 后面的推理代码跟 CPU 版本完全一致
# ...
注意: 如果你在 GPU 上跑 ONNX Runtime,发现结果跟 CPU 版本有微小差异(比如 1e-6 级别),这是正常的。浮点运算在不同硬件上的累加顺序不同,会导致结果不完全一致。但如果差异超过 1e-3,那就要警惕了,可能是算子实现有问题。

4.4 ONNX 模型正确性验证

验证不只是跑一次推理那么简单。我总结了一套「三步验证法」:

  1. 结构验证: 用 onnx.checker 检查模型结构是否合法。
  2. 数值验证: 用多组输入比对输出,覆盖不同数据分布。
  3. 动态形状验证: 如果模型支持动态 batch,要测试不同 batch size 下的正确性。

4.4.1 结构验证

import onnx

# 加载模型
model = onnx.load("model.onnx")

# 检查模型结构
onnx.checker.check_model(model)

print("✅ 模型结构检查通过")

这一步能发现很多低级错误,比如某个节点的输入输出名称不匹配、shape 信息缺失等。

4.4.2 多组输入验证

只用一组随机输入验证是不够的。我建议至少准备 3-5 组不同的输入:

  • 全零输入: 检查模型在极端情况下的行为。
  • 全一输入: 同上。
  • 随机输入: 模拟正常数据分布。
  • 真实数据: 从训练集里拿几张图,用预处理后的 tensor 做输入。
test_inputs = [
    np.zeros((1, 3, 224, 224), dtype=np.float32),
    np.ones((1, 3, 224, 224), dtype=np.float32),
    np.random.randn(1, 3, 224, 224).astype(np.float32),
    # 真实预处理后的数据
]

for i, test_input in enumerate(test_inputs):
    onnx_output = session.run([output_name], {input_name: test_input})
    # 跟原始框架输出做比对
    # ...

4.4.3 动态形状验证

如果你的 ONNX 模型支持动态 batch,一定要测试不同 batch size:

batch_sizes = [1, 2, 4, 8]

for bs in batch_sizes:
    dummy_input = np.random.randn(bs, 3, 224, 224).astype(np.float32)
    outputs = session.run([output_name], {input_name: dummy_input})
    assert outputs[0].shape[0] == bs, f"Batch size {bs} 验证失败"
    print(f"Batch size {bs}: ✅")
避坑指南: 我曾经遇到过一个模型,batch size 为 1 时完全正常,但 batch size 为 2 时结果就错了。查了半天,发现是某个自定义算子没有正确处理 batch 维度。所以,动态形状验证真的不能省。

4.5 知识结构图:ONNX 验证流程

下面这张图总结了我平时做 ONNX 验证的完整流程:

ONNX 模型验证流程 ONNX 模型验证流程 ① 导出 ONNX 模型 ② onnx.checker 结构验证 ③ ONNX Runtime CPU 推理 ④ 与原始框架数值比对 通过? ✅ 进入 GPU 验证 ❌ 排查导出问题 💡 验证要点 • 至少准备 3-5 组不同输入(全零、全一、随机、真实数据) • 误差阈值:分类模型 1e-5,检测/分割模型 1e-4 • 动态形状模型必须测试不同 batch size

4.6 常见问题与排查思路

验证过程中,你可能会遇到各种问题。我整理了几个高频场景:

现象 可能原因 排查方法
ONNX Runtime 报错:
Unsupported Operator
模型中有 ONNX 不支持的算子 用 onnx.helper.printable_graph() 查看模型结构,定位不支持算子
数值误差很大(>1e-3) 算子实现差异、精度问题 逐层比对输出,定位误差来源
动态 batch 时报错 模型未正确设置动态维度 导出时设置 dynamic_axes 参数
GPU 推理比 CPU 还慢 小模型、频繁 CPU-GPU 拷贝 检查 IO Binding,减少数据传输
重要提醒: 如果你在验证过程中发现 ONNX Runtime 的输出跟原始框架不一致,不要急着怀疑 ONNX Runtime 有 bug。先检查导出时有没有设置 opset_version,不同版本的 opset 对某些算子的实现可能有差异。我一般用 opset 17 或 18,兼容性比较好。

好了,ONNX 验证这一步走完,你的模型就已经通过了「体检」。接下来,就可以放心地把它交给 TensorRT 去做优化了。