第4章:ONNX Runtime推理引擎——让模型真正跑起来

模型转换完了,量化也搞定了,接下来呢?得让它跑起来。而且是在工业现场,稳定、高效地跑。

ONNX Runtime(简称ORT)就是干这个的。它不挑硬件,CPU、GPU、甚至一些边缘设备都能跑。我最早接触它是在一个产线项目里,客户要求模型必须在Windows工控机上推理,还不能装CUDA。当时试了好几个框架,最后ORT成了唯一的选择。

核心观点: ONNX Runtime不是“又一个推理框架”,它是连接ONNX模型与生产环境的桥梁。你不需要改模型结构,只需要配置好Session,就能在目标设备上跑起来。

4.1 安装与配置:别踩这些坑

安装ORT其实很简单,但有几个细节我吃过亏,得跟你聊聊。

4.1.1 基础安装

用pip装就行,但要注意版本匹配。我个人习惯先确认Python版本和ONNX版本,再装ORT。

# CPU版本(最常用)
pip install onnxruntime

# GPU版本(需要CUDA和cuDNN)
pip install onnxruntime-gpu

# 指定版本(推荐)
pip install onnxruntime==1.16.3
注意: GPU版本对CUDA版本有严格限制。我曾经在一个项目里装了onnxruntime-gpu 1.15.0,结果CUDA是11.7,死活跑不起来。查了半天文档才发现它只支持CUDA 11.6。所以,装之前一定去官方文档看兼容性矩阵。

4.1.2 验证安装

装完别急着跑模型,先验证一下环境。

import onnxruntime as ort

print(ort.__version__)          # 版本号
print(ort.get_device())         # 当前设备:CPU或GPU
print(ort.get_available_providers())  # 可用的执行提供程序

输出示例:

1.16.3
CPU
['CPUExecutionProvider']

如果你装了GPU版本,应该能看到类似 ['CUDAExecutionProvider', 'CPUExecutionProvider'] 这样的输出。如果只有CPU,说明GPU没配好。

4.2 使用Python API进行推理:三步走

ORT的推理流程很固定,说白了就三步:创建Session、准备输入、运行推理。我把它总结成一张图,你一看就明白。

ONNX Runtime推理流程 加载ONNX模型 ort.InferenceSession 配置Session providers / options 准备输入数据 numpy array / ortvalue 运行推理 session.run(output_names, input_feed) 获取推理结果 numpy array 💡 核心:Session一旦创建,输入输出的名称和形状就固定了。不要每次推理都重新创建Session。

4.2.1 创建Session

这是最基础的一步。你只需要把ONNX模型路径传进去就行。

import onnxruntime as ort
import numpy as np

# 创建Session(CPU)
session = ort.InferenceSession("yolov8n.onnx")

# 创建Session(GPU,如果可用)
session = ort.InferenceSession(
    "yolov8n.onnx",
    providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
)
我的习惯: 我会把providers写成一个列表,优先用GPU,不行就回退到CPU。这样在开发机上用GPU,部署到没有GPU的工控机上也能自动降级,不用改代码。

4.2.2 准备输入数据

YOLO模型的输入通常是图像,需要做预处理。ORT要求输入是numpy数组,而且数据类型、形状必须和模型要求的一致。

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

print(f"输入名称: {input_name}")
print(f"输入形状: {input_shape}")
print(f"输入类型: {input_type}")

# 假设你有一张预处理好的图像(1, 3, 640, 640)
input_data = np.random.randn(1, 3, 640, 640).astype(np.float32)

这里有个坑:ORT对输入数据的连续性有要求。如果你用OpenCV读图然后做transpose,得到的数组可能不是连续的。我遇到过推理结果全错的情况,排查了半天才发现是数据不连续导致的。

# 确保数据连续
input_data = np.ascontiguousarray(input_data)

4.2.3 运行推理

这一步最简单,但也是最容易出问题的地方。

# 获取输出名称
output_name = session.get_outputs()[0].name

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

# outputs是一个列表,每个元素对应一个输出
result = outputs[0]
print(f"输出形状: {result.shape}")
注意: session.run的第一个参数是输出名称列表。如果你传空列表 [],ORT会返回所有输出。但我不建议这么做——在工业场景里,明确指定输出名称可以减少不必要的计算,提升性能。

4.3 性能基准测试:用数据说话

模型跑起来了,但跑得快不快?稳不稳定?不能靠感觉,得测。

我一般会做三件事:测延迟、测吞吐、测稳定性。下面是我在一个项目里用的测试脚本框架。

4.3.1 延迟测试

import time
import numpy as np

def benchmark_latency(session, input_data, num_runs=100, warmup=10):
    # 预热
    for _ in range(warmup):
        session.run([], {input_name: input_data})
    
    # 正式测试
    latencies = []
    for _ in range(num_runs):
        start = time.perf_counter()
        session.run([], {input_name: input_data})
        end = time.perf_counter()
        latencies.append((end - start) * 1000)  # 转毫秒
    
    latencies = np.array(latencies)
    return {
        "mean": np.mean(latencies),
        "std": np.std(latencies),
        "min": np.min(latencies),
        "max": np.max(latencies),
        "p50": np.percentile(latencies, 50),
        "p95": np.percentile(latencies, 95),
        "p99": np.percentile(latencies, 99)
    }

# 测试
input_data = np.random.randn(1, 3, 640, 640).astype(np.float32)
results = benchmark_latency(session, input_data)
print(results)

4.3.2 结果解读

这是我之前在一条产线上测到的数据,用的是YOLOv8n模型,640x640输入,CPU推理。

指标 数值(ms) 说明
平均延迟 12.3 单次推理的平均耗时
P50 11.8 一半的推理在11.8ms内完成
P95 15.2 95%的推理在15.2ms内完成
P99 18.7 99%的推理在18.7ms内完成
最大值 22.1 最慢的一次推理
关键点: 别只看平均值。在工业场景里,P99和最大值更重要。如果P99太高,说明系统偶尔会卡顿,这对实时检测来说是致命的。我曾经在一个项目中,平均值只有8ms,但P99到了35ms,结果产线每几分钟就丢一帧检测。后来发现是内存碎片导致的,调整了内存池才解决。

4.3.3 吞吐测试

延迟测的是单次推理,吞吐测的是单位时间能处理多少张图。对于视频流场景,这个指标更关键。

def benchmark_throughput(session, input_data, duration=10):
    start = time.perf_counter()
    count = 0
    while time.perf_counter() - start < duration:
        session.run([], {input_name: input_data})
        count += 1
    elapsed = time.perf_counter() - start
    return {
        "fps": count / elapsed,
        "total_frames": count,
        "elapsed": elapsed
    }

# 测试
throughput = benchmark_throughput(session, input_data)
print(f"吞吐量: {throughput['fps']:.1f} FPS")

4.4 常见问题与避坑指南

最后,分享几个我在项目中踩过的坑,希望能帮你省点时间。

  • Session创建慢: 第一次创建Session可能会花几秒钟,因为ORT要解析模型图并做优化。别在推理循环里反复创建Session,创建一次,复用到底。
  • 内存泄漏: 如果你在循环里不断创建Session而不释放,内存会一直涨。我见过一个项目跑了三天,内存从200MB涨到了8GB。解决办法:用 del session 显式释放,或者用上下文管理器。
  • 多线程推理: ORT的Session是线程安全的,但多个线程共享一个Session时,性能反而可能下降。我建议每个线程创建自己的Session,或者用 InterOpNumThreads 和 IntraOpNumThreads 控制并行度。
  • 输入数据格式: YOLO模型通常要求输入是 NCHW 格式,而OpenCV读出来的是 HWC。别忘了做 transpose 和 normalize。
我的经验: 在正式部署前,先用 onnxruntime.quantization 做一次静态量化,把FP32转成INT8。延迟能降一半,精度损失通常不到1%。我在一个缺陷检测项目里试过,从12ms降到了5ms,产线速度直接翻倍。