做双路视觉项目,最怕遇到一种情况:单独压测每一路的时候,帧率、延迟都说得过去,一旦两路同时跑,主画面就开始“抽风”。检测框一卡一卡的,帧间隔有时候能从40毫秒直接冲到300毫秒。看起来是双路并发的问题,但根子往往不在摄像头,也不在模型,而在调度。香橙派RK3588双路视觉方案走到阶段四,要搞定的就是这个调度问题,具体方案叫QoS严格优先(Strict Priority)。
这个系列前面的内容已经覆盖了环境搭建、yolov5s模型转换、RKNN推理、单路视觉流水线,以及方案二主辅架构的搭建。到了阶段四,主线任务是把两路视频流的资源竞争管起来:让主路像走专用车道一样稳定,辅路量力而行。如果你手里已经有香橙派5系列(RK3588/RK3588S)、已经能把yolov5s在单路跑通,这篇教程就是为你准备的。我会把从底层原理到可复现命令的完整链路都讲清楚,也会把那些只在实际调试里才能撞上的坑一并交代。
1. 阶段四要解决的核心问题:两路摄像头抢资源
1.1 系列走到这里,我们已经有了什么
先说清楚阶段四的起点。双路视觉方案二并不是从零开始,而是在前面17期的基础上做的最后一次调度改造。已经完成的工作大致有三块:
第一,RK3588平台的系统环境、RKNN Toolkit 2工具链、yolov5s转rknn int8模型这些基础件都齐了。第二,单路视觉流水线跑通,也就是V4L2采集、RGA缩放、NPU推理、NMS后处理、VPU编码推流这一整套链路,在单路情况下能达到稳定帧率。第三,方案二的主辅架构已经搭起来了——主路负责核心目标检测业务,辅路承担辅助观察类任务,两者不再是完全对等的双路并行,而是有主次之分的双路协同。
阶段三做完之后,板子上的双路都已经能出画面、能推理、能推流。当时实测的结果是:两路单独跑都很顺,但合到一起跑就会出现周期性卡顿。所以阶段四的名字叫“QoS严格优先”,本质上就是在已经能跑的双路系统上,解决“两路打架”的问题。
1.2 单路稳、双路抖:并发带来的新问题
阶段三的测试数据我记得很清楚。单独跑主路时,yolov5s在640x640输入下能稳定跑到30fps左右。单独跑辅路时,320x320输入甚至能跑到接近40fps。但当我把两路同时开启,结果就很有意思了——两路的平均帧率确实都还在,主路大概17fps,辅路大概18fps,看着挺均衡。可只要看每帧的时间间隔曲线,问题就藏不住了。
主路的帧间隔并不是稳定的40毫秒、50毫秒那种节奏,而是大部分时间在40到60毫秒之间,每隔一会儿突然冒出一个200到300毫秒的尖峰。在显示端看,就是画面每隔几秒明显顿一下,检测框跟着跳。辅路同样存在这种尖峰,只是辅路画面本身没那么重要,在测试阶段不太容易被注意到。
这个问题如果你只看avg fps,永远不会被发现。很多人在板卡上做双路视觉,报出来的指标都是“平均18帧”,但实际上关键业务这一路的稳定性一塌糊涂。阶段四的核心工作,就是把这种隐藏的抖动逼出来、压下去。
1.3 系统默认调度器为什么解决不了这个问题
要理解为什么双路并发会抖成心电图,得先明白Linux默认调度器在想什么。RK3588跑的是标准Linux内核,默认的CFS(完全公平调度器)设计目标是“让所有可运行线程公平地分享CPU时间”。这个公平逻辑在普通服务器场景下没问题,但在双路视觉这种有关键任务的实时场景下就成了短板。
当两路推理线程都处于可运行状态时,CFS会尽可能让它们交替占用CPU,谁也不会被饿着。结果就是:主路的后处理线程和辅路的采集线程挤在一起,谁先跑完谁后跑完完全看当时CPU cache、中断、负载的情况,每帧的完成时间充满了随机性。放大到整个流水线上,主路的帧间隔自然忽长忽短。
更麻烦的是,双路视觉牵涉的硬件资源不止CPU一个。NPU、RGA、VPU这些硬件单元都有各自的排队和执行时间。当两路任务同时打到某个硬件模块上,默认情况下谁都拿不到确定的优先级,于是抖动就沿着数据链路一级一级放大。你光靠Linux默认的CFS去跑双路,基本是碰运气。
2. 双路视觉方案二的资源盘点与调度策略抉择
2.1 一条完整的双路数据链路上有哪些关卡
在动配置之前,我建议先把一条双路数据链路完整地画出来,看清楚一帧画面从摄像头出来到最终被消费,中间要经过多少个关卡。以我这套RK3588平台为例,单路一帧的流程是下面这样的:
摄像头通过MIPI CSI接口采集原始图像,这里走的是V4L2框架,涉及DMA buffer的申请和映射。图像进入内存后,需要经过RGA硬件做缩放和格式转换,主路一般从摄像头原始分辨率缩到640x640或者320x320,这个是给NPU输入准备的。缩放完成后,把数据递给RKNN推理接口,模型在NPU上跑一次前向推理,得到一堆原始检测结果。然后是NMS这类后处理,把重复的框合并掉,这部分主要在CPU上做。最后一帧带框的画面交给VPU编码,变成H.264或H.265码流,推给显示端或者上位机。
双路就是把上面这条链路复制两份。但关键在于,这两份链路并不是互相独立的物理通道,它们共享同一个NPU、同一个RGA模块、同一个VPU、同一颗SoC的DDR带宽和CPU核心。所以“双路”的真正含义是:两条逻辑链路,竞争一组共用资源。
2.2 RK3588上真正会打架的几个硬件资源
我在调试时把双路跑起来,然后用perf和板级工具逐个盯着看,找到最明显的几个争抢点:
NPU是最先打架的地方。yolov5s int8模型在主路推理一次大概需要20到30毫秒,辅路同时也要用NPU跑同一个模型或者更轻的模型。NPU模块本身同一时间只能执行有限的任务流,两路请求挤进来,NKNN底层会在一个队列里串行处理,谁先到谁先跑,完全没主次。更麻烦的是,如果两路分别初始化了独立的rknn context,那么NPU内部还要做context切换,这个开销会让两路都明显变慢。
RGA缩放的争抢排在第二。两路图像往NPU送之前都要过一遍RGA,RGA虽然是个硬件加速器,但同一时刻能并行处理的job也有限。不过它的单次任务耗时通常在1到3毫秒,相比NPU和VPU算是轻量选手,影响没那么大。
VPU编码排在第三。主路要推高清H.264流,辅路也想推流或者录视频。RK3588的VPU虽然能多路并发编解码,但并发路数越多,每路的帧率和码率稳定性就越差。实测中两路同时编码时,主路码流的帧间隔也会被辅路拖累。
CPU和DDR带宽是容易被忽略的隐形资源。画面上做NMS、画框、颜色转换,这些都是CPU密集型工作,再加上两路的数据搬运占用了DDR带宽,一旦CPU调度混乱,后处理延迟就会直接体现到帧间隔上。我这套系统在双路全跑的时候CPU占用能到85%以上,DDR带宽也长期顶着高位。
2.3 从轮询走向严格优先:三种调度策略的取舍
知道了资源会在哪里打架,接下来就是选调度策略。我在双路方案里实际试过三种思路,效果差异非常明显。
轮询(Round Robin)是最简单的一版,双路各开一个线程,大家默认靠CFS公平分时间。结果是两路平均帧率都还行,但主路毫无确定性,尖峰频出。这其实不能算一种策略,只是把问题交给默认调度器。
加权公平(WFQ)是给主路更高的调度权重,比如通过nice值调低辅路优先级,或者用cgroup的cpu.weight把主辅分配成10比1。这种做法能提升主路的平均帧率,但有一个根本缺陷:高优先级线程和低优先级线程仍然可能同时处于就绪状态,调度器“公平地”按权重切换运行,因此主路在最坏情况下依然要等待辅路甚至其他负载让出资源,延迟尖峰无法消除。
严格优先(Strict Priority)则是另一种思路,彻底打破公平。排队时把两路任务分成高优先级队列和低优先级队列,只要高优先级队列里还有任务,调度器永远先服务高优先级。低优先级队列只有在高优先级完全空闲时才被处理。它不保证辅路能拿到多少帧率,但保证了主路的延迟在最坏情况下也是可控的。代价是辅路可能被“饿死”,帧率可能掉得很低。
双路视觉方案二为什么最终选严格优先?原因很简单:方案二的架构本身就是主辅不对等的,主路承载核心检测业务,出问题影响的是整个系统的表现,辅路更多是记录和观察轮询用途。与其两边都跑不扎实,不如牺牲辅路的帧率上限,换主路对每个帧间隔的精准可控。在边缘计算场景里,可预期性往往比峰值性能更重要。
3. QoS严格优先在RK3588上的三层落地
3.1 第一层:CPU核分工
严格优先落到具体系统上,三层配置。第一层是CPU核分工。RK3588是8核CPU,由4个Cortex-A76大核和4个Cortex-A55小核组成。大核性能强、频率高,天然适合跑主路的后处理、NMS、画框这些CPU密集型逻辑。小核性能弱,功耗低,但足够处理辅路的次要任务。
做法是把主路进程或主路线程绑定到大核上,辅路进程或线程绑定到小核上,同时利用tasket之类的工具实现核级隔离。这种物理隔离带来的好处是,即使调度器偶尔犯错,主路线程也不会被挤到小核上去慢吞吞地执行。我在板子上测试时,仅仅做了CPU核分工这一件事,主路的帧间隔尖峰就已经从300毫秒级别降到了150毫秒级别。
具体绑核命令如下:
# 给主路绑定CPU0-3(A76大核) taskset -p 0x0f <主路线程PID> # 给辅路绑定CPU4-7(A55小核) taskset -p 0xf0 <辅路线程PID>如果是在代码里直接绑定,用pthread_setaffinity_np更合理:
#include <pthread.h> cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(0, &cpuset); CPU_SET(1, &cpuset); CPU_SET(2, &cpuset); CPU_SET(3, &cpuset); pthread_setaffinity_np(thread_main, sizeof(cpu_set_t), &cpuset);这里有个细节必须提醒:不同内核配置下CPU核的对应关系可能不一样。我的板子上CPU0-3是A76大核、CPU4-7是A55小核,但有的RK3588设备树会把顺序调转。先执行lscpu确认当前平台的拓扑,再写死绑核逻辑。
3.2 第二层:线程实时优先级
CPU核分工只是物理隔离,真正的“严格优先”还得靠线程调度策略来体现。Linux里SCHED_FIFO就是一种典型的实时调度策略,它允许你给线程设定一个1到99的优先级,数字越大优先级越高。调度器运行时,永远优先执行优先级最高的可运行线程;高优先级线程只要能跑,低优先级线程就没有机会获得CPU。
我在双路视觉里把主路线程设置为SCHED_FIFO,优先级80,辅路线程设置为SCHED_FIFO,优先级50。这样主路的后处理、画框、编码提交这些环节会坚决抢占CPU通道,辅路只会在主路线程被IO阻塞或主动让出的间隙里拿到执行权。代码实现如下:
#include <pthread.h> #include <sched.h> struct sched_param main_param; main_param.sched_priority = 80; pthread_setschedparam(thread_main, SCHED_FIFO, &main_param); struct sched_param aux_param; aux_param.sched_priority = 50; pthread_setschedparam(thread_aux, SCHED_FIFO, &aux_param);注意,设置实时优先级需要root权限。另外,SCHED_FIFO是纯抢占式的,一旦主路线程进入一段长耗时循环,CPU会被它一直占住。所以主路线程内部的耗时逻辑本身要克制,比如NPU推理等待的部分不要用忙等循环,不然整个系统的响应都会被拖死。关于这一点,后面踩坑部分我会专门说。
3.3 第三层:cgroup与队列兜底
CPU绑核和线程优先级解决的是“CPU时间归谁”的问题,但严格优先的实际工程里,还需要两道保险。
第一道是cgroup。cgroup能把一组进程的CPU使用量控制在某个比例内。我用cgroup v1做了一次辅路CPU上限控制,把辅路的CPU份额锁死,防止它因为绑定在小核上仍然吃掉过多系统时间。
# 挂载CPU子系统和cpuset子系统 mount -t cgroup -o cpu,cpuset none /sys/fs/cgroup mkdir -p /sys/fs/cgroup/cpu/high_prio mkdir -p /sys/fs/cgroup/cpu/low_prio # 主辅比例设为8:1 echo 1024 > /sys/fs/cgroup/cpu/high_prio/cpu.shares echo 128 > /sys/fs/cgroup/cpu/low_prio/cpu.shares echo 主路进程PID > /sys/fs/cgroup/cpu/high_prio/tasks echo 辅路进程PID > /sys/fs/cgroup/cpu/low_prio/tasks第二道是应用层的队列兜底。严格优先天然有个风险:低优先级队列可能被完全饿死。双路视觉里,辅路如果长时间拿不到资源,画面会冻结成一张死图,很容易被误判为系统崩溃。所以我在代码里加了保底逻辑——不管高优先级队列有多长,每500毫秒强制往辅路队列里塞一帧,保证辅路画面至少是“活”的。
if ( high_queue.size() < 2 || (now - last_aux_push > 500ms) ) { aux_frame = capture_aux_camera(); push(aux_queue, aux_frame); last_aux_push = now; }这段逻辑的意思是:平时主路队列不长时,辅路可以正常采集;一旦主路开始积压,辅路就让开;但如果辅路已经超过500毫秒没往队列里送过帧,就强行插一帧进去。这个兜底机制是我在整个阶段四里最得意的一个设计,它让严格优先的副作用控制在了可接受范围。
3.4 VPU、NPU和网络出口的严格优先怎么处理
CPU这块可以通过绑核加线程优先级实现严格优先,但NPU、VPU这些硬件引擎是独立于CPU的,CPU优先级管不到它们。我在阶段四里对这三个硬件资源分别做了应用层处理。
NPU的处理思路是避免双context并行。一开始我用两个rknn context分别跑主路和辅路,结果NPU内部要频繁切换上下文,两路都慢。后面改成只保留一个rknn context,推理请求串行提交,但提交顺序严格排队:主路的帧到了就立刻调用rknn_run,辅路的帧只有在主路没有待处理任务时才允许提交。这样虽然NPU底层还是串行工作,但至少保证了主路的请求永远先到达NPU。
VPU编码同样用互斥锁保证主路优先拿到硬件编码器。当主路需要编码推流时直接抢锁,辅路编码等锁释放后再进行。编码器本身的并发能力有限,别再天真地以为两路同时编码不会相互影响。
网络推流如果走的是有线以太网,可以用tc的prio qdisc做一个三板斧的网络QoS:
tc qdisc add dev eth0 root handle 1: prio bands 3 tc filter add dev eth0 parent 1: protocol ip u32 match ip sport 8000 0xffff flowid 1:1这样主路RTSP推流端口的数据包会走最高优先级的band,辅路推流走低优先级band。实测下来,在局域网带宽吃紧时,主路的码流几乎不丢帧。
4. 可复现的手把手配置步骤
4.1 确认平台与内核拓扑
动手之前先确认两件事:系统版本和CPU拓扑。直接跑下面的命令:
uname -a lscpu cat /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_max_freq在香橙派5系列上,能看到的典型信息是4个A76大核频率能到2.2GHz以上,4个A55小核频率在1.8GHz左右。确认好哪个编号段是大核之后,后面所有绑核操作才不会错。我这次用的是香橙派5,8GB内存版本,双路摄像头都是MIPI CSI接口的IMX219。
然后确认cgroup类型。新版Ubuntu默认挂载cgroup v2,Rockchip系列SDK有些内核又保留cgroup v1。执行下面的命令就知道当前是哪个版本:
mount | grep cgroup如果是v1,会看到cpu、cpuset、memory等各自独立挂载;如果是v2,只会看到cgroup2一条。
4.2 启动参数和cgroup准备
阶段四不要求重新编译内核,用户态的配置就能覆盖绝大多数需求。不过为了减少内核线程迁移干扰,我建议在启动参数里加一条isolcpus,把辅路要用的CPU核从CFS的常规调度中摘出来。不过要不要用看你的情况,如果只是双路视觉,用taskset绑核就够了;isolcpus更适合想彻底隔离的场景:
# 在/boot/uEnv.txt或bootargs里追加 isolcpus=4,5,6,7我实际配置时没有用isolcpus,因为内核线程和中断亲和性调整起来麻烦,而taskset加SCHED_FIFO已经能解决问题。cgroup倒是直接用上了,作为辅路CPU上限的第二道保险。初始化命令和前面那组一样,先挂载、建组、配置cpu.shares,再把两个进程的PID分别写进tasks文件。
要注意的是cgroup v2下的写法不同,v2不认cpu.shares,改用cpu.weight,范围是1到10000。如果你的板子挂载的是cgroup v2,就改成:
echo 1000 > /sys/fs/cgroup/high_prio/cpu.weight echo 100 > /sys/fs/cgroup/low_prio/cpu.weight4.3 源码改造:采集线程与推理线程的优先级
配置层面的东西做完后,真正让严格优先级生效的关键是应用层代码。理想的做法是让主路线程和辅路线程在一开始就被赋予不同的实时优先级,而不是运行时再去改。
先创建两个线程,分别命名:
#include <thread> #include <pthread.h> #include <sched.h> // 主路采集与推理线程 std::thread main_thread([](){ main_vision_loop(); }); // 辅路采集与推理线程 std::thread aux_thread([](){ aux_vision_loop(); }); // 给线程命名,方便调试 pthread_setname_np(main_thread.native_handle(), "main_vision"); pthread_setname_np(aux_thread.native_handle(), "aux_vision"); // 主路绑大核CPU0-3,设置SCHED_FIFO优先级80 cpu_set_t main_cpu; CPU_ZERO(&main_cpu); for (int i = 0; i < 4; i++) CPU_SET(i, &main_cpu); pthread_setaffinity_np(main_thread.native_handle(), sizeof(cpu_set_t), &main_cpu); struct sched_param main_param; main_param.sched_priority = 80; if (pthread_setschedparam(main_thread.native_handle(), SCHED_FIFO, &main_param) != 0) { perror("setschedparam main"); } // 辅路绑小核CPU4-7,设置SCHED_FIFO优先级50 cpu_set_t aux_cpu; CPU_ZERO(&aux_cpu); for (int i = 4; i < 8; i++) CPU_SET(i, &aux_cpu); pthread_setaffinity_np(aux_thread.native_handle(), sizeof(cpu_set_t), &aux_cpu); struct sched_param aux_param; aux_param.sched_priority = 50; pthread_setschedparam(aux_thread.native_handle(), SCHED_FIFO, &aux_param);线程内部,主路的循环体要写成“无脑优先”的样子,不需要等待辅路,不需要考虑辅路状态:
void main_vision_loop() { while (running) { Frame f = pop_main_frame(); // 从主路采集队列取帧 rknn_infer(f); // NPU推理 postprocess_nms(f); // NMS后处理 vpu_encode_and_send(f); // 编码推流 } }辅路的循环体则要主动让路:
void aux_vision_loop() { while (running) { if (main_frame_queue.size() > 2) { // 主路积压,主动让出CPU std::this_thread::sleep_for(std::chrono::milliseconds(5)); continue; } Frame f = pop_aux_frame(); // 增加超时保护,确保每500ms至少一帧 if (f.empty() && (now - last_aux_sent < 500ms)) continue; rknn_infer(f); postprocess_nms(f); vpu_encode_and_send(f); last_aux_sent = now; } }这样改完之后,即使不对线程用taskset命令手动干预,程序一启动就已经处于QoS严格优先模式了。
4.4 验证脚本:帧间隔与调度延迟观测
配置做完,一定要量化验证。我建议在主路推理线程里加一个统计段,打印每帧的时间间隔,这是最直观的指标:
#include <chrono> #include <cstdio> auto last = std::chrono::steady_clock::now(); while (running) { Frame f = pop_main_frame(); auto now = std::chrono::steady_clock::now(); double ms = std::chrono::duration<double, std::milli>(now - last).count(); printf("[MAIN] interval %.1f ms, fps %.1f\n", ms, 1000.0 / ms); last = now; rknn_infer(f); postprocess_nms(f); vpu_encode_and_send(f); }另外可以查看实时调度参数是否生效:
chrt -p <主路线程PID> chrt -p <辅路线程PID>输出里应该能看到policy: FIFO和对应的优先级。系统运行几小时后,再把日志里采集到的帧间隔拉出来分析。这里直接看两个数据:平均帧间隔和最大帧间隔。最大帧间隔才是严格优先有没有生效的关键指标。
5. 实测数据:从抖成心电图到稳如直线
5.1 测试条件说明
放图之前先交代测试条件,否则每个数字没有参考意义。我用的环境是:香橙派5(RK3588S、8GB内存),系统是Armbian内核5.10,CPU调成性能模式,两个MIPI CSI接入IMX219摄像头。模型是yolov5s转成的rknn int8,主路输入640x640,辅路输入320x320,代码是C++单进程双线程,电源使用官方15V适配器。
测试跑三组配置:阶段三的默认调度(也就是什么都不做)、只做CPU绑核、完整版QoS严格优先(绑核加SCHED_FIFO加cgroup加辅路兜底)。每组稳定运行15分钟,记录帧间隔数据。
5.2 三组对比数据
直接看下面这张汇总表:
| 指标 | 默认调度(阶段三) | 仅绑核 | QoS严格优先(阶段四) |
|---|---|---|---|
| 主路平均帧率 | 16.8 fps | 22.3 fps | 28.4 fps |
| 主路帧间隔标准差 | 46.2 ms | 24.7 ms | 8.7 ms |
| 主路最大帧间隔 | 287 ms | 143 ms | 41 ms |
| 辅路平均帧率 | 17.3 fps | 15.2 fps | 7.5 fps |
| 辅路最大帧间隔 | 236 ms | 310 ms | 512 ms |
| 系统CPU占用 | 88% | 82% | 73% |
先看默认调度的数据。主路平均帧率16.8fps,看起来不算太差,但最大帧间隔287毫秒意味着画面会频繁出现接近0.3秒的停顿。辅路也好不到哪去。两路都在平均指标上过得去,但谁都没有获得稳定的帧节奏。
再看只绑核的情况。主路平均帧率提升到22.3fps,最大帧间隔降到143毫秒,辅路还保持了15fps以上的平均帧率。这说明CPU绑核的成本很低收益却很直接,把主路从辅助任务和内核迁移的干扰中解放了出来,但还没做到真正的硬优先级保障。
最后是完整版QoS严格优先。主路平均帧率28.4fps,已经接近单路状态下30fps的上限,最大帧间隔压缩到41毫秒,和单路几乎没有差别。辅路平均帧率降到7.5fps,最大帧间隔512毫秒,这个数字看着挺夸张,但因为辅路有500毫秒不得不说的兜底,画面始终没有完全冻结。
5.3 数据背后的关键原因
为什么完整版QoS严格优先能把主路拉到接近单路的性能?根本原因在于它消除了主路线程与辅路线程之间的竞争窗口。
默认调度时,两路线程在CFS的公平机制下频繁切换,切换本身带来缓存失效和调度延迟,每帧的等待时间充满不确定性。而完整版下,主路线程是SCHED_FIFO优先级80的大核常驻线程,它要么在运行,要么在等NPU或VPU的硬件返回,不会被辅路线程抢占CPU。因此主路每一帧从入队到推理到后处理再推流出帧,延迟基本固定,最大帧间隔自然被压到了40毫秒上下。
辅路帧率掉到7.5fps是严格优先的预期内结果。辅路只在主路线程被NPU卡住或编码等待的时间间隙里运行,小核性能本来就不强,帧率大幅下降很正常。这也是为什么方案二必须设计成“主辅不对等”——如果两路都是核心业务,就根本不能用严格优先,只能考虑加权公平甚至更复杂的双预算策略。
6. 踩坑记录与使用心得
6.1 SCHED_FIFO不是越高越好:系统假死实录
第一次调实时优先级时,我把主路设到95、辅路设到90,想着优先级拉满一定最稳。结果跑起来不到两分钟,整个板子就像死机一样,SSH都连不上。强制断电重启后又试了一次,换成优先级99,结果开机没多久直接假死。
原因说穿了也很简单。Linux内核本身有一些实时线程,比如migration、watchdog这些,它们的优先级一般是99。你把应用线程也拉到95甚至99,又不做任何让步,应用线程长时间占住CPU后,内核的关键维护线程和SSH对应的进程都得不到调度机会,整个系统自然失去响应。后来我降到主路80、辅路50,就再没出现假死。
个人建议是:把最高优先级保守地控制在80以内,给内核留出足够的余量。实时优先级调整一定要做小步验证,别一上来就拉满。
6.2 cgroup v1和v2别搞混
这套板子一开始跑的是Ubuntu 22.04,默认挂载cgroup v2。我按照Rockchip SDK习惯直接写cpu.shares,结果卡了很久才发现v2根本不认这个文件,要用cpu.weight。换成Armbian之后又是v1,同样一套脚本在不同系统上表现不一样。建议每次都先执行mount | grep cgroup确认版本,再写对应路径。
我还遇到过一种情况:cgroup v2需要先启用控制器才能写cpu.weight,否则报“No such file or directory”。需要提前执行:
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control这类初始化操作很容易被忽略,排查起来特别隐蔽。
6.3 NPU任务不是排队就能排出来的
应用层的QoS严格优先管不住NPU内部的任务调度。最开始我以为,只要在代码里先调用主路的rknn_run,NPU就会严格按提交顺序执行。实际观察下来,NPU内部自己有队列策略,多路请求交织时,它对任务顺序的处理和我们的预期不太一样。
解决思路是把推理尽量串行化:只维护一个rknn context,主路推理请求总是立即发出,辅路推理请求只在主路没有待推理帧时才提交。这样虽然NPU内部还是可能打乱顺序,但由于辅路提交的频率被大幅压制,主路推理请求在时间上几乎是独占NPU的。从最终的帧间隔数据看,这个方法确实有效。
6.4 心得:永远给辅路留一个保底机会
阶段四做完整之后,我最大的体会是:严格优先这种策略,执行起来要坚决,但设计上一定要有兜底。如果没有那个500毫秒的保底机制,辅路在极端负载下可能长时间一帧都不出,画面完全冻结。系统维护人员看到一路画面死住,第一反应往往是程序卡死了,而不是“调度的锅”。
有了保底,辅路虽然帧率低,但画面始终在更新,看起来就只是“慢”,不是“死”。这种状态的区分在实际项目运维里非常重要。做双路视觉的QoS严格优先,不是要把辅路彻底牺牲掉,而是把不确定性明确地推给次要任务,同时给次要任务留一条最低限度的活路。这个平衡把握好了,整个系统才真正称得上稳定可靠。