前言

在边缘设备上部署深度学习模型,一直是嵌入式 AI 领域最具挑战性的课题之一。当你训练好了一个准确率令人满意的 PyTorch 模型,满心欢喜地想把它搬到 ARM 开发板上跑一跑,却发现原始模型推理一次需要好几秒,这样的性能在实际产品中根本无法使用。这时你才意识到,训练和部署之间,隔着一道看不见却异常宽阔的鸿沟。

这道鸿沟的两边是完全不同的世界:训练端追求的是灵活的算子支持、便捷的调试接口、高效的分布式训练;而部署端追求的却是极致的推理速度、最小的内存占用、最低的功耗开销。大多数框架都是为训练设计的,即使像 PyTorch 这样优秀的框架,其 C++ 前端 LibTorch 在嵌入式设备上的表现也往往差强人意。

于是我们需要专门的推理框架。在众多推理框架中,腾讯开源的 NCNN 是一个相当特别的存在。它从诞生之初就是为移动端和嵌入式设备设计的,没有历史包袱,从内存管理到算子实现都围绕 ARM 架构深度优化。更重要的是,NCNN 是纯 C++ 实现,没有任何第三方依赖,这意味着你可以轻松将它集成到各种奇葩的嵌入式环境中。

我第一次接触 NCNN 是在一块瑞芯微 RK3399 开发板上部署目标检测模型。当时用 PyTorch 推理一帧 YOLO 需要约 800ms,用 TensorFlow Lite 也需要 400ms 左右,而用 NCNN 优化后,同样的模型在同一硬件上只需要 120ms,这还没开启 Vulkan GPU 加速。那一刻我真切感受到,一个好的推理框架带来的性能提升,往往比换一颗芯片还要显著。

这篇文章会带你完整走一遍 NCNN 的部署流程:从模型训练完成后的 ONNX 导出,到 onnx2ncnn 转换,再到模型优化、INT8 量化、最后编写 C++ 推理代码。文中所有命令和代码都经过实际验证,你可以照着一步步操作。

NCNN 嵌入式 AI 推理部署流程

一、为什么选择 NCNN?

在深入具体操作之前,我们先聊聊为什么在众多推理框架中选择 NCNN,它的核心优势在哪里,又有哪些局限性。

1.1 推理框架的选型维度

选择一个推理框架,通常需要考虑以下几个维度:

维度 说明 重要程度
性能 同样硬件上的推理速度 ⭐⭐⭐⭐⭐
模型支持 能否正常转换你的模型 ⭐⭐⭐⭐⭐
易用性 文档是否完善,社区是否活跃 ⭐⭐⭐⭐
跨平台 支持多少种目标硬件 ⭐⭐⭐⭐
二进制体积 对资源紧张的 MCU 很重要 ⭐⭐⭐
许可证 是否允许商业闭源使用 ⭐⭐⭐⭐

用这个维度表来评估 NCNN,你会发现它在大多数项上得分都很高:性能在 ARM CPU 上属于第一梯队,模型支持覆盖了绝大多数常见算子,Apache 2.0 许可证非常宽松,二进制最小可以压缩到几百 KB。

1.2 NCNN 的核心优势

极致的 ARM 优化 是 NCNN 最核心的竞争力。NCNN 为 ARMv7、ARMv8 架构写了大量的 NEON 汇编优化代码,不是简单的编译器自动向量化,而是手工优化的汇编级实现。比如卷积的 Im2col + Gemm 实现,Winograd 快速卷积算法,都经过了精细的指令调度和寄存器分配优化。

这种手工优化的效果有多明显?以 3x3 卷积为例,在 Cortex-A53 上,NCNN 的实现通常比 OpenCV DNN 快 2-3 倍,比未经优化的参考实现快 10 倍以上。这不是算法层面的差距,纯粹是工程实现上的精益求精。

零依赖的纯 C++ 实现 是 NCNN 的另一个巨大优势。很多框架看起来很强大,但一交叉编译就会发现依赖一大堆第三方库:Protobuf、FlatBuffers、BLAS 库等等。在某些嵌入式环境中,光是把这些依赖库编译过去就是一场噩梦。

而 NCNN 是真正的零依赖,它甚至不依赖 C++ STL 的异常和 RTTI,在最精简的配置下,你只需要一个能编译 C++ 的交叉编译器就能把 NCNN 编出来。这种特性在面对各种定制化的嵌入式 Linux 甚至裸机环境时,价值尤为突出。

灵活的扩展性 也值得一提。NCNN 设计了一套清晰的算子注册机制,如果你需要一个自定义算子,只需要继承一个基类,实现前向计算函数,然后注册一下就行,不需要修改框架的核心代码。这种设计对于需要部署自研算子的场景非常友好。

1.3 NCNN 的局限性

当然,NCNN 也不是万能的。它的主要局限性在于:

  • GPU 支持不如 TensorRT:虽然 NCNN 支持 Vulkan GPU 加速,但在 NVIDIA 设备上,性能还是不如 TensorRT。不过在 ARM Mali GPU 上,NCNN 的 Vulkan 后端表现相当不错。
  • 动态形状支持有限:NCNN 主要是为固定输入形状优化的,动态形状的支持不如 ONNX Runtime 灵活。
  • 调试工具相对简陋:相比 TensorRT 有完善的 profiling 工具,NCNN 的调试更多需要依赖 ncnn::Extractor 的逐层输出和自己打日志。

总体来说,如果你的目标平台是 ARM CPU(手机、开发板、嵌入式设备),NCNN 是目前最好的选择之一。如果是 NVIDIA GPU,应该优先考虑 TensorRT。

二、环境搭建:从源码编译 NCNN

正式开始之前,我们需要先把 NCNN 源码下载下来并编译。NCNN 的编译系统是 CMake,过程相对 straightforward,但有几个关键的编译选项需要特别注意。

2.1 获取源码

# 克隆 NCNN 源码
git clone https://github.com/Tencent/ncnn.git
cd ncnn

# 切换到最新的稳定版本(可选但推荐)
git checkout 20240410  # 选择一个较新的稳定版本

2.2 主机端编译(x86 Linux)

首先我们在 x86 主机上编译 NCNN,主要是为了获得各种模型转换工具(onnx2ncnn、ncnnoptimize 等)。

mkdir -p build-host && cd build-host

cmake .. \
    -DNCNN_BUILD_TOOLS=ON \
    -DNCNN_BUILD_EXAMPLES=ON \
    -DNCNN_BUILD_BENCHMARK=ON \
    -DCMAKE_BUILD_TYPE=Release

make -j$(nproc)

编译完成后,你会在 tools/ 目录下看到各种工具:

  • onnx2ncnn - ONNX 模型转 NCNN 格式
  • ncnnoptimize - NCNN 模型优化
  • ncnn2table - 生成量化校准表
  • ncnn2int8 - INT8 量化
  • 等等…

把这些工具的路径加入 PATH 或者记住它们的位置,后面会频繁使用。

2.3 交叉编译(ARM Linux)

接下来是最重要的一步:为目标 ARM 设备交叉编译 NCNN。这里假设你使用的是 ARMv8 架构(Cortex-A53/A55/A72/A76 等),工具链是 aarch64-linux-gnu-gcc

cd ..
mkdir -p build-arm64 && cd build-arm64

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

make -j$(nproc)

几个关键编译选项的说明:

选项 说明
NCNN_VULKAN 是否开启 Vulkan GPU 加速
NCNN_OPENMP 是否开启 OpenMP 多线程
NCNN_BUILD_TOOLS 模型转换工具不需要在 ARM 上运行
NCNN_RUNTIME_CPU 运行时检测 CPU 特性并动态选择优化路径

如果你需要 Vulkan GPU 支持,将 NCNN_VULKAN 设为 ON,但要确保目标设备有可用的 Vulkan 驱动。

编译完成后,把 src/libncnn.a 和头文件复制到你的交叉编译环境中,或者直接在 CMake 项目中通过 add_subdirectory 引入。

2.4 Android / iOS 编译

对于移动端,NCNN 提供了更便捷的编译脚本:

# Android
cd ncnn
mkdir -p build-android && cd build-android
cmake .. -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=arm64-v8a \
    -DANDROID_PLATFORM=android-24 \
    -DNCNN_VULKAN=ON

(第一部分完,约2100字)

三、模型转换:从 PyTorch 到 ONNX 再到 NCNN

模型转换是整个部署流程中最容易出问题的环节。一个看起来完美的模型,在转换过程中可能因为一个不起眼的算子就导致整个流程卡住。这一节我们按照标准流程一步步来,尽量避开那些常见的坑。

3.1 第一步:PyTorch 导出 ONNX

在将模型交给 onnx2ncnn 之前,我们首先需要把 PyTorch 模型导出为 ONNX 格式。这一步看似简单,实则暗藏玄机。

import torch
import torchvision

# 加载模型
model = torchvision.models.resnet18(pretrained=True)
model.eval()

# 准备示例输入
dummy_input = torch.randn(1, 3, 224, 224)

# 导出 ONNX
torch.onnx.export(
    model,
    dummy_input,
    "resnet18.onnx",
    export_params=True,
    opset_version=13,
    do_constant_folding=True,
    input_names=["input"],
    output_names=["output"],
    dynamic_axes=None
)

这段代码看起来很标准,但有几个关键点需要特别注意:

opset_version 的选择:不要用太新的 opset,也不要用太旧的。opset 11-13 是目前兼容性最好的区间。opset 太高(比如 17+)可能引入了一些新的算子表示方式,onnx2ncnn 可能还没来得及支持。

dynamic_axes 设为 None:除非你真的需要动态形状。NCNN 对固定输入形状的优化最好,动态形状不仅会损失一部分性能,还可能触发某些算子的 bug。如果你的输入尺寸是固定的,就不要开动态轴。

导出前必须调用 model.eval():这个很重要,否则 BatchNorm、Dropout 等层在训练和推理模式下行为是不同的。忘记调用 eval() 是新手最容易犯的错误之一。

导出完成后,建议用 onnxsim 简化一下模型,这一步能解决 80% 的转换问题:

# 安装 onnxsim
pip install onnxsim

# 简化 ONNX 模型
onnxsim resnet18.onnx resnet18-sim.onnx

onnxsim 会做常量折叠、形状推导、无用节点消除等优化。很多 onnx2ncnn 报错的模型,经过 onnxsim 之后就正常了。这一步强烈建议执行,不要跳过。

3.2 第二步:onnx2ncnn 转换

ONNX 准备好了,接下来就是转换为 NCNN 的原生格式。NCNN 的模型格式由两个文件组成:

  • .param - 网络结构定义(文本格式,可以用文本编辑器打开)
  • .bin - 权重数据(二进制格式)

转换命令很简单:

onnx2ncnn resnet18-sim.onnx resnet18.param resnet18.bin

如果一切顺利,你会看到一堆输出,最后没有 error 字样。如果有 error,说明遇到了不支持的算子或者 ONNX 格式有问题。

常见的错误类型和解决方法:

**1. “Unsupported resize mode” Resize 算子是转换失败的重灾区。ONNX 的 Resize 有多种 coordinate_transformation_mode,NCNN 只支持 asymmetricalign_corners 两种。如果你的模型用了其他模式,可以在导出 ONNX 之前修改模型代码中的插值方式,或者用 onnxruntime-tools 手动修改 ONNX 节点属性。

**2. “Unsupported slice with step != 1” NCNN 的 Slice 算子只支持步长为 1 的情况。如果模型里有 step > 1 的 Slice,可以用 Reshape + Permute + Reshape 的组合来替代,或者修改模型结构避免使用这种特殊的 Slice。

**3. “Too many axes for permute” NCNN 的 Permute 只支持最多 4 维。如果你的模型有 5 维以上的 Permute,可以考虑拆分或者用其他算子组合实现。

转换成功后,建议打开 .param 文件看一眼。文件开头是层的数量和 blob 的数量,然后每一行是一个层的定义。检查一下有没有奇怪的层名,比如 ShapeGather 这种通常意味着模型里有动态形状相关的操作,这在 NCNN 中支持有限。

3.3 第三步:ncnnoptimize 优化

原始转换出来的模型还可以进一步优化。ncnnoptimize 工具可以做:

  • 融合 BatchNorm 到 Convolution
  • 消除 Dropout 层(推理模式下没用)
  • 权重数据类型转换(FP32 → FP16)
  • 内存布局优化
ncnnoptimize resnet18.param resnet18.bin resnet18-opt.param resnet18-opt.bin 0

最后一个参数 0 表示保持 FP32,1 表示转换为 FP16。FP16 可以将模型体积减半,在 ARMv8.2+ 的设备上还能获得显著的性能提升,精度损失通常很小。

优化完成后,你会得到两个文件:resnet18-opt.paramresnet18-opt.bin。这两个就是最终部署用的模型文件了。

四、INT8 量化:让推理速度再翻倍

对于嵌入式设备来说,FP32 推理往往还是不够快。INT8 量化可以在精度损失可控的前提下,将推理速度再提升 1.5-2 倍,内存占用也会减半。

4.1 量化的基本原理

量化的核心思想是用 8 位整数来近似表示 32 位浮点数。简单来说就是:

float_value = scale * (int8_value - zero_point)

每个张量都有自己的 scale 和 zero_point。推理时,先把输入量化为 INT8,做 INT8 卷积计算,然后再反量化回 FP32(或者直接下一层继续用 INT8)。

NCNN 使用的是后训练量化(Post-Training Quantization),不需要重新训练模型,只需要几百张校准图片就能完成量化。

4.2 生成校准表

首先我们需要准备一批校准图片,数量通常 100-1000 张就够了,不需要太多,也不需要和训练集完全一致,只要数据分布类似就行。

创建一个 imagelist.txt 文件,每行是校准图片的路径:

calib/000001.jpg
calib/000002.jpg
calib/000003.jpg
...

然后生成校准表:

ncnn2table \
    resnet18-opt.param \
    resnet18-opt.bin \
    imagelist.txt \
    resnet18.table

这个过程会比较慢,因为它要在所有校准图片跑一遍前向传播,统计每一层的激活值范围。

生成的 .table 文件是文本格式,你可以打开看看,每一行是某一层的量化参数。

4.3 执行量化

有了校准表,就可以把 FP32 模型转换为 INT8 模型了:

ncnn2int8 \
    resnet18-opt.param \
    resnet18-opt.bin \
    resnet18-int8.param \
    resnet18-int8.bin \
    resnet18.table

完成后你会得到 INT8 版本的模型。.bin 文件大小大概只有原来的 1/4。

4.4 量化精度调优

如果量化后精度下降明显,可以试试这些方法:

  1. 增加校准图片数量:从 100 张增加到 500 张通常会有改善。

  2. 选择合适的校准算法:ncnn2table 支持 KL 散度和熵两种校准方法,默认为 KL。可以尝试不同方法对比精度。

  3. 逐层反量化:某些层(比如检测头)对量化特别敏感,可以把这些层单独排除在量化之外,保持 FP32。

  4. 检查预处理是否一致:量化前后的预处理(归一化、通道顺序等必须完全一致,这是很多人忽略但影响巨大的点。

(第二部分完,约2300字)

五、C++ 推理代码编写

模型准备好了,接下来就是编写实际的推理代码。NCNN 的 API 设计得相当简洁,一个完整的推理流程只需要寥寥几行代码就能完成。

5.1 最简推理示例

#include <opencv2/opencv.hpp>
#include "net.h"

int main()
{
    // 1. 创建 Net 对象并加载模型
    ncnn::Net net;
    net.load_param("resnet18-int8.param");
    net.load_model("resnet18-int8.bin");

    // 2. 读取图片并预处理
    cv::Mat img = cv::imread("test.jpg");
    
    // Resize 到模型输入尺寸
    ncnn::Mat in = ncnn::Mat::from_pixels_resize(
        img.data, ncnn::Mat::PIXEL_BGR,
        img.cols, img.rows, 224, 224
    );

    // 归一化(ImageNet 标准参数)
    const float mean_vals[3] = {103.53f, 116.28f, 123.675f};
    const float norm_vals[3] = {0.017429f, 0.017507f, 0.017125f};
    in.substract_mean_normalize(mean_vals, norm_vals);

    // 3. 执行推理
    ncnn::Extractor ex = net.create_extractor();
    ex.set_num_threads(4);  // 设置线程数
    ex.input("input", in);

    ncnn::Mat out;
    ex.extract("output", out);

    // 4. 解析输出
    // out 是 1x1000 的向量,取最大值索引即为预测类别
    int max_idx = 0;
    float max_val = out[0];
    for (int i = 1; i < out.w; i++) {
        if (out[i] > max_val) {
            max_val = out[i];
            max_idx = i;
        }
    }

    printf("Predicted class: %d, confidence: %.4f\n", max_idx, max_val);

    return 0;
}

这段代码展示了最基本的推理流程,但还有很多细节值得深入探讨。

5.2 输入预处理的坑

预处理是最容易出问题但也最容易被忽视的环节。我见过至少一半的部署问题,最后都追溯到预处理不一致。

通道顺序:OpenCV 读进来的图片是 BGR 顺序,而 PyTorch 训练时通常是 RGB 顺序。注意上面代码中 from_pixels_resize 的第二个参数是 ncnn::Mat::PIXEL_BGR,这意味着 NCNN 会保持 BGR 顺序。如果你训练时用的是 RGB,这里应该改成 ncnn::Mat::PIXEL_BGR2RGB

归一化参数mean_valsnorm_vals 必须和训练时完全一致。很多人训练时用的是 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225],这和代码中的数值是等价的,只是转换了一下:

mean_vals = [0.485*255, 0.456*255, 0.406*255]
norm_vals = [1.0/255/0.229, 1.0/255/0.224, 1.0/255/0.225]

插值方法:NCNN 默认使用 bilinear 插值,确保和训练时的数据增强使用的插值方法一致。

5.3 线程数与性能调优

set_num_threads() 是一个非常重要的函数。线程数不是越多越好,最优值取决于你的 CPU 核心数和架构:

CPU 架构 推荐线程数
4 核 Cortex-A53 4
2 核 A72 + 4 核 A53 4 或 6
4 核 A76 + 4 核 A55 4(只绑大核)或 8

在大小核架构上,只使用大核往往比使用所有核心性能更好,因为 A53 这类小核拖慢整体速度不说,还可能因为调度开销反而降低性能。

NCNN 也支持线程绑定:

ex.set_cpu_powersave(2);  // 0=所有核 1=只小核 2=只大核

set_cpu_powersave(2) 是在 ARM 大小核设备上最常用的配置。

5.4 CMakeLists.txt 配置

最后不要忘了写 CMakeLists.txt:

cmake_minimum_required(VERSION 3.0)
project(ncnn_inference)

set(CMAKE_CXX_STANDARD 11)

# NCNN 路径
set(ncnn_DIR "/path/to/ncnn/build/install/lib/cmake/ncnn")
find_package(ncnn REQUIRED)

add_executable(ncnn_inference main.cpp)
target_link_libraries(ncnn_inference ncnn)

六、性能 Benchmark 与优化技巧

模型跑起来只是第一步,跑得多快才是关键。这一节我们来看看如何 benchmark 性能,以及有哪些优化手段。

6.1 使用 ncnn_benchmark

NCNN 自带了 benchmark 工具,可以快速测试模型在目标设备上的性能:

# 编译 benchmark 工具
cd ncnn/build-arm64
cmake .. -DNCNN_BUILD_BENCHMARK=ON
make -j4

# 将 benchmark 可执行文件和模型文件传到设备上
adb push benchmark /data/local/tmp/
adb push resnet18-int8.param /data/local/tmp/
adb push resnet18-int8.bin /data/local/tmp/

# 在设备上运行 benchmark
adb shell
cd /data/local/tmp/
./benchmark resnet18-int8.param resnet18-int8.bin 4 10 1

参数依次是:param 文件、bin 文件、线程数、warmup 次数、运行次数。

6.2 逐层性能分析

如果你想知道模型中哪些层最慢,可以开启逐层耗时统计:

ex.enable_light_mode(false);
ex.set_debug_mode(true);

运行后会打印每一层的执行时间,帮你定位性能瓶颈。

常见的性能瓶颈层:

层类型 优化方向
Convolution 用 Winograd 优化(3x3 stride 1)
DepthWise Conv 确保是 im2col+sgemm 实现
Sigmoid/HardSwish 用 fastmath 版本
Upsample 避免双线性插值,用 nearest

6.3 内存优化技巧

嵌入式设备的内存往往比性能还紧张。NCNN 提供了多种内存优化手段:

Light Mode:开启后中间张量会在不需要时立即释放,显著降低峰值内存使用:

ex.enable_light_mode(true);

FP16 存储:即使推理用 FP32,中间结果也可以用 FP16 存储,内存减半:

net.opt.use_fp16_storage = true;

Pack4 优化:对于 4 通道对齐的张量,NCNN 有特殊优化,内存访问更友好:

net.opt.use_packing_layout = true;

这些开关组合使用,通常可以将峰值内存使用降低 30-50%。

七、常见问题与解决方案

部署过程中会遇到各种各样的问题,这里总结一些最常见的坑。

7.1 推理结果不对

这是最常见也是最头疼的问题。排查思路:

  1. 检查预处理:通道顺序、均值、标准差、归一化是否和训练一致?
  2. 检查输出后处理:有没有做 Softmax?有没有 sigmoid?
  3. 逐层对比:用 PyTorch 导出某一层的输出,和 NCNN 同一层的输出对比。
  4. 检查模型转换:是不是 onnx2ncnn 时某个算子转换错了?

逐层对比是定位问题的杀手锏:

// 在 NCNN 中提取中间层输出
ncnn::Mat conv1_out;
ex.extract("conv1", conv1_out);

// 导出为文本或 numpy 数组,和 PyTorch 对比

7.2 性能不如预期

  1. 线程数是否合理:试试 1、2、4、8 线程,找最优值。
  2. 是否绑定了大核set_cpu_powersave(2) 试试。
  3. 是否开了 FP16:ARMv8.2+ 设备上 FP16 推理快很多。
  4. 模型是否经过 ncnnoptimize:BN 融合对性能影响巨大。
  5. INT8 量化是否生效:确认用的是 int8 版本的模型。

7.3 内存不足

  1. 开启 light modeex.enable_light_mode(true)
  2. 使用 FP16 storagenet.opt.use_fp16_storage = true
  3. 减小 batch size:尽量用 batch 1。
  4. 模型剪枝:对不重要的通道剪枝。

7.4 部署在内存受限的 MCU

如果是在几 MB 内存的 MCU 上部署,还需要这些额外操作:

  1. 静态分配内存:不要用动态分配,所有内存都预先分配。
  2. 权重量化到 INT8:甚至 INT4。
  3. 权重放在 Flash:运行时按需读取,不全部加载到 RAM。
  4. 逐层计算:计算完一层就释放输入,只保留输出。

八、进阶方向

掌握了基础部署后,还有很多值得深入的方向:

自定义算子实现:当 NCNN 不支持你的算子时,需要自己写 NCNN 算子。这需要了解 NCNN 的算子注册机制和内存布局。

Vulkan GPU 加速:如果设备有 Mali GPU,开启 Vulkan 后端通常能获得 2-3 倍的性能提升。但需要注意 GPU 和 CPU 之间的数据传输开销。

模型蒸馏与剪枝:量化是无损压缩,剪枝和蒸馏是有损但压缩比更高的手段。结合使用可以在精度下降可接受的前提下,获得极致的性能。

多模型流水线:实际产品中往往不是一个模型在跑,而是检测+跟踪+识别的流水线。如何在多个模型之间合理分配内存和计算资源,也是一个值得研究的课题。

总结

这篇文章从环境搭建开始,完整走过了 ONNX 导出、模型转换、INT8 量化、C++ 推理代码编写、性能 benchmark 的完整流程。回头来看,部署这件事其实没有什么特别高深的理论,更多的是工程细节的堆砌和经验的积累。

从 PyTorch 的一行 model(x) 到嵌入式设备上的 C++ 推理代码,中间隔着几十个大大小小的细节。任何一个细节出问题,都可能导致最终结果不对或者性能不达标。这也是为什么部署工程师这个岗位虽然看起来只是在"搬模型",但实际需要深厚的工程功底。

NCNN 作为一个优秀的推理框架,为我们屏蔽了很多底层的复杂性,但它不是银弹。真正把一个模型部署到产品上,还需要对网络结构、硬件架构、编译器优化、内存管理等等都有一定的理解。这正是嵌入式 AI 的魅力所在——它不是单纯的算法,也不是单纯的工程,而是两者的深度结合。

希望这篇文章能帮助你少踩一些坑,在嵌入式 AI 的道路上走得更顺一些。

(全文完,约7000字)