Skip to content

[QNN:Feature] Add intermediate tensor dumps for QNN backend debug#4663

Open
huangzhengxiang wants to merge 4 commits into
alibaba:masterfrom
Embedded-AI-Systems:qnn
Open

[QNN:Feature] Add intermediate tensor dumps for QNN backend debug#4663
huangzhengxiang wants to merge 4 commits into
alibaba:masterfrom
Embedded-AI-Systems:qnn

Conversation

@huangzhengxiang

Copy link
Copy Markdown
Contributor

Description

Add intermediate tensor dumps for QNN backend debug

Module

QNN

Type

  • Feature

Checklist

  • Commit message follows [Module:Type] Description format
  • Code compiles without errors
  • Tested on relevant platform(s). (Snapdragon 8Gen3.)
  • No unrelated format or style changes included

@Qxinyu

Qxinyu commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

感谢这个特性——中间张量 dump 对精度调试确实很有用。合并前有几个潜在问题,按严重程度大致排列如下:

1.(较严重)无条件 alloc 所有 native 张量会命中不支持的数据类型

registerDebugTensor() 会对每个被提升的 native 张量调用 QNNTensorWrapper::alloc(),但 alloc()QNNWrapper.cpp)的 dtype 白名单远窄于图中实际会出现的类型:它只接受 FLOAT_32/16、INT_32、UINT_32、SFIXED_POINT_8/32、UFIXED_POINT_8/16;而 getQnnDataTypeSize()(即真实会出现的类型)还包括 SFIXED_POINT_16INT_8/16UINT_8/16BOOL_8INT_64FLOAT_64 等。

当某个中间张量是这些类型时(量化图里很常见,例如 int16 激活、compare 算子产生的 BOOL_8 掩码):

  • Debug 构建MNN_ASSERT 触发 → 崩溃;
  • Release 构建-DNDEBUG,关闭 RTTI/异常):assert 变为空操作,switch 落到 defaulthalideType 保持 {code=0, bits=0}Tensor::create 分配出错误/零大小的 buffer,clientBuf.dataSize 也随之错误 → graphExecute 可能越界写或 QNN 校验失败。

建议对 alloc() 无法处理的 dtype 跳过并打印告警,而不是无条件分配。

2.(中等)在线与离线两条路径 dump 的张量集合不一致

  • 在线(QnnBackend::executeGraph)只 dump mDebugTensorWrappers(被提升的 native 中间张量),不含图的真实输出;
  • 离线(RawExecutorWrapper::runGraph)调用 dump(graph->outputTensors, graph->numOutputTensors),会 dump 所有输出(含真实输出)。

同一模型两种模式产出的文件集合不同,会给逐层比对工具带来困扰,建议统一。

3.(轻微)MNN2QNNModel 的 flag 解析过于脆弱

--dump_intermediate_outputs 只识别 argv[argc-1](最后一个参数)。如果用户把它放在 input shape 参数之前,会被静默地当成 shape 字符串/路径解析而报错。支持任意位置识别会更健壮。

4.(轻微)debug 张量可能跨 onResize 累积

createStageTensor() 每次调用都会注册一个 debug 张量。动态 shape 下 onResize 可能多次重建 stage 张量,而 mDebugTensorWrappers 只在 clean() 中清空——建议确认 resize 生命周期总是先 clean,否则会累积失效的 wrapper。

其它小问题

  • 即使没有可读张量,dump() 也会创建一个空的 manifest_NNNNNN.tsv
  • mExecution 无锁(QNN 单线程执行下无碍,仅作提示)。

确认没问题、无需改动的点:无条件的 MNNCreateDir(outputDir) 是幂等的(目录已存在返回 true),不是回归;新增的 dump_intermediate_outputs attr 向后兼容(旧模型缺失时为 false);真实输出与 debug 张量不会被重复统计。

@huangzhengxiang

Copy link
Copy Markdown
Contributor Author

@Qxinyu 感谢 review,问题已按建议修复并完成设备验证:

  1. dtype / host buffer 安全性
    已补全 QNNTensorWrapper::alloc() 对 FLOAT64、INT/UINT8/16/32/64、BOOL8、SFIXED/UFIXED 8/16/32 的 host-buffer映射。同时在提升 tensor 为 APP_READ 前检查 dtype 是否可分配;不支持的 dtype 保持 NATIVE 并打印 warning。若 buffer 分配失败,也会在 graph tensor 创建前回退到 NATIVE,不会留下未绑定 client buffer 的 APP_READ tensor。

  2. 在线/离线 dump 集合一致性
    在线路径现在 dump 完整的 graphExecute output 集合,而非仅 dump promoted native tensors。因此真实模型输出、cast/dequant 输出和中间 tensor 都会出现在 manifest 中,与离线graph->outputTensors 的语义一致。

  3. CLI flag 解析
    --dump_intermediate_outputs 改为从全部参数中识别并过滤,因此可放在任意 dynamic-shape 参数位置;未知 --xxx 会显式报错。已验证:

  MNN2QNNModel ... output --dump_intermediate_outputs 2 1x8  2x8

可成功生成两个 shape context。

  1. 多次 resize 生命周期
    确认 QnnBackend::onResizeBegin() 固定先调用 clean(),其中已清理 mDebugTensorWrappers,不会跨 resize 累积失效 wrapper。补充了测试方法论:动态 shape 设备测试必须从manifest/input tensor 确认实际选中的 runtime shape,不能仅以进程退出码判断。

其它:

  • manifest 改为首个 raw tensor 成功写出后才创建,不再产生空manifest。

  • mExecution 并发问题暂未单独加锁;QNN backend 当前本身没有同一实例并发执行契约,因此不是本改动引入的 blocker。

验证结果:

  • Host QNN backend 与 MNN2QNNModel 编译通过。

  • Android ARM64 QNN runner 编译通过。

  • MEIZU 21 / SM8650 上通过 adb-hub managed session 运行成功,exit code 0。

  • 最终 manifest 含 21 个 raw tensor;最终 FP32 t45 raw 与 runner 输出逐元素一致。

  • 已知输入首 batch 相对 PyTorch reference 最大绝对误差为1.32e-4。

相关文档也已更新:QNN README 说明完整 output-set dump 语义和 flag 任意位置使用方式。

@Qxinyu

Qxinyu commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

补充:在 8Gen5(v81) 真机实测发现一个 dump 专属的回归(QNN error 7004)

用当前分支(含 hardening commit)交叉编译 libMNN.soMNN_QNN=ON,QAIRT 2.48)部署到 SM8850/v81 设备,跑一个极简模型(data[1,3,16,16] → Conv → Relu → Conv)并开启 dump(backendConfig.flags = MNN_QNN_DUMP_INTERMEDIATE_OUTPUTSforwardType=MNN_FORWARD_NN)时,QNN 在建图阶段报错:

QnnDsp <E> tensor type 1 client buffer should be nullptr at tensor creation
QnnDsp <E> exits with 7004, tensor buffer parameters not supported
Error in QNNBackend.cpp, line 1796: error code 7004   // tensorCreateGraphTensor

对照实验:关闭 dump 时没有 7004,确认是 dump 路径引入的。由于 CALL_QNN 只是打日志 + assert(Release/NDEBUG 下 assert 为空操作),所以没有崩溃、dump 文件也照常产出,但 Debug 构建会直接 abort,且这是"带着建图错误硬跑"。

根因:hardening commit 把 registerDebugTensor()(内部 alloc() 会给 APP_READ 张量挂上 clientBuf.data)移到了 tensorCreateGraphTensor() 之前。而 QNN 要求 APP_READ/APP_WRITE 张量在建图时 client buffer 必须为 null,host buffer 只能在 execute 时提供 —— 首版 commit e630c491 正是"先建图、后 alloc"的顺序,是正确的;hardening 为了实现"alloc 失败回退为 NATIVE"把顺序调换,从而触发 7004。

修复:把两处 registerDebugTensor 调用移回建图之后即可。由于 canDumpTensor() 已在建图前完成 dtype 白名单校验,"alloc 失败回退 NATIVE"的前置逻辑不再需要。

--- a/source/backend/qnn/backend/QNNBackend.cpp
+++ b/source/backend/qnn/backend/QNNBackend.cpp
@@ onAcquire
     Qnn_Tensor_t * qnnTensor = qnnTensorWrapper->getNativeTensor();
+    // QNN rejects APP_READ/APP_WRITE tensors that already carry a client buffer at graph-tensor
+    // creation (error 7004). Create the graph tensor first (buffer still null), then attach the
+    // host dump buffer via registerDebugTensor.
+    CALL_QNN(mRuntime->mQnnInterface.tensorCreateGraphTensor(mQnnGraphHandle, qnnTensor));
     if (isDebugTensor && !registerDebugTensor(qnnTensorWrapper, tensorDimType)) {
-        qnnTensor->v1.type = QNN_TENSOR_TYPE_NATIVE;
         isDebugTensor = false;
     }
-    CALL_QNN(mRuntime->mQnnInterface.tensorCreateGraphTensor(mQnnGraphHandle, qnnTensor));
     mQNNTensorWrappers.push_back(qnnTensorWrapper);
     mTensorMap.insert({TensorUtils::getDescribe(tensor), mTensorCounter});
--- a/source/backend/qnn/execution/QNNCommonExecution.cpp
+++ b/source/backend/qnn/execution/QNNCommonExecution.cpp
@@ createStageTensor
     std::shared_ptr<QNNTensorWrapper> tensorWrapper =
         QNNTensorWrapper::create(tensorName, tensorType, dataType, dimensions, quantize);
-    if (dumpTensor && !mBackend->registerDebugTensor(tensorWrapper)) {
-        tensorWrapper->getNativeTensor()->v1.type = QNN_TENSOR_TYPE_NATIVE;
-        dumpTensor = false;
-    }
+    // Create the graph tensor first (client buffer must be null at creation, QNN error 7004),
+    // then attach the host dump buffer.
     mBackend->addTensor(tensorWrapper->getNativeTensor());
+    if (dumpTensor) {
+        mBackend->registerDebugTensor(tensorWrapper);
+    }
     mTempTensorWrappers.push_back(tensorWrapper);
     return tensorWrapper;

验证(同一设备、同一模型、同一命令,仅替换 libMNN.so):7004 从"每个 debug 张量都报"变为 0 次,dump 的 manifest_*.tsv + 每张量 .raw 仍正常产出(张量名 t1/t2/c1_ReluTensor/t3/t4、NHWC 维度、dtype FP16/FP32、量化字段均符合 README)。

备注:修复后仍存在一个与本 PR 无关error 6000(graphExecute 阶段,Invalid input/output parameters),关闭 dump 时同样出现,属于我这套环境(QAIRT 版本 / 模型 / shape)跑 QNN 在线图的 baseline 问题;因此本次未做端到端数值比对,但 dump 的机制/管线已验证正确。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants