结合Optrace与QNN Profiler协同调试: 多工具交叉验证AI推理瓶颈
作者注本文基于项目实现——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开发板设备信息ubuntuevk9075.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-Instructw8a16 量化编译为 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.jsondjson.load(open(p))base/opt/qc-ai/models/qwen2.5-7b-local-6split/d[dialog][engine][model][binary][ctx-bins][basebforbind[dialog][engine][model][binary][ctx-bins]]d[dialog][tokenizer][path]/tmp/optrace-blog/tokenizer.jsond[dialog][engine][backend][extensions]/tmp/optrace-blog/htp_backend_ext_config.jsonjson.dump(d,open(p,w),indent2)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:1416if(profiler){profiler-getJsonData();// 才把 JSON 写到 g_profilePath}returnEXIT_SUCCESS;用timeout 300跑长 prompt 时decode 到约 300s 进程被强杀未走到写文件那行。解决方法是改用短 prompt让其快速触发 EOS 自然退出并把 timeout 放宽到 540s$printfReply 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\--loginfo2.4 第1层结果artifacts/genie_profile_basic.jsonGenieDialog_query:{num-prompt-tokens:8,time-to-first-token:{value:166485,unit:us},// ~166 msprompt-processing-rate:{value:48.05,unit:toks/sec},// prefillnum-generated-tokens:2,token-generation-rate:{value:20.62,unit:toks/sec},// decodetoken-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 激活而非原始 tokeninput_list构造复杂。但实测decode graph 的输入极简单。3.2 decode graph 输入就是一个 int32 token id用qnn-context-binary-utilitydump context binary 结构GRAPH0nameprompt_ar128_cl4096_1_of_6inputinput_idsdims[1,128]dtypeINT_32(prefill)GRAPH1nametoken_ar1_cl4096_1_of_6inputinput_idsdims[1,1]dtypeINT_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 Keydevices/0/soc_model passedinconfig[ERROR]Unknown Keydevices/0/cores/0/profiling_level passedinconfig[ERROR]Backend extension configfiledoes not specify a valid profiling level.qnn-net-run期望的是两层 configartifacts/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只启用目标 graphartifacts/ctx_list.yamlversion: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.pytoken id151643是 Qwen 合法 tokenimportstructwithopen(/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.csvExecute 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%。WaitScheduler字段直接量化每个算子等待前序 DMA/HVX 完成的周期数。Gatherembedding 查表走 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 optrace由qnn-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-generator、qnn-context-binary-generator、qnn-onnx-converter、libQnnHtp.so、libHtpPrepare.so、libQnnHtpOptraceProfilingReader.so可被 WSL Ubuntu-24.04 直接从/mnt/c/Qualcomm/...调用无需安装# WSL 验证依赖库全部满足$ wsl-dUbuntu-24.04 --bash-cQNN/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.2602251436594.2 完整 optrace 工作流用 SDK 自带qnn_model_floatInceptionV3 小模型走完整三阶段。model.so只在 host 被 finalize 加载不部署到设备因此 x86_64-linux 的 model.so 即可。阶段1WSL 编译 model.so使用Makefile.linux-x86_64与share/QNN/converter/下的 wrapper 源码$ wsl --make-fMakefile.linux-x86_64QNN_MODEL_LIB_NAMElibqnn_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。阶段2WSL 用 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 qnnOutput dur56166cyc flags[dma_wait] ← 明确等待 DMA q::ConvLayer.opt.inconv qnnRelu dur18714cyc flags[uses_hvx] ← 走 HVX 计算单元 distinct QNN op types: Relu 258, Output 50, ... 346 HTP op events, 172 flagged DMA约 50% 走内存搬运 ...通过 optrace 可清晰看到哪些算子触发了dma_wait、uses_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 没有 schematicoptrace 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层 ResourcesDMA 第3层 optrace flags[dma_wait]第23层Gather 走 DMAoptrace 示例模型中 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层定位耗时阶段 → 第2层区分算子计算与等待 → 第3层定位等待的硬件资源类型。每层验证上一层的假设若第2层 excl-wait ≈ execute则第3层 optrace 不会有内存瓶颈可省。optrace需编译期埋点schematic 对预编译模型不可直接用但 WSL 里C:\Qualcomm\AIStack\QART\ver\bin\x86_64-linux-clang\自带完整 Linux 工具链可重编译补 schematic。linting 的 WaitScheduler是 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 必需的 schematicqnn-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 推理性能调优中有效利用多工具交叉验证精准定位瓶颈如有问题或建议欢迎在评论区交流讨论。

相关新闻