NPU 推理的 CPU 开销,怎么才能真正降下来?——(QCS6490)Radxa Dragon Q6A 十四组实测

azouyana 2026-11-13 10:23:32

📚 QCS6490 NPU 系列
① 部署 QAIRT SDK | ② ResNet50 NPU 实战 | ③ Qualcomm AI Hub 指南 | ④ Hexagon CPU 占用实测 | ⑤ 降 CPU 开销实战(本篇)

平台:Radxa Dragon Q6A · Qualcomm QCS6490(Hexagon NPU = HTP v68,4×A78 + 4×A55)
系统:Ubuntu 24.04.4 LTS aarch64 · kernel 6.18.2-3-qcom
环境:QAIRT SDK 2.42.0.251225
实测日期:2026-11-13


一句话回答

上一篇(④)证明了「NPU 推理时 CPU 有占用是正常的」,本篇回答下一个问题:那能不能把它降下来?

能,而且幅度很大 —— 同一模型、同一块板子,每帧 CPU 时间从 4.36 ms 压到 0.60 ms(−86%),吞吐从 140 FPS 涨到 694 FPS。

但真正把开销降下去的,不是 那些听起来很高级的参数(native IO、mmap、ION 共享内存加起来不到 10%),而是三件很朴素的事:

手段一句话实测每帧 CPU相对基线
绑大核taskset -c 4-7,一行命令4.36 → 2.06 ms−53%
全内存 I/O输入输出放 /dev/shm2.06 → 1.80 ms−13%
batch=44 帧合并成一次 execute2.06 → 0.60 ms−71%
而反例每帧起一个新进程4.36 → 89.6 ms+1956%

下面把 14 组对照实验的数据、机理和踩到的坑一次说清。


一、先看清楚:CPU 时间花在哪个线程上

优化之前先定位。跑推理的同时用 top -H 看线程级占用:

img

7 个线程,只有主线程在烧 CPU(45%~63%),其余 6 个线程全是 0.0%。

一次推理只占用一个 CPU 线程

这条观察决定了后面所有优化手段的边界:

img

结论:单流推理的 CPU 开销是「单线程串行」的 —— 加核、开线程池都没用,能做的只有三件事:

  1. 让这一个线程跑得更快(换大核、提频率);
  2. 让它少干活(少读一次、少拷一次、少转一次);
  3. 让它每帧少跑几趟(batch 摊薄固定开销)。

二、为什么「绑大核」能砍掉一半

QCS6490 是 4×Cortex-A78(大)+ 4×Cortex-A55(小)的 big.LITTLE 结构,Linux 默认 schedutil 调频:

CPU最高频率空闲时实测(scaling_cur_freq)
cpu0~3(A55)1958 MHz691 MHz(cpu0)
cpu4(A78)2707 MHz1517 MHz
cpu7(A78)2707 MHz2707 MHz

而 NPU 推理是典型的短时尖峰负载:单帧只有 5~7 ms,跑完就睡。
schedutil 的升频有延迟和滞回,根本来不及在这几毫秒内把 A55 拉到最高频,
调度器还可能把线程在小核之间来回迁移,每次迁移都要重建 cache。

所以实测结果就不奇怪了:

不绑核               4.36 ms CPU/帧   (随机落在小核 / 迁移中)
绑小核 0-3           4.43 ms CPU/帧   (稳定跑在低频 A55 上)
绑大核 4-7           2.06 ms CPU/帧   (−53%)
绑单核 cpu7          1.81 ms CPU/帧   (−59%,最稳)

![绑核策略对比:换个 CPU 就少一半 CPU 时间]
(https://i-blog.csdnimg.cn/communtity/93f2369f051541a0ba7523e205bc9fdc.png "#left")

💡 绑单核反而最好:既拿到了 A78 的高频,又完全避免迁移。
既然推理本来就是单线程热点,taskset -c 7 是最省事的一行优化。

注意:绑核只影响 CPU 时间(同样的活干得更快),不会改变 NPU 的计算速度。
但墙钟时间也跟着从 7.15 ms 降到 4.88 ms —— 因为这段「等待」本身也是主线程在忙等。


三、实验设计

项目设定
模型ResNet50 INT8,AIMET 量化,context binary(25.9 MB)
推理单元HTP v68,qnn-net-run
样本500 帧/例(首帧的上下文加载被摊薄)
主指标子进程 utime + stime ÷ 帧数 = 每帧 CPU 时间
副本指标墙钟/帧、占用核数、FPS

口径与上一篇(④)完全一致,不用 top 采样,避免抹平毫秒级尖峰。

NPU 推理 CPU 开销优化:14 组实测


四、全部结果

4.1 总表

#手段墙钟 ms/帧CPU ms/帧vs 基线FPS
1基线(不绑核,默认参数)7.154.361.00×140
2绑小核 A55(0-3)7.294.431.02×137
3绑大核 A78(4-7)4.892.060.47×204
4绑单大核(cpu7)4.881.810.41×205
5大核 + --use_native_output_files6.452.250.52×155
6大核 + --use_mmap4.982.220.51×201
7大核 + 输入张量缓存5.332.470.57×188
8大核 + ION 共享内存4.752.030.47×210
9大核 + rpc_polling_time=04.612.180.50×217
10大核 + 全内存 I/O4.451.800.41×225
11大核 + batch=41.440.600.14×694
12全开(3+5+9+10)4.191.830.42×239
A14 进程并发2.944.531.04×340
A24 进程并发 + 各绑一个大核3.382.590.59×296
B50 次「单帧进程」215.1989.6420.6×4.6
C参照:纯读输入(不推理)2.071.240.28×—

两轮独立测量(500 帧/例)的一致性:基线 4.36 / 4.62,大核 2.06 / 1.87,batch=4 0.60 / 0.61。
单例波动约 ±10%,表中取较新的一轮。
B 行的单帧进程数据在 DSP 会话正常时测得(50 次平均);会话异常时会退化到秒级,见「坑 3」。

4.2 三个最反直觉的数字

① 那些「高级」参数几乎是安慰剂。
native 输出、mmap、张量缓存、ION 共享内存,单看都是 2.0~2.5 ms,和只绑大核的 2.06 ms 在噪声范围内。
真正省下来的 2.3 ms 是绑核省下的,不是它们。

② FastRPC 参数调不动。
rpc_polling_time 从 9999 µs 调到 0,每帧 CPU 只从 4.11 变到 4.14(不绑核)/ 2.28 到 2.18(绑核,另做的一组同口径 A/B)。
说明 QNN 的 QnnGraph_execute 不是靠忙轮询等 DSP,而是真正阻塞(sys 时间只占 10%~20%)。
想靠调这个参数「让 CPU 闲下来」是行不通的。

③ 每帧起一个新进程 = 灾难。
50 次「启动进程 → 加载 25 MB context → 跑 1 帧 → 退出」平均 215 ms/帧,
是长驻进程(7.15 ms)的 30 倍。

单帧进程  :  ████████████████████████████████  215 ms
长驻进程  :  █                                  7 ms

把上面几组数据拆成 user / sys 看,能看出一件事:绑核是把两头一起砍,batch 是直接把固定开销抽掉。

每帧 CPU 时间的 user / sys 构成


五、逐类手段,哪些值得做

5.1 调度:唯一「白嫖」的优化 ✅

taskset -c 7 ./your_infer_app        # 绑单大核
# 或
taskset -c 4-7 ./your_infer_app      # 绑大核组

−53%,零代码改动,零精度风险。 如果只做一件事,就做这个。

同理,如果是自己写的程序,也可以在主线程里用 pthread_setaffinity_np();
更彻底的是在启动时把 CPU 频率策略改成 performance(需要 root)。

5.2 内存与传输:单用收益 < 10% ⚠️

手段原理实测结论
--use_native_output_files输出不做 uint8→float 反量化2.25 ms省不出多少;墙钟反而涨到 6.45 ms,不建议盲开
--use_mmapcontext binary 用 mmap 载入2.22 ms只影响启动,500 帧摊薄后看不见
--max_input_cache_tensor_sets=-1复用输入 tensor,不重建2.47 ms反而更慢(内存压力),且长序列会吃几百 MB
--io_tensor_mem_handle_type=ion输入输出走 ION 共享 buffer2.03 ms唯一略有正收益的,但也在噪声内

它们不是没用,而是在这个量级上不是瓶颈。
如果你的模型 I/O 是 4K 图像、点云、或者大 batch,这些手段的比重会显著上升。
另外第 12 行「全开」只有 1.83 ms,和单独第 10 行的 1.80 ms 持平 —— 叠加也没有额外收益。

5.3 I/O:全内存能再省 13% ✅

cp input.raw /dev/shm/ && qnn-net-run --input_list /dev/shm/list.txt --output_dir /dev/shm/out

参考量级:纯 cat 读 500 次输入文件(0.60 MB/帧)就要 1.24 ms CPU/帧。
也就是说,读文件这一项就占了基线 4.36 ms 的近 30%(cat 还有 fork/exec 开销,属于上界估计)。

生产环境中更彻底的做法:

  • 输入用 shared buffer / dmabuf 从相机直通,根本不落盘;
  • 输出不写文件,直接在内存里取 argmax。

5.4 FastRPC 参数:这个场景下无效 ❌

通过 backend extensions 可以调三个旋钮:

{
  "devices": [{
    "soc_model": 60,
    "dsp_arch": "v68",
    "cores": [{
      "perf_profile": "burst",
      "rpc_control_latency": 1000,
      "rpc_polling_time": 0
    }]
  }]
}
参数含义实测效果
perf_profileDVFS 策略(burst / high_performance / …)墙钟略降,CPU 时间基本不变
rpc_control_latencyRPC 控制线程的保持唤醒窗口(µs)无明显变化
rpc_polling_time轮询等待上限(µs,0~9999)0 与 9999 无差别

结论:QNN 的 execute 是事件阻塞式等待,不是忙轮询。
这个参数主要影响 RPC 线程的唤醒策略,对 QNN 这条路径收益很小。
(如果你直接用 libcdsprpc 自己发 RPC,它的权重就大得多。)

5.5 batch:最大杠杆,−71% ✅✅

这是本篇最强的单项优化:4 帧合并成一次 execute,每帧 CPU 从 2.06 ms 降到 0.60 ms,吞吐 204 → 694 FPS(3.4×)。

# 1) 把 4 帧 raw 拼成一个文件(batch 维度的数据要连续)
cat frame0.raw frame1.raw frame2.raw frame3.raw > batch4.raw
# 2) 每行一个 batch4 文件
qnn-net-run --batch_multiplier=4 --input_list list_batch4.txt ...

为什么省这么多?因为每帧的固定开销(文件读取、tensor 构建、RPC 往返、DSP 侧调度)被 4 帧平摊了:

batch=1:  [固定][计算]  [固定][计算]  [固定][计算]  [固定][计算]
batch=4:  [固定][计算 计算 计算 计算]
                ↑ 固定开销只付一次

batch 摊薄:同一份绑大核配置下算 4 帧的总墙钟

⚠️ 代价是延迟:攒满 4 帧才发一次 execute,首帧等待从 4.9 ms 变成 5.8 ms。
所以 batch 是「吞吐优化」,不是「延迟优化」。

💡 数据口径提醒:--batch_multiplier=4 时如果 input list 每行只有 1 帧的数据,
qnn-net-run 会把同一份数据复制 4 份 —— 读取量只有真实情况的 1/4,测出来的收益是假的。
上表用的是「真拼接 4 帧」的公平口径,收益依然成立。

5.6 并发:换吞吐,不换每帧 CPU ↔️

方式吞吐每帧 CPU说明
单进程140 FPS4.36 ms—
4 进程并发340 FPS4.53 ms每帧成本不变,但 4 个流一起跑
4 进程 + 各绑一个大核296 FPS2.59 ms每帧成本降了,吞吐受 DSP 调度限制

推理是单线程热点,所以开多个流能直接变多倍吞吐:
实测 4 进程 340 FPS(2.4×),代价是 4 个核被占;
跑得快时瓶颈会从 CPU 转到 DSP,再加进程就不再线性了。
对「多路摄像头 / 多路视频流」的场景,这是最直接的扩容方式。

5.7 进程模型:必须常驻 ⛔

模式每帧墙钟每帧 CPU
长驻进程(500 帧一次加载)7.15 ms4.36 ms
每帧一个新进程(加载 25 MB context)215 ms89.6 ms

启动一次 qnn-net-run 要加载 25.9 MB 的 context binary 并建立 DSP 会话,约 0.2 秒。
任何「每帧调一次命令行工具」的架构都是不可接受的。

正确做法:进程常驻 + 上下文只加载一次 + 循环 execute(C++/Python 绑定均可)。


六、CPU 开销到底能不能「避免」

严格说:不能全部避免,但可以分层削减。把每个环节拆开看:

img

环节能否搬走 / 消除手段备注
读输入⚠️ 只能加速tmpfs / shared buffer / 预读数据总要从某处来
量化、拼 tensor✅ 可搬到图内模型 I/O 直接定义成 uint8(--preserve_io / AIMET quantize_io)本例输入已是 float32,量化在 DSP 内完成
FastRPC 下发✅ 靠摊薄batch每帧一次的固定成本 → 每 4 帧一次
计算— 本来就在 DSP——
反量化✅ 可省掉--use_native_output_files,下游直接吃 uint8ResNet50 只有 1000 个输出值,实测收益≈ 0;输出大的模型才有意义
写输出✅ 可省掉不落盘,直接内存后处理本例 500 帧 × 4 KB 不大,视频场景要当心
进程启动✅ 必须消除常驻进程单帧新进程是 30× 的灾难

一句话总结:能搬到 DSP 的(量化/反量化)、能摊薄的(RPC)、能省掉的(落盘),都搬走;
剩下的就是「CPU 读数据 + 等 DSP」,这部分只受频率和核影响 —— 所以绑大核收益最大。


七、按场景给配方

场景 A:实时单帧(延迟敏感,如相机取流)

taskset -c 7 ./infer_app                                   # 主进程绑 cpu7
# 输入输出都走 /dev/shm,或直接 shared buffer(不落盘)

预期:4.36 → 1.8 ms CPU/帧,墙钟 7.15 → 4.5 ms。

场景 B:离线批处理(吞吐优先)

taskset -c 4-7 ./infer_app --batch_multiplier=4            # 攒 4 帧再算(实测值)

预期:每帧 CPU 0.6 ms,吞吐 ~690 FPS。

场景 C:多路视频流

# 4 路进程,每路绑一个大核
for i in 0 1 2 3; do
  taskset -c $((4+i)) ./infer_app --stream $i &
done

预期:吞吐 ≈ 300340 FPS,每帧 CPU 2.64.5 ms。

场景 D:产品级 C++ 集成

  • 常驻进程,context 加载一次;
  • 输入 buffer 复用(不要每帧 malloc/memcpy);
  • 输出直接给下游,不落盘;
  • 主线程绑大核,工作线程不参与推理。

官方 examples/QNN/SampleApp 是这套流程的参考实现。


八、🕳 踩到的坑

坑 1:ION 共享内存 + 输入张量缓存 → 直接崩

$ qnn-net-run ... --io_tensor_mem_handle_type=ion --max_input_cache_tensor_sets=-1
rc=17    Graph Execution failure

两者单独用都正常,500 帧规模叠加就失败。
推测是 ION 侧一次性注册的 buffer 总量超限(500 × 588 KB ≈ 294 MB)。
→ --max_input_cache_tensor_sets=-1 慎用,要么改用 --max_input_cache_size_mb,要么干脆别开。

坑 2:dma_buf 在这个后端上不支持

Input output tensor memHandle type = 3. But backend does not support property =1104

--io_tensor_mem_handle_type=dma_buf 直接初始化失败,只有 ion 可用。

坑 3:反复建/拆 DSP 会话会把 FastRPC 卡死 ⚠️

做「每帧一个新进程」的对照实验时,中途 kill 了一个正在推理的进程,结果:

$ ps aux | grep qnn-net-run
radxa  5321 ... D+   qnn-net-run ...        # D 状态,kill -9 无效
$ qnn-net-run ...                            # 之后所有推理
real    1m48s                                # 全部超时

进程卡在内核态等待(D 状态),kill -9 无效,之后所有推理请求全部超时。
本次实验到这里就无法继续,需要重启板子才能恢复。

卡死状态下的对照测量(连跑 10 次单帧进程):

单帧进程(不绑核)  每次墙钟 67589 ms | 每次 CPU 49.6 ms
单帧进程(绑 cpu7) 每次墙钟 61439 ms | 每次 CPU 40.7 ms

墙钟 60 多秒、CPU 只有几十毫秒 —— 说明线程 99.9% 的时间在等内核/远端超时,
不是在做计算。这个特征可以用来区分「DSP 真的卡了」和「只是模型慢」。

教训:

  • 不要在推理中途 kill 进程,应先让它自然结束;
  • 生产环境用常驻进程 + 优雅退出,避免频繁创建/销毁 DSP 会话;
  • 压测脚本里给每个子进程加 timeout,但不要对推理进程本身用 SIGKILL。

坑 4:batch 的 I/O 口径陷阱

--batch_multiplier=4 不会自动帮你把 4 帧数据拼起来。
input list 每行只有 1 帧时,运行时会把这份数据复制 4 份 ——
读 I/O 只有 1/4,测出来的「收益」是假的。必须自己拼接 raw 文件。


九、小结

  1. CPU 开销是单线程串行的:7 个线程里只有主线程忙,加核无用;
  2. 最强的单项优化是绑大核:taskset -c 7,4.36 → 1.81 ms(−59%),零代码改动;
  3. 其次是 batch:4 帧一次 execute,每帧 CPU 0.60 ms(−71%),吞吐 694 FPS,代价是攒帧延迟;
  4. I/O 不可忽视:输入输出全放 /dev/shm 再省 13%;纯读文件本身就占 1.24 ms/帧;
  5. native / mmap / ION / FastRPC 参数是小头:合计 < 10%,别把它们当成救命稻草;
  6. 进程模型决定成败:每帧新进程 215 ms/帧(30×),必须常驻;
  7. 能用一句话概括的判断法:CPU 时间 ÷ 墙钟 ≈ 0.4~0.6 核属于正常;
    降到 0.2 ms/帧以下已接近纯 I/O 下限,再压就得动架构(零拷贝、DSP 侧预处理)。

参考

...全文
28 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
内容概要:本文构建了一个包含PWM控制VSC-HVDC链路的IEEE30节点电力传输系统,并基于Simulink平台完成了系统的建模仿真。该模型聚焦于高压直流输电(HVDC)技术在区域电网互联中的应用,采用电压源型换流器(VSC)与脉宽调制(PWM)技术实现高效、灵活的电能传输。研究重点在于分析VSC-HVDC链路在复杂多节点电网中对潮流分布、系统稳定性及电能质量的调控能力,特别关注其在区域电网互联、新能源大规模接入以及跨区域电力交换等场景下的动态响应特性与控制性能,为现代智能电网的发展提供技术支撑与仿真验证手段。; 适合人群:具备电力系统、电气工程或自动化等相关专业背景的研究生、科研人员及工程技术人员。; 使用场景及目标:①用于研究VSC-HVDC在多节点电网(如IEEE30节点)中的建模与仿真方法;②分析PWM控制策略下HVDC链路对电网潮流分布、电压稳定性和故障穿越能力的影响;③为区域电网互联、新能源并网及智能电网调度等课题提供仿真基础和技术验证。; 阅读建议:此资源以Simulink仿真实现为核心,建议读者在掌握电力电子技术和电力系统分析基础知识的前提下,结合仿真模型深入理解VSC-HVDC的工作原理与控制策略。在学习过程中,应着重关注模型参数设置、控制环路设计及仿真结果的分析,以达到理论与实践相结合的目的。
内容概要:本研究针对不确定综合能源系统中的鲁棒调度优化问题,提出一种基于核密度估计-累积分布函数(KDE-CDF)置信带的建模方法,用以精确刻画风能、光伏及负荷等不确定性因素的非正态概率分布特征。通过构建KDE-CDF模型,能够有效捕捉实际运行中新能源出力与负荷需求的波动性与随机性,并在此基础上设计鲁棒优化调度框架,提升系统在不确定性环境下的运行可靠性与经济性。该方法结合Matlab编程实现,综合考虑系统运行成本、峰谷差、碳排放等多重目标,实现多维度协调优化。研究还探讨了与智能优化算法(如粒子群算法)相结合的应用路径,增强了模型的求解效率与工程实用性,为含多种储能装置、电转气设备及碳捕集系统的复杂综合能源系统提供了科学的调度决策支持。; 适合人群:具备电力系统分析、优化理论基础及Matlab编程能力的研究生、科研人员,以及从事综合能源系统规划、运行与调度工作的工程技术人员。; 使用场景及目标:①解决高比例可再生能源接入背景下综合能源系统调度中的不确定性建模难题;②提升调度方案在面对新能源强波动性时的鲁棒性与适应能力;③为融合多类型灵活性资源(如储能、电转气、碳捕集)的综合能源系统提供精细化、可落地的优化运行策略。; 阅读建议:建议读者在掌握概率统计、随机优化与电力系统运行基础知识的前提下,结合文中提供的Matlab代码进行实践操作,重点关注KDE-CDF置信带的构建流程及其在鲁棒优化模型中的嵌入机制,通过调整输入数据分布、参数设置与场景配置开展仿真对比实验,深入理解不确定性建模精度对调度决策质量的影响,进而掌握从数据驱动角度提升系统鲁棒性的关键技术路径。
内容概要:本文系统研究了含分布式电源的配电网可靠性评估方法,基于Matlab代码实现了完整的评估模型与算法框架。研究深入分析了分布式电源接入后对配电网结构与运行特性的影响,重点探讨了其对供电可靠性的作用机制。采用序贯蒙特卡洛模拟法对系统各类状态进行抽样与仿真,综合考虑了分布式电源出力的随机性、负荷波动性以及网络拓扑变化等多种不确定性因素,精确计算了系统及负荷点的多项可靠性指标。通过在标准测试系统上的仿真实验,验证了所提方法的有效性和实用性,为新型电力系统环境下配电网的规划、运行与可靠性提升提供了有力的技术支撑和决策依据。; 适合人群:具备电力系统分析、概率统计及可靠性基础理论知识,熟悉Matlab编程语言,从事电力系统规划、运行、可靠性评估等相关工作的科研人员、高校研究生以及电力行业工程师。; 使用场景及目标:①掌握含分布式电源的现代配电网可靠性评估的核心理论与关键技术;②学习并实践基于序贯蒙特卡洛模拟的电力系统可靠性分析算法的Matlab实现方法;③为实际工程中分布式电源的优化配置、配电网的升级改造以及制定可靠性提升策略提供科学的仿真工具和量化分析依据。; 阅读建议:建议读者结合文中提供的Matlab代码,逐行阅读并运行程序,重点理解系统建模、状态抽样、潮流计算与指标统计等核心模块的实现逻辑。可尝试调整分布式电源的类型、容量、接入位置及负荷水平等参数,观察其对系统可靠性指标的影响趋势,从而深化对系统动态行为与关键影响因素的理解。

8,027

社区成员

发帖
与我相关
我的任务
社区描述
本论坛以AI、IoT、PC 、XR、Auto等核心板块组成,为开发者提供便捷及高效的学习和交流平台。 高通开发者专区主页:https://qualcomm.csdn.net/
物联网人工智能开源 企业社区 北京·东城区
社区管理员
  • csdnsqst0050
  • chipseeker
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧