系列上一篇:Day8|多 batch 推理实测:RK3568 的 NPU 真被喂满了吗?
硬件:RK3568(正点原子 EVB1 DDR4 V10 板,1 核 NPU)|模型:yolov5s 640×640 INT8|runtime:librknnrt 1.5.0
一、引子:昨天我下了一个错误结论
Day8 结尾我写:“batch=4 只比单帧快 6%,说明 NPU 已经饱和。”
这句话有一个我没验证的假设——NPU 负载真的到 100% 了吗?
嵌入式工程师的本能应该是:别猜,去问硬件。RK3568 的 NPU 驱动在 sysfs 里暴露了负载计数器:
cat/sys/kernel/debug/rknpu/load# NPU load: 0% <- 空载于是我写了个采样线程,边推理边读这个节点。结果推翻了昨天的结论,还顺藤摸瓜挖出了三个更深的发现。这一天没有写一行 C++,没有碰一次交叉编译——全部用板端 Python(rknnlite)完成,这也是本文的第二个主题:不依赖工具链的板端基准测试方法。
二、发现一:RK3568 的 NPU 是单核(一个流传很广的误区)
起因是我一开始想测"NPU 多核调度",给 rknnlite 传了 core_mask:
fromrknnlite.apiimportRKNNLite rknn=RKNNLite()rknn.load_rknn('yolov5s.rknn')rknn.init_runtime(core_mask=0x07)# 想用 3 个核runtime 直接报错:
E The core_mask is only supported by RK3588. Please do not set this parameter on other platforms.core_mask 多核绑定是 RK3588 独有的。RK3588 有 3 个 NPU 核心(可以 1/2/3 核灵活组合),而 RK3568 只有一颗 NPU 核,设备树节点是fde40000.npu。
网上不少资料把"RK35xx 系列 NPU"混着说,“3 核 NPU”"多核并行"的说法很多。看芯片手册 + 实测报错,比看博客靠谱。这是本文的第一个坑:
坑25:RK3568 = 单核 NPU;
core_mask参数仅 RK3588 支持,在 RK3568 上设置会 init 失败(ret=-1)。
三、发现二:NPU 负载从来没超过 78%
把负载采样线程跑起来(50ms 间隔),同时用 1/2/3/4 个线程并发跑推理:
| 并发实例 | 单次推理(ms) | 总吞吐 | NPU load 平均/峰值 |
|---|---|---|---|
| 1 | 90.4 | 8.1 FPS | 23.9% / 54% |
| 2 | 130.3 | 10.5 FPS | 47.4% / 73% |
| 3 | 187.1 | 10.7 FPS | 56.0% / 78% |
| 4 | 251.2 | 7.3 FPS ⬇ | 53.1% / 77% |
两个信息:
- NPU 负载峰值只有 78%,从来就没有"饱和"。Day8 的结论要修正。
- 多实例并发到 2-3 个时总吞吐有 1.3 倍收益,到 4 个反而退化——说明并发填的是提交间隙(单线程时 CPU/NPU 互相等待的空档),而不是在压榨剩余算力。
顺带说明:Python rknnlite 单实例 90ms vs C++ API 的 60ms,差值主要是 Python 侧输出张量拷贝和封装开销。本文的结论看的是相对关系,不受绝对值影响。
四、发现三:600MHz 是谁钉死的?——DVFS 调频实验
上面所有测试里有个奇怪现象:NPU 频率从头到尾钉在 600MHz 不动。查 devfreq:
cat/sys/class/devfreq/fde40000.npu/available_frequencies# 200000000 297000000 400000000 600000000 700000000 800000000 900000000cat/sys/class/devfreq/fde40000.npu/governor# rknpu_ondemand7 个档位,最高900MHz,出厂 governor 是rknpu_ondemand——按负载升频。但负载计数器峰值才 78%(governor 的升频阈值估计在更高处),所以永远够不到 900MHz。
写了个 A/B 脚本:基线跑一遍 → 切performance强制 900MHz 再跑一遍 → 自动恢复原设置。
Python(间歇负载):
baseline (rknpu_ondemand, 600MHz) avg 82.91 ms 12.1 FPS performance (900MHz) avg 69.74 ms 14.3 FPS 提升 15.9%C++(连续负载):有意思的来了——C++ 版在默认 governor 下自己就升到了 900MHz:
C++ baseline (rknpu_ondemand→900MHz) NPU avg 50.69 ms 19.7 FPS C++ performance (900MHz) NPU avg 51.06 ms 19.6 FPS为什么 C++ 能升频而 Python 不能?因为 C++ 的推理是背靠背连续的,负载计数器读数高;Python 每次推理之间有 ~28ms 的宿主侧空隙,负载计数被稀释——governor 看到的是"半闲的 NPU",于是不给最高频。
这里藏着一个对做基准测试的人很重要的教训:
坑26:DVFS 设备上做 benchmark,必须先
echo performance > .../governor钉死频率。否则你测的不是算法/硬件的性能,而是"调度器觉得你配用多少频率"。同一份代码,负载形态不同(连续 vs 间歇),实测性能差 16%。
另外顺手验证了/sys/kernel/debug/rknpu/load这个计数器本身也不可靠:900MHz 满速推理时它平均只报 9.7%,跑完空闲时反而报 75%。采样的负载值只能当参考,别当证据;证据是墙钟时间。
五、终极实验:频率 x 并发的完整矩阵
最后把 performance 档下的多实例并发也跑了,凑齐 2×4 矩阵:
| 并发实例 | @600MHz 单次/总吞吐 | @900MHz 单次/总吞吐 |
|---|---|---|
| 1 | 90.4ms / 8.1 | 66.2ms / 9.2 |
| 2 | 130.3ms /10.5(1.30x) | 112.7ms /10.6(1.15x) |
| 3 | 187.1ms /10.7(1.32x) | 161.7ms /10.5(1.14x) |
| 4 | 251.2ms / 7.3 ⬇ | 205.8ms / 8.6 ⬇ |
这张表里最有价值的是加粗的那一列:总吞吐的天花板在 600MHz 和 900MHz 下都是 ~10.5 FPS,纹丝不动。
把频率提高 50%,单实例延迟明显改善(90→66ms),但总吞吐一动不动——这说明多实例并发的瓶颈根本不在 NPU 计算单元,而在 DDR 带宽或驱动层的串行化。yolov5s 每次推理要在 DDR 里搬运约 30MB 的权重+激活值,多实例并发时大家抢的是同一条内存总线。
六、结论修正:Day8 那句话应该怎么说
| Day8 的说法(错) | Day9 修正后(对) | |
|---|---|---|
| 单帧 60ms 的本质 | “NPU 饱和了” | 这是本 BSP 上该管线的延迟地板(900MHz、C++ 连续负载),与 NPU 利用率无关 |
| batch=4 只 +6% | “NPU 没有摊销空间” | batch 摊销掉的是提交间隙,但带宽压力不降,所以收益有限 |
| 正确的优化方向 | — | 单路追延迟:锁频+连续提交;多路追吞吐:上限 ~10.5 FPS 等效,撞的是内存墙 |
这套"用实验修正自己昨天结论"的过程,比结论本身更值钱。面试时它对应的是一类高频问题——"你怎么定位性能瓶颈?"标准答案不是背名词,而是这样的方法论:
“先找到硬件的观测接口(sysfs 计数器、devfreq、trace),让数据说话;发现矛盾(load 78% 但性能不再提升)就设计对照实验(锁频 A/B);最后用’改 X 不改 Y,Y 没动’的方式归因(频率+50% 吞吐不动 → 带宽瓶颈)。”
七、复现方法(板端直接跑,无需交叉编译)
三个脚本都在仓库 python/ 目录,scp 上板即可:
python3 bench_npu_core.py /root/yolov5s.rknn20# 单实例延迟统计(p50/p95/min/max)python3 bench_npu_satur.py /root/yolov5s.rknn15# 1-4 实例并发 + load/freq 采样python3 bench_npu_gov.py30# governor A/B 对比(自动恢复原设置)bench_npu_gov.py的安全设计值得抄:跑之前先读出原 governor/min_freq/max_freq 存起来,实验完无条件写回,异常退出也不至于把板子留在 performance 档白耗电。
八、30 秒面试话术
「我在 RK3568 上做过一次 NPU 饱和度的系统排查:最初多 batch 只提升 6%,我判断是 NPU 饱和;后来用 sysfs 负载采样发现峰值只有 78%,继续查 devfreq 发现出厂 governor 把频率钉在 600MHz(最高 900MHz)。锁 performance 档后单实例提速 16%,但多实例总吞吐在两个频点下都卡在同一个值——说明并发瓶颈是 DDR 带宽不是 NPU 算力。这件事让我形成了一个习惯:DVFS 设备上做基准测试先锁频,性能归因必须用对照实验,不能拿一个反常数据直接下结论。」
写在最后
- 明天后计划做 RKNN 多路视频流的工程化(既然并发吞吐上限是 10.5 FPS 等效,多路场景就要认真做跳帧/降分辨率权衡了)。
- 本系列所有代码与数据都在 GitHub 仓库,欢迎取用。