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

资讯详情

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

RK3588 NPU多路视觉模型并发部署实战:三模型同跑不卡顿

RK3588 NPU多路视觉模型并发部署实战:三模型同跑不卡顿 前一阵子接了个边缘计算项目需求挺有意思单块 RK3588 开发板要同时跑人员入侵检测、烟火识别、垃圾分类三个视觉模型。听起来像三个项目揉在一块其实最终落地就靠一块 RK3588 的 NPU 硬扛。先说结论RK3588 的 NPU 确实能同时干这三件事而且还能留出余量做 RTSP 拉流和业务逻辑。但难点不在“能不能跑”而在“怎么调度、怎么分配资源、怎么保证一路模型卡顿不拖垮另外两路”。这篇文章我把自己从模型转换到多路并发部署的完整思路和踩坑记录整理出来给准备在 RK3588 上做多路视觉推理的朋友一个可以直接参照的落地路径。1. 项目场景与需求拆解最开始拿到需求时我脑子里第一反应是“又要上服务器了”但客户给的硬件约束很死整机功耗不能高体积不能大只能上一块 RK3588 核心板。所以问题就变成了——一块 6 TOPS 算力的 NPU 到底能不能扛住三个检测模型同时跑。1.1 三路视觉任务的计算特点分析把三个任务拆开看人员入侵检测通常是 640x640 输入YOLOv8s 这类模型主要看人形目标实时性要求高最好能做到 5-10 FPS 的检测频率。烟火检测输入分辨率可以小一点但模型要能识别小目标烟雾和火焰的特征偏纹理和颜色检测难度比较高。垃圾分类类别多常见模型会分成 40 类左右但目标通常是固定的垃圾桶区域检测范围不大输入可以压缩到 320x320 甚至更小。三个模型如果都按 YOLOv8s 整图跑算力肯定不够。但如果把输入尺寸降下来、模型结构做轻量化、再错开调度6 TOPS 是够用的。1.2 为什么选择 RK3588 而非其他平台选 RK3588 不单是因为它 NPU 算力有 6 TOPSINT8更关键的是它的三核 NPU 支持多模型并发。官方文档里明确写了支持 3 个 NPU 核心独立使用或合并使用这意味着我可以把不同模型分配到不同核心上避免相互抢占。另外 RK3588 的 CPU 是 4 核 A76 4 核 A55做视频解码和业务逻辑也绰绰有余。整板功耗控制在 5-10W不需要主动散热就能长期跑当然加了风扇更稳这一点在边缘场景里非常重要。可以说RK3588 最适合的就是“中等算力需求、多路 IO、单板完成全部业务”的视觉盒子类项目。2. 模型转换与 RKNN 部署准备模型跑在 NPU 上必须先转成 RKNN 格式。这一步是整套流程里坑最多的环节很多人卡了好几天其实大部分问题出在模型结构和量化配置上。2.1 YOLOv8 等其他模型转 RKNN 的操作流程我这边用的 PyTorch 训练好的模型首先导出 ONNX然后用 RKNN-Toolkit2 转成 RKNN 格式。以 YOLOv8s 为例的转换步骤大概是# 安装 rknn-toolkit2注意 Python 版本要用 3.8/3.10/3.11 pip install rknn_toolkit2-2.3.0-cp38-cp38-linux_x86_64.whl导出 ONNX 时可以先把模型的输出端做简化去掉一些不必要的后处理节点我通常只保留主干网络的输出层把 NMS 放到 RK3588 的 CPU 上做这样转换成功率更高、部署也更灵活。速度快一些的 YOLOv8 变体也可以替换成 YOLOv5、YOLOv7-tiny、PPYOLOE 等结构。转换时的关键代码from rknn.api import RKNN rknn RKNN() rknn.config(mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588) rknn.load_onnx(modelyolov8s.onnx) rknn.build(do_quantizationTrue, datasetdataset.txt) rknn.export_rknn(yolov8s.rknn)这里有几个细节dataset 文件里放的是用于量化校准的图片路径列表一般选 100-200 张覆盖各种场景的图不需要太多但一定要有代表性。比如烟火检测的校准图里必须包含不同亮度、不同距离的火焰和烟雾。do_quantizationTrue是 INT8 量化模型体积会缩小到原来的 1/4 左右推理速度大幅提升但精度会有一点损失。如果精度掉得厉害可以尝试用混合量化把某些敏感层保留 FP16。量化方式默认是 normal量化失败或精度问题可以考虑quantized_dtypewino_s8或者调整优化等级。2.2 三个模型的类别映射与预处理差异三个模型检测的类别不同预处理方式也不完全一样。人员入侵和烟火检测用 RGB 输入垃圾分类也用 RGB但输入尺寸不同。为了简化部署我把三路模型的输入都统一成 RGB、0-255、除以 255 归一化这样在板端处理图像时只用写一份预处理代码。类别映射上要注意人员入侵模型可以只要 person 这一类也可扩展为 person、car、dog 等看项目需要。烟火检测模型烟雾 smoke 和火焰 fire 两类建议单独输出两个分数方便后端做报警分级。垃圾分类模型40 个类别可回收、厨余、有害、其他各自细分类别 ID 要和训练时保持一致千万别在部署时改了训练时的映射表。我建议在板端写一个配置文件把每个模型对应的类别名称、输入尺寸、阈值、锚点参数都放在一起方便后面调参。2.3 NPU 算子兼容性是最大的隐形坑转换模型时最头疼的就是遇到 NPU 不支持的算子。RK3588 的 NPU 对常见 CNN 算子支持得不错但有些模型结构里会有特殊算子比如某些注意力机制里的自定义 op、动态 shape 的 op这些在转成 RKNN 时很容易报错。一个典型的例子YOLOv8 的 C2f 结构里有一些 split、concat 算子理论上 RKNN 能支持但版本较旧的 RKNN-Toolkit 可能处理不了。解决办法就是把 RKNN-Toolkit 升级到 2.3.0 以上同时把 ONNX 的 opset 版本固定到 12 或 13有时候 opset 太高反而会触发奇怪的 bug。如果遇到算子不支持的报错优先检查RKNN-Toolkit 版本是否过旧模型文件中是否有动态维度ONNX 是否有冗余输出节点3. 单块 NPU 同时跑三路模型的并发架构设计这是整个项目最核心的部分如何让三个模型同时跑在 NPU 上互不干扰又能高效利用算力。3.1 多进程还是多线程深入对比后在 RK3588 上的选择在 RK3588 上做多路模型并发常见的有两种架构方案方案一单进程多线程每个线程持有一个 RKNN 上下文各自调用模型推理。 方案二多进程每个进程加载一个模型进程间通过网络或共享内存通信。我最初直接采用多进程方案因为逻辑清晰、隔离性好一个模型崩了不影响另外两个。但实测发现几个问题三个进程同时跑内存占用直接翻倍RK3588 板载内存只有 8GB如果上 16GB 版本会好些进程一多就紧张。NPU 资源仍由同一个驱动调度进程之间并不会有真正的资源隔离一个模型推理耗时过长另一个模型照样被阻塞。进程间通信比如把检测结果传回主控增加了延迟和代码复杂度。后来果断切换到单进程多线程方案主线程负责视频拉流和图像预处理。三个推理线程每个线程持有独立的 RKNN 上下文。这里说明一下RKNN Runtime 允许在一个进程内创建多个 RKNN 上下文分别绑定不同的模型文件如果希望进一步负载均衡也可以把同一个模型做成多个副本上下文轮询。推理线程拿到图像后直接调用推理接口结果写入带锁的环形队列由业务线程读取并执行报警、统计逻辑。实际测试下来单进程多线程的内存占用比多进程低了 30% 左右推理延迟也更稳定强烈建议这类单板多模型项目优先考虑线程模型。3.2 NPU 多核绑核策略与优先级调配RK3588 的 NPU 有三个核理解主网格阵列Main Grid Array和 MAC 阵列的概念对做绑核调优有帮助。简单说MAC 阵列是真正做乘加计算的硬件单元多个 MAC 阵列组成一个主网格NPU 核就是主网格阵列的调度单位。RKNN Runtime 内部会自动调度到多个核但在多路模型场景下自动调度未必最优。比如烟火检测模型很小让它占满三个核反而浪费人员入侵模型大需要更多算力。可以手动指定每个模型的 NPU 核分配// 以 C API 为例设置 RKNN 核心掩码 rknn_core_mask core_mask; core_mask RKNN_NPU_CORE_0; // 只使用核0 rknn_set_core_mask(ctx, core_mask);我实际测试过几种分配方式结论如下模型分配方案平均单帧推理耗时说明人员入侵 640x640使用全部3核38ms大模型吃满算力烟火检测 640x640使用核0核132ms中等需求垃圾分类 320x320使用核218ms轻量模型单核足够以上三模型全部自动调度45-55ms 波动大自动调度互相争抢手动指定核心后三路模型同时推理总耗时反而比自动调度低这是因为避免了核间频繁的任务切换开销。注意每个模型设定的 core_mask 一旦固定多个线程的推理请求会按模型各自的核心掩码并行执行互不干扰。需要提醒一点rknn_set_core_mask这个 API 必须在init_runtime之后、第一次推理之前调用否则不生效。runtime 版本较老的可能不支持该接口升级 RKNN Runtime 到 2.x 以上即可。3.3 推理调度策略固定帧率 vs. 事件触发不同视觉任务对检测频率的要求不一样没必要三个模型都每帧跑人员入侵对实时性要求高可以按视频流的每一帧或每隔一帧检测一次。烟火检测希望尽早发现异常但实际上烟雾火焰的变化不会在几十毫秒内发生3-5 FPS 足够了。垃圾分类属于慢变化场景垃圾桶状态通常几分钟才变化一次1-2 FPS 或者按事件触发即可。我在实现时就是固定循环动态降频人员入侵每帧检测 烟火检测每 3 帧检测一次 垃圾分类每 10 帧检测一次或检测到人靠近时立即执行一次。这样的调度方式大幅降低了 NPU 平均负载实际测试中三路模型并发时NPU 平均占用率只有 45% 左右最高瞬时占用率达到 87%但不会长期过载。3.4 多路 RTSP 拉流与图像帧同步三个任务对应的视频源可能不是一个摄像头人员入侵看的是周界摄像头烟火检测看的是厂区摄像头垃圾分类看的是垃圾桶上方摄像头。所以板子需要并发拉取多路 RTSP 流。RK3588 的硬件解码器能同时解码多路 1080p 视频实测拉 3 路 1080p 流没问题。我用 FFmpeg 库来实现拉流和解码解码后的帧直接转成 RGB 并缩放到模型输入尺寸注意一帧数据可以同时喂给多个模型避免重复解码。以前遇到的一个典型性能问题是三路视频流用三个线程各自调用 FFmpeg 解码内存拷贝开销很大。后来优化成一个拉流线程统一拉取三路视频。解码后的帧放到对应视频源的环形缓冲区。三个推理线程只从缓冲取最新帧不需要的时候直接丢旧帧。这样避免了多线程反复解码同一路流也减少了内存带宽压力。4. 核心实现过程与关键代码片段这一节把整个工程的骨架代码写出来大家可以直接参考改造。主控逻辑用 C 写的模型推理层的封装 C 和 Python 都提供示例。4.1 工程整体目录结构我习惯按功能拆目录一个典型的 RK3588 多路视觉工程目录结构如下. ├── CMakeLists.txt ├── config/ │ ├── intrusion.json │ ├── fire.json │ └── garbage.json ├── src/ │ ├── main.cpp │ ├── rknn_model.cpp │ ├── rknn_model.h │ ├── video_stream.cpp │ └── video_stream.h ├── models/ │ ├── intrusion.rknn │ ├── fire.rknn │ └── garbage.rknn └── third_party/ ├── rknn-toolkit-lite2 └── ffmpeg每个模型对应一个RknnModel对象支持的参数包括模型路径、输入尺寸、是否量化、NPU 核心掩码、置信度阈值等读入不同的 json 配置文件完成初始化。4.2 推理线程与多模型并发核心代码用 C 显示推理调用的基本流程如下// RknnModel 封装类 class RknnModel { public: bool init(const std::string model_path, rknn_core_mask core_mask); bool inference(cv::Mat input_image, std::vectorDetectResult results); private: rknn_context ctx_; }; // 三个模型并发推理 void* inference_thread(void* arg) { ThreadParam* param (ThreadParam*)arg; while (running) { cv::Mat frame param-input_queue-get_latest_frame(); std::vectorDetectResult results; param-model-inference(frame, results); param-result_queue-push(results); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } return nullptr; }这里要注意get_latest_frame是一个关键操作它从视频缓冲队列中取最新的一帧而不是取最早的保证推理线程不追赶旧帧。Python 版本的核心是调用rknn.inference():import numpy as np from rknnlite.api import RKNNLite rknn_lite RKNNLite() rknn_lite.load_rknn(intrusion.rknn) rknn_lite.init_runtime(core_maskRKNNLite.NPU_CORE_0) # 推理时 outputs rknn_lite.inference(inputs[img])在 Python 里同时创建三个RKNNLite实例分别配置不同的core_mask即可实现非常简洁。4.3 结果后处理与判断逻辑后处理方面YOLO 系列的解码我放在 CPU 线程上执行流程是输出特征图 - 解码候选框 - 按类别过滤 - NMS。这一步虽然有点耗时但 CPU 上做能降低 NPU 任务复杂度同时输出置信度分数给业务层。业务层拿到三路结果后按不同策略处理class IntrusionStrategy { public: void process(const std::vectorDetectResult results) { for (auto r : results) { if (r.label person r.score 0.6) { // 触发报警同时输出截图 alarm_publisher_-notify(ALARM_INTRUSION, r.bbox); } } } };人员入侵阈值建议 0.5-0.6烟火检测阈值可以降到 0.3-0.4漏报比误报更可怕垃圾分类阈值则要高于 0.6避免误分类。4.4 性能测试与真实数据记录我在 RK3588 开发板8GB 版本配 PWM 风扇上做了一组实测把数据记录下来供参考模型输入尺寸INT8 量化单帧耗时(ms)分配核备注人员入侵 YOLOv8s640x640是38全部3核最好的实时性烟火检测 YOLOv5s640x640是32核0核1小目标检测误检率低垃圾分类 MobileNetV3-SSD320x320是18核2轻量分类三路同时运行--总耗时约92ms如上平均帧率约11 FPS如果严格按帧同步跑三路模型每路各跑一帧的总耗时约 92ms但实际业务中由于调度错开NPU 平均压力明显小于这个数。按前面的调度策略总 NPU 占用率只有 45% 左右CPU 占用率约 60%板子温升稳定。5. 踩坑实录六个最容易翻车的地方下面这些坑都是实际开发过程中踩过的有些花了我两三天才查出来提前列出来能让有缘人少走弯路。5.1 RKNN 模型加载失败提示版本不匹配这个太常见了。RKNN ToolKit电脑上做模型转换和 RKNN Runtime板子上加载模型运行必须保持版本兼容。比如你用 Toolkit2 转换出的 v2 格式模型放到一个只能用 v1 格式的旧 Runtime 上必然报错。解决办法是转换和运行前都检查一下版本最好把板子上的librknnmrt.so也升级到和 Toolkit 对应版本一致。具体版本号对应关系见官方 release notes。5.2 推理偶尔超时延时不稳定如果你发现某个模型的推理时间偶尔从几十毫秒跳到二百毫秒先检查是不是其他模型占用了太多 NPU 资源。尤其是模型没设置 core_mask 时另一个大模型可能把三个核全占住导致小模型排队。此外还有可能是温度过高触发 NPU 降频。这时候两条路加 PWM 风扇主动散热并监控/sys/devices/platform/pwm-fan/hwmon/hwmon*/fan1_input读取风扇转速。逻辑上做降频保护如果检测到 NPU 连续 N 次推理超时就把非关键任务比如垃圾分类的频率降低。5.3 温度过高导致 NPU 降频或系统重启RK3588 满载跑三个模型发热量不可小觑。我最初用被动散热片结果跑了十分钟后 NPU 温度直接 85 度整机降频推理时间翻倍。后来换成 PWM 风扇利用板子上的 PWM 接口接风扇温度稳定在 65 度左右。可以在代码里定时读取温度节点超过阈值时开始在 NPU、CPU 频率和风扇转速之间做联动cat /sys/class/thermal/thermal_zone0/temp输出的是毫摄氏度值超过 70000 就该考虑降频或增强散热了。5.4 RTSP 流频繁断连恢复不及时有些摄像头的 RTSP 连接不稳定网络抖动或者摄像头重启都会断流。一开始我没有做断线重连机制导致模型一直在等新帧结果系统误判为无人入侵或检测不到烟火。后来加了一个看门狗逻辑如果某路视频流超过 5 秒没有新帧就自动重建 RTSP 连接同时业务层标记该通道异常。这个机制简单但非常有效。5.5 内存泄漏与长时间运行后系统卡死跑了大半天后发现系统越来越卡排查下来是每帧推理时的输入输出内存没有正确释放导致内存持续增长。RKNN 的推理接口会对输入输出的rknn_tensor_mem做一些缓存如果每次推理都重新申请、不释放内存迟早被吃光。解决办法推理循环内复用输入输出缓冲区并且在退出时统一释放避免频繁申请和释放 NPU 内存。5.6 多个 RKNN 上下文同时初始化导致启动慢如果三个模型在程序启动时同时加载和初始化会发现启动时间很长有时甚至超过 30 秒。原因是 NPU 驱动在申请连续内存时三个上下文同时初始化会发生竞争。我改成顺序初始化一个加载完成后再加载下一个同时把模型文件放进 ramdisk 或者提前加载到内存能把启动时间压缩到 8 秒左右。这块对产品体验有影响如果你做的是随时上电运行的设备值得优化。6. 如何验证系统稳定性和异常恢复能力多路视觉系统部署到现场前必须要做好几轮稳定性验证不能只在开发板上测几分钟。6.1 长稳测试与监控指标整理我写了一组监控脚本每 2 秒记录一次系统状态包括CPU 使用率、NPU 使用率、内存占用、温度、风扇转速、三路视频流帧率、推理耗时。测试方案是连续运行 72 小时前 24 小时是人工模拟入侵和烟火场景在摄像头前走动、放置烟雾弹后 48 小时跑日常环境。重点关注这几项指标内存占用是否缓慢增长推理耗时是否有持续上升的趋势温度是否超过 75 度有无 RTSP 断流无法自动恢复报警记录数量是否在合理范围内6.2 常见问题快速排查表现象可能原因排查与解决模型加载失败RKNN Runtime 版本不匹配升级 runtime 库确认模型格式推理速度慢未绑定 NPU 核多路争抢设置 core_mask按模型配置分配温度过高散热不足 / 满载运行加风扇、降低非关键任务频率RTSP 断连网络抖动或摄像头重启加断线重连和看门狗机制内存增长输入输出未复用、内存泄漏复用缓冲区循环中不重复申请检测框不准量化精度损失 / 阈值不合适增加校准图、调整阈值、考虑混合量化7. 扩展方向与进一步优化思路这套系统跑通之后后续还可以做很多优化和功能扩展我简单列几个方向供计划做 RK3588 视觉网关或边缘计算盒子的朋友参考。7.1 接入 ROS2 做多传感器协同有些场景需要把检测结果发布到 ROS2 话题中与机器人或 AR 系统联动。RK3588 跑 Ubuntu/Debian 系统时可以直接安装 ROS2然后把三个模型的检测结果包装成自定义消息按固定频率发布。因为 Debian 11 上主干支持也够用避免在板子上源码编译 ROS2 的长时间等待。我之前在 RK3588 上配置过 ROS2 环境只要选对发行版二进制包流程还算顺利。注意发布频率不宜过高否则会占用大量 CPU。7.2 接入 MIPI 摄像头与 IMU 传感器有些项目需要接 MIPI 接口的摄像头或者陀螺仪做防抖、姿态判断。RK3588 支持多路 MIPI 输入同时可以通过 I2C 或 SPI 接 BMI088 等运动传感器。MIPI YUV 格式的数据直接送给 NPU 之前需要做好格式转换和对齐这部分有单独的知识点项目需要时可以再深入。7.3 模型轻量化与 INT4 量化如果后面要同时跑更多路模型可以从模型轻量化入手改成 YOLOv8-n 或 MobileNetV4 等结构部分模型可试 INT4 量化但精度损失需要测试验证。RKNN-Toolkit 已支持 INT4 量化实测垃圾分类这类对纹理不敏感的模型INT4 量化后精度几乎无损而模型体积和推理速度进一步优化。7.4 把模型自动升级机制做进去部署到现场后如果模型需要更新不可能每次拿数据线刷机。可以做一个模型热更新模块将新模型文件放到指定目录程序通过 MD5 校验文件变化后重新加载对应模型上下文。这样模型升级完全远程完成不用重启系统。写在最后的实战体会整套系统从模型转换到三路并发跑通前后调试大约花了两周时间。我个人的体会是RK3588 的 NPU 算力其实比很多人想象中更充足瓶颈往往不在算力本身而在推理调度、内存管理、散热设计这些容易被忽略的地方。一个小技巧分享给大家在 RKNN 推理时如果连续跑多个模型可以尝试把模型加载完成后先做一次预热推理再用高性能模式跑实际任务。避开了第一次推理的初始化抖动检测延时会稳定很多。另外建议把每个模型的推理日志加个时间戳出现性能波动时结合系统温度日志一起排查比毫无头绪地猜问题高效得多。希望这篇文章能帮到正在 RK3588 上做多路视觉部署的朋友也欢迎在评论区分享你的踩坑经历。
返回列表