RK3588上面带了一个6T算力的NPU。目前RK的NPU采取的是自研架构,只支持使用不开源的驱动和库来操作。
rk的npu sdk分为两个部分,PC端使用的是rknn-toolkit2,可以在PC端进行模型转换,推理以及性能评估。具体来说是把主流的模型如Caffe、TensorFlow、TensorFlow Lite、ONNX、DarkNet、PyTorch 等转换为RKNN模型,并可以在PC端使用这个RKNN模型进行推理仿真,计算时间和内存开销。板端还有一部分,就是rknn runtime环境,包含一组C API库以及与NPU进行通信的驱动模块,可执行程序等。
本文介绍如何在Android板端使用rk的npu sdk。
rknn-toolkit2下载解压后的内容如下图所示,本文后续提到的相对路径都是相对于此目录的
NDK下载之后解压即可使用,用于交叉编译RKNN的应用程序
不管是在android下面还是Linux下面,将常用的模型,如 ONNX TFLite等转换为RKNN模型的方法都一样的,是在PC端的linux环境下使用 rknn-toolkit2 工具进行转换,如果读者想要自行生成模型,可以参考这两篇wiki,
本文直接使用SDK提供转换好的模型,这些模型位于 rknpu2/examples/ 的各个demo下面,如本文使用 mobilenet 模型,转换后的模型就是 rknpu2/examples/rknn_mobilenet_demo/model/RK3588/mobilenet_v1.rknn
内核默认带了NPU驱动,可以通过如下命令确认
rk3588_u:/ $ cat /sys/kernel/debug/rknpu/version
RKNPU driver: v0.9.8
确认板上的 rknn 动态库版本
rk3588_u:/ $ strings /vendor/lib64/librknnrt.so | grep -i "librknnrt version"
librknnrt version: 2.3.0 (c949ad889d@2024-11-07T11:34:26)
如果有更新版本的需要,可以通过如下方式更新这个 librknnrt.so 。在PC端的 adb 终端下执行如下命令
adb root
adb remount
adb push rknpu2/runtime/Android/librknn_api/arm64-v8a/librknnrt.so /vendor/lib64
adb reboot
进入 rknn_mobilenet_demo 目录,执行如下命令
export ANDROID_NDK_PATH={NDK解压目录}
./build-android.sh -t rk3588 -a arm64-v8a -b Debug
执行后,在此目录下面会生成 install/rknn_mobilenet_demo_Android 目录,这里面包含了可执行程序,模型,以及示例图
将install/rknn_mobilenet_demo_Android 目录通过adb等方式推送到板上
然后执行如下命令
LD_LIBRARY_PATH=/vendor/lib64 ./rknn_mobilenet_demo ./model/RK3588/mobilenet_v1.rknn ./model/dog_224x224.jpg
model input num: 1, output num: 1
input tensors:
index=0, name=input, n_dims=4, dims=[1, 224, 224, 3], n_elems=150528, size=150528, fmt=NHWC, type=INT8, qnt_type=AFFINE, zp=0, scale=0.007812
output tensors:
index=0, name=MobilenetV1/Predictions/Reshape_1, n_dims=2, dims=[1, 1001, 0, 0], n_elems=1001, size=2002, fmt=UNDEFINED, type=FP16, qnt_type=AFFINE, zp=0, scale=1.000000
rknn_run
--- Top5 ---
156: 0.884766
155: 0.054016
205: 0.003677
284: 0.002974
285: 0.000189
LD_LIBRARY_PATH=/vendor/lib64 ./rknn_mobilenet_demo ./model/RK3588/mobilenet_v1.rknn ./model/cat_224x224.jpg
model input num: 1, output num: 1
input tensors:
index=0, name=input, n_dims=4, dims=[1, 224, 224, 3], n_elems=150528, size=150528, fmt=NHWC, type=INT8, qnt_type=AFFINE, zp=0, scale=0.007812
output tensors:
index=0, name=MobilenetV1/Predictions/Reshape_1, n_dims=2, dims=[1, 1001, 0, 0], n_elems=1001, size=2002, fmt=UNDEFINED, type=FP16, qnt_type=AFFINE, zp=0, scale=1.000000
rknn_run
--- Top5 ---
283: 0.407227
282: 0.172485
286: 0.155762
278: 0.059113
279: 0.042603
实现apk调用npu的基本思路就是在上一节基础上,将这个可执行程序改为jni接口的函数,然后通过java调用它。
此外,rknn_mobilenet_demo 源码中使用了opencv的接口来缩放并解码图片,java里面可以用 Bitmap 来代替它。
具体实现过程如下。在教程第一章创建的app基础上,增加一个按钮,用于启动NPU demo
MainActivity的onCreate增加
button9.setText("npu test");
button9.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
byte[] inputData = ImageDecode.processImageToRGBData("/sdcard/test.jpg");
if (inputData != null) {
RKNNTest(inputData,"/sdcard/mobilenet_v1.rknn");
}
}
});
其中ImageDecode类为基于 Bitmap 实现的图片解码缩放的,实现如下
package com.example.testdemo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
public class ImageDecode {
static String TAG = "com.example.testdemo";
private static final int MODEL_IN_WIDTH = 224;
private static final int MODEL_IN_HEIGHT = 224;
public static byte[] processImageToRGBData(String imgPath) {
Bitmap origBitmap = BitmapFactory.decodeFile(imgPath);
if (origBitmap == null) {
Log.e(TAG, "Failed to load image: " + imgPath);
return null;
}
// 2. 检查是否需要缩放
Bitmap scaledBitmap = origBitmap;
if (origBitmap.getWidth() != MODEL_IN_WIDTH || origBitmap.getHeight() != MODEL_IN_HEIGHT) {
Log.i(TAG, "Resizing from " + origBitmap.getWidth() + "x" + origBitmap.getHeight() +
" to " + MODEL_IN_WIDTH + "x" + MODEL_IN_HEIGHT);
scaledBitmap = Bitmap.createScaledBitmap(
origBitmap, MODEL_IN_WIDTH, MODEL_IN_HEIGHT, true);
origBitmap.recycle(); // 释放原始Bitmap
}
// 3. 从Bitmap的ARGB格式提取RGB数据
byte[] rgbData = extractRGBFromARGB(scaledBitmap);
scaledBitmap.recycle(); // 释放缩放后的Bitmap
return rgbData;
}
/**
* 从ARGB格式的Bitmap中提取RGB数据(跳过Alpha通道)
*/
private static byte[] extractRGBFromARGB(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
// 分配RGB数据数组(每个像素3个float值:R,G,B)
byte[] rgbData = new byte[width * height * 3];
int index = 0;
for (int i = 0; i < pixels.length; i++) {
int pixel = pixels[i];
// 注意:Bitmap 的 getPixels() 返回的是 ARGB(A最高位)
rgbData[i * 3] = (byte) ((pixel >> 16) & 0xFF); // R
rgbData[i * 3 + 1] = (byte) ((pixel >> 8) & 0xFF); // G
rgbData[i * 3 + 2] = (byte) (pixel & 0xFF); // B
}
return rgbData;
}
}
注意NPU需要的数据为RGB888格式的原始数据流,因此上述代码直接返回了 byte[] 类型的数组,用于保存此数据流,接下来就是将原本的 rknn_mobilenet_demo 源码包装为JNI格式的,实现如下
#include "rknn_api.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <fstream>
#include <iostream>
#include <jni.h>
#include <cstring>
#include <android/log.h>
#include <dlfcn.h>
#define TAG "RKNN_TEST"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
using namespace std;
int is_load_rknn_api = 0;
typedef int (*rknn_query_func)(rknn_context, rknn_query_cmd, void*, uint32_t);
typedef int (*rknn_init_func)(rknn_context* context, void* model, uint32_t size, uint32_t flag, rknn_init_extend* extend);
typedef int (*rknn_inputs_set_func)(rknn_context context, uint32_t n_inputs, rknn_input inputs[]);
typedef int (*rknn_run_func)(rknn_context context, rknn_run_extend* extend);
typedef int (*rknn_outputs_get_func)(rknn_context context, uint32_t n_outputs, rknn_output outputs[], rknn_output_extend* extend);
typedef int (*rknn_outputs_release_func)(rknn_context context, uint32_t n_ouputs, rknn_output outputs[]);
typedef int (*rknn_destroy_func)(rknn_context context);
rknn_query_func rknn_query;
rknn_init_func rknn_init;
rknn_inputs_set_func rknn_inputs_set;
rknn_run_func rknn_run;
rknn_outputs_get_func rknn_outputs_get;
rknn_outputs_release_func rknn_outputs_release;
rknn_destroy_func rknn_destroy;
void load_rknn_api(void)
{
void* handle;
char* error;
handle = dlopen("/data/librknnrt.so", RTLD_LAZY);
if (!handle) {
return;
}
// 清除之前的错误
dlerror();
// 获取函数指针
rknn_query = (rknn_query_func)dlsym(handle, "rknn_query");
if ((error = dlerror()) != NULL) {
dlclose(handle);
return;
}
rknn_init = (rknn_init_func)dlsym(handle, "rknn_init");
if ((error = dlerror()) != NULL) {
dlclose(handle);
return;
}
rknn_inputs_set = (rknn_inputs_set_func)dlsym(handle, "rknn_inputs_set");
if ((error = dlerror()) != NULL) {
dlclose(handle);
return;
}
rknn_run = (rknn_run_func)dlsym(handle, "rknn_run");
if ((error = dlerror()) != NULL) {
dlclose(handle);
return;
}
rknn_outputs_get = (rknn_outputs_get_func)dlsym(handle, "rknn_outputs_get");
if ((error = dlerror()) != NULL) {
dlclose(handle);
return;
}
rknn_outputs_release = (rknn_outputs_release_func)dlsym(handle, "rknn_outputs_release");
if ((error = dlerror()) != NULL) {
dlclose(handle);
return;
}
rknn_destroy = (rknn_destroy_func)dlsym(handle, "rknn_destroy");
if ((error = dlerror()) != NULL) {
dlclose(handle);
return;
}
is_load_rknn_api = 1;
return;
}
/*-------------------------------------------
Functions
-------------------------------------------*/
static void dump_tensor_attr(rknn_tensor_attr *attr) {
LOGI(" index=%d, name=%s, n_dims=%d, dims=[%d, %d, %d, %d], n_elems=%d, size=%d, fmt=%s, type=%s, qnt_type=%s, "
"zp=%d, scale=%f\n",
attr->index, attr->name, attr->n_dims, attr->dims[0], attr->dims[1], attr->dims[2],
attr->dims[3],
attr->n_elems, attr->size, get_format_string(attr->fmt), get_type_string(attr->type),
get_qnt_type_string(attr->qnt_type), attr->zp, attr->scale);
}
static unsigned char *load_model(const char *filename, int *model_size) {
FILE *fp = fopen(filename, "rb");
if (fp == nullptr) {
LOGE("fopen %s fail!\n", filename);
return NULL;
}
fseek(fp, 0, SEEK_END);
int model_len = ftell(fp);
unsigned char *model = (unsigned char *) malloc(model_len);
fseek(fp, 0, SEEK_SET);
if (model_len != fread(model, 1, model_len, fp)) {
LOGE("fread %s fail!\n", filename);
free(model);
return NULL;
}
*model_size = model_len;
if (fp) {
fclose(fp);
}
return model;
}
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;
}
/*-------------------------------------------
Main Function
-------------------------------------------*/
extern "C" JNIEXPORT int JNICALL
Java_com_example_testdemo_MainActivity_RKNNTest(JNIEnv *env, jobject thiz, jbyteArray inputData,
jstring jmodel_path) {
const int MODEL_IN_WIDTH = 224;
const int MODEL_IN_HEIGHT = 224;
const int MODEL_IN_CHANNELS = 3;
rknn_context ctx = 0;
int ret;
int model_len = 0;
unsigned char *model = NULL;
const char *model_path = env->GetStringUTFChars(jmodel_path, nullptr);
jbyte* jniData = env->GetByteArrayElements(inputData, nullptr);
int dataLength = env->GetArrayLength(inputData);
if(is_load_rknn_api == 0) {
load_rknn_api();
if(is_load_rknn_api == 0) {
LOGE("load_rknn_api fail!\n");
return -1;
}
LOGI("load_rknn_api success!\n");
}
// Load RKNN Model
LOGI("model_path is %s!",model_path);
model = load_model(model_path, &model_len);
LOGI("model is %p, len %d !",model,model_len);
ret = rknn_init(&ctx, model, model_len, 0, NULL);
if (ret < 0) {
LOGE("rknn_init fail! ret=%d\n", ret);
return -1;
}
LOGI("rknn_init success!\n");
// Get Model Input Output Info
rknn_input_output_num io_num;
ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
if (ret != RKNN_SUCC) {
LOGE("rknn_query fail! ret=%d\n", ret);
return -1;
}
LOGI("model input num: %d, output num: %d\n", io_num.n_input, io_num.n_output);
LOGI("input tensors:\n");
rknn_tensor_attr input_attrs[io_num.n_input];
memset(input_attrs, 0, sizeof(input_attrs));
for (int i = 0; i < io_num.n_input; i++) {
input_attrs[i].index = i;
ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC) {
LOGE("rknn_query fail! ret=%d\n", ret);
return -1;
}
dump_tensor_attr(&(input_attrs[i]));
}
LOGI("output tensors:\n");
rknn_tensor_attr output_attrs[io_num.n_output];
memset(output_attrs, 0, sizeof(output_attrs));
for (int i = 0; i < io_num.n_output; i++) {
output_attrs[i].index = i;
ret = rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC) {
LOGE("rknn_query fail! ret=%d\n", ret);
return -1;
}
dump_tensor_attr(&(output_attrs[i]));
}
// Set Input Data
rknn_input inputs[1];
memset(inputs, 0, sizeof(inputs));
inputs[0].index = 0;
inputs[0].type = RKNN_TENSOR_UINT8;
// inputs[0].size = img.cols * img.rows * img.channels() * sizeof(uint8_t);
inputs[0].size = dataLength;
inputs[0].fmt = RKNN_TENSOR_NHWC;
// inputs[0].buf = img.data;
inputs[0].buf = (void*)jniData;
ret = rknn_inputs_set(ctx, io_num.n_input, inputs);
if (ret < 0) {
LOGE("rknn_input_set fail! ret=%d\n", ret);
return -1;
}
// Run
LOGI("rknn_run\n");
ret = rknn_run(ctx, nullptr);
if (ret < 0) {
LOGE("rknn_run fail! ret=%d\n", ret);
return -1;
}
// Get Output
rknn_output outputs[1];
memset(outputs, 0, sizeof(outputs));
outputs[0].want_float = 1;
ret = rknn_outputs_get(ctx, 1, outputs, NULL);
if (ret < 0) {
LOGE("rknn_outputs_get fail! ret=%d\n", ret);
return -1;
}
// Post Process
for (int i = 0; i < io_num.n_output; i++) {
uint32_t MaxClass[5];
float fMaxProb[5];
float *buffer = (float *) outputs[i].buf;
uint32_t sz = outputs[i].size / 4;
rknn_GetTop(buffer, fMaxProb, MaxClass, sz, 5);
LOGI(" --- Top5 ---\n");
for (int i = 0; i < 5; i++) {
LOGI("%3d: %8.6f\n", MaxClass[i], fMaxProb[i]);
}
}
// Release rknn_outputs
rknn_outputs_release(ctx, 1, outputs);
// Release
if (ctx > 0) {
rknn_destroy(ctx);
}
if (model) {
free(model);
}
return 0;
}
笔者这里使用了dl的API来引用 /data/librknnrt.so 中的API,其中 librknnrt.so 是推送到板上的
其中 Java_com_example_testdemo_MainActivity_RKNNTest 为JNI接口,需要两个参数,第一个是RGB格式数据流,第二个参数是 rknn模型路径
在 JAVA 中使用 RKNNTest 调用它即可
然后依次将动态库 图片 以及rknn模型推到板上指定目录
adb push rknpu2\examples\rknn_mobilenet_demo\model\RK3588\mobilenet_v1.rknn /sdcard/mobilenet_v1.rknn
adb push rknpu2\examples\rknn_mobilenet_demo\model\dog_224x224.jpg /sdcard/test.jpg
adb push rknpu2\runtime\Android\librknn_api\arm64-v8a\librknnrt.so /data/librknnrt.so
然后将apk安装到板上,按下NPU TEST的按钮,可以在logcat中看到如下信息