尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RV1106部署实战:RKNN-Toolkit2转换YOLOv8n与板端推理指南

RV1106部署实战:RKNN-Toolkit2转换YOLOv8n与板端推理指南 我第一次把RKNN-Toolkit2跑起来的时候心里想的是搞模型部署嘛转换一下烧进去就完事了。结果光一个版本匹配问题就卡了我大半天。RV1106这颗芯片在IPC和视觉模组领域出镜率很高但它和瑞芯微那些动辄3 TOPS、6 TOPS的大芯片不一样NPU算力只有0.5 TOPS这个量级部署模型完全不是“把大模型塞进去”的思路而是一场“怎么让模型在螺蛳壳里做道场”的精细化操作。这篇博文从RV1106的硬件底子讲起覆盖RKNN-Toolkit2环境搭建、YOLOv8n从ONNX到RKNN的完整转换、板端C API推理流程以及我实际部署中踩过的量化、内存、色彩通道等各类坑。无论你是刚开始接触这颗芯片的新手还是已经被版本匹配折磨过的“准受害者”按这条链路走一遍至少能少熬三个通宵。1. RV1106是颗什么样的芯片——先搞清楚算力边界再谈部署1.1 芯片底子与算力定位RV1106是瑞芯微面向智能摄像头、可视门铃、扫地机视觉模组这类场景的低功耗视觉SoC。主控是双核Cortex-A7主频大约1.2GHz还带了一颗RISC-V协处理器用于做一些轻量级的控制任务。最核心的部分是它集成的NPUINT8算力在0.5 TOPS这个量级同时集成了1080p的H.264/H.265硬件编解码和ISP。0.5 TOPS是什么概念拿大家熟悉的RK3588来对比RK3588的NPU是6 TOPS差了十几倍。即便和同为轻量级的RK35661 TOPS相比RV1106也明显低一个台阶。所以这颗芯片的定位非常清晰它不是一个通用AI计算平台而是一颗“感知辅助型”芯片。适合的任务是在摄像头端完成人脸检测、人形检测、车辆检测、火焰检测这类单一或少量目标检测然后把结果通过消息上报或者只在确实有事件时才推流录像。我见过不少人拿着一颗RV1106就开始幻想跑YOLOv5s甚至YOLOv8m然后被实测帧率劝退。这不是芯片的问题是没搞清楚算力定位。0.5 TOPS就是0.5 TOPS你得按它的规矩来。1.2 适合部署什么模型不适合部署什么模型先泼一盆冷水。0.5 TOPS意味着你在PC上用GPU跑得很顺的大模型基本不用考虑。目标检测领域稍微合适的窗口是YOLOv5n、YOLOv6n、YOLOv8n这类nano级别网络再小的还有PP-PicoDet、NanoDet、RFBnet、MobileNet-SSD。分类网络可以用MobileNetV3、ShuffleNetV2。分割网络的话轻量级的STDC或者定制的小UNet可以试但要做大量裁剪。Transformer类的检测头、ViT骨干、大分辨率输入在RV1106上要么算子不完全支持要么推理耗时长到不可用。注意力机制不是不能用但只能在小范围的通道注意力、轻量自注意力模块里适当引入搞一个大窗口的全局注意力层基本就是在挑战NPU的算子覆盖能力。实际部署中模型的输入分辨率往往是影响耗时最大的因素。拿YOLOv8n举例320x320输入和640x640输入相比理论计算量直接差4倍。在RV1106这个算力档位我建议先按320或384的输入开始调试验证功能后再尝试往640推看能不能接受。多数IPC场景下320x320对近距离的人形检测、5米内的人脸检测都够用刻意追求640反而是给后续调试上强度。提示先定输入分辨率再选网络结构最后才是调精度。顺序反了后面调参全是痛苦。1.3 部署路径总览RV1106模型部署和瑞芯微其他芯片类似都是“PC端转换板端推理”的流程PC上准备ONNX/TFLite/PyTorch模型。使用RKNN-Toolkit2把模型转换成RKNN格式可同时做INT8量化、算子融合、图优化。在PC的模拟器上先跑一遍确认输出、精度、耗时大致正常。把RKNN模型文件和runtime库librknnmrt.so拷到RV1106板子。板端通过C/C或Python调用RKNN API完成推理。对输出做后处理接业务逻辑。看起来不复杂但每一环都有自己的坑。下面我把这几步拆开讲重点放在“我实际踩过、也看到群里其他人踩过”的典型问题上。2. 环境准备与版本匹配——RKNN-Toolkit2的第一道坑2.1 PC端安装的正确姿势先说PC端。RKNN-Toolkit2是一个Python工具包官方主要支持x86 Linux环境建议用Ubuntu 18.04/20.04Python版本3.8以上。它会依赖一堆库numpy、onnx、opencv、tensorflow转换某些格式时需要、torch转换pytorch模型时需要等所以尤其容易被依赖冲突缠上。我自己的做法是用虚拟环境装python3 -m venv ~/venv/rknn source ~/venv/rknn/bin/activate pip install rknn-toolkit2-1.6.0-cp38-cp38-linux_x86_64.whl注意文件名里的cp38表示Python版本别下错了。如果你的Python是3.10就要找对应cp310的whl或者干脆降到3.8。安装完成后可以验证一下from rknn.api import RKNN print(RKNN.__version__)能打印出版本号说明装好了。这里强调一下RKNN-Toolkit2的版本号、whl文件名和你的Python版本三者必须严格匹配别信那些“通用版”的说法。2.2 板端runtime与固件的对应关系这里的“版本匹配”坑点在于PC端的rknn-toolkit2版本、模型转换时用的RKNN-Toolkit2版本、板端固件里的librknnmrt.so版本三者必须配套。什么意思呢RKNN模型文件虽然都是.rknn后缀但不同runtime版本对RKNN文件内部格式的兼容性并不总是向后兼容的。你用最新版工具转出来的模型跑到一个老版本固件的板子上极可能初始化失败或者推理结果全错。反过来老版本工具转的模型挂到新runtime上也偶发算子报错。最稳妥的办法是直接以板子出厂固件里的runtime版本为准。我现在的习惯是拿到板子先看SDK版本或固件版本号。到瑞芯微官方代码仓、SDK包附件或配套文档里找对应的rknn-toolkit2版本。用SDK包里附带的测试模型先跑通确认工具链版本一致再处理自己的模型。顺便说一句RV1106对应的板端runtime是librknnmrt.so在板子的/usr/lib或SDK的runtime/Linux目录下都能找到。这个文件和RK3588那套librknnrt.so不一样别混用硬拷过去会直接加载失败。2.3 常见报错runtime初始化失败很多人在板子上写第一个demo时都会遇到类似报错E RKNN: Cannot open lib: librknnmrt.so, rknn_init fail! ret -1或者E RKNN: rknn_init error, ret -8这种问题一般有三个来源库文件没拷到板子上或者路径没设置对。runtime版本和模型文件版本不匹配。板载NPU驱动没正常加载。排查方法先在板子上执行ls /usr/lib/librknn*看库在不在再看NPU设备节点能不能读到版本信息。读不到说明驱动层面就没起来。最后再回头看模型文件是用什么版本工具转的。如果时间紧迫最省事的办法是找一个“别人验证过能跑的demo”把它的rknn模型、librknnmrt.so、编译参数整体拿来跑通然后再替换成自己的模型。这样可以把问题域快速缩小到“转换环节”还是“runtime环节”。3. 从ONNX到RKNN——以YOLOv8n为例走通转换流程3.1 导出YOLOv8n的ONNX模型我用YOLOv8n作为演示目标因为它结构相对简单、开源资料多、也是做IPC检测时最常被问到的模型之一。导出ONNX时建议直接用ultralytics官方指令yolo export modelyolov8n.pt formatonnx opset12导出时有两个点要关注。一是opset版本RKNN-Toolkit2对太高的opset支持不一定完整我习惯固定到12或者13别默认冲17。二是输入尺寸导出时通过imgsz320把输入固定到320x320这样转换和板端预处理都省事。导出之后先用onnxruntime在PC上跑一遍确认ONNX模型本身的输出是正常的import onnxruntime as ort import numpy as np sess ort.InferenceSession(yolov8n.onnx) data np.random.rand(1, 3, 320, 320).astype(np.float32) out sess.run(None, {images: data}) print([o.shape for o in out])这里要注意一个问题RV1106的NPU输入布局通常是NHWC而ONNX导出的模型输入往往是NCHW。RKNN-Toolkit2的config里有inputs_layout参数可以指定转换脚本里必须把它对齐不然后续在板上解析数据时维度全乱。3.2 转换脚本与关键参数下面是我常用的转换脚本目标平台直接写rv1106from rknn.api import RKNN rknn RKNN() rknn.config( target_platformrv1106, mean_values[[0, 0, 0]], std_values[[255, 255, 255]], quantized_dtypew8a8, quantized_algorithmnormal, optimization_level3 ) ret rknn.load_onnx(modelyolov8n.onnx, inputs[images], outputs[output0]) if ret ! 0: print(load_onnx failed) exit(1) ret rknn.build(do_quantizationTrue, datasetdataset.txt) if ret ! 0: print(build failed) exit(1) ret rknn.export_rknn(yolov8n.rknn) if ret ! 0: print(export failed) exit(1)几个参数解释一下mean_values和std_values如果输入图像是uint8通常mean填0、std填255相当于把0~255归一化到0~1。如果你的预处理是在模型外面做的比如训练时用了mean[0.485,0.456,0.406]、std[0.229,0.224,0.225]那也要按顺序填进去并且注意RGB还是BGR千万别搞反。RV1106上的摄像头采集通道一般是NV12转RGB颜色通道顺序是个经典坑。quantized_dtypew8a8权重和激活都做INT8量化这是RV1106上最常用的配置内存占用最小速度最快。如果你发现精度掉得厉害可以试w8a16或者混合量化。optimization_level0~3数值越高做的图优化越激进。我一般从3开始出问题再往下降比较排查。3.3 量化数据集准备与校准量化不是直接转成INT8就完事它需要一个校准过程跑一批代表图像统计每一层的激活值分布再决定每个tensor的scale和zero point。dataset.txt的内容很简单就是一个图像路径清单./calib/0001.jpg ./calib/0002.jpg ./calib/0003.jpg ...关键点是这些图像要能代表真实场景。如果你的使用场景是室内看护摄像头校准图就应该是室内光照、人物走动、桌子椅子这些画面而不是网上随便下的一堆风景图。我见过有人拿20张猫图做校准模型上线后检测人形漏报一大半问题就出在校准集和实际场景分布差异太大。至于数量经验上三五十张到两百张都行不需要像训练那样拿几千张但也别少于20张。图像统一缩放到模型输入尺寸用opencv读进来再resize即可import cv2 img cv2.imread(calib/0001.jpg) img cv2.resize(img, (320, 320)) cv2.imwrite(calib_320/0001.jpg, img)3.4 用模拟器先验一遍RKNN-Toolkit2自带模拟器模型转换完之后可以在PC上直接跑推理不用上板。这一步强烈建议不要跳过尤其当你改过预处理参数、输出节点或者量化配置时。rknn.init_runtime(targetNone) img cv2.imread(test.jpg) img cv2.resize(img, (320, 320)) out rknn.inference(inputs[img]) print([o.shape for o in out])模拟器的意义在于把“模型转换问题”和“板端部署问题”隔离开。如果模拟器输出都是乱的那肯定是转换、量化、输入预处理的事先别急着上板浪费编译时间。它还有一个性能分析接口eval_perf能给出每一层的耗时预估虽然是模拟值但能帮你看出哪几个算子是大头为后续换模型结构提供参考。4. 上板部署——用RKNN C API跑推理的全流程4.1 板端文件组织与编译环境RKNN模型转换好后就进入板端阶段。RV1106上的runtime主要提供C API头文件是rknn_api.h动态库是librknnmrt.so。你在SDK的runtime目录下找到librknn_api文件夹后里面通常还有现成的示例代码和CMakeLists。需要拷到板子的文件包括yolov8n.rknn、librknnmrt.so、rknn_api.h以及交叉编译好的可执行文件。RV1106一般是ARM Cortex-A7交叉编译用gcc-arm-linux-gnueabihf。板厂SDK通常在buildroot output目录下带有配套工具链直接用即可避免自己单独下载的版本和板载库不兼容。4.2 核心API调用流程下面是一个最简C代码主流程省略了错误处理以保持可读性#include rknn_api.h #include stdio.h #include stdlib.h static unsigned char *load_file(const char *path, int *size) { FILE *fp fopen(path, rb); fseek(fp, 0, SEEK_END); *size ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char *buf (unsigned char*)malloc(*size); fread(buf, 1, *size, fp); fclose(fp); return buf; } int main() { int model_size 0; unsigned char *model load_file(yolov8n.rknn, model_size); rknn_context ctx; int ret rknn_init(ctx, model, model_size, 0, NULL); if (ret 0) { printf(rknn_init fail: %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); // 构造输入 unsigned char image_buffer[320 * 320 * 3]; // 预先读好的BGR图像 rknn_input inputs[1]; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].size 320 * 320 * 3; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf image_buffer; ret rknn_inputs_set(ctx, 1, inputs); ret rknn_run(ctx, NULL); rknn_output outputs[1]; outputs[0].want_float 0; // 直接拿量化后的输出 ret rknn_outputs_get(ctx, 1, outputs, NULL); // 到这里 outputs[0].buf 就是模型的原始输出 // 处理完后必须释放 rknn_outputs_release(ctx, 1, outputs); rknn_destroy(ctx); return 0; }这里有几个容易搞错的点输入图像大小要和模型输入一致。我不建议把resize的事放在板端CPU上做大图RAM带宽有限先缩放好再喂给NPU。want_float如果设为1runtime会帮你反量化转成float省事但慢如果设为0你拿到的是int8量化值需要结合输出tensor的scale和zero point手动转float。实时性优先的部署里我一般用want_float0省一次全tensor的类型转换时间。输出布局是NHWC还是NCHW可以通过rknn_query查输出属性得到不要想当然。YOLOv8的原生导出如果没改输出节点常见形状是[1, 84, 8400]铺开解析时注意维序。4.3 YOLOv8输出解析与NMS拿到输出后核心工作是把原始张量解析成框。YOLOv8的输出结构是对于每个anchor总共8400个前4个值是bbox的cx、cy、w、h相对输入尺寸归一化后面80个值是各类别得分已经过了sigmoid通常在0~1之间。简单解析逻辑如下for (int i 0; i 8400; i) { float *ptr output i * 84; float max_score 0; int max_cls -1; for (int c 4; c 84; c) { if (ptr[c] max_score) { max_score ptr[c]; max_cls c - 4; } } if (max_score score_thresh) continue; boxes[count].x ptr[0] - ptr[2] / 2; boxes[count].y ptr[1] - ptr[3] / 2; boxes[count].w ptr[2]; boxes[count].h ptr[3]; boxes[count].score max_score; boxes[count].cls max_cls; count; }最后接一个NMS把重叠的框去掉。如果CPU富余自己写个简单NMS就行如果不想在C里写也可以在导出ONNX时直接导出带NMS的版本做成模型内部算子但RV1106的NPU对NMS这类动态张量算子支持不好得不偿失。我始终建议在板端CPU上做NMS反正候选框数量不大耗时在毫秒级。4.4 摄像头输入与ISP通道的注意点IPC场景下图像通常来自摄像头经过ISP输出NV12或者RGB。RV1106的媒体链路一般走RKMPPVPSS可以把摄像头采集的帧缩放到模型输入尺寸。这里最容易出现的问题有两个色彩空间很多sensor默认输出YUVYUV转RGB的系数不对画面偏绿偏紫模型效果崩盘。建议在VPSS阶段直接做RGB输出不要在应用层用软转软转又慢又容易出格式问题。旋转摄像头安装方向可能导致输入画面旋转90度模型如果没针对旋转数据训练过检测结果会很差。要么在数据采集阶段就固定安装角度要么在VPSS里做旋转不要指望模型自带旋转鲁棒性。5. 性能调优与实测经验——量化、内存布局与典型报错5.1 量化方式怎么选RV1106的NPU核心加速在INT8所以w8a8是默认首选。但有些层对量化敏感尤其是检测头里的关键层一旦量化后精度掉得没法看可以单独对某些层做不量化处理保持float16或float32。RKNN-Toolkit2里可以通过混合量化配置来指定。我遇到过一个真实案例一个自定义的小目标检测模型w8a8量化后mAP掉了5个点排查后发现是检测头第一个卷积层的权重动态范围太大把它单独设成不量化mAP马上回来了。所以“全量INT8”不是必须的灵活混量化才是正经优化手段。另外quantized_algorithm可以选normal或mmse后者对精度略友好但转换时间会长一些。数据分布极端的情况下mmse救得回来。大家可以在转换时多试一组对比量化前后在模拟器上的输出差异再决定用哪套配置。5.2 耗时优化三板斧在0.5 TOPS的算力上常见的耗时优化方向有三个降低输入分辨率。这是最立竿见影的。320x320对比640x640速度快到怀疑人生。牺牲一点远距离小目标的召回换取翻倍的帧率这个交换在多数IPC场景里值得。尽量零拷贝。板端推理时如果能把摄像头采集的buffer地址直接透传给NPU的输入避免一次memcpy能省不少时间。RKNN API支持通过内存申请接口创建NPU侧内存然后直接用该内存接收VPSS输出再把地址作为输入。这个需要看SDK里有没有对应的媒体内存管理机制。多线程流水线。采集线程、推理线程、后处理线程分开用环形缓冲区传递帧避免采集等待推理。RV1106是双核A7线程调度得当的话整体吞吐能上来不少。5.3 常见报错对照表下面这几个现象我都在实际部署中见过整理成表现象原因处理方向rknn_init 返回 -8模型格式与runtime版本不符换成配套转换工具重新导出推理结果全0或乱码输入颜色通道顺序错误检查BGR/RGB顺序输出shape与预期不一致ONNX端拼接了额外输出修改导出裁剪附加节点转换报Unsupported op模型含NPU不支持的算子把算子移到CPU或换轻量结构运行时Segmentation fault输入size和runtime不匹配或内存越界检查输入buf大小和layout模拟器正常上板乱板子runtime库过旧升级固件或库文件5.4 两个低概率但影响巨大的坑最后说两个不算高频但一旦碰上就非常难受的坑。第一个是模型里的某些算子被工具链静默替换成了低精度实现。比如一些浅层卷积优化后可能在计算图上做算子融合融合后精度出现细微变化。这种问题最难查因为转换、编译都不报错只有对着输出数据逐层比对才能发现。我的经验是转换时保存一份“不优化”的版本optimization_level0做AB对比测试如果优化版精度掉太多就要考虑是不是某些层被激进融合了。第二个是板端时间戳和模型内部buffer的同步问题。当你连续取流推理时如果把输入buffer覆盖得太快NPU还没读完就重写了数据推理结果会出现“一帧卡一帧好”的诡异现象。这其实不是模型问题而是你在应用层没有做足够的数据隔离。记得给输入帧做个深度拷贝或者用双缓冲交替写。我自己的习惯是每次部署新模型都先写一个最少功能的测试程序固定输入一张图、不做摄像头、不做网络只验证rknn推理输出和PC端模拟器结果一致。这一步通过了再往里面加摄像头和业务逻辑。这样能把“模型问题”和“代码问题”彻底分开排查速度会快很多。以上这些经验基本覆盖了从PC转换到板上落地的全流程。每个人的场景不同模型结构也不同但核心思路是一致的版本配好、转换准确、量化谨慎、板端稳定。
返回列表