结合Optrace与QNN Profiler协同调试: 多工具交叉验证AI推理瓶颈
2026/7/3 13:45:12 网站建设 项目流程

作者注:本文基于项目实现——Qwen2.5-7B 在 高通跃龙IQ-9075 EVK开发板上的推理调试实践,完整记录了利用 Optrace 与 QNN Profiler 进行三层交叉验证,定位 decode 阶段性能瓶颈的全过程。所有命令均在 IQ9075-EVK 上验证通过,产物归档于artifacts/

目录

  • 0. 先澄清一个误解
  • 1. 环境与工具
  • 2. 第1层:Genie应用层profile——定位耗时阶段
  • 3. 第2层:QNN Profiler 算子级——分离“计算”与“调度等待”
  • 4. 第3层:Optrace 硬件级——定位等待的硬件资源类型
  • 5. 三层交叉验证结论
  • 方法论要点
  • 附录A:采集隔离说明
  • 附录B:产物清单(artifacts/)

0. 先澄清一个误解

“Optrace”和“QNN Profiler”常被当作两个独立软件,实际上它们是同一套qnn-profile-viewer工具搭配不同 reader 插件的两种性能统计方式:

方式reader 插件统计指标对应瓶颈
应用/调度总览genie-t2t-run --profile(JSON)TTFT, prefill/decode 速率耗时长的阶段
QNN Profiler 算子级libQnnHtpProfilingReader.so/libQnnChrometraceProfilingReader.so每算子 cycle 数 + Wait (Scheduler) 周期计算 vs 调度等待
HTP 硬件级libQnnHtpOptraceProfilingReader.soHTP 多核时间线、DMA/内存带宽、sequencer flow等待的硬件资源类型

交叉验证的逻辑是逐级下探
第1层定位耗时长的阶段 → 第2层定位耗时算子及计算/等待占比 → 第3层定位等待来自的硬件资源竞争。缺一层则归因不完整。

1. 环境与工具

开发平台:高通跃龙IQ-9075 EVK开发板

设备信息:

ubuntu@evk9075.local (IQ-9075, aarch64 Ubuntu 24.04, 34GB)
$ dpkg -l | grep libqnn1 ii libqnn1 2.43.0.260128-0ubuntu1 arm64 Qualcomm Neural Network SDK - Libraries $ ls /usr/lib/libQnnHtpOptraceProfilingReader.so /usr/lib/libQnnHtpProfilingReader.so \ /usr/lib/libQnnChrometraceProfilingReader.so # 三种 reader 全部就位 $ which qnn-profile-viewer qnn-net-run qnn-context-binary-generator /usr/bin/qnn-profile-viewer /usr/bin/qnn-net-run /usr/bin/qnn-context-binary-generator $ ls /opt/qc-ai/models/qwen2.5-7b-local-6split/ genie_config.json htp_backend_ext_config.json tokenizer.json qwen2_5_7b_instruct_part_{1..6}_of_6.bin # w8a16, 6-split context binary

检查已安装的 QNN 库和工具:

$ dpkg-l|greplibqnn ii libqnn2.43.0.260128-0ubuntu1 arm64 Qualcomm Neural Network SDK - Libraries $ls/usr/lib/libQnnHtpOptraceProfilingReader.so /usr/lib/libQnnHtpProfilingReader.so /usr/lib/libQnnChrometraceProfilingReader.so# 三种 reader 全部就位$whichqnn-profile-viewer qnn-net-run qnn-context-binary-generator /usr/bin/qnn-profile-viewer /usr/bin/qnn-net-run /usr/bin/qnn-context-binary-generator $ls/opt/qc-ai/models/qwen2.5-7b-local-6split/ genie_config.json http_backend_ext_config.json tokenizer.json qwen2_5_7b_instruct_part_{1..6}_of_6.bin# w8a16, 6-split context binary

被测模型与运行方式

  • 模型:Qwen2.5-7B-Instruct,w8a16 量化,编译为 6-split context binary。
  • 运行栈:Genie 通过libGenie.sodlopen 调用 HTP,在其上提供推理接口。本文借用该设备与模型做独立的 profiling 探索,所有采集在/tmp/optrace-blog/下进行。

Optrace 的开启机制:optrace 不是独立开关,而是编译期qnn-context-binary-generator --profiling_level detailed --profiling_option optrace产出*_schematic.bin,执行期qnn-net-run --profiling_option optrace收集事件,后处理qnn-profile-viewer --reader libQnnHtpOptraceProfilingReader.so --schematic ...重建多核时间线。这个“编译期埋点”特性是后续工程约束的根源。

2. 第1层:Genie应用层profile——定位耗时阶段

2.1 配置准备

把设备上现有的 config 复制一份到/tmp工作区,把ctx-bins改成绝对路径:

artifacts/rewrite_config.py内容:

importjson p="/tmp/optrace-blog/genie_config_run.json"d=json.load(open(p))base="/opt/qc-ai/models/qwen2.5-7b-local-6split/"d["dialog"]["engine"]["model"]["binary"]["ctx-bins"]=[base+bforbind["dialog"]["engine"]["model"]["binary"]["ctx-bins"]]d["dialog"]["tokenizer"]["path"]="/tmp/optrace-blog/tokenizer.json"d["dialog"]["engine"]["backend"]["extensions"]="/tmp/optrace-blog/htp_backend_ext_config.json"json.dump(d,open(p,"w"),indent=2)

2.2 HTP profiling 需要独占设备

当设备上已有推理负载时,再起一次 profiling 会话会竞争 SMMU/DSP 共享内存,直接失败:

fastrpc memory mapforfd:49with length:704643072failed with error: 0x1 Failed to map weights buffer to device!err:1002

结论:HTP profiling 应在独占 NPU的环境下进行。profiling 前让设备进入空闲、无其他 HTP 推理占用,采集完成后再恢复原有负载。这是 HTP 共享内存模型决定的通用要求,与具体上层服务无关。

2.3 profile 文件未生成的踩坑

genie-t2t-run--profile FILE在进程正常return EXIT_SUCCESS后才写文件(对应源码main.cpp:1416):

if(profiler){profiler->getJsonData();// 才把 JSON 写到 g_profilePath}returnEXIT_SUCCESS;

timeout 300跑长 prompt 时,decode 到约 300s 进程被强杀,未走到写文件那行。解决方法是改用短 prompt让其快速触发 EOS 自然退出,并把 timeout 放宽到 540s:

$printf'Reply with exactly one word: ready'>/tmp/optrace-blog/prompt.txt $ADSP_LIBRARY_PATH=/usr/lib/rfsa/adspLD_LIBRARY_PATH=/usr/lib\timeout540genie-t2t-run\-c/tmp/optrace-blog/genie_config_run.json\--prompt_file/tmp/optrace-blog/prompt.txt\--profile/tmp/optrace-blog/gp_basic.json\--loginfo

2.4 第1层结果(artifacts/genie_profile_basic.json

"GenieDialog_query":{"num-prompt-tokens":8,"time-to-first-token":{"value":166485,"unit":"us"},// ~166 ms"prompt-processing-rate":{"value":48.05,"unit":"toks/sec"},// prefill"num-generated-tokens":2,"token-generation-rate":{"value":20.62,"unit":"toks/sec"},// decode"token-generation-time":{"value":97013,"unit":"us"}}
  • TTFT 166ms 与工程基线 ~176ms 吻合,方法可靠。
  • prefill (48 tok/s) 是 decode (20.6 tok/s) 的 ~2.3 倍,符合 LLM 并行 vs 自回归特性。
  • 单看此层无法判断 decode 慢的根因是算子计算耗时还是在等调度/内存,需下钻到第2层。

3. 第2层:QNN Profiler 算子级 —— 分离“计算”与“调度等待”

3.1 Genie 无法直接产出算子级数据

genie-t2t-run --profile走的是 GenieProfile API,只产应用层 JSON,不会触发 QNN backend 的 detailed profiling log 落盘。在http_backend_ext_config.json的 cores 里加"profiling_level": "linting"后 genie 能跑通,但不产qnn-profiling-data.log—— linting log 需要qnn-net-run这类直接调 QNN API 的工具才会写。

qnn-net-run对 LLM 的难点是 context binary 里的 graph 输入是 attention 激活而非原始 token,input_list构造复杂。但实测decode graph 的输入极简单

3.2 decode graph 输入就是一个 int32 token id

qnn-context-binary-utilitydump context binary 结构:

GRAPH0name=prompt_ar128_cl4096_1_of_6input=input_idsdims=[1,128]dtype=INT_32(prefill)GRAPH1name=token_ar1_cl4096_1_of_6input=input_idsdims=[1,1]dtype=INT_32(decode)

decode graph 输入 [1,1] int32 = 一个 token id,完全可构造!这意味着能直接对真实 Qwen context binary 做算子级 profiling,无需重编译。

3.3 两层 config 的 schema 踩坑

直接把 genie 用的http_backend_ext_config.json喂给qnn-net-run --config_file会全部报Unknown Key

[ERROR]Unknown Key=devices/0/soc_model passedinconfig[ERROR]Unknown Key=devices/0/cores/0/profiling_level passedinconfig[ERROR]Backend extension configfiledoes not specify a valid profiling level.

qnn-net-run期望的是两层 config

artifacts/backend_extension_config.json

{"backend_extensions":{"shared_library_path":"/usr/lib/libQnnHtpNetRunExtensions.so","config_file_path":"/tmp/optrace-blog/htp_config.json"}}

artifacts/htp_config.json

{"devices":[{"profiling_level":"listing"}]}

3.4 多 graph 的 input 匹配踩坑 (2!=1)

context 里有 2 个 graph (prefill + decode),--input_list只给一个会报2 != 1。逗号分隔的___,file跳过语法对retrieve_context不生效。

正确解法是用--retrieve_context_list(YAML)graphName精确指定,并用enable_graphs只启用目标 graph:

artifacts/ctx_list.yaml

version:1contexts:-name:qwen_decodebinaryFilePath:/opt/qc-ai/models/qwen2.5-7b-local-6split/qwen2_5_7b_instruct_part_1_of_6.bincontextConfig:context_priority:normalenable_graphs:["token_ar1_cl4096_1_of_6"]inputFilePath:-graphName:token_ar1_cl4096_1_of_6inputFilePath:/tmp/optrace-blog/netrun/decode_input_list.txt

构造 int32 token id 输入(artifacts/make_decode_input.py,token id151643是 Qwen 合法 token):

importstructwithopen("/tmp/optrace-blog/netrun/input_decode.raw","wb")asf:f.write(struct.pack("<1i",151643))open("/tmp/optrace-blog/netrun/decode_input_list.txt","w").write("/tmp/optrace-blog/netrun/input_decode.raw\n")

3.5 执行 + 解析

执行qnn-net-run采集算子级 log:

$ADSP_LIBRARY_PATH=/usr/lib/rfsa/adspLD_LIBRARY_PATH=/usr/lib\qnn-net-run--backend/usr/lib/libQnnHtp.so\--retrieve_context_list/tmp/optrace-blog/ctx_list.yaml\--config_file/tmp/optrace-blog/backend_extension_config.json\--profiling_levelbackend\--output_dir/tmp/optrace-blog/netrun--log_levelinfo# 关键日志:# Profiling turned on; level = 3# Graph token_ar1_cl4096_1_of_6 execution finished with result 0

用标准 reader 解析成 CSV 和 chrometrace:

$ qnn-profile-viewer\--reader/usr/lib/libQnnHtpProfilingReader.so\--input_logqnn-profiling-data-qwen_decode_0.log\--outputlinting_htp.csv $ qnn-profile-viewer\--reader/usr/lib/libQnnChrometraceProfilingReader.so\--input_logqnn-profiling-data-qwen_decode_0.log\--outputlinting_chrometrace.json# 可在 chrome://tracing 打开

3.6 第2层结果(artifacts/qnn_linting_htp.csv

Execute Stat 1 (graph token_ar1_cl4096_1_of_6 = part_1 of 6 decode) Number of HVX threads used : 4 Accelerator (critical path execute) : 12875 cycles Input OpId_2 : 0 cycles Wait(Scheduler) 0 /Gather:OpId_21 : 7411 cycles Wait(Scheduler) 420 Resources: DMA Output OpId_3 : 4958 cycles Wait(Scheduler) 86 Resources: DMA Accelerator (execute) time : 1384 us Accelerator (execute excluding wait) : 159 us ← 真正计算 QNN (execute) time : 2271 us

这是交叉验证的核心证据

  • 计算与调度等待彻底分离:真正计算仅 159us,总 execute 1384us,等待占 88.5%
  • Wait(Scheduler)字段直接量化每个算子等待前序 DMA/HVX 完成的周期数。
  • Gather(embedding 查表)走 DMA 资源,7411 cycles 中 420 是调度等待,属内存/IO bound而非纯计算。
  • 第1层 decode 慢的根因由此明确:decode 阶段算子大量时间花在等待 DMA/调度,而非 HMX/HVX 计算

说明:part_1_of_6的 decode graph 仅暴露 3 个顶层算子(embedding gather + output),因 6-split 把 transformer 层分散到 6 个 context。要看完整 transformer 算子(attention/matmul 等),对part_2~6重复本流程即可,方法完全一致。

4. 第3层:Optrace 硬件级 —— 定位等待的硬件资源类型

4.1 schematic 约束与解除

optrace reader 重建 HTP 多核时间线必须有*_schematic.bin,而它只能在编译期用--profiling_option optraceqnn-context-binary-generator产出。先用真实 Qwen context binary 验证这个约束:执行期开 optrace 能收集到 log,但 reader 没有 schematic 时直接拒绝解析:

# 设备上对 Qwen decode graph 开 optrace 收集,log 产出正常$ qnn-net-run...--profiling_leveldetailed--profiling_optionoptrace# 日志:Profiling option set; option = 1 / Profiling turned on; level = 2# 产出 qnn-profiling-data-qwen_decode_0.log# 但用 optrace reader 解析预编译的 Qwen context binary:$ qnn-profile-viewer--reader/usr/lib/libQnnHtpOptraceProfilingReader.so\--input_logqnn-profiling-data-qwen_decode_0.log\--outputoptrace_out# 输出:No Valid Input Schematics / Error printing stats.

原因在于 optrace 事件数据在编译期嵌入 schematic,执行期仅收集,没有 schematic 则多核时间线、DMA 带宽、sequencer flow 事件无法重建。当前 Qwen context binary 是预编译的,目录里没有 schematic:

$ls/opt/qc-ai/models/qwen2.5-7b-local-6split/|grepschematic# 空,无 schematic.bin

解除约束的途径是本机C:\Qualcomm\AIStack\QAIRT\2.44\bin\x86_64-linux-clang\下的一整套 Linux x86_64 原生工具链,含qnn-model-lib-generatorqnn-context-binary-generatorqnn-onnx-converterlibQnnHtp.solibHtpPrepare.solibQnnHtpOptraceProfilingReader.so,可被 WSL Ubuntu-24.04 直接从/mnt/c/Qualcomm/...调用,无需安装:

# WSL 验证,依赖库全部满足$ wsl-dUbuntu-24.04 --bash-c'QNN=/mnt/c/Qualcomm/AIStack/QAIRT/2.44.0.260225; export LD_LIBRARY_PATH=$QNN/lib/x86_64-linux-clang:$LD_LIBRARY_PATH; ldd $QNN/bin/x86_64-linux-clang/qnn-model-lib-generator | grep "not found" || echo ALL_DEPS_OK; $QNN/bin/x86_64-linux-clang/qnn-context-binary-generator --version'# ALL_DEPS_OK# QNN SDK v2.44.0.260225143659

4.2 完整 optrace 工作流

用 SDK 自带qnn_model_float(InceptionV3 小模型)走完整三阶段。model.so只在 host 被 finalize 加载,不部署到设备,因此 x86_64-linux 的 model.so 即可。

阶段1:WSL 编译 model.so,使用Makefile.linux-x86_64share/QNN/converter/下的 wrapper 源码:

$ wsl --make-fMakefile.linux-x86_64QNN_MODEL_LIB_NAME=libqnn_model_float.so# 产出 libs/x86_64-linux-clang/libqnn_model_float.so (ELF 64-bit x86-64)

注意:strnDup符号定义在平台相关的jni/linux/QnnModelPal.cpp,需一并复制到jni/,否则会出现undefined symbol: strnDup

阶段2:WSL 用 optrace 选项编译 context binary

$ qnn-context-binary-generator\--profiling_leveldetailed--profiling_optionoptrace\--modellibqnn_model_float.so--backendlibQnnHtp.so\--config_filebackend_extension_config.json\--binary_fileqnn_model_float_optrace.bin--output_dirout/# 产出:# qnn_model_float_optrace.bin (context binary, 53KB)# convReluModel_schematic.bin (schematic, 210KB) -- optrace reader 必需

SDK 版本踩坑:2.44 编译的 bin 推到设备(libqnn 2.43)报Using newer context binary on old SDK err:5000,必须用2.42 SDK编译才向后兼容 2.43 设备。且 2.42 才正常产出schematic.bin

阶段3:设备执行 + reader 解析,将 context.bin 与 schematic push 到设备:

# 设备上执行,收集 optrace log$ qnn-net-run--backendlibQnnHtp.so\--retrieve_contextqnn_model_float_optrace.bin\--input_listinput_list_float_abs.txt\--config_filebackend_extension_config.json\--profiling_leveldetailed--profiling_optionoptrace# Graph convReluModel execution finished with result 0# optrace reader + schematic 重建 chrometrace$ qnn-profile-viewer\--readerlibQnnHtpOptraceProfilingReader.so\--input_logqnn-profiling-data_0.log\--schematicconvReluModel_schematic.bin\--outputoptrace_chrometrace.json# [1/11] Processing payload (schematic)# [4/11] Resolving tensor->op dependencies# [6/11] Render of Chrometrace# [8/11] QHAS HTML Generation# Complete! - optrace_chrometrace.json (314KB)

4.3 第3层结果

optrace chrometrace 含1749 个事件、346 个 HTP 算子事件,每个算子带 optrace 独有的硬件资源标注,这是标准 linting 给不出的信息:

top ops by Duration (cycles): q::*OutputSlice qnn=Output dur=56166cyc flags=['dma_wait'] ← 明确等待 DMA q::ConvLayer.opt.inconv qnn=Relu dur=18714cyc flags=['uses_hvx'] ← 走 HVX 计算单元 distinct QNN op types: Relu 258, Output 50, ... 346 HTP op events, 172 flagged DMA(约 50% 走内存搬运) ...

通过 optrace 可清晰看到哪些算子触发了dma_waituses_hvx等硬件级标注,从而精确定位等待的硬件资源类型。

optrace 相对第 2 层的增量价值

  • 第 2 层 linting 给每个算子一个总的Wait (Scheduler)周期数。
  • 第 3 层 optrace 给每个算子标注Flagsdma_wait/uses_hvx/ dma),把等待分类到具体硬件资源——是等 DMA 搬运、用HVX计算,还是sequencer排序依赖,并可生成多核时间线与内存带宽图,在chrome://tracing或 Perfetto UI 打开。

4.4 对 Qwen 的应用方式

完整 optrace 需要重新编译模型带 optrace 选项。对本文的 Qwen2.5-7B:

  • 预编译 context binary 没有 schematic,optrace reader 拒绝解析,4.1 节已证。
  • 补 schematic 需对 Qwen 的某 split 用qnn-context-binary-generator --profiling_option optrace重编译,model.so可由qnn-onnx-converter从已有的ONNX生成,工具链已在 WSL 就位。
  • 本文用 SDK 示例模型完整验证了 optrace 工作流与 schematic 机制,方法对 Qwen 完全适用,仅 Qwen 单 split 编译耗时较长留作后续。
  • 在 schematic 拿到前,第 2 层 linting 的Wait (Scheduler)Resources: DMA是可靠的降级指标,已能区分计算与调度等待,只是不能像optrace那样细分到 DMA 带宽与 sequencer 级别。

5. 三层交叉验证结论

瓶颈类型判据层级本例证据
计算 bound第2层:算子 cycles 高、Wait 低、excl-wait ≈ execute第2层本例 excl-wait 159us 远小于 execute 1384us,非纯计算 bound
调度 bound第2层:Wait (Scheduler) 高、execute > excl-wait第2层本例主因:等待占 88.5%
内存/DMA bound第2层 Resources=DMA + 第3层 optrace flags=[<dma_wait>]第2+3层Gather 走 DMA;optrace 示例模型中 172/346 op flagged DMA, OutputSlice 标 dma_wait 56166cyc
应用/IO bound第1层:TTFT/prefill/decode 速率异常第1层本例 TTFT 正常,速率符合预期,非应用层瓶颈

最终归因
Qwen2.5-7B 在 IQ-9075 上的 decode 阶段,主要瓶颈是调度/内存等待(DMA 搬运相关),而非 HTP 计算本身。这把 decode 慢的模糊感受拆成了可定位、可量化的硬件资源竞争问题,而这是单工具(只看第1层速率或只看第2层总耗时)做不到的。

方法论要点

  1. 逐级下钻,不跳层
    第1层定位耗时阶段 → 第2层区分算子计算与等待 → 第3层定位等待的硬件资源类型。

  2. 每层验证上一层的假设
    若第2层 excl-wait ≈ execute,则第3层 optrace 不会有内存瓶颈,可省。

  3. optrace需编译期埋点
    schematic 对预编译模型不可直接用,但 WSL 里C:\Qualcomm\AIStack\QART\<ver>\bin\x86_64-linux-clang\自带完整 Linux 工具链,可重编译补 schematic。
    linting 的 Wait(Scheduler)是 schematic 拿到前的可靠降级指标。

附录A:采集隔离说明

本文所有 profiling 命令均在/tmp/optrace-blog/(设备)与/tmp/optrace-wsl/(WSL)工作区下执行,仅引用现有模型与 SDK 路径,不修改任何既有部署。分析产物归档于artifacts/

附录B:产物清单(artifacts/)

文件说明
genie_profile_basic.json第1层 Genie profile 真实数据
qnn_linting_htp.csv第2层 算子级 cycle + Wait (Scheduler)
qnn_linting_chrometrace.json第2层 chrometrace,可在 chrome://tracing 打开
optrace_chrometrace.json第3层 optrace chrometrace,含 HTP 多核时间线与 dma_wait/uses_hvx 标注
qnn_model_float_optrace_242.binoptrace 编译的 context binary (2.42, 兼容设备 2.43)
convReluModel_schematic.binoptrace reader 必需的 schematic
qnn-profiling-data-qwen_decode_optrace.log第3层 optrace 原始 log,含 schematic 约束证据
wsl_build_242.sh/device_run_demo_v2.sh完整 optrace 工作流复现脚本
cross-validation-analysis.md三层数据汇总与归因
ctx_list.yaml/backend_extension_config.json/htp_config.json复现所需配置
run_linting_qnn-net-run.log/run_optrace_qnn-net-run.log原始执行日志

希望这篇实战记录能帮助你在 AI 推理性能调优中,有效利用多工具交叉验证,精准定位瓶颈!
如有问题或建议,欢迎在评论区交流讨论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询