十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

RV1126平台ONNX模型转RKNN完整指南:量化校准与部署避坑

RV1126平台ONNX模型转RKNN完整指南:量化校准与部署避坑 如果你拿到一块瑞芯微RV1126核心板想在板端跑人脸识别那第一件让你头疼的事大概率不是算法本身而是怎么把训练好的模型塞进NPU里。RV1126上的NPU不认PyTorch也不认普通的ONNX它只认瑞芯微自己的RKNN格式。很多人的项目就是卡在这一步看着转换脚本很简单实际一跑全是算子报错、精度崩溃、输出变成固定值、板端加载失败一个问题接一个问题。我这几年在RV1126平台上做过好几个刷脸项目从模型选型到RKNN转换到C接口部署踩过的坑能写满一页纸。这篇文章就把从ONNX到RKNN的完整流程连同那些官方文档里不会写的细节一起梳理出来给准备在瑞芯微平台做边缘AI部署的朋友当个参考。不管是刚接触RKNN的新手还是已经被量化精度折磨过的老手这篇文章应该都能让你少走点弯路。1. 项目背景与方案选型1.1 为什么选RV1126为什么必须转RKNNRV1126这颗芯片在IPC、门禁考勤、智能摄像头这类产品里出现频率很高四核Cortex-A7加一个2.0 TOPS算力的NPU功耗控制得不错价格也合适。它的定位很明确做轻量级视觉推理而不是像GPU那样跑大模型。所以如果你要在RV1126上做实时人脸识别模型的体积和计算量必须控制住同时还要把模型量化到INT8不然NPU的优势根本发挥不出来。有人会问为什么不能用ONNX Runtime直接推理板端确实可以用ONNX Runtime在CPU上跑但人脸识别这种任务在CPU上跑轻量模型也就勉强几帧根本谈不上实时。RV1126的NPU只接受RKNN格式的模型所以转换是绕不开的一步。RKNN是瑞芯微的私有模型格式里面包含了算子映射关系、权重重排、量化参数表这些信息你可以理解成NPU专用的“可执行文件”。转换的过程就是把通用模型“翻译”成NPU能直接执行的指令和数据布局。1.2 人脸识别模型选型与转换边界这里我先说明一下我项目的整体结构人脸检测用RetinaFace的轻量版特征提取用MobileFaceNet输入是112x112的RGB人脸图输出512维特征向量比对用余弦相似度。这个组合在RV1126上跑得很稳检测模型和识别模型的转换逻辑完全一样下面重点以识别模型为例讲整个流程。转换前一定要想清楚一个边界问题RKNN对算子的支持是有限的尤其是不支持NMS这类带逻辑分支的后处理算子。所以我的做法是模型只负责“前向推理”检测框的解码、坐标还原、NMS、人脸对齐这些全部放到CPU端做。这样模型转换干净后处理也灵活调起来方便。用Netron看一眼你的ONNX模型如果发现图里带了大量后处理节点我的建议是导出ONNX之前就把这些节点裁掉只保留主干网络。2. ONNX导出转换前的关键第一步2.1 从PyTorch导出ONNX的注意事项很多人在RKNN转换时报错最后发现根因是ONNX本身就没导对。PyTorch导出ONNX时有几个参数直接影响后续RKNN能不能顺利接住。我一般这样导出import torch dummy_input torch.randn(1, 3, 112, 112) model MobileFaceNet(embedding_size512).eval() torch.onnx.export( model, dummy_input, face.onnx, input_names[input], output_names[embedding], opset_version11, dynamic_axesNone )这里有两个关键点。第一opset_version固定用11RKNN Toolkit 1.x对这个版本的算子支持最稳太高容易遇到不认识的算子太低有些激活函数又表达不了。第二dynamic_axes我直接不给也就是说batch维度固定为1。虽然ONNX支持动态shape但RKNN在RV1126这种平台上的动态shape支持很有限强行用动态输入只会增加转换失败的概率。人脸识别推理时本来就是一张一张处理固定batch1完全够用。另外导出前一定把模型切到eval模式关掉dropout和batch norm的training状态不然导出的模型推理结果和训练时对不上后面排查起来非常折磨人。2.2 预处理对齐最容易埋雷的地方这一节是我最想强调的。我见过太多人模型转换成功率百分之百但一上板精度就不对查来查去根因是预处理做了两遍。具体来说训练时的预处理一般有两种做法做法A先归一化到0-1再减mean除std做法B直接把像素从0-255映射到0-1mean和std都设为0.5或1如果你在PyTorch里已经把Normalize层做进了模型那么ONNX模型里是带预处理逻辑的RKNN转换时就不要在config里再设置mean_values和std_values。反过来如果ONNX模型输入就是原始RGB像素值那么必须在rknn.config里把mean和std配上让RKNN在NPU推理前帮你做归一化。这个对应关系可以用一张表整理训练时预处理ONNX模型是否含预处理rknn.config设置像素/255mean0.5std0.5含不要重复设置原始像素mean127.5std127.5不含mean_values[[127.5,127.5,127.5]]std_values[[127.5,127.5,127.5]]原始像素ImageNet常用mean/std不含mean_values[[123.675,116.28,103.53]]std_values[[58.395,57.12,57.375]]判断模型里有没有预处理层最简单的办法是用Netron打开ONNX看输入节点后面是不是紧跟Sub、Div、Mul这类算子。有的话说明预处理已经包含在模型里了。2.3 转换前先用ONNX Runtime验证ONNX导出来之后别急着转RKNN先在PC上用ONNX Runtime跑一遍确认ONNX输出和PyTorch输出一致。这一步能筛掉大量低级错误。import onnxruntime as ort import numpy as np session ort.InferenceSession(face.onnx, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name onnx_out session.run(None, {input_name: img_np})[0]把ONNX输出的512维向量和PyTorch模型同图输入的结果对比算一下余弦相似度。正常情况下应该在0.999以上如果掉到0.99以下先别继续往下走回过去检查模型导出是不是有问题。这个习惯我后来一直保留省了特别多时间。3. rknn-toolkit环境搭建与转换脚本3.1 工具链选型RV1126到底该用哪个版本的rknn-toolkit这是新手最容易搞混的地方。网上搜RKNN教程铺天盖地都是rknn-toolkit2很多教程甚至直接说“安装rknn-toolkit2就行”看得人一头雾水。但我想告诉你一个关键事实瑞芯微的NPU分两代RV1126、RV1109、RK1808属于RKNPU1代配套的转换工具是rknn-toolkit 1.x而RK3566、RK3568、RK3588、RV1106这些属于RKNPU2代才用rknn-toolkit2。所以在RV1126上做转换要下载的是rknn-toolkit 1.7.5而不是rknn-toolkit2。装错了你会发现两个问题要么初始化直接报错要么转换出来的rknn模型在板端加载失败。判断的方法很简单先查自己芯片的NPU属于哪一代再决定工具链版本别看到新版就往上冲。3.2 安装rknn-toolkit 1.7.5rknn-toolkit 1.7.5对Python版本和系统版本有要求我实际用的组合是Ubuntu 18.04 Python 3.6。官方提供了whl包直接装就行pip install rknn_toolkit-1.7.5-cp36-cp36m-linux_x86_64.whl依赖库比较多如果是在干净的机器上装建议用虚拟环境或者直接用官方Docker镜像避免和别的项目互相污染。我个人的建议是用Docker因为rknn-toolkit依赖的protobuf、numpy版本和现代深度学习环境经常冲突在Docker里跑转换脚本能省掉一堆环境问题。装完以后用python -c from rknn.api import RKNN; print(ok)验证一下是否能正常导入。3.3 完整的转换脚本与参数解释下面是我实际项目里用的转换脚本关键参数我加了注释from rknn.api import RKNN rknn RKNN(verboseTrue) # 关键配置预处理参数必须和ONNX模型输入对齐 rknn.config( mean_values[[123.675, 116.28, 103.53]], std_values[[58.395, 57.12, 57.375]], target_platformrv1126, reorder_channel0 1 2, quantized_dtypeasymmetric_quantized-8 ) # 加载ONNX模型 ret rknn.load_onnx(modelface.onnx) if ret ! 0: print(load onnx failed) exit(-1) # 量化构建dataset.txt里是校准图片路径列表 ret rknn.build(do_quantizationTrue, datasetdataset.txt) if ret ! 0: print(build failed) exit(-1) rknn.export_rknn(face.rknn)这几个参数我详细解释一下。reorder_channel表示通道顺序0 1 2是RGB2 1 0是BGR。如果你的模型训练时用的是RGB图这里就写0 1 2不要默认它是什么顺序。quantized_dtype我用的asymmetric_quantized-8也就是非对称INT8量化。人脸识别这类模型输出特征向量分布不是严格的零对称非对称量化通常比对称量化效果更好。如果这个参数对你的数据不友好后面也可以换成对称量化试试这个后面量化调优部分会讲。build里的do_quantizationTrue表示开启量化dataset指向一个txt文件。注意量化不是随便填个路径就行这个dataset.txt的内容和数量直接影响最终模型的精度下一节专门说。3.4 量化校准数据集怎么做量化校准的本质是统计每一层激活值的分布范围然后根据这个范围算出量化scale和zero_point。所以校准图片必须能代表真实场景的数据分布这是量化精度最重要的外部因素。我一般准备50到200张图片宁多勿少宁真实勿通用。具体要求图片内容要贴近实际使用场景。做门禁人脸识别就用门禁场景下的人脸图做考勤机就用考勤现场的图。如果用了网上随便下载的人脸图当校准集上线后大概率出问题。图片里要有不同的角度、光照、背景、人脸大小。只放一种类型的图统计出来的激活范围会偏导致量化后输出异常。图片尺寸不需要和模型输入一致RKNN会自己缩放但内容才是关键。dataset.txt格式很简单每行一个图片的绝对路径/home/user/calib/face_001.jpg /home/user/calib/face_002.jpg ...我当时第一次量化图省事只放了30张从网络下载的明星脸照片结果量化后的模型在真实场景里提取的特征向量和浮点模型差了十万八千里特征比对直接失灵。后来换成现场拍摄的200张图重新校准精度一下就正常了。这个环节千万别偷懒。4. 量化精度问题与优化实战4.1 INT8量化后精度掉多少算正常人脸识别模型量化后比较直观的衡量指标是特征向量的一致性。我通常的做法是拿同一组测试图分别用FP32的ONNX模型和转换后的INT8 RKNN模型提取特征算两者的余弦相似度。正常情况下的参考值如下模型精度与浮点模型特征余弦相似度实际识别效果FP321.0基准INT8正常0.99以上基本无感INT8轻微异常0.95-0.99识别率轻微下降阈值可能需要微调INT8严重异常0.9以下基本不可用必须排查如果余弦相似度掉到0.95以下先别急着调模型优先检查两件事校准数据集够不够、预处理的mean/std对不对。这两项我遇到问题概率最高占了八成的排查量。4.2 accuracy_analysis逐层分析定位精度问题如果确认预处理和校准集都没问题但精度还是不行那就要做逐层分析了。rknn-toolkit提供了accuracy_analysis接口可以对比每一层在量化前后的输出差异快速定位是哪几层出了问题。# 复用前面的rknn对象加载量化模型 rknn.load_rknn(pathface.rknn) rknn.init_runtime() # 输入一张预处理好的图片 rknn.accuracy_analysis(inputs[img_array], output_dir./acc_result)运行完会在output_dir里生成每一层的量化前后输出对比包括余弦相似度、欧式距离这些指标。看报告的时候重点关注相似度特别低的层然后看这些层是什么类型的算子。我遇到的情况大多数异常集中在某些敏感结构上比如Attention模块、最后的全连接层、或者激活值范围特别小的层。定位到异常层之后方案有三个方向增加校准数据重新统计激活值范围这是成本最低的尝试。修改量化配置把量化的粒度从整图统计改成逐层统计有时候就能救回来。对异常层做混合量化也就是把这些层保留为FP16其余层继续保持INT8。RV1126的NPU对INT16和FP16也有支持混合量化是官方提供的思路。具体接口在rknn-toolkit 1.7.5里叫hybrid_quantization相关流程不同小版本接口名略有差异以你装的whl包官方文档为准。我的经验是把最后几层或者某个异常模块单独拎出来不量化往往一次就能把余弦相似度从0.9拉到0.99以上。4.3 输出数值固定一个典型的量化翻车现场“不量化正常int8量化后数值不动”这个问题很多人问过我在项目里也真实遇到过。现象是用RKNN模型跑推理输出的512维特征向量所有值都差不多大甚至完全一样像是一堆固定值。先复现一下当时的情况。我用的是FaceNet结构ONNX模型在FP32下输出正常用do_quantizationTrue转成INT8后在PC模拟器上测试输出向量所有维度都接近于同一个很小的数比如0.00012。当时第一反应是转换脚本有问题反复检查了mean、std、reorder_channel都没发现问题折腾了大半天。后来怎么解决的呢问题就出在校准数据集上。我最初的校准图片只有30张而且全部来自同一个拍摄场景画面高度相似。量化时模型统计到的激活值范围严重偏离真实分布导致很多层实际被压缩到很窄的数值区间输出自然就“糊”了。我把校准集换成覆盖多种场景的200张图重新构建模型输出立刻恢复正常。所以遇到输出数值不动的情况我的排查顺序是这样的先用do_quantizationFalse转一个不量化的版本确认RKNN本身能跑通、输出和ONNX一致。用不量化版本跑同一张图如果输出正常说明问题出在量化环节。检查校准数据集的数量和多样性优先扩充校准集。如果校准集没问题再用accuracy_analysis逐层看定位是哪一层开始异常。很多时候输出固定值不是因为模型坏了而是量化校准的“底子”没打好。5. 在RV1126板上部署与C接口调用5.1 板端验证先跑通再优化模型转换完成后先在PC上用rknn-toolkit的模拟器验证一遍我这个习惯能省不少事。模拟器跑通了再上板不然板端日志少排查问题效率很低。上板之后RV1126上跑的是librknnmrt.so这套运行时和PC端的rknn-toolkit不是一回事。你可以在板端用rknn-toolkit-lite这个Python库快速验证模型也可以直接写C代码调用。我建议先用rknn-toolkit-lite在Python里跑通推理流程确认模型在真实NPU上的输出和PC模拟器一致再切换到C接口做性能优化。要注意一个容易踩的坑板端Python验证时如果加载模型后输入尺寸或者通道顺序不对NPU不会给你报错而是直接输出垃圾结果。所以验证时一定要打印输入输出的shape和type和模型要求仔细对照。5.2 C接口调用一个可复用的推理模板项目落地最终要写C代码这里给一个精简版的人脸特征提取模板#include rknn_api.h rknn_context ctx; // 加载模型 rknn_init(ctx, /path/to/face.rknn, 0, 0, NULL); // 查询输入输出信息 rknn_input_output_num io_num; rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, io_num, sizeof(io_num)); rknn_tensor_attr input_attr; input_attr.index 0; rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, input_attr, sizeof(input_attr)); // 设置输入 rknn_input inputs[1]; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; // 或者按模型要求用FLOAT32 inputs[0].size 112 * 112 * 3; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf aligned_face_data; // 注意要和mean/std对齐 rknn_inputs_set(ctx, 1, inputs); // 推理 rknn_run(ctx, NULL); // 获取输出 rknn_output outputs[1]; outputs[0].want_float 1; // 直接拿到float特征 rknn_outputs_get(ctx, 1, outputs, NULL); float *embedding (float *)outputs[0].buf; // embedding就是512维特征向量和浮点模型比对 rknn_outputs_release(ctx, 1, outputs);几个细节提醒一下。第一输入图片数据的排列必须是连续的112x112x3的RGB buffer别在中间穿插meta信息否则内存不对齐轻则性能差重则NPU直接崩。第二want_float字段很实用设为1可以在NPU端就把INT8输出反量化为float省得在CPU端自己做反量化尤其适合人脸识别这种后处理需要浮点计算的场景。第三rknn_query这个调用我建议不要省虽然模型是固定的但开发过程中经常改输入shape动态查询一次能让代码更健壮。5.3 性能优化把NPU吃满的几个技巧模型能跑通只是第一步要做到实时还得抠性能。RV1126的NPU算力有限我的优化经验大概有这几条。首先是模型输入尺寸。RV1126的NPU对张量对齐有要求输入尺寸最好是4的倍数能对齐到16更好。MobileFaceNet用112x112输入本身是偶数一般没问题。但如果你自己改过输入分辨率比如128x96这种先确认对齐情况。其次是数据布局。RKNN对NHWC的支持通常比NCHW更高效因为内部权重重排和内存访问方式都更顺。上面C代码里我用的就是NHWC。第三是避免在NPU里做resize。人脸检测框裁剪出来后如果图尺寸和模型输入不一致别直接把不同大小的图喂给NPU先在CPU端用opencv或RGA把图resize到112x112再进NPU。RKNN的resize不是不能用但在RV1126上它占用的资源和耗时跟CPU端做一个resize差不多有时候还更慢。第四是context复用。多线程场景下不要频繁rknn_init和rknn_destroyNPU初始化开销不小。如果检测和识别两个模型并发跑建议每个线程持有自己的rknn_context避免互相干扰。我实际测试过合理复用context后整体帧率能提升20%到30%。最后说个参考数据MobileFaceNet在112x112输入、INT8量化的情况下RV1126上单次推理耗时大概在10毫秒到20毫秒这个量级。检测模型如果也控制在类似规模整套人脸识别pipeline做到10帧以上是完全可行的。具体数字受模型复杂度和NPU负载影响不建议拿别人的数据当做自己的性能指标。6. 常见问题速查表直接抄作业这里我把项目里遇到的高频问题整理成一张速查表每个问题都对应原因和解决办法遇到类似情况可以直接对着排查。现象可能原因解决办法load_onnx报错算子不支持ONNX opset版本太高或模型含后处理节点导出时用opset11裁剪后处理节点转化成功但板端加载失败工具链版本和NPU不匹配确认RV1126用rknn-toolkit 1.x不是toolkit2量化后精度大幅下降校准数据集太偏、预处理重复或缺失扩充校准集到200张以上检查mean/std输出数值固定不变激活值范围估计错误校准数据不真实换成真实场景图片重新量化量化和不量化输出完全一样可能没走NPU推理或者输入本身有问题打印输入输出shape确认推理路径板端初始化失败模型文件路径错误或运行时权限问题检查模型路径查看dmesg和rknn日志特征比对识别率低特征向量已经是降质输出先对比RKNN和浮点模型输出的余弦相似度推理性能远低于预期输入布局不对或频繁init context用NHWC、复用context、CPU端做resize这张表是这几年调模型的经验浓缩不能说覆盖所有情况但覆盖了我遇到的80%问题。新出现的问题也建议先从这几条开始排查。最后的几点实务建议模型转换这件事最后再啰嗦几句我的实操体会。最大的感受是大部分人踩的坑其实不在RKNN本身而在上游——ONNX导出不规范、预处理不统一、校准数据敷衍。转换之前花半天时间把ONNX和PyTorch的输出对齐后面能省一周的排查时间。其次量化校准数据一定要用目标设备实际场景的数据这个已经强调过多次但我还是想说这不是理论问题而是我拿一个晚上换来的教训。另外一个很实用的小技巧每次转换之前把rknn.config里用的mean、std、reorder_channel、dataset路径、rknn-toolkit版本号一起记录到项目文档里。别觉得这是小事等你过几个月回头优化模型发现自己根本记不清上次用的是哪套预处理参数那才是最崩溃的时刻。我后来养成了习惯每次成功的转换都保留一份带参数说明的配置文件团队协作和版本回溯都省力很多。最后想说的是RKNN转换这件事看起来是个工具链问题但做深了你会发现它和模型设计、数据分布、部署工程强相关。希望这篇文章能让还没开始的你少踩几个坑已经在坑里的你早点跳出来。
返回列表