Skip to content

Commit ee7ab19

Browse files
authored
Qualcomm AI Engine Direct - QNN ExecuTorch Intermediate Output Debugger (pytorch#15735)
### Summary - Enabled ExecuTorch QNN Intermediate Tensor Debugger. - Provide an API for users to define their own metrics - Offers a variety of output format to visualize the debug results: svg, csv, raw files. - A README file and tutorial script to guide users on how to debug a model. Example script: `python examples/qualcomm/util_scripts/qnn_intermediate_debugger_demo.py -b build-android -m SM8550 --device $DEVICE --dataset ../imagenet-mini/val/ --dump_intermediate_outputs` #### An example use case MobileVit V2 has significant drop in accuracy in certain QNN versions, while QNN 2.29 has good accuracy. With the help of accuracy debugger, we have targeted the node native_group_norm_default_1 in the model. As shown below, in QNN 2.29, this node has a cos_similarity (QNN V.S. CPU) of 0.997, while all other QNN versions has cos_similarity of 0, which provides us some hint it is possibly this group_norm node that is causing accuracy drop. <img width="1385" height="596" alt="image" src="https://github.com/user-attachments/assets/ba81bb5f-1cae-4d3b-a945-a00ca92efeef" /> #### What's Coming Next? - Currently, we dump CPU outputs by manually inserting observer nodes. However, ExecuTorch actually has built in methods (intermediate_output_capturer) that could dump intermediate output for us, in format of a dict{debug_handle : tensor_output}. We will enable `debug_handle` and reuse https://github.com/pytorch/executorch/blob/main/devtools/inspector/_intermediate_output_capturer.py in future instead. - Support graph with partitions - Support LLM models ### Test plan - E2E example script test - `python backends/qualcomm/tests/test_qnn_delegate.py -k TestExampleUtilsScript.test_intermediate_debugger -s $DEVICE --model SM8650 --build_folder build-android/ --executorch_root . --image_dataset ../imagenet-mini/val/ --artifact ./e2e_test_debug` - Simple model test - `python backends/qualcomm/tests/test_qnn_delegate.py -k TestQNNQuantizedUtils.test_qnn_backend_dump_intermediate_outputs_simple_model --model SM8550 --device $DEVICE --build_folder build-android` - `python backends/qualcomm/tests/test_qnn_delegate.py -k TestQNNQuantizedUtils.test_qnn_backend_dump_intermediate_outputs_topk --model SM8550 --device $DEVICE --build_folder build-android`
1 parent 23d36d8 commit ee7ab19

22 files changed

Lines changed: 1337 additions & 165 deletions

backends/qualcomm/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ PRs are always welcome to help improve the codebase in a comprehensive manner. B
133133
- **Code Reviews**:<br/>
134134
Please ping authors in Qualcomm AI Engine Direct related PRs for reviewing, possible candidates are listed below:
135135
- [shewu-quic](https://github.com/shewu-quic)
136-
- [chunit-quic](https://github.com/chunit-quic)
136+
- [chenweng-quic](https://github.com/chenweng-quic)
137137
- [winskuo-quic](https://github.com/winskuo-quic)
138138
- [DannyYuyang-quic](https://github.com/DannyYuyang-quic)
139139
- [haowhsu-quic](https://github.com/haowhsu-quic)

backends/qualcomm/builders/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ Now, we can start to fill in function body step by step:
225225
- **tensor_source_node**: current graph source node of the tensor
226226
- **target_build_node**: current node to build, which is important for fixed point mixed-precision to work properly
227227
- **tensor**: torch tensor emitted by node
228-
- **tensor_type**: type compatible with QNN SDK, oftenly use `QNN_TENSOR_TYPE_NATIVE` for intermediate outputs and `QNN_TENSOR_TYPE_STATIC` for constant parameters
228+
- **tensor_type**: type compatible with QNN SDK, often use `QNN_TENSOR_TYPE_NATIVE` for intermediate outputs and `QNN_TENSOR_TYPE_STATIC` for constant parameters
229229
- **nodes_to_wrappers**: dictionary of graph node and its output tensor (note: the tensor here is not a torch tensor but a wrapped object for QNN)
230230
- **node_name**: (optional) tensor name for user to specify
231231
- **wrapper_idx**: (optional) defaults to zero if node is not a tuple, otherwise it acts as an indexer to output tensors. e.g. when slicing input tensor into multiple outputs, `wrapper_idx` is necessary for getting correct wrapped tensor object
@@ -280,7 +280,7 @@ Now, we can start to fill in function body step by step:
280280
nodes_to_wrappers,
281281
)
282282
```
283-
Althought the input / output activations might map to the graph IOs (a.k.a. user inputs / outputs) with corresponding type `QNN_TENSOR_TYPE_APP_READ` / `QNN_TENSOR_TYPE_APP_WRITE`. Users are still expected to have `QNN_TENSOR_TYPE_NATIVE` for all nodes' IOs and leave the detection logic handled inside `define_tensor` method.
283+
Although the input / output activations might map to the graph IOs (a.k.a. user inputs / outputs) with corresponding type `QNN_TENSOR_TYPE_APP_READ` / `QNN_TENSOR_TYPE_APP_WRITE`. Users are still expected to have `QNN_TENSOR_TYPE_NATIVE` for all nodes' IOs and leave the detection logic handled inside `define_tensor` method.
284284

285285
5. Generate operator object in QNN graph:
286286
```python
@@ -330,7 +330,7 @@ Now, we can start to fill in function body step by step:
330330
- **data_type**: type compatible with QNN SDK, e.g. `QNN_DATATYPE_FLOAT_32`, `QNN_DATATYPE_UINT_32`, etc.
331331
- **rank**: dimensions of tensor
332332
- **dims**: shape of tensor
333-
- **data**: tesnor data
333+
- **data**: tensor data
334334
- **copy_data**: user should specify to True for constant parameters
335335

336336
8. Last, return operator object for partitioner to conduct validation:

backends/qualcomm/builders/node_visitor.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
QCOM_SCALE,
3131
QCOM_SCALE_OFFSET,
3232
QCOM_SCALES,
33+
QCOM_TENSOR_NAME,
3334
QCOM_ZERO_POINT,
3435
QCOM_ZERO_POINTS,
3536
)
@@ -395,6 +396,13 @@ def get_tensor_name(
395396
tensor_name = f"output_mutbuf_{position_index}_{tensor_name}"
396397
elif is_graph_output(node):
397398
tensor_name = f"output_{tensor_name}"
399+
400+
# Save this for intermediate debugger
401+
# Needs idx since node like topk has 2 outputs
402+
if QCOM_TENSOR_NAME in node.meta:
403+
node.meta[QCOM_TENSOR_NAME][wrapper_idx] = tensor_name
404+
else:
405+
node.meta[QCOM_TENSOR_NAME] = {wrapper_idx: tensor_name}
398406
return tensor_name
399407

400408
def define_custom_tensor_wrapper(

backends/qualcomm/debugger/README.md

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ python -m examples.qualcomm.util_scripts.qairt_visualizer_demo -H ${host} -s {de
1818
- If online prepare mode is `enabled`, the following artifacts will be generated:
1919
- `model`.dlc
2020
- `optrace`.json
21-
- `QHAS`
21+
- `QHAS`.json
2222
- If online prepare mode is `disabled`, the following artifacts will be generated:
2323
- `model`.bin
2424
- `optrace`.json
@@ -92,3 +92,160 @@ Note: Files ending with `.bin ` do not support graph visualization in qairt_visu
9292
</figure>
9393

9494
For more details, visit the [QAIRT Visualizer](https://pypi.org/project/qairt-visualizer/).
95+
96+
97+
# ExecuTorch QNN Intermediate Output Debugger
98+
99+
ExecuTorch QNN Intermediate Output Debugger is a tool that helps users debug intermediate output accuracy by comparing CPU outputs with QNN outputs. This tool offers a variety of output formats and flexibility for users to define their own metrics when debugging.
100+
101+
Below, we will go through the details step by step on how to customize your own debugger. By the end of this tutorial, users should understand the mechanism behind the ExecuTorch QNN Debugger and how to apply the debugger to the desired model. In the rest of the tutorial, we will use the term `intermediate output` and `per-layer dump` interchangeably.
102+
103+
To make the implementation process smooth, we have also provided an example script, [qnn_intermediate_debugger_demo.py](../../../examples/qualcomm/util_scripts/qnn_intermediate_debugger_demo.py), which is an end-to-end example that goes through the steps for implementation. Refer to [Example Script](#example-script) section for more information.
104+
105+
## Introduction
106+
107+
1. Why do we need ExecuTorch QNN Intermediate Output Debugger?
108+
During inference, there might be gaps between QNN and CPU final outputs. This leaves developers unsure about the root cause of accuracy drop. By using this debugger, users can gain better insight into which operation is causing the accuracy drop. Please note that the accuracy drop here refers to comparing QNN with CPU outputs, not the ground truth.
109+
110+
2. Who is this tool for?
111+
This tool is mainly for developers aiming to align QNN with CPU accuracy. Users will be able to identify which layer in the model is causing the accuracy drop, helping them either circumvent the issue by replacing the layer with other operations or contact authors in Qualcomm AI Engine Direct to resolve the accuracy issue. Please refer to the last section under [README.md](../README.md) for authors to contact when encountering any issues.
112+
113+
114+
## Design Flow
115+
```mermaid
116+
flowchart TB;
117+
nn.Module;
118+
nn.Module --> edge_program["Edge Program"];
119+
edge_program --> qnn_lower["QNN with Per-Layer Dump"];
120+
qnn_lower --> qnn_inference[QNN Inference];
121+
qnn_inference --> debug
122+
edge_program --> cpu_lower["Edge CPU with Per-Layer Dump"];
123+
cpu_lower --> cpu_inference["CPU Inference"];
124+
cpu_inference --> debug["Debug"];
125+
debug --> output["Output Results"]
126+
```
127+
128+
## Instructions
129+
130+
### 1. Setup
131+
1. Follow the [tutorial](https://pytorch.org/executorch/main/getting-started-setup) to set up ExecuTorch.
132+
2. Follow the [tutorial](https://pytorch.org/executorch/stable/build-run-qualcomm-ai-engine-direct-backend.html) to build Qualcomm AI Engine Direct Backend.
133+
134+
### 2. Enable Flag
135+
136+
When executing the script, please add the flag `--dump_intermediate_outputs`. This tells QNN to dump all intermediate tensors during execution.
137+
138+
### 3. Add debugger to the example script
139+
Initialize a `QNNIntermediateDebugger`. Please pass initialized `QNNIntermediateDebugger` and the `args.dump_intermediate_outputs` to `build_executorch_binary` method as well.
140+
#### Example:
141+
```python
142+
from executorch.examples.qualcomm.utils import build_executorch_binary
143+
from executorch.backends.qualcomm.debugger.qnn_intermediate_debugger import QNNIntermediateDebugger
144+
145+
qnn_intermediate_debugger = QNNIntermediateDebugger()
146+
build_executorch_binary(
147+
model=MyModel(),
148+
inputs=(torch.randn(200, 768),),
149+
soc_model="SM8650",
150+
file_name="my_model",
151+
dataset=my_dataset,
152+
dump_intermediate_outputs=args.dump_intermediate_outputs, # Add this flag
153+
qnn_intermediate_debugger=qnn_intermediate_debugger, # Add this flag
154+
)
155+
```
156+
157+
### 4. Set data num to 1
158+
It is perfectly fine for users to pass the desired amount of datasets to `build_executorch_binary`, which helps achieve better quantization results. However, after `build_executorch_binary` is called, we need to ensure that we only perform one inference during execution. Please ensure that CPU and QNN is using the same input during execution; otherwise, the debugging results might not be accurate.
159+
160+
### 5. Pass flag to SimpleADB
161+
When creating `SimpleADB`, please also pass the flag `args.dump_intermediate_outputs`. This tells the runner to create files that store the intermediate output schema and binary data.
162+
#### Example:
163+
```python
164+
adb = SimpleADB(
165+
qnn_sdk=os.getenv("QNN_SDK_ROOT"),
166+
build_path=f"{args.build_folder}",
167+
pte_path=f"{args.artifact}/{pte_filename}.pte",
168+
workspace=f"/data/local/tmp/executorch/{pte_filename}",
169+
device_id=args.device,
170+
host_id=args.host,
171+
soc_model=args.model,
172+
shared_buffer=args.shared_buffer,
173+
dump_intermediate_outputs=args.dump_intermediate_outputs, # Add this flag
174+
)
175+
```
176+
177+
### 6: Pull and process the results.
178+
After QNN execution with the runner, if the previous steps are done correctly, we should be able to get two files: `etdump.etdp` and `debug_output.bin`.
179+
The following example pulls the files back and calls a callback function to process the results. In this callback function, we create the `Inspector`. Then we perform CPU inference to get CPU intermediate results. Now, we have both QNN and CPU intermediate results, we can start generating results to compare the accuracy. Taking the following example, we should be able to get `debug_graph.svg` as an output in the current directory.
180+
#### Example:
181+
```python
182+
from executorch.backends.qualcomm.debugger.qnn_intermediate_debugger import OutputFormat
183+
def validate_intermediate_tensor():
184+
inspector = Inspector(
185+
etdump_path=f"{args.artifact}/etdump.etdp",
186+
debug_buffer_path=f"{args.artifact}/debug_output.bin",
187+
)
188+
qnn_intermediate_debugger.intermediate_output_module(*(inputs[0]))
189+
qnn_intermediate_debugger.generate_results(
190+
title="debug_graph",
191+
path=".",
192+
output_format=OutputFormat.SVG_GRAPHS,
193+
inspector=inspector,
194+
evaluator=CosineSimilarityEvaluator(0.9),
195+
)
196+
197+
adb.pull_debug_output(
198+
args.artifact, args.artifact, callback=validate_intermediate_tensor
199+
)
200+
```
201+
202+
#### Additional Options
203+
The above example sets output formats as SVG and evaluation metrics using Cosine Similarity. Based on different needs, users can choose other output formats as shown in the `OutputFormat` class under [qnn_intermediate_debugger](./qnn_intermediate_debugger.py)
204+
```python
205+
class OutputFormat(IntEnum):
206+
SVG_GRAPHS = 0
207+
CSV_FILES = 1
208+
DUMP_RAW = 2
209+
```
210+
211+
For evaluation metrics, if users would like to implement their own metrics, we have provided the option to implement [MetricEvaluatorBase](./metrics_evaluator.py). The following shows how to define custom metrics.
212+
```python
213+
class RootMeanSquaredErrorEvaluator(MetricEvaluatorBase):
214+
def __init__(self, threshold=0.02):
215+
self.threshold = threshold
216+
217+
def metric_name(self) -> str:
218+
return "Root Mean Squared Error"
219+
220+
def evaluate(
221+
self, qnn_output: torch.Tensor, cpu_output: torch.Tensor
222+
) -> Tuple[Any, bool]:
223+
mse = F.mse_loss(qnn_output, cpu_output)
224+
rmse = torch.sqrt(mse)
225+
valid = rmse < self.threshold
226+
return rmse, valid
227+
228+
qnn_intermediate_debugger.generate_results(
229+
title="my_metric",
230+
path=".",
231+
output_format=OutputFormat.SVG_GRAPHS,
232+
inspector=inspector,
233+
evaluator=RootMeanSquaredErrorEvaluator(),
234+
)
235+
```
236+
237+
### Example Script
238+
We have provided an inception_v3 demo script to help users better understand how to apply the debugger to their scripts. Please refer to [qnn_intermediate_debugger_demo.py](../../../examples/qualcomm/util_scripts/qnn_intermediate_debugger_demo.py) for the example script.
239+
240+
Before running the example script, please ensure that dataset is downloaded. Example dataset can be retrieved [here](https://www.kaggle.com/datasets/ifigotin/imagenetmini-1000).
241+
242+
To execute the model:
243+
```bash
244+
python examples/qualcomm/util_scripts/qnn_intermediate_debugger_demo.py -b build-android -m ${SOC_MODEL} --device ${SERIAL_NUM} --dataset ${PATH_TO_DATASET} --dump_intermediate_outputs
245+
```
246+
247+
### Limitation
248+
1. The current debugger only supports performing one execution. Multiple executions may cause unknown behavior and are not recommended.
249+
2. Please ignore this if you are using `qnn_executor_runner`. If you have decided to write your own runner, please follow the [tutorial](https://pytorch.org/executorch/stable/etdump.html) on how to implement etdump into your own runner.
250+
3. The current debugger does not support graph with partitions. (WIP)
251+
4. The current debugger does not support LLM models. (WIP)

backends/qualcomm/debugger/TARGETS

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,21 @@ runtime.python_library(
1010
"fbsource//third-party/pypi/pandas:pandas",
1111
]
1212
)
13+
14+
runtime.python_library(
15+
name = "qnn_intermediate_debugger",
16+
srcs = [
17+
"format_outputs.py",
18+
"metrics_evaluator.py",
19+
"qnn_intermediate_debugger.py",
20+
],
21+
deps = [
22+
"//caffe2:torch",
23+
"//executorch/backends/qualcomm/_passes:passes",
24+
"//executorch/backends/qualcomm/utils:utils",
25+
"//executorch/devtools:lib",
26+
"//executorch/exir:sym_util",
27+
"fbsource//third-party/pypi/graphviz:graphviz",
28+
"fbsource//third-party/pypi/pandas:pandas",
29+
],
30+
)

0 commit comments

Comments
 (0)