ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RK3576 NPU部署MobileNet实战:从模型转换到板端推理全链路

RK3576 NPU部署MobileNet实战:从模型转换到板端推理全链路 1. 为什么选择RK3576跑MobileNet从算力账本说起RK3576这颗芯片最近在边缘计算圈子里讨论度很高我自己拿到开发板之后第一件事就是盘算它的NPU到底能扛多少活。它内置的NPU标称算力是6 TOPS这个数字放在几年前是服务器级别的现在被塞进了一块功耗只有几瓦的板子里。但算力标称值只是纸面参数真正决定你能不能跑起来、跑得顺不顺的是内存带宽、算子支持度和工具链成熟度这三件事。MobileNet这个模型系列从v1到v3核心思路都是用深度可分离卷积替换标准卷积把参数量和计算量压下来。以MobileNetV1为例标准卷积的计算量是卷积核尺寸乘以输入通道乘以输出通道乘以特征图尺寸而深度可分离卷积把它拆成深度卷积和逐点卷积两步计算量能降到原来的八分之一到九分之一。这个特性让它天然适合RK3576这种边缘设备——NPU的算力虽然不算顶级但MobileNet的计算密度低两者匹配度很高。我见过不少人拿到开发板之后直接拿PyTorch的预训练模型往NPU上扔结果要么是算子不支持要么是精度掉得厉害。问题出在中间少了一步模型转换和量化。RK3576的NPU只认RKNN格式的模型你需要用RKNN-Toolkit2把ONNX或者TensorFlow模型转成RKNN这个过程里涉及到量化策略选择、算子映射、内存布局调整等一系列操作。每一步都有坑而且坑和坑之间是串联的前面没处理好后面一定出问题。这篇内容适合两类人一类是刚拿到RK3576开发板、想跑通第一个NPU推理的新手另一类是在其他平台上跑过模型、但第一次接触RKNN工具链的开发者。我会从环境搭建开始把模型转换、量化校准、板端部署、性能调优这条链路完整走一遍代码和命令都给全你照着敲就能复现。中间遇到的那些“文档里没写但实际会卡住”的地方我也会重点标出来。提示RKNN-Toolkit2的版本和RK3576的NPU驱动版本必须匹配版本不对会出现模型加载失败或者推理结果异常。建议先确认开发板固件里的NPU驱动版本再选择对应版本的Toolkit。2. 开发环境搭建PC端与板端的双线操作2.1 PC端RKNN-Toolkit2的安装与版本对齐RKNN-Toolkit2跑在x86的Ubuntu上官方推荐Ubuntu 20.04或22.04。我实测下来22.04更稳20.04在某些Python版本组合下会出现依赖冲突。安装方式有两种pip直接装或者用Docker镜像。新手我建议用pip因为出问题的时候排查路径短老手可以用Docker环境隔离更干净。pip安装的命令行是这样的pip install rknn-toolkit2 -i https://pypi.tuna.tsinghua.edu.cn/simple但这里有个关键点你不能直接装最新版。RK3576的NPU驱动在固件里是固定版本Toolkit2的版本必须和它对应。查看板端驱动版本的方法是cat /sys/kernel/debug/rknpu/version输出会类似RKNPU driver version: 0.9.6这样的信息。然后你去RKNN-Toolkit2的发布页面找对应版本。我这次用的是1.6.0版本对应驱动0.9.6。如果你装错了版本后面转换模型的时候会报Unsupported RKNN model version之类的错误。安装完成之后验证一下from rknn.api import RKNN print(RKNN().version)能打印出版本号就说明PC端环境OK了。2.2 板端运行环境的确认与依赖补齐板端这边RK3576通常预装了Linux系统NPU驱动已经在内核里了。你需要确认的是librknnrt.so这个运行时库是否存在find / -name librknnrt.so 2/dev/null正常情况会在/usr/lib/下面。如果没有需要从SDK里拷贝对应的so文件过去。这个库是板端推理程序链接的目标缺了它编译出来的可执行文件跑不起来。另外板端还需要确认Python环境。如果你打算用Python在板端跑推理需要安装rknn-toolkit-lite2pip install rknn-toolkit-lite2但注意板端的Python版本可能和PC端不一致lite2的wheel包需要选对aarch64架构的版本。我遇到过板端Python是3.8、PC端是3.10的情况这时候PC端转换好的模型在板端加载没问题但如果你在板端做后处理numpy的版本差异可能导致数组操作行为不一致。注意PC端和板端的Python版本不需要完全一致但numpy的major版本最好对齐。我踩过一次坑PC端numpy 1.24板端numpy 1.19模型输出在板端reshape的时候维度顺序对不上排查了半天才发现是numpy版本差异导致的。2.3 交叉编译工具链的准备板端算力有限直接在板子上编译推理程序会很慢。常规做法是在PC上用交叉编译工具链编译好可执行文件再拷贝到板端运行。RK3576是ARM Cortex-A72A53的大小核架构需要aarch64的交叉编译工具链。Ubuntu下安装sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu验证aarch64-linux-gnu-gcc --version编译的时候指定交叉编译器aarch64-linux-gnu-gcc -o test test.c -lrknnrt -lpthread这里-lrknnrt链接的就是前面说的板端运行时库。但PC上没有这个库所以你需要从板端把librknnrt.so和对应的头文件拷贝到PC的某个目录编译时用-I和-L指定路径。我一般会在PC上建一个rknn_sdk目录结构如下rknn_sdk/ ├── include/ │ └── rknn_api.h ├── lib/ │ └── librknnrt.so └── examples/ └── mobilenet_demo.c编译命令变成aarch64-linux-gnu-gcc -I./rknn_sdk/include -L./rknn_sdk/lib -o mobilenet_demo mobilenet_demo.c -lrknnrt -lpthread这样编译出来的可执行文件拷到板端就能直接跑。3. MobileNet模型转换从ONNX到RKNN的完整链路3.1 模型获取与ONNX导出我这次用的是MobileNetV1输入尺寸224x2241000类分类。你可以从PyTorch的torchvision里直接拿预训练模型import torch import torchvision.models as models model models.mobilenet_v1(pretrainedTrue) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, mobilenet_v1.onnx, input_names[input], output_names[output], opset_version11 )opset_version选11是有讲究的。RKNN-Toolkit2对opset 11的支持最完善opset 12以上有些算子会映射失败。我试过opset 13转换的时候报Unsupported ONNX op: Resize降到11就过了。导出之后用onnxsim简化一下pip install onnxsim onnxsim mobilenet_v1.onnx mobilenet_v1_sim.onnx简化能去掉一些冗余的算子减少转换时的算子映射工作量。但注意简化有时候会改变计算图的拓扑结构如果简化后转换报错就退回用原始ONNX。3.2 RKNN转换脚本的编写与量化策略选择转换脚本的核心是RKNN对象的初始化、配置、加载、构建、导出这五步。我直接给完整代码from rknn.api import RKNN rknn RKNN(verboseTrue) # 配置 rknn.config( mean_values[[123.675, 116.28, 103.53]], std_values[[58.395, 57.12, 57.375]], target_platformrk3576, quantized_dtypeasymmetric_quantized-8, optimization_level3 ) # 加载ONNX ret rknn.load_onnx(modelmobilenet_v1_sim.onnx) if ret ! 0: print(Load ONNX failed) exit(ret) # 构建 ret rknn.build(do_quantizationTrue, dataset./dataset.txt) if ret ! 0: print(Build failed) exit(ret) # 导出 ret rknn.export_rknn(./mobilenet_v1.rknn) if ret ! 0: print(Export failed) exit(ret) rknn.release()这里有几个关键参数需要展开说。mean_values和std_values是预处理参数。RKNN在推理时会自动做归一化你填的这三个值对应ImageNet的均值和标准差。注意顺序是RGB如果你的模型训练时用的是BGR这里要调整。填错了不会报错但推理结果会完全不对这是新手最容易踩的坑之一。quantized_dtype选asymmetric_quantized-8是8位非对称量化。RK3576的NPU对非对称量化的支持更好精度损失比对称量化小。如果你对精度要求极高可以用float16但推理速度会慢一倍左右而且模型体积翻倍。optimization_level3是最高优化级别会做一些算子融合和内存复用。实测下来level 3比level 1的推理速度快15%左右但转换时间会长一些。dataset.txt是量化校准数据集里面每行是一张图片的路径。校准集的数量建议在100到500张之间太少会导致量化精度下降太多转换时间会很长。我一般用200张从训练集里随机抽覆盖各种类别。3.3 量化校准数据集的准备与精度验证校准集的图片需要做和推理时一样的预处理resize到224x224保持RGB通道顺序。但不需要做归一化因为RKNN会在量化时自己处理。from PIL import Image import os img_dir ./calibration_images output_txt ./dataset.txt with open(output_txt, w) as f: for img_name in os.listdir(img_dir): if img_name.endswith(.jpg) or img_name.endswith(.png): img_path os.path.join(img_dir, img_name) img Image.open(img_path).convert(RGB) img img.resize((224, 224)) save_path os.path.join(./calibration_resized, img_name) img.save(save_path) f.write(save_path \n)转换完成之后一定要做精度验证。RKNN-Toolkit2提供了accuracy_analysis接口rknn.accuracy_analysis( inputs[./test_image.jpg], output_dir./snapshot, targetrk3576 )这个命令会生成每一层的量化误差分析输出在snapshot目录下。重点看cosine_similarity这一列如果某一层的余弦相似度低于0.9说明这一层的量化误差偏大可能需要调整量化策略或者把这一层设为浮点。我实测MobileNetV1在8位量化下Top-1精度从71.8%掉到70.5%左右掉1.3个百分点这个损失在可接受范围内。如果你发现掉得超过3个百分点检查校准集是否覆盖了足够的类别多样性。提示量化校准集的图片分布要和实际推理场景匹配。如果你实际场景是室内场景分类校准集里全是风景照量化后的模型在室内场景上精度会明显下降。这个细节文档里不会强调但实际项目中很关键。4. 板端部署与推理从可执行文件到实际跑通4.1 C推理程序的完整实现板端推理用C写性能最好我给出核心代码框架。完整的代码比较长这里展示关键部分#include stdio.h #include stdlib.h #include string.h #include rknn_api.h #define MODEL_PATH ./mobilenet_v1.rknn #define INPUT_SIZE 224 int main() { // 读取模型文件 FILE *fp fopen(MODEL_PATH, rb); fseek(fp, 0, SEEK_END); int model_size ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char *model_data (unsigned char *)malloc(model_size); fread(model_data, 1, model_size, fp); fclose(fp); // 初始化RKNN rknn_context ctx; int ret rknn_init(ctx, model_data, model_size, 0, NULL); if (ret 0) { printf(rknn_init failed: %d\n, ret); return -1; } // 查询输入输出属性 rknn_input_output_num io_num; rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, io_num, sizeof(io_num)); printf(input num: %d, output num: %d\n, io_num.n_input, io_num.n_output); // 准备输入数据 rknn_input inputs[1]; memset(inputs, 0, sizeof(inputs)); inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].size INPUT_SIZE * INPUT_SIZE * 3; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf input_data; // 预处理好的图片数据 // 设置输入 ret rknn_inputs_set(ctx, 1, inputs); // 运行推理 ret rknn_run(ctx, NULL); // 获取输出 rknn_output outputs[1]; memset(outputs, 0, sizeof(outputs)); outputs[0].want_float 1; ret rknn_outputs_get(ctx, 1, outputs, NULL); // 后处理找最大值 float *output_data (float *)outputs[0].buf; int max_idx 0; float max_val output_data[0]; for (int i 1; i 1000; i) { if (output_data[i] max_val) { max_val output_data[i]; max_idx i; } } printf(Predicted class: %d, score: %f\n, max_idx, max_val); // 释放 rknn_outputs_release(ctx, 1, outputs); rknn_destroy(ctx); free(model_data); return 0; }编译命令前面已经给过了。这里重点说几个容易出问题的地方。rknn_init的第四个参数是flags传0表示默认。如果你需要多线程推理可以传RKNN_FLAG_PRIORITY_HIGH之类的标志但一般单线程就够了。输入数据的格式RKNN_TENSOR_UINT8表示输入是0-255的整数RKNN内部会自动做归一化。如果你传的是已经归一化好的float数据type要改成RKNN_TENSOR_FLOAT32同时size要乘以4。我建议传UINT8让RKNN做归一化这样PC端和板端的预处理逻辑一致减少出错概率。want_float1表示输出要转成float。如果你设成0输出是量化后的int8数据需要自己做反量化比较麻烦。设成1的话RKNN会自动反量化精度也够用。4.2 输入预处理的细节与常见错误输入图片的预处理包括resize、颜色空间转换、通道顺序调整。RKNN的输入格式是NHWC也就是通道在最后。如果你用OpenCV读图默认是BGR需要转成RGBcv::Mat img cv::imread(test.jpg); cv::cvtColor(img, img, cv::COLOR_BGR2RGB); cv::resize(img, img, cv::Size(224, 224));然后直接把img.data赋给inputs[0].buf就行。这里有个坑OpenCV的Mat数据是行连续的但如果你做了ROI裁剪或者用了非连续的内存数据布局会不对。保险的做法是cv::Mat img_resized; cv::resize(img, img_resized, cv::Size(224, 224)); img_resized img_resized.clone(); // 确保内存连续clone()会强制拷贝一份连续内存避免后续推理时数据错位。另一个坑是图片的宽高比。直接resize到224x224会拉伸图片如果原始图片不是正方形物体会变形影响分类精度。常规做法是先按短边缩放再中心裁剪int h img.rows, w img.cols; int short_side std::min(h, w); float scale 224.0 / short_side; cv::resize(img, img, cv::Size(w * scale, h * scale)); int x (img.cols - 224) / 2; int y (img.rows - 224) / 2; cv::Rect roi(x, y, 224, 224); img img(roi).clone();这样能保持宽高比分类精度会好一些。4.3 推理性能的实测数据与瓶颈分析我在RK3576上实测MobileNetV1的推理耗时单次推理包括预处理和后处理大约在8到12毫秒之间纯NPU推理时间在5到7毫秒。这个数据是用rknn_run前后打时间戳测的不包括图片读取和显示。影响推理速度的因素有几个。首先是NPU的频率RK3576的NPU默认跑在最高频率但如果你开了省电模式频率会降下来推理时间可能翻倍。查看当前频率cat /sys/class/devfreq/fdab0000.npu/cur_freq如果发现频率偏低可以手动设成performance模式echo performance /sys/class/devfreq/fdab0000.npu/governor其次是内存带宽。MobileNetV1的参数量是4.2M量化后模型文件大约4.3MB推理时的中间特征图占用内存也不大。但如果你的系统同时跑着其他吃内存的任务NPU访问内存的延迟会增加推理时间会波动。建议在推理前确认系统负载top -n 1 | head -5如果CPU占用率很高可以考虑把推理线程绑到大核上cpu_set_t cpuset; CPU_ZERO(cpuset); CPU_SET(4, cpuset); // 假设大核是CPU4-7 pthread_setaffinity_np(pthread_self(), sizeof(cpuset), cpuset);RK3576的CPU是4个A72大核加4个A53小核大核编号通常是4到7。绑核之后推理时间的抖动会小很多。注意不要盲目追求最低延迟。在实际产品中推理的稳定性比峰值性能更重要。我见过一个案例开发者为了压榨性能把NPU频率锁在最高结果连续跑几个小时后板子过热降频推理时间反而比默认设置还长。散热设计要跟上。5. 精度调优与算子兼容性处理5.1 量化精度损失的定位方法前面提到MobileNetV1量化后精度掉1.3个百分点这个损失主要来自哪里用accuracy_analysis生成的报告可以定位。报告里会列出每一层的量化误差重点看两类层深度卷积层和逐点卷积层。深度卷积层的量化误差通常比逐点卷积层大因为深度卷积的每个通道只有一个卷积核通道内的数值分布范围可能很宽8位量化容易溢出。如果你发现某一层深度卷积的余弦相似度低于0.85可以考虑把这一层设为浮点rknn.config( ..., quantized_algorithmnormal, quantized_methodchannel )quantized_methodchannel表示按通道量化每个通道有独立的scale和zero_point比按层量化精度高。但通道量化会增加模型体积和推理时的计算量需要权衡。另一个方法是混合量化。RKNN-Toolkit2支持在build的时候指定某些层不量化rknn.build( do_quantizationTrue, dataset./dataset.txt, rknn_batch_size1 )然后在生成的RKNN模型里你可以通过rknn_quantize接口对特定层做调整。不过这个操作比较复杂新手建议先用通道量化试试不行再考虑混合量化。5.2 不支持的算子如何绕过RKNN-Toolkit2对ONNX算子的支持是有限的。MobileNetV1里用到的算子包括Conv、DepthwiseConv、Relu、GlobalAveragePool、FC这些RKNN都支持。但如果你用的是MobileNetV2或V3里面会有Add、Mul、HardSwish等算子其中HardSwish在早期版本的Toolkit2里不支持。遇到不支持的算子有三种处理方式。第一种是替换算子。比如HardSwish可以用ReLU6近似精度会掉一点但能跑通。在PyTorch里改模型定义class MobileNetV3Block(nn.Module): def __init__(self, ...): ... self.act nn.ReLU6(inplaceTrue) # 替换HardSwish第二种是自定义算子。RKNN-Toolkit2支持通过rknn_custom_op接口注册自定义算子但需要写C代码实现算子的CPU版本NPU版本需要联系芯片厂商。这个门槛比较高一般项目不建议走这条路。第三种是拆分模型。把不支持的算子留在CPU上跑支持的算子放到NPU上。RKNN支持模型分段但分段之间的数据传输会带来额外开销。我实测过一个分段模型NPU部分推理5msCPU部分推理15ms数据传输2ms总共22ms比纯CPU推理的40ms快但比纯NPU推理的7ms慢很多。提示在模型设计阶段就考虑NPU的算子支持度比事后补救要省事得多。如果你有模型结构的决定权优先选择RKNN官方文档里列出的支持算子。MobileNetV1之所以适合入门就是因为它的算子集非常干净。5.3 多输入多输出模型的处理有些场景下MobileNet不是单独用的比如目标检测里MobileNet作为backbone后面接SSD或YOLO的head。这时候模型可能有多个输出或者输入不止一个。多输出的处理在C代码里就是循环rknn_output outputs[3]; memset(outputs, 0, sizeof(outputs)); for (int i 0; i 3; i) { outputs[i].want_float 1; } ret rknn_outputs_get(ctx, 3, outputs, NULL); // 分别处理outputs[0]、outputs[1]、outputs[2]多输入类似rknn_inputs_set的第二个参数传输入个数inputs数组里每个元素对应一个输入。这里有个细节多输入模型的输入顺序必须和转换时ONNX的输入顺序一致。如果你在ONNX导出时input_names是[input1, input2]板端设置输入时index 0对应input1index 1对应input2。顺序搞反了不会报错但推理结果完全不对。6. 从Demo到产品工程化落地的几个关键决策6.1 模型版本管理与OTA升级产品化之后模型不是一成不变的。你可能需要根据实际数据迭代模型然后推送到设备上。RKNN模型文件本身不大MobileNetV1量化后4MB左右直接通过HTTP下载就行。但要注意版本管理每个RKNN模型文件应该带一个版本号板端程序在加载模型前先检查版本不匹配就触发下载。我一般会在模型文件名里带版本号比如mobilenet_v1_v1.2.0.rknn同时在板端存一个model_version.txt。程序启动时读取当前版本和服务器上的版本比对不一致就下载新模型并重启推理进程。OTA升级的时候要注意原子性。下载新模型到临时目录校验MD5然后重命名替换旧模型。不要直接覆盖万一下载中断模型文件损坏设备就变砖了。6.2 内存与功耗的平衡策略RK3576的NPU在满载运行时功耗大约2到3瓦加上CPU和内存整板功耗在5瓦左右。如果是电池供电的设备这个功耗需要仔细管理。一个有效的策略是动态频率调整。推理任务不密集的时候把NPU频率降下来检测到连续推理请求时再升到最高频率。RK3576的devfreq接口支持这种动态调整# 查看可用的频率档位 cat /sys/class/devfreq/fdab0000.npu/available_frequencies # 设置频率 echo 800000000 /sys/class/devfreq/fdab0000.npu/userspace/set_freq但手动调频需要自己写策略比较麻烦。更简单的做法是用simple_ondemand调速器它会根据负载自动调整echo simple_ondemand /sys/class/devfreq/fdab0000.npu/governor实测下来simple_ondemand在推理间隔大于100ms的场景下平均功耗能降低30%左右推理延迟增加不到1ms。6.3 多模型并行的资源分配有些产品需要同时跑多个模型比如一个人脸识别设备需要先跑人脸检测再跑人脸识别。两个模型都放在NPU上跑就需要考虑资源分配。RK3576的NPU是单核的同一时刻只能跑一个模型。多模型并行实际上是分时复用。RKNN的context是独立的你可以创建两个context分别加载两个模型然后交替调用rknn_run。但context切换有开销大约0.5到1ms。如果两个模型的推理频率不高分时复用完全够用。但如果两个模型都需要实时跑比如30fps那就需要算一下总算力是否够。人脸检测模型假设5ms一帧人脸识别模型假设8ms一帧加起来13ms理论上能跑76fps但实际因为context切换和内存拷贝能跑到50fps就不错了。我的建议是如果多模型场景下帧率要求高考虑模型融合。把两个模型合并成一个多任务模型共享backbone这样NPU只需要跑一次前向传播。当然这需要重新训练模型工作量不小但长期来看收益很大。6.4 实际项目中的踩坑记录最后分享几个我在实际项目中踩过的坑都是文档里不会写的。第一个坑是图片解码。板端用OpenCV的imread读JPEG图片在某些固件版本上会报错原因是OpenCV编译时没有链接libjpeg。解决办法是用imdecode配合手动读取文件FILE *fp fopen(test.jpg, rb); fseek(fp, 0, SEEK_END); int size ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char *buf (unsigned char *)malloc(size); fread(buf, 1, size, fp); fclose(fp); std::vectorunsigned char data(buf, buf size); cv::Mat img cv::imdecode(data, cv::IMREAD_COLOR);第二个坑是NPU的内存对齐。RKNN要求输入数据的内存地址按64字节对齐如果你传的指针没有对齐推理会失败或者结果异常。用posix_memalign分配对齐内存void *input_data; posix_memalign(input_data, 64, 224 * 224 * 3);第三个坑是模型加载时间。RKNN模型在第一次加载时会做一次完整的初始化耗时可能在几百毫秒。如果你在程序启动时才加载模型用户会感觉到明显延迟。解决办法是在程序启动后立即加载模型把初始化时间藏在启动画面里。或者用rknn_init的RKNN_FLAG_CONTINUE_ON_FAIL标志让模型加载异步进行。第四个坑是温度。RK3576在持续推理时NPU温度会升到70度以上如果散热不好会触发降频。我建议在NPU上加一个小散热片成本几块钱但能保证长时间稳定运行。软件层面可以监控温度cat /sys/class/thermal/thermal_zone0/temp如果温度超过80度主动降低推理频率或者暂停推理等温度降下来再继续。这些经验都是我在多个项目中积累下来的有些是花了几天时间才排查出来的。你如果在部署过程中遇到类似问题可以对照着检查一下。RK3576这颗芯片的NPU潜力很大MobileNet只是一个起点后面还可以尝试YOLO、Transformer等更复杂的模型工具链的用法是相通的。
返回列表