纸翼 · 加载中
1880 words
9 minutes
RK3588 端侧 AI 部署(五):Lite2、C API 与零拷贝工程化
所属合集 RK3588 端侧 AI 部署 第 5 / 6 篇
  1. 1 RK3588 端侧 AI 部署(一):平台、NPU 与 RKNN 工具链
  2. 2 RK3588 端侧 AI 部署(二):主机与板端环境搭建
  3. 3 RK3588 端侧 AI 部署(三):ONNX 转 RKNN 与推理验证
  4. 4 RK3588 端侧 AI 部署(四):量化、评估与模型优化
  5. 5 RK3588 端侧 AI 部署(五):Lite2、C API 与零拷贝工程化 正在阅读
  6. 6 RK3588 端侧 AI 部署(六):MobileNet、YOLO 与 RKLLM 实战

RK3588 端侧 AI 部署(五):Lite2、C API 与零拷贝工程化#

模型通过 Toolkit2 验证后,下一步是让程序真正独立运行在 RK3588 上。可以先用 Lite2 快速复现 Python 结果,再用 RKNPU2 C/C++ API 构建正式应用。

上一篇:量化、评估与模型优化

一、三种运行方式如何选择#

方式运行位置适合阶段主要限制
Toolkit2 连板x86 主机控制 RK3588转换后快速验证和评估依赖主机、ADB 和 server
Toolkit-Lite2RK3588 板端 Python原型、算法验证、小工具Python 开销和工程能力有限
RKNPU2 C/C++RK3588 板端原生程序产品、视频流、并发和性能优化开发复杂度更高

推荐顺序:

Toolkit2 连板结果正确
→ Lite2 在板端复现
→ 通用 C API 复现
→ 零拷贝、多 Context、RGA 和线程池优化

二、RKNN-Toolkit-Lite2 快速验证#

RKNN-Toolkit2 仓库 中选择与下面三项匹配的 Lite2 wheel:

  • RKNN SDK release;
  • 板端 Python cpXX
  • aarch64 架构。
Terminal window
conda create -n rknnlite python=3.10 -y
conda activate rknnlite
pip install ./rknn_toolkit_lite2-*-cp310-*-linux_aarch64.whl

最小推理结构:

from rknnlite.api import RKNNLite
runtime = RKNNLite(verbose=True)
ret = runtime.load_rknn("model.rknn")
if ret != 0:
raise RuntimeError(f"load_rknn failed: {ret}")
ret = runtime.init_runtime(core_mask=RKNNLite.NPU_CORE_AUTO)
if ret != 0:
raise RuntimeError(f"init_runtime failed: {ret}")
try:
outputs = runtime.inference(
inputs=[input_tensor],
data_format=["nhwc"],
)
finally:
runtime.release()

这里的 input_tensor 必须与 Toolkit2 验证脚本完全一致。Lite2 只运行 .rknn,不能加载 ONNX 后执行 build()

三、RKNPU2 通用 C API 的生命周期#

完整流程可以记成:

rknn_init
→ rknn_query
→ 准备输入
→ rknn_inputs_set
→ rknn_run
→ rknn_outputs_get
→ 后处理
→ rknn_outputs_release
→ rknn_destroy

rknn_context 是贯穿整个流程的句柄。所有返回值都要检查,所有成功申请的资源都要在错误路径中释放。

四、读取模型并创建 Context#

模型可以先读入内存,再传给 rknn_init()

#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
#include "rknn_api.h"
std::vector<unsigned char> LoadFile(const std::string& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
throw std::runtime_error("cannot open model: " + path);
}
const auto size = file.tellg();
if (size <= 0) {
throw std::runtime_error("empty model: " + path);
}
std::vector<unsigned char> data(static_cast<size_t>(size));
file.seekg(0, std::ios::beg);
file.read(reinterpret_cast<char*>(data.data()), size);
if (!file) {
throw std::runtime_error("read model failed: " + path);
}
return data;
}
int main(int argc, char** argv) {
if (argc < 2) return 1;
auto model = LoadFile(argv[1]);
rknn_context ctx = 0;
int ret = rknn_init(&ctx, model.data(), model.size(), 0, nullptr);
if (ret != RKNN_SUCC) {
throw std::runtime_error("rknn_init failed: " + std::to_string(ret));
}
// query / inference / postprocess ...
rknn_destroy(ctx);
return 0;
}

正式工程应使用 RAII 封装 Context,确保异常或提前返回时也能执行 rknn_destroy()

五、不要写死张量属性#

先查询输入输出数量:

rknn_input_output_num io_num{};
int ret = rknn_query(
ctx,
RKNN_QUERY_IN_OUT_NUM,
&io_num,
sizeof(io_num)
);

再逐个查询 Tensor:

std::vector<rknn_tensor_attr> input_attrs(io_num.n_input);
for (uint32_t i = 0; i < io_num.n_input; ++i) {
input_attrs[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) {
throw std::runtime_error("query input attr failed");
}
}

每个 Tensor 至少打印:

  • index 和 name;
  • n_dims、dims、元素数;
  • NHWC/NCHW;
  • UINT8/INT8/FLOAT16/FLOAT32;
  • 量化类型、zero point 和 scale;
  • size、size_with_stridew_stride

后处理应依据查询结果验证模型契约,而不是看到“YOLO”就默认三个输出。

六、通用 API 输入、运行和输出#

1. 设置输入#

rknn_input input{};
input.index = 0;
input.buf = image_data;
input.size = image_bytes;
input.type = RKNN_TENSOR_UINT8;
input.fmt = RKNN_TENSOR_NHWC;
input.pass_through = 0;
ret = rknn_inputs_set(ctx, 1, &input);
if (ret != RKNN_SUCC) {
throw std::runtime_error("rknn_inputs_set failed");
}

image_data 的实际 shape、通道、dtype 和预处理必须匹配模型。如果 RKNN 已内置均值和标准差,输入通常可以是预处理后的 UINT8 图像;不能再次手工标准化。

2. 执行推理#

ret = rknn_run(ctx, nullptr);
if (ret != RKNN_SUCC) {
throw std::runtime_error("rknn_run failed");
}

3. 获取和释放输出#

std::vector<rknn_output> outputs(io_num.n_output);
for (uint32_t i = 0; i < io_num.n_output; ++i) {
outputs[i] = {};
outputs[i].index = i;
outputs[i].want_float = 1;
}
ret = rknn_outputs_get(ctx, io_num.n_output, outputs.data(), nullptr);
if (ret != RKNN_SUCC) {
throw std::runtime_error("rknn_outputs_get failed");
}
// 使用 outputs[i].buf 完成 Softmax、Top-K、解码或 NMS
rknn_outputs_release(ctx, io_num.n_output, outputs.data());

want_float=1 使用方便,但 Runtime 需要反量化/转换。高性能检测后处理可以保持 INT8 输出,并用查询到的 scale、zero point 自行计算:

float_value = (quant_value - zero_point) × scale

七、通用 API 的数据路径#

CPU buffer
→ rknn_inputs_set 复制/转换
→ NPU 推理
→ rknn_outputs_get 复制/转换
→ CPU 后处理

它的优点是简单可靠,适合作为正确性基线;缺点是高清、多路或高帧率时,输入输出搬运可能占据明显时间。

计时应至少拆成:

读取/解码 | 预处理 | 输入提交 | rknn_run | 输出获取 | 后处理 | 绘制/编码

八、零拷贝 C API#

零拷贝使用绑定到 Context 的 Tensor Memory:

rknn_tensor_mem* input_mem =
rknn_create_mem(ctx, input_attr.size_with_stride);
rknn_tensor_mem* output_mem =
rknn_create_mem(ctx, output_attr.size_with_stride);
if (!input_mem || !output_mem) {
throw std::runtime_error("rknn_create_mem failed");
}
rknn_set_io_mem(ctx, input_mem, &input_attr);
rknn_set_io_mem(ctx, output_mem, &output_attr);
// 把输入写入 input_mem->virt_addr
rknn_run(ctx, nullptr);
// 从 output_mem->virt_addr 读取结果
rknn_destroy_mem(ctx, input_mem);
rknn_destroy_mem(ctx, output_mem);

完整函数声明和结构体以当前 release 的 RKNNRT C API 文档与头文件 为准。

最容易踩坑的 stride#

width 是逻辑宽度,w_stride 是实际每行对齐宽度。二者不同时不能把整张图片直接连续复制:

for (int y = 0; y < height; ++y) {
std::memcpy(
dst + y * width_stride * channels,
src + y * width * channels,
width * channels
);
}

分配大小应按 size_with_stride 或 API 查询值。使用外部 FD/物理内存时,还要按照 BSP/SDK 要求处理 Cache flush/invalidate。

“零拷贝”表示减少 RKNN 路径上的额外搬运,不代表摄像头、RGA、NPU、显示之间绝对没有任何数据移动。

推荐迁移顺序#

  1. 通用 API 输出完全正确;
  2. 保存同一输入的通用 API 输出;
  3. 改用 Runtime 分配的 Tensor Memory;
  4. 对比两条路径输出;
  5. 再导入 DMA-BUF/外部 FD;
  6. 最后进行 RGA、摄像头和显示链路内存复用。

九、交叉编译和板端运行#

官方 Model Zoo 已提供构建脚本,MobileNet 示例:

Terminal window
cd rknn_model_zoo
export GCC_COMPILER=<AARCH64_TOOLCHAIN_PREFIX>
./build-linux.sh -t rk3588 -a aarch64 -d mobilenet
adb push install/rk3588_linux_aarch64/rknn_mobilenet_demo/ /userdata/
adb shell
cd /userdata/rknn_mobilenet_demo
export LD_LIBRARY_PATH=./lib
./rknn_mobilenet_demo model/mobilenetv2-12.rknn model/bell.jpg

如果在板端原生编译,要确认 OpenCV、CMake 和 RKNN 头文件/库都是 AArch64 版本。交叉编译报 invalid ELF 通常表示链接到了错误架构的库。

十、把 Demo 重构成可维护工程#

推荐目录:

src/
├─ engine/
│ ├─ nn_engine.hpp # 与硬件无关的引擎接口
│ └─ rknn_engine.cpp # RKNPU2 实现
├─ tasks/
│ ├─ classification/ # Softmax、Top-K
│ └─ detection/ # 解码、阈值、NMS
├─ preprocess/ # resize/letterbox、颜色和 layout
├─ types/ # Tensor、Detection、错误码
├─ utils/ # 日志、计时、模型文件读取
└─ main.cpp # 参数和业务流程

分层原则:

  • 引擎层只负责模型和 Tensor,不知道“安全帽”或“公交车”;
  • 任务层实现分类/检测后处理,不直接管理 RKNN Context;
  • 业务层管理摄像头、队列、告警和输出;
  • 使用 RAII、std::vector、智能指针明确资源所有权;
  • 错误码区分模型加载、输入、推理、输出和后处理阶段。

十一、多 Context 和线程#

一个高吞吐视频系统通常是流水线:

解码线程
→ 有界输入队列
→ 多个推理 worker(每个独立 Context)
→ 有界结果队列
→ 后处理/显示/编码线程

注意:

  • 不要让多个线程无保护地共享同一个 Context;
  • 限制队列长度,避免延迟和内存无限增长;
  • 为帧附带序号和时间戳;
  • 明确实时模式是丢旧帧还是阻塞生产者;
  • 统计吞吐的同时统计单帧端到端延迟;
  • 退出时唤醒等待线程并释放全部 Context。

十二、本篇实践验收#

  • Lite2 能在板端复现 Toolkit2 的结果;
  • 通用 C API 能正确查询 Tensor、推理和释放资源;
  • 能解释 want_float、scale/zero point 和数据复制;
  • 零拷贝结果与通用 API 在容差内一致;
  • 工程按引擎、任务、预后处理和业务分层;
  • 性能报告能区分预处理、NPU、后处理和端到端时间。

下一篇:RK3588 端侧 AI 部署(六):MobileNet、YOLO 与 RKLLM 实战

RK3588 端侧 AI 部署(五):Lite2、C API 与零拷贝工程化
https://blog.huangzy.xyz/posts/rk3588-端侧-ai-部署五/
Author
纸翼
Published at
2026-07-21
License
CC BY-NC-SA 4.0

Some information may be outdated