做到方案二的双路视觉已经是我在香橙派RK3588上折腾yolov5s的第四轮了。前面三个阶段采集、模型转换、双路推理全都串通了主路识别和副路检测都能出结果但稍微跑久一点就会发现不对劲——两路摄像头一旦同时满载帧率就开始互相打架。主路从稳定的30帧掉到15帧副路后排队的图像越积越多后台抓日志和SSH操作也卡得厉害。这个阶段的核心任务就一个给双路视觉链路建立一套可落地的QoS机制而且用的是严格优先。这篇文章不会跟你聊“为什么QoS很抽象”这种话题我会直接把我调通的细节、踩过的坑、改过的配置全部摊开。适合已经跑通双路推理、正要处理资源打架问题的人阅读同时也给刚接触RK3588的人一条完整的参考路径从模型量化、线程绑核到tc网络调度、NPU推理排队一层一层把优先级理顺。1. 双路视觉方案二的整体设计与阶段四的目标既然已经走到阶段四有必要先把方案二的整体思路捋一遍。所谓“方案二”是相对于一开始的单路方案说的——单路把所有算力都压在一路摄像头上双路则要同时处理两路画面一套yolov5s模型在同一个芯片上跑两份推理任务输出独立的检测结果。方案二的优点是可以一板两用主路用来做前向检测副路做侧向或环视感知硬件的利用率高。缺点也非常明显RK3588就算有6 TOPS的NPU资源也不是无限可分两路同时跑必定有竞争。阶段四要解决的就是竞争下的确定性问题了哪一路检测结果先出哪一路的画面优先被处理哪一路的帧率可以被牺牲这些都得在系统层面定下来。严格优先Strict Priority在这里的含义不是“两路都很快”而是“关键路径上的任务永远先执行直到它不再需要资源为止”。1.1 方案二的前三阶段跑了什么把这个阶段放在前面说是因为阶段四不是凭空冒出来的功能它是前三个阶段留下的“历史债”。阶段一做的事是环境适配。香橙派RK3588这套板子跑的是Linux系统我用的Debian系镜像先解决的是NPU驱动、RKNN-Toolkit2库、多路MIPI/USB摄像头接入这些底层问题。阶段二做的是yolov5s模型到RKNN格式的转换包括只保留必要的输出层、量化成INT8、在PC上验证推理精度。阶段三做的是双路视频流的软件架构两个采集线程、两个推理线程、两套后处理和显示逻辑最终在屏幕上同时看到两个摄像头的检测框叠加画面。前三个阶段搞定之后功能没问题性能的问题就暴露出来了。双路同时推理时RK3588的CPU大核、NPU算力、内存带宽、甚至网卡出口的收发带宽都会争抢。阶段四的QoS严格优先就是把“谁该抢、谁该让”的规则硬化下来。1.2 为什么双路视觉必须引入QoS有人会问既然NPU有6 TOPS跑两个yolov5s按理说绰绰有余为什么要做调度实际跑起来你就知道瓶颈往往不在NPU本身的峰值算力而在“访问路径”。yolov5s单路模型量化成INT8之后一次推理大概只要十几毫秒看起来很快但如果两路摄像头同时上来采集线程、ISP处理、RGA缩放、模型推理、后处理NMS这些任务全都在竞争CPU核心和内存控制器资源。而且yolov5s里含有不少小算子比如上采样、拼接、卷积后的激活操作这些算子在NPU上不是全都能算的一部分要回落到CPU上CPU大核一旦被采集线程占满推理延迟立刻升高。没有QoS的时候系统调度器按默认策略分配资源谁先到谁先用看起来很“公平”但对视觉系统来说公平往往意味着关键路径失控。我们需要的不是公平是“主路永远不被副路拖垮”。这就是严格优先的核心动机。2. QoS严格优先先搞清楚给谁优先、优先什么进入实操之前先把任务的优先级梳理清楚。很多教程上来就贴tc命令说你这样配就对了。但那样配完你根本不知道自己在给谁优先出了问题也没法改。我建议你先画一张实时链路图把两路摄像头的数据流标出来然后逐段打优先级标签。2.1 双路视觉链路中的资源瓶颈拆解以我的实际环境为例主路摄像头是前向视角负责障碍物检测检测结果要实时反馈给上层控制逻辑副路摄像头是侧向视角负责环境记录检测结果可以容忍几百毫秒的延迟。两条链路可以拆成下面几个资源点摄像头采集主路的采集线程和副路采集线程都会触发DMA传输抢占内存带宽。ISP/RGA处理图片缩放、格式转换、去畸变在RK3588上通常走RGA硬件但调度的优先级是可配置的。NPU推理两路共用同一个NPURKNN会话之间需要排队谁先提交谁先执行。CPU后处理NMS、坐标映射、绘制检测框这里全是CPU计算最容易互相干扰。结果显示/推流如果要把检测结果通过网络传给上位机网卡队列也会冲突。注意这里的QoS不仅仅是“网络QoS”。很多开发者一提QoS就想到tc命令控制带宽但在双路视觉里QoS是一个系统级的概念——它需要覆盖CPU调度、NPU会话调度、内存带宽分配、网络队列调度四个层面。你只有把每一层的优先级都设定成同一个方向严格优先才算真正落地。2.2 严格优先和加权轮询怎么选QoS调度里最常见的两种模式是严格优先Strict Priority / Priority queuing和加权轮询WRR / Weighted Fair Queuing。它们的区别决定这套方案的走向严格优先的逻辑是只要高优先级队列里有任务低优先级队列就必须等待。低优先级相当于“可运行但随时被抢占”。优点是关键路径时延极低且可预测缺点是高优先级持续繁忙时低优先级可能一直得不到执行——这就是所谓的“饿死”。加权轮询的逻辑是按照权重在多个队列之间轮流执行高优先级获得更多时间片但低优先级总能分到一部分带宽。好处是公平坏处是延迟抖动不可控关键路径上可能出现一个完整的调度周期等待。双路视觉这种场景主路需要的是“确定性时延”而不是“平均带宽”。所以方案二选择严格优先是合理的。实现上既可以采用网络设备中常见的classful qdisc也可以在Linux线程调度里使用SCHED_FIFO实时优先级这两者在RK3588的Linux系统里都支持得很不错。3. 在RK3588上实现QoS严格优先的完整步骤这一节是文章的重头戏。我按系统调度、NPU会话调度、网络队列三个层次来写每一个层次都有可以直接照抄的配置方法。说明一下以下配置基于我的实际环境你的板子系统版本、内核配置可能有差异但原理和步骤是通用的。3.1 第一层系统CPU调度与线程优先级隔离首先是CPU层面的严格优先。RK3588是8核架构4个Cortex-A76大核加4个Cortex-A55小核。A76处理能力明显强于A55在做任务分配时理想状态是把主路推理线程钉在A76大核上把副路和后台任务赶到A55小核上物理隔离互不抢占。我用的辅助工具是taskset和chrt。taskset用来设定CPU亲和性chrt用来修改实时调度策略。启动主路推理进程时这样操作# 查看哪个进程是主路推理线程 ps -eLo pid,tid,comm,psr | grep infer_main # 将pid为12345的主路推理线程锁定到CPU4-5A76核 taskset -pc 4,5 12345 # 副路线程锁定到CPU0-1A55核 taskset -pc 0,1 23456仅仅绑核还不够CPU调度器还会在同一个核上切换线程所以要用chrt把主路线程的调度策略改成SCHED_FIFO并给一个较高的实时优先级。SCHED_FIFO是真正的严格优先一旦主路线程就绪它会立即抢占同核上所有普通调度线程。# 查看当前调度策略 chrt -p 12345 # 将主路线程设置成SCHED_FIFO实时优先级85 chrt -f -p 85 12345这里需要特别提醒实时优先级不是越高越好。RK3588上如果多个线程都设置成SCHED_FIFO同优先级的FIFO线程之间不会互相抢占这会导致后到的推理请求排队时间过长。我实操下来主路给85、副路给50比较合适主路被副路抢断的概率几乎为零而副路也不至于完全饿死。如果你不想每次启动都手工敲命令可以直接在代码里用pthread API#include sched.h struct sched_param param; param.sched_priority 85; pthread_setschedparam(thread_handle, SCHED_FIFO, param);这种方式的优点是不依赖外部工具应用启动时自动完成配置。再配合一个systemd service文件在服务启动时给进程授予实时调度权限整个系统重启后也能自动生效。还有一个容易被忽略的系统参数/proc/sys/kernel/sched_rt_runtime_us。Linux内核默认会限制RT调度占用的总CPU时间通常是95%如果主路推理线程占用的CPU时间超限内核会产生节流导致明明设置了FIFO却还是出现卡顿。做严格优先部署时需要把这个限制放宽echo -1 /proc/sys/kernel/sched_rt_runtime_us这一条我调试时查了很久最终确定是它在偷偷限制RT线程的CPU时间。系统日志里看不到任何报错但主路推理延迟就是不稳定时好时坏罪魁祸首就是RT预算被消耗完毕。3.2 第二层NPU会话调度与推理请求排队管理CPU层面的严格优先只能解决CPU资源分配但双路推理拼的是NPU资源。RK3588的NPU虽然有三个核心但驱动层面的调度仍然遵循“先到先服务”——谁先调用rknn_run谁先占用NPU。三路会话同时提交时内部的调度行为对应用层是不可见的因此需要应用实现自己的“请求优先级队列”。我的做法是在应用层实现一个简单的优先级队列主路推理请求进入高优先级队列副路推理请求进入低优先级队列。一个调度线程负责从高优先级队列取请求提交给NPU如果高优先级队列为空再处理低优先级队列。伪代码如下import queue import threading high_queue queue.Queue() low_queue queue.Queue() def npu_scheduler(): while True: if not high_queue.empty(): req high_queue.get() elif not low_queue.empty(): req low_queue.get() else: time.sleep(0.001) continue run_inference(req)严格来说这个调度器是“非抢占式严格优先”——一旦副路推理已经开始执行主路请求需要等待本次推理结束。要对到毫秒级别需要进一步做时间片切分把单次推理请求切分为多个子任务每个子任务执行一段固定时间后检查高优先级队列。不过我实测下来在yolov5s这种单次推理十几毫秒的场景下非抢占式队列的时延已经足够可控不必过度设计。关于双路推理的提交时机我还做过一个优化把主路的预推理处理图像归一化、色彩通道调整提前到采集线程里完成。这样主路一旦拿到NPU占用权不需要再做数据预处理可以节省大约3-5毫秒。另外使用RKNN-Toolkit2的API时建议为两路各建一个独立的RKNN上下文不要共用同一个上下文。我在一开始共用上下文的时候遇到了主路图像进入副路推理缓冲区的串数据问题排查了很久才确认是上下文状态干扰。独立上下文虽然多占一点内存但双路隔离干净调试也方便。3.3 第三层网络出口QoS与结果推流优先控制双路视觉系统如果只显示在本机屏幕上网络层面的QoS可以不配。但如果你的系统像我的应用一样需要把主路检测结果实时推给上位机副路画面只做低优先级留存那么网络出口就必须做严格优先。Linux流量控制的经典做法是使用classful qdisc先用prio或htb建立多个优先级class再通过iptables在mangle表里给不同流打上分类标记最后由filter把对应标记丢进不同class。严格优先模式下使用htb并配合prio参数是最常见的方案# 创建根句柄 tc qdisc add dev eth0 root handle 1: htb default 20 # 创建两个class高优先级带宽保证80M低优先级带宽保证20M tc class add dev eth0 parent 1: classid 1:10 htb rate 80mbit prio 0 tc class add dev eth0 parent 1: classid 1:20 htb rate 20mbit prio 1 # 设置filter匹配DSCP标记为EF(46)的包进入高优先级队列 tc filter add dev eth0 parent 1: protocol ip prio 1 handle 0x2e fw flowid 1:10注意到这里的关键是htb的prio参数prio 0表示严格优先执行该class的流量只有当1:10的流量没有超过rate限制时低优先级class才会被服务。这就是“严格优先”它不是简单的带宽切分而是高优先级class占绝对主导地位。~/ 再用iptables把主路推流的UDP端口打上fwmark也就是在mangle表的POSTROUTING链上把目的端口8300的包标记为0x2e让上边的filter能够识别:iptables -t mangle -A POSTROUTING -p udp --dport 8300 -j MARK --set-mark 0x2e这套配置是标准的tc网络QoS方法在嵌入式Linux上一样生效。我唯一踩过的坑是RK3588板端网卡的队列把htb的处理结果又分发了一次导致优先级效果不明显。后来我在板卡的网络驱动里确认了网卡本身只有单队列才排除了这个隐患。4. 双路视觉下yolov5s的模型适配与推理链路优化QoS调度只是把资源的分配规则定下来了但yolov5s模型本身在RK3588上要怎么适配双路场景同样有讲究。这里单独拿出来讲是因为模型部署层面如果做得不好就算QoS调度再严格推理时延也降不下来。4.1 yolov5s轻量化处理与RKNN转换细节yolov5s这个模型原始权重是FP32的直接转成RKNN格式能跑但推理效率不高。要发挥RK3588 NPU的INT8算力必须做量化。量化后的模型体积可以从原来的几十MB降到十几MB左右推理速度明显提升。用RKNN-Toolkit2转换时量化数据集的选取非常关键。我建议不要随便抓几张图就量化而是从实际应用场景里截取覆盖不同光照、不同角度、不同目标的图像至少准备几百张跑一次完整的量化校准。理由很朴素量化校准集决定了激活值浮点范围的统计范围没取准低比特量化误差会非常大。转换过程中还有两个容易被忽略的选项。第一个是目标平台要明确指定为rk3588让工具链针对RK3588的NPU指令做优化。第二个是开启模型的后训练优化让工具自动把某些不适合NPU的算子替换成更高效的实现方式。这些选项都是在RKNN-Toolkit2里通过config函数配置的。from rknn.api import RKNN rknn RKNN(verboseTrue) rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588 ) rknn.load_onnx(modelyolov5s.onnx) rknn.build(do_quantizationTrue, datasetdataset.txt) rknn.export_rknn(yolov5s.rknn)4.2 双路摄像头取流的帧率均衡策略有了模型下一件事是让两路摄像头稳定取流。双路视觉的帧率不均衡是很多人的老大难问题QoS严格优先正好可以通过帧分配策略来解决。我使用的方案是分别给两路摄像头开独立的v4l2设备节点主路设置为640x48060FPS副路设置为1280x72030FPS。你可能会奇怪为什么副路分辨率反而更高因为副路的应用是留存画面更看重分辨率不看重帧率主路需要高帧率来降低运动模糊所以分辨率低一点反而合理。这也是QoS分配思想在取流层面上的体现——不是平均分配资源而是按业务价值分配。取流线程使用内存映射mmap方式读取缓冲区并设置足够的缓冲区数量。我主路设置了6个缓冲区副路设置了4个缓冲区。缓冲区太少了容易丢帧太多了又会增加延迟这个比例需要根据实际帧率调整。主路因为是关键路径宁可多占内存也要保证不丢帧。这一步本身没有任何特殊之处但配合后面的“帧入场排队”就能让两路取流的优先级固化下来。4.3 帧同步与推理请求路由的实现双路视觉很难避免一个问题主路的推理已经完成但副路因为处理慢导致主路的下一次推理被迫等待同步。所以双路不能做全局帧同步帧同步的边界必须清晰——只有主路内部需要同步采集帧与推理结果一致副路完全异步运行。我在代码里用了一个帧路由表来记录双路帧的状态主路帧进入高优先级推理队列副路帧进入低优先级队列。调度线程每轮从高队列采样一帧立即提交推理如果高队列为空才从低队列取帧。这样“高优先级队列”就等同于严格优先里最高优先级的那个band。为了避免低队列长期饥饿我还增加了一个防饿死机制如果低队列的积压帧数超过30帧调度线程会主动从低队列中抽出一帧处理即使此时高队列有少量积压。这个机制是对纯严格优先的一种修正适合生产环境的实际需求。副路虽然相对次要但也不能让它完全卡死。5. 常见问题与调试排查实录把方案完整跑通是一回事能让它稳定持续运行是另一回事。第四阶段我实际踩的坑比前面三个阶段加起来都多。下面是几条最有价值的排查记录对你大概率会有帮助。5.1 QoS配置不生效的典型原因网络上配了tc严格优先之后我用iperf3压测主路UDP推流和副路TCP回传发现两个流的时延几乎没有差别说明严格优先没起作用。排查过程是这样的先用tc -s -d qdisc show dev eth0查看队列统计发现高优先级class的发送包计数变化很小而default class的包一直在走。问题就出在iptables的MARK动作没有和fw classifier对上。iptables打上的fwmark是0x2e但tc filter用的是handle标记二者必须一一对应。我最初把handle写成了0x2e其实应该是0x46才对因为fw classifier匹配的是mark值不是DSCP值。修正后效果立刻出来。另一个常见原因是iptables规则加在PREROUTING链上出方向流量根本不会经过它。一定要加在POSTROUTING或者OUTPUT链。5.2 两路推理在NPU上的“绕道”现象双路推理跑起来之后我观察到主路帧率偶尔会抖动一下副路帧率则出现周期性的停滞。抓了线程状态之后发现主路线程在调用rknn_run的时候经常被调度器切出去导致实际推理的总时延被拉长。这其实是RKNN底层驱动的线程做了一次内部同步NPU推理结束后需要唤醒等待线程这个唤醒动作在Android/Linux系统里会走一次内核调度队列如果CPU负载高唤醒延迟会变大。即便我主路线程设置了高优先级等待唤醒期间它也是睡眠状态无法强制CPU立刻处理唤醒。最终解决方法是把主路推理线程和采集线程同时绑定到同一对A76核心上这样唤醒延迟能降低不少。原因是同一个核心上的线程唤醒不涉及跨核缓存同步内核唤醒路径缩短了。实测下来P99延迟从原先的50毫秒降到了32毫秒左右。5.3 线程锁导致的隐式优先级反转优先级反转这个词很多人只在操作系统课里见过真到双路视觉系统里就会遇到。我的应用里主路线程和显示线程共用同一个图像缓存队列主路往队列里写结果显示逻辑从中取数据画框。正常情况下没问题但副路后处理偶尔会占用同一个显示队列的锁主路拿不到锁只能阻塞等待。严格优先调度遇到这种情况就是典型的优先级反转高优先级任务等待低优先级任务持有的锁反而被低优先级阻塞。处理方案有两个一个是给显示队列加多个缓冲区尽最大可能避免竞争另一个是使用可冲入锁配合超时机制主路等待锁超过2毫秒就直接放弃绘制缓冲保证推理线程不被卡死。这种问题在压测时特别容易暴露单路跑时几乎不可能出现双路共同访问共享资源后各种隐式竞争就都冒出来了。5.4 严格优先影响下的副路饿死处理前面提到防饿死机制单独拿出来详细说明一下。最长的一次压测跑了大概40分钟副路检测框的显示直接冻结进程还在但画面完全不更新。我下了一个结论副路的帧队列满了而调度线程一直优先处理主路请求副路推理线程拿不到NPU执行机会。严格优先的代价就是低优先级在重负载下可能完全得不到服务。我的对策不是把调度策略改回WRR而是在调度器层面增加一个“低压保护条件”低队列积压帧数超过阈值时把副路的推理请求优先级临时提升一级插入高优先级队列的尾部。这保留严格优先的主干逻辑同时避免了完全饿死的极端情况。生产系统中这种修正很有价值。6. 双路QoS严格优先的实测参考数值理论说得再多不如看一组实测数据。以下是我在一台香橙派5系列板子上使用相同模型、相同场景、相同负载条件下采集的指标对比未开QoS与严格优先开启后的表现。指标默认调度QoS严格优先主路平均端到端延迟45ms26ms主路P99延迟82ms37ms副路平均端到端延迟41ms68ms主路掉帧率万帧1382副路掉帧率万帧203486CPU0-3平均负载61%83%CPU4-7平均负载74%49%一眼就能看出严格优先之后主路体验获得了质的提升代价是副路部分帧被主动丢弃。这正是方案二要的效果资源总量有限那就把确定性给关键业务。测试时有一个需要注意的点端到端延迟要打上时间戳不要用简单的FPS平均值作为唯一指标。FPS平均值掩盖了抖动P99才是判断“严格优先是否生效”的核心指标。我每次调整配置都会同时盯着P99和掉帧率看前者反映调度质量后者反映资源分配偏向。7. 写在最后的阶段总结方案二的阶段四做完了但严格优先这件事并没有结束。实际使用中我还有一个感受QoS配置是一个持续修正的过程不是把一组命令跑完就能一劳永逸。实时系统的负载特征会随场景变化比如夜间画面的像素分布不一样副路的检测任务复杂度也不一样优先级参数需要动态调整。我的做法是在应用层预留一个配置接口通过配置文件改线程优先级、CPU亲和性、队列阈值改完不用重启热加载即可生效。最后一个建议如果你也准备在RK3588上做双路甚至多路视觉一定要把QoS管理模块从一开始就设计进去别像我一样跑通了功能才回来补资源调度的课。严格优先的底层逻辑很简单但真正跑顺需要你对系统里每一处资源竞争点做到心里有数。把链路图画出来、把优先级标上去、把每个队列的行为测清楚你也能做出稳定可靠的双路视觉方案。