Day11 实战:多路视频流加权公平调度------令牌桶 QoS 让关键摄像头不被拖垮
本篇是「RK3568 边缘 AI 部署」系列的第十一篇。前情:Day10 我证明了 RK3568 的 NPU 是单核串行 资源,多路总吞吐卡在 ~11 FPS,且简单轮询是"无差别公平"------超额订阅时所有路被平均压到 ~2.7 FPS。安防告警摄像头会被风景摄像头拖到同一低帧率 ,这是"无 QoS"的代价。这一篇用令牌桶(Token Bucket)解决它:给关键流预留 带宽、给背景流按权重分配剩余带宽,并用实测数据回答------预留真的能保住关键流的帧率吗?
一、为什么 Day10 的调度不够用
Day10 的调度是「轮询 + 时间驱动跳帧」,本质是 equal share:N 路平分那块 ~11 FPS 的 NPU 预算。
超额订阅下(4 路各 30fps,总需求 120fps),它的表现是:4 路全部被压到 ~2.7 FPS,没有一路被饿死,但也毫无优先级。
真实产品里这是不能接受的:
- 前视安全相机要 10 FPS 才能稳定检测行人/车辆;
- 周界风景相机 2 FPS 就够看是否有人翻越。
如果两者抢同一块 NPU,Day10 的方案会让安全相机也只剩 2.7 FPS------漏检风险。所以需要 QoS:关键流优先、背景流退让。
二、令牌桶原理(三句话讲清)
给每路一个桶:
- 桶以
rate(令牌/秒)匀速补充,上限capacity(允许短时突发 burst); - 每送 NPU 一帧消耗 1 个令牌;
- 想送帧必须同时满足 两件事:「到自己
target_fps的时间片」且「桶里有 ≥1 令牌」。
rate 怎么定?先把 NPU 总容量(实测可持续 FPS,这里是 10.85)扣掉预留,剩下的按权重分:
reserved = Σ guaranteed_i # 先扣预留
remaining = max(0, capacity_total - reserved)
be_weight_sum = Σ weight_j (guaranteed_j == 0)
有 guaranteed 的流:rate = guaranteed # 预留多少拿多少
背景流 j: rate = remaining * weight_j / be_weight_sum
调度顺序:QoS 模式每轮按 (-guaranteed, -weight) 排,关键流先服务;RR 模式保持原顺序(作 baseline 对照)。
三、一个隐藏大坑:多 VideoCapture 同文件 + 贪婪抓取
这是我 Day11 踩到的新坑,值得单独记。
Day10 复用同一个 200 帧视频文件 给 4 路 cv2.VideoCapture,且主循环每轮给 4 路都 read() 。当每路抓取数超过 200,解码器反复 seek(0) 回绕,H264 裸流解码抖动------吞吐从 ~11 FPS 骤降到 ~2 FPS,进程像"假死"。
两个修法:
- 每路独立文件副本 (
v0.h264~v3.h264),互不干扰; - 懒抓取(lazy grab) :只在「桶里有令牌 且 到时间片」时才真正
grab(),使抓取数 ≈ 推理数(75--200/路),不再触发循环 seek。
验证:rr loops=300 从假死恢复到 28.9s / 10.38 FPS 稳定。这条坑后面文章会展开讲。
四、实测数据(3 组对照)
板子:RK3568 + yolov5s.rknn(640×640 INT8),OpenCV 读 H264 测试视频(每路独立副本)。容量基准 10.85 FPS @600MHz,每组 300 推理帧:
| 场景 | 模式 | 总吞吐 | 关键流(src0) | 背景流(src1-3) |
|---|---|---|---|---|
| A | RR(4 路各 w1) | 10.73 | 2.68(w1) | 各 2.68 |
| B | QoS 预留(critical w4 g8) | 8.28 | 5.52(cap200) | 0.92/0.91/0.91(cap33-34) |
| C | QoS 仅加权(critical w4) | 10.54 | 5.97(cap170) | 1.55/1.51/1.51(cap43-44) |
cap = 该路实际推理帧数;rate = 令牌桶补充速率(tok/s)。
五、三个结论(也是面试考点)
结论 1:QoS 真正把关键流抬起来了
看 A vs B/C:RR 下关键流被压在 2.68 FPS (和背景一样);QoS 下关键流涨到 5.52 / 5.97 FPS ------是背景流的 5.5--6 倍。Day10 暴露的"无 QoS"缺口,这里补上了:关键路保住了帧率,背景路退让。
结论 2:预留 ≠ 一定达到预留值(关键真相)
B 组我给 critical 设了 guaranteed=8,但它只跑到 5.52 FPS,没到 8。原因有两层:
- 令牌桶是平均速率 :
capacity=6封顶了突发,流不能瞬间花掉 8 个令牌; - NPU 是串行的:即使 critical 有令牌,它的帧也可能排在一帧背景推理后面等。
所以预留保的是"带宽份额",不是"瞬时延迟保证"。这是真实、可教学的现象,不是 bug。真要稳到 8 FPS,得同时满足"预留 8" + "NPU 确实撑得住 8"(这里单帧 ~71ms,8 FPS 需要独占 ~88% NPU,而 3 路背景也在提交,critical 会周期性被插队)。
结论 3:加权模式整体利用率更高
B(预留)总吞吐 8.28,C(仅加权)总吞吐 10.54------差在 B 有"预留但未用满"的地板。C 把全部 10.85 按权重分(critical 4/7≈6.2,bg 1/7≈1.55),没有闲置带宽,利用率更高。工程取舍:要硬保底选预留;要最大吞吐选加权。
六、复现方法(板端直接跑)
bash
# 每路独立视频副本 + 脚本上板
for i in 0 1 2 3; do cp /userdata/200frames_count.h264 /userdata/v$i.h264; done
scp multi_stream_qos.py run_day11_clean.sh root@board:/userdata/
bash /userdata/run_day11_clean.sh # 跑 A/B/C 三组,写 /tmp/day11_{rr,qos_g,qos_w}.json
# 单场景手动跑
python3 multi_stream_qos.py --mode rr \
--sources "video:/userdata/v0.h264:30,video:/userdata/v1.h264:30,video:/userdata/v2.h264:30,video:/userdata/v3.h264:30" \
--loops 300 --out /tmp/day11_rr.json
python3 multi_stream_qos.py --mode qos --capacity 10.85 \
--sources "video:/userdata/v0.h264:30:4:8,video:/userdata/v1.h264:30,video:/userdata/v2.h264:30,video:/userdata/v3.h264:30" \
--loops 300 --out /tmp/day11_qos_g.json
注意:Python 间歇负载会把 NPU 钉在 600MHz(Day10 结论),所以这些数都是 @600MHz。要 900MHz 天花板先
echo performance > /sys/class/devfreq/fde40000.npu/governor。
七、30 秒面试话术
「我在 RK3568 上做过多路视频流的加权公平调度。Day10 发现 NPU 是单核串行、多路总吞吐卡 ~11 FPS,但简单轮询是无 QoS 的------超额订阅时安防相机也会被拖到 2.7 FPS。所以我加了令牌桶 :每路一个桶按 rate 补令牌、送帧消耗 1 个,rate 由'先扣预留、剩余按权重分'算出来,QoS 模式按(-预留,-权重)优先服务关键流。实测:RR 关键流 2.68,加 QoS 后提到 5.5--6 FPS,背景流退让。有个重要认知------预留保的是带宽份额不是瞬时延迟,我设 guaranteed=8 实际只到 5.52,因为令牌桶是平均速率 + NPU 串行排队。还有一个坑:多路共享同一视频文件且每轮贪婪读帧会触发解码 seek 抖动把吞吐打到 2 FPS,我用'每路独立文件 + 懒抓取'解决了。」
八、下一篇
多路调度 v3 方向有两个:① 零丢帧缓冲 ------当前 skipped 是调度决策计数,真实场景需每路缓存最新帧、从缓冲取,避免漏掉运动目标;② 动态令牌桶------根据 NPU 实时队列深度自适应调 rate。或者转向训练侧------补上"模型是怎么练出来的"(Karpathy micrograd)。看后续时间。
数据、脚本、3 组原始 JSON 都在 GitHub 仓库 python/ 与 data/day11/,技术细节见 docs/day11_qos.md。