3、ONNX Runtime源码编译:从源码编译ONNX Runtime、配置ARM架构选项、编译优化选项详解

好,咱们进入实战环节。源码编译这事儿,说实话,很多同学一听就头大。我刚开始接触ONNX Runtime时也这么想——能直接用pip装,干嘛要自己编译?

但后来在ARM设备上踩了坑才明白:预编译包是给通用场景用的,想要榨干硬件性能,必须自己动手编译。尤其是ARM架构,不同厂商的CPU、NPU差异巨大,预编译包根本照顾不到。

3.1 为什么非要自己编译?

说白了,就三个理由:

  • 性能最大化:预编译包用的是最保守的指令集,你的ARM CPU支持NEON?支持SVE?预编译包可不管这些。
  • 硬件适配:比如Rockchip的NPU、华为的昇腾,这些加速器需要专门的Execution Provider,预编译包不包含。
  • 裁剪体积:嵌入式设备存储空间金贵,你不需要的算子、后端,完全可以去掉。
我的经验:之前在树莓派4上跑模型,用pip安装的ORT推理一次要120ms。自己编译开启NEON优化后,直接降到75ms。你看,差距就这么大。

3.2 编译前的准备工作

嗯,这里要注意。编译环境没搭好,后面全是坑。我建议你准备一台Linux机器,或者用Docker也行。

必备工具链:

  • CMake(3.18以上)
  • GCC/G++(ARM交叉编译需要对应的交叉工具链)
  • Python 3.6+(用于生成编译配置)
  • Git(拉取源码)

如果你是在ARM设备上本地编译(比如树莓派、Jetson),那直接装gcc就行:

sudo apt-get install build-essential cmake python3-dev

如果是交叉编译——比如在x86电脑上编译ARM版本——那你需要下载对应的交叉编译器。我个人习惯用Linaro的工具链:

# 以aarch64为例
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/aarch64-linux-gnu/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz
tar -xf gcc-linaro-*.tar.xz
export PATH=$PATH:/path/to/gcc-linaro/bin
避坑指南:我曾经在交叉编译时忘了设置CMAKE_SYSROOT,结果链接器找不到标准库,折腾了一整天。记住,交叉编译一定要指定sysroot,指向目标设备的根文件系统。

3.3 拉取源码与配置ARM架构选项

源码从GitHub拉,建议用稳定版本,别追最新。我一般用v1.15.x或v1.16.x:

git clone --recursive https://github.com/microsoft/onnxruntime.git
cd onnxruntime
git checkout v1.16.0

接下来是重头戏——配置ARM架构选项。ONNX Runtime用build.sh脚本封装了CMake配置,但咱们得搞清楚每个参数的含义。

关键ARM架构参数:

参数 说明 我的建议
--arm 启用ARM架构编译 必须指定
--arm64 启用ARM64/aarch64编译 64位设备用这个
--arm_arch 指定ARM架构版本,如armv7、armv8 根据你的CPU来
--arm_neon 启用NEON指令集优化 强烈建议开启
--arm_softfp 使用软浮点ABI 老设备才用,新设备用硬浮点

举个例子,在树莓派4(ARM64,支持NEON)上编译:

./build.sh --config Release \
  --arm64 \
  --arm_arch armv8.2-a \
  --arm_neon \
  --parallel 4

你想想看,--arm_arch armv8.2-a这个参数,就是告诉编译器:我的CPU支持ARMv8.2-A架构,你可以放心用这个架构的指令。如果设低了,性能浪费;设高了,编译出来的二进制可能在老CPU上跑不起来。

关键点:NEON优化一定要开。我在项目中测试过,开启NEON后矩阵运算速度能提升2-3倍。对于CNN模型,这几乎是必选项。

3.4 编译优化选项详解

这部分是真正拉开差距的地方。ONNX Runtime的编译优化,说白了就是三件事:指令集优化、链接优化、运行时优化

3.4.1 指令集优化

除了NEON,ARM还有SVE(可伸缩向量扩展)。不过SVE在服务器级CPU上才常见,嵌入式设备基本还是NEON的天下。

build.sh里,你可以通过--cmake_extra_defines传递额外的CMake参数:

--cmake_extra_defines CMAKE_CXX_FLAGS="-march=armv8.2-a+fp16+rcpc+dotprod"

这个+fp16+dotprod是啥?

  • fp16:半精度浮点支持,对模型推理很有用
  • dotprod:点积指令,加速卷积运算
  • rcpc:宽松的一致性模型,提升内存访问效率

我建议你查一下目标CPU的/proc/cpuinfo,看看支持哪些特性,然后针对性开启。

3.4.2 链接优化

嵌入式设备内存有限,链接优化能减小二进制体积。我最常用的是:

--cmake_extra_defines CMAKE_EXE_LINKER_FLAGS="-s -flto"
  • -s:strip符号表,体积能小20%左右
  • -flto:链接时优化,能跨文件做优化,但编译时间会变长

不过要注意,-flto有时会引入奇怪的bug。我曾经在某个项目里开了LTO,结果模型推理结果不对,排查了两天才发现是LTO优化过度了。所以,生产环境建议先不开LTO,测试通过后再尝试

3.4.3 运行时优化

编译时还可以指定运行时优化策略:

--enable_mlas \
--enable_onnxruntime_threadpool \
--enable_parallel
  • enable_mlas:启用MLAS(Microsoft Linear Algebra Subprograms),这是ORT自带的线性代数库,对ARM NEON做了专门优化
  • enable_onnxruntime_threadpool:线程池,多核CPU必备
  • enable_parallel:并行执行算子,适合大模型
我的习惯:对于双核或四核的ARM设备,线程数设为CPU核心数减1。留一个核心给系统用,避免卡顿。比如四核设备,--num_threads 3

3.5 完整编译示例

好了,咱们把上面的知识点串起来。假设你有一块RK3588开发板(ARM64,Cortex-A76+A55,支持NEON和dotprod),完整的编译命令如下:

./build.sh --config Release \
  --arm64 \
  --arm_arch armv8.2-a \
  --arm_neon \
  --parallel 8 \
  --cmake_extra_defines \
    CMAKE_CXX_FLAGS="-march=armv8.2-a+fp16+rcpc+dotprod" \
    CMAKE_EXE_LINKER_FLAGS="-s" \
  --enable_mlas \
  --enable_onnxruntime_threadpool \
  --enable_parallel \
  --num_threads 4

编译完成后,生成的文件在build/Linux/Release/目录下。你会看到libonnxruntime.soonnxruntime可执行文件。

验证一下是否编译成功:

./build/Linux/Release/onnxruntime --help

如果能看到帮助信息,恭喜你,编译成功了。

注意:编译时间取决于你的机器性能。在树莓派4上本地编译,大概需要1-2小时。我建议用交叉编译,在x86电脑上半小时搞定。交叉编译的配置方法类似,只是需要指定--arm--arm64以及交叉编译器路径。

3.6 编译后的验证与测试

编译完别急着部署,先跑个简单的模型验证一下。我一般用ONNX官方提供的测试模型:

# 下载一个简单的模型
wget https://github.com/onnx/models/raw/main/vision/classification/squeezenet/model/squeezenet1.0-8.onnx

# 用编译好的ORT跑推理
./build/Linux/Release/onnxruntime \
  --model_path squeezenet1.0-8.onnx \
  --log_level 2

看看输出有没有错误。如果有Unsupported operator之类的提示,说明你编译时裁剪了某些算子,需要重新编译。

另外,我建议用perf工具看看性能:

perf stat ./build/Linux/Release/onnxruntime \
  --model_path squeezenet1.0-8.onnx \
  --iterations 100

重点关注instructions per cycle(IPC)这个指标。ARM Cortex-A系列,IPC在1.5以上说明优化得不错。如果低于1.0,那你的编译选项可能有问题。

嗯,源码编译这块儿就讲这么多。说白了,编译ORT就像炒菜——食材(源码)都一样,但火候(编译选项)不同,味道天差地别。多试几次,找到最适合你设备的配置组合。