第81章:ONNX Runtime:集成ONNX Runtime推理引擎
说实话,做AI推理部署这件事,我踩过不少坑。早些年用TensorRT,后来试过OpenVINO,再后来接触了ONNX Runtime。嗯,这玩意儿给我的第一印象就是——真省心。你训练好的模型,不管是用PyTorch还是TensorFlow,转成ONNX格式后,ONNX Runtime基本都能一把梭。今天我们就聊聊,怎么在CMake项目里把ONNX Runtime集成进来。
为什么是ONNX Runtime?
我个人习惯把推理引擎分成两类:一类是跟框架绑死的,比如PyTorch自家的libtorch;另一类是开放中立的,ONNX Runtime就属于后者。它最大的优势是:一次转换,到处运行。你想想看,模型从训练到部署,中间要经历多少环境变化?ONNX Runtime帮你把这一步标准化了。
我在项目中遇到过最头疼的事——客户要求同时支持CPU和GPU推理,而且模型来源五花八门。用ONNX Runtime,我只需要维护一套推理代码,换后端就行了。CPU用OpenML,GPU用CUDA Execution Provider,代码几乎不用改。
CMake集成ONNX Runtime的两种方式
集成方式其实就两种:预编译包和源码编译。我建议你根据项目阶段来选。
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 预编译包 | 快速、稳定、无需编译工具链 | 无法定制、体积较大 | 快速原型、标准部署 |
| 源码编译 | 可裁剪、可定制优化选项 | 编译耗时、依赖复杂 | 嵌入式、特殊硬件支持 |
方式一:使用预编译包(推荐新手)
ONNX Runtime官方提供了NuGet包和Release包。我们直接下载解压,然后用CMake的find_package或者手动指定路径就行。
先下载:https://github.com/microsoft/onnxruntime/releases,选对应平台的包。比如Linux x64 CPU版本,下载onnxruntime-linux-x64-1.15.1.tgz。
解压后目录结构大概是这样:
onnxruntime-linux-x64-1.15.1/
├── include/
│ └── onnxruntime/
│ ├── core/
│ └── ...
├── lib/
│ └── libonnxruntime.so
└── ...
然后写CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(OnnxRuntimeDemo)
# 设置ONNX Runtime路径
set(ONNXRUNTIME_ROOT "/path/to/onnxruntime-linux-x64-1.15.1")
# 头文件
include_directories(${ONNXRUNTIME_ROOT}/include)
# 链接库
link_directories(${ONNXRUNTIME_ROOT}/lib)
add_executable(onnx_demo main.cpp)
target_link_libraries(onnx_demo onnxruntime)
嗯,这里要注意:link_directories在CMake 3.20之后建议用target_link_directories,但为了兼容性,我习惯用老方式。如果你用CMake 3.20+,可以改成:
target_link_directories(onnx_demo PRIVATE ${ONNXRUNTIME_ROOT}/lib)
target_link_libraries(onnx_demo onnxruntime)
方式二:使用FetchContent自动下载(更优雅)
我个人比较喜欢这种方式——CMake脚本里直接声明依赖,自动下载解压。省得手动管理包。
cmake_minimum_required(VERSION 3.16)
project(OnnxRuntimeFetchDemo)
include(FetchContent)
# 定义ONNX Runtime版本
set(ONNXRUNTIME_VERSION "1.15.1")
set(ONNXRUNTIME_URL "https://github.com/microsoft/onnxruntime/releases/download/v${ONNXRUNTIME_VERSION}/onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}.tgz")
FetchContent_Declare(
onnxruntime
URL ${ONNXRUNTIME_URL}
SOURCE_DIR "${CMAKE_BINARY_DIR}/onnxruntime"
)
FetchContent_MakeAvailable(onnxruntime)
# 设置路径
set(ONNXRUNTIME_ROOT "${CMAKE_BINARY_DIR}/onnxruntime")
add_executable(onnx_fetch_demo main.cpp)
target_include_directories(onnx_fetch_demo PRIVATE ${ONNXRUNTIME_ROOT}/include)
target_link_directories(onnx_fetch_demo PRIVATE ${ONNXRUNTIME_ROOT}/lib)
target_link_libraries(onnx_fetch_demo onnxruntime)
这样做的好处是——你只需要改版本号,就能升级ONNX Runtime。我曾经在多个项目里用这个模板,维护起来特别省事。
方式三:源码编译(进阶)
如果你需要定制ONNX Runtime,比如去掉不需要的算子、启用特定硬件加速,那就得源码编译了。说实话,这个过程有点痛苦,我第一次编译花了整整一个下午。
先克隆源码:
git clone --recursive https://github.com/microsoft/onnxruntime.git
cd onnxruntime
./build.sh --config Release --build_shared_lib --parallel
编译完成后,产物在build/Linux/Release/下。然后CMake里这样引用:
set(ONNXRUNTIME_ROOT "/path/to/onnxruntime/build/Linux/Release")
# 其余同上
这里有个坑——编译ONNX Runtime需要Python 3.x和cmake 3.13+。我曾经在CentOS 7上编译,系统自带的cmake版本太老,折腾了半天才搞定。建议用Ubuntu 20.04+或者直接上Docker。
一个完整的推理示例
集成好了,总得跑个例子验证一下。下面是一个简单的图像分类推理代码:
#include <onnxruntime/core/session/onnxruntime_cxx_api.h>
#include <iostream>
#include <vector>
int main() {
// 1. 创建环境
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test");
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(1);
// 2. 加载模型
const char* model_path = "model.onnx";
Ort::Session session(env, model_path, session_options);
// 3. 获取输入输出信息
Ort::AllocatorWithDefaultOptions allocator;
size_t num_input_nodes = session.GetInputCount();
std::vector<const char*> input_names;
std::vector<int64_t> input_shapes;
for (size_t i = 0; i < num_input_nodes; i++) {
auto input_name = session.GetInputName(i, allocator);
input_names.push_back(input_name);
auto type_info = session.GetInputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
input_shapes = tensor_info.GetShape();
}
// 4. 准备输入数据(假设是1x3x224x224的图片)
std::vector<float> input_data(1 * 3 * 224 * 224, 1.0f);
std::vector<int64_t> input_shape = {1, 3, 224, 224};
Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(
OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
memory_info, input_data.data(), input_data.size(),
input_shape.data(), input_shape.size());
// 5. 推理
const char* output_names[] = {"output"};
auto output_tensors = session.Run(Ort::RunOptions{nullptr},
input_names.data(), &input_tensor, 1,
output_names, 1);
// 6. 获取结果
float* output_data = output_tensors[0].GetTensorMutableData<float>();
std::cout << "推理结果: " << output_data[0] << std::endl;
return 0;
}
这段代码我用了很多次,基本是标准模板。你只需要改模型路径和输入数据的预处理逻辑就行。
避坑指南
我曾经踩过的坑:
- 动态链接库找不到:运行时提示
libonnxruntime.so找不到。解决方案:设置LD_LIBRARY_PATH或者用rpath。我习惯在CMake里加一句set_target_properties(onnx_demo PROPERTIES INSTALL_RPATH "${ONNXRUNTIME_ROOT}/lib")。 - 版本不匹配:ONNX Runtime的版本和ONNX opset版本要对应。比如ONNX Runtime 1.15支持opset 21,如果你模型是用opset 22导出的,就会报错。解决办法:导出模型时指定opset版本。
- GPU版本需要额外依赖:如果用CUDA Execution Provider,需要安装CUDA和cuDNN,而且版本要跟ONNX Runtime匹配。我建议先看官方文档的版本对应表。
知识体系总览
下面这张图,是我对ONNX Runtime集成流程的总结。你看一眼,心里就有谱了。
总结
集成ONNX Runtime,说白了就是三步:下载库、配置CMake、写推理代码。我个人推荐用FetchContent方式,省心又干净。如果你只是快速验证,预编译包也够用。源码编译嘛,除非你有特殊需求,否则没必要折腾。
最后提醒一句——别忘了检查ONNX Runtime的版本和你的模型opset是否兼容。这个坑我替你们踩过了,你们就别再踩了。
小技巧:如果你不确定ONNX Runtime版本和opset的对应关系,直接去官方GitHub的Release页面看Release Notes,里面写得很清楚。