RK3588双路视觉的QoS严格优先调度实战:从CPU到NPU的全面优化 到阶段四这章我琢磨着先交代一下背景因为这个系列的读者跟着做到这里应该能明显感觉到一件事双路视觉方案在RK3588上跑起来不难难的是两路同时跑起来之后系统还知道先伺候哪一路。我做阶段四的起因很简单双路摄像头都接入、两路yolov5s模型都转成rknn、推理线程框架也搭好了可只要从路那一侧一启动主路检测的延迟就从35ms左右一路涨到70ms以上偶尔还飙到100ms。这不是算力不够是调度出了问题。阶段四要做的就是给这套双路视觉方案加上一套QoS严格优先机制让主路检测永远吃最好的资源从路和其他业务编码、录像、显示只能在主路忙完之后捡剩下的资源用。这个思路不复杂但落到RK3588这种芯片上要调的地方比想象中多CPU调度策略、NPU推理队列、内存带宽、帧缓冲策略每一层都得打通。这篇文章就是把我实际操作的过程、配置方法、踩过的坑和实测数据完整记录下来。1. 阶段四到底要解决什么问题从两路抢芯片资源说起1.1 到阶段三为止双路方案已经具备了什么按系列顺序走到这儿的读者手里的环境通常是这样的香橙派5RK3588这个级别的板子上双路MIPI CSI摄像头都已经接好一路挂在CSI0一路挂在CSI1通过media链路把sensor的出图通道梳理干净两路都能稳定拿到图像帧yolov5s模型已经通过rknn-toolkit2转成了rknn格式前面阶段里已经验证过单路推理、双路推理都能出结果检测线程有了自己的后处理逻辑NMS、类别过滤、框坐标映射都放在了CPU侧处理如果我猜得没错你还有一路显示或编码任务挂在旁边比如把画面推到本地屏幕或者把从路画面存成录像。这些基础齐了之后你大概率会遇到一个现象单独跑主路检测时一切正常推理时间稳定在35到45毫秒之间但一旦从路推理线程也开始往NPU提交任务主路那个rknn_run的耗时就变得忽快忽慢严重的甚至直接掉帧。这就是资源竞争。1.2 为什么算力看起来够用实际却互相拖累RK3588这颗芯片的NPU实力不差但两路yolov5s同时跑已经不是算力足不足的问题了。咱们把竞争拆开看第一层是NPU队列竞争。NPU只有一个调度器两路推理请求同时进来谁先谁后、谁占多核谁占单核如果没有明确配置公平调度下大家轮流来主路该等就得等。第二层是CPU竞争。每路推理的后处理、图像预处理、坐标变换都在CPU上做如果跑在同一个核或者同一组核上主路的线程就会被从路的线程分走时间片。更麻烦的是rknn_run本身还要在CPU上做数据搬运和结果回收CPU调度延迟会直接影响推理延迟。第三层是内存带宽竞争。双路图像帧都是几兆字节的YUV数据RGB转换、缩放、再拷贝进NPU的输入缓冲区再加上一路H.264编码内存总线忙不忙直接决定NPU读数据快不快。阶段四的QoS严格优先核心就是把这四层竞争一一理顺让系统在任何一个瞬间都知道主路来了其他都得让路。1.3 QoS严格优先在视觉方案里的定义和验收标准QoS这个词在网络设备里见得比较多但在嵌入式视觉里意思是一样的对不同的数据流给不同的服务等级。严格优先意味着当主路检测有请求时资源直接切给主路从路必须等待主路空闲时从路才能捡漏用资源。我在做这一阶段前给自己定了几个验收指标这里分享给你参考主路推理的p95延迟在从路满载运行时要保持和单路跑时接近允许的浮动不超过15%从路推理允许变慢但平均帧率不能掉到可用线以下我给自己定的底线是10帧/秒主路的输入帧必须是新鲜的不允许因为排队拿两三百毫秒之前的旧帧去推理测试过程中不允许出现CSI采集回调超时丢掉整帧的情况。这些指标你不需要照抄但建议动手之前先写清楚不然调了两个小时自己都不知道算不算调好了。2. 严格优先不是一句口号四个层面的优先级设计2.1 第一层CPU调度策略谁先占核、谁能抢时间片这层是基础也是最容易被忽略的。Linux默认的CFS调度器讲究的是公平公平对严格优先来说恰恰是坏事。主路检测线程要告诉内核我不是来参加公平竞赛的我是有硬性延迟要求的。具体做法是把主路的检测线程从普通调度策略切到实时调度策略。Linux下有两种实时策略SCHED_FIFO和SCHED_RR对嵌入式这种场景来说SCHED_FIFO更合适它的语义是只要这个线程处于就绪状态它就会抢占同一个核上所有非实时线程。配合优先级值1到99彻底把主路线程的地位抬高。这块要注意一个原则实时调度策略只给真正关键的那一两个线程不要给整个进程。后面踩坑部分我会细说提前打预防针如果你把采集线程、显示线程、主路线程一股脑全设成SCHED_FIFO系统真的会因为你来我往的实时抢占而崩溃式丢帧。此外还有CPU亲和性的问题。RK3588有四个大核A76和四个小核A55主路检测应该锁在A76上直接taskset绑死。从路检测和后台杂务锁在A55上两边物理隔开比单纯调优先级更稳。2.2 第二层NPU推理队列直接告诉rknn谁更重要CPU这层解决了但如果两路推理请求到了NPU还是先来后到排队主路照样被堵。好在rknn runtime在设计时就考虑过优先级librknnrt的头文件里有一组初始化标志核心的是几个带PRIOR的宏RKNN_FLAG_PRIOR_LOW、RKNN_FLAG_PRIOR_MEDIUM、RKNN_FLAG_PRIOR_HIGH。初始化rknn context的时候把这个标志传到rknn_init的flags参数里NPU侧的调度器就会按这个优先级给不同context分配执行机会。我的配置是主路context用RKNN_FLAG_PRIOR_HIGH从路context用RKNN_FLAG_PRIOR_LOW一高一低严格优先的骨架就搭起来了。这里要提醒一句不同版本librknnrt的宏名可能略有差异我在部分旧版本上见过的是RKNN_FLAG_PRIOR_XXX但也有人遇到过名字带NATIVE_前缀的版本。拿到板子先翻一下头文件确认实际宏名再写代码不用硬背。2.3 第三层内存带宽与DMA通道减少无声的竞争内存带宽这层最不起眼但对RGA和NPU的影响很大。我自己做个简单测试主路推理时如果同时跑一路4K的h264编码推理耗时能明显变长原因就是编码器大量占内存总线带宽NPU读输入数据要排队。QoS严格优先到内存这层能做的主要是给关键道让路而不是锁带宽毕竟Linux内核标准API没有直接给你设置内存带宽上限的接口。实际操作上我用了几个办法主路的图像预处理不放CPU上做交给Rockchip的RGA硬件用DMA通道做格式转换和缩放不占用NPU逻辑和CPU时间片从路的图像处理帧率调低在摄像头采集回调里直接丢弃多余帧而不是每帧都送到NPU排队编码器的码率限制到一个合理值我的从路录像用2Mbps的H.264帧率15fps编码线程绑定在A55小核上减少对A76和内存通道的冲击。这几个手段单个看不怎么起眼合在一起效果非常明显后面实测数据里有体现。2.4 第四层帧缓冲策略永远消费最新一帧这层很多人会忽略但它是QoS体验的最后一公里。严格优先说得再好如果主路推理线程拿到的还是几百毫秒前的旧帧画面、追踪、报警通通滞后等于白做。主路的帧缓冲策略我用的是最新帧覆盖采集回调里拿到的帧直接覆盖上一次的帧不排队。推理线程启动时就拿当前最新帧然后立刻提交给rknn_run。这样即使NPU偶尔排队推理结果对应的输入帧也是新鲜数据不会因为积压导致越来越延迟。从路的帧策略反而相反它要的是保帧率可以允许短暂排队但也要设一个上限。我从路队列深度设为3帧超过就直接丢弃最老的那一帧绝不无限制积压。从路毕竟是捡漏的角色丢几帧录像完全无所谓不掉队才是关键。这四个层面全打通才叫真正的严格优先。只做其中一层效果都要打折扣。3. 落地步骤从线程调度到NPU队列的完整配置3.1 主路检测线程的实时调度与大核绑定先说主路检测线程。我是在线程启动函数里写死配置的用一个pthread_setschedparam加pthread_setaffinity_np的组合。#include pthread.h #include sched.h void setup_main_detect_thread(pthread_t tid) { struct sched_param sp; memset(sp, 0, sizeof(sp)); sp.sched_priority 80; if (pthread_setschedparam(tid, SCHED_FIFO, sp) ! 0) { perror(main detect set sched failed); } cpu_set_t set; CPU_ZERO(set); for (int i 4; i 8; i) { CPU_SET(i, set); } pthread_setaffinity_np(tid, sizeof(set), set); }A76大核在RK3588上的CPU编号逻辑上是4到7A55是0到3。绑核的另一个好处是L1/L2缓存命中率更高推理线程不会被迁移到别的核上导致缓存全部失效。优先级我给了80这个数值不算极端既能抢占普通线程又不会跟内核实时任务纠缠太深个人推荐在这个量级上下取。主路线程的nice值顺便说一句SCHED_FIFO下nice值已经不起作用了实时调度线程不参与nice的优先级计算所以不用额外设。3.2 从路检测线程的低优先级与大核隔离从路检测线程我反着来用SCHED_OTHER就是普通调度nice值调高一些让它老老实实排在所有普通任务后面。从路绑定到A55小核上彻底跟主路物理隔离。void setup_slave_detect_thread(pthread_t tid) { struct sched_param sp; memset(sp, 0, sizeof(sp)); sp.sched_priority 0; if (pthread_setschedparam(tid, SCHED_OTHER, sp) ! 0) { perror(slave detect set sched failed); } // 进程级的nice调整在非实时调度下有效 setpriority(PRIO_PROCESS, 0, 10); cpu_set_t set; CPU_ZERO(set); for (int i 0; i 4; i) { CPU_SET(i, set); } pthread_setaffinity_np(tid, sizeof(set), set); }这里有个细节容易踩坑setpriority设置的是进程级nice多线程程序里如果用pthread的方式创建线程setpriority会对整个进程生效而不只是当前线程。所以从路线程的优先级调整主要靠pthread_setschedparam的SCHED_OTHER参数nice只是锦上添花。你要是发现setpriority之后主路也变慢八成就是这里的问题。3.3 NPU优先级rknn_init的flags参数NPU这层的配置在初始化阶段就定死。每个rknn context创建的时候传入不同的优先级标志。#include rknn_api.h rknn_context g_main_ctx; rknn_context g_slave_ctx; // 主路模型高优先级 int ret rknn_init(g_main_ctx, main_model_path, RKNN_FLAG_PRIOR_HIGH, NULL); if (ret ! RKNN_SUCC) { printf(main ctx init failed: %d\n, ret); } // 从路模型低优先级 ret rknn_init(g_slave_ctx, slave_model_path, RKNN_FLAG_PRIOR_LOW, NULL); if (ret ! RKNN_SUCC) { printf(slave ctx init failed: %d\n, ret); }两个ctx一旦初始化完成后面的rknn_run调用框架完全一样不需要再传优先级。NPU调度器自己会按context的优先级属性做统筹。我实际测试下来这个flag的作用非常直接开启之前主路要从从路手里抢NPU开启之后从路只要跟主路同时有任务NPU会先执行主路context的任务。如果你的runtime版本没有暴露优先级标志还有个退路把从路推理改成动态帧率比如根据主路当前有没有在run来调度。但这个方案我觉得效果差不少还是优先研究头文件确认标志位。3.4 帧缓冲实现主路最新帧覆盖、从路有界队列主路这块我用一个带锁的最新帧槽就够了不需要队列。#include mutex #include opencv2/opencv.hpp std::mutex g_main_frame_mutex; cv::Mat g_main_frame; volatile bool g_main_frame_valid false; // 采集回调里调用只负责覆盖 void on_main_camera_frame(const cv::Mat frame) { std::lock_guardstd::mutex lock(g_main_frame_mutex); frame.copyTo(g_main_frame); g_main_frame_valid true; } // 主路推理线程循环里调用 bool take_latest_main_frame(cv::Mat out) { std::lock_guardstd::mutex lock(g_main_frame_mutex); if (!g_main_frame_valid) { return false; } out g_main_frame; g_main_frame_valid false; return true; }我实际项目中因为图像帧比较大用的是双缓冲原子索引避免每次copyTo。但如果你是第一次做先用这个简单的版本跑通等验证QoS有效再优化拷贝开销。从路的帧队列我直接用了固定长度3的环形缓冲。每帧进来如果队列满就pop最老的再push新的。这个是有界队列的好处它能帮从路在性能波动时不至于无限积压导致延迟失控。3.5 配置完成后的验证方法所有配置写完开机先别急着看推理效果先验证线程的调度策略确实生效了。我用一套命令行组合# 先找到检测线程的TID ps -eLo pid,tid,cls,pri,ni,comm | grep detect # 假设主路TID是1234从路是2345 chrt -p 1234 chrt -p 2345 taskset -c -p 1234 taskset -c -p 2345chrt输出里如果主路显示policy: SCHED_FIFO、priority: 80说明配置成功。从路显示SCHED_OTHER、ni为10也正常。taskset确认主路绑在4-7从路绑在0-3就说明CPU亲和性生效了。NPU优先级有没有生效命令行看不到我用的是代码统计。在rknn_run前后各取一次clock_gettime统计主路和从路的推理耗时时长。如果主路的耗时在从路满载时依然稳定而只有从路变慢说明NPU优先级通了。如果你连着系统的perf工具也可以看rknn相关线程在CPU上的调度延迟时间能更直观。4. 实测数据QoS开关前后主路和从路各发生了什么4.1 测试环境与测试条件先说明一下我测试的环境方便你对比板卡香橙派5RK3588系列芯片64GB存储版运行官方debian内核摄像头双路MIPI CSI两路都是OV13850模组分辨率1080p输出30帧/秒模型yolov5s640x640输入rknn量化版rknn-toolkit2版本是1.5.2业务分配主路做实时检测用于屏幕显示和报警从路做检测后只存Bounding Box信息和录像走硬件H.264编码从路编码设置1080p、H.264、2Mbps、15fps。测试分三组第一组什么都不做保持默认调度第二组只设置NPU优先级就是只改rknn_init的flag第三组把CPU调度、NPU优先级、帧策略全部打开。每组连续跑10分钟统计推理耗时、p95延迟、掉帧数。4.2 三组实验的对比数据指标默认调度仅NPU优先级QoS全开主路平均推理耗时48.2ms36.1ms34.7ms主路p95延迟87.5ms46.3ms39.8ms主路最大延迟138ms58.1ms43.2ms主路丢帧数10分钟126帧21帧4帧从路平均推理耗时46.5ms71.2ms83.4ms从路有效帧率18fps12fps10.5fps从路丢帧数342帧158帧87帧这套数据是在我从路满载的同时测的主路的业务是报警和屏幕显示所以主路实时性是第一位的。从路慢一点、丢一点帧无所谓只要录像画面能保持基本的连续感。4.3 从数据里能读出几个关键结论第一个结论只开NPU优先级就已经能解决大部分主路延迟问题。主路平均耗时从48.2ms降到36.1msp95从87.5降到46.3说明NPU队列竞争是主路延迟失控的最大因素这个rknn_init标记得早点加上。第二个结论QoS全开之后主路的最大延迟从58.1降到43.2这个变化来自CPU调度和帧缓冲策略。CPU调度让主路线程不会在关键步骤上被从路线程抢时间片帧缓冲策略保证推理线程永远拿的是最新帧不会因为某个时刻排队久了输出明显滞后的检测结果。第三个结论从路确实被牺牲了这是严格优先的代价也是预期内的事。没有QoS时两路差不多QoS全开后从路平均耗时涨到83.4ms帧率掉到10.5fps。如果从路的业务也是实时性要求很高的那种严格优先就不能这么极端要改成加权调度了。这个跟你的业务判断有关技术上是完全可以调的。5. 我踩过的几个QoS坑以及怎么绕开5.1 把所有线程都设成SCHED_FIFOCSI回调直接丢帧我刚开始调的时候图省事直接把整个进程里的线程全设成SCHED_FIFO觉得既然要优先就大家一起优先。结果非常惨烈双路摄像头的CSI采集回调频繁超时每秒钟丢掉大量帧主路画面直接卡成PPT。原因是CSI回调线程变成实时高优先级后会跟rknn的推理线程在CPU上互相抢占而rknn_run执行时回调线程又必须等NPU完成才能拿新帧两者形成了死锁式的循环等待。实时调度策略只会加重这个优先级反转导致的混乱。正确的做法是把高优先级限定在推理提交结果回收这个短临界区上或者更简单点只给推理线程设置SCHED_FIFO采集回调保持普通优先级。采集回调代码本身就要短小精悍只做拷贝帧、丢进队列这种极简操作让它快点结束没必要给实时优先级。5.2 只调NPU优先级不调帧缓冲主路延迟还是会偶发跳变第一次做完NPU优先级配置后我挺兴奋实测平均延迟也很漂亮36ms左右。但看p95和最大延迟时还是有不小的毛刺。排查了半天发现是帧缓冲策略的问题我主路用的是普通FIFO队列采集速度和推理速度不匹配时队列会积压推理线程拿到的是排队前的旧帧而且队尾越长输入和输出之间的时间差越大。这就是为什么帧缓冲策略必须跟上。改成最新帧覆盖后那个偶发延迟毛刺基本消失了。严格优先不只是让NPU的执行顺序变得可预期还要保证每个环节排队等待的人都是最新鲜的那个人。5.3 两路都用PRIOR_HIGH等于没有QoS有个阶段我调试从路的时候顺手也把从路的rknn_init flags改成了PRIOR_HIGH想从路也体验一下高质量服务。结果两路都是高优先级NPU还是交替执行主路的延迟又回到了没有QoS时的水平。这个坑背后的道理很简单优先级的作用只有存在差异时才存在。一高一低系统才有谁先谁后的依据。两路都高等于排队规则变成插队规则进去之后还是轮流。如果你有三路任务就按业务重要性排HIGH、MEDIUM、LOW确保有一个明确的梯度。5.4 在主路推理线程里做printf日志性能直接雪崩这个坑严格说起来不是QoS配置本身而是调优期间的自我干扰。我在验证主路推理耗时的时候为了看得清楚每帧都在推理线程里加printf输出时间戳。结果printf本身同步写终端要等内核终端I/O完成这个操作把主路线程的实时性彻底拖垮了实测推理耗时从35ms变成800ms看起来像是在慢动作播放。后来我把日志改成累积到环形缓冲区由另一个低优先级线程定期排空才恢复正常。这个经验适用于所有做了实时优先级设置的线程日志要异步、字符串拼接要放外面、调试打印不是你想加就能加的东西。5.5 rknn runtime版本不同优先级宏名可能不一样最后提一句关于rknn版本差异的坑。RKNN官方对NPU优先级这块的接口并没有做到各版本完全一致有的版本里flags就直接叫RKNN_FLAG_PRIOR_HIGH有的版本还多出一组NATIVE_FP16模式之类扩展命名可能变化。我在朋友的RK3588s板子上发现他用的老版librknnrt根本不认PRIOR_HIGH这个宏传进去直接初始化失败返回错误码。所以拿到新板子第一时间去翻/usr/include/rknn_api.h或者你们SDK自带的头文件搜PRIOR。别照着我的代码闷头抄不同平台、不同runtime版本确实会有差别。6. 阶段四做完之后的几点体会严格优先这套配置做完到现在我的双路方案已经连续跑了两周多主路的延迟一直稳定在35到40毫秒从路录像也从来没断过。回想一下阶段四跟前面阶段最大的不同是它不教你增加功能而是教你管理资源。你手里的算力就那么多NPU就一个内存总线就一条谁能用得好谁的双路方案才是真正能落地的系统。最后分享一个我觉得很实用的小经验QoS配置完之后不要急着收工留一个环境变量开关在代码里让QoS全开、NPU优先级only、默认调度三者可以一键切换。我后来验证从路模型换成yolov5n、甚至尝试yolov8时都用这个开关快速对比过效果你看哪个策略搭配新模型更好一目了然。调试嵌入式视觉这种能随时切回去的实验习惯会帮你省很多时间。