4. 推理框架选型与交叉编译:NCNN/TensorRT/MNN/OpenVINO 实战

模型训练好了,量化也跑通了,接下来就是把它塞进嵌入式设备里跑起来。这一步,说白了就是选一个合适的推理框架,然后搞定交叉编译。我见过不少团队,模型精度刷得挺高,结果卡在移植环节好几个月——框架选型没想清楚,依赖库版本打架,链接符号找不到,折腾一圈下来比训练还痛苦。

这一章,我就把几个主流推理框架的选型思路、CMake 交叉编译的配置套路、静态库与动态库的链接陷阱,以及 OpenCV、Protobuf 这些依赖项的处理方法,一次性讲清楚。

4.1 主流推理框架对比:谁更适合你的板子?

先看一张总览表,心里有个底。

框架 作者/社区 硬件倾向 量化支持 交叉编译难度 典型场景
NCNN 腾讯 ARM CPU / Vulkan GPU INT8 / FP16 ★★☆☆☆(简单) 手机、IoT、低功耗设备
TensorRT NVIDIA NVIDIA GPU (Jetson/Orin) INT8 / FP16 / TF32 ★★★☆☆(中等) Jetson 系列、边缘 GPU 盒子
MNN 阿里巴巴 ARM CPU / OpenCL GPU INT8 / FP16 ★★☆☆☆(简单) 端侧推理、电商场景
OpenVINO Intel Intel CPU / 集成GPU / VPU INT8 / FP16 ★★★☆☆(中等) Intel x86 平台、工业视觉

我个人习惯这样选:

  • ARM 板子(树莓派、RK3588、全志) → NCNN 或 MNN。NCNN 的 ARM 汇编优化做得最久,MNN 的算子覆盖更全。我曾在 RK3399 上同时测过两个框架跑 MobileNetV2,NCNN 单帧快了 3ms,但 MNN 的 INT8 量化工具链更顺手。
  • NVIDIA Jetson 系列 → TensorRT。没得选,这是官方亲儿子。TensorRT 的层融合和 INT8 校准,能把模型压到极致。我记得有一次在 Jetson Xavier NX 上部署 YOLOv5s,用 TensorRT 的 FP16 推理比原始 PyTorch 快了 5 倍。
  • Intel x86 工控机 → OpenVINO。它针对 Intel CPU 的指令集做了深度优化,尤其是集成显卡的推理,其他框架比不了。
避坑: 我曾经在 RK3588 上强行用 TensorRT,结果发现它根本不支持 ARM 后端,白白浪费了两天编译时间。选框架之前,先确认你的硬件平台在不在它的支持列表里。

4.2 CMake 交叉编译配置:三板斧搞定

交叉编译说白了就是:在 x86 的电脑上,编译出能在 ARM 板子上跑的二进制。CMake 的交叉编译配置,核心就是写一个 toolchain.cmake 文件。我把它拆成三步:

4.2.1 写 toolchain 文件

以 ARM64 为例,一个最简的 toolchain 长这样:

# aarch64-linux-gnu.toolchain.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

# 指定交叉编译器路径
set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++)

# 指定 sysroot(目标板的根文件系统)
set(CMAKE_SYSROOT /path/to/sysroot)

# 查找库和头文件时,只在 sysroot 里找
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

这里有个关键点:CMAKE_SYSROOT 指向目标板的根文件系统。你想想看,如果 sysroot 里没有 OpenCV 的 .so 和头文件,CMake 就找不到依赖,编译必然失败。

4.2.2 编译时传入 toolchain

mkdir build && cd build
cmake .. \
  -DCMAKE_TOOLCHAIN_FILE=../aarch64-linux-gnu.toolchain.cmake \
  -DCMAKE_INSTALL_PREFIX=./install
make -j4
make install

嗯,这里要注意:-DCMAKE_INSTALL_PREFIX 最好指定一个本地目录,别直接装到系统路径里,否则多个项目混在一起容易乱。

4.2.3 处理框架自身的交叉编译

每个推理框架的 CMake 配置大同小异。以 NCNN 为例:

cmake .. \
  -DCMAKE_TOOLCHAIN_FILE=../toolchains/aarch64-linux-gnu.toolchain.cmake \
  -DNCNN_VULKAN=OFF \
  -DNCNN_BUILD_EXAMPLES=OFF \
  -DNCNN_BUILD_TOOLS=OFF \
  -DNCNN_OPENMP=ON

我建议第一次编译时,把 BUILD_EXAMPLES 和 BUILD_TOOLS 关掉,只编译核心库。这样能减少一半的编译时间,也避免示例代码里引入额外的依赖。

小技巧: 如果编译过程中报找不到 pthread 或 dl,别慌。在 toolchain 文件里加上 set(CMAKE_EXE_LINKER_FLAGS "-lpthread -ldl") 通常就能解决。这是嵌入式交叉编译的常见问题,因为 sysroot 里的链接器默认搜索路径可能不完整。

4.3 静态库 vs 动态库:链接的艺术

框架编译出来,你会得到 .a(静态库)和 .so(动态库)两种产物。怎么选?我直接说结论:

对比维度 静态库 (.a) 动态库 (.so)
可执行文件大小 大(库代码被复制进可执行文件) 小(运行时加载 .so)
部署复杂度 低(一个二进制文件搞定) 高(需要把 .so 也拷到板子上)
更新灵活性 差(更新库需要重新编译整个应用) 好(替换 .so 即可)
链接时符号冲突 容易遇到(多个 .a 里同名符号) 较少(动态链接器会处理)

我个人习惯:开发调试阶段用动态库,方便快速迭代;正式发布时用静态库,避免板子上缺 .so 导致程序跑不起来。我曾经在客户现场遇到过,拷了可执行文件忘了拷 libncnn.so,结果现场 debug 了半小时——从那以后,发布版本我全用静态链接。

CMake 里控制链接方式很简单:

# 链接静态库
target_link_libraries(my_app PRIVATE /path/to/libncnn.a)

# 链接动态库
target_link_libraries(my_app PRIVATE /path/to/libncnn.so)

或者用 find_library 让 CMake 自动找:

find_library(NCNN_LIB ncnn PATHS /path/to/lib)
target_link_libraries(my_app PRIVATE ${NCNN_LIB})
注意: 如果同时链接了多个静态库,且它们之间有符号依赖,链接顺序很重要。GCC 的链接器是从右向左解析符号的,所以被依赖的库要放在右边。比如 target_link_libraries(my_app PRIVATE A B),如果 A 依赖 B,B 必须放在 A 的右边。这个坑我踩过好几次,尤其是 OpenCV 和 Protobuf 混用时。

4.4 框架依赖项处理:OpenCV 和 Protobuf 的交叉编译

推理框架大多依赖 OpenCV(图像预处理)和 Protobuf(模型序列化)。这两个库的交叉编译,是移植过程中最容易出问题的环节。

4.4.1 交叉编译 OpenCV

OpenCV 的 CMake 选项非常多,但嵌入式场景下,我们只需要核心模块:

cmake .. \
  -DCMAKE_TOOLCHAIN_FILE=../aarch64-linux-gnu.toolchain.cmake \
  -DCMAKE_INSTALL_PREFIX=./install \
  -DBUILD_opencv_world=ON \
  -DBUILD_LIST=core,imgproc,imgcodecs \
  -DWITH_GTK=OFF \
  -DWITH_QT=OFF \
  -DWITH_OPENCL=OFF \
  -DBUILD_SHARED_LIBS=OFF  # 编译静态库,方便部署

我建议只编译 core、imgproc、imgcodecs 三个模块。其他像 highgui、video 在嵌入式设备上基本用不到,编译它们还会引入 GTK、FFmpeg 等额外依赖,徒增烦恼。

4.4.2 交叉编译 Protobuf

Protobuf 的交叉编译有个特殊之处:它需要先用本地编译器生成 protoc 工具,再用交叉编译器编译库本身。

# 第一步:编译本地 protoc
cd protobuf
./configure --prefix=/tmp/protobuf-host
make -j4
make install

# 第二步:交叉编译 protobuf 库
./configure \
  --host=aarch64-linux-gnu \
  --with-protoc=/tmp/protobuf-host/bin/protoc \
  --prefix=/path/to/sysroot/usr
make -j4
make install

嗯,这里要注意:--with-protoc 必须指向第一步编译出来的本地 protoc,否则交叉编译时生成的 .pb.cc 文件会编译失败。我刚开始做的时候,忘了指定这个参数,结果编译出来的库在板子上解析模型文件时一直段错误,查了两天才发现是 protoc 版本不匹配。

核心经验: 依赖项的交叉编译,一定要先确认目标板 sysroot 里有没有对应的运行时库。如果没有,就把依赖项编译成静态库,直接链接进你的应用里。这样虽然可执行文件大一点,但能避免 90% 的运行时错误。

4.5 本章知识体系总览

下面这张图,把推理框架选型、交叉编译配置、链接策略和依赖处理串在了一起。你可以把它当作一个检查清单,每次移植新模型时对照着走一遍。

推理框架移植知识体系 推理框架移植知识体系 硬件平台 ARM / x86 / GPU Jetson / RK / 树莓派 框架选型 NCNN / MNN / TRT / OV 量化 / 算子 / 硬件适配 交叉编译配置 Toolchain / CMake / Sysroot 编译器 / 链接器 / 路径 链接策略 静态库 .a / 动态库 .so 符号冲突 / 链接顺序 依赖处理 OpenCV / Protobuf 交叉编译 / 静态链接 提示:从硬件出发,依次经过框架选型 → 交叉编译 → 链接策略 → 依赖处理,每一步都决定最终部署成败

这张图从左到右展示了移植的完整链路。你每次拿到一个新板子,都可以按这个流程走一遍:先确定硬件,再选框架,然后配置交叉编译环境,决定链接方式,最后搞定依赖。每一步都有坑,但按这个顺序排查,能省下不少时间。