
做 RK3588 边缘 AI 最头疼的往往不是单路模型跑不快而是多路任务一起上时互相打架。这篇是这个系列的第 3 篇前两篇我们把 RK3588 上的模型部署链路、单路视频的推理加速都过了一遍这一篇专门聊聊“同源多任务调度”——也就是多路视频、多个检测需求同时存在时怎么让它们共享一路数据源、共享同一个模型、共享同一块 NPU还能各跑各的不掉链子。适合正在 RK3588 上做多路视觉检测、工业质检、安防布控类项目的朋友看完可以直接拿去改到自己的框架里。先交代一下背景。我手头这套设备是一块 RK3588 核心板外接 4 路海康 IPC需要同时跑人脸检测、安全帽未佩戴检测、明火/烟雾识别这几个业务。刚开始的做法非常朴素每个业务各拉一路 RTSP 流各加载各的模型各开各的线程去推。结果一上电就发现系统资源被迅速吃满视频卡顿、NPU 占用率忽高忽低内存也频繁告警。真正的问题不是某一个模型慢而是多个任务在“同源”场景下没有统一调度大家抢内存、抢解码器、抢 NPU最后谁也跑不顺。1. 先拆清楚“同源多任务”到底卡在哪很多人一上来就想着怎么优化模型、怎么压帧率但多任务场景下真正要解决的是资源编排问题。我先说清楚这里的“同源”指的是什么以及 RK3588 这块芯片到底有哪些资源容易成为瓶颈。1.1 边缘视觉任务的三种同源形态我在实际项目里总结出“同源”至少有三层含义这三层往往是叠加出现的。第一层是数据源同源4 路摄像头接入后人脸检测、安全帽检测、明火检测都要消费这 4 路视频流。如果每个业务都独立拉流、独立解码同一路视频会被解码好几遍内存带宽和 CPU 消耗直接翻倍。正确做法是只拉一份码流、只解一次码把结果放到公共帧池里所有业务按需取帧。第二层是模型同源很多时候多个业务复用的是同一个基础模型。比如用同一个 YOLOv5s 做检测只是不同业务对输出类别的过滤和后处理不一样。这时候完全没有必要加载 3 份模型、占 3 份内存、排 3 个推理任务而是加载一次多个业务共用同一个 RKNN context靠调度器分配推理时间片。第三层是资源同源无论业务怎么拆最终处理器、内存带宽、NPU 算力、解码器这些硬件资源都是同一套。RK3588 虽然有 3 个 NPU 核心但依然不是“无限并发”的更不能让每个任务都觉得自己独占 NPU。资源同源决定了多任务调度是必然要做的而不是可选项。1.2 RK3588 的家底够不够用RK3588 的参数大家都很熟CPU 是 4 个 A76 大核加 4 个 A55 小核GPU 是 Mali-G610 MC4NPU 标称 6 TOPSINT8内置 3 个 NPU 核心还带 VPU支持 8K 级别的视频硬解。单看数据很唬人但落到真实项目里要清醒一点6 TOPS 的算力其实相当有限。拿最常见的 YOLOv5s 来说如果输入分辨率是 640x640INT8 量化后在 RK3588 上推理一次大概 15~25ms具体取决于模型结构、量化精度和散热状态。这意味着 NPU 满负荷跑每秒也就处理 40~60 帧。4 路视频如果每路都要求 25 帧光一个模型就把 NPU 全部吃掉了更别说其他任务。所以这类项目必须学会做“减法”缩减输入分辨率、控制检测帧率、共享模型参数以及最重要的——用调度去保证关键业务的优先级。不要指望硬件堆得越高越好RK3588 的定义是“够用但必须精打细算”的边缘设备调度的价值就在这里。2. 数据通路设计让一份视频流喂饱所有任务多任务调度不是从推理才开始而是从摄像头接入那一刻就要规划好。同一个视频源只有一份但消费它的业务可能有四五个这个阶段如果设计不好后面调什么都白搭。我的做法是“统一接入、集中分发、零拷贝取帧”。2.1 统一拉流与硬解码4 路海康 IPC 通过 RTSP 协议接入。项目里不建议每个业务各自拉流而是用一个单独的接入模块负责所有视频流。这个模块创建 4 个拉流线程每个线程用 FFmpeg 的 API 去拉一路流也可以通过命令行ffmpeg -rtsp_transport tcp -i rtsp://user:passip:554/Streaming/Channels/101 -frames 1 out.jpg先验证流的可用性但正式代码里还是推荐用 libavformat/libavcodec 做库调用把 RTSP 流解码成 NV12 格式的原始帧。解码这一步要放在 VPU 上不要用 CPU 去软解。RK3588 的 VPU 可以并行处理多路 1080p 解码CPU 软解 4 路 1080p 会直接吃掉好几个核。硬解码出来的帧建议直接落到 NV12 或 RGB 的共享内存里这样后续做模型前处理时不用再转一次格式省掉一笔不小的开销。# 验证单路 RTSP 流是否可用 ffmpeg -rtsp_transport tcp -i rtsp://admin:pass192.168.1.64:554/Streaming/Channels/101 -frames 1 -pix_fmt nv12 frame.nv12 -y2.2 帧池所有业务从同一个地方取帧视频帧被解出来之后不能直接丢给各个业务线程而是先放进一个“帧池”。帧池本质上是一个环形队列加引用计数每一帧在内存里只有一份拷贝谁要用就“借”走用完立即归还或释放。这样能避免同一帧在多个业务线程里被复制好几遍对内存带宽的压力小很多。一个比较容易接受的类比是快递柜快递员解码线程把包裹放进柜子每个取件人业务线程凭取件码去拿拿走之后快递员才能清空格子装下一个包裹。边缘视频任务帧率不需要无限高帧池深度通常设置为 3~5 帧超过这个阈值说明消费速度跟不上应该做丢帧而不是越积越多。帧池还需要考虑“帧龄”问题。如果某个业务跑得慢拿到的一直是十几秒前的老帧那结果没有意义。我的处理方式是在帧结构体里打时间戳调度器在派发任务时会把过老的帧标记为失效让后续业务跳过它直接取新帧宁可少算一帧也不要算一个过期结果。3. 推理任务的调度NPU 是公共资源必须排队数据通了之后核心矛盾就转移到推理环节。这里有一个 RKNN Runtime 的硬性规则同一个 RKNN context 不允许被多个线程同时调用rknn_run否则轻则结果不稳定重则直接崩溃或使 NPU 掉算力。所以“多业务共享同一个模型”的前提是必须有一个串行化的推理调度层把所有请求排成队一个一个喂给 NPU。3.1 为什么不能直接多线程调同一个模型第一次做多任务时我踩过这个坑在一个进程里创建了 3 个线程同时调用同一个rknn_inputs_set和rknn_run结果半小时内 NPU 就出现输出张量错乱后面模型推理速度肉眼可见地变慢。后来查了 RKNN Runtime 的文档才知道NPU 的上下文是绑定到单个 context 的多线程并发读写同一个 context 属于未定义行为。解决办法有两种一种是给每个业务创建独立的 RKNN context相当于每个业务一个私有的推理通道另一种是只创建一个 context所有业务通过任务队列串行访问。前一种适合不同业务用不同模型的情况后一种适合多个业务共用同一个模型的场景。我们这个项目里主要业务都复用一个检测模型所以采用“单 context 任务队列”的方案内存占用小调度也更可控。3.2 一个可落地的推理队列实现推理调度器是我后来反复打磨的核心。它的结构不复杂但几个细节决定了稳定性。整体思路是外部业务模块只负责构造推理请求投递到一个队列里调度器内部的工作线程逐个取出请求执行 RKNN 推理然后把原始输出写回请求对象由各业务的回调去做后处理。// 简化版推理任务结构 struct InferTask { int task_type; // 0: 人脸检测, 1: 安全帽, 2: 明火 int frame_id; // 当前帧号 uint8_t* input_ptr; // 指向帧池中的共享帧 uint32_t input_size; int priority; // 优先级, 数字越大越先执行 std::functionvoid(const rknn_output*, int) callback; }; class InferScheduler { public: void push(InferTask task) { std::lock_guardstd::mutex lock(mtx_); queue_.push(std::move(task)); // 按优先级重排完整实现可用优先队列 } private: std::mutex mtx_; std::queueInferTask queue_; };调度器构造函数里调用rknn_init加载模型rknn_query查询输入输出维度然后启动一个常驻的工作线程。工作线程的伪代码就是不断从队列取任务、调用rknn_run、输出结果再执行回调。这里有两个关键点输入张量的内存最好提前分配好不要在每次推理时重新申请推理后处理的回调不要放在工作线程里做耗时操作否则会把后续任务全部堵住应该把后处理丢到业务自己的线程池。3.3 多模型场景下的资源切分如果业务确实需要多个不同的模型比如除了 YOLOv5s 之外还要跑一个人脸关键点模型那建议创建多个 context并在调度器里做“模型分组排队”。每个 context 有自己的队列和工作线程但全局共享同一份“NPU 权限管理”。RK3588 的 3 个 NPU 核心在驱动层是自动调度的但我们不能依赖它自动均衡还是要手动控制每个模型的时间片。我的习惯是N 个模型就创建 N 个工作线程但每个线程默认执行不超过设定好的时间片比如 100ms时间片用完就切到下一个模型防止某个模型长期霸占 NPU 导致其他任务饿死。整体上这是一个非常朴素的“时间片轮转 优先级提升”思路但实测在边缘设备上已经够用了不需要引入复杂度太高的实时调度算法。4. 调度策略选型与权重计算队列和线程只是骨架真正体现“调度”的是策略。项目一开始我用的是最简单的先来先服务结果发现一旦明火检测偶尔丢帧人脸抓拍就把 NPU 排队通道占满高优任务被低优任务堵死。因此我在调度器里加入了“优先级 权重轮询”的组合策略。4.1 三种常见调度策略的取舍策略核心思路优点缺点适用场景先来先服务按请求到达顺序依次执行实现最简单公平性可接受低优任务可能阻塞高优任务单任务或任务相互独立优先级抢占高优先级请求插队紧急任务及时响应低优任务可能被饿死明火、急停等强实时业务加权轮询按预设权重分配推理时间片兼顾延迟和公平权重设置依赖经验多路视频多业务并存的常态这个项目最终采用的是“加权轮询为主优先级为兜底”。说白了就是平时大家都按权重排队但如果监控业务出现了超过 N 次连续未处理的高优先级请求调度器会临时把它前面的任务往后压确保关键业务尽快执行。4.2 权重是怎么算出来的权重不能拍脑袋定我有一套基于“目标帧率”的计算方式。假设当前模型单次推理耗时 T比如 20ms业务 A 的目标帧率是 25 FPS业务 B 是 10 FPS业务 C 是 5 FPS。不考虑其他开销时每秒钟用于推理的总时间大约是25*20ms 10*20ms 5*20ms 800ms还不到 1000ms说明算力是够的这时候按目标帧率归一化就能得到权重比例 25:10:5即 5:2:1。真正执行轮询时调度器按权重分配“连续推理次数”。权重为 5 的任务一轮最多连续推理 5 次权重为 2 的 2 次权重为 1 的 1 次一轮结束后循环。这样既能保证高帧率业务拿到更多的时间片又不至于让低帧率业务完全饿着。要注意的是这个计算要在模型推理耗时稳定的前提下才有意义所以调度器还要动态统计每次rknn_run的实际耗时如果某段时间耗时明显上升比如 NPU 降频了就要及时调整分配次数避免时间片被浪费。4.3 帧率自适应的兜底策略边缘设备的负载不是恒定的高温降频、多路视频码流波动都会影响推理耗时。我在调度器里加了一个最简单的自适应逻辑每隔 5 秒统计一次各业务的平均端到端延迟从取帧到回调结束如果某个业务延迟超过目标帧率对应的周期比如目标 25 帧周期 40ms就把它的权重下调一个档位同时把富余的时间片补给延迟较低的快速业务。这套逻辑不需要复杂的控制理论就是一个比例积分式的调整在实测中效果很直观能让整体视频流畅度稳定在可接受范围内。5. 实战调优与踩坑记录调度框架搭好之后真正的战斗才开始。下面把我在 RK3588 上跑同源多任务时遇到的高频问题、排查思路和最终方案整理出来这些内容在官方文档里往往找不到但对做项目的人来说比算法原理更救命。5.1 NPU 降频与风扇散热的联动问题RK3588 满载跑 NPU 时发热非常猛尤其是塞在密闭工业机箱里的设备不一会儿就触发热限。RK3588 的 NPU 频率受 devfreq 控制不同板卡路径可能不同可以用ls /sys/class/devfreq/查看常见的是类似fdab0000.npu的节点。跑推理时可以监控它的 cur_freq如果发现频率从 1.0GHz 掉到 500MHz 以下功耗降了、帧率也必然崩。# 查看当前 NPU 频率和可用频率 cat /sys/class/devfreq/fdab0000.npu/cur_freq cat /sys/class/devfreq/fdab0000.npu/available_frequencies我的做法是两层联动第一层在 dts 里配置好 pwm-fan 策略让风扇在 NPU 温度超过 60 度时提前介入而不是等到 85 度才全速转第二层在应用层把 NPU 负载反馈给调度器如果检测到 NPU 频率长时间低于最高档位就要主动降低非关键业务的帧率权重。优先保明火检测和实时预览牺牲那些可以做慢速分析的统计业务这样用户体验不会明显下降。另外读取风扇转速可以看/sys/class/thermal/cooling_device*/cur_state或者查驱动里暴露的 fan 节点排查时先确认风扇策略真的生效否则散热问题会直接表现为推理变慢很容易被误判成算法问题。5.2 内存带宽为什么比 CPU/GPU 更早成为瓶颈多路视频硬解码非常吃内存带宽。4 路 1080p 解码后每帧 NV12 数据大约 3MB按 25 帧算每秒就是 300MB 的数据吞吐这还没算模型输入缩放、输出后处理以及图形叠加的拷贝。当调度器把所有业务都跑起来后我发现 CPU 占用并不高NPU 也没跑满但视频硬解码偶尔会丢帧问题就出在内存带宽被抢了。解决思路是把“不必要的数据搬运”砍掉。帧池里保留 NV12 原始帧模型前处理直接在帧数据上做缩放和归一化不要先把 NV12 转成 RGB 再缩放一遍所有业务的后处理输出只保留小尺寸的结果图比如 320x320不要为了画框而保存整帧。另外帧池深度控制在 3 以内避免解码线程预解码过多帧把内存占住。5.3 典型问题速查表现象可能原因排查命令 / 手段解决方案多路视频画面周期性卡顿内存带宽饱和htop看 CPU 不高但硬解线程丢帧压缩帧池深度减少格式转换NPU 推理越来越慢芯片温度过高导致降频cat /sys/class/devfreq/fdab0000.npu/cur_freq观察频率提前开启 PWM 风扇降非关键业务权重同模型多线程调用输出错乱共享同一 RKNN context 并发调用日志中出现rknn_run异常改为单工作线程 推理队列某业务长期拿不到推理机会优先级或权重设置失衡打印每个任务的执行次数和等待时长引入最小执行时间片用加权轮询替代完全抢占拉流一段时间后 RTSP 断流网络抖动或解码线程阻塞ping网关看延迟dmesg看驱动错误拉流线程加自动重连解码和推理彻底解耦内存持续增长最终 OOM帧池没有正确释放/引用计数错误free -m持续监控进程 VmRSS检查帧借用和归还路径设置最大帧数5.4 调优后的一组参考数据在我这套 4 路海康 IPC YOLOv5s 同源多任务的工程里调优前后的对比比较明显。最开始各业务独立处理时4 路视频平均只有 12~15 帧NPU 使用率忽高忽低CPU 经常冲到 80%内存 8GB 被吃掉 6.5GB。改成统一拉流、共享帧池、单模型推理队列和加权轮询调度之后4 路视频稳定在 22~25 帧NPU 使用率平缓地维持在 80% 左右CPU 降到 40% 附近内存占用控制在 4GB 上下。这些数字不同项目之间会有差异但资源利用率提升的方向是一致的。写在最后的一点经验同源多任务调度这件事做完之后回头看真正重要的不是调度算法写得有多花哨而是数据通路和资源边界从一开始就定了规矩。先保证同一路视频只解一次码同一个模型只加载一份再谈调度策略。调度器的代码可以很简单一个队列加一个工作线程就能解决 80% 的问题剩下的 20% 靠的是在对 NPU、VPU、内存带宽有了充分了解之后做的清醒取舍。最后分享两个小技巧。第一所有业务的推理请求一定要收敛到同一个调度器入口后续要加新业务时只需注册一个任务类型和权重参数框架不用动。第二日志里一定要记录每个任务从入队到出队的等待时间这个指标比帧率更能反映调度健康状况。排查问题时先看等待时间再去看 NPU 频率和内存占用基本能快速定位到瓶颈环节。