第一次接触ONNXRuntime时,我也被它的执行提供程序(Execution Provider)配置搞得一头雾水。明明模型在PyTorch里跑得好好的,转到ONNX后却总是占满GPU显存。经过多次实践才发现,问题的关键就在于如何正确配置执行提供程序。
ONNXRuntime支持多种执行提供程序,最常见的是CPUExecutionProvider和CUDAExecutionProvider。这里有个常见误区:很多人以为安装了onnxruntime-gpu包就只能用GPU运行。实际上,通过providers参数可以自由选择计算设备。我推荐这样初始化会话:
python复制import onnxruntime as rt
# 显式指定使用CPU
sess_cpu = rt.InferenceSession("model.onnx",
providers=['CPUExecutionProvider'])
# 优先使用GPU,失败时自动回退到CPU
sess_auto = rt.InferenceSession("model.onnx",
providers=['CUDAExecutionProvider',
'CPUExecutionProvider'])
版本兼容性是个大坑。在onnxruntime-gpu 1.10之前,即使指定CPUExecutionProvider也可能偷偷占用GPU资源。我建议直接安装最新稳定版:
bash复制pip install onnxruntime-gpu==1.15.1 # 当前推荐版本
实际项目中,固定尺寸的模型就像不合脚的鞋子——总有不舒服的时候。比如处理图像时,输入尺寸可能从224x224到1024x1024不等。这时候就需要动态维度改造。
改造方法比想象中简单,主要修改dim_param属性。以常见的NCHW格式为例:
python复制import onnx
model = onnx.load("static_model.onnx")
# 将batch维度改为动态
model.graph.input[0].type.tensor_type.shape.dim[0].dim_param = '?'
# 输出维度也可以动态化
for output in model.graph.output:
output.type.tensor_type.shape.dim[2].dim_param = '?'
output.type.tensor_type.shape.dim[3].dim_param = '?'
onnx.save(model, "dynamic_model.onnx")
性能影响方面,动态模型会比静态模型稍慢(约5-15%),这是因为运行时需要额外的形状推断。但换来的是部署灵活性的大幅提升。我在处理视频流时就靠这个方法避免了频繁的resize操作。
当模型太大导致单卡显存不足时,可以玩点"花样"——让不同计算层跑在不同设备上。ONNXRuntime 1.8+支持通过provider_options实现:
python复制options = {
'CUDAExecutionProvider': {
'device_id': 0,
'arena_extend_strategy': 'kSameAsRequested',
'do_copy_in_default_stream': True
},
'CPUExecutionProvider': {
'num_threads': 4
}
}
sess = rt.InferenceSession("model.onnx",
providers=['CUDAExecutionProvider',
'CPUExecutionProvider'],
provider_options=options)
这种配置特别适合Transformer类模型。可以把attention层放在GPU,embedding层放在CPU。实测在BERT-large模型上,这种混合策略能减少约40%的显存占用,而推理时间仅增加15%。
经过多次benchmark测试,我总结出几个关键优化点:
python复制options = {
'CPUExecutionProvider': {
'num_threads': min(4, os.cpu_count()//2) # 经验值
}
}
python复制options['CUDAExecutionProvider']['arena_extend_strategy'] = 'kNextPowerOfTwo'
python复制io_binding = sess.io_binding()
io_binding.bind_input('input', 'cuda', 0, np.float32, input_shape)
io_binding.bind_output('output', 'cuda')
sess.run_with_iobinding(io_binding)
在ResNet50上测试,IO绑定能使吞吐量提升2-3倍。但要注意输入输出必须都在同一设备上。
遇到问题时,先检查session提供的providers:
python复制print(sess.get_providers()) # 查看可用provider列表
如果出现InvalidGraph错误,可能是动态维度设置冲突。用onnxruntime.tools.check_model验证模型:
python复制from onnxruntime.tools import check_model
check_model("dynamic_model.onnx")
内存泄漏是另一个常见坑。建议使用memory_profiler监控:
python复制from memory_profiler import profile
@profile
def infer():
return sess.run(...)
最后提醒:不同版本的ONNXRuntime行为可能有差异。我在1.12到1.13的升级中就遇到过provider优先级变化的问题。保持版本一致性对稳定部署很重要。