RK3588 推理优化:多线程、零拷贝与内存管理
RK3588 推理优化:多线程、零拷贝与内存管理
模型转好了、量化也做了,还能不能更快?答案是能。这篇讲 RK3588 上的推理优化技巧:NPU 核心调度、零拷贝、多线程流水线、内存管理、CPU/NPU 协同——从单帧推理优化到完整的视频流处理流水线。
大家好,我是黒漂技术佬。
很多人把 RKNN 模型跑起来就完事了,但其实还有不少优化空间。同样的模型,优化得好和不好,吞吐量能差两三倍。
这篇讲 RK3588 上推理优化的各种手段:NPU 三核调度、零拷贝、预处理加速、多线程流水线、内存优化。
一、RK3588 NPU 架构
先了解一下硬件,才能针对性优化。
RK3588 NPU 规格
- 三核 NPU:三个独立的 NPU 核心(NPU0、NPU1、NPU2)
- 总算力:6 TOPS(INT8)
- 支持:INT8/INT16/FP16 混合精度
- 内存:和 CPU 共享内存(通过 DMA 传输)
三核模式
RK3588 的 NPU 有三种运行模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 单核心 | 只用一个核 | 低负载、单路轻量模型 |
| 联合模式(默认) | 三核一起算一个模型 | 单模型最大性能 |
| 独立模式 | 三个核各跑各的 | 多路并发、多模型 |
默认是联合模式,三个核合力跑一个模型,单模型速度最快。
如果要同时跑多路或者多个模型,可以切到独立模式,每个核独立跑,总吞吐量更高。
二、NPU 调度优化
1. 核心模式选择
单路推理:用默认联合模式,三核合力,单帧最快。
多路并发推理:用独立模式,每个核跑一路,总吞吐更高。
# 设置 NPU 核心掩码rknn.init_runtime(core_mask=RKNN_NPU_CORE_0_1_2)# 三核联合(默认)# 或者rknn.init_runtime(core_mask=RKNN_NPU_CORE_0)# 只用核02. 多路并发方案
比如 3 路摄像头同时推理:
摄像头1 → NPU0 摄像头2 → NPU1 摄像头3 → NPU2三个模型分别绑定到三个核,并行推理,总吞吐 = 单路 × 3。
比三个模型抢一个联合模式的 NPU 效率高很多。
3. 异步推理
RKNN 支持异步推理,提交任务后不用等,可以去做预处理,结果好了再取。
// 异步提交rknn_run_async(ctx,NULL);// 做别的事情(预处理下一帧)// ...// 等结果rknn_wait(ctx);rknn_outputs_get(ctx,...);配合流水线使用,隐藏推理延迟。
三、零拷贝优化
数据拷贝是推理 pipeline 里的隐形开销,很多人忽略了。
传统流程(有拷贝)
摄像头数据 → CPU内存 → 预处理 → 拷到NPU输入 → NPU推理 → 拷回CPU内存中间几次内存拷贝,加起来开销不小。
零拷贝思路
用 RKNN 的零拷贝接口,输入输出直接用物理内存,少拷贝几次。
RKNN 零拷贝 API
// 分配 NPU 可用的物理连续内存void*buf=rknn_create_mem(ctx,size);// 直接把数据写进这块内存// ...// 设置输入(零拷贝)rknn_input inputs[1];inputs[0].index=0;inputs[0].buf=buf;inputs[0].size=size;inputs[0].pass_through=1;// 直通模式,不做内部拷贝rknn_inputs_set(ctx,1,inputs);pass_through=1就是告诉 RKNN 不用再拷一份,直接用这块内存。
适用场景
- 大输入尺寸(1080p 以上)
- 高帧率推理
- 多路并发,内存带宽紧张
能省 10-30% 的端到端时间,具体看输入大小。
注意
- 零拷贝内存是物理连续的,分配有上限,别太多
- 用完要释放:
rknn_destroy_mem(ctx, buf) - 内存对齐要满足 NPU 要求
四、预处理优化
预处理(缩放、颜色转换)有时候比推理本身还慢。
方案 1:RGA 硬件加速
RK3588 有 RGA(Raster Graphic Acceleration)硬件模块,专门做图像缩放、裁剪、格式转换。
CPU 软件缩放 → RGA 硬件缩放 速度提升 3-5 倍librga 库可以直接调用:
// RGA 缩放 + 格式转换rga_info_tsrc,dst;src.fd=-1;src.virAddr=yuv_data;src.width=1920;src.height=1080;src.format=RK_FORMAT_YCbCr_420_SP;dst.fd=-1;dst.virAddr=rgb_data;dst.width=640;dst.height=640;dst.format=RK_FORMAT_RGB_888;c_RkRgaBlit(&src,&dst,NULL);方案 2:把预处理融合进模型
归一化、通道转换这些简单操作,可以在转 RKNN 时通过 mean/std 配置融合进去,推理时自动算,不用 CPU 做。
rknn.config(mean_values=[[0,0,0]],std_values=[[255,255,255]],//除以255的归一化融合了 quant_img_RGB2BGR=False,)方案 3:YUV 直接输入
如果摄像头输出是 NV12/YUV420,可以直接喂给 RKNN,不用转 RGB:
rknn.config(quant_img_RGB2BGR=False)# 输入直接传 YUV 数据少一次颜色空间转换,省时间。
预处理时间占比参考
| 方案 | 1920→640 缩放 + 格式转换 |
|---|---|
| CPU 软件(OpenCV) | ~15ms |
| RGA 硬件 | ~2ms |
| 融合进模型(只有归一化) | 0ms(推理顺带做了) |
差一个数量级。预处理优化的收益可能比优化推理本身还大。
五、多线程流水线
单帧推理再快,端到端也有极限。用流水线把各阶段并行起来,吞吐量翻倍。
典型的三阶段流水线
┌──────────┐ ┌──────────┐ ┌──────────┐ │ 预处理 │ → │ NPU推理 │ → │ 后处理 │ │ (CPU) │ │ (NPU) │ │ (CPU) │ └──────────┘ └──────────┘ └──────────┘三个阶段各占一个线程,用队列串起来。
没有流水线(串行)
帧1: 预处理→推理→后处理 → 帧2: 预处理→推理→后处理 → ... 总时间 = N × (预处理 + 推理 + 后处理)有流水线
帧1: 预处理 → 推理 → 后处理 帧2: 预处理 → 推理 → 后处理 帧3: 预处理 → 推理 → 后处理 总时间 ≈ 预处理 + 推理 + 后处理 + (N-1) × max(各阶段时间)理想情况下,吞吐量由最慢的阶段决定。
实际收益
售货柜项目里:
- 串行:一帧 15ms(预处理5 + 推理7 + 后处理3)
- 流水线:稳定 7ms 一帧(由推理阶段决定)
- 吞吐量提升一倍多
队列长度控制
队列不能无限长,不然延迟会累积。设置最大长度,满了就丢旧帧。
if(queue.size()>=MAX_QUEUE_SIZE){// 丢最旧的一帧autoold=queue.front();queue.pop();release(old);}实时视频流处理,保证低延迟比保证每帧都处理更重要。
六、内存管理优化
1. 预分配内存,循环复用
不要每帧 malloc/free,提前分配好,循环用:
// 初始化时分配uint8_t*input_buf=rknn_create_mem(ctx,input_size);uint8_t*output_buf=rknn_create_mem(ctx,output_size);// 循环里复用while(running){// 写数据到 input_buf// 推理// 读 output_buf// 不用释放,下一帧接着用}// 退出时释放rknn_destroy_mem(ctx,input_buf);2. 控制模型数量
每个 RKNN 模型实例都占内存。多路同型号的话,能不能共享模型看具体 API。
一般每个线程一个 context,多路就多几个 context,内存线性增长。控制好并发路数。
3. 及时释放中间结果
后处理完就释放输出内存,别堆着。
4. 大模型考虑内存峰值
多模型并发时注意峰值内存会不会爆。RK3588 内存和 CPU 共享,系统还要用,别把 NPU 内存占满了。
七、CPU 亲和性与调度
RK3588 是 8 核 CPU:4 个 A76 大核 + 4 个 A55 小核。
推理线程绑大核
把推理和预处理的线程绑到 A76 大核上,性能更稳定:
// 绑 CPU 核心 4-7(A76 大核)cpu_set_t cpuset;CPU_ZERO(&cpuset);CPU_SET(4,&cpuset);CPU_SET(5,&cpuset);pthread_setaffinity_np(pthread_self(),sizeof(cpu_set_t),&cpuset);不同任务分不同核
- 预处理、后处理:大核
- 拉流、IO:小核就行
- NPU 推理:不占 CPU 核,随便哪个线程发
合理分配,避免大核被 IO 任务占了。
调频策略
性能模式下 CPU 和 NPU 频率最高,速度最快但功耗大。
# 设置性能模式echoperformance>/sys/devices/system/cpu/cpu4/cpufreq/scaling_governor功耗敏感的场景可以调省电模式,看具体需求。
八、性能测量与瓶颈分析
优化之前先测瓶颈在哪。
各阶段打点计时
autot0=std::chrono::high_resolution_clock::now();preprocess();autot1=std::chrono::high_resolution_clock::now();inference();autot2=std::chrono::high_resolution_clock::now();postprocess();autot3=std::chrono::high_resolution_clock::now();printf("预处理: %.2fms, 推理: %.2fms, 后处理: %.2fms\n",ms(t1-t0),ms(t2-t1),ms(t3-t2));找到最慢的阶段,针对性优化。
常见瓶颈
- 预处理最慢:上 RGA 硬件加速
- 推理最慢:优化模型、量化、NPU 调度
- 后处理最慢:优化 NMS 算法、多线程
用 rknn 自带的性能统计
rknn.init_runtime(target='rk3588',perf_debug=True)开启性能调试,能看到每层的耗时,找最慢的层。
九、优化效果总结
RK3588 上单路 YOLOv8n 640 输入,各优化手段的效果:
| 阶段 | 端到端耗时 | 说明 |
|---|---|---|
| 基础版(CPU预处理+同步推理) | ~22ms | 刚跑起来的水平 |
| RGA 预处理加速 | ~14ms | 预处理从 12ms 降到 2ms |
| 零拷贝输入 | ~11ms | 少了两次拷贝 |
| 三阶段流水线 | ~7ms | 吞吐量由推理决定 |
| NPU 调频性能模式 | ~6ms | 再快一点 |
从 22ms 优化到 6ms,快了 3 倍多。大部分优化都是工程层面的,模型本身没变。
十、常见坑
坑 1:RGA 和 NPU 内存不通用
RGA 分配的内存不能直接给 NPU 用,格式不一样。需要注意内存类型,该拷的还是得拷。
坑 2:多线程共用一个 rknn context
RKNN context 不是线程安全的,多线程要每个线程一个 context,或者加锁。
坑 3:零拷贝内存分配失败
物理连续内存有限,分配太大或太多会失败。提前分配好,复用。
坑 4:流水线队列爆了
处理不过来队列越来越长,延迟越来越大。一定要有长度限制,满了丢帧。
坑 5:CPU 降频
温度高了 CPU/NPU 降频,速度忽快忽慢。散热做好,或者设置固定频率。
十一、本篇小结
- RK3588 NPU 三核,联合模式单模型最快,独立模式多路并发吞吐高
- 预处理优化收益很大:RGA 硬件缩放比 CPU 快几倍
- 零拷贝减少内存拷贝开销,适合大输入高帧率
- 三阶段流水线(预处理→推理→后处理)吞吐量翻倍
- 内存预分配复用,避免每帧分配释放
- CPU 绑核、调频,保证性能稳定
- 先测瓶颈再优化,别瞎调
下一篇讲多路视频推理架构:多个摄像头、多个模型怎么组织,多进程还是多线程,资源怎么分配。
我是黒漂技术佬。
