RK3588使用RKNPU2实现0拷贝推理
使用RKNPU2实现模型推理的两种方式:传统API流程和零拷贝API流程。传统流程包括模型初始化、数据设置、推理执行和结果获取四个步骤,存在数据拷贝开销。零拷贝流程通过显式分配NPU共享内存(rknn_create_mem)、直接内存绑定(rknn_set_io_mem)和内存复用,避免了输入输出数据的额外拷贝,显著提升推理性能。两种方式均包含完整的代码实现示例,涵盖图像预处理、模型查询、推理执行
首先,使用rknn_toolkit2工具将训练好的模型转换为RKNN模型,然后就可以开始使用RKNPU2实现推理了
通用api的使用流程

1. 初始化阶段
-
调用
rknn_init接口创建rknn_context对象并加载RKNN模型
2. 模型信息获取
-
调用
rknn_query接口查询模型输入输出属性、推理时间、SDK版本等信息
3. 推理执行阶段
-
调用
rknn_inputs_set设置模型输入数据 -
调用
rknn_run执行模型推理 -
调用
rknn_outputs_get获取推理输出数据
4. 后处理与资源释放
-
对输出数据进行后处理
-
调用
rknn_outputs_release释放输出资源 -
调用
rknn_destroy释放rknn_context及相关资源
#include <stdio.h>
#include "rknn_api.h"
#include "opencv2/core/core.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include "string.h"
using namespace cv;
static int rknn_GetTop(float* pfProb, float* pfMaxProb, uint32_t* pMaxClass, uint32_t outputCount, uint32_t topNum)
{
uint32_t i, j;
#define MAX_TOP_NUM 20
if (topNum > MAX_TOP_NUM)
return 0;
memset(pfMaxProb, 0, sizeof(float) * topNum);
memset(pMaxClass, 0xff, sizeof(float) * topNum);
for (j = 0; j < topNum; j++) {
for (i = 0; i < outputCount; i++) {
if ((i == *(pMaxClass + 0)) || (i == *(pMaxClass + 1)) || (i == *(pMaxClass + 2)) || (i == *(pMaxClass + 3)) ||
(i == *(pMaxClass + 4))) {
continue;
}
if (pfProb[i] > *(pfMaxProb + j)) {
*(pfMaxProb + j) = pfProb[i];
*(pMaxClass + j) = i;
}
}
}
return 1;
}
int main(int argc, char *argv[])
{
char *model_path = argv[1]; //从命令行参数获取模型路径
char *image_path = argv[2]; //从命令行参数获取图像路径
//初始化RKNN上下文并加载模型
rknn_context context;
rknn_init(&context, model_path, 0, 0, NULL);
//查询模型输入输出数量
rknn_input_output_num io_num;
rknn_query(context, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
printf("model input num: %d, output num: %d\n", io_num.n_input, io_num.n_output);
//读取并预处理输入图像
cv::Mat img = cv::imread(image_path);
cv::cvtColor(img, img, cv::COLOR_BGR2RGB);
//设置模型输入数据
rknn_input inputs[1];
memset(inputs, 0, sizeof(rknn_input)); //清空输入结构体
inputs[0].index = 0; //输入索引
inputs[0].buf = img.data; //图像数据指针
inputs[0].size = img.rows * img.cols * img.channels() * sizeof(uint8_t); //输入数据大小
inputs[0].pass_through = 0; //启用数据预处理
inputs[0].type = RKNN_TENSOR_UINT8; //数据类型为无符号8位整数
inputs[0].fmt = RKNN_TENSOR_NHWC; //数据格式为NHWC
rknn_inputs_set(context, io_num.n_input, inputs); //设置输入数据
//执行模型推理
rknn_run(context, NULL);
//获取推理输出结果
rknn_output outputs[1];
memset(outputs, 0, sizeof(outputs)); //清空输出结构体
outputs[0].index = 0; //输出索引
outputs[0].is_prealloc = 0; //由SDK自动分配内存
outputs[0].want_float = 1; //输出浮点数格式
rknn_outputs_get(context, 1, outputs, NULL); //获取输出数据
//后处理:获取Top5分类结果
for (int i = 0; i < io_num.n_output; i++)
{
uint32_t MaxClass[5]; //存储Top5类别索引
float fMaxProb[5]; //存储Top5概率值
float* buffer = (float*)outputs[i].buf; //输出数据缓冲区
uint32_t sz = outputs[i].size / 4; //输出数据大小(浮点数个数)
//获取Top5结果
rknn_GetTop(buffer, fMaxProb, MaxClass, sz, 5);
//打印Top5分类结果
printf(" --- Top5 ---\n");
for (int i = 0; i < 5; i++) {
printf("%3d: %8.6f\n", MaxClass[i], fMaxProb[i]);
}
}
//释放输出资源
rknn_outputs_release(context, 1, outputs);
//销毁RKNN上下文,释放所有相关资源
rknn_destroy(context);
return 0;
}
编译生成 install文件后后
install文件就是我们要放在开发板上运行的文件夹
使用adb拷贝过去 adb push ./install /
./resnet18 ./model/RK3588/resnet18.rknn ./model/space_shuttle_224.jpg
对于一些已经生成 install 然后移动了位置,报错的项目,可以 rm -rf build # 删除整个 build 目录
然后重新build一下
0拷贝api的使用流程
不使用0拷贝:
[CPU内存] --拷贝--> [NPU内部内存] --推理--> [NPU内部内存] --拷贝--> [CPU内存]
两次数据拷贝(输入一次,输出一次):
-
在嵌入式设备(如RK3588)上,CPU ↔ NPU 的拷贝是 性能瓶颈。
-
拷贝耗时可能比推理本身还长
0拷贝:
[NPU共享内存] --直接使用--> [推理] --直接访问--> [NPU共享内存]
用 memcpy()把图像数据 直接写入 input_mem[0]->virt_addr(NPU内存)。所以要提前rknn_create_mem(),直接分配 NPU可访问的内存。这块内存 同时能被CPU和NPU访问(共享内存)。
-
初始化阶段
-
rknn_init():创建RKNN上下文并加载模型 -
rknn_query(RKNN_QUERY_INPUT_ATTR/OUTPUT_ATTR):查询输入输出tensor属性(包括维度、数据类型等)
-
-
内存准备阶段(关键区别点)
-
rknn_create_mem():显式创建输入输出内存空间 -
输入内存大小根据
input_attr.size_with_stride确定 -
输出内存大小根据
output_attr.n_elems * sizeof(float)计算 -
memcpy():将图像数据拷贝到已创建的输入内存
-
-
内存绑定阶段
-
rknn_set_io_mem():将创建的内存与RKNN上下文绑定 -
需要明确指定tensor的数据类型(如UINT8/FLOAT32)
-
绑定后NPU直接使用这些内存区域
-
-
推理执行阶段
-
rknn_run():执行模型推理 -
由于已预先绑定内存,无需在推理时额外拷贝数据
-
-
结果处理阶段
-
直接从绑定的输出内存读取结果(
output_mem[0]->virt_addr) -
使用
rknn_GetTopN()处理原始输出数据
-
-
资源释放阶段
-
rknn_destroy_mem():释放创建的输入输出内存 -
rknn_destroy():销毁RKNN上下文
-
对比传统API的主要优势:
-
避免了推理过程中额外的数据拷贝
-
内存生命周期由开发者显式控制
-
支持更灵活的内存布局(如带stride的输入)
-
适用于需要高性能的场景
#include <stdio.h>
#include "rknn_api.h"
#include "opencv2/core/core.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include "string.h"
using namespace cv;
//获取概率最高的topN分类结果
static int rknn_GetTopN(float* pfProb, float* pfMaxProb, uint32_t* pMaxClass, uint32_t outputCount, uint32_t topNum)
{
uint32_t i, j;
uint32_t top_count = outputCount > topNum ? topNum : outputCount;
//初始化topN结果数组
for (i = 0; i < topNum; ++i) {
pfMaxProb[i] = -FLT_MAX;
pMaxClass[i] = -1;
}
//遍历找出概率最高的topN类别
for (j = 0; j < top_count; j++) {
for (i = 0; i < outputCount; i++) {
if ((i == *(pMaxClass + 0)) || (i == *(pMaxClass + 1)) || (i == *(pMaxClass + 2)) || (i == *(pMaxClass + 3)) ||
(i == *(pMaxClass + 4))) {
continue;
}
if (pfProb[i] > *(pfMaxProb + j)) {
*(pfMaxProb + j) = pfProb[i];
*(pMaxClass + j) = i;
}
}
}
return 1;
}
int main(int argc, char *argv[])
{
char *model_path = argv[1]; //模型文件路径
char *image_path = argv[2]; //输入图像路径
//初始化RKNN模型上下文
rknn_context context;
rknn_init(&context, model_path, 0, 0, NULL);//返回rknn_context句柄,等后续api调用
//读取并预处理输入图像
cv::Mat img = cv::imread(image_path);
cv::cvtColor(img, img, cv::COLOR_BGR2RGB);//BGR->RGB(深度学习模型使用)
//查询模型输入输出tensor属性
rknn_tensor_attr input_attr[1],output_attr[1];
memset(input_attr,0,sizeof(rknn_tensor_attr)); //清空输入属性结构体
memset(output_attr,0,sizeof(rknn_tensor_attr)); //清空输出属性结构体
rknn_query(context,RKNN_QUERY_INPUT_ATTR,input_attr,sizeof(input_attr)); //获取输入属性
rknn_query(context,RKNN_QUERY_OUTPUT_ATTR,output_attr,sizeof(output_attr)); //获取输出属性
//0拷贝内存管理,1、为输入输出数据申请NPU可访问的内存,避免额外拷贝
rknn_tensor_mem *input_mem[1],*output_mem[1];
input_mem[0] = rknn_create_mem(context,input_attr[0].size_with_stride); //创建输入内存
output_mem[0] = rknn_create_mem(context,output_attr[0].n_elems*sizeof(float)); //创建输出内存
//将图像数据拷贝到输入内存
unsigned char *input_data = img.data;
memcpy(input_mem[0]->virt_addr, input_data, input_attr[0].size_with_stride);
//设置输入输出内存属性并绑定到RKNN上下文,NPU会直接使用这些内存进行推理
input_attr[0].type = RKNN_TENSOR_UINT8; //设置输入数据类型
output_attr[0].type = RKNN_TENSOR_FLOAT32; //设置输出数据类型
rknn_set_io_mem(context, input_mem[0], input_attr); //绑定输入内存
rknn_set_io_mem(context, output_mem[0], output_attr); //绑定输出内存
//执行模型推理
rknn_run(context, NULL);
//获取并输出top5分类结果
uint32_t topNum = 5;
uint32_t MaxClass[topNum];
float fMaxProb[topNum];
float* buffer = (float*)output_mem[0]->virt_addr; //输出数据指针
uint32_t sz = output_attr[0].n_elems; //输出数据元素个数
int top_count = sz > topNum ? topNum : sz; //实际输出的topN数量
rknn_GetTopN(buffer, fMaxProb, MaxClass, sz, topNum); //计算topN结果
//打印分类结果
printf("---- Top%d ----\n", top_count);
for (int j = 0; j < top_count; j++) {
printf("%8.6f - %d\n", fMaxProb[j], MaxClass[j]);
}
//释放申请的内存资源
rknn_destroy_mem(context, input_mem[0]); //释放输入内存
rknn_destroy_mem(context, output_mem[0]); //释放输出内存
//销毁RKNN上下文
rknn_destroy(context);
return 0;
}
更多推荐

所有评论(0)