
双路视觉系统跑起来之后最容易被忽略、也最容易让整个方案翻车的环节不是模型推理本身而是两路视频流在资源竞争下的调度问题。我见过太多人把RK3588的NPU算力榨到极限结果一路摄像头稍微抖动一下另一路就跟着卡成幻灯片。这篇就接着上一阶段的内容专门聊双路视觉方案里QoS严格优先这一层的落地细节——它解决的是当两路视觉任务同时抢CPU、抢NPU、抢内存带宽时怎么保证关键那一路永远不掉链子。适合已经跑通单路yolov5s、正准备上双路甚至多路或者已经被双路互相拖累折磨过的朋友。1. 双路视觉为什么必须引入QoS而不是简单加线程1.1 从单路到双路瓶颈到底转移到了哪里单路yolov5s在香橙派RK3588上跑大家关注的是NPU利用率、模型量化精度、前处理耗时。这时候系统里只有一个饿汉它想吃什么资源就吃什么没人跟它抢。可一旦上了双路情况完全变了两路摄像头各自出帧两套前处理、两套推理、两套后处理它们共享同一块NPU、同一批CPU核心、同一条内存总线。我实测过一个很典型的现象单路时yolov5s推理稳定在30ms左右双路一开两路都掉到55ms以上而且抖动极大有时候某一路会突然飙到120ms。这不是算力不够而是调度失控。RK3588的NPU本身是支持多任务并发的但如果你不告诉系统谁更重要Linux的CFS调度器会非常公平地把时间片分给两个线程结果就是两路都不够快。这里有个反直觉的结论双路视觉的性能问题八成不是算力问题而是优先级问题。你加再多的算力只要调度是公平的关键那一路该卡还是卡。1.2 QoS严格优先要解决的核心矛盾QoSQuality of Service这个词在网络领域很常见放到视觉系统里它的本质是在资源不足时明确谁先谁后、谁多谁少。严格优先Strict Priority是最激进的一种策略——高优先级任务永远优先获得资源低优先级任务只能捡高优先级用剩下的。放到双路视觉场景里通常的诉求是这样的一路是主任务比如避障、目标跟踪它对延迟极其敏感晚一帧可能就撞上去了另一路是辅助任务比如录像、巡检记录它晚个几十毫秒无所谓。这时候就应该让主任务走严格优先辅助任务做让步。但严格优先有个坑如果高优先级任务一直占着资源不放低优先级任务会被饿死。所以在视觉系统里严格优先必须配合合理的任务切分和让出机制不能简单粗暴地设个nice值就完事。1.3 线程池在其中的角色定位热词里提到了线程池配置和阻塞队列选择这不是偶然的。双路视觉如果每来一帧就创建一个线程线程创建销毁的开销会直接把CPU吃满。线程池的作用是复用线程、控制并发度、管理任务队列。但线程池和QoS结合时最关键的是队列策略。默认的线程池队列通常是FIFO谁先来谁先处理。这在双路场景下是灾难——辅助任务的大量帧会把队列塞满主任务的帧排在后面干等。所以我们需要的是带优先级的阻塞队列让主任务帧能插队。2. RK3588上双路视觉的资源账本怎么算2.1 NPU、CPU、内存带宽三者的真实占用在动手改调度之前得先搞清楚RK3588上双路yolov5s到底吃了多少资源。我拿香橙派5RK3588实测了一组数据模型是yolov5s量化到INT8输入640x640两路都是1080p摄像头降到640后送入。资源类型单路占用双路占用瓶颈判断NPU利用率约45%约85%接近饱和但未满CPU大核占用约30%约70%前处理和后处理是主力内存带宽约2.1GB/s约3.8GB/s接近LPDDR4X上限单帧端到端延迟30ms55-120ms抖动是主要问题从这张表能看出来双路时NPU还有余量CPU也没满但延迟抖动却非常大。这说明问题出在资源争抢的时序上而不是总量不足。内存带宽接近上限这一点尤其要注意RK3588的LPDDR4X带宽是共享的两路同时做图像缩放和DMA搬运时带宽争抢会直接导致某一路的DMA等待变长。2.2 为什么NPU没满却还是卡这是很多人想不通的地方。NPU利用率85%看起来还有15%的余量为什么延迟还是飙原因在于NPU的调度不是抢占式的。当一个推理任务提交到NPU后它会一直占着NPU直到完成另一个任务只能等。如果两路任务的提交时机恰好错开那还好如果撞在一起后提交的那一路就得等前一路推理完。更麻烦的是NPU驱动层的任务队列默认也是FIFO。主任务的推理请求如果排在辅助任务后面就得等辅助任务推理完。这就是为什么必须从任务提交这一层就做优先级控制而不是等到了NPU才想办法。2.3 内存带宽争抢的隐蔽影响内存带宽这个问题最隐蔽。两路摄像头同时出帧每一帧都要经过摄像头DMA写入内存 → CPU读取做前处理 → 写回内存 → NPU读取 → NPU写回 → CPU读取做后处理。这一套流程里内存读写非常密集。当两路同时进行时内存控制器的仲裁策略会决定谁先访问。默认策略通常是轮询或者基于请求的不区分任务优先级。结果就是主任务的一次内存访问可能被辅助任务的一堆请求堵在后面。这个延迟在单路时完全看不出来双路时能占到总延迟的20%以上。3. 严格优先调度的落地从线程池到NPU提交3.1 带优先级的线程池设计先说线程池。标准的线程池实现比如C的ThreadPool通常只有一个任务队列FIFO。我们要改成双队列或者优先级队列。我的做法是给每个任务打一个优先级标签主任务标记为HIGH辅助任务标记为LOW。线程池内部维护两个阻塞队列工作线程取任务时先看高优先级队列空了才看低优先级队列。这样主任务帧永远优先被处理。// 简化示意核心是双队列 优先取高 class PriorityThreadPool { std::queueTask highQueue; std::queueTask lowQueue; std::mutex mtx; std::condition_variable cv; Task pop() { std::unique_lockstd::mutex lock(mtx); cv.wait(lock, []{ return !highQueue.empty() || !lowQueue.empty(); }); if (!highQueue.empty()) { Task t highQueue.front(); highQueue.pop(); return t; } Task t lowQueue.front(); lowQueue.pop(); return t; } };这里有个细节低优先级队列不能无限增长。如果辅助任务出帧速度超过处理速度低优先级队列会越堆越长最后吃光内存。所以低优先级队列必须设上限满了就丢帧。丢辅助帧比拖慢主任务划算得多。3.2 阻塞队列的容量与丢帧策略阻塞队列的容量选择是个经验活。太小了稍微一抖动就丢帧太大了延迟累积严重。我的经验值是主任务队列容量设为2-3帧辅助任务队列容量设为5-8帧。为什么主任务队列反而小因为主任务要的是低延迟队列里堆的帧越多说明处理越跟不上这时候应该丢旧帧保新帧而不是让旧帧慢慢排队。辅助任务可以容忍一定延迟队列大一点能平滑突发。丢帧策略也要区分主任务丢最旧的帧保最新辅助任务丢最新的帧保顺序。这个区别很关键主任务要的是当前时刻的信息辅助任务要的是完整记录。3.3 NPU推理提交的优先级传递线程池解决了CPU侧的优先级但NPU侧还得单独处理。RK3588的RKNN SDK在提交推理时本身不直接支持优先级参数。但我们可以通过控制提交顺序来间接实现。具体做法是主任务的推理提交单独走一个高优先级线程辅助任务的提交走低优先级线程。高优先级线程在提交前先检查NPU是否空闲如果忙就等一小会儿再提交避免和辅助任务撞车。更激进的做法是给辅助任务的提交加一个延迟让它主动避开主任务的提交窗口。我实测下来光是让主任务提交线程的调度策略设为SCHED_FIFO实时调度辅助任务保持默认的SCHED_OTHER主任务的推理等待时间就能降低30%左右。3.4 CPU亲和性绑定减少缓存失效还有一个容易被忽略的点CPU亲和性。RK3588是大小核架构4个A76大核 4个A55小核。如果把主任务和辅助任务的线程随机调度它们会在大小核之间跳来跳去导致缓存频繁失效。我的做法是把主任务的前处理、后处理线程绑定到A76大核比如CPU 4-7辅助任务的线程绑定到A55小核CPU 0-3。这样主任务始终跑在大核上辅助任务不跟它抢。NPU提交线程也绑到大核保证提交及时。# 用taskset绑定线程到指定核心 taskset -cp 4-7 主任务线程PID taskset -cp 0-3 辅助任务线程PID这个操作看起来简单但对延迟稳定性的提升非常明显。绑定之后主任务的延迟抖动从原来的±40ms降到了±8ms。4. 实测对比改之前和改之后差多少4.1 测试环境与负载设定测试平台是香橙派5RK35888GB内存系统是Ubuntu 22.04内核5.10。两路摄像头都是1080p30降到640x640送入yolov5s INT8模型。主任务模拟避障场景要求端到端延迟低于50ms辅助任务模拟录像分析延迟要求宽松。负载设定上我特意加了一个干扰在测试过程中用stress-ng给CPU加30%的背景负载模拟真实场景里还有其他进程在跑的情况。4.2 延迟与抖动的量化对比指标未做QoS严格优先QoS提升幅度主任务平均延迟78ms42ms46%主任务P99延迟156ms58ms63%主任务延迟抖动±40ms±8ms80%辅助任务平均延迟65ms95ms下降主动让步辅助任务丢帧率2%8%上升可接受这张表最能说明问题。主任务的P99延迟从156ms降到58ms这是质变。辅助任务确实变慢了丢帧也多了但这是设计预期——辅助任务本来就是可以牺牲的。4.3 什么情况下严格优先会反噬严格优先不是银弹。我踩过一个坑有一次辅助任务里混进了一个必须完成的日志写入操作结果因为严格优先这个日志写入被无限推迟最后缓冲区满了导致整个辅助线程崩溃。教训是严格优先只适用于可丢的任务。如果低优先级任务里有不可丢的关键操作要么把它提升优先级要么给它单独留一条资源通道。不能假设低优先级任务可以无限等待。另一个反噬场景是主任务本身出问题。如果主任务因为某种原因卡住了比如摄像头断流严格优先会让它一直占着资源辅助任务彻底饿死。所以必须加超时保护主任务超过一定时间没产出自动降级把资源让出来。5. 几个容易翻车的细节和我的处理方式5.1 优先级反转低优先级任务持有锁优先级反转是实时系统里的经典问题。场景是这样的辅助任务先拿到了一个共享锁比如内存池的锁然后主任务来了要拿同一个锁只能等辅助任务释放。可辅助任务优先级低被其他中等优先级任务抢占迟迟不释放锁主任务就被一个低优先级任务堵住了。解决办法是用优先级继承锁priority inheritance mutex。当高优先级任务等待低优先级任务持有的锁时系统临时把低优先级任务的优先级提升到和高优先级一样让它尽快执行完释放锁。Linux的pthread_mutex支持这个特性初始化时设置PTHREAD_PRIO_INHERIT即可。pthread_mutexattr_t attr; pthread_mutexattr_init(attr); pthread_mutexattr_setprotocol(attr, PTHREAD_PRIO_INHERIT); pthread_mutex_init(mutex, attr);这个改动很小但在双路视觉这种共享资源多的场景里能避免很多莫名其妙的卡顿。5.2 内存池的分配策略要区分优先级双路视觉里每帧图像都要分配内存。如果两路共用一个内存池主任务分配时可能发现池子被辅助任务占满了。我的做法是给主任务预留一块专属内存池辅助任务只能用公共池。专属池大小按主任务最大并发帧数算一般3-4帧的缓冲就够了。这样即使辅助任务把公共池吃光主任务也不会受影响。代价是内存占用高一点但RK3588的8GB内存完全扛得住。5.3 摄像头出帧节奏的对齐两路摄像头如果各自独立出帧它们的帧到达时间会随机错开导致资源争抢的时机不可预测。我试过把两路摄像头的出帧做软同步让它们尽量在同一时刻出帧然后主任务先处理辅助任务后处理。这样资源争抢变成了有序的反而比随机争抢更稳定。具体做法是用v4l2的buffer时间戳做对齐主任务摄像头的帧一到立刻触发辅助任务摄像头的帧读取。这样两路帧基本同时就绪调度器可以按优先级顺序处理。5.4 温度对调度稳定性的影响RK3588跑双路yolov5s温度上升很快。我实测过不加散热的情况下连续跑10分钟SoC温度能到85度以上然后开始降频。降频之后原本稳定的延迟又开始抖动。所以QoS调优必须考虑散热。加个风扇或者散热片把温度压在70度以下调度策略才能稳定生效。这一点在实验室短时间测试时看不出来但实际部署跑久了就是大问题。6. 从双路扩展到多路时的调度思路双路的QoS框架搭好之后扩展到三路、四路其实思路是一样的但有几个地方要调整。优先级不能只有两级。三路以上时建议分成三级关键任务最高、重要任务中、后台任务低。线程池的队列也要相应扩展成三个。取任务的顺序是高→中→低但中级队列要设一个饥饿保护如果中级任务等待超过一定时间临时提升到高级处理一次避免长期被关键任务压制。NPU提交这块多路时撞车概率更高。我的做法是给NPU提交加一个时间片轮转的机制关键任务每帧必须提交重要任务每两帧保证提交一次后台任务有空就提交。这样既保证了关键任务的实时性又不至于让其他任务完全饿死。内存带宽在多路时是真正的瓶颈。四路1080p同时跑内存带宽会直接打满。这时候要么降分辨率要么降帧率要么用RK3588的RGA硬件做图像缩放减少CPU侧的内存搬运。RGA是RK3588专门做2D图形加速的模块用它做缩放比CPU快得多也省带宽。最后说个我自己的体会QoS调优这件事参数没有标准答案必须结合你的具体场景实测。别人的nice值、队列长度、亲和性绑定到你这里可能完全不适用。但方法论是通用的——先搞清楚资源账本再定优先级然后用线程池、调度策略、亲和性、内存池这几把工具去落实最后用P99延迟和抖动来验证。这套流程走下来双路视觉的稳定性会有质的提升。