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 的指令集做了深度优化,尤其是集成显卡的推理,其他框架比不了。
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})
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 版本不匹配。
4.5 本章知识体系总览
下面这张图,把推理框架选型、交叉编译配置、链接策略和依赖处理串在了一起。你可以把它当作一个检查清单,每次移植新模型时对照着走一遍。
这张图从左到右展示了移植的完整链路。你每次拿到一个新板子,都可以按这个流程走一遍:先确定硬件,再选框架,然后配置交叉编译环境,决定链接方式,最后搞定依赖。每一步都有坑,但按这个顺序排查,能省下不少时间。