[其他] 【OK1126BJ-S 开发板测评(四):NPU 推理与性能测试,3TOPS 到底能跑多快】

[复制链接]
20|0
怀揣少年梦 发表于 2026-9-3 11:25 | 显示全部楼层 |阅读模式

前一篇准备好了 FP16 和 INT8 两块"料",这一篇把它们丢进板子的 NPU 里,看看 3 TOPS 的纸面算力到底能跑多快。所有数据都在板端实测:帧率、CPU/NPU 占用、精度损失,一个一个来。

一、测试前的准备:一张能计算时间的图

测速最忌讳拍脑袋。PC 端 toolkit 连板推理那条路不能用来测性能——模型和图像每帧都要过一遍 WiFi adb,测的是网速不是 NPU 速度(前一篇"单次推理 30 秒"就是这么来的)。所以这次直接写板端 C 程序,交叉编译后推上去,librknnrt.so 本地推理,网络零干扰。

一共写了两个:基准程序 rknn_bench.c,和一套完整的 YOLOv5 检测管线 rknn_yolo_e2e.cc。测试从底层往上分四层,一层比一层接近真实应用:

  1. NPU 纯推理耗时。只计时 rknn_run(),同时用官方 RKNN_QUERY_PERF_RUN 硬件计数器交叉验证,两边对得上才算数。
  2. 全链路单帧耗时。输入量化拷贝 → rknn_run → 输出反量化,整条链掐表,这是常规部署方式下的真实帧率。
  3. 多实例并发。同时建 2~3 个 context 轮流推理,看吞吐能不能叠上去。
  4. 端到端应用帧率。读图、预处理、推理、NMS、画框存图一条龙,测的是应用每秒能处理几张真实图片(第四节)。

输入是 bus.jpg 缩到 640×640 的真实图像(不是随机噪声),INT8 预热 10 帧后连续测 200 帧,FP16 测 50 帧。

整套流程做成了 12 个可复现脚本(基准 9 个 + 端到端 3 个),在 WSL 里按序号跑,一个脚本出一个数据点,本文所有数字都能原样复现:

脚本 内容
01_connect ~ 09_collect 基准套件:连板 → 编译 → 部署 → INT8/FP16 基准 → 输入格式对比 → 多实例并发 → CPU 采样 → 日志回收汇总
e2e_run.sh 端到端管线:推程序 → 双模型单帧验证 → 30 帧循环计时 → 拉回检测框结果图
e2e_pull.sh / e2e_redeploy.sh 单独拉取板端检测结果图 / 板子重启后一键重新部署恢复

【代码块1:板端 C 基准程序核心计时逻辑(实测代码节选)】

/* 全链路计时:量化拷贝 + NPU + 反量化 */
double t0 = now_ms();
for (int i = 0; i < iters; i++) {
    rknn_inputs_set(ctx, 1, &in);          // 输入拷贝(含量化)
    rknn_run(ctx, NULL);                   // NPU 推理
    rknn_outputs_get(ctx, io.n_output, outs, NULL);  // 输出(含反量化)
    rknn_perf_run pr;                      // 官方硬件计数器交叉验证
    rknn_query(ctx, RKNN_QUERY_PERF_RUN, &pr, sizeof(pr));
    hw_us += pr.run_duration;
    rknn_outputs_release(ctx, io.n_output, outs);
}
printf("full_chain=%.2f ms | npu_perf_run=%.2f ms\n",
       (now_ms()-t0)/iters, (double)hw_us/1000.0/iters);

二、直接上结果:INT8 vs FP16

两种模型、四种测法跑完,数据全部拉表(INT8 200 帧、FP16 50 帧,官方计数器与本地计时交叉验证一致):

指标 FP16 INT8 说明
模型大小 14.90 MB 7.77 MB INT8 小 48%
NPU 纯推理 51.21 ms 25.21 ms 官方计数器 50.18/25.23 交叉一致
NPU 纯推理帧率 19.53 FPS 39.66 FPS NPU 真实算力
全链路单帧 148.29 ms(6.74 FPS) 66.85 ms(14.96 FPS) 含输入量化 + 输出反量化
全链路单帧(float 输入) 117.11 ms(8.54 FPS) runtime 自动量化多耗 50 ms
端到端应用帧率 139.69 ms(7.16 FPS) 32.96 ms(30.34 FPS) uint8 直喂 + 输出直读,见第四节
推理中 CPU 占用 8.91% 9.69% 基线 0.12%,四核大量富余
NPU 占用 接口不可读 接口不可读 load 节点恒 0,用多实例实测反推(第五节)
精度(INT8 对 FP16 检测框) 基准 IoU 0.959~0.992 5/5 目标全部一致,见第三节

file-20260902180750557.png

file-20260902180828321.png

有几个数字单独说一下。

INT8 恰好快一倍:NPU 纯推理 51.21 ms 对 25.21 ms,19.53 FPS 对 39.66 FPS,官方硬件计数器报 50.18 和 25.23,跟本地计时对得上。同一颗 NPU,换个量化格式吞吐直接翻倍。做实时检测的话,这个差距没有讨价还价的余地。

CPU 占用低得出乎意料。推理期间 us+sy 合计不到 10%(INT8 实测 9.69%、FP16 8.91%,基线只有 0.12%),四个 A53 基本闲着,那点开销全是输入输出数据在 CPU 上的搬运转换。换句话说,NPU 干活的时候 CPU 是空出来的,可以拿来跑业务逻辑——CAN FD 上报、协议解析、传图,互不抢资源。工业落地这点很关键。

输入格式的影响比想象中大。同一个 INT8 模型,喂 float 输入 117.11 ms,喂预量化 int8 输入只要 66.33 ms,慢了七成多。原因也不复杂:runtime 自动量化要在 CPU 上逐像素转换 640×640×3,这个活部署时自己提前做掉就行,是不花成本的提速。

还有一条更意外的:输入输出的交接方式再改一下,66.85 ms 还能降到 32.96 ms。不换模型、不超频,纯 API 用法问题。这个放到第四节细说。

三、精度损失怎么测:别只看体积和速度

只测速度不测精度等于耍流氓。测法是把端到端检测管线在板端跑两遍:同一张 bus.jpg,分别用 FP16 和 INT8 两版模型做完整推理(预处理 → NPU → 解码),把各自的检测框坐标、置信度、类别逐项对齐比较。

【代码块2:INT8 vs FP16 端到端检测结果对比(一手日志 e2e_run.log 节选)】

===== INT8 : 5 detections =====
  [person] conf=0.884 box=(208,244,286,506)
  [person] conf=0.868 box=(478,238,559,526)
  [person] conf=0.825 box=(110,238,230,534)
  [    bus] conf=0.701 box=( 91,129,554,467)
  [person] conf=0.334 box=( 79,353,122,516)

===== FP16 : 5 detections =====
  [person] conf=0.881 box=(208,242,286,508)
  [person] conf=0.859 box=(478,238,560,525)
  [person] conf=0.841 box=(109,237,232,534)
  [    bus] conf=0.702 box=( 91,129,555,465)
  [person] conf=0.319 box=( 79,355,121,515)

=== 逐框对齐:置信度偏差 / IoU ===
  bus     0.701 <-> 0.702   Δconf=0.001  IoU=0.992
  person  0.884 <-> 0.881   Δconf=0.003  IoU=0.985
  person  0.868 <-> 0.859   Δconf=0.009  IoU=0.984
  person  0.825 <-> 0.841   Δconf=0.016  IoU=0.972
  person  0.334 <-> 0.319   Δconf=0.015  IoU=0.959

file-20260902181026127.png

结论:
INT8 量化在这张图上几乎无损:5 个目标两版模型全部检出,零漏检零错判;逐框 IoU 全部大于 0.95,bus 的框 IoU 0.992,坐标偏差不超过 2 个像素;置信度最大偏差 0.016,有升有降。对安全帽识别这种"戴没戴"的二值判断,这个级别的波动完全无感。只有置信度阈值卡得极死、或者类别间差异很小的细粒度任务,才需要认真做量化精度评估(官方 accuracy_analysis 逐层对比是更硬的证据,我们实测三层输出余弦相似度都是 1.00000)。

四、从跑分到应用:端到端管线实测

回头看,前三层测试其实都在"伺候 NPU"——输入预先备好、输出全量转 float,为的是把 NPU 的能力单独测出来。但真实应用不长这样:一张 jpg 进去、一张画好框的图出来,中间还夹着预处理和 NMS。所以又写了第四个程序 rknn_yolo_e2e.cc,把完整检测管线在板端跑起来:

读图(jpg解码) → BGR2RGB + resize 640×640 → uint8 直喂 NPU → 推理 → 解码+NMS → 画框 → 存图

【代码块3:端到端管线核心逻辑(rknn_yolo_e2e.cc 节选)】

/* 输入: uint8 直喂 + pass_through=0, runtime 自动转 int8/fp16, 两种模型通吃 */
rknn_input in = {};
in.index = 0;
in.type = RKNN_TENSOR_UINT8;
in.fmt  = RKNN_TENSOR_NHWC;
in.pass_through = 0;
in.buf  = resized_img.data;

/* 输出: INT8 模型 want_float=0, 原始 int8 直读, 后处理用到哪个值才反量化哪个 */

for (int i = 0; i < loop; i++) {
    t0 = now_ms();
    rknn_inputs_set(ctx, 1, &in);
    rknn_run(ctx, NULL);                        /* NPU 推理 */
    rknn_outputs_get(ctx, io.n_output, outs, NULL);
    t1 = now_ms();  run_ms  += t1 - t0;         /* 计时1: 推理+IO */

    t0 = now_ms();
    post_process(...);                          /* 解码+NMS(按需反量化) */
    t1 = now_ms();  post_ms += t1 - t0;         /* 计时2: 后处理 */

    rknn_outputs_release(ctx, io.n_output, outs);
}

计时在同一循环里分段做,推理和后处理各掐各的表,端到端是两段之和。一开始用的是分两次循环各测一遍的办法,后来发现两次之间的系统波动会把数据搞脏,甚至算出过后处理耗时为负的笑话,才改成现在这样。

30 帧循环实测:

阶段 INT8 FP16
预处理(读图 + resize,单次另计) 33.81 ms 29.71 ms
推理 + IO 31.49 ms 138.12 ms
NMS 后处理 1.47 ms 1.57 ms
端到端(推理 + IO + NMS) 32.96 ms(30.34 FPS) 139.69 ms(7.16 FPS)

file-20260902181308536.png

最有意思的是拿这组数和第二节全链路对比:同一个 INT8 模型,NPU 推理都是 25 ms 左右,端到端却比全链路快了一倍(32.96 对 66.85)。账拆开就明白了。全链路 66.85 ms 里 NPU 只占 25.21 ms,剩下约 42 ms 大头是输出反量化——214 万个输出元素在 CPU 上逐个乘 scale 加 zp 转成 float,光这一项就吃掉约 40 ms。端到端版本改成 want_float=0 直读 int8 原始输出,后处理解码到哪个候选框、才反量化哪个值,一帧 NMS 只需要几百次乘法,IO 开销从 42 ms 压到 6 ms。部署时照抄这个写法就行。

FP16 那边连 IO 都天生吃亏:端到端 139.69 ms 里 NPU 只占 51.21 ms,剩下约 88 ms 全是格式转换(输入 uint8→fp16、输出 fp16→fp32)。精度高一档,搬运成本翻几倍。这是选 INT8 的又一条理由。

顺便说个插曲。FP16 第一版跑出来全是 conf=0.996 的饱和值,满屏零面积垃圾框,NMS 单帧跑了半秒多。查了半天,根因特别朴素:FP16 模型的输入张量类型是 FLOAT16,程序还在沿用 INT8 的套路(pass_through=1 硬塞量化后的 int8 数据),NPU 把这串 bit 按 fp16 解释,数值自然全错。正确写法是输入统一 uint8 + pass_through=0 让 runtime 自动转换;输出 INT8 模型设 want_float=0 直读、FP16 模型设 want_float=1 转 float32。改完之后 FP16 的 5 个检测框和 INT8 完全对齐,第三节的数据就是修复后的。

还有个口径问题要交代:循环计时不包含读图加 resize 的 30 ms 上下(INT8 33.81 ms、FP16 29.71 ms)。那基本是 JPEG 软解码的开销,真实相机管线从 V4L2 直接拿 NV12 帧,不走这条路,算进去反而冤枉板子。NMS 后处理只占端到端的 1%~5%,四核 A53 跑 640 输入的后处理毫无压力,以后加跟踪、加业务逻辑都有富余。

五、资源占用:多开几路看看余量

测完单路,又试了多路并行——工业场景经常要同时处理多个相机。同时建 2 个、3 个模型 context 轮流推理:

file-20260902181216679.png

实例数 聚合吞吐 每路帧率
1 14.96 FPS 14.96 FPS
2 14.85 FPS 7.42 FPS
3 15.02 FPS 5.01 FPS

结果有点反直觉:吞吐完全不随实例数增长。1 路 14.96 FPS,2 路 14.85,3 路 15.02,基本纹丝不动。单实例就已经把这颗 NPU 打满了,多开两三路只是让 NPU 把时间片切给各路,每路等比变慢。

这个结果顺手补上了一个遗憾:debugfs 的 rknpu/load 节点在这版内核恒为 0,NPU 占用率百分比一直读不出来。但"多开无增益"本身就是满载的最好证明——NPU 是串行调度推理任务的,没有并发算力可挤。

对多路需求的启示也很直接:别指望多开实例提速。要么用更小的输入或更轻的模型把单帧压到 10 ms 级,要么接受每路 5~7 FPS 的分时结果,要么外挂多颗 NPU。一台板子看两三个点位、每秒几帧够用的场景,这块板是够格的。

六、小结

这一篇的答案:INT8 跑 YOLOv5s,NPU 纯算力 39.66 FPS,落到完整检测管线(含 IO 和 NMS)还有 30.34 FPS,推理期间 CPU 占用不到 10%。

生产上选 INT8 几乎不用犹豫:快一倍、小一半、搬运成本低几倍,精度损失在这个场景下无感。CPU 解放是最实用的部分——算法在 NPU、业务在 CPU,互不打架。最后一点,部署姿势本身就是优化:uint8 直喂加输出直读,模型一行不改,帧率翻倍。

您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:一切皆有可能

51

主题

487

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部
0