RK3588 NPU 三路视觉任务并发部署实战:模型分配与性能调优 一块 RK3588 的板子要同时处理三路视觉任务人员入侵要抓烟火要盯垃圾分类也要出结果。刚接到这个需求的时候我第一反应是“先拆开跑”无非就是三个模型轮流上。真正落地之后才发现单块 RK3588 的 NPU 虽然给了 6 TOPS 算力但三个任务各自的数据流、模型大小、实时性要求完全不同处理不好就会互相抢资源最后谁都不快。这篇文章就围绕着“单块 RK3588 NPU 如何同时跑人员入侵、烟火检测、垃圾分类”这个场景把我在实际项目里的方案选型、RKNN 转换、多进程并发调度、性能调优和踩坑记录都整理出来。适合正在做 RK3588 边缘视觉盒子、或者准备在 rknn-toolkit2 上部署多模型的人参考。我默认你已经有了 RK3588 开发板跑过基础 demo知道 RKNN 和 rknn-toolkit2 大概是什么下面直接进入正题。1. 任务拆解与资源预算1.1 三个视觉任务在边缘端算力上的真正差异先说结论这三个任务虽然都是计算机视觉但“体感”完全不同。人员入侵是一个典型的实时告警任务人一旦踏入危险区域留给系统的反应时间通常只有几秒。烟火检测是“越早越好”型任务火焰初期可能就几十个像素模型对小目标的召回率比帧率更重要。垃圾分类则是一个低频识别任务垃圾箱面前的人一般会停留几秒甚至十几秒识别结果晚个一两秒完全没影响。这意味着三者对帧率、输入分辨率、模型容量的需求是三个方向人员入侵需要高帧率中分辨率烟火检测需要中低帧率高分辨率尽量保住小目标垃圾分类只需要极低帧率小分辨率。如果我把三个任务都按“实时检测”的标准去设计那 NPU 资源肯定不够但如果按各自真实需求分配6 TOPS 其实是富余的。1.2 RK3588 的 NPU 家底盘点RK3588 的 NPU 标称算力是 6 TOPSINT8由 3 个 NPU core 组成。注意FP16 下的实际算力会打折到 3 TOPS 左右所以量产方案基本都要走 INT8 量化这个是后话。3 个 NPU core 之间不是只能联合跑一个大模型恰恰相反它们可以各自独立执行不同的模型也可以把其中两个或三个核心联合起来跑同一个大模型通过 rknn-toolkit2 的 core_mask 参数控制。这是整个方案的核心基础。既然有三个核心最简单的做法就是一个任务绑一个核心人物检测绑 core0、烟火检测绑 core1、垃圾分类绑 core2。这样从硬件层面就把三路人马隔离开了理论上不存在互相抢占算力的问题。实际跑下来也确实如此NPU 层面的并行度很高反倒是 CPU 侧的数据搬运、预处理、后处理更容易成为瓶颈。1.3 资源分配的核心策略不是堆模型是分资源我的最终策略可以总结成一句话每个任务都往“够用”方向压而不是往“最强”方向配。人员入侵用 YOLOv5s640×640烟火检测用 YOLOv5s416×416垃圾分类直接用 MobileNetV3-Small224×224三者的模型体积和处理开销差异很大。这样安排之后NPU 单核跑一遍的时间大致是人员入侵 25~35ms烟火检测 15~20ms垃圾分类 5~10ms。三个核各自忙各自的整体看板子的推理吞吐量可以接近 50~60 FPS 的等效算力。但如果反过来让三个模型都用 YOLOv5s 640还每帧都跑那即使 NPU 能扛住CPU 侧的图像缩放、颜色格式转换、归一化也会成为瓶颈而且模型之间会因为 DDR 带宽争抢而明显变慢。所以我非常不建议一上来就追求“全模型、全帧率”。2. 模型选型与 RKNN 转换细节2.1 每个任务选什么网络为什么尽量统一到 YOLO 生态先说人员入侵。我选的是 YOLOv5s输入 640×640。人员入侵本身只是“行人检测 区域判定”区域判定可以完全放在后处理检测出人的目标框后判断框底部中心点是否落在预设的电子围栏多边形内用射线法几十行代码搞定不需要模型去学“入侵”这个概念。烟火检测我原本想用专门的烟雾火焰检测网络后来还是换回了 YOLOv5s输入压到 416。原因很简单RKNN 对 YOLO 系列算子的支持已经非常成熟转模型时几乎不用处理算子兼容问题。烟火目标的特征是颜色偏红/偏亮、边缘不规则这些特征靠数据增强和训练数据多样性就能让 YOLOv5s 学会没必要为了这个任务引入 Faster R-CNN 或者更重的网络。如果你的烟火样本里有大量小目标可以在训练时把输入调回 640或者加一层 P2 小目标检测头但实际部署时先跑通流程再优化精度。垃圾分类严格说是一个“检测分类”的复合任务。我最初的构想是先检测出垃圾区域再对每个区域做分类。后来发现对于固定机位的垃圾桶场景直接用检测模型一阶段输出框类别更省事。但如果你的场景里垃圾类别特别多比如几十种检测模型的类别头会明显变大这时候用“轻量检测 MobileNetV3 分类”的两级方案更划算。我最终为了部署方便统一了推理框架让三个模型都走同一套 rknn 推理代码。2.2 转换前的算子检查无论用什么网络转 RKNN 之前必须把模型导出成 ONNX然后在 rknn-toolkit2 里做转换。转换时我最关心的不是“能不能转成功”而是“日志里有没有算子 fallback 到 CPU”。在 rknn-toolkit2 的转换日志里如果出现类似W Unknown layer XXX, will fallback to CPU或I [op] YYYY use NPU这样的信息一定要逐条确认。凡是在 CPU 上执行的算子都会把整段推理的时延拉长而且 NPU 和 CPU 之间还要来回搬运数据性能损失远比想象中大。对 YOLOv5/YOLOv8 这类模型最常见的坑是导出 ONNX 时带了太多辅助输出或者后处理算子NMS也被塞进了模型里。我的做法是导出 ONNX 时就裁掉 NMS让模型只输出原始预测张量后处理全部放到 NPU 外面的 CPU 上跑。RK3588 的 CPU 也不弱做一版带置信度筛选的 NMS 完全来得及还让模型更干净。还有一个容易被忽略的点ONNX 里如果用了动态维度RKNN 转换时会有问题。我习惯在导出时把输入固定成batch1输入尺寸固定死比如1x3x640x640。动态分辨率虽然也能转成多个静态 shape但部署复杂度高对这三个任务来说完全没必要。2.3 量化校准集和参数怎么配RK3588 NPU 的常见部署方式是 INT8 量化因为 6 TOPS 的算力指的就是 INT8。模型转换时的量化配置直接影响最终精度。我整理了三个关键参数mean_values和std_values必须和训练时的预处理一致。很多项目在量化后精度崩掉查到最后发现是均值方差写错了模型输入分布完全对不上。量化数据类型rknn-toolkit2 通常默认asymmetric_quantized-8就够了它对大多数视觉模型效果都不错。如果精度特别敏感再考虑混合量化。校准集这是最容易偷懒也最容易翻车的地方。校准集图片要尽量贴近真实部署场景而不是随便从训练集里拿几张。我实际会从摄像头录像里抽帧覆盖不同光线、不同距离、不同姿态每类任务大概 300 张左右多场景混合。校准集的数量不需要很多300 张足够过多反而拖慢转换时间。关键是有代表性。像烟火检测如果校准集里全是远距离小火苗量化后模型对近距离大火的响应就不好反过来也一样。2.4 精度验证与“回退 CPU”的坑转换完成后我会用 rknn-toolkit2 的accuracy_analysis功能逐层对比量化和浮点模型的输出差异重点看最后几层的余弦相似度。如果相似度低于 0.99我会先怀疑校准集分布再怀疑模型里有没有对量化特别敏感的层。比如检测头里的某些大数值输出层可以在 config 里指定对这些层不量化或者用更高精度。这里再强调一次“回退 CPU”的坑。哪怕最终模型精度没问题也要检查每个算子的执行设备。我遇到过一次很隐蔽的情况YOLOv5s 模型转换成功精度验证也通过但部署后单帧推理要 200ms后来一看日志模型里某个 Resize 算子和一个 Split 算子全部 fallback 到了 CPU真正跑在 NPU 上的算子没几个。这类问题不在转换阶段严格把关上线后很难查。3. 多模型并发调度的工程实现3.1 三核并发的入口core_mask 用法RKNN 在运行时通过init_runtime传入core_mask参数来决定模型跑在哪些 NPU core 上。可用的掩码大致有这些core_mask 取值含义RKNN_NPU_CORE_0只在 NPU core0 上执行RKNN_NPU_CORE_1只在 NPU core1 上执行RKNN_NPU_CORE_2只在 NPU core2 上执行RKNN_NPU_CORE_0_1在 core0 和 core1 上联合执行RKNN_NPU_CORE_0_1_2三个核心联合执行RKNN_NPU_CORE_AUTO由驱动自动分配我建议在多模型场景下不要用AUTO显式给每个进程指定核心。AUTO模式在单模型时很省心但多模型同时跑的时候驱动不一定能理解你的业务优先级。我实际采用的做法是人员入侵绑RKNN_NPU_CORE_0烟火检测绑RKNN_NPU_CORE_1垃圾分类绑RKNN_NPU_CORE_2。三种任务从初始化开始就井水不犯河水。有一点要提醒同一个 rknn 模型实例不能被多个线程同时inferenceRKNN Python 接口本身不是线程安全的。所以我不用线程而是直接用多进程每个进程创建独立的 RKNN 实例各自加载模型、各自设置 core_mask。3.2 共享视频帧避免三路重复解码如果三个模型各自从摄像头拉流、各自解码CPU 和内存带宽会很快被消耗掉而且三路视频流时间戳还会产生偏移。正确做法是只开一个视频采集进程把解码出来的帧放进共享内存三个推理进程自己按需取帧。实际项目里我用的是 Python 的multiprocessing.shared_memory配合一个带锁的循环队列。视频采集进程把帧写成共享内存块并维护一个frame_id计数器推理进程拿到共享内存句柄后只做图像数据的引用和格式转换不再重复解码。需要注意帧的生命周期管理一个推理进程处理慢另一个推理进程想覆盖这块共享内存时必须等前者释放所以我在队列里常驻 2~3 帧缓冲避免互相阻塞。如果你用的是 C/C 部署也可以用 DMA-BUF 或 RGA 来做零拷贝性能会更好。Python 方案在 RK3588 上也能跑通但对内存拷贝要特别敏感稍有疏忽 CPU 使用率会飙升。3.3 推理调度优先级 降帧率组合拳多进程跑起来之后还要解决“业务优先级”的问题。三个模型都独占了一个 NPU core但人员入侵和烟火检测是告警型任务垃圾分类只是统计型任务。如果垃圾分类每帧都跑即使它跑得再快也会占用一部分 DDR 带宽影响另外两个任务的端到端时延。我的调度策略很简单高优先级任务每帧都推理低优先级任务按 N 帧取一次。人员入侵每帧都跑25 FPS烟火检测也是每帧都跑15~25 FPS垃圾分类则每 5~10 帧才跑一次相当于 2~5 FPS。这不会牺牲垃圾识别准确性因为一个人丢垃圾的过程通常会持续好几秒低频采样完全能抓到关键帧。如果后续想让系统更智能可以加一个“联动触发”人员入侵检测到有人进入垃圾桶区域时临时把垃圾分类的推理频率提上来。不过这个属于锦上添花先把基础调度跑稳最重要。3.4 关键代码骨架三进程部署下面我用伪代码把整体结构写出来方便对照实现。这是 Python 版本的骨架用 multiprocessing 创建三个子进程。import multiprocessing as mp from rknn.api import RKNN def process_intrusion(frame_queue, result_queue): rknn RKNN() rknn.load_rknn(./models/intrusion.rknn) rknn.init_runtime(core_maskRKNN.NPU_CORE_0) while True: frame frame_queue.get() # 预处理、推理、后处理、区域判定 dets rknn.inference(inputs[frame]) result_queue.put((intrusion, dets)) def process_fire(frame_queue, result_queue): rknn RKNN() rknn.load_rknn(./models/fire.rknn) rknn.init_runtime(core_maskRKNN.NPU_CORE_1) while True: frame frame_queue.get() dets rknn.inference(inputs[frame]) result_queue.put((fire, dets)) def process_garbage(frame_queue, result_queue): rknn RKNN() rknn.load_rknn(./models/garbage.rknn) rknn.init_runtime(core_maskRKNN.NPU_CORE_2) frame_id 0 while True: frame frame_queue.get() if frame_id % 5 ! 0: # 每5帧识别一次 frame_id 1 continue frame_id 1 dets rknn.inference(inputs[frame]) result_queue.put((garbage, dets)) def capture_frames(shared_frames, frame_queue): # 从摄像头/RTSP取流写入共享内存并把帧引用放进frame_queue pass if __name__ __main__: frame_queue mp.Queue(maxsize3) result_queue mp.Queue() p1 mp.Process(targetprocess_intrusion, args(frame_queue, result_queue)) p2 mp.Process(targetprocess_fire, args(frame_queue, result_queue)) p3 mp.Process(targetprocess_garbage, args(frame_queue, result_queue)) p0 mp.Process(targetcapture_frames, args(shared_frames, frame_queue)) p0.start(); p1.start(); p2.start(); p3.start()实际部署时还需要在采集进程里维护共享内存的生命周期并且把告警事件统一汇总到一个事件总线里方便上层做联动。这个骨架只是把最核心的并发关系呈现出来。4. 实测性能与调优清单4.1 一份三轮并发实测数据我在 RK3588 开发板8GB 内存、LPDDR4x上跑过一组比较典型的数据环境是 Linux rknn-toolkit2 1.6。模型全部 INT8 量化三个进程独立绑定不同 NPU core各跑各的输入分辨率任务网络与输入绑定核心单帧推理耗时实测部署帧率人员入侵YOLOv5s 640×640core028ms 左右25 FPS烟火检测YOLOv5s 416×416core116ms 左右25 FPS垃圾分类MobileNetV3-Small 224×224core28ms 左右5 FPS每5帧取一次三个模型同时跑的时候人员入侵的端到端时延会从单跑时的 28ms 增加 5~8ms主要原因不是 NPU 算力不够而是 CPU 侧的后处理和图像预处理在抢核心。总体来看NPU 利用率大约在 60%~70%还有余量。如果全部模型都用 YOLOv5s 640并且在同一个 core_mask 下跑那单帧耗时很可能会被拉长到 100ms 以上三个任务全部卡顿。差距就是这么大所以“核绑定 分级帧率”这套组合是必须的。4.2 调优优先级排序如果跑完发现性能还是不够我建议按下面的顺序做调优而不是盲目换模型第一优先确认每个算子都跑在 NPU 上消除 CPU fallback。第二优先调整低优先级任务的推理频率垃圾分类降到 1~2 FPS 都不会有问题。第三优先压缩输入分辨率。烟火检测从 640 降到 512人员入侵从 640 降到 512优先保证目标仍然可辨识。第四优先用 RGA 硬件做图像缩放和格式转换把 CPU 从预处理中解放出来。第五优先检查线程/进程的 CPU 亲和性把三个推理进程分别绑到不同的 CPU 大核上。实测中很多性能瓶颈其实出在预处理和后处理上。比如 YOLOv5s 的后处理需要遍历数千个候选框在 Python 里循环很慢我一般会用向量化的 NumPy 操作或者直接写成 C 扩展。4.3 长期运行的稳定性与温控RK3588 三个 NPU core 长期满载发热非常明显。我们第一次跑满负载测试时板子温度很快冲到 85℃ 以上然后 NPU 开始降频推理耗时直接翻倍。后来加了主动散热风扇并且用 PWM 根据温度动态调速才把温度稳定在 65℃ 左右。如果你用的是 RK3588 的开发板建议在系统里提前打通风扇控制逻辑比如根据/sys/class/thermal/thermal_zone0/temp的温度值动态调整 PWM 风扇转速。这个话题和“RK3588 读取风扇转速”“PWM 风扇”这类问题经常一起出现其实就是在设备树里配置好风扇的 PWM 引脚然后写一个简单的温度控制循环。对于单板长时间跑三路视觉的场景这块不能省。另外如果项目用了 buildroot 或自定义系统建议把 CPU 调频策略改成performance模式避免 CPU 动态降频干扰推理进程的实时性。NPU 本身没有独立的调频接口但 NPU 的负载高了CPU 调度和 DDR 频率都会受到影响所以要整体观察。5. 常见问题与排查实录5.1 模型转换成功但推理极慢先查算子执行设备。用 rknn-toolkit2 的init_runtime时可以打开perf_debugTrue跑一次推理后输出每个算子的执行耗时。如果看到大量算子的耗时都集中在 CPU 设备上说明模型在转换时发生了 fallback。我遇到最多的是 YOLOv8 的输出头里某些 Gather/Slice 算子被放到 CPU解决方法是导出 ONNX 时对检测头做裁剪或者在模型结构里避免动态 shape 操作。5.2 多进程 init_runtime 冲突或失败RKNN 多进程分别初始化时偶尔会遇到第二个进程init_runtime失败报 NPU 上下文创建错误。这个问题在多个 RKNN 实例同时初始化的瞬间容易出现。我的处理办法是让三个子进程启动后先各自sleep0.5~1 秒错开初始化时间窗口或者在一个主进程里先依次完成三份 RKNN 模型的初始化再通过 fork 创建子进程。前者改动小后者更稳。现象可能原因处理办法第二个模型 init_runtime 报错多个 RKNN 上下文同时抢占驱动子进程错峰启动间隔 0.5~1s推理结果全为 0输入数据格式或归一化不一致检查 mean/std 和 data_format 设置偶发段错误共享内存生命周期管理不当用带引用计数的共享结构避免提前释放长时间运行越来越慢NPU 降频或内存泄漏监控温度、检查推理循环中是否有对象反复创建5.3 内存带宽竞争导致丢帧三路推理同时进行时每路都要做图像缩放、颜色空间转换、推理输入拷贝。如果全部用 CPU 做DDR 带宽很快被打满。我用perf top查过CPU 大量时间花在memcpy和resize上。最后的解法是把缩放和格式转换交给 RGA 硬件处理同时把共享内存的拷贝次数降到最低。如果项目用 Python建议对摄像头帧直接复用同一块输入缓冲区能省一次拷贝就省一次。5.4 量化后精度崩了精度崩了先不要急着换模型先做三件事第一确认校准集图片和真实场景分布一致第二检查mean_values、std_values是否和训练一致第三用accuracy_analysis定位第一个掉点的层。如果问题出在检测头可以对检测头和主干分别设置量化策略比如检测头用更高精度的量化方式主干保持 INT8。这个操作在 rknn.config 里对应混合量化配置。5.5 烟火检测误报多模型层面能解决一部分但更多是在后处理方面做时间维度的确认。火焰和烟雾在连续帧里是运动的而路灯、红色汽车、太阳反光等干扰源相对静止。我实际应用时会给烟火检测加一个“连续 N 帧触发”机制比如连续 3 帧都检测到才报火警误报率能下降一大截。垃圾分类也是同样逻辑连续两次识别到同一类垃圾才计入统计结果避免人一走动就误判。结尾一点个人经验这套方案真正落地之后我最大的体会是单块 RK3588 的 NPU 同时跑三个模型并没有那么难难的是知道“哪个任务可以少跑几帧”“哪个模型必须独占核心”。资源分配和业务优先级对齐了三个任务的问题就会自动解决。最后再分享一个小技巧三个模型的推理输出不要各写各的告警函数统一丢到一个事件队列里由上层按时间戳和任务类型统一处理这样后面接显示屏、接平台、接联动设备都会轻松很多。