1. 为什么我选择从嵌入式重新切入端侧 AI1.1 一个让我下定决心的真实场景去年年底我接手了一个工业质检的小项目。客户的需求很朴素在产线终端上实时识别零件表面的划痕延迟不能超过 80 毫秒设备成本控制在千元以内而且现场没有稳定的网络环境。我第一反应是上一块 GPU 板卡但算下来功耗和散热都过不了关机箱空间也塞不进去。后来换成带 NPU 的嵌入式 SoC整机功耗压到 5W 以内成本直接砍掉三分之二。这件事让我重新审视了一个问题过去几年大家都在卷大模型、卷云端推理但真正落到产线、落到摄像头、落到车载盒子里的需求其实一直是嵌入式的地盘。端侧 AI 不是把云端的模型搬下来那么简单它要求你在算力、内存、功耗、成本四条线之间反复权衡。而 CNN恰恰是这套权衡体系里最成熟、最可控、最容易落地的一类模型结构。我决定重新从 CNN 开始不是因为它新而是因为它足够“老”老到每一个算子、每一层内存占用、每一次量化误差都有迹可循。你把这些底层的东西吃透了再去看 Transformer 的端侧部署思路是相通的。1.2 端侧 AI 和嵌入式 AI 到底差在哪很多人把这两个词混着用其实它们侧重点不一样。嵌入式 AI 更强调“跑在嵌入式系统上”关注的是交叉编译、根文件系统、驱动适配这些工程问题端侧 AI 更强调“推理发生在数据产生的那一端”关注的是模型压缩、算子融合、硬件加速这些算法与芯片协同的问题。我自己的理解是嵌入式 AI 是底座端侧 AI 是目标。你得先能把 Linux 跑起来、把 NPU 驱动加载上、把摄像头数据取出来然后才谈得上模型怎么量化、怎么调度。这也是为什么我坚持从 CNN 开始——它的计算图相对规整卷积、池化、激活、全连接每一层都能对应到 NPU 的硬件单元上调试起来心里有底。1.3 这篇文章适合谁看如果你是从纯算法转过来的想搞清楚模型怎么真正落到板子上这篇内容会帮你补齐工程侧的关键环节。如果你是从单片机、嵌入式 Linux 转过来的想搞明白 CNN 到底在算什么、量化为什么会掉点这篇也会给你一条从底层往上走的路径。我不打算写成教科书而是按我自己踩过的顺序来先讲整体设计思路再拆核心细节然后走一遍完整实操最后把常见坑列出来。你照着做至少能少走两三个月的弯路。2. 整体设计思路为什么是 CNN NPU 量化这条线2.1 模型选型为什么不是 Transformer端侧部署选模型第一原则不是精度最高而是“可预测”。CNN 的计算量集中在卷积层参数量、FLOPs、内存占用都能在编译前算得八九不离十。Transformer 的自注意力机制在端侧有个很现实的问题序列长度一变计算图和内存峰值就跟着变很多 NPU 的编译器对动态 shape 支持并不好。我实测过一个小型 ViT 和一个同量级的 MobileNet在同样的 NPU 上ViT 的首次推理延迟波动能到 40% 以上而 MobileNet 基本稳定在 5% 以内。对于产线这种要求确定性的场景稳定性比那一点点精度提升重要得多。当然这不是说 Transformer 不能上端侧而是说在你还没把 CNN 这条链路跑通之前直接上 Transformer 会让变量太多出了问题很难定位。2.2 硬件选型NPU 不是万能药现在市面上带 NPU 的嵌入式芯片不少选型时我主要看四个指标指标为什么重要我的经验阈值算力TOPS决定模型规模上限1~4 TOPS 足够跑 MobileNet 级别内存带宽决定实际推理速度低于 10GB/s 时卷积会明显受限算子支持度决定模型能否完整卸载优先选支持 Conv、DepthwiseConv、Pooling 的工具链成熟度决定调试效率有量化工具和可视化编译器的优先我踩过最大的坑是某款芯片标称 4 TOPS但 Depthwise 卷积不支持硬件加速结果 MobileNet 里占大头的深度可分离卷积全落到 CPU 上实际速度还不如一块纯 CPU 的板子。所以选型时一定要拿你的目标模型去跑一遍别只看纸面参数。2.3 量化路线为什么从 INT8 开始量化是把 FP32 的权重和激活值映射到低比特整数的过程。端侧最常用的是 INT8原因很直接NPU 的 INT8 算力通常是 FP16 的两倍甚至四倍内存占用直接降到四分之一。但量化不是免费的。我一般按这个顺序推进先训练一个 FP32 的基线模型确认精度达标。做训练后量化PTQ用一批校准数据统计激活值分布。如果掉点超过 2%再考虑量化感知训练QAT。最后在 NPU 上验证对比 CPU 上的浮点结果。这里有个细节校准数据的分布一定要贴近真实场景。我有一次用随机噪声做校准量化后模型在测试集上掉了 8 个点换成真实产线图片后只掉了 0.6 个点。校准集不是越多越好而是要“像”。3. 核心细节解析CNN 在端侧到底怎么跑3.1 卷积层的内存访问模式卷积是 CNN 的主力也是端侧最吃内存带宽的地方。一个标准的 3x3 卷积每个输出像素要读取 9 个输入像素和 9 个权重。如果按最朴素的方式实现内存访问量是计算量的好几倍。NPU 通常会用几种手段来优化行缓存Line Buffer把输入特征图按行缓存相邻输出复用同一批输入减少重复读取。权重驻留把卷积核权重放在片上 SRAM避免反复从 DDR 取。分块计算Tiling把大特征图切成小块让每一块都能塞进片上缓存。你在部署时能做的是尽量让模型的特征图尺寸和通道数对齐 NPU 的偏好。比如很多 NPU 对 4 通道或 8 通道对齐的卷积效率更高那你在设计网络时就可以把通道数设成 8 的倍数。3.2 激活函数与算子融合ReLU 这类逐元素激活单独算一次就要读写一遍特征图。NPU 编译器一般会把 Conv Bias ReLU 融合成一个算子中间结果不落 DDR。这个融合能不能生效取决于你的模型导出格式和编译器支持度。我的经验是导出 ONNX 时尽量用标准算子别自己写自定义激活。有些框架会把 ReLU6 拆成 Clip编译器识别不了就融合失败速度直接掉一截。遇到这种情况可以在导出前把激活统一成编译器明确支持的版本。3.3 量化误差从哪里来INT8 量化的误差主要来自三处权重分布不均某些通道的权重范围特别大量化后小权重全被压成 0。激活值离群点ReLU 之后的激活没有上界个别极大值会把量化范围拉宽导致整体分辨率下降。累加溢出卷积累加结果超出 INT8 范围需要 INT32 累加器如果硬件不支持就会截断。针对第一点我通常用逐通道量化Per-Channel每个卷积核单独算 scale比全局量化稳很多。针对第二点可以在激活后加 Clip 限制范围或者用移动平均统计更稳的校准阈值。3.4 数据布局NCHW 还是 NHWC这个细节很多人忽略但对性能影响很大。NCHW 是通道在前NHWC 是通道在后。CPU 上 NCHW 对某些库更友好但 NPU 和移动端 GPU 普遍偏好 NHWC因为通道连续时向量化读取更高效。我一般这样做训练时用框架默认布局导出时转成 NHWC然后在 NPU 上验证。如果转换后精度有变化再回头检查是否有算子不支持 NHWC。4. 实操过程从零把 CNN 跑到 NPU 上4.1 环境准备与工具链搭建先说明不同芯片的工具链差别很大我这里给的是通用流程具体命令你要对照自己芯片的文档。第一步装好交叉编译工具链和芯片厂商的 SDK。以常见的嵌入式 Linux 为例你需要# 安装交叉编译器示例 sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu # 解压厂商 SDK tar -xzf vendor_sdk.tar.gz -C /opt/ # 设置环境变量 export PATH/opt/vendor_sdk/bin:$PATH export LD_LIBRARY_PATH/opt/vendor_sdk/lib:$LD_LIBRARY_PATH第二步确认 NPU 驱动已经加载# 查看 NPU 设备节点 ls /dev/ | grep -i npu # 查看驱动版本 cat /sys/class/npu/version如果设备节点不存在先解决驱动问题别急着跑模型。我见过太多人模型转换失败最后发现是驱动根本没起来。4.2 模型训练与导出我用一个简化的 MobileNet 做示例输入 224x224x3输出 10 类。import torch import torch.nn as nn class SmallNet(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 16, 3, stride2, padding1), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), nn.Conv2d(16, 32, 3, stride2, padding1, groups16), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d(1) ) self.classifier nn.Linear(32, num_classes) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)训练完成后导出 ONNXmodel.eval() dummy torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, smallnet.onnx, input_names[input], output_names[output], opset_version11 )导出后一定要用 ONNX Runtime 验证一遍确认输出和 PyTorch 一致误差在 1e-4 以内。4.3 量化与编译厂商工具链一般提供量化脚本。核心是准备校准数据# 校准数据加载示例 def calibration_loader(data_dir, batch_size8): dataset MyDataset(data_dir, transformval_transform) loader DataLoader(dataset, batch_sizebatch_size, shuffleFalse) for i, (images, _) in enumerate(loader): if i 20: # 一般 100~200 张就够 break yield images.numpy()然后调用量化工具# 示例命令具体参数看厂商文档 npu_quantizer \ --model smallnet.onnx \ --calibration ./calib_data \ --output smallnet_int8.quantized \ --per-channel \ --input-layout NHWC编译成 NPU 可执行文件npu_compiler \ --model smallnet_int8.quantized \ --target npu_v2 \ --output smallnet.nb4.4 板端部署与验证把生成的文件拷到板子上写一个最小的推理程序#include npu_runtime.h int main() { npu_context_t ctx; npu_init(ctx, smallnet.nb); float input[224*224*3]; float output[10]; // 填充输入数据 load_image(test.jpg, input); npu_inference(ctx, input, output); for (int i 0; i 10; i) { printf(class %d: %.4f\n, i, output[i]); } npu_release(ctx); return 0; }编译并运行aarch64-linux-gnu-gcc infer.c -o infer -lnpu_runtime scp infer rootboard:/root/ ssh rootboard ./infer验证时重点看三件事输出是否合理、单帧延迟是否达标、连续跑一小时有没有内存泄漏。4.5 性能调优的几个抓手跑通之后如果速度不达标按这个顺序排查看算子分布用厂商的 profiler 看哪些层落在 CPU 上优先把 CPU 层挪回 NPU。调 batch 和线程端侧一般 batch1但推理线程数可以调找到吞吐和延迟的平衡点。检查内存拷贝输入数据从 CPU 到 NPU 的拷贝往往是隐藏瓶颈能用零拷贝就用零拷贝。降分辨率如果业务允许输入从 224 降到 160速度能提升接近一倍。5. 常见问题与排查技巧实录5.1 量化后精度掉得厉害怎么办先别急着上 QAT按这个顺序查校准集是不是和真实数据分布差太远换成真实场景图片再试。有没有用逐通道量化没有的话先打开。第一层和最后一层是不是也量化了这两层对精度敏感可以保留 FP16。有没有 BN 折叠BN 没折叠进卷积会引入额外误差。我遇到过一次掉点原因是校准图片全是白天场景而测试集有大量夜间图片。补了夜间样本后掉点从 5% 降到 0.8%。5.2 NPU 推理结果和 CPU 对不上这种问题一般是三个原因现象可能原因排查方法整体偏移量化 scale 不一致对比量化前后的 scale 参数个别类别错算子融合顺序不同关闭融合逐层对比输出全为 0输入布局错误检查 NCHW/NHWC 是否匹配我习惯的做法是先用一张固定图片在 CPU 和 NPU 上各跑一遍把中间层输出 dump 出来逐层对比哪一层开始分叉问题就在那一层附近。5.3 板端跑一段时间就卡死大概率是内存问题。端侧内存本来就紧张常见原因有每次推理都重新申请内存没释放。输入输出 buffer 没有复用。NPU 和 CPU 共享内存时出现竞争。我的做法是推理上下文只初始化一次输入输出 buffer 预分配好循环里只做数据填充和推理调用。改完之后连续跑 24 小时没再出问题。5.4 工具链报错但看不懂厂商工具链的报错信息往往很晦涩。我的经验是先把模型简化到最小可复现比如只有一层卷积看能不能过。逐步加层定位到具体哪一层不支持。查厂商的算子支持列表确认版本匹配。实在不行把 ONNX 用 Netron 打开看有没有异常节点。有一次编译一直失败最后发现是 ONNX 里有个 Identity 节点没被优化掉手动去掉就过了。5.5 常见问题速查表问题快速定位解决方向精度掉点对比逐层输出换校准集、开逐通道、保留首尾 FP16速度不达标profiler 看算子分布挪回 NPU、降分辨率、零拷贝结果不一致检查布局和 scale统一 NHWC、对齐量化参数运行崩溃查内存申请释放预分配 buffer、复用上下文编译失败最小模型复现查算子支持、清理冗余节点6. 我在嵌入式端侧 AI 这条路上的一些体会从嵌入式转到端侧 AI最大的门槛不是算法而是“把两套语言打通”。你得能看懂模型结构也得能看懂芯片手册你得会调 Python也得会看 C 和汇编你得知道量化在数学上做了什么也得知道 NPU 在硬件上怎么实现。我自己的学习路径是先用一块带 NPU 的开发板把官方 demo 跑通然后换成自己的模型再然后优化性能最后回头补理论。这个顺序比先啃论文再动手要快得多因为每一步都有即时反馈你知道自己卡在哪。CNN 只是一个起点。等你把卷积、量化、算子融合这套东西摸熟了再去看检测、分割、甚至轻量级注意力模型会发现底层逻辑是相通的。端侧 AI 这个方向拼的不是谁模型新而是谁对“算力、内存、功耗、成本”这四件事理解得更透。最后分享一个小技巧每次部署新模型前先用一张图片在 CPU 上跑一遍把每一层的输出 shape 和数值范围记下来。等 NPU 上出问题的时候这份记录就是你的对照表能省掉大量瞎猜的时间。