RV1126B 把 3TOPS@INT8 的 NPU 塞进了边缘视觉 SoC 里,但把 ONNX 模型真正跑到这块 NPU 上并拿到可用的精度与延迟,中间隔着一条比“convert 一下”长得多的路。以下流程以 RKNN-Toolkit2 2.3.2 + target_platform='rv1126b' 为基准,覆盖从模型契约确认到板端 C API 推理的完整链路。
先确认模型契约,再碰转换脚本
RV1126B 的 NPU 支持 INT4/INT8/INT16/FP16 混合精度运算,并针对 Transformer 结构做了优化。但“支持”不等于“任何模型丢进去都能跑”。转换之前必须把输入布局、颜色顺序、归一化方式、输出语义固定下来。RKNN-Toolkit2 不会替你做这些决策;如果 ONNX 的输入契约和板端预处理不一致,FP16 模型会跑出“看起来正常但精度差一截”的结果,INT8 阶段会把这个差距放大。
推荐 ONNX 使用静态 batch=1 输入。动态 shape 在 RV1126B 的 NPU 上需要额外注意算子支持,静态输入能省掉后续排障的一大类变量。
工具链环境:版本对齐比“装最新”重要
RV1126B 的模型转换在 x86_64 Linux PC 或 WSL2 上完成,不要在板端直接转。RKNN-Toolkit2 必须是主机端转换工具包,不能用rknn-toolkit-lite2替代;后者是板端运行时,不是转换器。
版本匹配是硬约束。target_platform必须显式设为'rv1126b',Toolkit 版本需要与板端固件中的librknnrt.so对齐。Seeed 量产固件的内部基线是 RKNN 2.3.2,设备上的/oem/usr/lib/librknnrt.so是唯一可信来源,不要从其他板子拷贝运行时库。
python
from rknn.api import RKNN rknn = RKNN(verbose=True) rknn.config( target_platform='rv1126b', mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], quantized_dtype='asymmetric_quantized-8', )mean_values/std_values必须和训练、ONNX 导出时的预处理严格一致。INT8 量化会把这个契约“焊死”在量化参数里,写错一个系数,后面用校准集也救不回来。
先出 FP16 基线,再谈 INT8
一个容易被跳过的步骤是先生成 FP16 模型。FP16 不是最终交付物,但它是判断“问题出在计算图/预处理,还是出在量化”的基线。如果 FP16 模型在板端精度就不对,INT8 只会让定位更难。
python
# FP16 基线 rknn.load_onnx(model='model.onnx') rknn.build(do_quantization=False) rknn.export_rknn('model_fp16.rknn')FP16 在 RV1126B 上可以跑,但算力利用率和内存带宽效率不如 INT8。它存在的意义是给你一个“不带量化噪声的参照点”。
INT8 量化:校准集不是“随便找 20 张图”
INT8 的核心是校准。RKNN-Toolkit2 通过rknn.build(do_quantization=True, dataset='./calib.txt')完成量化,calib.txt中每行是一张校准图片的路径。校准集的质量直接决定量化后模型能不能用。
校准集必须从与部署场景同分布的数据中抽取,覆盖模型实际会遇到的输入变化范围。对于检测模型,通常 100–500 张有代表性的图片是一个合理的起点;数量不是关键,覆盖度才是。校准集中如果没有夜间、逆光、小目标等困难样本,量化后的 NPU 在这些场景上会率先失效。有实践记录显示,同一校准集下 RKNN INT8 量化后的 mAP 相比 ONNX 基线下降可达 12%,其中相当一部分损失来自校准数据与部署分布不匹配。
python
# INT8 量化 rknn.load_onnx(model='model.onnx') rknn.build(do_quantization=True, dataset='./calib.txt') rknn.export_rknn('model_int8.rknn')RV1126B 还支持 W4A16 / W8A16 混合精度量化以及 INT8/INT16 混合运算。如果 INT8 全量化在某个算子上精度掉得太厉害,可以针对该算子保留 INT16,而不是把整个模型退回 FP16。这需要配合rknn.config()中的量化策略和逐层精度分析来定位。
板端推理:C API 是生产路径,Python 不是
.rknn文件在板端通过 RKNN Runtime 的 C API 执行。核心调用链很短:
c
#include "rknn_api.h" rknn_context ctx = 0; rknn_init(&ctx, model_data, model_size, 0, NULL); // 设置输入 rknn_input inputs[1]; inputs[0].index = 0; inputs[0].buf = input_data; inputs[0].size = input_size; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].fmt = RKNN_TENSOR_NHWC; rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, NULL); rknn_output outputs[io_num.n_output]; // ... 查询输出属性、获取输出、后处理 rknn_outputs_get(ctx, io_num.n_output, outputs, NULL); rknn_outputs_release(ctx, io_num.n_output, outputs); rknn_destroy(ctx);推理完成后可以用rknn_query(ctx, RKNN_QUERY_PERF_RUN, ...)查询本次运行的耗时信息,用于验证 NPU 是否真正承接了计算。如果耗时和 CPU 推理差不多,通常意味着模型中有大量算子回退到了 CPU,需要回去检查 ONNX 算子兼容性或量化配置。
Python 路径(rknn-toolkit-lite2)适合快速验证和原型,但生产部署应走 C/C++ +librknnrt.so,避免 Python 运行时开销和部署依赖问题。
一个务实的精度—性能检查顺序
拿到 INT8 模型后,不要直接在板端看单帧延迟就宣布“部署完成”。建议按以下顺序检查:
板端 FP16 精度 vs ONNX 精度:确认计算图和预处理无误。
板端 INT8 精度 vs 板端 FP16 精度:量化损失是否在可接受范围内。如果差距过大,先扩校准集、再考虑混合精度。
NPU 耗时占比:用
RKNN_QUERY_PERF_RUN确认推理确实落在 NPU 上;若大量算子回退 CPU,INT8 的延迟优势会被吃掉。内存占用:必要时用
RKNN_FLAG_MEM_ALLOC_OUTSIDE+RKNN_QUERY_MEM_SIZE查询权重和中间 tensor 的内存分布,判断是否需要在模型切分或算子融合上做调整。
RV1126B 的 3TOPS 是一个“够用但不宽裕”的算力水位。INT8 部署的真正门槛不在转换命令本身,而在校准集是否忠于部署分布、FP16 基线是否干净、以及板端是否真的在 NPU 上执行。这三点守住,INT8 才能在精度和延迟之间给出可预期的收益。
本文实践项目由洛阳澜荣科技完成,团队位于洛阳洛龙,专注YOLO工业视觉、大模型微调、内网私有化RAG知识库落地。 技术原文首发:https://www.golamp.cn