尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RK3588边缘AI盒子实战:多模型并发调度与部署经验

RK3588边缘AI盒子实战:多模型并发调度与部署经验 做边缘AI盒子这几年RK3588这颗SoC在安防和工业视觉领域确实火得不行。6 TOPS的NPU算力听起来不大但放在单板设备上已经能撑起不少实时视觉任务。很多人问我单块RK3588到底能不能同时跑人员入侵检测、烟火识别、垃圾分类这种“三合一”需求我的答案是能但前提是你得把模型选型、NPU调度、多路取流的细节吃透。这篇文章就把我实际调通的这套方案完整拆开讲从算力分配到线程模型再到踩坑记录都是可以直接抄作业的经验。先说清楚这套系统是干什么的一路或多路RTSP摄像头画面进来先在边缘端完成人员入侵检测人形框越界/逗留规则、烟火检测火焰和烟雾区域框、以及垃圾分类对特定ROI区域内的垃圾类别做识别。所有推理都在RK3588的NPU上完成不依赖云服务单板独立运行。适合做智慧园区、工地安防、垃圾站监控这类场景也适合正在评估RK3588作为边缘AI主控的开发者参考。1. 整体设计别一上来就搞三个模型并发1.1 算力预算是先决条件很多人第一反应是“我有三个任务那就加载三个模型分别跑三个线程”这个思路在RK3588上大概率会翻车。原因很简单NPU的6 TOPS是INT8算力而且是所有核心共享的模型太多、并行度太高反而会因为上下文切换和内存带宽争抢导致实际吞吐下降。我实测过同时加载3个大模型并随意并发NPU占用率忽高忽低单路推理延迟能从30ms飙到80ms以上。所以第一个决策是把三个任务合并成“两个模型”来跑。具体合并方式是这样的人员入侵和烟火检测都是“目标检测”任务它们的输出格式一致——目标类别、置信度、检测框。那干脆把它们合并进同一个检测模型类别集合设计成 person、fire、smoke 三类如果还想要更细可以把 fire 拆成 flame 和 smoke按现场需要定。这样一次前向推理就能同时拿到人员框和烟火框省掉一个完整模型的算力开销。垃圾分类不同它是纯图像分类任务输入的是某个ROI区域的裁剪图输出的是垃圾类别所以保留一个轻量分类模型就够了。为什么垃圾分类不合并进检测模型因为分类和检测的任务粒度完全不同检测模型在640x640下分类太多会牺牲小目标召回率训练数据也更难做。而且垃圾分类通常只需要对特定区域比如垃圾桶画面区域、传送带区域的ROI做识别用一个224x224的轻量分类网络去处理精度更高、算力消耗更低逻辑也更清晰。1.2 模型选型精度、速度、转换友好度三者的平衡模型选型这块我试过一版比较激进的方案——YOLOv8n做检测、MobileNetV3-Small做分类后来又换过YOLOv5s和EfficientNet-Lite0最终稳定下来的组合是任务模型输入尺寸量化精度实测单次推理耗时人员入侵烟火检测YOLOv8n改类别头640x640INT825~38ms垃圾分类MobileNetV3-Small224x224INT82~5ms选YOLOv8n的原因有三个第一是nano版参数量小ONNX导出成熟rknn-toolkit2转换零报错第二是检测精度在人员、火焰这些中大型目标上完全够用小目标烟雾也能靠训练数据弥补第三是RK3588对YOLO系的支持最完善后处理代码网上也多坑最少。MobileNetV3-Small更是因为轻量才选它。垃圾分类是个低频任务不需要每帧都跑我在实际系统里是检测到ROI区域有“疑似垃圾”的移动或堆叠才触发分类所以224x224输入下2~5ms的延迟完全可以忽略。1.3 多模型并发的调度策略两个模型定了接下来就是怎么调度。我采用的架构是一个检测线程 一个分类线程检测线程绑定NPU核心0/1分类线程绑定NPU核心2。这样做的目的很直接让检测任务独占大部分算力分类任务只在NPU有空闲窗口时插入执行避免两个模型频繁抢占核心导致的两败俱伤。RKNN Runtime是支持NPU核心绑定的Python接口里用core_mask参数C接口里用rknn_init的core_mask参数。具体代码后面第三部分会给出。2. NPU硬件特性与多模型并发的底层逻辑2.1 RK3588 NPU到底是怎么工作的RK3588的NPU是瑞芯微自研的第三代NPU整颗SoC集成了三个NPU核心总算力6 TOPS。这里说的6 TOPS单位是INT8 OPS就是每秒6万亿次整数运算。它的计算阵列是类似“主网格阵列”的组织方式数据在MAC阵列里流水线式执行卷积和矩阵运算量化后的int8数据直接喂给MAC阵列计算效率远高于用CPU或GPU跑。真正要注意的是这个NPU不是“独立显卡”它和CPU、GPU、VPU共用同一片内存。也就是说NPU算力只是上限能不能跑满还取决于DDR带宽、数据搬运开销、模型并行度这些因素。我见过太多人在PC上测模型飞快部署到RK3588就慢了大多跌在这上面。2.2 多模型并发时的内存和带宽瓶颈RK3588通常搭配LPDDR4/4X四通道设计理论带宽五十多个GB/s。听着很高但多路视频解码、图像缩放、NPU输入搬运、前后处理都要抢这份带宽。我实测当一路1080p RTSP硬解码 YOLOv8n推理 分类模型推理同时跑时DDR带宽占用能到30%~40%如果再叠加两路取流带宽瓶颈会先于NPU算力出现。这也是为什么我在方案设计里强调多路摄像头接入时优先把视频流降到720p或960p再送推理而不是在1080p上硬扛。NV12转RGB、resize到640x640这些操作放在CPU上做每一步都在烧带宽。用RK MPI或者FFmpeg rkmpp硬解码可以缓解但最终喂给NPU的数据量必须控制在合理范围。具体来说我建议单路检测输入保持在640x640分类输入224x224这是性能和数据精度权衡后的甜点值。2.3 RKNN Runtime的并发特性RKNN Runtime本身支持在同一进程里初始化多个上下文每个上下文可以加载不同的模型分配到不同的NPU核心上。这个机制就是多模型并发的基石。但有个大坑如果你用同一个RKNN上下文依次调用两个模型的inference它是串行执行的第一个跑完才跑第二个这样根本发挥不了并发优势。正确做法是分开两个RKNNLite实例Python接口或者两个rknn_contextC接口各自绑定核心然后放到不同线程里。3. 实操过程从模型转换到双线程调度完整落地3.1 环境准备和模型转换我用的rknn-toolkit2版本是1.6.0对应的板端rknpu驱动是0.9.6。转换模型之前先确认三件事模型是ONNX格式且输入输出节点明确动态维度已固定或转为静态数据集文件dataset.txt里至少放8~10张有代表性的图片用于量化校准量化方式是asymmetric_quantized-8这是RK3588上INT8推理的默认方案转换YOLOv8n的Python脚本核心部分如下from rknn.api import RKNN rknn RKNN(verboseTrue) # 配置目标平台、量化方式、均值方差 rknn.config( target_platformrk3588, mean_values[[0, 0, 0]], std_values[[255, 255, 255]], quantized_dtypeasymmetric_quantized-8, quantized_algorithmnormal, ) # 加载onnx模型 rknn.load_onnx(modelyolov8n_3class.onnx) # 模型构建和量化 rknn.build(do_quantizationTrue, datasetdataset.txt) # 导出rknn模型 rknn.export_rknn(yolov8n_3class.rknn)转换过程中最容易出问题的是std_values写错。很多人习惯沿用训练时归一化参数比如mean0、std255这个没问题但如果你训练时用的是mean[0.485,0.456,0.406]、std[0.229,0.224,0.225]这种ImageNet归一化转RKNN时也照样把这个写进去板端推理时输入数据就不再需要额外归一化了。我见过有人两边各做一次归一化导致精度掉得离谱。分类模型MobileNetV3-Small的转换流程完全一样唯一的区别是输入尺寸和dataset.txt里的校准图要换成垃圾分类场景的图。3.2 板端推理代码双线程 核心绑定板端我用了Python的rknnlite.api接口虽然不是极致性能但胜在开发效率高。核心代码如下import threading import cv2 import numpy as np from rknnlite.api import RKNNLite # 初始化两个独立的RKNNLite实例 det_rknn RKNNLite() det_rknn.load_rknn(yolov8n_3class.rknn) # 绑定NPU核心0和1检测模型独占大部分算力 det_rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0_1) cls_rknn RKNNLite() cls_rknn.load_rknn(mobilenetv3_garbage.rknn) # 分类模型绑定NPU核心2与检测并发 cls_rknn.init_runtime(core_maskRKNNLite.NPU_CORE_2) def detect_worker(): 检测线程人员入侵 烟火检测 while True: frame get_frame_from_rtsp() # 从解码队列取一帧 # 数据预处理BGR-RGB, resize到640x640, 转为NHWC uint8 input_data preprocess(frame, (640, 640)) # 推理 outputs det_rknn.inference(inputs[input_data]) dets postprocess_yolov8n(outputs[0]) # 解码出boxes, scores, classes # 规则引擎人员越界判断、烟火区域报警 rule_engine(dets) def classify_worker(): 分类线程对ROI区域做垃圾分类 while True: roi get_roi_from_queue() # 从检测线程的结果队列拿ROI裁剪图 if roi is None: threading.Event().wait(0.05) # 避免空转 continue input_data preprocess(roi, (224, 224)) outputs cls_rknn.inference(inputs[input_data]) cls_id int(np.argmax(outputs[0])) # 更新垃圾分类结果 update_garbage_label(cls_id) t1 threading.Thread(targetdetect_worker, daemonTrue) t2 threading.Thread(targetclassify_worker, daemonTrue) t1.start() t2.start()这里面有几个关键点值得展开第一NPU_CORE_0_1和NPU_CORE_2的划分不是随便定的。RK3588的NPU三个核心物理上独立绑定后能减少运行时调度开销。实测这个分配方式比默认的NPU_CORE_AUTO稳定尤其在高负载下不会出现两个模型抢核心的情况。第二rknnlite的inference接口默认是同步阻塞的。也就是说检测线程在推理时分类线程的推理是可以同时进行的因为它们是两个独立的RKNNLite实例、两个线程。这个并发模型非常简单不用去搞异步回调。第三线程通信我用了一个queue.Queue检测线程把检出的ROI比如垃圾桶区域的裁剪图塞进队列分类线程从队列取图。队列长度限制在2~3帧超过就丢最旧的避免处理不过来时内存暴涨。3.3 多路RTSP取流用硬解码解掉带宽焦虑实际项目中很少只接一路摄像头。我的方案是4路RTSP接入其中2路做人员入侵2路做烟火和垃圾分类。多路取流如果全用CPU软解四路1080p就能吃掉大部分CPUNPU再快整体也没救。我用的方案是FFmpeg rkmpp硬解码。rkmpp是Rockchip Media Process Platform支持H.264/H.265硬解。在FFmpeg里通过-c:v h264_rkmpp指定解码器可以极大降低CPU占用。ffmpeg -rtsp_transport tcp -i rtsp://admin:password192.168.1.64:554/stream1 \ -c:v h264_rkmpp -f rawvideo -pix_fmt nv12 pipe:1这条路出来的NV12数据直接送GPU或CPU做缩放转RGB再喂给NPU。我自己没有用RK MPI的C接口做零拷贝因为Python环境下做零拷贝链路太折腾收益也不明显。NV12转RGB用OpenCV的cvtColor即可实测对CPU占用影响可以接受。3.4 规则引擎别让检测结果漏报模型输出的是“有人”“有火”“有烟”这类目标框但真正报警判定需要一层规则引擎。我踩过的坑是直接拿框就报警导致树影晃动、车辆灯光、工地焊花全都在误报。我的规则引擎逻辑分三层人员入侵检测到person框后用目标框的底边中心点坐标判断是否越过预设警戒线连续3帧判定为入侵才报警滤除瞬时误检。烟火检测fire类别的置信度高于0.5且框面积大于阈值才报警smoke类别用“连续5帧内出现至少3次”的投票机制来降误报。垃圾分类只在垃圾桶区域或传送带ROI内有检测框时触发分类分类置信度低于0.6则标记为“待人工确认”。这层逻辑不复杂但在实际部署中价值极大直接决定了系统是“能用”还是“没法用”。4. 常见问题与排查技巧实录4.1 NPU占用率上不去推理延迟忽高忽低这是我的高频问题。查下来主要有三类原因第一是模型跑到了CPU后端而不是NPU。rknnlite的init_runtime如果不指定core_mask有些版本默认是NPU_CORE_AUTO理论上会自动分配但我确实遇到过它自动分配到CPU的情况。排查方法很简单在init_runtime后打印rknn.get_sdk_version()和rknn.list_support()再看推理时间——如果单次推理大于500ms基本就是跑到CPU上了。解决办法是强制指定core_maskRKNNLite.NPU_CORE_0_1或NPU_CORE_AUTO。第二是DDR带宽争抢。多路视频流解码和NPU推理同时进行时如果是720p以上分辨率带宽压力会传导到推理延迟上。我的优化办法是把取流分辨率限制在960p推理输入640x640不变实测延迟从50ms降到33ms。第三是模型输入尺寸设置过大。RK3588的NPU算力有限输入分辨率每翻一倍算力需求翻四倍。检测模型640x640、分类224x224这两个尺寸是我实测后保留的再往上加就得不偿失。4.2 模型量化后精度掉得离谱怎么办量化掉点是INT8部署的老大难。YOLOv8n原本FP32精度mAP可能有0.5左右量成INT8掉到0.3甚至更低这种情况我遇到过。排查思路有三个检查量化校准数据集是否覆盖足够多的场景。如果校准图全是白天场景夜间红外画面量化后精度就会崩。校准图至少包含白天、夜晚、逆光、阴暗各若干张。尝试不同的量化算法。rknn.config里有quantized_algorithm参数支持normal和mmse后者是通过最小化均方误差来确定量化参数通常精度更好但转换更慢。我用mmse在烟火检测上挽回了不少掉点。如果还是不行考虑混合量化——某些敏感的层保留FP16。rknn-toolkit2支持custom_quantize配置但这个操作比较进阶建议先尝试前两种方案。4.3 长时间运行内存泄漏板端设备要7x24小时跑内存泄漏是致命问题。我用的是Python rknnlite早期版本在反复inference调用时存在Tensor内存累积问题跑一天内存占用能从200MB涨到2GB。解决办法有三个层面升级rknn-toolkit2到1.6.0以上板端rknn_server和librknnmrt.so同步更新到匹配版本这个问题基本消失。代码层面不要每次inference都重新创建input_data的numpy数组尽量复用固定大小的buffer减少内存碎片。加一个监控线程定时用psutil或读/proc/meminfo检查内存占用超过阈值主动重启相关线程。这个属于保底方案实测下来不用走到这一步。4.4 常见问题速查表现象可能原因排查与解决方法推理时间异常高100ms模型跑在CPU后端检查core_mask设置强制指定NPU核心多模型并发时延迟飙升DDR带宽争抢/未做核心绑定减少取流分辨率绑定不同NPU核心量化后精度崩掉校准数据集场景单一增加多场景校准图尝试mmse量化算法内存随时间缓慢增长旧版RKNN Runtime内存泄漏升级rknn-toolkit2和板端rknpu驱动第一次inference特别慢模型首次加载和预热时间在初始化后先跑一次空推理完成预热检测框抖动/误报后处理阈值低/无规则引擎提高置信度阈值增加时域滤波逻辑4.5 两个独家避坑技巧第一个是模型预热。RKNN Runtime在加载模型后的第一次inference会做图优化和内存池分配延迟能达到后面正常推理的5~10倍。我通常会在初始化完成后立刻跑一次空推理把预热时间从正式运行时剔除。实测这个操作能避免第一帧画面漏检。第二个是反初始化顺序。程序退出或看门狗重启功能时先销毁推理线程的RKNNLite实例再退出线程。我遇到过一次crash原因是分类线程还在inference时主线程先把det_rknn销毁了两个上下文共用底层驱动资源时出现竞态。正确的做法是停止线程 - 加入线程join - 再删RKNNLite实例。5. 性能实测与部署体验我这套方案最终稳定运行的配置是2路1080p接入实际处理时缩到720p一路YOLOv8n检测线程绑定NPU核心0/1一路MobileNetV3分类线程绑定核心2。整机推理延迟检测单帧约33ms分类单帧约4ms整机输出帧率约25FPS因为规则引擎不需要每帧都处理。CPU占用在四核A76负载约40%整板功耗8~12W用被动散热片就能压住。这套系统在园区安防场景下跑了三周误报率控制在可以接受的范围。唯一想吐槽的是NPU的6 TOPS听起来不大实际能做的事比很多人想象的多——前提是把模型尺寸、输入分辨率、并发调度这些关键参数都抠到位。最后再分享一个实际操作中的体会不要一上来就想着把检测模型做大、把输入分辨率拉满。RK3588这块NPU的甜点区就是640x640的检测模型加224x224的分类模型这个组合能把算力用满而不过热同时精度也足够大多数安防场景。先在这个甜点区里把整套链路跑通再考虑要不要为了更高精度去换更大的模型、调整核绑定策略这是最稳妥的落地路径。
返回列表