8,027
社区成员
发帖
与我相关
我的任务
分享📚 QCS6490 NPU 系列
① 部署 QAIRT SDK | ② ResNet50 NPU 实战 | ③ Qualcomm AI Hub 指南 | ④ Hexagon CPU 占用实测(本篇)
平台:Radxa Dragon Q6A · Qualcomm QCS6490(Hexagon NPU = HTP v68,8 核 A78/A55)
系统:Ubuntu 24.04.4 LTS aarch64
环境:QAIRT SDK 2.42.0.251225
实测日期:2026-11-12
正常。 Hexagon 只是把「计算」放到 NPU,但数据搬运、量化、通信全在 CPU 上,
所以 NPU 推理时 CPU 不可能是 0。
真正要看的不是「CPU 高不高」,而是——
CPU 花的时间,是不是比 NPU 该有的水平高了一个数量级?
下面用三组对照实验把这个判断标准量化出来。
一次 NPU 推理,CPU 要做这些事:

绿色那一格(④)才是 NPU,其余 6 个环节全部跑在 CPU 上。
而且注意:推理越快,这些固定开销占比就越高。一个 6 ms 的推理,CPU 忙 4 ms 是完全合理的。
测量方式见第四章。三个口径分别是:
utime + stime(不是 top 的采样值)| 场景 | 每帧墙钟 | 每帧 CPU 时间 | 占用核数 | 占 8 核比例 | CPU/墙钟 |
|---|---|---|---|---|---|
| ResNet50 INT8 @ NPU | 6.72 ms | 3.99 ms | 0.59 核 | 7.4% | 59% |
| MobileNetV2 INT8 @ NPU | 10.96 ms | 8.13 ms | 0.74 核 | 9.3% | 74% |
| MobileNetV2 float @ CPU 回退 | 132.80 ms | 205.17 ms | 1.54 核 | 19.3% | 155% |
ResNet50 统计 500 帧、MobileNetV2 统计 100 帧,均已平摊上下文加载开销。

① 从「整机」看,CPU 占用很低:0.59 和 0.74 核,只占 8 核的 7%~9%。
所以如果你在 top 里看总 CPU 使用率,会觉得「一点不高」。
② 从「单核」看,推理期间 CPU 几乎是满的:CPU/墙钟 = 59%~74%,
意味着在推理那几毫秒里,一直有半个到三分之四个核在忙。
如果你盯着某个核(top -1 或 pidstat -t),会看到明显的尖峰。
③ 两个口径都对,只是问的问题不同。
关键对比:同一个模型,跑 NPU 和跑 CPU,CPU 时间差多少。
MobileNetV2 INT8 @ NPU : 8.13 ms CPU / 帧
MobileNetV2 float @ CPU : 205.17 ms CPU / 帧
─────────────────
约 25 倍
| 现象 | 判断 |
|---|---|
| 每帧 CPU 时间在 几毫秒量级,占用 < 1 核 | ✅ 正常,确实在 NPU 上 |
| 每帧 CPU 时间 上百毫秒,占用 1.5+ 核 | ❌ 算子回退到 CPU 了 |
| 单核 100% 打满、其余全闲、时延远超官方指标 | ⚠️ 疑似 RPC 轮询 / 单线程瓶颈 |
本例的 float 版模型被 HTP 拒绝,日志里出现 69 次:
ERROR: [Qnn] <E> The SocModel doesn't support FP16
WARNING: [Qnn Delegate] Qnn Backend op validation failed with error: 3110
换成 INT8 量化模型后,同样命令:
# 0 条 FP16 报错 → 全程 NPU
时延从 132.8 ms 降到 10.96 ms,CPU 时间从 205 ms 降到 8.13 ms。
💡 这就是最实用的排查手法:先看日志有没有
op validation failed/doesn't support,
再看 CPU 时间量级。 两者都对上,再决定要不要优化。
toptop 是按固定间隔采样再平均,会抹平几毫秒级的尖峰——而 NPU 推理恰好就是尖峰型负载。
getrusage 取子进程真实 CPU 时间import resource, subprocess, time, os
def measure(cmd, env, n_infer):
before = resource.getrusage(resource.RUSAGE_CHILDREN)
t0 = time.monotonic()
subprocess.run(cmd, env=env) # 跑推理
wall = time.monotonic() - t0
after = resource.getrusage(resource.RUSAGE_CHILDREN)
cpu = (after.ru_utime - before.ru_utime) + (after.ru_stime - before.ru_stime)
cores = cpu / wall # 占用了几个核
print(f"墙钟 {wall*1000/n_infer:.2f} ms/帧 | "
f"CPU {cpu*1000/n_infer:.2f} ms/帧 | "
f"占用 {cores:.2f} 核 ({cores/os.cpu_count()*100:.1f}%)")
完整脚本:~/QAIRT/demo/bench_cpu_usage.py
| 注意点 | 说明 |
|---|---|
| 样本要够多 | 单帧测不准,至少 100 帧,把上下文加载(~0.2 s)摊掉 |
| 区分 user / sys | user 高 = 量化/反量化/内存拷贝;sys 高 = 文件 I/O、驱动调用 |
| 输入路径要绝对 | 本例第一次测量就因为相对路径失败(Graph Execution failure),白白浪费一轮 |
本例 ResNet50 的 500 帧总 CPU 是 user 1.52 s + sys 0.48 s —— **user 占 76%**,
说明主要开销在用户态的数据处理,而不是系统调用。
| 手段 | 原理 | 预期效果 |
|---|---|---|
| 加大 batch / 多帧一次 execute | 摊薄每帧的固定开销(RPC、量化) | 帧数越多,每帧 CPU 越低 |
| C++ + shared buffer(zero-copy) | 省掉 memcpy 和重复量化 | 显著 |
--use_native_output_files | 跳过输出反量化 | 中等 |
| 用 context binary 而非 TFLite delegate | delegate 每帧有额外解释开销 | 本例 3.99 vs 8.13 ms |
--htp_performance_mode 2(Burst) | 减少 DVFS 抖动,缩短墙钟 | 减少墙钟,CPU 时间基本不变 |
注意表中的 3.99 ms 还包含读 raw 文件的 I/O。
真实部署用 C++ + 内存张量时,每帧 CPU 时间还能更低。
| 误判 | 纠正 |
|---|---|
| 「总 CPU 才 7%,说明没用到 NPU」 | 反过来才对:CPU 占比低、时延也低,才是 NPU 在工作 |
| 「某个核 100%,肯定出问题了」 | NPU 推理期间单核忙是正常的,看的是量级 |
「用 top 看平均值就够」 | 会掩盖尖峰,必须用 getrusage 或高频采样 |
| 「读文件的时间也算推理耗时」 | 那部分算 I/O,压测时要单独剔除或摊薄 |
pidstat -t / top -1,看整机就用总 CPU%,两个口径都要报。qairt/2.42.0.251225/docs/QAIRT-Docs/QNN/libcdsprpc~/QAIRT/demo/bench_cpu_usage.py