4、推理引擎入门:ONNX Runtime安装与使用、CPU推理与GPU推理、性能基准测试
各位同学,欢迎来到第四章。
前几章我们聊了模型训练、导出,算是把模型「生」出来了。但模型生出来不是供着的,得让它跑起来,对吧?
这一章,我们就来聊聊怎么让模型真正「动」起来。说白了,就是选一个趁手的推理引擎。
4.1 为什么是ONNX Runtime?
市面上推理引擎不少,TensorRT、OpenVINO、TNN……我为什么先讲ONNX Runtime?
原因很简单:通用性。
ONNX(Open Neural Network Exchange)本身是一个中间格式,就像一个「通用语言」。你不管用什么框架训练(PyTorch、TensorFlow),都能转成ONNX。而ONNX Runtime就是专门跑这种格式的引擎。
我个人习惯是:先转ONNX,再用ORT跑。这样后续换硬件、换平台,成本最低。
4.2 安装ONNX Runtime
安装其实很简单。但要注意区分CPU版本和GPU版本。
4.2.1 CPU版本安装
如果你只是本地测试,或者服务器没有独立显卡,用CPU版本就够了。
pip install onnxruntime
嗯,就这一行。装完你可以验证一下:
import onnxruntime
print(onnxruntime.__version__)
print(onnxruntime.get_device()) # 输出应该是 'CPU'
4.2.2 GPU版本安装
要上GPU推理,得装带CUDA支持的版本。这里有个坑,我当年踩过。
pip install onnxruntime-gpu
注意:不是在同一个环境里装两个版本。CPU和GPU版本是互斥的。
4.3 CPU推理 vs GPU推理
装好了,我们来跑个实际例子。假设你有一个训练好的ResNet-50模型,已经转成了ONNX格式。
4.3.1 CPU推理代码
import onnxruntime as ort
import numpy as np
# 创建推理会话
session = ort.InferenceSession("resnet50.onnx")
# 构造输入数据(假设输入是224x224的图片)
input_name = session.get_inputs()[0].name
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 执行推理
outputs = session.run(None, {input_name: input_data})
print("推理完成,输出形状:", outputs[0].shape)
这段代码CPU和GPU都能跑。区别在哪?在于创建session时用的provider。
4.3.2 GPU推理代码
import onnxruntime as ort
# 显式指定使用CUDA
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
session = ort.InferenceSession("resnet50.onnx", providers=providers)
# 后面的代码和CPU版本一样
input_name = session.get_inputs()[0].name
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
outputs = session.run(None, {input_name: input_data})
看到没?核心区别就是providers参数。我建议你总是把CPUExecutionProvider放在最后,作为兜底。
4.4 性能基准测试
光说GPU比CPU快,那快多少?咱们得用数据说话。
4.4.1 写一个简单的测速脚本
import time
import onnxruntime as ort
import numpy as np
def benchmark(model_path, providers, num_runs=100):
session = ort.InferenceSession(model_path, providers=providers)
input_name = session.get_inputs()[0].name
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 预热(warm up)
for _ in range(10):
session.run(None, {input_name: input_data})
# 正式测试
start = time.time()
for _ in range(num_runs):
session.run(None, {input_name: input_data})
end = time.time()
avg_time = (end - start) / num_runs
fps = 1.0 / avg_time
return avg_time, fps
# CPU测试
cpu_time, cpu_fps = benchmark("resnet50.onnx", ['CPUExecutionProvider'])
print(f"CPU: 平均耗时 {cpu_time*1000:.2f} ms, FPS: {cpu_fps:.2f}")
# GPU测试
gpu_time, gpu_fps = benchmark("resnet50.onnx", ['CUDAExecutionProvider', 'CPUExecutionProvider'])
print(f"GPU: 平均耗时 {gpu_time*1000:.2f} ms, FPS: {gpu_fps:.2f}")
4.4.2 典型结果对比
我在一台普通工作站上跑过(CPU: i7-12700, GPU: RTX 3060),结果大概是这样:
| 推理设备 | 平均耗时(ms) | FPS | 加速比 |
|---|---|---|---|
| CPU(12核) | 45.2 | 22.1 | 1x |
| GPU(RTX 3060) | 3.8 | 263.2 | 约12x |
看到了吧?GPU推理快了整整一个数量级。这就是为什么生产环境里,能上GPU就上GPU。
4.5 避坑指南
最后,分享几个我实际踩过的坑:
- CUDA版本不匹配: ONNX Runtime的GPU版本对CUDA版本有严格要求。比如ort 1.15要求CUDA 11.8。装错了直接报错。我建议你装之前先查官方文档的兼容性矩阵。
- 显存泄漏: 如果你在循环里反复创建session,显存会一直涨。正确做法是只创建一次session,重复使用。
- 动态输入形状: 有些模型输入尺寸不固定,ORT默认不支持。需要在导出ONNX时设置
dynamic_axes参数。这个我们后面章节会细讲。
好了,这一章的内容就到这里。下一章我们会深入ONNX模型的导出细节,包括怎么处理动态输入、怎么优化算子。到时候见。