Giới thiệu về RKNPU2 SDK
Khác với RKNN-Toolkit2-Lite2 cung cấp giao diện Python để chạy trực tiếp trên board, RKNPU2 SDK mang đến bộ giao diện lập trình đa nền tảng (C/C++). Bộ SDK này cho phép các kỹ nh sư nhúng tích hợp và triển khai các mô hình RKNN vào ứng dụng cuối cùng với hiệu suất tối ưu.
Cấu hình biên dịch và liên kết
Để xây dựng ứng dụng sử dụng NPU, nhà phát triển cần include các file header chứa định nghĩa hàm và liên kết với thư viện runtime tương ứng theo nền tảng phần cứng.
- Thư viện runtime:
librknnrt.so(hỗ trợ dòng chip RK356X/RK3588). - Header files:
rknn_api.hvàrknn_matmul_api.h. - Đường dẫn hệ thống (Linux): Thư viện thường được đặt tại
/usr/libvà header tại/usr/includeđể compiler có thể tìm thấy.
Cơ chế hoạt động của API
RKNPU2 cung cấp hai phương thức giao tiếp chính với NPU, phân biệt bởi cách quản lý bộ nhớ:
1. API tiêu chuẩn (General API)
- Dữ liệu đầu vào được copy từ bộ nhớ ứng dụng sang bộ nhớ dành riêng cho NPU trước mỗi lần suy luận.
- Kết quả đầu ra cũng được copy từ bộ nhớ NPU về bộ nhớ ứng dụng.
- Phù hợp cho các ứng dụng đơn giản, dễ triển khai nhưng có độ trễ do thao tác copy bộ nhớ.
2. API zero-copy
- Yêu cầu người dùng cấp phát trước một vùng nhớ vật lý liên tục (physical contiguous memory).
- NPU truy cập trực tiếp vào vùng nhớ này để đọc đầu vào và ghi đầu ra.
- Loại bỏ hoàn toàn thao tác copy dữ liệu giữa CPU và NPU, tối ưu hóa băng thông và giảm độ trễ.
Triển khai với API tiêu chuẩn
Dưới đây là ví dụ về quy trình suy luận mô hình YOLOv5 sử dụng phương thức copy bộ nhớ mặc định. Mã nguồn đã được tinh chỉnh để minh họa luồng xử lý cơ bản.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include "rknn_api.h"
// Helper: Đọc file binary vào buffer
static unsigned char* read_binary_file(const char* path, int* out_size) {
FILE* fp = fopen(path, "rb");
if (!fp) return NULL;
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
fseek(fp, 0, SEEK_SET);
unsigned char* buf = (unsigned char*)malloc(size);
if (buf) fread(buf, 1, size, fp);
fclose(fp);
*out_size = size;
return buf;
}
// Helper: In thông tin tensor
static void print_tensor_info(rknn_tensor_attr* attr) {
printf("Tensor[%d]: %s, dims=%d, size=%d, type=%s\n",
attr->index, attr->name, attr->n_dims, attr->size,
attr->type == RKNN_TENSOR_UINT8 ? "UINT8" : "FLOAT32");
}
int main(int argc, char** argv) {
if (argc < 3) {
printf("Usage: %s <model.rknn> <input.jpg>\n", argv[0]);
return -1;
}
rknn_context ctx = 0;
int model_size = 0;
unsigned char* model_buf = read_binary_file(argv[1], &model_size);
if (!model_buf) {
printf("Failed to load model\n");
return -1;
}
// 1. Khởi tạo context
int ret = rknn_init(&ctx, model_buf, model_size, 0, NULL);
if (ret < 0) {
printf("rknn_init failed: %d\n", ret);
return -1;
}
// 2. Truy vấn thông số model
rknn_input_output_num io_num;
rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
printf("Inputs: %d, Outputs: %d\n", io_num.n_input, io_num.n_output);
rknn_tensor_attr input_attr;
memset(&input_attr, 0, sizeof(input_attr));
input_attr.index = 0;
rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &input_attr, sizeof(input_attr));
print_tensor_info(&input_attr);
// 3. Chuẩn bị dữ liệu đầu vào (Giả lập)
int input_size = input_attr.size;
unsigned char* input_data = (unsigned char*)malloc(input_size);
memset(input_data, 0, input_size); // Placeholder for actual image data
rknn_input inputs[1];
memset(inputs, 0, sizeof(inputs));
inputs[0].index = 0;
inputs[0].type = RKNN_TENSOR_UINT8;
inputs[0].size = input_size;
inputs[0].fmt = RKNN_TENSOR_NHWC;
inputs[0].buf = input_data;
// 4. Thiết lập đầu vào
rknn_inputs_set(ctx, io_num.n_input, inputs);
// 5. Chạy suy luận
ret = rknn_run(ctx, NULL);
if (ret < 0) {
printf("rknn_run failed\n");
}
// 6. Lấy kết quả
rknn_output outputs[io_num.n_output];
memset(outputs, 0, sizeof(outputs));
for(int i=0; i<io_num.n_output; i++) outputs[i].want_float = 0;
rknn_outputs_get(ctx, io_num.n_output, outputs, NULL);
// Xử lý hậu kỳ ở đây (Post-processing)
printf("Inference done. Output[0] size: %d\n", outputs[0].size);
// 7. Giải phóng tài nguyên
rknn_outputs_release(ctx, io_num.n_output, outputs);
rknn_destroy(ctx);
free(model_buf);
free(input_data);
return 0;
}
Các hàm API chính (Chế độ tiêu chuẩn)
rknn_init: Khởi tạo ngữ cảnh và nạp mô hình.rknn_query: Lấy thông tin version, số lượng input/output, thuộc tính tensor.rknn_inputs_set: Copy dữ liệu từ RAM ứng dụng vào bộ nhớ NPU.rknn_run: Kích thích NPU thực thi tính toán.rknn_outputs_get: Copy kết quả từ NPU về RAM ứng dụng.rknn_outputs_release: Giải phóng bộ nhớ đệm đầu ra.rknn_destroy: Dọn dẹp ngữ cảnh RKNN.
Triển khai với chế độ Zero-copy
Để đạt hiệu suất cao nhất, ta sử dụng bộ nhớ vật lý liên tục (MMZ). Ví dụ dưới đây minh họa cách cấp phát bộ nhớ bên ngoài và ánh xạ vào NPU.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "rknn_api.h"
#include "rk_mpi_mmz.h"
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s <model.rknn>\n", argv[0]);
return -1;
}
rknn_context ctx = 0;
// Khởi tạo chỉ với đường dẫn file (SDK tự load) hoặc buffer
int ret = rknn_init(&ctx, argv[1], 0, 0, NULL);
if (ret < 0) return -1;
// Truy vấn thông tin
rknn_input_output_num io_num;
rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
rknn_tensor_attr input_attr;
input_attr.index = 0;
rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &input_attr, sizeof(input_attr));
// 1. Cấp phát bộ nhớ vật lý (Physical Memory)
int mem_flags = RK_MMZ_ALLOC_TYPE_CMA | RK_MMZ_ALLOC_UNCACHEABLE;
MB_BLK input_block;
size_t input_size = input_attr.size_with_stride;
ret = RK_MPI_MMZ_Alloc(&input_block, input_size, mem_flags);
if (ret < 0) {
printf("MMZ Alloc failed\n");
return -1;
}
void* virt_addr = RK_MPI_MMZ_Handle2VirAddr(input_block);
uint64_t phys_addr = RK_MPI_MMZ_Handle2PhysAddr(input_block);
// 2. Tạo đối tượng tensor từ bộ nhớ vật lý
rknn_tensor_mem* input_mem = rknn_create_mem_from_phys(ctx, phys_addr, virt_addr, input_size);
// 3. Nạp dữ liệu vào bộ nhớ đã cấp phát
memset(virt_addr, 128, input_size); // Giả lập dữ liệu ảnh
// 4. Binding bộ nhớ vào IO của model
ret = rknn_set_io_mem(ctx, input_mem, &input_attr);
if (ret < 0) {
printf("rknn_set_io_mem failed\n");
return -1;
}
// 5. Thực thi suy luận (Không cần set/get input/output mỗi lần)
int loop = 10;
for (int i = 0; i < loop; i++) {
ret = rknn_run(ctx, NULL);
if (ret < 0) break;
}
// 6. Dọn dẹp
rknn_destroy_mem(ctx, input_mem);
RK_MPI_MMZ_Free(input_block);
rknn_destroy(ctx);
return 0;
}
Các hàm API chính (Chế độ Zero-copy)
RK_MPI_MMZ_Alloc: Cấp phát vùng nhớ vật lý liên tục.RK_MPI_MMZ_Handle2VirAddr/PhysAddr: Lấy địa chỉ ảo và vật lý của khối nhớ.rknn_create_mem_from_phys: Wrapper vùng nhớ vật lý thành đối tượng tensor RKNN.rknn_set_io_mem: Gán vùng nhớ đã tạo vào cổng input/output của model.rknn_destroy_mem: Hủy đối tượng tensor bộ nhớ.RK_MPI_MMZ_Free: Giải phóng khối nhớ vật lý.