
我最初接RK3588边缘盒子项目时遇到的第一个棘手问题不是模型精度不够也不是算子不支持而是“摄像头一多整个流水线就卡死”。按常理说RK3588的8核CPU加6TOPS NPU跑几个视觉模型绰绰有余但实际一跑NPU占用率不高CPU却被打满帧率忽高忽低甚至出现内存暴涨。排查到最后问题全出在“多路视频任务”的调度方式上——各路人马各抢各的资源没人统一协调。这篇文章就围绕“同源多任务调度”展开聚焦一个具体场景多路视频流接入同一套AI视觉流水线每一路任务结构相同、模型相同、处理逻辑相同但数据源不同。这种“同源”特征决定了我们完全可以用一套统一的调度机制去管理它们而不是让每个任务各自为战。文章会先说明“同源多任务”的核心矛盾和它在RK3588平台上的特殊性再讲我们最终落地的三层调度架构然后把实际踩过的深坑特别是RGA缩放并发冲突完整复盘一遍最后给出关键资源冲突的实测数据和调优建议。这篇文章适合正在用RK3588做边缘视觉盒子、或者准备把多路视频AI推理从“能跑”做到“稳定跑”的开发者。如果你手里也有类似项目相信这篇内容能帮你少走不少弯路。1. 先说清楚“同源多任务”到底在解决什么问题1.1 多路视觉任务的本质看似独立实则抢同一份资源一台典型的RK3588边缘盒子接4到8路RTSP视频流每路视频都要做“解码 → 缩放 → 模型推理 → 后处理 → 结果上传”这一整套流水线。表面上看8路任务互不相干不同的摄像头、不同的画面、不同的检测结果。但在硬件层面它们抢的是同一颗NPU、同一块DDR带宽、同一组RGA缩放通道、同一片内存池。这就带来一个很尴尬的局面每个任务单独测都没问题一旦同时跑满8路资源竞争就出现了。个别任务推理延迟从15ms飙到80ms部分帧被直接丢弃CPU占用率在40%和95%之间剧烈抖动。最要命的是这种抖动是随机的、非线性的你很难通过给每个任务固定一个CPU核心或者固定一段内存来彻底解决。“同源”这个词的关键就在于既然8路任务结构完全一致那就不该给它们各自设计一套资源分配方案——那是给“异构多任务”用的思路。同源任务的最佳做法是把8个任务当作同一个“任务族”用统一的调度器去分配资源、排队执行、处理异常。1.2 RK3588平台上“同源”的特殊性RK3588不是单纯的CPU板卡它有四类计算资源参与视觉流水线CPU做解码和后处理、RGA做图像缩放与格式转换、NPU做模型推理、VPU做视频编解码本场景不涉及编码暂不展开。这四类资源在物理上是并行的——理论上CPU在跑后处理的同时NPU可以并行推理下一帧、RGA可以缩放另一帧。但实际落地时想把它们完全流水线化需要精心设计。如果做不好就是“看起来并行实际上互相等”CPU等NPU出结果、NPU等RGA喂数据、RGA等CPU释放内存。一路这样做还勉强能撑8路这样做任何一环卡住都会扩散成全系统的帧率暴跌。同源多任务调度的本质就是要把8条相互独立的“串行流水线”改造成一条“共享资源池”的并行流水线。每个阶段之间通过队列解耦各个硬件单元各干各的谁也别等谁。2. 方案选型时的两种思路固定管道 vs. 共享线程池先说结论我们最终选择了“共享线程池动态资源分配”方案而不是更简单的“每路任务固定线程”方案。这个选择直接影响后面所有代码的写法、排错的方式值得单独拿出来聊一聊。2.1 方案A每路视频固定一套线程1:1 模型最直观的做法是8路视频就开8组线程每组线程内部是“取帧→缩放→推理→后处理”各干各的。这个方案在代码层面最简单——完全不用考虑线程间同步每一路就是一条独立的串行流水线。但它在RK3588上跑起来有几个致命问题NPU是共享的8路同时调用rknn_run驱动内部会排队。这个排队时间不可控某些任务可能等了50ms才轮到推理。每路等待时间还不同导致各路帧率参差不齐。每个任务单独申请输出缓冲区8路就是8份DDR带宽压力增大。RK3588的NPU和CPU共享同一份DDR带宽带宽被抢占后CPU取帧和解码都会变慢。当一路视频断流重连时它的线程组要整体释放再重建会引起整个系统的资源抖动。实测中某一路重连时其他7路的帧率都会掉5到8帧持续几百毫秒才恢复。2.2 方案B共享线程池N阶段流水线M:N 模型我们最终采用的方案是解码线程N个 预处理线程M个 推理线程K个 后处理线程L个所有视频流共享这些线程池每个阶段通过无锁队列交接数据。这个方案的好处是任何一路视频的帧处理都不再“独占”某个线程线程利用率大幅提升。某一帧解码慢了不会阻塞其他线程里的帧。NPU调用集中在同一个推理线程池里可以对调用顺序做统一调度——紧急任务插队、普通任务按序执行。断流重连只影响“取帧”这个环节解码线程和推理线程池完全不用动资源抖动被限制在单路上。代价是代码复杂度上升。同一路视频当前帧在解码、上一帧在缩放、再上一帧在推理这种“跨阶段乱序”天然需要环形缓冲区和带帧ID的同步机制。我们后面花了很大精力处理的就是这些细节。2.3 为什么最终选了B核心原因是实测中RK3588的NPU在批处理1时的推理耗时只有15~25ms但当多路同时提交时rknn_run实际调度延迟会涨到40ms以上。这说明NPU层面的并行度是有限的“多路同时提交”反而容易造成拥堵。方案B把NPU调用收敛到一个池子里做统一排队配合优先级控制调度延迟可以稳定控制在20~30ms以内。CPU侧同理解码和预处理放到共享线程池里8路视频的解码总耗时比独立线程方案少了约22%原因是CPU核心之间的负载更均衡了不会出现某个核心空闲、另一个核心满载的情况。3. RK3588平台上“同源多任务”的四层架构设计3.1 架构总览整个调度体系分四层接入层负责多路RTSP视频流的拉取和断线重连。流水线层解码 → RGA缩放 → NPU推理 → 后处理每个阶段都有独立的线程池和任务队列。调度核心层统一管理任务优先级、NPU推理排队、资源回收。输出层各任务的处理结果统一封装上报到业务端。3.2 接入层的超时控制RTSP拉流是最容易出问题的环节。网络抖动、摄像头重启、带宽不足都会导致取帧超时。我们的做法是给每路流设置独立的超时时间默认5秒无帧判定为断流断流后自动进入指数退避重连——第一次等2秒第二次4秒第三次8秒最多60秒避免断线时疯狂重连把带宽占满。这里有一个容易被忽略的点重连后要清空该路流的旧帧缓冲。如果不清理重新拉流后前几秒会涌出一堆延迟了很久的旧帧整个流水线会被这些“僵尸帧”占满其他路的任务反而排不上队。清空操作在接入层做毫秒级开销但能避免后续所有阶段的拥堵。3.3 流水线层的线程数设计线程数不是越多越好在RK3588上尤其如此。这颗SoC是4个A76大核4个A55小核大小核调度如果做得不好线程很容易全部堆在小核上性能直接腰斩。我们最终实测下来的配置阶段线程数绑定的CPU核心说明解码含RTSP取帧2A76大核解码对单核性能敏感绑大核图像缩放RGA2不绑定主要耗时在硬件RGACPU只负责提交和等待NPU推理1A76大核NPU调用本身就是串行的单线程加排队管理最稳后处理2A55小核后处理是纯CPU计算绑小核可以省电实测速度和A76差距不大这里最关键的变量是“推理线程数”。你可能觉得NPU是异步设备多线程提交能提高吞吐。但实测数据显示3个推理线程比1个推理线程的吞吐量高不了多少但延迟抖动明显变大。原因是NPU的硬件队列深度有限多线程同时提交会导致驱动层的锁竞争加剧。最终我们稳定在“1个推理线程应用层任务队列”的模式上。3.4 调度核心层的优先级设计同源多任务的优先级设计很多人会忽略。他们觉得既然是同源就应该一视同仁地按先来后到处理。但实际项目中8路视频里往往有1到2路是关键区域监控其余是辅助视角——关键区域的轮询频率和延迟要求都更高。我们引入了“两级优先级”高优先级任务在队列里可以插队但设置了一个保护上限高优先级任务连续插队最多3次之后必须让低优先级任务出队一次防止“饿死”。这个设计在实际项目中很有用一旦关键区域出现入侵告警事件系统能立刻切换到“事件联动模式”把当前帧优先送入推理缩短告警响应时间而平时无须告警时所有路按普通轮询处理不因为高优先级任务的存在而拖垮整体吞吐。4. 踩坑全过程RGA缩放并发冲突这套系统里最坑的环节不是NPU而是RGA。RGA是Rockchip的2D图形加速硬件专门做缩放、旋转、格式转换这类操作几乎不耗CPU。听起来很完美但RK3588的RGA在并发场景下有一个大坑多路同时调用RGA、且缩放尺寸差异较大时吞吐会骤降部分帧的缩放耗时会从1~2ms暴涨到20ms以上。4.1 现象某一帧“卡住”了最初我们用8路流水线做压力测试时观察到一种奇怪的现象正常跑3小时都没事第4小时开始个别帧的RGA缩放耗时突然从2ms涨到25ms。不单单是这一帧变慢连带着后续队列里的帧全部被堵住整个流水线的帧率从25fps掉到12fps。4.2 排查链路从怀疑驱动到锁定RGA队列我的排查过程如下先是怀疑NPU推理超时。把推理和RGA拆开分别打点发现NPU耗时正常问题确在RGA。再怀疑是CPU抢占导致RGA等待。但RGA操作是硬件行为CPU只负责提交CPU负载当时不高这条也排除了。最后一步是并行提交测试写了一个最小复现程序多线程同时调用RGA做缩放发现只要并发数超过3且缩放的输出尺寸差异大于两倍就会出现“后提交的任务反而先完成”的情况同时整体整体吞吐下降。4.3 根因和最终解法RK3588的RGA单元内部有一条任务队列任务提交后由硬件顺序执行。但不同尺寸的缩放操作在硬件内部的执行时间差异极大。小尺寸缩放1ms内就能完成大尺寸可能需要10ms。当队列里积压了大量大小差异极大的任务时小任务不得不等大任务跑完——这就是“2ms变成25ms”的原因。理解了根因解法就好做了按输出尺寸对RGA任务分桶同类尺寸走同一个队列不同桶之间并行提交。这样小缩放任务不再被大任务堵住整体RGA吞吐恢复到了正常水平的90%以上。我们最终把RGA任务分成三个桶小尺寸输出320×320、中尺寸320×320~640×640、大尺寸640×640每桶一个队列两两并发实测稳定性大幅提升。如果你也在RK3588上做多路视觉缩放强烈建议提前做RGA并发压力测试不要等系统级联调时才暴露这个问题。5. 同源任务“共享”和“隔离”的边界怎么画5.1 共享的内容模型实例、线程池、内存池同源多任务最值得做的一件事就是让所有任务共享同一个模型实例。每个RKNN模型实例大约占60~120MB内存视模型大小而定8路任务如果各加载一份模型内存直接多占几百MB。共享同一份模型实例时推理阶段的rknn_run调用是线程安全的配合推理线程池的单线程提交模式完全没问题。共享线程池的好处前面已经说了核心是资源利用率提升。5.2 必须隔离的内容帧缓冲区、任务状态、队列同源任务因为运行同一套处理逻辑如果缓冲区也共享就会出现很隐蔽的数据错乱——A路视频的帧被B路的后处理给消费了。这种bug不一定会崩溃但检测结果会莫名其妙地错位排查起来极其痛苦。我们的经验是帧缓冲区按视频路数做物理隔离但内存分配走统一的内存池。比如8路视频每路分配独立的输入缓冲区和输出缓冲区内存池负责按需分配和回收避免频繁的malloc/free导致内存碎片。调度器在任务提交时在帧结构体里写入video_id和frame_id后处理阶段校验这两个ID不匹配的帧直接丢弃并告警——这是防御性编程不指望它解决多少问题但能让你快速发现调度逻辑哪里写错了。5.3 同源任务之间需要“流量控制”多路任务同时涌入时如果不加流量控制队列会无限积压导致延迟越来越大、掉帧越来越频繁。我们的做法是每一路视频检测队列积压情况超过阈值比如队列中还有30帧未处理时直接丢弃新到的帧而不是把帧塞进队列尾部。丢弃策略要用“丢新留旧”而不是“丢旧留新”。视觉任务对实时性要求高旧帧虽然“来得早”但已经过时了新帧更接近当前画面检测结果更有意义。6. 链路实测关键性能数据和优化前后的对比6.1 测试环境和压测方法测试板卡是RK3588DDR带宽足够8路视频均为1080p25fps模型是YOLOv8sCOCO 80类输入尺寸640×640。压测方式是让系统连续跑24小时记录每秒实际处理帧数、推理延迟均值与P99延迟、内存占用峰值、CPU占用率。6.2 单路、4路、8路的性能表现场景每路平均耗时ms每路推理延迟ms系统总吞吐fpsP99延迟ms单路281835224路3322120298路413019548可以看到8路满载时单路平均耗时从28ms涨到41ms仍在实时范围内每帧周期40ms边缘浮动但P99延迟已经到了48ms说明偶发情况下单路帧会迟到。对实时告警类业务来说之后我们又做了优先级插队机制高优先级任务的P99延迟降到了32ms代价是普通任务延迟略增到52ms——可接受。6.3 优化前后的CPU占用对比项目优化前固定线程1:1优化后共享线程池M:NCPU平均占用72%51%内存峰值1.7GB1.2GB小任务RGA耗时最高25ms稳定在3ms以内断流重连对其他路的影响帧率掉5~8fps帧率波动0~1fps两组数据对比下来的结论是同源任务的收益最大化途径不是优化单次推理的速度而是消除任务之间的无效资源竞争。6.4 省电与发热上的额外收益这一项是顺带发现的优化前CPU平均占用72%整板功耗大约在8W左右散热量明显优化后CPU占用降到51%功耗降到6.5W风扇噪音也低了不少。对于做商用边缘盒子的团队来说功耗和散热直接关系到产品能不能塞进小型外壳里量产这是一个很实际的好处。7. 再分享几个实际调试中最好使的经验7.1 在RK3588上尽量用CPU亲和性绑核RK3588的大小核调度不太聪明如果不主动绑核后台服务线程容易跑在A55小核上导致排队。建议启动时用pthread_setaffinity_np把关键线程绑到A76核上后处理这类非关键线程绑到A55核上。体感差异非常明显单路推理延迟能降低3到5ms。7.2 多路视频解码之后最好先做统一分辨率归一化在接入阶段就把各路视频统一缩放成模型输入尺寸后续的RGA负载会大幅降低。我们的方案是解码层直接把1080p画面缩放到640×640中间不保留1080p副本。内存占用低了很多同时也减少了RGA的工作量。若后续要接入新的摄像头比如2K甚至4K的统一分辨率的好处会体现得更明显。7.3 定期检查RK3588的温度和核心频率RK3588在高负载下如果散热不好NPU频率和CPU频率都会自动下调这会导致性能断崖式下降。但这个表现是缓慢变化的不容易在调试时发现。最后在压力测试时把cat /sys/class/thermal/thermal_zone*/temp写成定时任务每10秒记录一次温度发现在机箱散热不良时CPU核的温度会跑到82度以上性能下降比例明显。后来加强了散热性能才稳定下来。7.4 用“帧ID连续性”作为系统健康度的金标准除了延迟和吞吐我们额外记录了每个任务的帧ID连续性。在断流重连、内存抖动等异常场景中帧ID连续性比延迟更能快速反映调度是否出问题。每路视频维护一个frame_id如果发生跳号说明有丢帧如果连续丢帧超过阈值就自动切换重连逻辑。8. 后续可以继续做的扩展方向这套同源多任务的调度框架扩展到“不同模型的多任务”时只需要改两层一层是任务描述结构体里增加模型类型字段另一层是NPU推理线程池改为按模型类型做子队列。推理线程池的“单线程统一排队”模型换成“多线程按模型分队列”模型其余框架可以复用。另外RK3588在多路视频编码硬件编码上也有一套类似的资源调度问题。如果场景里用到了mpp_enc做视频推流核心资源冲突的原理、共享和隔离的边界和本文分析RGA/NPU的思路也基本一致算力有限、队列有限、内存共享谁抢到队列谁就快反而是做统一调度才能消峰。在多个项目上用过这套方法后我的感受是边缘AI设备的多路视频调度大多数时候不是模型不够快而是任务调度代码不成熟。同源任务场景里尤其如此——只要资源冲突管理不好哪怕单个模型推理只要15ms8路一起跑也能跑出50ms的延迟。希望这篇文章能帮你少踩几个坑。