Skip to content

Qualcomm AI Engine Direct - QAIRT Visualizer Engagement #10873

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions backends/qualcomm/debugger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# QAIRT Visualizer

[QAIRT Visualizer](https://pypi.org/project/qairt-visualizer/) is a Python package designed to help you visualize and analyze data from Qualcomm AI Engine Direct (QNN) models. It provides tools to generate and interpret op traces (`optrace`) and QNN HTP Analysis Summary (`QHAS`), enabling detailed insights into your model's performance and behavior.

## Installation

You can install the QAIRT Visualizer package directly from [QAIRT Visualizer](https://pypi.org/project/qairt-visualizer/):

```bash
pip install qairt-visualizer
```

## Quick start
This command launches an interactive GUI interface to visualize the `optrace` and `QHAS` results.
```
python -m examples.qualcomm.util_scripts.qairt_visualizer_demo -H ${host} -s {device} -b build-android -a ${path_to_output_folder} --online_prepare
```
- If online prepare mode is `enabled`, the following artifacts will be generated:
- `model`.dlc
- `optrace`.json
- `QHAS`
- If online prepare mode is `disabled`, the following artifacts will be generated:
- `model`.bin
- `optrace`.json
- `QHAS`.json

Note: Model visualization is supported only in online prepare mode.
The `.bin` format is not compatible with the QAIRT visualizer.
To enable model visualization, please add the `--online_prepare` flag.

## Details
### 1. Lower to QNN backend
Generate an ExecuTorch binary for Qualcomm platforms.
```python
build_executorch_binary(
model,
example_input,
args.model,
f"{args.artifact}/{pte_filename}",
[example_input],
quant_dtype=QuantDtype.use_8a8w,
online_prepare=args.online_prepare,
optrace=True,
)
```
### 2. Generate optrace and QHAS
Generate optrace and QHAS files using QNN tools under $QNN_SDK_ROOT. After finishing, you will get a `binaries_trace` dictionary.
``` python
adb = SimpleADB(
qnn_sdk=os.getenv("QNN_SDK_ROOT"),
build_path=f"{args.build_folder}",
pte_path=f"{args.artifact}/{pte_filename}.pte",
workspace=f"/data/local/tmp/executorch/{pte_filename}",
device_id=args.device,
host_id=args.host,
soc_model=args.model,
)
binaries_trace = generate_optrace(
args, adb, f"{args.artifact}/{pte_filename}.pte", example_input
)
```
- **`binaries_trace`**: A dictionary where keys are the dumped file paths and values are tuples containing the paths to the generated optrace and QHAS JSON files.

- Example 1: {"forward_0.dlc": (optrace.json, optrace_qnn_htp_analysis_summary.json)}
- Example 2: {"forward_0.bin": (optrace.json, optrace_qnn_htp_analysis_summary.json)}

### 3. Visualizing and Analyzing optrace and QHAS

Once you have the optrace and QHAS files, you can leverage the QAIRT Visualizer to visualize the model graph, optrace and QHAS data. Here's how you can do it:

```python
import qairt_visualizer
qairt_visualizer.view(f"{args.artifact}/forward_0.dlc", reports=[optrace, qhas])
```
or
```python
import qairt_visualizer
qairt_visualizer.view(reports=[optrace, qhas])
```

- `model`: Path to your QNN model file (e.g., `path_to_your_model.dlc`).
- **`reports`**: List of report file paths, including the optrace (`optrace.json`) and QHAS (`optrace_qnn_htp_analysis_summary.json`).

Note: Files ending with `.bin ` do not support graph visualization in qairt_visualizer.

## Demo

<figure>
<img src="assets/qairt_visualizer_demo.png" alt="QAIRT visualizer demo"> <figcaption>
</figcaption>
</figure>

For more details, visit the [QAIRT Visualizer](https://pypi.org/project/qairt-visualizer/).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
261 changes: 261 additions & 0 deletions backends/qualcomm/debugger/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import json
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Tuple

import executorch.backends.qualcomm.python.PyQnnWrapperAdaptor as PyQnnWrapper
import pandas as pd
import torch
from executorch.backends.qualcomm.serialization.qc_schema import QcomChipset
from executorch.backends.qualcomm.utils.utils import dump_context_from_pte

from graphviz import Digraph


Expand Down Expand Up @@ -180,3 +189,255 @@ def draw(self):
dot_file = os.path.join(temp_directory, f"{self.filename}")
dot_dest_file = os.path.join(".", f"{self.filename}.dot")
shutil.move(dot_file, dot_dest_file)


class QnnTool:
def __init__(
self,
tmp_dir,
sample_input,
soc_id,
adb,
build_folder,
workspace="/data/local/tmp/qnn_executorch_test",
):
self.qnn_sdk = os.environ.get("QNN_SDK_ROOT", None)
self.ndk = os.environ.get("ANDROID_NDK_ROOT", None)
assert self.qnn_sdk, "QNN_SDK_ROOT was not found in environment variable"
assert self.ndk, "ANDROID_NDK_ROOT was not found in environment variable"

self.tmp_dir = tmp_dir
self.workspace = workspace
self.adb = adb
self.sample_input = sample_input
self.build_folder = build_folder
self.root = str(Path(__file__).resolve().parents[3])
self.config = {
"backend_extension_config": {
"backend_extensions": {
"config_file_path": "config.json",
},
"features": {
"qhas_json": True,
},
},
"config": {
"devices": [
{
"profiling_level": "linting",
"cores": [
{"perf_profile": "burst", "rpc_control_latency": 100}
],
"soc_id": int(soc_id),
}
]
},
}

def qnn_context_binary_generator(
self,
qnn_binary_file="forward_0.dlc",
binary_name="forward.serialized",
):
for file_name, data in self.config.items():
with open(f"{self.tmp_dir}/{file_name}.json", "w") as json_file:
json.dump(data, json_file, indent=4)

target = "x86_64-linux-clang"
cmds = [
f"{self.qnn_sdk}/bin/{target}/qnn-context-binary-generator",
"--backend",
f"{self.qnn_sdk}/lib/{target}/libQnnHtp.so",
"--model",
f"{self.qnn_sdk}/lib/{target}/libQnnModelDlc.so",
"--dlc_path",
f"{self.tmp_dir}/{qnn_binary_file}",
f"--config_file {self.tmp_dir}/backend_extension_config.json",
f"--binary_file {binary_name}",
f"--output_dir {self.tmp_dir}",
"--profiling_level detailed",
"--profiling_option optrace",
]
result = subprocess.run(
" ".join(cmds),
shell=True,
executable="/bin/bash",
capture_output=True,
)
assert os.path.isfile(f"{self.tmp_dir}/{binary_name}.bin"), result.stderr

def qnn_net_run(self, graph_name="forward.serialized"):
input_list = ""
for idx, _ in enumerate(self.sample_input):
input_name = f"input_{idx}_0.raw"
input_list += input_name + " "
input_list = input_list.strip() + "\n"

self.config["backend_extension_config"]["backend_extensions"][
"shared_library_path"
] = "./libQnnHtpNetRunExtensions.so"
for file_name, data in self.config.items():
with open(f"{self.tmp_dir}/{file_name}.json", "w") as json_file:
json.dump(data, json_file, indent=4)

target = "aarch64-android"
files = [
f"{self.qnn_sdk}/lib/{target}/libQnnHtpNetRunExtensions.so",
f"{self.tmp_dir}/backend_extension_config.json",
f"{self.tmp_dir}/config.json",
f"{self.tmp_dir}/{graph_name}.bin",
f"{self.qnn_sdk}/bin/{target}/qnn-net-run",
]
cmds = [
f"export LD_LIBRARY_PATH={self.workspace} &&",
f"export ADSP_LIBRARY_PATH={self.workspace} &&",
f"cd {self.workspace} &&",
"./qnn-net-run",
"--backend libQnnHtp.so",
"--input_list input_list.txt",
f"--retrieve_context {graph_name}.bin",
"--use_native_input_files",
"--use_native_output_files",
"--config_file backend_extension_config.json",
"--profiling_level detailed",
"--profiling_option optrace",
]
self.adb.push(
inputs=self.sample_input,
input_list=input_list,
files=files,
)
self.adb.execute(custom_runner_cmd=" ".join(cmds))
self.adb._adb(
[
"pull",
"-a",
f"{self.workspace}/output/qnn-profiling-data_0.log",
self.tmp_dir,
]
)

assert os.path.isfile(
f"{self.tmp_dir}/qnn-profiling-data_0.log"
), f"Error: qnn-profiling-data_0.log not found in {self.tmp_dir}"

def qnn_profile_viewer(self, graph_name="forward_schematic", graph_idx=0):
self.config["backend_extension_config"]["backend_extensions"][
"shared_library_path"
] = "./libQnnHtpNetRunExtensions.so"
self.config["backend_extension_config"] = {"features": {"qhas_json": True}}
for file_name, data in self.config.items():
with open(f"{self.tmp_dir}/{file_name}.json", "w") as json_file:
json.dump(data, json_file, indent=4)

target = "x86_64-linux-clang"
cmds = [
f"{self.qnn_sdk}/bin/{target}/qnn-profile-viewer",
f"--config {self.tmp_dir}/backend_extension_config.json",
f"--schematic {self.root}/{graph_name}.bin",
f"--reader {self.qnn_sdk}/lib/{target}/libQnnHtpOptraceProfilingReader.so",
f"--input_log {self.tmp_dir}/qnn-profiling-data_0.log",
f"--output {self.tmp_dir}/optrace_{graph_idx}.json",
]
result = subprocess.run(
" ".join(cmds),
shell=True,
executable="/bin/bash",
capture_output=True,
)
assert (
result.returncode == 0
), f"Process failed with error: {result.stderr.decode('utf-8')}"

def generate_optrace(
self,
qnn_binary_file="forward_0.dlc",
):
"""
Generate Qnn HTP Optrace Profiling https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-50/htp_backend.html#qnn-htp-optrace-profiling
and QNN HTP Analysis Summary (QHAS) https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-50/htp_backend.html#qnn-htp-analysis-summary-qhas
. You can utilize the QAIRT Visualizer (https://pypi.org/project/qairt-visualizer/) to visualize the results from the files above.
"""
graph_name, file_extension = os.path.splitext(qnn_binary_file)
assert file_extension in [
".dlc",
".bin",
], f"Invalid file extension '{file_extension}'. Supported extensions are 'dlc' and 'bin'."

# Attempt to extract a numeric index from the end of the graph name (e.g., "forward_123")
match = re.match(r"^(.*)_(\d+)$", graph_name)
graph_base_name = graph_name
graph_idx = 0

if match:
graph_base_name = match.group(1)
graph_idx = int(match.group(2))

# Handle .dlc file extension by generating a serialized version of the graph
if file_extension == ".dlc":
self.qnn_context_binary_generator(
qnn_binary_file, f"{graph_base_name}.serialized"
)
graph_name = f"{graph_base_name}.serialized"

# Run the QNN graph and generate the schematic
self.qnn_net_run(graph_name=graph_name)
self.qnn_profile_viewer(
graph_name=f"{graph_base_name}_schematic", graph_idx=graph_idx
)

# Clean up the schematic binary file if it exists
schematic_bin_path = os.path.join(self.root, f"{graph_base_name}_schematic.bin")
if os.path.isfile(schematic_bin_path):
os.remove(schematic_bin_path)

optrace_path = os.path.join(self.tmp_dir, f"optrace_{graph_idx}.json")
qhas_path = os.path.join(
self.tmp_dir, f"optrace_{graph_idx}_qnn_htp_analysis_summary.json"
)
assert os.path.isfile(optrace_path) and os.path.isfile(qhas_path), (
"Error: Required files not found - either "
f"{os.path.basename(optrace_path)} or {os.path.basename(qhas_path)} is missing."
)

return optrace_path, qhas_path


def generate_optrace(
artifact, soc_id: QcomChipset, adb, pte_path: str, inputs: Tuple[torch.Tensor]
):
"""
Generate optrace and QHAS (QNN HTP Analysis Summary) JSON files.

Args:
artifact (str): Path to the artifact folder.
adb (SimpleADB): An object for communicating with Android device
pte_path (str): The path to the generated PTE file, including the file extension (e.g., model.pte).
inputs (Tuple[torch.Tensor]): The input tensors for the model.


Returns:
dict: A dictionary where keys are the dumped file paths and values are tuples containing the paths
to the generated optrace and QHAS JSON files.
"""
filename, _ = os.path.splitext(pte_path.split(os.sep)[-1])

# Dump compiled binaries
dumpfiles = dump_context_from_pte(pte_path)

# Generate optrace and QHAS
qnn_tool = QnnTool(
artifact,
inputs,
soc_id,
adb,
build_folder=adb.build_path,
workspace=adb.workspace,
)

binaries_trace = {}
for file in dumpfiles:
filename = file.split(os.sep)[-1]
optrace, qhas = qnn_tool.generate_optrace(filename)
binaries_trace[file] = (optrace, qhas)
return binaries_trace
Loading
Loading