第4章:ONNX Runtime推理引擎——让模型真正跑起来
模型转换完了,量化也搞定了,接下来呢?得让它跑起来。而且是在工业现场,稳定、高效地跑。
ONNX Runtime(简称ORT)就是干这个的。它不挑硬件,CPU、GPU、甚至一些边缘设备都能跑。我最早接触它是在一个产线项目里,客户要求模型必须在Windows工控机上推理,还不能装CUDA。当时试了好几个框架,最后ORT成了唯一的选择。
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
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、准备输入、运行推理。我把它总结成一张图,你一看就明白。
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']
)
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}")
[],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 | 最慢的一次推理 |
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,产线速度直接翻倍。