公司动态

c++运行RKNN

📅 2026/8/31 14:45:23
c++运行RKNN
这里需要注意下面介绍的所有函数都是在debian系统下的RK3588芯片进行运行的其CPU框架为aarch64因此下面所有的函数需要自己核对官方文档是否为同一型号。参考rknn-toolkit2/rknn-toolkit2/examples/functions/hybrid_quant at master · airockchip/rknn-toolkit2背景之前的文章中我们介绍通过量化将模型转化为onnx格式同时在树莓派上利用c部署onnx的模型。这里我们将通过onnx这个中间态转化为rknn后将rknn格式利用c部署在rk3588上。转化好过后的fp32格式可能会含有两个文件model.onnx和model.onnx.data前者虽然显示的是fp32但是实际上是fp16格式这是因为rknn的量化没有fp32的格式这里只是为了方便阅读所以标注为fp32可以看出其大概为onnx-fp32的0.5倍同时下面的int8格式采用的是int8和fp16混合的格式大部分采用int8进行量化分割头采用fp16回退这样得到的结果虽然内存相对大一些但是精度却降低得更少。但是其实通过模型得运行内存可以看出fp32和int8之间无论是onnx还是rknn格式都没有降低太多这是因为数据采集、预处理等过程更加消耗内存同时由于其模型使用的是yolov8n-seg其本身便是最小的实例分割模型因此量化后运行内存的提升并不明显。流程准备RK3588运行环境-加载模型-模型初始化-开始推理-资源释放其中后处理的方式和onnx完全相同都是通过nms但是由于这里是实例分割模型因此使用二值掩膜输出物体的质心和角度。可以看出这里加载模型相对于onnx其实提前了这是因为在加载onnx格式之前还需要确定具体如何执行这个模型比如分配多少核CPU去处理onnx格式的并行算子是否进行图优化等。而rknn则在转化后就基本决定了怎么执行准确来说也不是提前了二是rknn将图优化、目标硬件适配等工作提前在量化阶段变完成了因此后续初始化的目的也与onnx格式不同其目标便是确定量化好的rknn与板端之间是否有通信、驱动或者硬件之间的问题同时确定好上下文和输入输出。准备RK3588运行环境由于RK各个系列之间算子的兼容性、CPU框架等都不尽相同因此这一阶段的任务主要是确认开发板的型号以及NPU是否运行等。确认系统和CPU框架首先这是一个Linux的系统我购买的是正点原子的RK3588其包含安卓和Linux两个系统安卓系统我不太熟悉因此重新安装了Linux中Debian的镜像。其本身便是Ubuntu的基础因此Ubuntu上面的只是在这里也可以使用后续都在此基础上进行讲解。查看Linux内核版本查看处理器架构最主要的便是确定处理器架构因此后续在链接动态库中需要的是Linux aarch64版本下的so文件该so文件在瑞芯微官方存在多个版本因此需要仔细分辨。通常编译时需要的文件为librknnrt.so程序运行时使用的动态库和rknn_api.h编译时使用的头文件。在此需要区分板端和PC端分别需要的文件是什么PC端通常用于做RKNN的量化因此需要完整的RKNN-Toolkit2而这个库只有Linux版本的因此这需要在你的电脑上安装一下Linux虚拟环境系统。如果你的电脑不算特别老的话这里建议是直接通过微软在自己系统上安装的WSL软件其可以直接帮你下载Ubuntu系统同时其功能和完整的Ubuntu也基本相同也无需自行分配内存其可以在指定硬盘内安装。确认芯片的NPU驱动是否正常NPU是RK3588最主要的推理模块同时瑞芯微官方封装好的函数都会使用NPU进行模型的处理。sudo dmesg | grep -Ei rknpu|npu通过sudo dmesg读取完整的内核日志然后通过grep -Ei rknpu|npu来筛选内核日志中带有rknpu或者npu的日志信息其无论大小写均可以匹配。-E是因为后面有|来表示“或者”-i则表示忽略大小写。因此其实后面还可以抓取一些错误信息如再通过管道筛选抓取sudo dmesg | grep -Ei rknpu|npu | grep -i fail当然具体的错误肯定不止fail还有很多其他的这些不做过多赘述。通过上面代码来确定NPU驱动是否正常通常并不能说明NPU驱动完全无法使用其只要不出现下述关键字都可以向下执行直接在程序端口进行进一步检查。日志关键词含义在 RK3588 NPU 中可能表示严重程度iommu faultIOMMU 地址转换异常NPU访问了没有映射、已释放或无权限的内存高page fault页地址访问异常NPU或程序访问了无效虚拟地址中到高timeout等待操作超过规定时间NPU任务长时间未完成或没有返回中断高failed to submit提交任务失败RKNN任务没有成功提交给NPU执行高device unavailable设备不可用RKNN Runtime当前无法使用NPU设备高probe failed驱动探测初始化失败NPU驱动没有成功绑定硬件很高reset设备被复位驱动主动重置NPU可能是在恢复异常看是否反复出现hang设备或任务卡住NPU任务不再继续执行高lockup内核或CPU长时间无法调度系统级卡死比普通程序卡住更严重很高segmentation fault段错误C/C程序访问了非法内存高加载模型// 模型 std::ifstream model_stream(use_model, std::ios::binary | std::ios::ate); // 模型大小 const std::streamoff model_size model_stream.tellg(); // 模型数据 std::vectoruint8_t model_data(static_caststd::size_t(model_size)); model_stream.seekg(0, std::ios::beg); model_stream.read(reinterpret_castchar*(model_data.data()),static_caststd::streamsize(model_size))首先通过fstream库读取模型信息然后使用tellg函数进行字节偏移从而得出的大小最后通过read函数将模型信息读取进容器中保存。RKNN常用函数和宏定义RKNN错误码在具体讲解模型之前首先需要补充一下RKNN中错误码的知识RKNN错误码存在于rknn_api.h中这个文件可能会因为不同架构不同因此在具体书写代码之前需要先对该文档进行一个阅读和了解。RKNN_SUCC0成功RKNN_ERR_FAIL-1一般性错误RKNN_ERR_TIMEOUT-2超时RKNN_ERR_DEVICE_UNAVAILABLE-3设备不可用RKNN_ERR_MOALLOC_FAIL-4内存分配无效RKNN_ERR_PARAM_INVALID-5参数无效RKNN_ERR_MODEL_INVALID-6模型无效查询参数信息--rknn_queryint rknn_query( rknn_context context, // 上下文权柄 rknn_query_cmd cmd, // 需要查找参数的信息 void* info, // 输出信息 uint32_t size // 输出信息的大小 ); // 返回RKNN错误码在使用这个函数之前通常需要创建该参数对应的结构体cmd需要查找的参数信息意义结构体RKNN_QUERY_SDK_VERSIONSDK和驱动版本rknn_sdk_version sdk_version{};RKNN_QUERY_IN_OUT_NUM输入输出数量rknn_input_output_num io_num{};RKNN_QUERY_INPUT_ATTR输入张量属性rknn_tensor_attr input_attr{};RKNN_QUERY_OUTPUT_ATTR输出张量属性rknn_tensor_attr output_attr{};RKNN_QUERY_MEM_SIZE模型内存信息rknn_mem_size mem_size{};RKNN_QUERY_PERF_RUN推理耗时rknn_perf_run perf_run{};RKNN_QUERY_SDK_VERSION其有两个char类型的参数api_version[256]、drv_version[256]第一个参数储存程序运行时实际加载的RKNN Runtime/API版本第二个参数表示当前NPU内核驱动版本。RKNN_QUERY_IN_OUT_NUM其有两个uint32_t类型的参数n_input、n_output第一个参数表示输入参数数量第二参数表示输出参数数量。RKNN_QUERY_INPUT_ATTR其参数较多放在下面做介绍RKNN_QUERY_OUTPUT_ATTR同RKNN_QUERY_INPUT_ATTRRKNN_QUERY_MEM_SIZE其参数如下类型参数意义uint32_ttotal_weight_size模型权重运行时需要的内存uint32_ttotal_internal_size模型推理中间张量和工作区需要的内存不包括缓冲区uint64_ttotal_dma_allocated_size当前分配的DMA相关内存总量uint32_ttotal_sram_sizeRKNN预留或使用的系统SRAM规模uint32_tfree_sram_size当前这部分预留SRAM中还剩多少可用空间uint32_treserved[10]为上面结果预留的位置这个并不能查找全部的内存信息其需要使用cat /proc/进程PID/status来进行查找其主要查看RSS当前实际占用内存和HWM运行以来最高RSS即可。RKNN_QUERY_PERF_RUN其仅有一个int64_t类型的参数run_duration表示最近一次模型推理的运行时间单位是微秒。张量属性--rknn_tensor_attr在使用查找张量属性之前需要将rknn_tensor_attr.index0字段含义对程序的作用index张量编号区分第几个输入或输出name张量名称检查是否为预期节点n_dims维度数量例如四维图像张量dims[]每个维度的大小确定输入高、宽、通道和批次n_elems元素总数检查张量元素数量size不考虑额外步长时的字节数普通逻辑数据大小size_with_stride包含对齐或步长后的字节数零拷贝申请内存时尤其重要fmtNCHW、NHWC等格式决定数据排列方式typeUINT8、INT8、FP16、FP32等决定输入数据类型qnt_type量化类型判断是否采用仿射量化等zp零点仿射量化参数scale缩放系数仿射量化或反量化参数w_stride宽方向步长判断是否存在宽度填充h_stride高方向步长相关配置设置IO内存时使用数据类型数据格式模型初始化加载并创建上下文int rknn_init( rknn_context* context, // 上下文--类成员 void* model, // 模型数据 uint32_t size, // 模型大小 uint32_t flag, // 初始化标志 rknn_init_extend* extend // 额外扩展信息 ); // 返回RKNN错误码初始化标志意义0默认无设置RKNN_FLAG_PRIOR_HIGH设置高优先级环境RKNN_FLAG_PRIOR_MEDIUM设置中优先级环境RKNN_FLAG_PRIOR_LOW设置低优先级环境RKNN_FLAG_ASYNC_MASK异步模式RKNN_FLAG_COLLECT_PERF_MASK性能采集模式RKNN_FLAG_MEM_ALLOC_OUTSIDE将所有内存分配到外部包括权重/内部/输入/输出部分RKNN_FLAG_PRIOR_HIGH/MEDIUM/LOW在系统有多个任务时通过这个标志分配任务的优先级RKNN_FLAG_ASYNC_MASK异步模型启用后rknn_outputs_get直接获取上一帧的结果从而提高单线程模式下的帧率但代价是 rknn_outputs_get 无法获取当前帧的结果。 在多线程模式下无需开启此模式。RKNN_FLAG_COLLECT_PERF_MASK启用后可通过 rknn_query(...) 中获取详细的性能报告但会降低帧率。RKNN_FLAG_MEM_ALLOC_OUTSIDE将所有内存分配到外部包括权重/内部/输入/输出部分。查询SDK和驱动版本同上面查询参数信息所述rknn_sdk_version sdk_version{}; rknn_query( context_, RKNN_QUERY_SDK_VERSION, sdk_version, sizeof(sdk_version) ) std::cout RKNN API: sdk_version.api_version , 驱动: sdk_version.drv_version std::endl;设置NPU核心int rknn_set_core_mask( rknn_context context, // 模型上下文 rknn_core_mask core_mask // ); // 返回RKNN错误码core_mask数字意义RKNN_NPU_CORE_AUTO0默认会在NPU上随机运行RKNN_NPU_CORE_01在NPU0上运行RKNN_NPU_CORE_12在NPU1上运行RKNN_NPU_CORE_24在NPU2上运行RKNN_NPU_CORE_0_1RKNN_NPU_CORE_0 | RKNN_NPU_CORE_1在NPU0和NPU1上运行RKNN_NPU_CORE_0_1_2RKNN_NPU_CORE_0_1 | RKNN_NPU_CORE_2在NPU0、NPU1和NPU2上运行RKNN_NPU_CORE_ALL0xffff自动选择据情况选择运行平台查询输入输出数量同上面查询参数信息所述rknn_input_output_num io_num_{}; rknn_query( context_, RKNN_QUERY_IN_OUT_NUM, io_num_, sizeof(io_num_))查询输入输出属性同上面查询参数信息所述std::vectorrknn_tensor_attr input_attrs_; std::vectorrknn_tensor_attr output_attrs_; input_attrs_.assign(io_num_.n_input, rknn_tensor_attr{}); for (uint32_t index 0; index io_num_.n_input; index) { input_attrs_[index].index index; rknn_query( context_, RKNN_QUERY_INPUT_ATTR, input_attrs_[index], sizeof(rknn_tensor_attr))); } output_attrs_.assign(io_num_.n_output, rknn_tensor_attr{}); for (uint32_t index 0; index io_num_.n_output; index) { output_attrs_[index].index index; rknn_query( context_, RKNN_QUERY_OUTPUT_ATTR, output_attrs_[index], sizeof(rknn_tensor_attr))); }创建输入输出缓冲区在此之前通常都需要确定这个输入和输出的维度、长度等信息是否正确。普通接口普通接口的输入由程序管理输出由RKNN Runtime自动分配。所需要的函数rknn_inputs_set(); rknn_run(); rknn_outputs_get(); rknn_outputs_release();均会在下面介绍零拷贝接口开始推理设置模型输入输入格式--rknn_inputrknn_input input{}; input.index 0; input.buf img.get_image_buffer(); input.size static_castuint32_t(img.get_image_size()); input.pass_through 0; input.type RKNN_TENSOR_UINT8; input.fmt RKNN_TENSOR_NHWC;index:输入索引buf:输入数据size:输入大小pass_through:通过模式。若为真则会将buf数据直接传递给RKNN模型的输入节点无需任何转换。若为假则会将buf数据转换为与模型相一致的输入形式其会根据以下类型和格式进行设置。因此需要设置后续变量。type:输入数据类型fmt:输入数据格式。NPU的内部输入格式默认为NCHW。设置输入数据--rknn_inputs_setrknn_inputs_set(context_, 1, input)输入:context:模型上下文n_inputs:输入数量input[]:输入信息输出:RKNN错误码执行推理--rknn_runrknn_run(context_, nullptr)输入:context:模型上下文extend:输出:RKNN错误码获取输出输出格式--_rknn_outputstd::vectorrknn_output outputs(io_num_.n_output); for (uint32_t index 0; index io_num_.n_output; index) { outputs[index].index index; outputs[index].want_float 1; outputs[index].is_prealloc 0; }输入:want_float:是否将模型原始输出通过反量化转化为float32is_prealloc:输出缓存区是否已经提前申请好了这是也是普通接口和零拷贝接口最明显的特征当为false时下面的参数无法填写当为true时则需要填写index:输出索引buf:输出数据size:输出大小释放上一帧出错时的资源在程序运行中获得输出数据后将输出数据缓存在输出缓存区但是在后续后处理中出现问题提前return或者抛出异常输出缓存区中的数据并不会自动清空因此在每次分配输出缓存区时需要释放上一帧出错的资源。但是如果上一帧没有出错这一帧仍然释放则会出现重复释放的问题因此通常需要一个标志符判断。class RknnOutputsGuard { public: RknnOutputsGuard(rknn_context context, std::vectorrknn_output outputs) : context_(context), outputs_(outputs) { } RknnOutputsGuard(const RknnOutputsGuard) delete; RknnOutputsGuard operator(const RknnOutputsGuard) delete; void arm() noexcept { active_ true; } ~RknnOutputsGuard() { if (!active_) { return; } const int result rknn_outputs_release( context_, static_castuint32_t(outputs_.size()), outputs_.data()); if (result ! RKNN_SUCC) { std::cerr rknn_outputs_release 失败RKNN 错误码: result std::endl; } } private: rknn_context context_; std::vectorrknn_output outputs_; bool active_{false}; }; RknnOutputsGuard output_guard(context_, outputs);其实这也类似于多线程中的RAII资源守卫在成功获取输出数据后调用类成员函数arm将标识符转为true。设置输出数据--rknn_outputs_setrknn_outputs_get(context_, io_num_.n_output, outputs.data(), nullptr)输入:context:模型上下文n_outputs:输出个数outputs[]:输出数据extend:输出扩展内容输出:RKNN错误码获取输出数据const rknn_output detection_output outputs[detection_output_index_]; const rknn_output prototype_output outputs[prototype_output_index_];后处理后处理的详细介绍可以查看c运行onnx模型这篇文章后处理都相同这里不做过多介绍资源释放由于RK3588有自己的上下文因此需要在析构函数中删除而官方的rknn_destroy函数便是实现此功能void ClassifyModel::destroy_context() noexcept { if (context_ 0) { return; } const int result rknn_destroy(context_); if (result ! RKNN_SUCC) { std::cerr rknn_destroy 失败RKNN 错误码: result std::endl; } context_ 0; }后台查看模型运行情况查看RK3588三个核心的实时负载sudo cat /sys/kernel/debug/rknpu/load查看RK3588运行内存sudo cat /proc/进程PID/status