2026/9/8 15:33:14

RK3588多模型并发部署指南:一颗NPU跑通三类视觉任务

RK3588多模型并发部署指南:一颗NPU跑通三类视觉任务 前段时间给一个边缘智能盒子的项目做技术预研需求本身不复杂现场有一路或者两路摄像头既要管周界的人员入侵又要盯着烟火隐患还得对垃圾投放行为做分类。第一版方案里我差点被客户的功能清单带着走打算每个功能独立配一块板子结果一算物料成本和整机功耗就发现太离谱——RK3588 单芯片 NPU 有 6 TOPS 算力同时带三个轻量化视觉模型完全够用。问题不在于“能不能跑”而在于怎么把人员入侵、烟火检测、垃圾分类这三个模型合理地塞进同一颗 NPU 里让各自不掉链子整体还不卡顿。这篇文章就围绕这个目标展开。我会从架构选型、算力核算、模型转换、多模型并发调度、后处理联动到实测踩坑完整梳理一遍。适合正在做边缘 AI 盒子、安防一体机、智能相机或者打算从单模型部署切到多模型并发部署的朋友参考。如果你用的不是 RK3588只要 NPU 算力在 4~10 TOPS 之间这套思路同样能迁移。1. 三路模型挤一块板子架构上先想清楚三件事1.1 为什么这三个任务适合共用同一个 NPU人员入侵、烟火检测、垃圾分类看起来是三个完全不同的业务但它们有一个共同点数据源都是同一路或者同一区域的视频画面。既然都是对着摄像头画面做分析那就没必要在硬件层把它们拆开让三块板子各看各的。共用一个 NPU 的核心逻辑很简单输入侧可以复用一路视频流经过一次硬解码、一次缩放就能喂给三个模型输出侧再按各自的业务逻辑分发告警结果。当然复用输入流只是第一步。真正让“一芯三用”成立的前提是三个模型的算力需求不能超过 RK3588 的 NPU 总量并且对实时性的要求可以分级。前面提到的三个任务里烟火检测对实时性要求最高人员入侵次之垃圾分类通常是事后统计延迟几百毫秒都无所谓。这种天然的优先级差异恰恰适合用一颗 NPU 做时间片调度。1.2 业务优先级决定算力分配比例很多初次做多模型部署的人上来就想让三个模型“绝对公平”地轮询跑每个模型各占三分之一算力。这种思路在技术上最容易实现但业务上往往不合理。我习惯先跟客户把优先级谈清楚然后按优先级定采样策略业务实时性要求建议推理频率优先级烟火检测秒级内必须报警每帧或每两帧最高人员入侵可接受 1~2 秒延迟每两帧到每三帧中垃圾分类事后统计延迟不敏感每五帧甚至每半秒一帧低这样一个“1:1:0.2”的算力配比比“1:1:1”健康得多。烟火检测绝不能因为垃圾分类的推理任务排到队列前面而延迟否则真出了火情告警慢半拍整个系统的价值就没了。1.3 两条路线对比分时复用 vs 常驻并发聊到具体实现最常见的方案有两种第一种是“分时复用”也就是主循环里按顺序调三个模型先跑人员入侵再跑烟火检测最后跑垃圾分类跑完再回第一帧。这种方案写起来非常省事但问题很大——一帧画面从进入到出结果的端到端延迟是三个模型推理时长的总和。如果三个模型各需要 25ms、20ms、5ms一帧就要 50ms算下来只有 20FPS 左右如果烟火模型输入分辨率拉高延迟会继续膨胀。更致命的是CPU 和 NPU 完全串行所有的预处理、推理、后处理都堵在一条线上。第二种是“常驻并发”三个模型各自拥有独立的推理线程或者由一个调度线程统一向 NPU 提交请求。这样烟火检测和人员入侵的请求可以交错排队NPU 不至于被某个慢任务占死每个任务都能拿到更稳定的响应时间。代价是代码复杂度会高一点需要对队列和线程同步有一定把握。我实际做下来RK3588 上最稳定的并不是三线程各自狂调 rknn_run而是一个 NPU 调度线程加多个输入队列的组合。后面第五章会展开讲具体做法。2. RK3588 的算力账量化后的模型吃多少还剩多少2.1 6 TOPS 的实际可用算力是多少RK3588 的 NPU 理论算力是 6 TOPSINT8内部是三核设计三个核心可以独立使用也可以合并成一个大的计算域。很多宣传材料都会把这 6 TOPS 说得满满当当但做部署的人必须保持清醒理论算力只有在算子执行效率接近 100% 的时候才成立。实际跑 YOLO 这类检测模型由于 Concat、Resize、后处理中的某些算子会在 NPU 和 CPU 之间来回切换NPU 的整体利用率能做到 60%~80% 已经算优秀。我做多模型并发时习惯按“可用算力 4~4.5 TOPS”来做预算超出这个预算的方案一律砍掉。这样就算遇到模型里某个算子被编译器优化得不够好或者输入分辨率临时提高系统仍然有余量不会一压测就崩。2.2 三模型的参数量、计算量与 INT8 量化后的真实开销选型阶段我建议先做一张“算力体检表”。下面这组数据是基于常见公开模型和 RKNN 实测得到的参考值不同分支版本会有差异但量级不会差太多模型任务输入尺寸计算量 (FP32)INT8 量化后RKNN 文件大小单次推理参考耗时YOLOv8n人员入侵检测640×640约 8.7 GFLOPs约 10~12 MB20~30 msYOLOv5s烟火检测640×640约 16.5 GFLOPs约 18~20 MB35~50 msMobileNetV3-Large垃圾分类224×224约 0.22 GFLOPs约 4~6 MB3~8 ms注意两个关键点。第一烟火检测我选了 YOLOv5s 而不是 YOLOv8s是因为烟火属于小目标和半透明目标模型过小容易漏检YOLOv5s 的检测头对烟雾这类弥散性目标更友好RKNN 编译器对 YOLOv5 系列的优化也更成熟。第二垃圾分类不要用检测模型直接用分类模型就好。垃圾分类的业务核心是判断“这袋垃圾投没投对”而不是给每个垃圾画框MobileNetV3 的 224 输入足够用推理开销几乎可以忽略。如果三个模型都跑满单帧总耗时大约在 60~85ms。但这只是理论串行值通过并发调度和错帧推理实际能做到 20FPS 左右的整体出图率后面第五章会给实测数据。2.3 显存、内存与带宽的隐形天花板算力之外还有一个容易被忽视的天花板内存带宽。RK3588 的内存接口虽然带宽可观但多路视频解出来的 NV12 帧、三份缩放后的模型输入、三个模型的中间特征图、后处理要裁剪的图片区域全部都在抢 DDR 带宽。我遇到过一种情况单独跑人员入侵模型时非常流畅一加上烟火模型两个模型的实际耗时分别增加了 30% 以上。表面看是 NPU 算力不够实际上是因为三个模型的中间张量频繁读写内存把带宽打满了。解决办法有两个思路一是把输入分辨率降下来二是合理控制并发帧缓冲的数量不要让每个模型线程都缓存那么多历史帧。我建议整机项目如果用 8GB 内存版本保留给系统、解码、业务逻辑的内存大约 4.5~5GB剩下 3GB 给算力相关模块在这个预算内做规划。三个 RKNN 模型本身加载后占的内存很小真正吃内存的是多路视频缓冲和推理中间结果。3. 要把三个模型喂到同一张调度图里输入侧先统一3.1 视频源选择RTSP / USB / MIPI 在实际项目里的取舍接入视频源这件事看起来跟 NPU 没关系但它直接影响 NPU 能不能吃到稳定的帧。如果只有一个摄像头我建议优先用 MIPI CSI 或者 USB 摄像头延迟最低驱动也稳。如果是做盒子和 NVR 类产品摄像头大概率是网络摄像头这时候只能走 RTSP 拉流。RK3588 的 MPP 硬解模块对 RTSP 的 H.264/H.265 视频流支持很好解码出来的帧可以直接作为推理输入。我实际测试过单路 1080p25fps 的 RTSP 流用 MPP 硬解后 CPU 占用不到 5%所以不要用 FFmpeg 软解来做实时推理否则 CPU 会被解码吃掉一两个核留给 NPU 调度的余量就没了。3.2 缩放与颜色转换交给 RGA别让 CPU 干这种粗活三个模型的输入尺寸不一样人员入侵 640×640烟火检测 640×640垃圾分类 224×224。所以每一帧从解码器出来之后至少要做两次缩放。这个操作如果放在 CPU 上用 OpenCV 的 resize 做看似没事但多路视频叠加后 CPU 占用会明显上涨。RK3588 自带的 RGA 硬件加速模块就是干这个的。把 NV12 原始帧直接送进 RGA一次可以输出三份不同分辨率的 RGB 数据分别给三个模型。颜色格式转换和缩放都在硬件里完成CPU 几乎零负担。刚开始做的时候我也犯过懒直接拿 OpenCV 处理直到压测到四路视频时 CPU 被打满才发现 RGA 这个近道。3.3 输入分辨率的折中方案640 还是 512 还是 320很多初学者喜欢直接把训练时的分辨率搬过来用这没问题但要注意 NPU 对输入尺寸的对齐要求。RKNN 一般要求宽高是 16 的倍数如果原始分辨率不是 16 的倍数RKNN-Toolkit 转换或运行时会自动 padding这会白白浪费一部分算力。我的经验是检测模型保底 640×640如果实测帧率不够优先砍到 512×512 而不是 416×416。因为 512 仍然能保持对中等目标的召回416 会对小目标比如远处的烟头、远处的行人造成明显损失。垃圾分类分类模型用 224 或者 288 都行这个环节最不吃分辨率。还有一个必须统一的细节三个模型各自训练时可能用了不同的 letterbox 策略。喂给 NPU 之前一定要确保预处理里 letterbox 参数、归一化系数、通道顺序RGB 还是 BGR跟模型训练时一致。不然模型推理出来的置信度会被微妙地压低尤其是烟火这种正负样本不均衡的目标差一个点的置信度就可能导致漏报。4. RKNN 转换与多模型加载不要硬把三个模型塞进同一个 context4.1 用 RKNN-Toolkit2 做 INT8 量化时的关键设置从 PyTorch 训练好的模型到 RK3588 能跑的 .rknn 文件中间要经过 RKNN-Toolkit2 的转换。转换步骤本身不复杂但有几个点非常影响最终效果。首先是导出 ONNX。YOLOv8 导出时要固定输入尺寸不要在模型里留动态 shape动态 shape 在 RKNN 转换时虽然能过但运行效率会比静态输入差不少。导出时把 opset 固定在 12 或 13不要用太新的算子集RKNN 编译器对旧版 ONNX 的兼容性反而更好。其次是量化数据集。INT8 量化需要准备代表性图片这些图片应该贴近真实场景而不是直接从训练集里随便抽几张。做烟火检测量化时我专门从夜间红外、逆光、阴天三种场景各抽样了几十张图片量化出来的模型在夜间的漏检率明显低于随手抽样。量化数据集每类几十张就够了关键是多样性不追求数量。量化时我建议用--target_platform rk3588量化精度选i8。如果发现某些层掉点严重可以尝试把quantized_dtype设成w8a8或者对关键层做混合量化保留 FP16。不过混合量化会拖慢推理速度能不用就不用。4.2 多模型加载每个模型独立 context 是最稳的姿势在板端代码里最容易出现的问题就是想当然地用一个模型句柄反复加载。RKNN Runtime 支持在同一进程里创建多个 context每个 context 独立加载一个 .rknn 模型。我的建议就是人员入侵一个 context烟火检测一个 context垃圾分类一个 context三者互不干扰。#include rknn_api.h static rknn_context ctx_intrusion; static rknn_context ctx_fire; static rknn_context ctx_garbage; // 初始化阶段分别加载 rknn_init(ctx_intrusion, yolov8n_intrusion.rknn, 0); rknn_init(ctx_fire, yolov5s_fire.rknn, 0); rknn_init(ctx_garbage, mobilenetv3_garbage.rknn, 0);这三个 context 相当于三个独立的推理会话每个会话有自己的输入输出缓冲区。把它们交给调度线程统一管理不只代码逻辑清晰出现问题时也容易定位是哪个模型出的错。很多人问“能不能只 init 一次然后反复切换模型”技术上可以但切换模型会导致上一次模型的计算图从 NPU 里卸载下一次推理时再重新装载这个开销远比多开一个 context 大所以不要干这种省芝麻丢西瓜的事。4.3 要不要合并成一个多任务模型项目评审时有同事提过一个问题能不能把三个模型的 backbone 合并变成一个多任务模型这样只需要一次前向推理就能同时出三个结果。这个想法理论上很诱人但落地时有几个现实问题。第一三个模型的训练数据不同人员入侵用的是行人数据集烟火检测用的是烟火数据集垃圾分类用的是垃圾图片数据集。硬凑成一个多任务模型需要重新标注一份同时包含行人、烟火、垃圾三种标签的数据集标注成本极高。第二多任务模型的调参难度成倍增加三个任务的 loss 需要重新平衡搞不好一个任务涨了另一个任务就掉了。第三RKNN 编译多任务模型时如果模型里有多个不同尺度的检测分支和分类分支算子融合效率并不一定高。所以我的结论很明确部署阶段的合并价值有限除非你在算法训练阶段就是按统一的多任务架构来设计和训练的否则不要指望在部署阶段通过模型合并来省算力。独立加载、统一调度才是最稳妥的方案。5. 时间片调度实战让 NPU 按优先级轮转5.1 三种并发方案的实测表现我把 RK3588 上常见的多模型并发方案都试了一遍效果差别挺大第一种是“单线程串行”。代码最简单烟火检测、人员入侵、垃圾分类按顺序跑。实测三模型全跑时整体吞吐帧率只有 12FPS 左右烟火检测的端到端延迟接近 200ms。这个方案只适合验证功能不适合交付。第二种是“三线程各自跑”。每个模型一个线程每个线程里都是一个 while 循环不断执行 rknn_run。理论上三个模型可以并行排队实测吞吐帧率能到 20FPS 左右烟火检测延迟降到 150ms 以内。但有个隐患三个线程同时向 librknnrt 提交请求时内部的排队顺序不是我们能控制的烟火检测的优先级优势体现不出来偶尔还会出现某两帧请求同时到达 NPU导致一帧等待时间超过 60ms。第三种是“单调度线程 多输入队列”我最终采用这个方案。主循环统一从三个输入队列里取最新帧按优先级策略依次向三个 context 提交推理请求。这样做的好处是调度权完全掌握在业务代码手里而不是交给 librknnrt 的默认排队逻辑。// 调度线程主循环伪代码 while (running) { if (fireQueue.hasFrame()) { rknn_run(ctx_fire, ...); // 烟火最高优先级 } if (intrusionQueue.hasFrame()) { rknn_run(ctx_intrusion, ...); // 人员次之 } if (garbageQueue.hasFrame()) { rknn_run(ctx_garbage, ...); // 垃圾分类最后 } }5.2 优先级与抢占烟火检测必须被优先喂帧这里有个细节值得展开调度线程不是到了循环里才判断优先级而是要在帧入队阶段就做丢弃策略。比如烟火检测线程发现队列里有 3 帧待处理但它每帧只能处理 1 帧那就保留最新的一帧把前面两帧丢掉。因为对实时告警来说旧的帧已经没有意义了处理旧帧只会增加输出延迟。人员入侵可以稍微放宽一点保留最近两帧因为入侵行为有一个“走进来”的过程保留两帧能更好地判断移动轨迹。垃圾分类完全不追求实时队列里能保留一帧最新的就行甚至可以把推理周期拉长到 500ms 一次。工业现场最容易犯的错是把每个模型都做成“有帧就处理”的消耗式线程这样看起来每个模型都很忙实际上都在处理垃圾帧。把丢弃策略放在入口比在 NPU 调度上做抢占要高效得多。5.3 晚到结果与事件确认机制调度再合理也避免不了极端情况下一帧的推理结果晚到。比如烟火模型正在处理一帧时人员模型先提交了一个短任务NPU 先完成了人员模型这时候烟火结果才姗姗来迟。对业务逻辑而言不能把这批晚到的结果当作最新状态直接上报否则会出现“人早就走了告警才弹出来”的尴尬。我的处理方式是给每路输入帧打一个时间戳推理结果返回后先核对时间戳。如果结果对应的帧已经过时比如超过 200ms不触发业务告警只作为统计信息。另外真正的告警不要依赖单帧结果建议连续 3 帧在同一位置检测到目标才触发。这个“3 帧确认”机制能滤掉大量抖动误报尤其在烟火检测里能有效抑制阳光反射、红色塑料袋这类干扰。5.4 用系统文件实时监控 NPU 负载做并发调度最怕的是拍脑袋调参数不知道 NPU 到底紧不紧张。RK3588 在调试模式下的 sysfs 节点可以读出 NPU 的实时负载cat /sys/kernel/debug/rknpu/load正常能看到类似NPU load: 72%的输出。我调调度策略时会先把三模型跑到满载看负载基线再逐步调整帧率配额。如果 NPU 负载长期超过 90%说明系统太满需要降低某个低优先级任务的频率如果长期只有 40%说明方案还有余量可以提高人员入侵的检测频率。内存侧的监控不要只看 free要看可用内存的波动。多模型并发时RKNN 内部有内存池机制如果反复执行 init/release内存碎片会越积越多。建议把 NPU 上下文初始化放在程序启动阶段运行期间不要频繁销毁重建。6. 后处理阶段的结果打架与业务联动6.1 三份结果的坐标统一因为三个模型的输入分辨率不同它们输出的检测框/分类结果必须还原回原始画面坐标才能做联动分析。这里的核心是换算比例不要手写错。比如原始帧是 1920×1080经过 RGA 缩放并 letterbox 到 640×640实际缩放比例是min(640/1920, 640/1080)计算得到约 0.333。检测框还原到原图时要先把 letterbox 偏移量去掉再除以缩放比例。三个模型的 letterbox 参数必须跟训练时保持一致否则框会整体漂移。我建议在后处理阶段写一个公共的scale_coords函数三个模型结果都走同一个函数避免每路单独实现导致比例错误。别问我为什么强调这个第一次联调时我就因为烟火模型忘了减 letterbox 偏移导致在画面上叠加告警框时全部偏了 40 个像素排查了大半天。6.2 类别冲突烟火模型的火源框 vs 垃圾分类模型的可回收物多模型一起跑之后会出现一个很有意思的问题不同模型的类别体系有重叠。烟火检测模型里的“火源”类别可能把垃圾投放点的打火机、烟头也检测成火源垃圾分类模型里的“烟头”如果是可回收物里的有害垃圾类别又会跟烟火模型的火源框重叠。处理方式不是把某个模型的结果直接屏蔽而是让它们互相作为旁证。我设计了一套逻辑场景烟火模型结果垃圾分类模型结果最终动作烟雾/火焰fire/smoke 置信度 0.6无触发火警垃圾堆自燃fire 置信度 0.4~0.6检测到大量可燃垃圾触发“疑似火情”复核烟头高温fire 置信度 0.3~0.5检测到烟头记录事件不直接报警这个设计既避免误报又能把垃圾分类模型的结果作为火情研判的补充信息。6.3 业务联动入侵告警、烟火告警、垃圾分类证据留痕单跑模型没有任何业务价值真正的价值在联动。我这里举一个实际做过的场景某个园区垃圾投放点客户希望识别“是否有外来人员进入投放点内部区域”以及“是否有人混投垃圾”。三者是这么联动的人员入侵模型先在投放点周界画一个虚拟围栏检测到有人进入并跨过围栏边界时立刻从原始视频流里截一帧高分辨率 JPEG 作为证据图同时把这一帧送入垃圾分类模型如果分类结果不是当前投放点允许的垃圾类别就叠加记录一条“人员入侵 垃圾混投”事件。烟火检测全程后台运行只要画面里有烟雾或火焰特征不管有没有人都第一时间触发告警。这套联动跑下来客户非常满意因为它不只是一堆模型结果的罗列而是真正解决了“谁、在什么时候、在哪个区域、做了什么事”的现场问题。这也是我认为多模型并发部署最大的价值所在。7. 实测结果与现场踩坑记录7.1 多模型并发帧率与时延实测在“单调度线程 多输入队列”方案下我用 RK3588 8GB 开发板单路 1080p25fps RTSP 视频流做压测三模型全跑得到的大致数据如下业务输入分辨率调度频率单次推理平均耗时端到端事件时延人员入侵640×640每帧21 ms140 ms烟火检测640×640每帧38 ms180 ms垃圾分类224×224每 5 帧5 ms约 1.2 s整体画面吞吐率稳定在 20~22 FPSNPU 负载大约 80%CPU 占用在 25% 左右。如果把烟火模型的输入降到 512×512单次推理会降到 25ms整体吞吐率能到 28FPS烟火小目标的召回率会有轻微下降。需要实时性和召回率之间怎么权衡取决于具体场景。双路 1080p 视频同时跑时我的建议是不要每路都全模型满帧跑而是按业务重要性分配第一路关键区域烟火和人员检测每帧跑第二路一般区域烟火检测每两帧跑、人员检测每三帧跑。这样 NPU 负载不会超过 85%整体帧率也能稳定。7.2 高温降频NPU 满负荷之后最现实的问题三模型并发会让 NPU 长时间处于高负载发热非常可观。裸板无风扇状态下我实测跑 30 分钟后 NPU 温度能到 82℃此时 rockchip 的温控策略会自动降频性能直接掉三成。烟火检测从 38ms 变成 52ms这种忽快忽慢的体验在交付时很难接受。解决办法就是在设备树里配好 pwm-fan让风扇根据 CPU/NPU 温度自动调速。很多开发板默认不打开风扇调速需要手动配置。如果板子不带风扇至少要在结构上加散热片并保证机箱通风。实测有风扇主动散热后NPU 长时间满载温度能压在 65℃ 以内推理时延波动很小。另外我建议在业务层加一个高温保护逻辑当 NPU 温度超过 75℃ 时自动降低低优先级任务垃圾分类的频率保住烟火和人员检测的性能。7.3 稳定性句柄泄漏与内存碎片多模型并发和单模型部署最大的差别在于运行期间每个 context 都在持续申请和释放内存如果代码写得不规范很容易出现内存泄漏。我遇到过最典型的案例是rknn_outputs_get拿到的输出指针在用完之后没有及时rknn_outputs_release运行 8 小时后内存被吃掉了 300MB最后系统触发 OOM 把主进程杀了。排查方法很简单在测试阶段给系统加上内存监控脚本每 5 分钟记录一次/proc/meminfo里的 MemAvailable 和每个进程的 RSS。只要 RSS 随运行时间单调上涨基本可以断定有泄漏。不要等到客户现场跑挂了再回头查。另外RKNN 多 context 并发时同一时间提交给 NPU 的任务数并不是无限的。我在压力测试中发现当三个 context 都在执行 rknn_run 时如果我的调度线程没有做好互斥两个 context 同时提交请求会导致其中一个等待时间异常拉长。最后给调度线程加了互斥锁每次只允许一个 context 的 rknn_run 在运行实测反而更稳定。7.4 一个让我排查了一个下午的异步队列问题最后分享一个比较隐蔽的坑。我在测试阶段参考官方文档给模型开了异步模式希望多个 context 可以重叠执行。逻辑上没毛病实际跑起来却发现三个模型的推理结果偶尔会串线烟火模型的输出偶尔跑到人员模型的回调里而且只在系统负载高的时候出现。排查过程排除了模型加载错误最后发现是 librknnrt 在异步模式下如果多个 context 共享了同一个输入缓冲区并且后一个 context 的输入在前一个 context 的推理还没结束时就被覆盖就会出现“结果张冠李戴”。RKNN 的异步模式对缓冲区的生命周期管理要求非常严格官方文档虽然写了要复制输入但没强调多 context 并发时必须每个 context 使用独立缓冲区。我的解决办法很简单三个 context 各用各的输入输出缓冲区彻底隔离数据不共享任何推理中间资源。从那以后再也没有出现过结果串线。如果你打算开异步模式这条经验能帮你省不少时间。三路模型跑在同一颗 RK3588 上并不是把三个模型文件扔进板子里让他们自己跑那么简单。架构上要分优先级算力上要有余量输入侧要统一预处理模型侧要独立 context调度上要搞时间片和丢帧策略最后还得处理后处理阶段的类别冲突和业务联动。这些环节环环相扣任何一环偷懒最终都会以系统卡顿或者误报漏报的形式还回来。按我上面这套流程走一遍基本能保证单板方案稳定落地。如果你正在做类似的边缘多任务视觉盒子希望这篇文章能让你少走几个弯路。