资讯动态

RK3588 NPU 多模型并发部署实战:目标检测与识别的单板调度方案

发布时间:2026/9/8 18:06:10 来源:尧图企业网站定制
导语去年接到一个挺典型的边缘项目园区周界要识别人员闯入配电房和仓库区域要盯烟火垃圾分类督导点还要能自动判断垃圾桶里投进去的是哪类垃圾。现场条件很直接——不给云服务器不允许再塞一台主机只有一台已经放在弱电间里的 RK3588 盒子外加几路普通枪机摄像头。所有算法都得落到这一块板子上。拿到需求的第一反应是有点慌。看了一些 AI 边缘设备的方案大多是一个模型占一块板三个业务就得上三台盒子。但项目预算和机柜空间都不允许这么干。RK3588 那颗 NPU 算力标称 6 TOPS真要同时塞下目标检测、烟火识别、分类这三个任务关键不在模型能不能跑而在怎么让它们在同一块 NPU 上排队、错峰、不互相踩脚。这篇文章就把我最终跑通的完整方案捋一遍从模型转换、量化、并发调度到实测踩坑记录希望能给同样准备在 RK3588 上一机多用的朋友一点参照。1. 项目背景一块 RK3588 到底能同时干多少活1.1 现场硬件与任务拆解先看清楚手里的牌。RK3588 是瑞芯微的旗舰级 SoC8 核 CPU 是大核 A76 加小核 A55 的 big.LITTLE 架构内存最大支持 32GB LPDDR4/LPDDR5但绝大多数开发板和边缘盒子常见是 8GB 或 16GB 版本。NPU 部分是三个核心官方标称总算力 6 TOPS支持 INT4、INT8、INT16 以及 FP16 等精度模型一般通过 RKNN-Toolkit2 转换后部署。本次项目实际需要运行的 AI 任务有三个人员入侵检测对监控画面中的行人目标进行定位判断是否进入布防区域输出目标框和置信度。该任务对空间定位精度要求不高但要求漏报率尽量低。烟火检测实时识别画面中的明火和烟雾。烟雾目标形状不固定、边缘模糊且对小目标相对不敏感在算力分配上要适当给足模型容量。垃圾分类识别对指定投放点画面中出现的垃圾袋、饮料瓶、纸箱等物体进行分类输出类别标签。大多数时候属于高频率但低算力消耗的任务。这三个任务对模型输入分辨率要求也完全不同。人员检测可以用 640×640 输入烟火检测为了保证小目标召回也用 640×640 比较稳垃圾分类则直接用 224×224 左右的轻量分类模型即可甚至不需要做检测框。1.2 初始方案评估为什么没有选择三台设备一开始评审会里确实有人提出三台 RK3588 盒子各跑一个模型稳定且互不影响。这个方案从技术风险角度看最省事但现场条件不答应。首先是成本翻了接近三倍其次是机柜容量和供电线路已经预留好无法再增加设备数量。另一个替代方案是全部丢到云端识别但这涉及园区视频流外发合规问题尤其是针对人员行为识别这类带有管理属性的业务客户的 IT 部门明确要求数据不出园区。所以最终只能走单板多任务这条路。单板多任务在理想情况下似乎是三个模型轮流跑就能实现但实际没那么简单。每个模型都要先做视频解码、帧预处理、NPU 推理、后处理、业务逻辑上报如果简单把所有环节串起来跑一遍那总耗时等于三个模型耗时的叠加烟火检测的实时性会变得完全不可接受。所以核心问题从一开始就不是“模型能不能跑”而是“计算流水线怎么设计”。1.3 三个模型的关键约束指标为了让后续调度有明确依据我在项目启动第一周就和业务方确认了各项性能指标任务模型输入最低分析频率最大可接受延迟优先级人员入侵检测640×6405 FPS500ms高烟火检测640×6402 FPS800ms最高垃圾分类识别224×2241 FPS1500ms低这三个指标直接决定了一个关键事实烟火检测单帧跑起来最慢但频率要求其实很低人员检测更看重响应速度垃圾分类帧间隔最长。如果能设计好采样节奏单块 NPU 的 6 TOPS 算力远远够用。这也是为什么我不建议一上来就把所有模型都调到最高帧率那样内耗很大实际业务收益却几乎为零。2. 模型选型与端到端架构设计2.1 三个模型分别用什么结构模型选型的时候我参考了不少同类项目。人员入侵检测是最成熟的场景直接选了 YOLOv8n参数量小、推理速度快、对行人这类中大型目标表现不错转换成 RKNN 后算子支持也完整。烟火检测用 YOLOv5s 做了一次微调训练原因有两个一是 YOLOv5s 的 Neck 结构对于烟雾这类边缘模糊目标有更好的语义融合能力二是烟火样本本身少YOLOv5s 相对好收敛不容易过拟合。垃圾分类没有采用目标检测因为投放点摄像头通常是固定角度垃圾物体基本在画面中央区域直接把画面裁剪后交给一个分类模型就可以了。最终用的是 MobileNetV3-Small224×224 输入模型权重只有几 MB量化为 INT8 后对 NPU 来说几乎不占时间。这里有一个重要的经验不同任务不要盲目用同一套模型架构。有人图省事把垃圾分类也用一个 YOLO 检测模型去跑结果就是明明只需要输出一个类别却白白计算了边界框回归损失和大量 anchor 分支推理时间多了好几倍。在算力受限的边缘设备上任务复杂度要和模型容量严格匹配。2.2 端到端数据流不能只盯着 NPU整条端到端链路的瓶颈往往不在 NPU而在视频解码和预处理。板端视频流通过 RTSP 拉流后一般先用 FFmpeg 或 Rockchip 的 MPP 硬解码模块转成 YUV 帧再调用 RGA 做缩放和格式转换。这三个任务其实可以复用同一路解码结果一帧原始图像拉出来后分别缩放到 640×640 和 224×224 再喂给不同模型。最初团队里有人设计成“每路摄像头解码一次再按 AI 任务复制三份 YUV 数据”这种做法非常浪费 DDR 带宽。正确的做法是共用一份解码帧用 RGA 分别做缩放。RK3588 的 RGA 是专门做图像转置缩放的硬件模块不占 NPU 算力同时缩放多份小图非常快基本在 1ms 到 2ms 内完成。2.3 边缘一体机的优势以及可复用的其他场景这次选择单板三重任务的另一层原因是后续还要扩展。项目里如果后续加入车辆违停识别或垃圾桶满溢检测也就是再增加一两个轻量模型的问题。因为架构从第一天就按“共享输入帧 互相独立的分析任务”来设计新增任务只需要加一个推理线程和一份后处理逻辑。这种玩法很常见商场监控要同时跑客流统计、口罩检测、电梯困人识别工厂产线要同时跑安全帽检测、工服检测、违规操作识别。本质上都是“环境感知是同一个摄像头业务目标是多侧并行的”。如果只盯着“模型文件数量”而不是“计算流水线”大概率会把每一路视频流当成独立的完整链路来处理最后把板卡资源白白消耗在重复解码上。3. 模型转换、量化与运行时参数这些坑要提前避开3.1 RKNN-Toolkit2 的标准转换流程选好模型后第一件实操就是转换 RKNN 格式。RKNN-Toolkit2 跑在 PC 上它本身不依赖开发板只需安装对应版本的 Python 环境即可。以 ONNX 格式为例from rknn.api import RKNN rknn RKNN() # 第一步加载 ONNX 模型 ret rknn.load_onnx(modelyolov8n_person.onnx) if ret ! 0: print(模型加载失败先检查算子是否全部支持) # 第二步配置量化参数 rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588, quantized_dtypew8a8 ) # 第三步构建 RKNN 模型 ret rknn.build(do_quantizationTrue, datasetcalib_person.txt) if ret ! 0: print(构建失败) # 第四步导出模型 rknn.export_rknn(person.rknn)这里参数看起来简单但有一个很容易踩的坑mean_values 和 std_values 必须和你训练模型时做预处理的方式一致。比如训练时你用 ImageNet 标准归一化均值 0.4850.4560.406方差 0.2290.2240.225那配置里就不能简单写成 0 和 255。尤其是 YOLO 系模型很多代码是在训练时用 0~1 进行归一化并由后处理自行恢复这种情况下如果忘掉 mean/std 会导致量化后精度崩掉。3.2 检测模型和分类模型的量化不能一锅端在量化环节三个模型要分开处理。人员检测和烟火检测属于回归加分类任务输出目标框坐标和类别对边界框的回归精确度比较敏感如果量化校准集覆盖不充分很容易出现目标框偏移、置信度下降的问题。垃圾分类模型是纯分类任务类别概率输出对量化误差相对鲁棒只要校准集里每个类别都有一定数量的样本就能保持精度。给每个模型建独立的校准集非常重要。RKNN 量化需要一个校准数据文件里面写的是图片路径清单每一行一张图片通常准备 100 到 300 张足够。关键是对应场景的多样性人员检测的校准集要包含远距离小人、弯腰、逆光等画面烟火检测的校准集要把烟雾和白色云朵的近似样本都放进垃圾分类的校准集要保证覆盖所有类别而不是只找容易分类的样本。三份校准集在转换时要分别传给对应的rknn.build()调用。我见过有同学为了让流程简单把三个模型用同一份校准集做量化结果烟火检测的烟雾置信度掉了一半以上。虽然最后可以通过降低置信度阈值来兜底但误报也会大幅上升完全得不偿失。3.3 运行时参数core_mask 到底该怎么理解RK3588 的 NPU 有三个核RKNN 运行时支持通过 core_mask 指定使用哪些核。官方在rknn.init_runtime()接口里提供了RKNN.NPU_CORE_0、RKNN.NPU_CORE_1、RKNN.NPU_CORE_2以及组合值。有的资料会建议把三个模型分别绑定到三个核上看起来像是每个模型独占一个 NPU 核。实际在项目早期我也这么试过代码长这样rknn_person.init_runtime(core_maskRKNN.NPU_CORE_0) rknn_fire.init_runtime(core_maskRKNN.NPU_CORE_1) rknn_waste.init_runtime(core_maskRKNN.NPU_CORE_2)但实测后发现core_mask 更准确的理解是“当前模型优先在指定核心上调度”并非严格独占。因为本质上三个模型仍然共享 NPU 的指令队列和 DDR 带宽如果某个模型的输入帧宽度很大数据搬移依然会影响另外两个核。这一点是理解 RK3588 多模型并发的关键。所以我最终推荐的做法是在低并发负载下用默认的RKNN.NPU_CORE_AUTO让驱动自己调度核心真正靠上层任务帧率策略来减少冲突只有当长期 CPU 占用偏高或者发现某个大型模型频繁抢占时才利用 core_mask 做人工隔离。理由后面在并发调度部分会展开说明。4. “同时跑”的正确姿势不是多核并行而是时间片错峰4.1 NPU 调度的本质一条排队队列在进入编码阶段前我先做了一个模型延迟单测用同一台板子分别跑三个模型得到大致耗时数据后很多人都会拿这些数据做加法算出“总共需要多少 TOPS”然后说 NPU 算力不够。但这个加法本身是错的。RK3588 的 NPU 工作方式更接近于“接收推理请求 - 进入执行队列 - 驱动程序分配到空闲核心 - 计算结果返回”。模型 A 推理时模型 B 完全可以在等待队列里等待当 A 完成前一个 batchB 就可以立刻补上。真正要优化的不是让每个模型都“同时”占着 NPU而是让每个模型在时间轴上尽量均匀错峰避免出现三个任务同时把请求塞进队列的高峰期。一句话总结NPU 不是一台带三个独立通道的交换机更像一个食堂窗口只有一个三份订单要想不打架得学会分批下。4.2 三级任务调度策略是怎么定的我把任务调度拆成三层帧采样层各自规定任务多久跑一次。由于垃圾桶分类对实时性要求最低我设置为每 1 秒执行一次烟火检测每 500ms 一次人员检测每 200ms 一次。推理触发器用一个定时器或循环间隔触发对应的推理线程。三路触发时间尽量错开比如人员检测在 0ms、200ms、400ms 触发烟火检测在 100ms、600ms 触发垃圾分类在 300ms 触发。这样从宏观时间轴上几乎不会出现两个线程同时发起大模型推理。结果发布层模型推理完成后各自独立做后处理和结果上报。后处理在 CPU 上执行与 NPU 推理天然异步不会阻塞后续帧。用代码框架来理解就是import threading import time def person_loop(): while True: frame latest_frame() results rknn_person.inference(frame) handle_person_results(results) time.sleep(0.2) def fire_loop(): while True: frame latest_frame() results rknn_fire.inference(frame) handle_fire_results(results) time.sleep(0.5) threading.Thread(targetperson_loop, daemonTrue).start() threading.Thread(targetfire_loop, daemonTrue).start()这里最容易被忽略的是latest_frame()去拿最新帧的时机。如果某次 NPU 排队时间较长模型拿到的是 100ms 前的老帧这不一定是问题前提是后端业务能接受这个延迟。但如果你拿最新帧后发现上一帧还没算完建议直接丢弃旧帧而不是继续往队列里压帧因为堆积只会让画面越来越卡。4.3 为什么绑定单核不一定比自动调度快项目后期我专门做过对比测试。第一种方案是三个模型分别用 core_mask 绑定到 NPU_CORE_0、1、2第二种方案是都用NPU_CORE_AUTO统一交给驱动调度。实测结果很有意思在单人单卡的空载场景下两种方案差距很小。但把 CPU 后处理加上再叠加多路视频解码后绑定单核的方案反而偶尔出现 15% 的性能抖动因为某些核心在排队而另一些核心空闲驱动无法跨核做任务重排。所以后来我把代码改成了默认NPU_CORE_AUTO只把最重的烟火检测模型单独通过配置项允许手工指定核心作为调试开关保留。对多数项目来说自动调度加任务错峰远比手动绑定核心更可靠。4.4 CPU 端要做的事别让后处理拖了后腿NPU 推理只占整个耗时的一部分。以 YOLOv8n 640 输入为例NPU 推理大约 25ms解码和缩放大约 4ms后处理——包括解码模型输出、NMS 过滤、阈值判断——反而可能占到 15ms 以上。如果三个任务的后处理都在同一个线程里做串行处理整体延迟照样会被拖垮。我是这样安排的三个推理线程各自持有独立的后处理线程推理完成后把原始输出放入一个很小的结果队列后处理线程取走异步计算。这样即使垃圾分类模型后处理中出现小概率阻塞也不会影响烟火检测的下一帧推理。实际在 Python 里使用threading就够了但如果对性能极限有要求最好换成 C 加线程池或者直接用 RKNN 的 C API。5. 实测性能数据与调优记录5.1 单模型独立跑测出的基线数据整个系统搭建完成后我先关掉调度分别独立测了三份模型的实际性能。测试条件RK3588 开发板 8GB 内存系统 Ubuntu 20.04 桌面版关闭桌面环境以节省显存摄像头输入为 1080p 25fps 的 RTSP 流测试运行在同一路视频流之上。各模型独立运行数据如下模型算法类型输入分辨率单帧推理耗时独立帧率配置人员入侵检测YOLOv8n640×640约 26ms约 35 FPSINT8 量化烟火检测YOLOv5s640×640约 48ms约 20 FPSINT8 量化垃圾分类识别MobileNetV3-Small224×224约 5ms约 150 FPSINT8 量化注意这里“独立帧率”是极端情况下的数字实际业务根本不需要这么高。YOLOv8n 如果长期跑 35 FPS板卡功耗和发热都会明显上升散热不好的机箱里很容易触发温度降频。5.2 加入并发调度后的综合表现按照上面 4.2 的调度参数三个模型同时运行时得到的输出节奏如下任务目标频率实测平均输出间隔最大单帧延迟说明人员入侵检测5 FPS约 205ms320ms正常烟火检测2 FPS约 505ms600ms正常垃圾分类识别1 FPS约 1005ms1250ms正常整机 CPU 占用率约 40% 到 65%内存占用约 3.2GBNPU 的平均占用率在 55% 上下。之所以 CPU 占用偏高主要原因是后处理和 Python 运行时开销后来我把垃圾分类模型切换到 C 接口后CPU 降了大约 10 个百分点。从数据能明显看出一个结论即使总负载远超单个模型的算力需求只要在时间轴上做错峰排队延迟对任务自身的影响是可控的。真正要担心的不是算力不足而是某些任务在下一次采样到来时上一次推理还没结束导致任务整体周期越来越长最终形成“雪崩式延迟”。5.3 两次比较关键的调优操作第一次调优是减少无意义的输入缩放。最初团队里有人把每帧原始 1080p 图像送入模型之前先缩放成一整套统一尺寸再对不同模型各自裁剪。这个操作看起来很省事但 RGA 在高分辨率缩放上要花费比模型推理本身还高的时间。改成各自直接缩放后整体延迟下降了近 12%。第二次调优聚焦在 YOLOv5s 的 NMS 阈值。烟火检测任务里烟雾和火焰通常不会是高密度小目标但原始模型置信度偏低把置信度阈值从 0.45 降到 0.25 后召回明显上升。不过副作用是误报增多于是我又额外加了一条后处理规则同一检测框连续 3 帧都命中才认为是一次有效烟火事件。这套“低阈值加多帧确认”的组合对动态场景非常有效。5.4 如何判断 NPU 是否真的到瓶颈了很多人在调优时习惯盯着htop或者/proc/loadavg看 CPU 负载但 NPU 的使用率在标准 Linux 工具里是看不全的RKNN 在运行日志里会输出每一次推理耗时。更直接的办法是开一个长期统计脚本每隔 1 分钟统计一次最近 N 次推理的平均耗时。如果平均耗时相对单测数据突然翻倍说明模型在排队或者系统降频了。还有一个容易混淆的点画面抖动的源头往往是内存带宽不够而不是 NPU 不够。三个模型并发运行时每帧输入图像要从 DDR 搬到 NPU模型参数和中间特征图也在 DDR如果内存只有 4GB 或系统还开了图形桌面内存交换很容易拖慢一切。项目里使用的 8GB 内存版本跑满三模型后还剩了近半内存基本不用为内存焦虑。6. 常见问题与排查技巧实录6.1 模型初始化和加载阶段就报错常见报错之一是E rockchip: failed to load rknn model或者“找不到 librknnmrt.so”。前者多半是 RKNN 模型文件损坏或版本不匹配重新导出一次即可后者通常是因为板端没有设置动态库路径。解决方式是确认是否用sudo ldconfig刷新过库目录或者直接在启动脚本中写上export LD_LIBRARY_PATH/usr/local/lib:$LD_LIBRARY_PATH还有一种情况是开发板系统刚刷完固件NPU 驱动版本和模型转换工具版本不一致。RKNN 的模型文件向下兼容做得一般最好让板端 NPU 驱动版本和 PC 端 RKNN-Toolkit2 版本保持同一大版本跨了大版本很容易出现算子不支持或初始化失败。6.2 INT8 量化后烟火检测精度下降明显烟火检测模型独立跑 FP16 时表现不错一量化成 INT8 后烟雾漏报率明显上升。排查下来发现最根本的原因是校准集里烟雾样本太少而且分布不均匀大部分样本是晴空背景下的远景烟雾缺少中景、近景和强逆光数据。解决办法是重新整理校准集从训练集里分层抽样确保近景、远景、室内、室外、夜晚各占一定比例。如果这样还不行可以退一步为烟火模型单独设置混合精度量化把模型最后的输出层保留为 FP16。在 RKNN-Toolkit2 中使用混合精度功能时要手动指定敏感层的精度操作成本并不高能换来约 3% 的准确率提升。6.3 多任务并发后某些模型推理时间突然翻倍这种问题排查优先级我先看内存。用free -h查看可用内存如果剩余很少先处理内存占用问题。再看任务触发时间是否发生了“同频共振”比如原来固定 200ms 的任务和固定 400ms 的任务刚好是倍数关系运行一段时间后两个任务会周期性撞在一起。解决办法是在触发时间中加入随机抖动time.sleep(period * (0.9 random.random() * 0.2))设置合理范围内的随机抖动可以显著降低多路任务在同一时刻发起推理的概率。这是一种很实际但少有人写的技巧尤其在多路摄像头和多模型同时运行的项目里格外好用。6.4 摄像头出现码流延迟或花屏摄像头侧的问题用系统后处理很难完全规避。我遇到过几次板端网络被其他服务占满RTSP 拉流出现丢包导致画面花屏。可以先单独测一下板子到摄像头的延迟用 VLC 直接拉流看缓冲时间如果延迟超过 300ms说明链路本身有问题。尽量不要在系统里使用过多占用网卡的进程也不要给每路摄像头单独设置过大的接收缓冲加大缓冲反而会放大延迟。6.5 垃圾分类长期运行后模型结果漂移分类模型的输入是固定画面摄像头长时间角度变化或者脏污遮挡会让识别率下降。业务方一开始不接受“每天定时清洗摄像头”的运维方案所以在代码里加了一个视觉质量检测模块判断当前帧的平均亮度、清晰度和画面偏移如果连续多帧模糊或过曝就报警而不是跑模型硬撑。这个模块用一个轻量级 OpenCV 函数就能完成CPU 开销很小。7. 一些额外的体会如果只让我挑一句话给准备做同类项目的人我会说不要按“一个模型独占一台设备”的思路去想问题边缘设备上了 NPU 以后真正该设计的是任务调度和数据流。好几个同行在聊 RK3588 时特别喜欢盯着算力数字看算完就觉得 6 TOPS 不够用。其实目标检测、烟火识别、垃圾分类这些任务在实际业务中几乎不需要每秒处理 25 帧对安防场景来说5 FPS 甚至 2 FPS 已经是可用的水平。单块 RK3588 的并发能力被很多人低估了但前提是你得把每个模型的触发频率、优先级、等待队列和 CPU 后处理都规划好。另一个比较深的体会是板端 AI 项目超过一半的时间都花在调试工具链和排队问题上真正调模型结构的时间反而少。RKNN 工具链每年都在变网上很多旧教程里的接口名已经失效遇到问题最好的习惯是先看板端 SDK 目录下的官方示例再把模型逐步缩小范围去定位算子兼容性问题。最后再分享一个操作层面的小技巧在 RK3588 板子上调试多模型时尽量在后台用dmesg盯着 NPU 驱动日志遇到 NPU 异常挂起可以直接看到是哪个模型触发的。不要盲目重启有日志的排查效率会高得多。这套方案最终在项目现场跑了两周多中途还顺手加了第 4 个模型整个过程因为底座设计得够灵活新增任务只花了一个下午就联调完毕。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价