3、ONNX Runtime安装与配置:在ARM Linux上编译ONNX Runtime、Python与C++ API选择

好,咱们进入第三章。这一章讲的是ONNX Runtime的安装与配置,说白了就是怎么在ARM Linux上把这个东西跑起来。我见过不少同学,模型转换好了,结果卡在安装这一步,折腾一整天。嗯,咱们今天就把这事彻底说清楚。

3.1 为什么要在ARM上编译?

你可能会问:直接用pip安装不香吗?

香,但仅限于x86平台。ARM平台上,尤其是树莓派、瑞芯微、全志这些嵌入式板子,官方预编译包要么没有,要么性能拉胯。我去年在RK3588上做过一个项目,直接用pip装了个ONNX Runtime,推理速度慢得离谱。后来一查,发现它用的是CPU通用指令集,根本没针对ARM的NEON指令集做优化。

所以,自己编译是绕不开的路。虽然过程有点繁琐,但编译一次,后面就爽了。

3.2 编译前的准备工作

先看看你的板子环境。我个人习惯用Ubuntu 20.04或22.04的ARM版本,Debian也行。需要确认以下几点:

  • CPU架构:aarch64还是armv7l?用 uname -m 看一眼
  • 内存:至少2GB,推荐4GB以上。我曾在1GB内存的板子上编译,swap分区设了4GB,结果编译了整整6个小时
  • 磁盘空间:至少留出5GB

依赖库方面,先把这些装上:

sudo apt update
sudo apt install -y build-essential cmake git python3-dev
sudo apt install -y libprotobuf-dev protobuf-compiler
sudo apt install -y libssl-dev libcurl4-openssl-dev
小提示: 如果你用的是树莓派,建议先开启swap分区。命令是 sudo dphys-swapfile swapoff && sudo dphys-swapfile setup && sudo dphys-swapfile swapon。不然编译到一半内存爆了,那叫一个崩溃。

3.3 正式编译ONNX Runtime

从GitHub拉取源码,我建议用稳定版本,别追最新。目前1.16.x系列比较稳:

git clone --recursive -b v1.16.3 https://github.com/microsoft/onnxruntime.git
cd onnxruntime

然后就是编译配置。这里有个关键点:你要想清楚用Python还是C++ API。我个人建议,如果只是做快速验证,用Python;如果是产品级部署,必须用C++。

编译Python版本:

./build.sh --config Release --build_shared_lib --parallel
pip install build/Linux/Release/dist/*.whl

编译C++版本:

./build.sh --config Release --build_shared_lib --parallel --cmake_extra_defines CMAKE_INSTALL_PREFIX=/usr/local/onnxruntime
cd build/Linux/Release
make install

这里有个坑,我曾经踩过:--parallel 参数默认用满所有CPU核心。如果你的板子散热不行,比如树莓派4B,建议手动指定核心数,比如 --parallel 2。不然温度飙到85度,直接降频,编译速度反而更慢。

注意: 编译时间取决于板子性能。树莓派4B大概需要1-2小时,RK3588大概30-40分钟。别急,泡杯茶等着。

3.4 Python API vs C++ API:怎么选?

这个问题我经常被问到。直接上对比表:

对比项 Python API C++ API
开发效率 高,几行代码搞定 低,需要写不少胶水代码
推理性能 中等,有Python解释器开销 高,几乎没有额外开销
内存占用 较高,Python运行时占不少 低,适合资源受限设备
调试难度 容易,print大法好 较难,需要gdb
适用场景 原型验证、快速迭代 产品部署、性能敏感场景

我的建议是:前期用Python验证模型正确性,后期用C++做最终部署。我在一个安防项目中就是这么干的——先用Python调通了模型,然后花了两天时间把推理代码改成C++,推理速度提升了将近30%。

3.5 Python API快速上手

编译完成后,验证一下是否安装成功:

python3 -c "import onnxruntime; print(onnxruntime.__version__)"

如果能打印出版本号,恭喜你,成了。

一个简单的推理示例:

import onnxruntime as ort
import numpy as np

# 创建推理会话
sess = ort.InferenceSession("model.onnx")

# 获取输入输出信息
input_name = sess.get_inputs()[0].name
output_name = sess.get_outputs()[0].name

# 构造输入数据(假设是1x3x224x224的图像)
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)

# 执行推理
output = sess.run([output_name], {input_name: input_data})
print(output[0].shape)

你看,就这么几行。Python API的简洁性确实没得说。

3.6 C++ API实战

C++这边稍微复杂点。首先,编译时需要链接ONNX Runtime的动态库:

g++ -o inference inference.cpp \
    -I/usr/local/onnxruntime/include \
    -L/usr/local/onnxruntime/lib \
    -lonnxruntime \
    -Wl,-rpath,/usr/local/onnxruntime/lib

代码示例:

#include <onnxruntime/core/session/onnxruntime_cxx_api.h>
#include <iostream>
#include <vector>

int main() {
    // 初始化环境
    Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test");
    Ort::SessionOptions session_options;
    session_options.SetIntraOpNumThreads(1);

    // 加载模型
    Ort::Session session(env, "model.onnx", session_options);

    // 获取输入输出信息
    Ort::AllocatorWithDefaultOptions allocator;
    auto input_name = session.GetInputNameAllocated(0, allocator).get();
    auto output_name = session.GetOutputNameAllocated(0, allocator).get();

    // 构造输入
    std::vector<int64_t> input_shape = {1, 3, 224, 224};
    std::vector<float> input_data(1*3*224*224, 1.0f);
    Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
        Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault),
        input_data.data(), input_data.size(), input_shape.data(), input_shape.size()
    );

    // 推理
    auto output_tensors = session.Run(Ort::RunOptions{nullptr},
        &input_name, &input_tensor, 1,
        &output_name, 1
    );

    // 获取输出
    float* output_data = output_tensors[0].GetTensorMutableData<float>();
    std::cout << "Output shape: " << output_tensors[0].GetTensorTypeAndShapeInfo().GetShape()[0] << std::endl;

    return 0;
}

说实话,C++代码确实啰嗦。但性能优势摆在那里,尤其是嵌入式设备上,每一毫秒都珍贵。

关键点: 在ARM平台上,C++ API能更好地利用NEON指令集和硬件加速器。我实测过,同样的模型,C++推理比Python快20%-35%。

3.7 避坑指南

最后,分享几个我踩过的坑:

  • 编译时内存不足:我曾经在1GB内存的板子上编译,直接OOM。解决方案是增加swap分区,或者用 --parallel 1 单线程编译
  • Python版本不匹配:ONNX Runtime的Python绑定只支持特定Python版本。编译前用 python3 --version 确认一下,我建议用Python 3.8或3.10
  • 动态库找不到:编译完C++程序后,运行时提示找不到libonnxruntime.so。记得设置 LD_LIBRARY_PATH 或者用 -Wl,-rpath 指定库路径
  • 模型加载失败:如果模型是用更高版本的ONNX导出的,而你的ONNX Runtime版本较低,会报错。保持版本一致很重要

嗯,这一章的内容就这些。安装配置是基础,但基础打牢了,后面优化才能事半功倍。下一章咱们聊聊模型量化和精度校准,那才是真正拉开性能差距的地方。