RK3588多任务边缘AI实践:单NPU跑三模型的调度与优化全解析

发布时间:2026/9/6 10:38:42
RK3588多任务边缘AI实践:单NPU跑三模型的调度与优化全解析 客户把需求拍在桌上那一刻我第一反应是“这块板子怕是扛不住”单块RK35886 TOPS的NPU要同时跑人员入侵、烟火检测、垃圾分类。先不说模型精度光是在一块板卡上把三个视觉任务长期稳定跑起来就够折腾一阵。但真把算力账算清、把调度逻辑理顺之后结论反而乐观不但能跑还能在8GB内存版本上留出足够余量继续接一路甚至两路摄像头。这篇文章把这套从模型选型、RKNN转换到多模型并发的完整过程写出来给准备在RK3588上做多任务边缘AI的朋友做个参考。1. 先算账6 TOPS的NPU到底能同时喂饱几个模型1.1 RK3588的NPU算力到底是什么规格先说一个最容易误解的地方RK3588标称的6 TOPS指的是INT8量化下的峰值算力不是你想怎么用都能拿到的通用算力。实际项目中推理框架、内存带宽、数据搬运、解码单元都会分走性能实测下来能把峰值的一半稳定用出来已经算优化得不错。所以我个人习惯把这6 TOPS当成3 TOPS的有效算力来做方案规划这样后面算帧率预算时不容易翻车。RK3588的NPU是三个核心支持INT4、INT8、INT16精度频率最高能到1GHz。单看数据确实亮眼但它不是独立显卡那样的大规模并行单元而是需要把输入数据从DDR搬进NPU内部缓存算完再搬回来。所以内存带宽和缓存命中率对实际吞吐的影响往往比峰值算力更大。这也是为什么有些人拿到板子跑同一个模型帧率却比论坛上别人测的低一截——DDR频率配置、散热条件、同时解码几路视频都会直接影响NPU表现。1.2 三个任务每次推理要花掉多少毫秒回到三个任务本身。模型选型不同单帧消耗差好几倍。我最终确定下来的方案前提是做了“目标场景拆分”不让一个模型去包打天下人员入侵用YOLOv8n输入缩到416x416单次NPU推理大约16ms烟火检测烟火目标普遍偏小早期火苗可能只有几十个像素我用YOLOv5s输入保持640x640单次约45ms垃圾分类因为摄像头固定在某个垃圾桶位用MobileNetV3-small做纯分类输入160x160单次3到5ms。这个过程里我做了大量对比测试同样的模型在板子上跑出来的时间会因为量化方式、输入尺寸、NPU负载产生挺大波动。上面这些数是“低负载、1GHz NPU、良好散热”下的参考值。如果环境温度高导致降频后面会专门讲。1.3 算清楚帧率预算为什么不用每帧都跑全部任务直接算一笔账如果25fps视频每一帧都跑这三个模型每秒需要25×(16454)1625ms远超NPU一秒1000ms的处理能力这条路从数学上就堵死了。但视觉项目没人规定每个任务都要跑满帧率人员入侵不需要每帧都产出告警烟火从出现到蔓延是相对缓慢的过程垃圾更不用24小时持续扫。我把策略调成人员入侵每3帧跑一次大约8fps单路视频每秒NPU占用约128ms烟火检测每6帧跑一次大约4fps单路视频每秒NPU占用约180ms垃圾分类事件触发只有检测到有人靠近垃圾桶并离开后才采样一次每秒最多占几毫秒。这样单路视频一整秒的NPU占用大概300ms左右整体负载只有三成。如果你只做一两路摄像头那绰绰有余到三四路的时候才需要考虑真正意义上的错峰调度。算完这笔账我心里就踏实了。剩下的问题不再是“能不能跑”而是“怎么把这三件事在同一个进程里安排得明明白白”。2. 三个任务分开选模型目标差异决定了路线差异2.1 入侵检测的本质大目标、高召回、人与规则结合人员入侵的核心不是“识别人”而是“识别到人之后判断这个人做了什么”。所以模型部分只需要把人体框稳定地标出来真正的报警逻辑放在规则层是否进入ROI区域、是否越界、是否逗留超时。这样模型任务被大幅简化召回率反而更容易保证。我用YOLOv8n并在自己的数据上重新训练过。数据集主要覆盖三种场景白天强光、夜间红外、人员部分被遮挡的情况。道路监控和工地场景差别很大最好用目标现场的素材做fine-tune。YOLOv8n在416输入下对中远距离人体有不错的表现但如果是想检测很小的目标就需要切成512或640输入推理耗时会上浮项目里要对这个平衡有预期。入侵检测最容易翻车的点不是模型漏检而是误报。树枝晃动、光影变化、动物经过都可能产生人体框。规则层一定要加“连续N帧检出且框位移小于阈值”的确认机制或者用轻量跟踪把同一目标的轨迹串起来再做跨线判断。否则甲方第二天就会给你打电话说“你们系统被一只猫触发了一晚上报警”。2.2 烟火检测的本质小目标、半透明、昼夜差异巨大烟火检测是这三个任务里最憋屈的因为“烟”和“火”的目标属性差异非常大。火焰有自己的纹理和颜色分布算是有形状的目标高温烟气则半透明、无固定纹理、边缘模糊跟云、水汽、汽车尾气经常长得差不多。我在烟火模型上最终选YOLOv5s保持640输入因为目标尺度小下采样太大容易直接丢掉。数据方面白天场景要收集阳光反射、红色灯光、晚霞等强负样本夜晚场景要收集路灯、车灯、LED屏幕等干扰源。有条件的话做白天和夜晚两个模型或者加昼夜切换逻辑因为同一个模型在两种光照下很难同时做好。烟火判定我只信多帧确认连续3帧都有检出并且中间停检不能超过5帧才触发报警。单帧闪烁的火花不值得报但连续帧出现的火焰纹理必须及时推给值班人员。2.3 垃圾分类的本质固定ROI加轻量分类别硬上检测垃圾分类这个任务刚拿到需求时很多人第一反应是用YOLO检测垃圾桶里的瓶子、纸箱、易拉罐。但在真实部署里垃圾桶位置固定镜头角度固定还经常遇到反光、遮挡、光线变化。直接做检测会引入大量背景干扰模型还得学“什么是垃圾”和“垃圾在哪儿”算力浪费严重效果还未必好。我把它拆成两步第一步在画面里固定一个ROI框框住垃圾桶口区域第二步由调度器触发把ROI裁剪后送进MobileNetV3-small做四分类可回收、厨余、有害、其他。这个方案对摄像头安装位置有要求但绝大多数垃圾房、垃圾亭的监控角度都满足条件。MobileNetV3在160x160输入下板端推理只要几毫秒模型文件也小得多这让整个系统的资源预算一下子轻松了不少。从结果看固定ROI加分类比端到端检测模型更稳也更省钱。这个思路其实可以复制到很多类似的场景只要相机不动就别让模型去学“定位”把定位交给几何规则模型只负责“识别”。3. ONNX转RKNN模型转换里的深坑与量化校准3.1 环境与版本工具链和板端驱动必须一一对应模型训练好了下一步就是转成RKNN格式。这个阶段我踩过最大的坑不是模型本身而是工具版本错配。rknn-toolkit2是跑在PC上的转换工具板端则通过librknnrt.so加载RKNN模型。两者版本必须匹配否则会出现rknn_init失败、甚至初始化成功但推理结果全是乱框的诡异问题。建议先确认板子固件里SDK的版本号再去下载对应版本的rknn-toolkit2。官方一般会提供版本对应表。我自己习惯在x86的Docker环境里跑转换Python版本用3.8或3.10避免本机环境把依赖搞乱。还有个老生常谈的点转换前先对ONNX跑一遍onnxsim很多莫名其妙的op不支持问题都能在简化之后消失。3.2 从PyTorch导出ONNX并完成RKNN转换转换脚本本身不难难的是你知不知道每行参数在干什么。我的基础流程是这样from rknn.api import RKNN rknn RKNN(verboseTrue) # mean/std 配 0 和 255含义是把输入归一化到 0~1 # 这样模型内部就不再额外做减均值除方差量化精度通常会更好看 rknn.config( target_platformrk3588, mean_values[[0, 0, 0]], std_values[[255, 255, 255]] ) rknn.load_onnx(modelyolov8n_sim.onnx) rknn.build(do_quantizationTrue, datasetcalibration_dataset.txt) rknn.export_rknn(yolov8n.rknn)导出时有个容易被忽略的点如果PyTorch模型包含动态shape或者带有大量自定义前处理逻辑导出ONNX后要先固定shape再把前处理剥离出去。RK3588的NPU喜欢静态shape动态shape会让内存规划非常难受性能也会打折。3.3 量化校准集到底该放什么图do_quantizationTrue的时候校准集直接决定模型精度。校准集不是越多越好而是覆盖度越高越好。我给三个模型分别准备了校准集人员入侵白天、夜晚、逆光、远距离、部分遮挡各取一些总共大概300张烟火检测近火远火、灶台炊烟、路边摊烟雾、晚霞、车灯300到500张垃圾分类不同时段、不同光线下的垃圾桶ROI截图150张左右就够因为分类网络本身简单。校准集图片推荐直接从现场视频里抽帧而不是用训练集的典型样本因为现场的实际数据分布更接近部署环境。抽帧时用一个脚本每隔几秒存一帧覆盖从早到晚的完整光线变化校准出来的量化精度会比你随便找的图片好一大截。3.4 转换中的常见报错和量化掉点修复转换阶段我遇到过三类问题一是“op not support”或者“op version mismatch”。这种优先用onnxsim简化再不行就查一下该op对应的RKNN版本是否支持有时升级工具链版本就能解决。二是量化后精度明显掉。比如mAP掉了5个点以上首先怀疑校准集分布和真实场景差距大重新准备校准集其次可以打开混合量化把某些敏感层单独指定成fp16虽然推理时间会涨一点但精度损失能控制在1%以内。三是转换后模拟器能用、上板不能用。这类问题基本都是板端librknnrt版本和PC端工具链不匹配直接替换板端运行库或升级固件别在代码层面硬调。4. 多模型调度器常驻权重、优先级队列、事件触发4.1 为什么不推荐“三个进程各跑一个模型”很多人拿到这种需求第一反应是写三个Python进程或者干脆开三个Docker容器一个跑入侵一个跑烟火一个跑垃圾分类。这套方案最省事但不是最优解。首先RK3588的NPU只有一个多个进程同时提交rknn_run时驱动要在不同上下文之间做任务切换不仅排队时间不确定还可能出现其中一个进程把NPUHAL锁占满另一个模型长时间得不到调度的情况。其次三个进程各自解码、各自预处理同一路视频会被重复处理三次CPU和DDR带宽白白浪费。最后是运维层面三个进程崩溃任何一个都很难监控日志还分散。我的做法是反过来一个常驻进程内部管理三份模型权重用同一个调度器统一分配NPU时间。模型上下文在启动时全部rknn_init好运行期间绝不销毁重建避免模型加载带来的秒级卡顿。4.2 调度器的结构与代码骨架调度器说到底就是三件事优先级、帧间隔、事件触发。我用Python写过一版验证逻辑写得很直白class MultiTaskScheduler: def __init__(self): self.person RKNNModel(yolov8n.rknn) self.fire RKNNModel(yolov5s_fire.rknn) self.trash RKNNModel(mobilenetv3_trash.rknn) self.frame_cnt 0 def on_frame(self, frame, timestamp): self.frame_cnt 1 # 优先级1人员入侵每3帧一次 if self.frame_cnt % 3 0: self.run_person(frame) # 优先级2烟火检测每6帧一次 if self.frame_cnt % 6 0: self.run_fire(frame) # 优先级3事件触发人员离开垃圾ROI后采样 if self.trash_trigger.is_ready(timestamp): crop self.roi.extract(frame) self.run_trash(crop) def run_person(self, frame): img rga_resize(frame, 416) boxes self.person.infer(img) self.rule_engine.on_person_boxes(boxes)这里的关键是所有推理共享同一个帧循环串行提交给NPU。我不用多线程去并发跑rknn_run因为串行提交反而能减少任务切换开销单次推理时间更稳定。实际测试下来三个模型轮流跑就算某一次两个任务挤在同一帧整体延迟也只增加几个毫秒完全在可接受范围。4.3 用规则引擎把三个任务串起来调度器只解决“什么时候跑哪个模型”真正让系统智能起来的是模型之上那层规则引擎。人员入侵模型出框后规则引擎判断框是否进入ROI、是否越线、是否逗留超过15秒。烟火模型出结果后多帧确认器负责去抖。垃圾分类则直接复用入侵模型的结果当作“触发器”当人员框进入垃圾桶ROI区域、随后离开超过2秒调度器才去采集垃圾图像并跑分类模型。这样垃圾模型一天可能只被触发几百次而不是7x24小时空转。这个串行触发逻辑让三个任务不是简单“同时跑”而是按业务因果关系协作算力效率一下子上来了。之前担心的“NPU负载不够”问题在这种设计下基本消失。5. 别让预处理和后处理拖后腿硬解码、RGA与低成本后处理5.1 解码环节RKMPP硬解基本链路调度器设计好了还得保证图像送到NPU之前别让CPU累死。如果视频流是RTSP或者本地H264文件不要用OpenCV的VideoCapture去软解。RK3588自带的RKMPP硬解模块就是干这个的1080p视频解码大概只要几毫秒CPU占用几乎可以忽略。我用的是MPP解码拿到的帧是NV12格式的dma_buf。这里有个关键习惯不要把解码后的NV12转成JPEG或者BMP再去做后续处理直接把这个buffer传给RGA做缩放和格式转换。每多一次拷贝都是在浪费内存带宽和CPU缓存。5.2 缩放与颜色转换RGA替代OpenCV模型需要的输入是RGB而且尺寸从416到640不等。如果每帧都先用OpenCV做resize再做cvtColorA76核心会被大量消耗尤其在多任务并行的场景下CPU很可能比NPU先成为瓶颈。RGA是Rockchip的硬件2D图像处理单元一次调用可以同时完成缩放、颜色空间转换、裁剪。我之前测试过同样的720p转416x416 RGBOpenCV大概要6到8毫秒RGA基本在2毫秒以内。在调度器里我维护了两份预处理缓存一份416的RGB给入侵一份640的RGB给烟火ROI裁剪到160给垃圾分类。同一帧只需要做两次RGA缩放而不是每个模型各做一次。5.3 后处理与线程池别让CPU成为新瓶颈NPU跑得快但模型输出的原始张量还得做decode、NMS、阈值过滤。如果这部分用Python循环去写CPU占用会瞬间飙到300%以上。我最后把三个模型的后处理全部下沉到C实现并放到一个4线程的线程池里跑。YOLOv8n的输出decode加NMS一次大约2到3毫秒YOLOv5s大约3到4毫秒MobileNet分类就更快了完全可以忽略。这里还有个容易被忽略的细节RKNN在推理时输出内存如果复用不当每次rknn_outputs_get都可能发生拷贝。我通常直接用RKNN的零拷贝接口把输出buffer固定住后处理线程只读取数据不额外分配大内存。长期跑下来内存碎片也少很多。5.4 端到端延迟和整板负载实测在RK3588、8GB内存、1GHz NPU、良好散热的条件下单路1080p视频源做三任务部署端到端数据大概是阶段耗时RKMPP硬解1080p H2643 ~ 5msRGA缩放颜色转换到416/6402 ~ 3ms入侵模型NPU推理16 ~ 20ms烟火模型NPU推理40 ~ 50ms后处理decodeNMS3 ~ 4ms规则引擎判断小于1ms也就是说从一帧到达解码器到入侵告警或烟火告警输出端到端大约30到35毫秒。整机CPU占用大概在30%到40%NPU平均负载30%左右内存常驻约1.2GB。在这个状态下再接一路视频压力也不大。6. 一次温度墙引起的性能骤降完整排查链路复盘6.1 线上症状跑了四十分钟后报警变迟钝东西部署到现场后客户反馈说系统刚开始一切正常跑了大概四十分钟入侵报警的响应越来越慢。一开始我也怀疑是网络问题但本地看画面延迟并不高就是模型出框的时间变长了。我们在代码里给每次rknn_run打了耗时日志发现同一个推理任务冷启动时只要16ms跑一段时间后涨到了25ms、28ms甚至接近40ms。这不是模型变笨了是硬件在“自我保护”。RK3588的NPU、CPU、DDR共享散热模组温度一高SoC会自动降频。尤其NPU长时间满负载跑温度墙很快就到。6.2 逐步排查从进程状态一路查到温度墙排查链路是这样的第一步先排除内存泄漏。在板子上连续跑free -m发现内存占用稳定排除持续缓存增长的问题。第二步看CPU占用。用top看有没有失控的进程结果CPU占用正常没有哪个进程在空转吃核。第三步打npu推理时间戳。发现涨的是NPU耗时而不是预处理或后处理问题锁定在NPU侧。第四步查温度。读取/sys/class/thermal/thermal_zone*/temp发现已经到80度以上。再查NPU当前频率通过debugfs看到频率从1GHz掉到666MHz左右问题实锤了。温度墙导致频率下降推理耗时自然上涨。这也是很多边缘设备项目都会遇到的坑开发时环境开着空调板子裸奔测试一切正常到了夏天阳光直射的机柜里没有风扇性能直接砍半。6.3 修复方案散热、降载、保优先级解决分了三个层次第一层是治本加主动散热。把PWM风扇接上通过/sys/devices/platform/pwm-fan/hwmon/hwmon0/fan_target设置目标温度夏天转速调到70%以上。实测加风扇后温度稳在55度左右NPU保持1GHz不再掉频。第二层是治标软件降载。如果现场结构限制装不了风扇就把烟火检测降到每8帧跑一次入侵检测降到每4帧跑一次以减少NPU持续负载。效果是响应稍微变慢但至少不会性能雪崩。第三层是保重点。调度器里加一个温度状态判断当SoC温度超过阈值时主动丢弃低优先级的垃圾分类任务保证入侵和烟火始终处于最高响应级别。这套机制上线后再也没接到客户“报警变慢”的投诉。6.4 同类坑的预判版本不匹配和NPU任务堆积跑多模型项目还容易碰到的两个坑我顺便列在这。一个是版本不匹配。有次把新模型带过去rknn_init直接报错查了半小时发现是板端librknnrt.so版本太老。建议每次在板上部署之前先确认librknnrt.so版本和PC端rknn-toolkit2版本一致。另一个是任务堆积。调度器的队列如果消费速度小于生产速度会把无效帧一直留在队列里导致“越跑越慢”的假象。我在每个队列里加了最大长度限制队列满了直接丢最老的帧保证当前帧永远是最新的画面。所以如果你打算在RK3588上同时跑多个NPU任务我的建议是把散热方案和调度策略写进第一版设计文档而不是等项目上线了再补。我这次就是被现场温度墙逼着从头补了这一课过程相当酸爽。先把这些底子打好后面哪怕增加一路摄像头、多跑一个模型都不会慌。

相关新闻