NCNN框架在嵌入式设备上的交叉编译实战:以Jetson Nano为例
在边缘计算和嵌入式AI应用日益普及的今天,将深度学习模型高效部署到资源受限的设备上,是许多开发者面临的核心挑战。NVIDIA Jetson Nano作为一款性能与功耗平衡出色的嵌入式开发板,凭借其ARM架构的CPU和集成的GPU,成为了众多AI项目的首选平台。然而,直接在Jetson Nano上编译复杂的深度学习推理框架,如腾讯开源的NCNN,往往会遇到编译耗时过长、内存不足等问题。这时,交叉编译技术便成为了提升开发效率、实现快速迭代的关键手段。
交叉编译的本质是在一台性能更强的宿主机(通常是x86_64架构的PC)上,为目标设备(如ARM架构的Jetson Nano)生成可执行程序。这个过程不仅节省了目标设备宝贵的计算资源,还能利用宿主机更快的CPU和更大的内存,显著缩短编译时间。对于NCNN这样依赖OpenCV、Protobuf等多个库的框架,在宿主机上一次性配置好交叉编译环境,后续的模型部署和测试将变得异常顺畅。本文将带你从零开始,一步步构建一个针对Jetson Nano的NCNN交叉编译环境,涵盖工具链配置、依赖库编译、框架优化选项设置,直至最终的部署与性能验证。无论你是正在为嵌入式设备寻找轻量级推理方案的工程师,还是希望将AI模型落地到实际硬件的爱好者,这篇实战指南都将提供清晰的路径和可复现的操作细节。
1. 交叉编译环境搭建:宿主机与目标设备准备
进行交叉编译的第一步,是明确并准备好宿主机和目标设备两端的环境。宿主机我们选择一台运行Ubuntu 22.04 LTS的x86_64电脑,它拥有完整的开发工具链和充足的磁盘空间。目标设备则是Jetson Nano,它运行着基于ARM架构的Ubuntu系统。两者之间需要通过网络(SSH)保持连接,以便传输编译好的库文件和测试程序。
在宿主机上,我们首先需要安装基础的编译工具和CMake。虽然许多教程会建议安装build-essential,但对于交叉编译,我们更关注于获取目标架构的特定工具链。NVIDIA为Jetson系列提供了名为L4T (Linux for Tegra) 的BSP(板级支持包),其中包含了针对其ARM处理器的交叉编译工具链。最直接的方式是从NVIDIA开发者网站下载与你的Jetson Nano系统版本对应的L4T工具链。例如,对于JetPack 4.6版本,对应的L4T版本是R32.6.1。
# 假设你已经下载了名为gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu.tar.xz的工具链
# 将其解压到/opt目录下
sudo tar -xJf gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu.tar.xz -C /opt
接下来,需要将工具链的二进制文件路径添加到系统的PATH环境变量中,并设置相关的环境变量,以便后续的CMake能够识别。
# 编辑你的shell配置文件,例如 ~/.bashrc
echo 'export PATH=/opt/gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu/bin:$PATH' >> ~/.bashrc
echo 'export CROSS_COMPILE=aarch64-linux-gnu-' >> ~/.bashrc
echo 'export CC=aarch64-linux-gnu-gcc' >> ~/.bashrc
echo 'export CXX=aarch64-linux-gnu-g++' >> ~/.bashrc
source ~/.bashrc
完成上述步骤后,你可以在终端输入aarch64-linux-gnu-gcc --version来验证工具链是否安装成功。同时,别忘了在宿主机上安装CMake的较新版本(至少3.10以上),因为NCNN的构建脚本会用到一些较新的特性。
注意:不同版本的JetPack对应的L4T工具链可能不同,务必确保宿主机工具链的Glibc版本不高于目标设备系统的Glibc版本,否则编译出的程序可能在目标设备上无法运行。一个简单的检查方法是分别在宿主机(查看工具链中的libc.so)和目标设备(运行
ldd --version)上查看Glibc版本。
另一方面,目标设备Jetson Nano也需要进行一些基础准备。首先,确保系统已更新,并安装一些运行时可能需要的库。通过SSH连接到Jetson Nano,执行以下命令:
sudo apt update
sudo apt upgrade -y
# 安装一些基础库,例如OpenCV的运行库(如果打算使用OpenCV后端)、Vulkan驱动等
sudo apt install -y libopencv-dev libvulkan1 mesa-vulkan-drivers
为了后续性能测试方便,建议在Jetson Nano上安装一些性能监控工具,如tegrastats(通常已预装)、htop等。至此,交叉编译的舞台已经搭好,接下来我们将进入具体的依赖库编译环节。
2. 依赖库的交叉编译:Protobuf与OpenCV
NCNN框架的正常运行依赖于几个关键的第三方库,其中最主要的是Protocol Buffers (Protobuf) 和OpenCV。Protobuf用于解析模型文件(如ONNX格式),而OpenCV则为NCNN的示例程序提供了图像加载和显示功能。我们需要在宿主机上,使用交叉编译工具链,为ARM架构的Jetson Nano编译这些库。
2.1 交叉编译Protobuf
Protobuf的交叉编译相对直接。首先从GitHub仓库下载稳定版本的源码(例如3.20.3),解压后进入目录。由于我们需要交叉编译,不能直接使用默认的./configure脚本,而是要通过CMake来配置。
# 下载并解压protobuf源码
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.3/protobuf-cpp-3.20.3.tar.gz
tar -xzf protobuf-cpp-3.20.3.tar.gz
cd protobuf-3.20.3
创建一个用于交叉编译的构建目录,并使用CMake进行配置。关键的CMake选项是指定交叉编译工具链文件,但这里我们更直接地通过定义CMAKE_C_COMPILER和CMAKE_CXX_COMPILER来指定编译器。
mkdir build-aarch64 && cd build-aarch64
cmake .. \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
-DCMAKE_INSTALL_PREFIX=/path/to/your/cross/install/dir/protobuf \
-DBUILD_SHARED_LIBS=OFF \
-Dprotobuf_BUILD_TESTS=OFF
这里有几个要点:
CMAKE_SYSTEM_NAME和CMAKE_SYSTEM_PROCESSOR告知CMake我们正在为Linux ARM64系统编译。CMAKE_C_COMPILER和CMAKE_CXX_COMPILER直接指向我们之前安装的交叉编译器。CMAKE_INSTALL_PREFIX指定了编译产物的安装路径。建议设置一个独立的目录(如~/jetson_nano_sysroot),用于集中存放所有交叉编译好的库,方便管理。BUILD_SHARED_LIBS=OFF选择编译静态库。在嵌入式环境中,使用静态库可以避免目标设备上动态库依赖的麻烦,但会增大最终可执行文件的体积。你可以根据需求选择。
配置完成后,进行编译和安装:
make -j$(nproc)
make install
编译完成后,检查安装目录下的lib文件夹,应该能看到libprotobuf.a等静态库文件,以及include文件夹中的头文件。
2.2 交叉编译OpenCV
OpenCV的交叉编译过程比Protobuf稍复杂,因为它包含更多的模块和可选的依赖(如GTK、FFmpeg等)。为了简化,我们只编译核心模块,并禁用一些在嵌入式设备上可能不需要的功能(如GUI相关的highgui模块,除非你确定需要)。
首先,下载OpenCV源码(例如4.8.0版本)及其扩展模块opencv_contrib(如果需要额外的算法模块)。
git clone --branch 4.8.0 https://github.com/opencv/opencv.git
git clone --branch 4.8.0 https://github.com/opencv/opencv_contrib.git
创建构建目录并进入。OpenCV的CMake配置提供了专门用于交叉编译的工具链文件模板,但我们可以手动指定编译器。一个更重要的步骤是指定目标系统的根文件系统(sysroot)。虽然对于Jetson Nano,很多基础库(如libc)与宿主机工具链兼容,但指定sysroot可以让CMake更准确地找到目标系统的头文件和库。我们可以从Jetson Nano上复制一份根文件系统到宿主机,或者直接使用工具链自带的sysroot(如果工具链包内包含)。
假设我们有一个sysroot目录位于/opt/sysroot-jetson,CMake命令如下:
cd opencv
mkdir build-aarch64 && cd build-aarch64
cmake .. \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
-DCMAKE_INSTALL_PREFIX=/path/to/your/cross/install/dir/opencv \
-DCMAKE_PREFIX_PATH=/path/to/your/cross/install/dir/protobuf \ # 如果OpenCV需要protobuf(如DNN模块)
-DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \
-DBUILD_opencv_world=OFF \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_TESTS=OFF \
-DBUILD_PERF_TESTS=OFF \
-DBUILD_EXAMPLES=OFF \
-DWITH_GTK=OFF \
-DWITH_GTK_2_X=OFF \
-DWITH_WEBP=OFF \
-DWITH_JPEG=ON \
-DWITH_PNG=ON \
-DWITH_TIFF=OFF \
-DENABLE_NEON=ON \ # 启用ARM NEON指令集优化
-DENABLE_VFPV3=ON \
-DCPU_BASELINE=NEON \
-DBUILD_LIST=core,imgproc,imgcodecs,dnn \ # 只编译必要的模块
-DCMAKE_FIND_ROOT_PATH=/opt/sysroot-jetson \
-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \
-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \
-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY
这个配置做了大量精简,只保留了核心、图像处理、图像编解码和DNN模块,并关闭了测试和示例以加快编译。ENABLE_NEON和CPU_BASELINE的设置为ARM架构启用了关键的SIMD指令优化,这对性能至关重要。
提示:如果编译过程中遇到关于
protobuf的错误(例如Protobuf_LIBRARY未找到),这通常是因为CMake在sysroot或默认路径中找不到我们刚刚编译的protobuf库。确保CMAKE_PREFIX_PATH正确指向了protobuf的安装目录,并且该目录下有lib/cmake/protobuf或类似的CMake配置文件。
配置成功后,同样使用make -j$(nproc)和make install进行编译和安装。OpenCV的编译耗时较长,请耐心等待。完成后,你的交叉编译库目录中应该就有了OpenCV的库和头文件。
3. NCNN框架的交叉编译与优化选项配置
当Protobuf和OpenCV这两个主要依赖就绪后,我们就可以开始交叉编译NCNN本身了。NCNN的CMake构建系统对交叉编译支持良好,提供了清晰的工具链文件示例。我们的目标是编译一个针对Jetson Nano优化、并尽可能启用其硬件特性(如GPU加速)的版本。
首先,克隆NCNN的官方仓库并更新子模块:
git clone https://github.com/Tencent/ncnn.git
cd ncnn
git submodule update --init
NCNN源码的toolchains目录下已经包含了许多预定义的工具链文件,例如aarch64-linux-gnu.toolchain.cmake。我们可以直接使用它,或者基于它进行修改。查看该文件内容,会发现它已经设置了交叉编译器、目标系统等基本变量。我们创建一个构建目录,并使用这个工具链文件。
mkdir build-aarch64 && cd build-aarch64
cmake .. \
-DCMAKE_TOOLCHAIN_FILE=../toolchains/aarch64-linux-gnu.toolchain.cmake \
-DCMAKE_INSTALL_PREFIX=/path/to/your/cross/install/dir/ncnn \
-DCMAKE_BUILD_TYPE=Release \
-DNCNN_BUILD_EXAMPLES=ON \
-DNCNN_BUILD_TOOLS=ON \
-DNCNN_VULKAN=ON \
-DNCNN_SYSTEM_GLSLANG=OFF \
-DVulkan_INCLUDE_DIR=/path/to/vulkan/headers \
-DVulkan_LIBRARY=/path/to/vulkan/lib \
-DProtobuf_INCLUDE_DIR=/path/to/your/cross/install/dir/protobuf/include \
-DProtobuf_LIBRARIES=/path/to/your/cross/install/dir/protobuf/lib/libprotobuf.a \
-DOpenCV_DIR=/path/to/your/cross/install/dir/opencv/lib/cmake/opencv4
让我们逐一解析这些关键选项:
CMAKE_TOOLCHAIN_FILE:这是交叉编译的“钥匙”,它告诉CMake所有关于目标平台、编译器、链接器的信息。CMAKE_BUILD_TYPE=Release:启用编译器优化,生成性能最优的版本。NCNN_BUILD_EXAMPLES和NCNN_BUILD_TOOLS:建议开启。示例程序(如nanodet、squeezenet)是验证编译是否成功的最佳方式;工具(如ncnnoptimize、模型转换工具)在后续模型部署中非常有用。NCNN_VULKAN=ON:这是为Jetson Nano启用GPU加速的关键。Jetson Nano的GPU支持Vulkan API,开启此选项可以让NCNN利用GPU进行神经网络算子的计算,极大提升推理速度。NCNN_SYSTEM_GLSLANG=OFF:GLSLang是Vulkan着色器编译器。由于我们是在交叉编译,通常选择让NCNN使用其内置的(bundled)GLSLang,避免再去交叉编译这个复杂的依赖。Vulkan_INCLUDE_DIR和Vulkan_LIBRARY:需要指定Vulkan SDK的头文件和库路径。对于Jetson Nano,Vulkan驱动和头文件通常已经安装在目标系统上。但交叉编译时,我们需要在宿主机上有对应的ARM架构的Vulkan开发包。一个可行的方法是从Jetson Nano上复制/usr/include/vulkan和/usr/lib/aarch64-linux-gnu/libvulkan.so等文件到宿主机的sysroot中,然后在此处指向sysroot中的路径。Protobuf_INCLUDE_DIR和Protobuf_LIBRARIES:指向我们之前交叉编译好的Protobuf。OpenCV_DIR:指向交叉编译好的OpenCV的CMake配置目录,这样NCNN的CMake就能找到OpenCV。
配置过程中可能会遇到各种错误,最常见的是找不到Vulkan或Protobuf。请仔细检查路径是否正确,以及对应的库文件是否是为ARM架构编译的。配置成功后,运行编译:
make -j$(nproc)
make install
编译完成后,在安装目录(/path/to/your/cross/install/dir/ncnn)下,你会看到bin、lib、include等文件夹。其中lib文件夹里的libncnn.a(静态库)或libncnn.so(动态库)就是我们最终需要的核心文件。
为了验证编译出的库是否真的是ARM架构,可以使用file命令检查:
file /path/to/your/cross/install/dir/ncnn/lib/libncnn.a
# 输出应包含“ELF 64-bit LSB relocatable, ARM aarch64”字样
4. 部署、测试与性能验证
编译产出物需要被部署到Jetson Nano上才能运行。我们将编译好的NCNN库、工具、示例程序,以及其依赖的OpenCV和Protobuf库(如果使用动态链接)一起打包,传输到Jetson Nano。
一个简单的部署方式是,在宿主机上将整个安装目录打包,然后通过SCP传输到Jetson Nano:
# 在宿主机上
tar -czf ncnn_jetson.tar.gz -C /path/to/your/cross/install/dir .
scp ncnn_jetson.tar.gz username@jetson-nano-ip:/home/username/
在Jetson Nano上解压,并设置环境变量,让系统能够找到这些库:
# 在Jetson Nano上
tar -xzf ncnn_jetson.tar.gz
export LD_LIBRARY_PATH=/home/username/ncnn_jetson/lib:$LD_LIBRARY_PATH
export PATH=/home/username/ncnn_jetson/bin:$PATH
现在,可以运行NCNN自带的示例程序进行测试。我们以经典的图像分类模型SqueezeNet为例:
cd /home/username/ncnn_jetson
# 确保有测试图片和模型文件,可以从ncnn源码的examples目录获取
cp -r /path/to/ncnn-source/examples/* .
./squeezenet test-image.jpg
如果一切顺利,你将看到终端输出图像分类的top-5类别及置信度。这证明NCNN库、模型和依赖库都正常工作。
接下来是激动人心的性能测试环节。我们需要对比CPU推理和启用Vulkan的GPU推理之间的性能差异。NCNN的benchncnn工具是进行基准测试的利器。但首先,我们需要为Jetson Nano交叉编译这个工具(在之前的步骤中,如果开启了NCNN_BUILD_BENCHMARK,它应该已经被编译了)。将其传输到Jetson Nano并运行:
./benchncnn [循环次数] [线程数] [是否使用GPU] [模型列表]
例如,测试MobileNetV2在4线程CPU和GPU上的性能:
# CPU测试(4线程)
./benchncnn 20 4 0 mobilenetv2
# GPU测试(Vulkan)
./benchncnn 20 4 1 mobilenetv2
测试结果会输出每层网络的平均耗时、总耗时、以及整体帧率(FPS)。根据我的实测,在Jetson Nano上,对于合适的模型,启用Vulkan后推理速度可以有数倍甚至一个数量级的提升。当然,具体提升幅度取决于模型结构和算子对GPU的友好程度。
除了基准测试,在实际应用中还需要关注内存占用和功耗。使用tegrastats工具可以监控Jetson Nano的CPU/GPU/内存使用率以及功耗情况。在运行你的AI应用时,观察这些指标,确保它们在设备的设计约束之内。
最后,将你的应用与NCNN库链接。如果你使用CMake管理项目,可以编写如下的CMakeLists.txt,它需要正确找到我们部署在Jetson Nano上的NCNN和OpenCV。
cmake_minimum_required(VERSION 3.10)
project(my_ncnn_app)
set(CMAKE_CXX_STANDARD 11)
# 设置NCNN和OpenCV的路径
set(ncnn_DIR "/home/username/ncnn_jetson/lib/cmake/ncnn")
set(OpenCV_DIR "/home/username/ncnn_jetson/lib/cmake/opencv4")
find_package(ncnn REQUIRED)
find_package(OpenCV REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app ncnn ${OpenCV_LIBS})
在开发过程中,你可能会遇到一些陷阱。例如,模型转换问题(确保使用正确的ncnnoptimize工具处理你的模型)、内存对齐问题(ARM架构对内存访问有对齐要求,NCNN已内部处理)、或者多线程性能调优(通过set_cpu_powersave、set_omp_num_threads等API调整)。多查阅NCNN的GitHub Wiki和Issues,社区通常已经有了解决方案。
356



被折叠的 条评论
为什么被折叠?



