Skip to main content

qnn-debug

MNN QNN(高通 HTP/NPU)后端的问题定位、修复与算子适配。覆盖运行时报错(1002/6000/1003/6004)、模型转换/算子校验失败、推理结果异常、精度不达标、以及性能问题;并指导为 QNN 新增/适配算子。核心方法:用 MNN2QNNModel --dump_intermediate_outputs 一次性导出 QNN 全部中间张量,与 CPU(fwd=0) 基线逐张量对比,直接定位首个出错算子(旧的 testMNNFromOnnx.py 截断二分已降为回退手段);区分“真 bug/量化/HTP fp16 精度”;新增算子先查 SDK 算子文档(MasterOpDef/HtpOpDefSupplement)再实现。

소스 정보

저장소
alibaba/MNN
최근 소스 활동
2026년 9월 22일 07:42
감지된 SKILL.md 언어
중국어
스타
16,226
포크
2,476

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
4 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
qnn-debug
description
MNN QNN(高通 HTP/NPU)后端的问题定位、修复与算子适配。覆盖运行时报错(1002/6000/1003/6004)、模型转换/算子校验失败、推理结果异常、精度不达标、以及性能问题;并指导为 QNN 新增/适配算子。核心方法:用 MNN2QNNModel --dump_intermediate_outputs 一次性导出 QNN 全部中间张量,与 CPU(fwd=0) 基线逐张量对比,直接定位首个出错算子(旧的 testMNNFromOnnx.py 截断二分已降为回退手段);区分“真 bug/量化/HTP fp16 精度”;新增算子先查 SDK 算子文档(MasterOpDef/HtpOpDefSupplement)再实现。
# MNN QNN 后端 定位 / 修复 / 适配 SKILL > **触发条件**:用户报告 QNN / NPU(高通 HTP)后端任何问题,或要求适配算子。常见表述:"QNN 结果不对/NPU 精度差"、"QNN 报错 1002/6000/1003/6004"、"graphFinalize/graphExecute 失败"、"模型转换失败 / validateOpConfig failed"、"LLM 在 NPU 上输出乱码"、"QNN 性能差"、"给 QNN 加/支持 XXX 算子"、"某算子 QNN 不支持回落 CPU"。 ## 概述 QNN 后端有**两条执行路径**,排查前先分清在跑哪条(详见 [reference.md · 两条执行路径](./reference.md#后端内部机制定位时必须知道的)): - **在线 finalize 路径**:加载普通 `.mnn`(视觉/CNN),运行时逐算子构图 → `graphFinalize` → 整图执行。入口多为 `ModuleBasic`。报错码常见 **1002(finalize)/6000(execute)**。 - **离线预编译路径**:LLM 经 `llmexport.py`→`generate_llm_qnn.py` 生成预编译 QNN 二进制,`llm_demo` 加载运行。报错码常见 **1003/6004(运行时 IO 与图定义不一致)**、转换期 **validateOpConfig failed**。 本 SKILL 覆盖三类工作,可组合: - **A. 数值/精度 & 报错定位**(主线):**建立可信基线 → dump 全部中间张量比对定位首个出错点(回退:截断二分)→ 读误差模式并数值验证 → 区分真 bug / 量化 / HTP fp16 精度 → 打桩定位到代码**。 - **B. 新增/适配算子**:**先查 SDK 算子文档(MasterOpDef/HtpOpDefSupplement/SupportedOps) → 照现有算子模板实现并注册 → 二分探测验证**。见 [新增 / 适配 QNN 算子](#新增--适配-qnn-算子)。 - **C. 性能定位**:开 QNN Profile → 找过多 quant/dequant、数据搬运、CPU fallback → 优化。见 [性能定位与优化](#性能定位与优化)。 ### 问题分诊(先按症状选路径) | 症状 / 错误 | 路径 | 去哪一节 | |---|---|---| | `error 1002` graphFinalize 失败 / `could not create op` | 在线 | [步骤 3.5](#步骤-35--graphfinalize--graphexecute-失败时启用-qnn-错误日志) + [新增/适配算子](#新增--适配-qnn-算子) | | `error 6000` graphExecute 失败 / 输入全零、结果恒为 bias | 在线 | 先试 `shapeMutable=false`(见关键约束) + [步骤 3.5](#步骤-35--graphfinalize--graphexecute-失败时启用-qnn-错误日志) | | 结果不对 / 精度差 / 与 CPU 不一致 | 在线 | [步骤 0→4](#定位流程):dump 中间张量比对(回退:截断二分)| | `error 1003/6004` 运行时报错(离线/LLM) | 离线 | [运行时报错 1003/6004](./reference.md#案例-7--离线llm-路径-10036004输入输出与离线图定义不一致) | | `validateOpConfig failed` / 转换失败(离线/LLM) | 离线 | [新增/适配算子](#新增--适配-qnn-算子)(同查 OpDef) + [reference 案例 8](./reference.md#案例-8--转换期算子校验失败-validateopconfig) | | LLM 输出乱码 / 数值异常(能跑不报错) | 离线 | [reference 案例 9 · 量化精度](./reference.md#案例-9--llm-离线推理结果乱码量化参数问题) | | 性能不达预期 | 两者 | [性能定位与优化](#性能定位与优化) | | 要新增/适配某算子 | 两者 | [新增/适配算子](#新增--适配-qnn-算子) | > A/B 常交织:定位到"某 OpType 没实现/实现有 bug"就转 B,补完再用 A 验证。 ### 核心原则 1. **先建立可信基线**:任何对比都先确认 `CPU fp32` 与 ONNX 一致(`TEST_SUCCESS` / diff ≈ 1e-4)。参考 txt 与 input.txt 必须是**同一次**生成的,否则会误判(见坑 1)。 2. **一次性 dump 全部中间张量**(首选,替代反复截断):用 `MNN2QNNModel <sdk> <soc> <arch> <model.mnn> <out> --dump_intermediate_outputs` 生成 debug 版模型,它把**每个** QNN native 激活提升为 `APP_READ` 图输出;在真机上跑一遍即把全部中间张量连同 `manifest_*.tsv` 落盘,再与 CPU 基线**逐张量**比对,一次定位首个出错算子——不必每探一个点就重转一次模型。截断二分(`testMNNFromOnnx.py <model> <tensor>`)保留为**回退手段**(dump 跑不起来、或想快速二分少数点时用)。见 [步骤 1](#步骤-1--一次性-dump-全部中间张量并比对首选)。 3. **看误差模式,别只看 diff 数值**:每通道常数、全常数、转置、NaN 各有明确含义(见 [reference.md](./reference.md#误差模式速查))。用 Python 对照 bias/权重等**数值验证**你的假设,而不是猜。 4. **三方对比区分“bug”还是“精度”**:QNN-fp16、**CPU-fp16**(fp16 地板,fp32 累加)、CPU-fp32(可信基线)一起对比(`qnn_probe.sh` 一次给全)。真 bug 会在某算子处 **QNN 突跳而 CPU-fp16 不跳**;fp16 精度问题是**平滑累积**、经 Pool/GAP 会**下降**。需要时再拿 OpenCL-fp16 交叉验证。 5. **能定位就停**:定位到首个出错算子 + 数值证据即可下结论;改代码前先 `git blame` 看该处是否近期改动。 6. **闭环沉淀**:每次非平凡定位/修复/适配完成后,若有可复用经验,**主动**按 [复盘:回写 reference](#复盘自动总结并回写-reference) 追加案例——让本 skill 越用越强。 ### 关键约束 > **严禁访问 `schema/private/` 和 `source/internal/`。** > **结果异常先试一招**:若模型输入形状固定,先用 `shapeMutable=false`(`Session_Input_Inside`)跑一遍。QNN 在线路径在 `Session_Input_User`(`shapeMutable=true`,多数入口默认)下有**输入不被拷入、首个算子吃全零**的已知问题;`shapeMutable=false` 零代码规避。详见 [reference.md 案例 1](./reference.md#真实案例)。 > **两套构建目录别搞混**:`build/`(macOS 主机构建,`MNN_QNN=OFF`)只提供 `MNNConvert` 等主机工具;真机 `libMNN.so` 来自 `project/android/build_64`(`MNN_QNN=ON`,NDK arm64)。**改后端代码后要在 `build_64` 里 `make MNN` 并 push 它产出的 `libMNN.so`**,push 错目录的库是最常见的“修了没效果”原因。 --- ## 前置依赖(环境准备) | 依赖 | 用途 | 检测 | |------|------|------| | **adb + 真机** | 设备已连接,`/data/local/tmp/MNN/` 下已就绪 `ModuleBasic.out`、`libMNN.so`、`libQnnHtp*.so`、`libc++_shared.so` 等 | `adb devices`;`adb shell ls /data/local/tmp/MNN/` | | **主机 MNNConvert** | ONNX→MNN 转换(截断后重转) | `ls build/MNNConvert`(或 `which mnnconvert`) | | **python 环境** | `testMNNFromOnnx.py` 依赖 `onnx onnxruntime numpy` | `python3 -c "import onnx,onnxruntime,numpy"` | | **Android QNN 构建** | 改后端代码后重编 `libMNN.so` | `grep MNN_QNN: project/android/build_64/CMakeCache.txt` 应为 `ON` | > 设备上运行都要 `export LD_LIBRARY_PATH=.`(在 `/data/local/tmp/MNN/` 下)。 ### 必背:ModuleBasic 命令与后端编号 ``` ./ModuleBasic.out <model.mnn> <dir> <runMask> <forwardType> <loops> <threads> <precision> ``` - **forwardType**:CPU=`0`,OpenCL=`3`,**QNN=`16`**(QNN 注册为 `MNN_FORWARD_QNN`) - **precision**:Normal=`0`,High=`1`,Low=`2`;QNN 里 `mUseFP16 = (precision != High)` - `dir` 内需有 `input.txt`、`input.json` 和 `<outputName>.txt` 参考;比对阈值为 `1%`(`absMaxV*0.01 < diffmax` 判失败) > 用户给的复现命令示例:`./ModuleBasic.out onnx/test.mnn onnx 0 5 1 4 2`(QNN, fp16)。 ### 必背:MNN2QNNModel + 中间张量 dump(定位主力,见步骤 1) ``` MNN2QNNModel <qnnSDKPath> <socId> <hexagonArch> <src.mnn> <outDir> [totalShapeNum] [shape...] [--dump_intermediate_outputs] ``` - 常见 SoC:8Gen2→`socId 43 / arch 73`,8Gen3→`57 / 75`,8Elite→`69 / 79`。 - 加 `--dump_intermediate_outputs` 生成 **debug 版**离线模型(`outDir/<name>.mnn` + `.bin`):它把 QNN 图里每个 native 张量提升为 `APP_READ` 输出;**该标志已烘焙进模型**,运行时无需再传任何 flag,跑一遍即自动 dump。 - **对普通 CNN 同样适用**:MNN2QNNModel 接受任意 `.mnn`,所以在线路径的模型也能用这条离线 dump 通道拿到全部中间张量(在线 `ModuleBasic` 本身不透传 dump flag)。 - 输出落盘:默认写到模型旁的 `qnn_intermediate_outputs/`;设环境变量 `MNN_QNN_DUMP_DIR` 改目录。每次执行产出一个 `manifest_NNNNNN.tsv` + 每张量一个 raw 文件。详见 [reference.md · QNN 中间张量 dump](./reference.md#qnn-中间张量-dump定位主力)。 > 在线路径若要在自己的 runner 里开 dump:`backendConfig.flags = MNN_QNN_DUMP_INTERMEDIATE_OUTPUTS`(`1<<16`,见 `MNNForwardType.h`)。`ModuleBasic.out` 未透传该 flag,故在线模型走上面的 MNN2QNNModel 通道最省事。 ### 离线 / LLM 路径的准备(仅 LLM/NPU 预编译模型需要) ```bash source $QAIRT/bin/envsetup.sh # 设 QNN_SDK_ROOT 等 # 1) 导出 NPU 版 MNN(量化):关键 --generate_for_npu --quant_bit 4 --act_bit=16 --sym --smooth --hqq python3 transformers/llm/export/llmexport.py --path <model> --export mnn --dst_path <out> --generate_for_npu ... # 2) MNN → QNN 预编译离线图(报错→转"转换期校验失败") python3 transformers/llm/export/npu/generate_llm_qnn.py --model <mnn> --soc_id=57 --dsp_arch=v75 # 3) 设备运行 adb shell "cd /data/local/tmp && LD_LIBRARY_PATH=. ./llm_demo <model>/config_qnn.json prompt.txt" ``` > 除 `libMNN.so` 外,离线路径还需 push QNN SDK 运行库:`libQnnHtp.so`/`libQnnSystem.so`/`libQnnHtpV<arch>Stub.so`/`libQnnHtpV<arch>Skel.so`(来自 `$QNN_SDK_ROOT/lib/{aarch64-android,hexagon-v<arch>/unsigned}`)。 --- ## 定位流程 ### 步骤 0 · 建立可信基线 ```bash cd build # 主机构建目录,有 MNNConvert python3 ../tools/script/testMNNFromOnnx.py ../models/<model>.onnx # 全模型:生成 onnx/ 参考 + convert_cache.mnn,并做 CPU 自检 ``` 末尾应打印 `TEST_SUCCESS`。把 `convert_cache.mnn`→`onnx/test.mnn`,连同 `input*.txt/input.json/<out>.txt` push 到设备,跑 `CPU fwd=0 prec=1` 确认 diff≈1e-4。**至此确认模型/转换无误,问题在 QNN。** > ⚠️ **源 onnx 必须放在 `onnx/` 目录之外**(如 `cp build/onnx/test.onnx build/src_model.onnx`)——否则 `testMNNFromOnnx.py` 把它拷成 `onnx/test.onnx` 时报 SameFileError。多输入模型会生成 `input0.txt/input1.txt/…`,push 时用 `onnx/input*.txt` 覆盖。 ### 步骤 1 · 一次性 dump 全部中间张量并比对(首选) 一趟 dump 拿到 QNN 全部中间激活,再与 CPU 基线逐张量比,直接找出**第一个**出错算子——取代"每探一个点就重转一次"的截断循环。 1. **生成 debug 模型并跑一遍**(CNN / LLM 通用): ```bash MNN2QNNModel $QNN_SDK_ROOT 57 75 onnx/test.mnn dbg --dump_intermediate_outputs # soc/arch 按真机改 adb push dbg/test.mnn dbg/test.bin /data/local/tmp/MNN/ # 连同 input.txt/input.json/.bin adb shell "cd /data/local/tmp/MNN && LD_LIBRARY_PATH=. MNN_QNN_DUMP_DIR=dump ./ModuleBasic.out test.mnn . 0 5 1 4 2" adb pull /data/local/tmp/MNN/dump ./dump # manifest_*.tsv + 每张量一个 raw ``` 2. **读 manifest 逐张量对比 CPU**:`manifest_NNNNNN.tsv` 每行给出 `name / file / data_type / dimensions / quant_encoding / scale / offset`。raw 文件是 **QNN 布局(NHWC)+ QNN dtype**,比对前要按 manifest 的 quant(反量化 `f = (q - offset) * scale`)与布局还原,再和 CPU-fp32 基线(`testMNNFromOnnx.py` 全模型跑出的中间张量,或 `MNNDump2Json` 的张量表)对齐。张量名形如 `t42`,可回查 MNN 张量表定位到具体 op。 3. **找突跳点**:按图执行顺序扫每个张量的 diff,第一个"**输入好、输出坏**"且 QNN 远大于 CPU-fp16 地板的即首个出错算子;随后转 [步骤 2/3](#步骤-2--读误差模式--数值验证) 读误差模式、区分 bug/精度。 > 细节(manifest 字段、布局/量化还原、局限)见 [reference.md · QNN 中间张量 dump](./reference.md#qnn-中间张量-dump定位主力)。 #### 回退:截断 + 二分(dump 跑不起来 / 只想快速二分少数点时) - 先看图节点顺序:`python3 -c "import onnx;m=onnx.load('src_model.onnx');[print(i,n.op_type,list(n.output)) for i,n in enumerate(m.graph.node)]"`;或 `MNNDump2Json` 看 MNN 侧执行顺序(MNN 名与 ONNX 名可能不同)。 - 用 `scripts/qnn_probe.sh <tensor> [<tensor> ...]` 对中间张量截断→转换→push→**一次并排跑 QNN-fp16 / CPU-fp32 / CPU-fp16**,对节点序号二分找"输入好、输出坏"的第一个算子。 - 脚本已**自动注入 `shapeMutable=false`**(否则 QNN 输入不进去,见关键约束)。 - **陷阱**见 [坑 2/坑 3](./reference.md#常见坑):换模型前 `rm -f .tempcache`(QNN 图缓存,脚本已带);单独取 QNN 输出要单独跑 fwd=5 再 `cat output/0_0.txt`(同一条命令里跑 CPU 会覆盖它)。 ### 步骤 2 · 读误差模式 + 数值验证 把 QNN 输出与参考 reshape 后用 numpy 比对,对照 [误差模式速查](./reference.md#误差模式速查)。例如“每通道 std=0 的常数”几乎一定是 **conv 收到全零输入 → 输出==bias**,可与 ONNX 里该 conv 的 bias 逐通道核对确认。 ### 步骤 3 · 区分“真 bug”还是“HTP fp16 精度” 最简单:直接看 `qnn_probe.sh` 已经并排给出的 **CPU-fp16** 列——它是"行为良好的 fp16 地板"(CPU/OpenCL 的 fp16 都用 fp32 累加器)。某点 **QNN-fp16 ≫ CPU-fp16 且突跳** = 真 bug;**同步平滑增长** = fp16 累积。 需要 OpenCL 做交叉验证时,再用 `scripts/cmp_probe.sh`(设备需 `libMNN_CL.so`)一次输出 QNN 与 OpenCL 的 diff。判据: | 现象 | 结论 | |------|------| | 某算子处 QNN 误差**突跳**、OpenCL 不跳 | 该算子**真 bug**,深挖它 | | QNN 与 OpenCL **平滑同步**增长,经 GlobalAveragePool/Pool 后误差**下降** | 随机精度噪声,非离散 bug | | QNN 比 OpenCL 同精度大 **~2.5×/层**并随深度放大 | **HTP fp16 累加**(OpenCL fp16 用 fp32 累加器)| | QNN `High` 与 `Low` 结果几乎相同 | **HTP 忽略 fp32 请求**,底层纯 fp16(见 reference)| ### 步骤 3.5 · graphFinalize / graphExecute 失败时:启用 QNN 错误日志 如果 QNN 报 error code 1002(graphFinalize 失败)或 6000(graphExecute 失败),需要启用 QNN 内部日志来获取详细错误信息: 1. **启用 log callback**:在 `QNNBackend.cpp` 中找到 `QnnLog_create` 或 log level 设置处,将级别改为 `QNN_LOG_LEVEL_ERROR`(1)或更详细的级别(2=WARN, 3=INFO, 4=DEBUG)。 2. **重编并测试**:`cd project/android/build_64 && make MNN -j8 && adb push libMNN.so /data/local/tmp/MNN/` 3. **查看日志**:运行测试时 grep `QNN_LOG`,关注: - `could not create op` → 某算子约束不满足,查 MasterOpDef.html - `Wrong number of Inputs` → 输入数量不对 - `Op creation failure, total_inputs=N` → 检查各 Input 的类型(F16Crouton=fp16, PlainFloat=fp32) 4. **查 SDK 算子文档确认约束**:SDK 根取自编译配置——`SDK=$(grep -i QNN_SDK_ROOT project/android/build_64/CMakeCache.txt | head -1 | cut -d= -f2)`(或 `$QNN_SDK_ROOT`),再进 `$SDK/docs/QNN/`(老版)或 `$SDK/docs/QAIRT-Docs/QNN/`(2.48) 下的 `OpDef/`。同目录 **`HtpOpDefSupplement.html`** 是 HTP 专属约束权威来源,**`SupportedOps.html`** 是各后端支持列表。搜算子名确认输入数量、类型、维度约束。 > 详见 [reference.md · QNN 错误日志](./reference.md#qnn-错误日志qnn-log-callback) 和 [QNN 算子约束查询](./reference.md#qnn-算子约束查询)。 ### 步骤 4 · 打桩定位到代码 / 给结论 在 QNN 后端加临时 `MNN_PRINT`(`build_64` make 后 push)常用打点:
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기