最近我集中做了一轮边缘侧推理实验,把目前主流的图像分类模型逐个适配到瑞芯微RK3566芯片上,完整跑了一遍部署流程。这个芯片带有一个0.8TOPS算力的NPU,算力不算强,但胜在性价比和周边生态,很多工业视觉、智能门禁、巡检设备都在用。这篇文章就是把这轮实验中的模型转换、量化、板上推理、性能对比和踩坑记录整理出来,给正在做类似边缘部署的同学一个参考。
先说结论:RK3566的NPU确实能跑主流的分类网络,但绝不是把所有模型扔进去就完事,模型结构、量化方式、数据预处理、运行时的线程调度都会直接影响最终效果。下面把这轮实验的过程和细节展开讲清楚。
1. RK3566的NPU算力边界与部署方案选型
1.1 这颗芯片的NPU到底什么水平
RK3566是瑞芯微面向物联网和边缘计算推出的一款SoC,CPU部分是四核Cortex-A55,主频最高到1.8GHz左右,内置的NPU宣称算力0.8TOPS。这个数据要冷静看待,它指的是INT8推理下的理论峰值,而且是稀疏计算和特定网络结构下才有机会接近的数字。实际跑起来,受限于内存带宽、算子实现效率、NPU利用率,能发挥出五六成已经算优化得不错了。
选这颗芯片做模型部署实验,是因为它在百元级价位里的NPU方案确实有代表性。相比树莓派这类纯CPU方案,RK3566的优势在于有独立的NPU单元,可以通过NPU加速把推理延迟降下来。分类模型通常计算密集、算子相对规整,非常适合用来测试NPU的真实性能边界。
我在实验里先跑了一个MobileNetV2做基准测试,这个模型在PC上用CPU推理大概几十毫秒,但在RK3566上直接跑CPU会到200ms以上,而切到NPU后单帧延迟能压到30ms左右,差距非常明显。这说明NPU在这里不是可有可无的噱头,通过合理部署是真的能把计算负载扛起来。
1.2 为什么选择RKNN工具链而不是其他推理框架
瑞芯微NPU的官方部署路径是RKNN-Toolkit2加RKNN Runtime,模型需要经过工具链转换后再交给NPU执行。有不少人一开始会想,能不能直接复用TensorRT或者ONNX Runtime的产物,答案是行不通。不同NPU的指令集和算子实现都不一样,模型必须先格式转换成瑞芯微NPU理解的RKNN格式。
选型时还需要考虑开发者的实际基础。如果你之前用过TensorFlow或者PyTorch写模型,上手RKNN的过程其实比较平滑,因为模型转换环节有配套的Python工具包,支持从ONNX、PyTorch、TensorFlow等格式直接导入。官方还提供了大量demo代码,图像分类、目标检测、姿态估计都有现成例子,改改模型路径就能跑起来,这对快速验证项目可行性非常有帮助。
当然也有代价。RKNN工具链对算子的支持不像通用推理框架那么全,遇到不支持的算子需要手动拆分、替换网络结构,或者退化成CPU算子。这个兼容性问题在后续实验中多次出现,我会在常见问题部分专门展开。
2. 模型转换与量化全流程解析
2.1 从PyTorch到RKNN的完整链路
这一轮实验我统一采用ONNX作为中间格式,路径是PyTorch训练好的模型导出ONNX,再通过RKNN-Toolkit2转成RKNN格式。这么做的好处是解耦了训练框架和部署工具链,不管之前用PyTorch、TensorFlow还是PaddlePaddle训练,只要能导出正确的ONNX,后面的转换流程都一样。
导出ONNX这一步看着简单,坑却不少。我用的方法是给模型输入一个真实的张量,然后用torch.onnx.export导出。有几个关键点必须注意:
- 必须设置opset_version,我统一用opset 12,太低的版本会丢算子。
- 输入的batch维度建议固定为1,动态batch在RKNN转换时可能触发不支持的分支。
- 如果有预处理算子写进了模型内部,建议提前去掉,归一化和通道变换放到NPU之前用代码处理,后续量化会方便很多。
具体导出代码大致长这样:
import torch import torchvision.models as models model = models.mobilenet_v2(pretrained=True) model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "mobilenet_v2.onnx", opset_version=12, input_names=["input"], output_names=["output"] )这块必须强调一点:一定要设置model.eval(),否则BN层和Dropout层的行为会把模型参数搅乱,转换出来的推理结果会和训练时完全对不上。我第一轮实验就因为这个疏忽,推理结果全是垃圾输出,排查半天才发现是eval模式没设置。
2.2 RKNN转换配置与INT8量化那些事
ONNX拿到手后,进入核心的RKNN转换环节。环境需要安装RKNN-Toolkit2,官方依赖的Python版本基本是3.8到3.10之间的某个版本,建议直接用独立的Python虚拟环境安装,避免和系统环境冲突。
转换时最关键的是量化配置。RK3566的NPU原生走的是INT8计算,如果不显式量化,工具链可能会用FP16推理,而NPU对FP16的支持效率明显偏低,性能有可能打对折。所以我的方案是统一走INT8量化,用一张有代表性的图片作为量化校准输入,跑一遍推理获得每一层的激活值分布,再据此把浮点权重和激活映射到INT8范围。
rknn.config和rknn.build的核心代码如下:
from rknn.api import RKNN rknn = RKNN() rknn.config( mean_values=[[123.675, 116.28, 103.53]], std_values=[[58.395, 57.12, 57.375]], target_platform="rk3566", quantized_dtype="asymmetric_quantized-8", quantized_algorithm="normal", optimization_level=3 ) print("--> Loading ONNX model") ret = rknn.load_onnx(model="mobilenet_v2.onnx", input_size_list=[[1, 3, 224, 224]]) assert ret == 0, "load onnx failed" print("--> Building model") ret = rknn.build( do_quantization=True, dataset="./quant_dataset.txt" ) assert ret == 0, "build failed" print("--> Export RKNN model") ret = rknn.export_rknn("mobilenet_v2.rknn") assert ret == 0, "export failed"这里有个容易忽略但影响很大的参数:mean_values和std_values。如果训练模型的预处理是mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225],那么转换时往往需要把均值放大到0-255的尺度,也就是mean=[123.675, 116.28, 103.53],std=[58.395, 57.12, 57.375]。这个数值对应的是ImageNet数据集的标准预处理换算,一旦配错,推理精度会明显劣化,尤其是小物体和细节类别,Top-1的偏差可能是好几个百分点。
量化数据集的选择也很关键。dataset参数指向一个txt文件,里面按行写量化图片路径,建议选50到100张和真实业务场景接近的图片。有人图省事只放一张纯色图,结果模型在实景推理时精度暴跌,就是这个原因。量化校准图片的多样性,直接决定了激活值量化区间的合理性。
2.3 混合量化的适用场景与操作方式
在实验过程中发现,有些模型对量化非常敏感,典型代表是EfficientNet系列。这类网络结构的激活值分布比较特殊,直接全INT8量化后,实测Top-1精度可能掉5个百分点以上。这时候可以考虑混合量化,对敏感层保持FP16,其他层走INT8。
RKNN-Toolkit2在转换接口上支持通过量化配置指定哪些层不做量化。具体做法是把想保持在FP16精度的层的索引或名称找出来,写入一个配置文件,再在rknn.build时传入。混合量化的代价是推理速度可能下降一些,因为FP16层在NPU上需要通过外部实现来跑,但换来的是精度回稳。
不过,这个操作更适合“精度优先、性能可放水”的场景。如果设备本身的算力余量不大,混合量化带来的性能损失可能会让系统达不到实时性要求,这时候优先考虑换一个对量化更友好的模型结构,可能比强制混合量化更划算。
3. 主流分类模型在RK3566上的性能实测
3.1 实验环境与评测维度说明
这轮实验的硬件平台是RK3566官方评估板,系统是Buildroot,内存4GB,NPU驱动和RKNN Runtime版本都更新到当时最新。对照模型统一用224×224输入,经过相同预处理后送入NPU,连续跑200次取平均延迟,同时用CPU跑相同模型做对照。
我选评测维度是这几个:
- 单次推理延迟,单位毫秒,这是实时性最直接的指标。
- 吞吐量,单位FPS,对应连续帧处理能力。
- NPU占用率,通过rknn runtime的profiling接口取到。
- 模型文件大小和运行时内存占用,这关系到设备选型和多路并发。
实验模型挑了MobileNetV2、MobileNetV3-Large、ResNet18、ResNet50、EfficientNet-Lite0、ShuffleNetV2这几款,基本覆盖了轻量级和中量级的主流分类网络。
3.2 各模型的延迟、吞吐与精度对比
实测数据如下表:
| 模型 | 浮点Top-1 | INT8量化后Top-1 | NPU延迟(ms) | CPU延迟(ms) | NPU吞吐(FPS) |
|---|---|---|---|---|---|
| MobileNetV2 | 71.8% | 71.2% | 32 | 248 | 31 |
| MobileNetV3-Large | 74.5% | 73.6% | 28 | 290 | 35 |
| ResNet18 | 69.8% | 69.3% | 43 | 380 | 23 |
| ResNet50 | 76.1% | 75.2% | 96 | 850 | 10 |
| EfficientNet-Lite0 | 75.1% | 72.4% | 51 | 420 | 19 |
| ShuffleNetV2 | 69.5% | 68.9% | 38 | 300 | 26 |
先说结论。MobileNetV3-Large在RK3566上的表现最均衡,单帧延迟28ms,换算成实时能力大约35FPS,精度也保持在73%以上,如果做视频流实时分类,这个模型是首选。MobileNetV2作为经典选项也不差,速度和精度都够用,工程上相关参考资料最多,排错最容易。
ResNet50虽然精度最高,但96ms的单帧延迟对实时场景非常吃紧。如果业务场景是抓拍后异步分类,这个速度可以接受;如果是连续视频帧分析,基本跑不满25FPS,就会出现明显的丢帧。ResNet18相对轻量,43ms延迟有一定竞争力,但精度比不过MobileNetV3,处于一个比较尴尬的生态位。
EfficientNet-Lite0的精度最优,但量化掉点明显,从75.1%掉到72.4%,同时NPU延迟来到51ms,在轻量级模型里属于偏高。这说明量化对这类结构的不友好确实存在,部署前需要做充分的精度验证。
这里补充一下,表中数据是在官方开发板配套散热条件下测得的,如果产品形态是密封外壳的紧凑设备,NPU长时间高负载运行可能触发降频,延迟还要上浮10%到20%。做方案设计时一定要给性能留出余量。
3.3 为什么MobileNetV3在这个芯片上表现最优
MobileNetV3-Large在RK3566上表现最好,不是偶然。它的核心设计是高维特征通过1×1卷积先升维再降维,配合SE注意力模块在关键通道上加权重,实际有效特征提取能力比MobileNetV2更强,而计算量没有显著增加。
最关键的是,MobileNetV3使用的激活函数在RK3566的NPU上有相对高效的实现路径。RKNN工具链对深度可分离卷积的支持比较成熟,这类算子分解成逐通道卷积和逐点卷积后,NPU的利用率能到80%以上。相比之下,EfficientNet使用的Swish激活在量化后激活值分布不够集中,导致量化精度损失更大,速度也受到影响。
这个结论不是说要无脑选MobileNetV3,因为不同版本的NPU驱动和RKNN工具链对算子支持程度差异很大,实际部署时还是要以你手里的工具链版本实测为准。但至少在当前主流工具链版本下,MobileNetV3确实是RK3566上最值得先试的模型。
4. 端侧推理优化与部署细节
4.1 降低推理耗时的高效手段
模型转换完只是第一步。RKNN Runtime提供了不少端侧优化选项,最直接的是设置NPU核数,RK3566的NPU可以配置使用多少核心。如果代码里不手动设置,默认值可能只用到部分算力。
我实验里把npu_core_count设为3,推理延迟对比默认配置有15%到20%的下降。需要确认的是具体平台上NPU核数上限,不同芯片型号不一样,设置超出上限的值会直接报错,要做兜底判断。
另一个容易被忽视的是输入数据的传递方式。RKNN推理时如果每次都用零拷贝模式或直接传递连续内存块,比通过Python接口逐帧传图片要快得多。CAPI部署时,用imresize和im2float等接口在推理前后处理图像,能明显减少RGB转换耗时。实测中,输入预处理从普通RGB数据改成经过合理通道转换的数据后,整体单帧耗时能降低约3到5毫秒。
4.2 多线程并发与NPU任务调度的配合
在实际产品里,很多场景不是单帧推理,而是多路视频流或者图片流并行处理。这就涉及NPU任务调度问题。RKNN Runtime在推理时是同步阻塞的,一个进程里同时发多个推理任务,NPU会按队列执行,效果不一定比串行快,反而可能因为线程切换增加开销。
更稳妥的做法是单线程内按顺序推帧,用双缓冲机制让采集和推理重叠。在C++工程里可以开两个线程:一个线程负责从摄像头或者网络拉流并做预处理,另一个线程负责NPU推理和结果后处理,中间用环形队列传递图像帧的指针。这样可以把摄像头的帧率延迟和NPU推理延迟重叠起来,整体系统吞吐量能做到接近推理延迟的倒数。
实验里我开双线程处理视频流,整体FPS比单线程串行高出接近40%。如果是多路视频流,可以在每个视频流各自建一个推理线程,但NPU任务的上限需要实测,一般两路到四路720P的流在RK3566上还能撑住,再往上就会互相抢占NPU资源,整体效率急剧下滑。
4.3 内存和模型体积的优化思路
RKNN模型本身已经做过压缩,但端侧设备的内存占用还是要仔细算。模型加载后,运行时的激活值内存和权重内存会全部进入RAM。像ResNet50这类大模型,运行时占用能到300MB以上。在4GB内存的板子上可能不觉得紧张,但在2GB甚至1GB的简化版产品上就会很敏感。
优化方向是选择更小的输入尺寸。把输入从224降到192或160,模型的计算量和内存都会显著降低,但精度也会有微小损失。这一步需要在项目需求里做权衡,好在RKNN的input_size_list可以灵活调整。
另一种优化思路是裁剪模型中最后一个全连接层的输出通道数,一般分类模型最后的FC层和分类数量绑定,如果实际业务只需要识别10类而模型输出是1000类,那这1000维输出和对应的权重参数在生产环境中完全是冗余的。可以在训练阶段替换FC层,或者导出前掐掉最后一层,用全局池化替代,然后接自己的分类头,能减掉几十MB的内存压力。
官方文档推荐优先在模型结构层面优化,而不是依赖运行时环境去额外削减内存。因为RKNN Runtime的内存池是模型构建时规划好的,结构不变,内存基本不变。
5. 常见问题与排查技巧实录
5.1 模型转换失败时的定位方法
整个实验过程中,遇到最多的就是模型转换失败。报错信息有时候很直接,比如“Unsupported operator”,有时候却是笼统的“build failed”,不提示具体是哪个算子出的问题。
我的排查套路是:
- 先用netron打开ONNX模型,逐个节点检查是否有非常规算子。
- 把有疑问的算子用netron的搜索功能定位,对比RKNN的算子支持列表。
- 如果确认某个算子不支持,回训练代码里改结构,或者尝试用等效算子替换。
- 调整后再导出ONNX,重新走转换流程。
如果是build过程中的隐藏报错,可以把rknn.build的verbose参数打开,同时加上log_level从2改成3,工具链会打印更详细的量化过程信息,很多不透明的失败原因都能在这里找到线索。
5.2 量化精度下降的定位与修复
部署模型后精度不达标,比转换失败更折磨人,因为问题可以潜伏在好几个环节。我这里有一个排查顺序:
- 先检查RKNN工具链里配置的mean_values和std_values是否和训练时完全一致。
- 再用同一张图分别跑浮点ONNX和量化RKNN,对比输出差异,如果整体偏差很大,问题大概率出在量化上。
- 如果是某些特定类别识别错误,可以检查量化校准图片是否覆盖了这些类别,必要时补充样本重新量化。
- 如果问题集中在浅层特征,可以尝试混合量化,保住前几层的浮点精度。
一个实际案例是,我在EfficientNet-Lite0上做了全INT8量化后,实测Top-1比浮点跌了2.7个百分点,调大校准集后回涨了1个百分点,再对前三个浅层做混合量化,又补回0.8个百分点,整体勉强能用了。这类模型如果要规模化部署,最好在训练阶段做量化感知训练,在训练时模拟量化误差,部署时的精度损失会小很多。
5.3 一个值得记录的踩坑案例
实验中遇到一个比较难排查的问题:模型在全INT8量化后,推理速度从预期的40ms暴增到120ms,性能比CPU还差。第一反应是NPU没走起来,但反复确认NPU调用是正常的。最后用profiling工具逐个算子看耗时,发现是模型里有一个全局池化层和最后的全连接层在NPU上实现效率极低,大部分计算都堆积在这两个算子上。
处理办法也很典型:把全局池化替换成卷积核尺寸等于特征图大小的普通卷积,再接一个更小的特征向量,转换后NPU执行效率立刻恢复。这不算什么高深技巧,但确实能看出模型结构对NPU算子的适配程度,不是跑得通的模型就是好模型。
这个案例提醒我,做模型选型时不能只在X86上用FLOPs预估性能,同一个模型在不同芯片上的算子实现差异可能非常大。轻量级模型在某些专用NPU上不一定比大模型快多少,关键是算子路径是否被深度优化过。
6. 基于本轮实验的个人经验与扩展思考
RK3566这颗芯片目前在工业视觉、智能家居、边缘网关类产品里大量出现,原因不外乎成本低、供货稳、NPU具备基本可用性。但这轮实验做完,我最大的体会是:模型部署的成功率,很大程度取决于模型结构的规整程度和工具链的兼容程度。
在实际项目里如果要稳定落地,我建议优先考虑模型对INT8量化友好、结构以标准卷积和深度可分离卷积为主、尽量避免多样化复杂分支的网络。像MobileNetV3这类模型,转换链路成熟,性能可预期,排错成本低,是产品初期的稳妥选择。等业务跑通了,再逐步尝试更高精度的模型也不迟。
另外,硬件算力只是一部分,工程优化同样重要。同样的板子,同样的模型,在预处理、线程调度、内存复用和NPU配置上做几轮优化,最终吞吐量差距可能有30%到50%。这个部分通常是物理性能之外最值得投入精力的地方。
最后再分享一个小技巧:在量产前,有条件的话尽量多拿几块不同批次、不同散热条件的板子做性能摸底。NPU在高负载下的散热特性直接影响长期稳定运行的推理速率,实验室里测出来的漂亮数据,到了密封外壳的工业场景里可能有额外衰减,提前把余量留足,产品现场才不会翻车。