RK3588 边缘 AI 盒子多模型部署实战:单板卡同时跑入侵烟火垃圾分类
设备还没到货需求文档已经压过来了。施工工地出入口要人形入侵告警废品回收站要看烟火和垃圾混投分类甲方点名要一套能在单板边缘盒子上离线跑完所有算法的方案。我当时看了一眼预算和算力表脑子里第一个念头是RK3588 那颗 6 TOPS 的 NPU 能不能扛住三路模型同时推理。后来整套方案落地实测数据比我预想的好不少也踩了足够多的坑值得单独写一篇记录。这篇内容不是 RKNN 官方文档的复读也不是跑个 mnist demo 的玩具贴。我会从算力盘算、模型选型、模型转换、多模型并发调度、性能调优到最后排障清单把单块 RK3588 同时跑三个检测模型这件事完整拆开。适合正在做边缘 AI 盒子、多路视频分析、RK3588 部署的朋友参考。1. 需求拆解与算力评估一块 RK3588 到底够不够用1.1 三个检测任务一起上的真实业务场景先别急着谈模型和帧率先搞清楚这类需求通常出现在什么现场。人员入侵检测一般放在工地周界、园区围墙、配电房门口目标是检测人形目标进入预设区域报警延迟要求高通常希望从画面上出现人到产生告警不超过 1 秒。烟火检测放在仓库、垃圾场、山林监测点目标是火焰和烟雾这类目标形状变化大、纹理不清晰模型召回率不能太低。垃圾分类则是在垃圾投放点或者传送带上方安装摄像头识别瓶子、纸张、易拉罐、电池、厨余等常见类别它对单帧分类的准确性要求高但实时性要求相对宽松。这三个任务有一个共同特点全是视觉检测都要跑深度神经网络而且都要长期在线运行。如果每个任务买一块 Jetson 或者配一台 x86 工控机成本直接翻三倍功耗和体积也完全不适合边缘场景。所以单块 RK3588 全包不是甲方异想天开是整个行业在内卷成本之后必然会提出的要求。这里有一个很关键的认知要先纠正RK3588 是一颗 SoC不是单纯的一块 NPU 加速卡。它内部有 4 个 Cortex-A76 大核、4 个 Cortex-A55 小核、Mali-G610 GPU以及一颗算力 6 TOPS 的 NPU。这意味着你不仅要考虑 NPU 能不能算得过来还要考虑 CPU 能不能把图像数据预处理、模型调度、结果后处理、日志上报这些事情全部扛住。1.2 RK3588 NPU 算力指标与真实可利用算力RK3588 的 NPU 标称 6 TOPS这指的是 INT8 精度下的峰值算力。它内部有 3 个 NPU 核心理论最高支持同时处理多路模型。但是峰值算力这个东西只存在于 datasheet 里。真实部署的时候NPU 的利用率能到 60% 到 80% 就已经算调得很不错了原因有三点一是模型算子不一定完全被 NPU 加速遇到不支持的算子会回退到 CPU 跑二是大量中间张量的搬移会占用总线带宽特别是当输入分辨率比较大时DDR 带宽会成为瓶颈三是多模型并发时NPU 核心之间的任务分配不可能是绝对均匀的。对于 6 TOPS 的算力上限做一个直观换算。以 YOLOv5s 为例INT8 量化后640x640 输入在 RK3588 上的纯 NPU 推理时间实测通常在 20 毫秒到 40 毫秒之间换算成算力占用大约 2 TOPS 到 3 TOPS。也就是说单颗 YOLOv5s 大约会吃掉 NPU 一半左右的峰值能力。如果三个模型都是 640x640 的 YOLOv5s理论上会卡到 60 到 90 毫秒一帧实际跑起来会非常吃力。所以要回答够不够用必须把模型规格做小把输入分辨率降下来这需要在算法精度和算力占用之间找平衡点。以我这次的项目为例最终决定使用一大两小的配置人员入侵用 640x640 输入以保证远距离小目标召回率烟火检测和垃圾分类用 320x320 输入以压缩计算量。这个组合在算力上留出了充足冗余也为后续接入更多路视频流留了余地。1.3 先算总账再动手每路视频的算力预算动手写代码之前我习惯先列一张算力账单。比如目标帧率是 10 FPS三个任务的输入分别为 640x640、320x320、320x320。用 RKNN 提供的 profiling 工具粗略估算640x640 的 YOLOv5s 单帧约 25 ms320x320 的 YOLOv5n 单帧约 8 ms三个模型叠加的理论耗时约 41 ms。这意味着如果理想情况下三路模型完全并行10 FPS 没有问题甚至能跑到 20 FPS 左右。如果三路模型需要串行排队推理那一帧的处理时间就接近 40 毫秒对应 25 FPS 上限实际还要叠加上 CPU 预处理和后处理的时间。这里我建议每个人都在项目早期做这个简单的数学题需要的目标帧率 x 每个模型的单帧延迟 总 NPU 负载。用这个数去和 6 TOPS 做对比立刻就知道方案是否可行而不是等代码写完跑起来才发现卡成 PPT。2. 模型选型与 RKNN 转换把 PyTorch 模型塞进 NPU2.1 三个模型怎么选精度、算力与板载内存的三角博弈模型选型是整个项目里最影响最终效果的一步。很多人一上来就选 YOLOv8m 甚至 YOLOv8l理由是精度高、效果好。但在边缘设备上模型参数量直接决定显存占用和推理延迟稍微不注意就会把 NPU 吃满。以我这次的三个任务为例人员入侵检测选的是 YOLOv5s。原因很朴素YOLOv5s 在 RK3588 上的生态最成熟RKNN-Toolkit2 对它的算子支持最完整量化后精度掉得少而且 C3 结构的计算密度在 NPU 上发挥得不错。同时人员检测只需要一个类别我可以把模型的类别数从 COCO 的 80 类裁剪到 1 类输出张量大幅减小后处理时间也跟着下降。烟火检测选的是 YOLOv8n。火焰和烟雾的形态与常规物体差异很大YOLOv8 的 anchor-free 设计对这类无固定形状目标更友好。考虑算力预算我用 n 版本而不是 s 版本把参数量压在 3.2M 左右。垃圾分类选的是 YOLOv5n 再加一个分类头。严格来说垃圾投放点的识别不需要检测框只要判断当前画面中垃圾属于哪一类但实际业务中往往需要把垃圾位置也标记出来所以我仍然采用检测模型类别设置为 6 类。320x320 输入下用 YOLOv5n单帧 NPU 推理可以做到 8 毫秒以内。建议大家在选型时记住一个口诀能用 tiny 不用 small能用 small 不用 medium输入分辨率从 640 起步往下砍类别数能少就少。边缘设备上精度差两三个点远没有帧率掉一半致命。2.2 RKNN-Toolkit2 环境搭建与模型转换全流程模型转换需要用到 Rockchip 官方提供的 RKNN-Toolkit2 工具包。它运行在 x86 主机上可以把 PyTorch、ONNX、TensorFlow 等格式的模型转换成 RK3588 NPU 可以加载的 .rknn 文件。先交代环境版本这一步非常关键因为 RKNN-Toolkit2 的版本兼容性问题极其突出。我使用的是 rknn-toolkit2 1.6.0 版本对应 rknpu2 运行时库 1.6.0。注意主机端转换工具的版本必须和板端 runtime 版本匹配否则加载模型时会报版本不兼容错误。转换环境建议用官方 docker 镜像或者直接在 conda 虚拟环境里安装。rknn-toolkit2 依赖的 Python 版本和库很多直接 pip 安装很容易把宿主机环境搞坏。我个人习惯是建一个独立的 conda 环境Python 3.8然后按官方 requirements 安装。转换的核心流程如下from rknn.api import RKNN rknn RKNN() # 1. 配置预编译、量化等参数 rknn.config(mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588) # 2. 加载 ONNX 模型 rknn.load_onnx(modelyolov5s.onnx) # 3. 量化校准 rknn.build(do_quantizationTrue, datasetdataset.txt) # 4. 导出 RKNN 模型 rknn.export_rknn(yolov5s_rk3588.rknn) # 5. 在 PC 端模拟推理验证 rknn.init_runtime() outputs rknn.inference(inputs[img])这段代码里最值得注意的就是rknn.config中的mean_values和std_values。这两个参数必须和你训练模型时使用的预处理方式完全一致。如果你的训练代码里用了 ImageNet 的 mean0.485, 0.456, 0.406和 std0.229, 0.224, 0.225但转换配置里却填了 0 和 255那推理结果会完全乱套。这也是 RKNN 新手最容易犯的错。另外dataset.txt是量化校准图片列表文件每一行是一张图片的路径建议使用 50 到 200 张具有真实场景代表性的图片。量化校准图片不要用训练集里的图最好从实际部署场景中截取这样量化后的模型对真实数据的分布拟合更好。2.3 模型转换中那些不改就会掉精度的细节模型转换看似是脚本一把梭但有几个细节很影响最终精度和 NPU 运行效率。第一个是开启预编译。rknn.config中可以加一个optimization_level3这个参数会在模型转换时将部分算子在 NPU 上做算子融合。比如 ConvBatchNormReLU 的固定组合会被融合成一个算子大幅减少中间张量的内存读写。实测下来同样的模型开启 optimization_level3 之后推理时间可以缩短 15% 到 30%。但要注意这个优化在模型输出层要小心某些模型中融合之后会影响输出张量的排布方式导致后处理解码时坐标错位所以在导出前一定要在 PC 上用 rknn.inference 对比原始 ONNX 的输出确认输出张量一致性。第二个是片外内存布局。RKNN 默认的输出张量可能是 NHWC 格式而 PyTorch 后处理代码通常按 NCHW 写。转换工具不会自动帮你做数据排布转换你需要在后处理代码中显式处理。最好的方式是转换前就在 onnx 模型里用 Transpose 节点把输出转换为预期布局或者在代码里做output.reshape((1, 84, 8400)).transpose((0, 2, 1))这类操作。第三个是量化精度补偿。INT8 量化对检测框回归的影响通常不大但对分类概率的影响比较明显尤其是垃圾这种类别间形态相似的场景。如果发现量化后某类别的召回率下降优先调整量化校准图片的类别分布。比如垃圾分类的六类垃圾桶中如果电池样本在校准集里太少量化后大概率会把电池误判为其他类别需要补充对应图片重新校准。3. 单 NPU 多模型并发真正决定项目成败的调度方案3.1 先搞清楚 RK3588 NPU 的并发机制到底支持什么就算模型选好、转换成功单块 RK3588 同时跑三路模型也依然有门槛。很多人的第一反应是把三个模型塞进一个 .rknn 文件这个思路是错的。RKNN 没有提供类似 TensorRT 的多模型串联图优化能力也不建议把多个模型合并成一个输入。合理的做法是运行时创建多个 RKNN 上下文每个模型对应一个独立的 RKNN 对象然后在多线程里分别调用inference接口。这里需要明确一个关键认知RKNN 接口本身不是线程安全的多线程并发调用同一个 RKNN 对象的inference方法会导致崩溃。正确做法是为每个模型创建一个独立的 RKNN 实例。每个实例内部拥有独立的上下文和内存空间NPU 硬件会自动在多个核心之间分配任务不需要应用层手动指定哪个模型跑哪个核心。实测下来同时初始化三个 RKNN 实例是可行且稳定的。这也是 Rockchip 官方推荐的多模型多实例方案。3.2 相同优先级并发三个模型同时跑的实现我的调度架构可以简化为三线程 独立 RKNN 实例模式。伪代码如下// 每个模型一个线程线程内循环执行推理 void infer_person(void* ctx) { auto rknn create_rknn(person_rk3588.rknn); while (running) { cv::Mat frame capture_frame(0); // 从对应通道取帧 rknn.inference(frame); // NPU 推理 post_process_person(output); // 解码、画框、告警逻辑 } } void infer_fire(void* ctx) { auto rknn create_rknn(fire_rk3588.rknn); while (running) { cv::Mat frame capture_frame(1); rknn.inference(frame); post_process_fire(output); } } void infer_garbage(void* ctx) { auto rknn create_rknn(garbage_rk3588.rknn); while (running) { cv::Mat frame capture_frame(2); rknn.inference(frame); post_process_garbage(output); } }这里面每个线程都是独立的消费者互不阻塞。NPU 硬件内部会按照实际提交的任务负载自动分配算力。如果某一时刻三个模型同时提交推理请求NPU 的调度器会排队处理前面的请求会稍稍阻塞后面的请求。实测中三路模型并发时的总吞吐量大约比串行低 8% 到 15%这是可接受的代价。这个架构的好处在于逻辑简单每个线程只关心自己的模型和感知结果不涉及复杂的锁和共享状态后续如果要把单路摄像头扩展成多路只需要在线程内部加一个摄像头通道轮询即可。3.3 内存铺排与 Debug跑起来之后怎么办三路模型跑起来之后内存占用是一个非常容易忽视的问题。RKNN 推理时的内存开销主要分三块模型参数内存、输入输出张量内存、运行时临时缓冲区。以我的三个模型为例模型文件总大小约 25MB但运行时每个模型会额外分配约 100MB 到 200MB 的临时内存。三个模型同时存在内存占用轻松超过 600MB。如果开发板的系统内存低于 4GB建议在分配 RKNN 实例时使用RKNN_FLAG_MEM_ALLOC_OUTSIDE标志把输出张量内存分配到应用层管理的内存池中这样能减少反复申请释放内存带来的碎片问题。同时要注意创建第二个 RKNN 实例时如果上一个模型的临时内存没有释放干净会出现 coredump排查方式是在创建新实例之前调用一次强制内存回收并观察/proc/meminfo中的 MemAvailable 数值变化。另外强烈建议在开发阶段给每个线程的推理函数加一个耗时统计打印最近 500 帧的平均推理延迟。这样做的好处是能快速发现某个时段的性能劣化比如垃圾模型开始时延异常升高通常不是因为 NPU 算力不够而是 SD 卡读写或者视频解码线程抢占 CPU 造成的。4. 实际性能测试与调优记录帧率、负载和延时到底怎么样4.1 详解一下我的推理耗时分解我把三个模型放进完整的应用流程里开启 RKNN 的 profiling 功能拿到的真实数据是这样的模型输入尺寸单帧 NPU 推理耗时CPU 预处理耗时后处理解码耗时实际帧率人员入侵 YOLOv5s640x64022.6 ms3.8 ms1.2 ms约 30 FPS烟火检测 YOLOv8n-pose320x3207.4 ms2.1 ms0.8 ms约 45 FPS垃圾分类 YOLOv5n320x3206.9 ms2.3 ms1.0 ms约 45 FPS注意这个数据是三路模型同时跑、系统压力最大的情况下的数据。NPU 的总体利用率大约在 75% 左右已经逼近合理上限。如果把人员入侵模型的输入提高到 960x960单帧推理会跳到接近 50 ms帧率会被砍半整体系统也会开始出现视频丢帧。从这里可以得出一个很重要的结论三路模型的帧率叠加起来完全足够覆盖业务需求。人员入侵只需要 5 FPS 就能保证告警及时性烟火检测和垃圾分类 3 FPS 就够所以我实际把三路模型统一配置为 10 FPS给系统留出非常充裕的冗余。4.2 调度周期与帧率的最优配置很多人在部署时习惯能跑多快跑多快把所有模型的推理都调到最高帧率。实际上在边缘设备上高帧率带来三个问题CPU 占用暴涨、NPU 发热加剧、系统不稳定。我后来统一改成了定时推理模式用 Linux 的 timerfd 每隔 100 毫秒唤醒一次各个推理线程从摄像头取最新一帧来处理。取帧的方式很重要不要用阻塞式的cv2.VideoCapture.read()因为如果摄像头帧率只有 15 FPSread()会一直阻塞等待线程的其他清理工作就全被堵住了。而是先看缓冲区状态用grab()加retrieve()的方式取最新帧或者直接轮询相机 API 的帧回调。通过这种固定节拍的方式三路模型的总算力负载被控制在一个稳定的水平不会再出现偶发的连续多帧同时提交导致的 NPU 排队震荡。从系统稳定角度看固定 10 FPS 比拼命跑 30 FPS 时要稳得多。4.3 稳定性测试7×24 小时烧机下的 NPU 温度与内存抖动边缘盒子最怕的不是性能不够而是跑一个月后随机死机。我在交付前做了一轮 7×24 小时压力测试监控三项指标NPU 温度、系统内存、推理延迟曲线。先说温度。RK3588 的 NPU 满载运行时核心温度大约稳定在 65℃ 到 75℃。我的测试环境是密闭铝合金壳体外加一个 5V 风扇温升在合理范围内。如果没有主动散热温度会一路飙到 90℃ 以上触发 SoC 的降频保护推理延迟会突然翻倍。所以如果你用的是无风扇的被动散热方案建议把三路模型的帧率配置改成 5 FPS 加上跳过帧策略即每两帧处理一帧。内存抖动是另一个坑。最初版本我每次推理后显式释放输入输出内存但长期运行后内存碎片越来越严重MemAvailable 慢慢从 2GB 掉到 1GB 以下。后来改成内存池预分配方案在创建 RKNN 实例时就把输入输出内存一次性分配完成推理过程中只复用不释放内存曲线变得非常平稳。一个小技巧是定时检查/sys/class/thermal/thermal_zone0/temp温度和/proc/meminfo可用内存超过阈值就在业务日志里打告警提前发现问题而不是等客户反馈设备挂了才去现场看。5. 多模型并存的进阶玩法任务优先级抢占与资源隔离5.1 紧急任务优先入侵检测 20ms 响应如何保住三路模型同样重要只是理想情况实际业务里人员入侵告警的紧急程度远高于垃圾分类。如果入侵检测线程恰好被其他模型的推理任务排在了后面告警延时就会变得不可控。所以生产级方案还需要优先级抢占机制。在 Linux 线程层面可以使用pthread_setschedparam给入侵检测线程设置SCHED_FIFO实时调度策略和较高优先级让它能够优先抢占 CPU 时间片。这保证图像预处理、后处理等 CPU 环节不会被其他线程拖累。但注意NPU 的调度并不完全由 CPU 线程优先级控制无法保证 NPU 内部的任务抢占。更可靠的方式是在应用层实现一个低优先级模型跳帧逻辑当入侵检测线程的高帧率任务已经提交并开始推理时其他检测线程在提交推理前先检查一下 NPU 队列的占用情况如果发现有紧急任务正在运行就果断跳过当前帧等下一个节拍再处理。这个策略在我实际测试中非常有效。正常配置下即使垃圾模型和烟火模型刚好同时提交入侵检测的端到端告警延迟也能稳定保持在 200 毫秒以内完全满足工地周界告警对秒级响应的要求。5.2 利用核心隔离和线程优先级给业务留出后路RK3588 是 4 大核 4 小核的架构。推理线程的 CPU 亲和性设置也值得讲究。我的分配策略是视频采集、解码、预处理线程绑定到大核 0-3 中的两个核心保证图像处理不被卡住。三路模型的 NPU 推理线程绑定在剩下的两个大核上它们的主要任务是提交 NPU 任务和执行后处理。网络上报、日志记录、MQTT 推送等后台任务绑定到小核 A55 上这些任务对时延不敏感没必要抢占大核资源。使用sched_setaffinity可以将线程绑定到指定核心。这层优化做完之后即使后台有大量日志写入或者网络抖动三路模型的推理延迟也不会出现明显毛刺。还有一个容易忽略的优化点是内存带宽隔离。当三路模型同时运行时DDR 带宽是共享资源。如果视频解码同时从摄像头拉取多路 1080P 流带宽占用会非常大。一个务实的做法是把摄像头的视频流分辨率设置为 1280x720而不是 1920x1080在清晰度损失可以接受的范围内大幅降低带宽压力。实测 1080P 三路解码 三模型推理时总内存带宽占用约为 80%降到 720P 后占用降为 55%系统响应平滑很多。6. 踩坑实录从模型转换到多路视频流的十二个常见坑最后把这几个月里踩过的坑做成了清单每一条都是真金白银换来的教训。放在最后给要动手的朋友当挡箭牌。模型转换相关rknn.config中的 mean/std 值与训练预处理不一致导致输出概率全部偏移。解决办法是训模时保存一份预处理参数转换时原样复制。RKNN-Toolkit2 版本与板端 librknnrt.so 版本不匹配模型加载直接失败。拿到固件后先查看板端 runtime 版本再决定主机端工具版本。load_onnx时遇到不支持的算子层通常出现在自定义网络层。解决办法是先转 ONNX 再打开 simplify 开关特例情况需要重写算子树。量化校准图片用了训练集图片导致部署场景掉点严重。重新截取现场真实画面做校准集后mAP 提升明显。多线程并发相关多个线程共享同一个 RKNN 实例会随机崩。必须一模型一实例。频繁创建和销毁 RKNN 实例会导致内存碎片。启动时一次性初始化全部模型进程生命周期内不释放。没有给推理线程设 CPU 亲和性线程在不同核心间来回调度延迟抖动高达 30%。绑定核心后抖动降到 5% 以内。三路视频解码没有用硬件解码全用 OpenCV 软解CPU 占用直接飙到 300% 以上。RK3588 有专门的视频解码单元务必走硬编解码通道。部署运行相关风扇转速没有读取和监控夏季高温时 NPU 突然降频帧率掉一半。读取风扇转速的方法可以借用 i2c 或者 pwm 控制器实时监控并自动告警。网络断线时 MQTT 推送线程一直阻塞重连占用大量 CPU。改成带超时和退避策略的非阻塞推送同时把缓冲队列大小设成固定值。推理线程与 UI 线程共用同一个视频帧缓存出现画面撕裂。用环形缓冲池替代单一缓存变量。日志写得太频繁同时写 SD 卡和串口导致 IO 阻塞。把日志级别在生产环境调到 ERROR同时把日志文件写到内存盘而不是 SD 卡。这十二个坑每一个都对应过线上问题。尤其是第二和第七条属于不定位到进程级别根本发现不了的问题排查起来非常耗费时间。建议所有准备做 RK3588 多模型部署的人把这些检查项当成启动前的标准自检流程而不是出了问题再补救。回到标题里那个问题单块 RK3588 的 NPU 能不能同时跑人员入侵、烟火检测、垃圾分类答案是可以而且稳定跑 10 FPS 三路并发毫无压力。前提是模型要选小、输入要控制、并发要用多实例、调度要做节拍控制做到这几点这块 6 TOPS 的小芯片能给你的惊喜比你预想的多得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →