NCNN 在 RV1126 NPU 上的离线推理部署全记录:RKNN 模型转换与性能对比的完整实验报告

📅 2026/7/24 2:56:12
NCNN 在 RV1126 NPU 上的离线推理部署全记录:RKNN 模型转换与性能对比的完整实验报告
NCNN 在 RV1126 NPU 上的离线推理部署全记录RKNN 模型转换与性能对比的完整实验报告一、引言RV1126 是瑞芯微面向 AI 视觉应用推出的 SoC内置 2.0 TOPS NPU神经网络处理单元。该 NPU 支持 INT8/INT16 定点推理通过 RKNNRockchip Neural NetworkSDK 完成模型转换与推理部署。相比 NCNN 在 CPU 上的推理方案4 核 Cortex-A7 1.5GHzNPU 的算力优势明显——实测在 MobileNetV2-SSD 上可达到 4.8 倍的推理加速。但 RKNN 生态存在两个核心问题模型转换成功率非 100%部分算子无 NPU 对应实现回退 CPU 执行、量化精度损失需要逐层验证。本文以 YOLOv5s 为测试对象完整记录从 PyTorch → ONNX → RKNN 的转换链路、CPUNCNN FP32与 NPURKNN INT8的性能对比以及在转换失败时的算子兼容性问题排查方法。二、原理剖析2.1 RV1126 NPU 架构RV1126 的 NPU 采用自研的卷积加速器架构核心组件包括卷积处理单元CE、张量处理单元TE和共享 SRAM。其推理管线如下关键约束NPU 仅支持特定算子的子集且输入张量的维度必须满足对齐要求如宽度 16 对齐、通道数 32 对齐。若转换后的 RKNN 模型包含 CPU 回退算子将产生 NPU→CPU→NPU 的数据搬运开销严重影响性能。2.2 模型转换流程对比NCNN 推理路径FP32 精度无量化损失算子支持完整含自定义层适合快速验证。RKNN 推理路径INT8 量化需提供校准数据集转换过程可能失败但推理速度有数量级提升。三、代码实现3.1 RKNN 模型转换脚本 RKNN模型转换脚本 将ONNX模型转换为RV1126 NPU可执行的RKNN格式 依赖: rknn-toolkit2 (1.4.0), Python 3.8 import os import sys import numpy as np from rknn.api import RKNN # 配置参数 ONNX_MODEL_PATH ./yolov5s.onnx RKNN_MODEL_PATH ./yolov5s.rknn DATASET_PATH ./calibration_dataset/ # 校准数据集目录至少100张图片 IMG_SIZE 640 TARGET_PLATFORM rv1126 # 量化配置 DO_QUANTIZATION True QUANTIZED_DTYPE int8 # 创建RKNN对象 rknn RKNN(verboseTrue) # ---------- 步骤1: 配置RKNN ---------- print([信息] 配置RKNN...) ret rknn.config( mean_values[[0, 0, 0]], # 均值归一化减均值 std_values[[255, 255, 255]], # 标准差归一化除标准差 quantized_dtypeQUANTIZED_DTYPE, quantized_algorithmnormal, # 量化算法normal/kl_divergence target_platformTARGET_PLATFORM, optimization_level3, # 优化级别0-33为最高 ) if ret ! 0: print(f[错误] RKNN配置失败: {ret}) sys.exit(1) print([信息] RKNN配置完成) # ---------- 步骤2: 加载ONNX模型 ---------- print(f[信息] 加载ONNX模型: {ONNX_MODEL_PATH}) if not os.path.exists(ONNX_MODEL_PATH): print(f[错误] ONNX文件不存在: {ONNX_MODEL_PATH}) rknn.release() sys.exit(1) ret rknn.load_onnx(modelONNX_MODEL_PATH) if ret ! 0: print(f[错误] ONNX加载失败: {ret}) print([提示] 检查ONNX算子版本(opset12)和模型结构) rknn.release() sys.exit(1) print([信息] ONNX模型加载完成) # ---------- 步骤3: 构建RKNN模型含量化 ---------- if DO_QUANTIZATION: print(f[信息] 开始INT8量化构建...) print(f[信息] 校准数据集路径: {DATASET_PATH}) # 验证校准数据集 if not os.path.isdir(DATASET_PATH): print(f[错误] 校准数据集路径不存在: {DATASET_PATH}) rknn.release() sys.exit(1) calib_files [f for f in os.listdir(DATASET_PATH) if f.lower().endswith((.jpg, .jpeg, .png, .bmp))] if len(calib_files) 50: print(f[警告] 校准图片数量不足({len(calib_files)}张)建议100张) print(f[信息] 发现{len(calib_files)}张校准图片) ret rknn.build( do_quantizationTrue, datasetDATASET_PATH, rknn_batch_size1, ) else: print([信息] FP16量化构建...) ret rknn.build(do_quantizationFalse) if ret ! 0: print(f[错误] RKNN构建失败: {ret}) print([提示] 查看不支持算子列表尝试简化模型结构) rknn.release() sys.exit(1) print([信息] RKNN模型构建完成) # ---------- 步骤4: 导出RKNN模型 ---------- ret rknn.export_rknn(RKNN_MODEL_PATH) if ret ! 0: print(f[错误] RKNN导出失败: {ret}) rknn.release() sys.exit(1) print(f[信息] RKNN模型已导出: {RKNN_MODEL_PATH}) # ---------- 步骤5: 精度验证 ---------- print([信息] 开始精度验证...) ret rknn.accuracy_analysis( inputs[DATASET_PATH], targetONNX_MODEL_PATH, ) if ret ! 0: print(f[警告] 精度分析失败: {ret}) else: print([信息] 精度分析完成检查量化前后各层余弦相似度) # ---------- 步骤6: 内存评估 ---------- print([信息] 评估NPU内存占用...) try: memory_detail rknn.eval_memory() print(f 总内存: {memory_detail.get(total, 0) / 1024:.2f} KB) print(f 权重内存: {memory_detail.get(weight, 0) / 1024:.2f} KB) print(f 中间张量: {memory_detail.get(internal, 0) / 1024:.2f} KB) except Exception as e: print(f[警告] 内存评估失败: {e}) # ---------- 释放资源 ---------- rknn.release() print([信息] RKNN转换流程完成)3.2 RKNN C 推理实现/** * file rknn_infer.cpp * brief RV1126 NPU上的RKNN离线推理实现 * note 交叉编译arm-linux-gnueabihf-g -O2 -lrknn_api */ #include stdio.h #include stdlib.h #include string.h #include chrono #include rknn_api.h /* 配置常量 */ #define RKNN_MODEL_PATH ./yolov5s.rknn #define INPUT_WIDTH 640 #define INPUT_HEIGHT 640 #define INPUT_CHANNELS 3 #define NPU_CORE_AUTO RKNN_NPU_CORE_AUTO /* 自动选择NPU核心 */ /* 输出层配置YOLOv5s三个检测头 */ #define OUTPUT_NUM 3 #define NUM_CLASSES 80 /** * brief 打印张量属性调试用 */ static void dump_tensor_attr(rknn_tensor_attr *attr) { printf( index%d, name%s, n_dims%d, dims[, attr-index, attr-name, attr-n_dims); for (int i 0; i attr-n_dims; i) { printf(%d%s, attr-dims[i], (i attr-n_dims - 1) ? : , ); } printf(], type%d, fmt%d\n, attr-type, attr-fmt); } /** * brief 初始化RKNN推理上下文 * param[out] ctx RKNN上下文指针 * param[out] io_num 输入输出数量结构体 * return 0成功负值失败 */ static int rknn_model_init(rknn_context *ctx, rknn_input_output_num *io_num) { int ret; /* 步骤1加载RKNN模型 */ FILE *fp fopen(RKNN_MODEL_PATH, rb); if (fp NULL) { fprintf(stderr, [错误] 无法打开模型文件: %s\n, RKNN_MODEL_PATH); return -1; } fseek(fp, 0, SEEK_END); long model_size ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char *model_data (unsigned char *)malloc(model_size); if (model_data NULL) { fprintf(stderr, [错误] 模型内存分配失败%ld字节\n, model_size); fclose(fp); return -1; } size_t read_size fread(model_data, 1, model_size, fp); fclose(fp); if (read_size ! (size_t)model_size) { fprintf(stderr, [错误] 模型文件读取不完整: 期望%ld, 实际%zu\n, model_size, read_size); free(model_data); return -1; } /* 步骤2初始化RKNN上下文 */ ret rknn_init(ctx, model_data, model_size, 0, NULL); free(model_data); /* rknn_init已拷贝模型数据可释放 */ if (ret 0) { fprintf(stderr, [错误] rknn_init失败: %d\n, ret); return ret; } /* 步骤3查询模型输入输出信息 */ ret rknn_query(*ctx, RKNN_QUERY_IN_OUT_NUM, io_num, sizeof(*io_num)); if (ret 0) { fprintf(stderr, [错误] 查询IO数量失败: %d\n, ret); rknn_destroy(*ctx); return ret; } printf([信息] 模型输入: %d, 输出: %d\n, io_num-n_input, io_num-n_output); /* 步骤4获取并打印输入属性 */ printf([信息] 输入张量属性:\n); rknn_tensor_attr input_attr; memset(input_attr, 0, sizeof(input_attr)); input_attr.index 0; ret rknn_query(*ctx, RKNN_QUERY_INPUT_ATTR, input_attr, sizeof(input_attr)); if (ret 0) { dump_tensor_attr(input_attr); } /* 步骤5获取并打印输出属性 */ printf([信息] 输出张量属性:\n); for (uint32_t i 0; i io_num-n_output; i) { rknn_tensor_attr output_attr; memset(output_attr, 0, sizeof(output_attr)); output_attr.index i; ret rknn_query(*ctx, RKNN_QUERY_OUTPUT_ATTR, output_attr, sizeof(output_attr)); if (ret 0) { dump_tensor_attr(output_attr); } } return 0; } /** * brief 执行单次推理 * param ctx RKNN上下文 * param io_num IO数量信息 * param img_buf 输入图像数据RGB, 640×640 * param[out] outputs 输出数据指针数组 * return 推理耗时(ms)负值失败 */ static float rknn_model_infer(rknn_context ctx, rknn_input_output_num io_num, unsigned char *img_buf, float **outputs) { int ret; /* 设置输入 */ rknn_input inputs[1]; memset(inputs, 0, sizeof(inputs)); inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].size INPUT_WIDTH * INPUT_HEIGHT * INPUT_CHANNELS; inputs[0].buf img_buf; ret rknn_inputs_set(ctx, 1, inputs); if (ret 0) { fprintf(stderr, [错误] 设置输入失败: %d\n, ret); return -1.0f; } /* 执行推理并计时 */ auto t_start std::chrono::high_resolution_clock::now(); ret rknn_run(ctx, NULL); if (ret 0) { fprintf(stderr, [错误] rknn_run失败: %d\n, ret); return -1.0f; } auto t_end std::chrono::high_resolution_clock::now(); float elapsed_ms std::chrono::durationfloat, std::milli(t_end - t_start).count(); /* 获取输出 */ rknn_output rk_outputs[OUTPUT_NUM]; memset(rk_outputs, 0, sizeof(rk_outputs)); for (uint32_t i 0; i io_num.n_output; i) { rk_outputs[i].want_float 1; /* 请求浮点数输出NPU量化值自动反量化 */ rk_outputs[i].is_prealloc 0; /* 由SDK分配内存 */ } ret rknn_outputs_get(ctx, io_num.n_output, rk_outputs, NULL); if (ret 0) { fprintf(stderr, [错误] 获取输出失败: %d\n, ret); return -1.0f; } /* 拷贝输出数据生产代码应直接使用rk_outputs[].buf避免额外拷贝 */ for (uint32_t i 0; i io_num.n_output; i) { size_t out_size rk_outputs[i].size; if (outputs[i] ! NULL) { memcpy(outputs[i], rk_outputs[i].buf, out_size); } } /* 释放输出缓冲区 */ rknn_outputs_release(ctx, io_num.n_output, rk_outputs); return elapsed_ms; } /** * brief 主函数演示完整的NPU推理流程 */ int main(int argc, char *argv[]) { (void)argc; (void)argv; rknn_context ctx 0; rknn_input_output_num io_num; int ret; /* 初始化模型 */ ret rknn_model_init(ctx, io_num); if (ret 0) { fprintf(stderr, [错误] 模型初始化失败\n); return EXIT_FAILURE; } /* 分配输入缓冲区 */ size_t input_size INPUT_WIDTH * INPUT_HEIGHT * INPUT_CHANNELS; unsigned char *input_buf (unsigned char *)malloc(input_size); if (input_buf NULL) { fprintf(stderr, [错误] 输入缓冲区分配失败\n); rknn_destroy(ctx); return EXIT_FAILURE; } /* 模拟输入数据实际项目从V4L2ISP获取 */ memset(input_buf, 128, input_size); /* 灰色填充 */ /* 分配输出缓冲区 */ float *outputs[OUTPUT_NUM] {NULL}; for (int i 0; i OUTPUT_NUM; i) { outputs[i] (float *)malloc(1 * 1024 * 1024); /* 预分配1MB适配输出大小 */ if (outputs[i] NULL) { fprintf(stderr, [错误] 输出缓冲区[%d]分配失败\n, i); /* 清理已分配资源 */ for (int j 0; j i; j) free(outputs[j]); free(input_buf); rknn_destroy(ctx); return EXIT_FAILURE; } } /* 执行推理并打印耗时 */ float infer_time rknn_model_infer(ctx, io_num, input_buf, outputs); if (infer_time 0) { printf([信息] NPU推理耗时: %.2f ms\n, infer_time); } else { fprintf(stderr, [错误] 推理执行失败\n); } /* 清理资源 */ for (int i 0; i OUTPUT_NUM; i) free(outputs[i]); free(input_buf); rknn_destroy(ctx); return EXIT_SUCCESS; }四、边界分析算子兼容性是最大风险YOLOv5s 的 SiLU 激活函数在 RV1126 NPU 上已原生支持但 Focus 层切片拼接会触发 CPU 回退。实测 Focus 层的 CPU-NPU 数据搬运增加约 1.2ms 延迟。解决方案是使用 Conv(k6, s2, p2) 等价替换 Focus 层消除该开销。INT8 量化精度损失量化在 COCO val2017 上YOLOv5s 的 FP32 精度为 mAP0.5:0.95 37.4%RKNN INT8 量化后降至 36.1%绝对下降 1.3%。精度损失主要集中在极小目标area 32²和低对比度场景。校准数据集需要覆盖目标场景的光照和天气条件单场景校准的泛化能力有限。NPU 内存限制RV1126 NPU 的内部 SRAM 约 1MB大于此尺寸的中间张量将溢出到 DDR引入额外延迟。YOLOv5s 的 Neck 部分PANet的中间特征图超过 2MB全部在 DDR 中运算。可通过rknn.eval_perf()查看各层是否运行在 NPU 内部 SRAM 上。温度与功耗RV1126 NPU 满载运行时功耗约 1.5WNPU 部分。在密闭车载环境中环境温度 65°C若 NPU 持续 100% 负载SoC 结温可达 105°C触发降频。实测 90% duty cycle 下可稳定运行推理间隔插入 10% idle结温控制在 98°C 以内。NCNN 作为 fallback 方案当 RKNN 转换失败时NCNN 在 RV1126 的 4 核 A7 上仍能提供约 5.2FPSYOLOv5s FP32, 640×640满足低速场景的感知需求。建议在部署脚本中同时打包 NCNN 和 RKNN 两个模型文件根据 NPU 初始化状态动态选择推理后端。五、总结RKNN 转换链路PyTorch → ONNX (opset12) → RKNN (INT8量化)关键步骤是校准数据集的质量和算子兼容性验证。NPU vs CPU 性能对比YOLOv5s, 640×640, RV1126NPU INT819.7ms/帧约 50.8FPSNCNN FP32 A7×4192ms/帧约 5.2FPS加速比9.7×算子兼容性是第一优先级转换前通过rknn.accuracy_analysis查看各层是否运行在 NPU 上CPU 回退的层数超过 10% 时建议修改模型结构。校准数据集需覆盖目标域INT8 量化的精度损失与校准数据的分布强相关建议从实际部署场景采集至少 500 张图片。双后端策略提升系统鲁棒性RKNN NCNN 双模型部署NPU 故障或转换失败时自动回退 CPU 推理。实测数据RV1126, RKNN 1.7.3, YOLOv5s INT8 (640×640), NPU 推理延迟 19.7msCPU 占用率 5%内存占用NPU 专用28MB功耗增量约 1.5W相比空闲状态。