news 2026/10/3 8:01:19

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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协处理器,用于做一些轻量级的控制任务。最核心的部分是它集成的NPU,INT8算力在0.5 TOPS这个量级,同时集成了1080p的H.264/H.265硬件编解码和ISP。

0.5 TOPS是什么概念?拿大家熟悉的RK3588来对比,RK3588的NPU是6 TOPS,差了十几倍。即便和同为轻量级的RK3566(1 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端转换,板端推理”的流程:

  1. PC上准备ONNX/TFLite/PyTorch模型。
  2. 使用RKNN-Toolkit2把模型转换成RKNN格式,可同时做INT8量化、算子融合、图优化。
  3. 在PC的模拟器上先跑一遍,确认输出、精度、耗时大致正常。
  4. 把RKNN模型文件和runtime库(librknnmrt.so)拷到RV1106板子。
  5. 板端通过C/C++或Python调用RKNN API完成推理。
  6. 对输出做后处理,接业务逻辑。

看起来不复杂,但每一环都有自己的坑。下面我把这几步拆开讲,重点放在“我实际踩过、也看到群里其他人踩过”的典型问题上。

2. 环境准备与版本匹配——RKNN-Toolkit2的第一道坑

2.1 PC端安装的正确姿势

先说PC端。RKNN-Toolkit2是一个Python工具包,官方主要支持x86 Linux环境,建议用Ubuntu 18.04/20.04,Python版本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版本为准。我现在的习惯是:

  1. 拿到板子先看SDK版本或固件版本号。
  2. 到瑞芯微官方代码仓、SDK包附件或配套文档里找对应的rknn-toolkit2版本。
  3. 用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 model=yolov8n.pt format=onnx opset=12

导出时有两个点要关注。一是opset版本,RKNN-Toolkit2对太高的opset支持不一定完整,我习惯固定到12或者13,别默认冲17。二是输入尺寸,导出时通过imgsz=320把输入固定到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 转换脚本与关键参数

下面是我常用的转换脚本,目标平台直接写rv1106:

from rknn.api import RKNN rknn = RKNN() rknn.config( target_platform='rv1106', mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], quantized_dtype='w8a8', quantized_algorithm='normal', optimization_level=3 ) ret = rknn.load_onnx(model='yolov8n.onnx', inputs=['images'], outputs=['output0']) if ret != 0: print('load_onnx failed') exit(1) ret = rknn.build(do_quantization=True, dataset='dataset.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_dtype='w8a8':权重和激活都做INT8量化,这是RV1106上最常用的配置,内存占用最小,速度最快。如果你发现精度掉得厉害,可以试w8a16或者混合量化。
  • optimization_level:0~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(target=None) 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; }

这里有几个容易搞错的点:

  1. 输入图像大小要和模型输入一致。我不建议把resize的事放在板端CPU上做大图,RAM带宽有限,先缩放好再喂给NPU。
  2. want_float:如果设为1,runtime会帮你反量化转成float,省事但慢;如果设为0,你拿到的是int8量化值,需要结合输出tensor的scale和zero point手动转float。实时性优先的部署里,我一般用want_float=0,省一次全tensor的类型转换时间。
  3. 输出布局是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的媒体链路一般走RKMPP,VPSS可以把摄像头采集的帧缩放到模型输入尺寸。这里最容易出现的问题有两个:

  1. 色彩空间:很多sensor默认输出YUV,YUV转RGB的系数不对,画面偏绿偏紫,模型效果崩盘。建议在VPSS阶段直接做RGB输出,不要在应用层用软转,软转又慢又容易出格式问题。
  2. 旋转:摄像头安装方向可能导致输入画面旋转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的算力上,常见的耗时优化方向有三个:

  1. 降低输入分辨率。这是最立竿见影的。320x320对比640x640,速度快到怀疑人生。牺牲一点远距离小目标的召回,换取翻倍的帧率,这个交换在多数IPC场景里值得。
  2. 尽量零拷贝。板端推理时,如果能把摄像头采集的buffer地址直接透传给NPU的输入,避免一次memcpy,能省不少时间。RKNN API支持通过内存申请接口创建NPU侧内存,然后直接用该内存接收VPSS输出,再把地址作为输入。这个需要看SDK里有没有对应的媒体内存管理机制。
  3. 多线程流水线。采集线程、推理线程、后处理线程分开,用环形缓冲区传递帧,避免采集等待推理。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_level=0)做AB对比测试,如果优化版精度掉太多,就要考虑是不是某些层被激进融合了。

第二个是板端时间戳和模型内部buffer的同步问题。当你连续取流推理时,如果把输入buffer覆盖得太快,NPU还没读完就重写了数据,推理结果会出现“一帧卡一帧好”的诡异现象。这其实不是模型问题,而是你在应用层没有做足够的数据隔离。记得给输入帧做个深度拷贝,或者用双缓冲交替写。

我自己的习惯是,每次部署新模型,都先写一个最少功能的测试程序:固定输入一张图、不做摄像头、不做网络,只验证rknn推理输出和PC端模拟器结果一致。这一步通过了,再往里面加摄像头和业务逻辑。这样能把“模型问题”和“代码问题”彻底分开,排查速度会快很多。以上这些经验,基本覆盖了从PC转换到板上落地的全流程。每个人的场景不同,模型结构也不同,但核心思路是一致的:版本配好、转换准确、量化谨慎、板端稳定。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 8:01:14

数字频带传输全解析:2ASK/2FSK/2PSK/2DPSK原理与误码率仿真实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 8:01:14

含氢综合能源系统多目标分布鲁棒低碳调度MATLAB复现全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:59:59

Coze与Dify接口能力三层对比:编排、执行、治理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:59:58

4D成像雷达热-磁耦合设计:导热与吸波材料选型及实测经验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:59:21

PLC与触摸屏低成本三轴示教器方案:从脉冲当量到脚本通信

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:59:21

ACD/Labs核磁数据分析全流程:从原始FID到结构验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华