diff --git a/backends/qualcomm/debugger/README.md b/backends/qualcomm/debugger/README.md index afe2336c1d8..f5fce379095 100644 --- a/backends/qualcomm/debugger/README.md +++ b/backends/qualcomm/debugger/README.md @@ -1,93 +1,213 @@ -# QAIRT Visualizer +# ExecuTorch QNN Debugger & Profiler -[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. +This directory bundles three independent debugging and profiling flows for the ExecuTorch QNN backend. They are independent and address different failure modes — jump directly to the section you need. -## Installation +**Table of contents** -You can install the QAIRT Visualizer package directly from [QAIRT Visualizer](https://pypi.org/project/qairt-visualizer/): +- [Executorch QNN HTP Profiling](#executorch-qnn-htp-profiling) +- [ExecuTorch QNN Intermediate Output Debugger](#executorch-qnn-intermediate-output-debugger) +- [ExecuTorch QNN HTP Heap Profiling](#executorch-qnn-htp-heap-profiling) + +--- +# Executorch QNN HTP Profiling + +This section shows how to produce HTP profiling results and inspect the output reports. + +- The most accurate profiling mode is **on device profiling** with `generate_htp_profile_result()`, this requires a connected android device through ADB, see details in section 2.1. + +- Another profiling mode is **on host estimiation** with `estimate_htp_profile_result()` this doesn't require a device and can be run on host machine. However, the accuracy and soc support might be limited, see details in section 2.2. + + +Different HTP profiling features support different prepare modes, prepare mode is set in AOT config, and decides which format of QNN internal graph is packed in `.pte` file, + * On device profiling support both `online_prepare` and `offline_prepare` + * On host profiling reqruires `online_prepare` + +**Host Estimation vs On-Device Generation** +| Mode | Source of measurements | Requires device? | Requires `QnnConfig.online_prepare=True` for `.pte` generation ? | +|:--------------|:---------------------------------------|:-----------------|:-----------------| +| `generate_htp_profile_result` (`"optrace"`) | On-device hardware counters (real run) | Yes | No — support both mode (`.dlc` or `.bin`) | +| `estimate_htp_profile_result` (`"hextimate"`) | Compile-time performance-model estimate | No (host only) | Yes | + + +**Profile level** + +| `profile_level` | QNN configuration | Use | +|:----------------|:------------------|:----| +| `0` | Profiling disabled | Default inference. | +| `2` | `QNN_PROFILE_LEVEL_DETAILED` | Collect QNN graph and per-node timing through the ExecuTorch profiler. | +| `3` | Detailed + `QNN_PROFILE_CONFIG_OPTION_ENABLE_OPTRACE` | Enable HTP Optrace hardware-trace artifacts for `qnn-profile-viewer`; required at export for offline-prepare Optrace. | + +The rest of the guide is arranged here: +1. **`.pte` generation prepare mode:** +We introduce how to trigger each preparation mode in section 1.1 and 1.2: + - 1.1 `online_prepare`: controlled by `QnnConfig.online_prepare=True`. + - 1.2 `offline_prepare`: controlled by `QnnConfig.online_prepare=False`. + +3. **Public Functions for Generating Profiling Results** + + 2.1 `generate_htp_profile_result()`: generates device-based profiling results (Optrace in QNN SDK). + + 2.2 `estimate_htp_profile_result()`: estimates host-based profiling results (Hextimate in QNN SDK). + +4. **HTP Profile Output Format:** `QnnHtpProfileArtifacts` contains genrated HTML, JSON and chrometrace files. + +5. **Qairt-Visualizer:** QAIRT Visualizer can open the QHAS result from `qhas_json` and `chrometrace_json`. Use `QnnHtpProfileArtifacts.visualizer_reports()` to pass related reports. +## 1. Select AOT Prepare modes for `.pte` Generation +Users choose one prepare mode before exporting the `.pte`: + +| Prepare mode | QNN config | User-facing behavior | +|:-------------|:-----------|:---------------------| +| **online_prepare** | `QnnConfig.online_prepare=True` | Export stores a graph description; profiling tools finish preparation later. | +| **offline_prepare** | `QnnConfig.online_prepare=False` | Export stores the finalized executable form; on-device profiling requires `profile_level=3`. | + +Internal detail: online prepare carries a `.dlc`, and offline prepare carries a finalized QNN context binary (`.bin`). Offline prepare must set `profile_level=3` because OpTrace instrumentation has to be baked into that context binary during export. The schematic file used by `qnn-profile-viewer` is generated or unpacked beside the dumped profiling artifacts. + +The example demos keep the profiling route explicit: + +- `examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_online.py`: on-device OpTrace with `online_prepare`. +- `examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_offline.py`: on-device OpTrace with `offline_prepare`. +- `examples/qualcomm/util_scripts/htp_profiling_on_host_hextimate.py`: host-only Hextimate with `online_prepare`. + +### 1.1 Online Prepare Mode `.pte` + +**Demo script**: ```bash -pip install qairt-visualizer +python -m examples.qualcomm.util_scripts.htp_profiling_on_device_op_trace_online \ + --host ${host} --device ${device} --soc_model ${SOC_MODEL} --build_folder build-android \ + -a ${path_to_output_folder} --online_prepare ``` -## Quick start -This command launches an interactive GUI interface to visualize the `optrace` and `QHAS` results. +**Export call**: +The export step does not need to set `profile_level`: +```python +build_executorch_binary( + model=model, + qnn_config=qnn_config, # online_prepare=True; profile_level not required + file_name=f"{args.artifact}/{pte_filename}", + dataset=[example_input], + quant_dtype=QuantDtype.use_8a8w, +) ``` -python -m examples.qualcomm.util_scripts.qairt_visualizer_demo --host ${host} --device {device} --soc_model ${SOC_MODEL} --build_folder build-android -a ${path_to_output_folder} --online_prepare + +### 1.2 Offline Prepare Mode `.pte` +**Demo script**: +```bash +python -m examples.qualcomm.util_scripts.htp_profiling_on_device_op_trace_offline \ + --host ${host} --device ${device} --soc_model ${SOC_MODEL} --build_folder build-android \ + -a ${path_to_output_folder} --profile_level 3 ``` -- If online prepare mode is `enabled`, the following artifacts will be generated: - - `model`.dlc - - `optrace`.json - - `QHAS`.json -- 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. -Ensure that qnn_config.profile_level is set to 3, which will generate op_trace. + +**Export call**: +The export step **must** set `profile_level=3`: ```python qnn_config.profile_level = 3 build_executorch_binary( model=model, - qnn_config=qnn_config, + qnn_config=qnn_config, # online_prepare=False (default) file_name=f"{args.artifact}/{pte_filename}", dataset=[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_config=qnn_config, + +## 2.1 Generate HTP Profile Result (OpTrace) `generate_htp_profile_result()` + +Use `generate_htp_profile_result()` when you want real on-device hardware-counter profiling. This path requires sample inputs and `adb`. +```python +from executorch.backends.qualcomm.debugger.utils import generate_htp_profile_result + +artifacts = generate_htp_profile_result( + artifact_dir=args.artifact, + soc_id=get_soc_to_chipset_map()[args.soc_model], pte_path=f"{args.artifact}/{pte_filename}.pte", - workspace=f"/data/local/tmp/executorch/{pte_filename}", -) -binaries_trace = generate_optrace( - args, adb, f"{args.artifact}/{pte_filename}.pte", example_input + inputs=example_inputs, + adb=adb, ) ``` -- **`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)} +## 2.2 Estimation of HTP profiling on Host (Hextimate) `estimate_htp_profile_result()` + +Use `estimate_htp_profile_result()` when you want host-only compile-time performance estimation. This path does not use sample inputs or `adb`. -### 3. Visualizing and Analyzing optrace and QHAS +> [!WARNING] +> `estimate_htp_profile_result()` hard-errors on SDKs below 2.41 and on unsupported SoCs. -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: +**Demo script**: +```bash +python -m examples.qualcomm.util_scripts.htp_profiling_on_host_hextimate \ + --soc_model QCS9100 -a ${path_to_output_folder} --online_prepare +``` ```python -import qairt_visualizer -qairt_visualizer.view(f"{args.artifact}/forward_0.dlc", reports=[optrace, qhas]) +from executorch.backends.qualcomm.debugger.utils import estimate_htp_profile_result + +estimates = estimate_htp_profile_result( + artifact_dir=args.artifact, + soc_id=get_soc_to_chipset_map()[args.soc_model], + pte_path=f"{args.artifact}/{pte_filename}.pte", +) +``` + +Limitations: +- Requires `QnnConfig.online_prepare=True`. +- Requires QNN SDK >= 2.41. +- Currently supports only the following soc_model: + - SA8540 + - SA8255 + - QCS9100 + - SA8797 + +## 3. HTP Profiling Output `QnnHtpProfileArtifacts` +Both public functions `estimate_htp_profile_result()` and `generate_htp_profile_result()` return one `QnnHtpProfileArtifacts` per compiled binary in the `.pte` (partitioned graphs yield multiple entries). + +Important fields: + +- `qhas_html`: QHAS HTML report. This is the most convenient artifact for viewing the QNN HTP Analysis Summary. +- `qhas_json`: QHAS JSON report. +- `chrometrace_json`: Chrome trace JSON. Open it with `chrome://tracing` or Perfetto. +- `htp_graph_json`: HTP graph JSON after optimization. +- `htp_graph_before_json`: HTP graph JSON before optimization. +- `runtrace_json`: runtrace JSON for device profiling, or `None` when not emitted. + +Other context fields: + +- `binary_path`: dumped internal `.dlc` or `.bin` used by `qnn-profile-viewer`. +- `mode`: `"optrace"` or `"hextimate"`. +- `prepare_mode`: `"online"` or `"offline"`. + + +## 4. Viewing HTP Analysis Summary (QHAS) with QAIRT Visualizer + +**Install** + +```bash +pip install qairt-visualizer ``` -or + +**Usage** + +QAIRT Visualizer can open the QHAS result from `qhas_json` and `chrometrace_json`. Use `QnnHtpProfileArtifacts.visualizer_reports()` to pass related reports. + ```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`). +for artifact in artifacts: + qairt_visualizer.view(reports=artifact.visualizer_reports()) + print(f"QHAS HTML: {artifact.qhas_html}") +``` +**Example** +The example scripts already call QAIRT Visualizer after producing artifacts when `qairt-visualizer` is installed. If it is not installed, they still print the generated `qhas_html` path: -Note: Files ending with `.bin` do not support graph visualization in qairt_visualizer. +- `examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_online.py` +- `examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_offline.py` +- `examples/qualcomm/util_scripts/htp_profiling_on_host_hextimate.py` -## Demo
- QAIRT visualizer demo
+ QAIRT Visualizer showing HTP profiling results
-For more details, visit the [QAIRT Visualizer](https://pypi.org/project/qairt-visualizer/). +For the viewer package, see [QAIRT Visualizer](https://pypi.org/project/qairt-visualizer/). # ExecuTorch QNN Intermediate Output Debugger diff --git a/backends/qualcomm/debugger/utils.py b/backends/qualcomm/debugger/utils.py index 01cb267e917..84a13744090 100644 --- a/backends/qualcomm/debugger/utils.py +++ b/backends/qualcomm/debugger/utils.py @@ -4,12 +4,22 @@ import shutil import subprocess import tempfile -from typing import Sequence, Tuple +from dataclasses import dataclass +from typing import List, Literal, Optional, Sequence, Tuple import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManager import pandas as pd import torch -from executorch.backends.qualcomm.serialization.qc_schema import QcomChipset +from executorch.backends.qualcomm.serialization.qc_schema import ( + QcomChipset, + QnnExecuTorchProfileLevel, +) +from executorch.backends.qualcomm.serialization.qc_schema_serialize import ( + flatbuffer_to_option, +) +from executorch.backends.qualcomm.utils.check_qnn_version import ( + is_qnn_sdk_version_less_than, +) from executorch.backends.qualcomm.utils.utils import dump_context_from_pte from graphviz import Digraph @@ -190,14 +200,78 @@ def draw(self): shutil.move(dot_file, dot_dest_file) +@dataclass(frozen=True) +class QnnHtpProfileArtifacts: + """Files emitted for one HTP optrace/hextimate run. + + Data fields: + - binary_path: dumped `.dlc` or `.bin` input. + - mode: `"optrace"` for on-device hardware counters, or `"hextimate"` + for host-only compile-time estimation. + - prepare_mode: `"online"` for `.dlc` from `QnnConfig.online_prepare=True`, + or `"offline"` for `.bin` from `online_prepare=False`. + - chrometrace_json: Chrome trace JSON, viewable with `chrome://tracing` or + Perfetto. + - qhas_json: QHAS JSON + - qhas_html: QHAS HTML report. + - htp_graph_json: HTP graph JSON after optimization. + - htp_graph_before_json: HTP graph JSON before optimization. + - runtrace_json: runtrace JSON for optrace, or None when not emitted. + + Use visualizer_reports() for the subset safe to pass to + qairt_visualizer.view(reports=...). + """ + + binary_path: str + mode: Literal["optrace", "hextimate"] + prepare_mode: Literal["online", "offline"] + chrometrace_json: str + qhas_json: Optional[str] + qhas_html: str + htp_graph_json: str + htp_graph_before_json: str + runtrace_json: Optional[str] + + def visualizer_reports(self) -> List[str]: + """Reports safe to pass to qairt_visualizer.view(reports=...).""" + reports = [self.chrometrace_json] + if self.qhas_json is not None: + reports.append(self.qhas_json) + return reports + + +# Hextimate (compile-time perf estimation) requires SDK >= 2.41. Below that, +# qnn-context-binary-generator silently drops the hextimate parameters +_MIN_SDK_FOR_HEXTIMATE = "2.41" +_HEXTIMATE_SUPPORTED_SOCS = ( + QcomChipset.SA8540, + QcomChipset.SA8255, + QcomChipset.QCS9100, + QcomChipset.SA8797, +) + + +# - QNN: libQnnHtpNetRunExtensions.so +# - QAIRT: libQairtHtpBackendExtensions.so (QAIRT 2.49+ sdk) +_BACKEND_EXTENSIONS_LIB = "libQnnHtpNetRunExtensions.so" + + class QnnTool: + """Host-side wrapper around the QNN profiling CLI toolchain. + + Compatibility (see README.md §QAIRT Profiling for the user-facing table): + - Optrace: SDK 2.37+ + - Hextimate: SDK 2.41+ (gate and raise error) + + """ + def __init__( self, - tmp_dir, - sample_input, + artifact_dir, soc_id, adb, - build_folder, + sample_input=None, + build_folder=None, workspace="/data/local/tmp/qnn_executorch_test", ): self.qnn_sdk = os.environ.get("QNN_SDK_ROOT", None) @@ -205,17 +279,18 @@ def __init__( 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.artifact_dir = artifact_dir self.workspace = workspace self.adb = adb self.sample_input = sample_input self.build_folder = build_folder - self.root = os.getcwd() - self.config = { - "backend_extension_config": { - "backend_extensions": { - "config_file_path": "config.json", - }, + self.soc_id = soc_id + + def _get_base_config(self): + # Generate base device profile — every subprocess call clones from this. + return { + "backend_extensions": { + "config_file_path": os.path.join(self.artifact_dir, "config.json"), }, "config": { "devices": [ @@ -224,59 +299,94 @@ def __init__( "cores": [ {"perf_profile": "burst", "rpc_control_latency": 100} ], - "soc_id": int(soc_id), + "soc_id": int(self.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) + def _write_config_files( + self, backend_extensions: Optional[dict] = None + ) -> Tuple[str, str]: + """Write backend_extension_config.json and config.json in artifact_dir. + + Returns (backend_ext_path, config_path). + """ + if backend_extensions is None: + backend_extensions = self._get_base_config()["backend_extensions"] + + backend_ext_path = os.path.join( + self.artifact_dir, "backend_extension_config.json" + ) + config_path = os.path.join(self.artifact_dir, "config.json") + with open(backend_ext_path, "w") as f: + json.dump({"backend_extensions": backend_extensions}, f, indent=4) + with open(config_path, "w") as f: + json.dump(self._get_base_config()["config"], f, indent=4) + return backend_ext_path, config_path + + def _run(self, cmd: List[str], step: str) -> None: + """Run a subprocess with argv list; assert on non-zero exit.""" + result = subprocess.run(cmd, capture_output=True) + assert result.returncode == 0, ( + f"{step} failed (exit {result.returncode}): " + f"{result.stderr.decode('utf-8', errors='replace')}" + ) + + def _qnn_context_binary_generator( + self, + qnn_binary_file: str, + binary_name: str, + enable_hextimate: bool, + ) -> None: target = "x86_64-linux-clang" - cmds = [ + backend_ext = self._get_base_config()["backend_extensions"] + if enable_hextimate: + backend_ext["shared_library_path"] = ( + f"{self.qnn_sdk}/lib/{target}/{_BACKEND_EXTENSIONS_LIB}" + ) + backend_ext_path, _ = self._write_config_files(backend_ext) + + cmd = [ 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", + os.path.join(self.artifact_dir, qnn_binary_file), + "--config_file", + backend_ext_path, + "--binary_file", + binary_name, + "--output_dir", + self.artifact_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"): - - 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) + self._run(cmd, "qnn-context-binary-generator") + expected = os.path.join(self.artifact_dir, f"{binary_name}.bin") + assert os.path.isfile( + expected + ), f"qnn-context-binary-generator ran but did not produce {expected}" + + def _qnn_net_run(self, graph_name: str) -> None: + # backend-extensions library path is device-relative when running via adb + backend_ext = { + "shared_library_path": f"./{_BACKEND_EXTENSIONS_LIB}", + "config_file_path": "config.json", + } + backend_ext_path, config_path = self._write_config_files(backend_ext) 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}/lib/{target}/{_BACKEND_EXTENSIONS_LIB}", + backend_ext_path, + config_path, + os.path.join(self.artifact_dir, f"{graph_name}.bin"), f"{self.qnn_sdk}/bin/{target}/qnn-net-run", ] cmds = [ @@ -303,132 +413,289 @@ def qnn_net_run(self, graph_name="forward.serialized"): "pull", "-a", f"{self.workspace}/output/qnn-profiling-data_0.log", - self.tmp_dir, + self.artifact_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"] = {"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) + f"{self.artifact_dir}/qnn-profiling-data_0.log" + ), f"Error: qnn-profiling-data_0.log not found in {self.artifact_dir}" + + def _qnn_profile_viewer(self, schematic_stem: str, graph_idx: int) -> None: + # profile-viewer takes its own config schema (`features`), not the + # device profile schema. Written to the SAME file name because that's + # the flag qnn-profile-viewer expects — a per-step fresh config avoids + # any leak from earlier CBG/net-run configs. + backend_ext_path = os.path.join( + self.artifact_dir, "backend_extension_config.json" + ) + with open(backend_ext_path, "w") as f: + json.dump({"features": {"qhas_json": True}}, f, indent=4) target = "x86_64-linux-clang" - cmds = [ + # TODO: remove assumption that AOT dumpped schematic file exists in same cwd + # we need to make .pte self-contained. + schematic = os.path.join(os.getcwd(), f"{schematic_stem}.bin") + assert os.path.isfile(schematic), ( + f"qnn-profile-viewer expected schematic at {schematic}; " + "in case of online_prepare, the context-binary-generator step should have produced it in artifact_dir. " + "in case of offline_prepare, the schematic should be dumpped from pte, make sure profiling_level=3 when generating pte. " + ) + + cmd = [ 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", + "--config", + backend_ext_path, + "--schematic", + schematic, + "--reader", + f"{self.qnn_sdk}/lib/{target}/libQnnHtpOptraceProfilingReader.so", + "--input_log", + os.path.join(self.artifact_dir, "qnn-profiling-data_0.log"), + "--output", + os.path.join(self.artifact_dir, f"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')}" + self._run(cmd, "qnn-profile-viewer") - def generate_optrace( - self, - qnn_binary_file="forward_0.dlc", - ): + def _validated_qhas_json(self, qhas_path: str) -> Optional[str]: + """Return path if the QHAS JSON parses; None if truncated (hextimate SDK bug). + + Note: QHAS JSON is truncated for hextimate mode (SDK bug, still open as of + 2.50 nightly). When time_us == 0, the SDK divides 1e6 / 0 = +Infinity, + rapidjson rejects the write, and the JSON stream is cut at ~3900 bytes + with "inf_per_s": . We detect this and return qhas_json=None + rather than repair — the HTML report and chrometrace remain valid. """ - Generate Qnn HTP Optrace Profiling https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-10/htp_backend.html#qnn-htp-optrace-profiling - and QNN HTP Analysis Summary (QHAS) https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-10/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. + if not os.path.isfile(qhas_path): + return None + try: + with open(qhas_path, "r") as f: + json.load(f) + return qhas_path + except json.JSONDecodeError: + return None + + def run( + self, + mode: Literal["optrace", "hextimate"], + binary_file: str, + ) -> QnnHtpProfileArtifacts: + """Run qnn-profile-viewer for one dumped .dlc/.bin. + + Docs: + - Optrace: https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-10/htp_backend.html#qnn-htp-optrace-profiling + - Hextimate: https://docs.qualcomm.com/doc/80-63442-10/topic/htp_backend.html#qnn-htp-hextimate-profiling + - QHAS: https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-10/htp_backend.html#qnn-htp-analysis-summary-qhas """ - 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'." + assert mode in ("optrace", "hextimate"), f"unknown mode {mode!r}" - # 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 + graph_name, ext = os.path.splitext(binary_file) + if mode == "optrace": + assert ext in (".dlc", ".bin"), ( + f"optrace supports .dlc (online prepare) and .bin (offline prepare); " + f"got {ext!r}" + ) + else: # hextimate + assert ext == ".dlc", ( + f"hextimate requires .dlc (online prepare); got {ext!r}. " + "For offline-prepare context binaries, use mode='optrace' instead." + ) + if is_qnn_sdk_version_less_than(_MIN_SDK_FOR_HEXTIMATE): + # SDK < 2.41 silently drops hextimate config and produces a + # standard context binary — fail loudly before that trap. + raise AssertionError( + f"hextimate requires QNN SDK >= {_MIN_SDK_FOR_HEXTIMATE}; " + f"the current SDK at $QNN_SDK_ROOT={self.qnn_sdk} is older. " + "Older SDKs silently ignore hextimate parameters and emit " + "an ordinary profiling log with no hextimate events." + ) + + prepare_mode: Literal["online", "offline"] = ( + "online" if ext == ".dlc" else "offline" + ) + # Extract graph index if the file follows the "_" convention. + match = re.match(r"^(.*)_(\d+)$", graph_name) 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" + else: + graph_base_name = graph_name + graph_idx = 0 + + # Step 1: for online-prepare (.dlc), materialize the context binary + + # schematic on the host. Offline-prepare (.bin) already has both. + if ext == ".dlc": + self._qnn_context_binary_generator( + qnn_binary_file=binary_file, + binary_name=f"{graph_base_name}.serialized", + enable_hextimate=(mode == "hextimate"), ) 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 + # Step 2: for optrace, run on device. Hextimate is host-only. + if mode == "optrace": + self._qnn_net_run(graph_name=graph_name) + + # Step 3: post-process into optrace.json + QHAS side-artifacts. + self._qnn_profile_viewer( + schematic_stem=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) + # Collect the six output files. qnn-profile-viewer names them by + # stripping `.json` off --output and appending suffixes. + base = os.path.join(self.artifact_dir, f"optrace_{graph_idx}") + chrometrace_json = f"{base}.json" + qhas_json_candidate = f"{base}_qnn_htp_analysis_summary.json" + qhas_html = f"{base}_qnn_htp_analysis_summary.html" + htp_graph_json = f"{base}_htp.json" + htp_graph_before_json = f"{base}_htp_graph_before.json" + runtrace_json = f"{base}_runtrace.json" - 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( + chrometrace_json + ), f"qnn-profile-viewer did not produce {chrometrace_json}" + + return QnnHtpProfileArtifacts( + binary_path=os.path.join(self.artifact_dir, binary_file), + mode=mode, + prepare_mode=prepare_mode, + chrometrace_json=chrometrace_json, + qhas_json=self._validated_qhas_json(qhas_json_candidate), + qhas_html=qhas_html, + htp_graph_json=htp_graph_json, + htp_graph_before_json=htp_graph_before_json, + runtrace_json=(runtrace_json if os.path.isfile(runtrace_json) else None), ) - 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." + + +def _validate_pte_profile_level(pte_path: str) -> None: + """Assert that any offline-prepare .pte was built with profile_level=3.""" + from executorch.exir._serialize._program import deserialize_pte_binary + + with open(pte_path, "rb") as f: + program = deserialize_pte_binary(f.read()).program + + for execution_plan in program.execution_plan: + for delegate in execution_plan.delegates: + if delegate.id != "QnnBackend": + continue + spec = delegate.compile_specs[0] + options = flatbuffer_to_option(bytes(spec.value)) + if options.online_prepare: + continue # online-prepare: profile_level is set on-host later + assert options.profile_level == QnnExecuTorchProfileLevel.kProfileOptrace, ( + f"{pte_path} was compiled with online_prepare=False and " + f"profile_level={options.profile_level.name}." + "HTP Profling (Optrace) feature requires profile_level=3 (kProfileOptrace) " + "at build_executorch_binary() time — \n" + "Please re-export with qnn_config.profile_level=3, or use online_prepare=True" + ) + + +def _validate_hextimate_soc(soc_id: QcomChipset) -> None: + if soc_id not in _HEXTIMATE_SUPPORTED_SOCS: + supported = ", ".join(soc.name for soc in _HEXTIMATE_SUPPORTED_SOCS) + raise AssertionError( + f"hextimate currently supports only {supported}; got {soc_id.name}." ) - return optrace_path, qhas_path + +def _generate_htp_analysis_result( + artifact_dir: str, + soc_id: QcomChipset, + pte_path: str, + mode: Literal["optrace", "hextimate"], + inputs: Optional[Sequence[Tuple[torch.Tensor]]] = None, + adb=None, +) -> List[QnnHtpProfileArtifacts]: + assert mode in ("optrace", "hextimate"), f"unknown mode {mode!r}" + if mode == "optrace": + assert adb is not None, "optrace requires adb for on-device execution" + _validate_pte_profile_level(pte_path) + + dumpfiles = dump_context_from_pte(pte_path, output_dir=artifact_dir) + + qnn_tool = QnnTool( + artifact_dir=artifact_dir, + sample_input=inputs, + soc_id=soc_id, + adb=adb, + build_folder=(adb.build_path if adb is not None else None), + workspace=(adb.workspace if adb is not None else None), + ) + + return [qnn_tool.run(mode=mode, binary_file=os.path.basename(f)) for f in dumpfiles] +# backward compatibility shim def generate_optrace( - artifact, + artifact: str, soc_id: QcomChipset, adb, pte_path: str, inputs: Sequence[Tuple[torch.Tensor]], -): - """ - Generate optrace and QHAS (QNN HTP Analysis Summary) JSON files. +) -> List[QnnHtpProfileArtifacts]: + """Legacy positional wrapper for generate_htp_profile_result().""" + return generate_htp_profile_result(artifact, soc_id, pte_path, inputs, adb) - 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 Sequence((Tuple[torch.Tensor])): The input tensors for the model. +def generate_htp_profile_result( + artifact_dir: str, + soc_id: QcomChipset, + pte_path: str, + inputs: Sequence[Tuple[torch.Tensor]], + adb, +) -> List[QnnHtpProfileArtifacts]: + """Generate HTP optrace artifacts from a .pte by running on device. + + Arguments: + - artifact_dir: host directory for dumped `.dlc`/`.bin`, schematics, QNN + configs, profiling logs, and qnn-profile-viewer outputs. + - soc_id: target SoC used by the compiled `.pte`; must match the device. + - pte_path: `.pte` produced by build_executorch_binary(). + - inputs: sample input tensors used by qnn-net-run for optrace collection. + - adb: SimpleADB helper for pushing files, running qnn-net-run, and + pulling `qnn-profiling-data_0.log` from the device. + + Supported prepare modes for the input pte file: + - `QnnConfig.online_prepare=True`: `.pte` contains `.dlc`; + We will create the context binary and schematic before device execution. + - `QnnConfig.online_prepare=False`: `.pte` contains finalized `.bin`, user must + also set `QnnConfig.profile_level=3` so optrace instrumentation is already baked in; + We will extract schematic and context binary from pte and continue device execution. - 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]) + return _generate_htp_analysis_result( + artifact_dir=artifact_dir, + soc_id=soc_id, + pte_path=pte_path, + inputs=inputs, + mode="optrace", + adb=adb, + ) - # 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, +def estimate_htp_profile_result( + artifact_dir: str, + soc_id: QcomChipset, + pte_path: str, +) -> List[QnnHtpProfileArtifacts]: + """Estimate HTP performance with host-only hextimate artifacts. + + Arguments: + - artifact_dir: host directory for dumped `.dlc`, QNN configs, and + qnn-profile-viewer outputs. + - soc_id: target SoC used by the compiled `.pte`; currently limited to: + SA8540, SA8255, QCS9100, and SA8797. + - pte_path: `.pte` produced by build_executorch_binary(), requiring + `QnnConfig.online_prepare=True` for `.pte` generation and QNN SDK >= 2.41. + """ + _validate_hextimate_soc(soc_id) + return _generate_htp_analysis_result( + artifact_dir=artifact_dir, + soc_id=soc_id, + pte_path=pte_path, + mode="hextimate", ) - - 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 diff --git a/backends/qualcomm/serialization/qc_compiler_spec.fbs b/backends/qualcomm/serialization/qc_compiler_spec.fbs index 57708c959e9..c5fbabb90c3 100644 --- a/backends/qualcomm/serialization/qc_compiler_spec.fbs +++ b/backends/qualcomm/serialization/qc_compiler_spec.fbs @@ -65,6 +65,7 @@ enum QcomChipset: int { SW6100 = 96, QCM6490 = 93, SM8845 = 97, + SA8540 = 62, } /// Indicate the information of the specified SoC. diff --git a/backends/qualcomm/serialization/qc_schema.py b/backends/qualcomm/serialization/qc_schema.py index aeffbc069b6..1ad7363fb16 100644 --- a/backends/qualcomm/serialization/qc_schema.py +++ b/backends/qualcomm/serialization/qc_schema.py @@ -72,6 +72,7 @@ class QcomChipset(IntEnum): SW6100 = 96 # v81 QCM6490 = 93 # v68 SM8845 = 97 # v81 + SA8540 = 62 # v68 @dataclass @@ -106,6 +107,7 @@ class SocInfo: QcomChipset.SW6100: SocInfo(QcomChipset.SW6100, HtpInfo(HtpArch.V81, 4)), QcomChipset.QCM6490: SocInfo(QcomChipset.QCM6490, HtpInfo(HtpArch.V68, 2)), QcomChipset.SM8845: SocInfo(QcomChipset.SM8845, HtpInfo(HtpArch.V81, 8)), + QcomChipset.SA8540: SocInfo(QcomChipset.SA8540, HtpInfo(HtpArch.V68, 8)), } diff --git a/backends/qualcomm/tests/test_qnn_delegate.py b/backends/qualcomm/tests/test_qnn_delegate.py index 1ebda343c8c..2e9571be419 100644 --- a/backends/qualcomm/tests/test_qnn_delegate.py +++ b/backends/qualcomm/tests/test_qnn_delegate.py @@ -22,7 +22,10 @@ from executorch.backends.qualcomm._passes.qnn_pass_manager import ( get_qnn_pass_manager_cls, ) -from executorch.backends.qualcomm.debugger.utils import generate_optrace +from executorch.backends.qualcomm.debugger.utils import ( + estimate_htp_profile_result, + generate_htp_profile_result, +) from executorch.backends.qualcomm.export_utils import ( get_backend_type, @@ -31,6 +34,7 @@ ) from executorch.backends.qualcomm.quantizer.rules import Q_ANNOTATION_KEY from executorch.backends.qualcomm.serialization.qc_schema import ( + QcomChipset, QnnExecuTorchBackendType, QnnExecuTorchHtpPerformanceMode, ) @@ -99,6 +103,19 @@ from torchao.quantization.pt2e.quantizer import SharedQuantizationSpec +class TestQNNDebuggerProfilePublicApis(unittest.TestCase): + def test_estimate_htp_profile_result_rejects_unsupported_soc_before_pte(self): + with self.assertRaisesRegex( + AssertionError, + "hextimate currently supports only.*SA8540.*SA8255.*QCS9100.*SA8797", + ): + estimate_htp_profile_result( + artifact_dir="/path/that/must/not/be/read", + soc_id=QcomChipset.SM8650, + pte_path="/path/that/must/not/be/read/model.pte", + ) + + class TestQNNFloatingPointOperator(TestQNN): def setUp(self): match get_backend_type(self.backend): @@ -4443,19 +4460,20 @@ def test_qnn_backend_hadamard_transform_linear(self): with open(pte_path, "wb") as f: edge_prog_mgr.write_to_file(f) adb = self.get_adb_tool(pte_path) - binaries_trace = generate_optrace( - tmp_dir, - self.chipset_table[TestQNN.soc_model], - adb, - pte_path, - [sample_input], + artifacts = generate_htp_profile_result( + artifact_dir=tmp_dir, + soc_id=self.chipset_table[TestQNN.soc_model], + pte_path=pte_path, + inputs=[sample_input], + adb=adb, ) htp_ops = [] - for _, (_, qhas) in binaries_trace.items(): - with open(qhas, "r") as qhas_file: + for artifact in artifacts: + self.assertIsNotNone(artifact.qhas_json) + with open(artifact.qhas_json, "r") as qhas_file: qhas_data = json.load(qhas_file) - for row in qhas_data["data"]["qnn_op_types"]["data"]: - htp_ops.append(row["op"]) + for row in qhas_data["data"]["qnn_op_types"]["data"]: + htp_ops.append(row["op"]) self.assertTrue( any("HadamardTransform" in op for op in htp_ops), "Expected linear to be lowered to HadamardTransform " @@ -4503,19 +4521,20 @@ def test_qnn_backend_hadamard_transform_matmul(self): with open(pte_path, "wb") as f: edge_prog_mgr.write_to_file(f) adb = self.get_adb_tool(pte_path) - binaries_trace = generate_optrace( - tmp_dir, - self.chipset_table[TestQNN.soc_model], - adb, - pte_path, - [sample_input], + artifacts = generate_htp_profile_result( + artifact_dir=tmp_dir, + soc_id=self.chipset_table[TestQNN.soc_model], + pte_path=pte_path, + inputs=[sample_input], + adb=adb, ) htp_ops = [] - for _, (_, qhas) in binaries_trace.items(): - with open(qhas, "r") as qhas_file: + for artifact in artifacts: + self.assertIsNotNone(artifact.qhas_json) + with open(artifact.qhas_json, "r") as qhas_file: qhas_data = json.load(qhas_file) - for row in qhas_data["data"]["qnn_op_types"]["data"]: - htp_ops.append(row["op"]) + for row in qhas_data["data"]["qnn_op_types"]["data"]: + htp_ops.append(row["op"]) self.assertTrue( any("HadamardTransform" in op for op in htp_ops), "Expected matmul to be lowered to HadamardTransform " @@ -4557,19 +4576,20 @@ def test_qnn_backend_hadamard_transform_conv(self): with open(pte_path, "wb") as f: edge_prog_mgr.write_to_file(f) adb = self.get_adb_tool(pte_path) - binaries_trace = generate_optrace( - tmp_dir, - self.chipset_table[TestQNN.soc_model], - adb, - pte_path, - [sample_input], + artifacts = generate_htp_profile_result( + artifact_dir=tmp_dir, + soc_id=self.chipset_table[TestQNN.soc_model], + pte_path=pte_path, + inputs=[sample_input], + adb=adb, ) htp_ops = [] - for _, (_, qhas) in binaries_trace.items(): - with open(qhas, "r") as qhas_file: + for artifact in artifacts: + self.assertIsNotNone(artifact.qhas_json) + with open(artifact.qhas_json, "r") as qhas_file: qhas_data = json.load(qhas_file) - for row in qhas_data["data"]["qnn_op_types"]["data"]: - htp_ops.append(row["op"]) + for row in qhas_data["data"]["qnn_op_types"]["data"]: + htp_ops.append(row["op"]) self.assertTrue( any("HadamardTransform" in op for op in htp_ops), "Expected conv to be lowered to HadamardTransform " @@ -6523,19 +6543,20 @@ def test_qnn_backend_activation_fusion(self): with open(pte_path, "wb") as f: edge_prog_mgr.write_to_file(f) adb = self.get_adb_tool(pte_path) - binaries_trace = generate_optrace( - tmp_dir, - self.chipset_table[TestQNN.soc_model], - adb, - pte_path, - [tc[QCOM_SAMPLE_INPUTS]], + artifacts = generate_htp_profile_result( + artifact_dir=tmp_dir, + soc_id=self.chipset_table[TestQNN.soc_model], + pte_path=pte_path, + inputs=[tc[QCOM_SAMPLE_INPUTS]], + adb=adb, ) htp_ops = [] - for _, (_, qhas) in binaries_trace.items(): - with open(qhas, "r") as qhas_file: - qhas_data = json.load(qhas_file) - for row in qhas_data["data"]["htp_op_types"]["data"]: - htp_ops.append(row["op"]) + for a in artifacts: + self.assertIsNotNone(a.qhas_json) + with open(a.qhas_json, "r") as f: + qhas_data = json.load(f) + for row in qhas_data["data"]["htp_op_types"]["data"]: + htp_ops.append(row["op"]) has_conv = any("ConvLayer" in op for op in htp_ops) self.assertTrue( has_conv, f"Expected Conv op in HTP ops, got: {htp_ops}" @@ -6652,20 +6673,21 @@ def test_qnn_backend_masked_softmax(self): with open(pte_path, "wb") as f: edge_prog_mgr.write_to_file(f) adb = self.get_adb_tool(pte_path) - binaries_trace = generate_optrace( - tmp_dir, - self.chipset_table[self.soc_model], - adb, - pte_path, - [sample_input], + artifacts = generate_htp_profile_result( + artifact_dir=tmp_dir, + soc_id=self.chipset_table[self.soc_model], + pte_path=pte_path, + inputs=[sample_input], + adb=adb, ) has_masked_softmax = False - for _, (_, qhas) in binaries_trace.items(): - with open(qhas, "r") as qhas_file: - qhas_data = json.load(qhas_file) - for row in qhas_data["data"]["htp_op_types"]["data"]: - if "MaskedSoftmax" in row["op"]: - has_masked_softmax = True + for a in artifacts: + self.assertIsNotNone(a.qhas_json) + with open(a.qhas_json, "r") as f: + qhas_data = json.load(f) + for row in qhas_data["data"]["htp_op_types"]["data"]: + if "MaskedSoftmax" in row["op"]: + has_masked_softmax = True self.assertTrue(has_masked_softmax) @unittest.skip("UT pass before QNN 2.26, segfault during partitioner") @@ -7356,13 +7378,16 @@ def test_qnn_backend_dump_context_from_pte(self): module, sample_input, compiler_spec ).to_executorch() - with tempfile.TemporaryDirectory() as tmp_dir: + with ( + tempfile.TemporaryDirectory() as tmp_dir, + tempfile.TemporaryDirectory() as dump_dir, + ): pte_path = f"{tmp_dir}/model.pte" with open(pte_path, "wb") as f: edge_prog_mgr.write_to_file(f) - dump_context_from_pte(pte_path) - binary_name = f"{tmp_dir}/forward_0.bin" + dump_context_from_pte(pte_path, output_dir=dump_dir) + binary_name = f"{dump_dir}/forward_0.bin" self.assertTrue(os.path.isfile(binary_name)) with open(binary_name, "rb") as f: stripped_binary = f.read() @@ -7512,12 +7537,17 @@ def test_qnn_backend_draw_graph(self): def test_qnn_backend_generate_optrace(self): if self.enable_x86_64: self.skipTest( - "At the moment, testing is only being conducted on the device." + "Optrace requires on-device execution; not supported on x86_64 host." ) module = SimpleModel() # noqa: F405 sample_input = (torch.ones(1, 32, 28, 28), torch.ones(1, 32, 28, 28)) backend_options = generate_htp_compiler_spec(use_fp16=True) + # Two compiler specs exercise both prepare modes: + # - online-prepare: profile_level is set by the QNN CLI at host-side + # context-binary-generation time, so no profile_level here. + # - offline-prepare: profile_level=3 is REQUIRED at AoT so the .pte's + # embedded context binary carries optrace instrumentation and schematic.bin. compiler_specs = [ generate_qnn_executorch_compiler_spec( soc_model=self.chipset_table[TestQNN.soc_model], @@ -7532,40 +7562,33 @@ def test_qnn_backend_generate_optrace(self): ] for compiler_spec in compiler_specs: + edge_prog_mgr = to_edge_transform_and_lower_to_qnn( + module, sample_input, compiler_spec + ).to_executorch() with tempfile.TemporaryDirectory() as tmp_dir: - edge_prog_mgr = to_edge_transform_and_lower_to_qnn( - module, sample_input, compiler_spec - ).to_executorch() pte_path = f"{tmp_dir}/model.pte" with open(pte_path, "wb") as f: edge_prog_mgr.write_to_file(f) adb = self.get_adb_tool(pte_path) - binaries_trace = generate_optrace( - tmp_dir, - self.chipset_table[self.soc_model], - adb, - pte_path, - [sample_input], + artifacts = generate_htp_profile_result( + artifact_dir=tmp_dir, + soc_id=self.chipset_table[self.soc_model], + pte_path=pte_path, + inputs=[sample_input], + adb=adb, ) - for _, (optrace, qhas) in binaries_trace.items(): - with open(optrace, "r") as optrace_file: - optrace_data = json.load(optrace_file) - # { - # header: - # { - # 'header_version': {'major': x, 'minor': y, 'patch': z}, - # 'version': {'major': x, 'minor': y, 'patch': z}, - # 'artifact_type': 'OP_TRACE' - # } - # traceEvents: - # {...} - # } - for row in optrace_data["traceEvents"]: - self.assertIn("pid", row) - with open(qhas, "r") as qhas_file: - qhas_data = json.load(qhas_file) - self.assertIn("data", qhas_data) + for a in artifacts: + with open(a.chrometrace_json, "r") as f: + chrometrace = json.load(f) + for row in chrometrace["traceEvents"]: + self.assertIn("pid", row) + self.assertIsNotNone( + a.qhas_json, + "optrace mode should produce a valid QHAS JSON.", + ) + with open(a.qhas_json, "r") as f: + self.assertIn("data", json.load(f)) class TestQNNQuantizedUtils(TestQNN): @@ -8603,7 +8626,7 @@ def test_qnn_backend_draw_graph(self): def test_qnn_backend_generate_optrace(self): if self.enable_x86_64: self.skipTest( - "At the moment, testing is only being conducted on the device." + "Optrace requires on-device execution; not supported on x86_64 host." ) if get_backend_type(self.backend) == QnnExecuTorchBackendType.kLpaiBackend: self.skipTest("LPAI does not support optrace generation.") @@ -8612,6 +8635,11 @@ def test_qnn_backend_generate_optrace(self): module = self.get_qdq_module(module, sample_input) backend_options = generate_htp_compiler_spec(use_fp16=True) + # Two compiler specs exercise both prepare modes: + # - online-prepare: profile_level is set by the QNN CLI at host-side + # context-binary-generation time, so no profile_level here. + # - offline-prepare: profile_level=3 is REQUIRED at AoT so the .pte's + # embedded context binary carries optrace instrumentation and schematic.bin. compiler_specs = [ generate_qnn_executorch_compiler_spec( soc_model=self.chipset_table[TestQNN.soc_model], @@ -8626,40 +8654,83 @@ def test_qnn_backend_generate_optrace(self): ] for compiler_spec in compiler_specs: + edge_prog_mgr = to_edge_transform_and_lower_to_qnn( + module, sample_input, compiler_spec + ).to_executorch() with tempfile.TemporaryDirectory() as tmp_dir: - edge_prog_mgr = to_edge_transform_and_lower_to_qnn( - module, sample_input, compiler_spec - ).to_executorch() pte_path = f"{tmp_dir}/model.pte" with open(pte_path, "wb") as f: edge_prog_mgr.write_to_file(f) adb = self.get_adb_tool(pte_path) - binaries_trace = generate_optrace( - tmp_dir, - self.chipset_table[self.soc_model], - adb, - pte_path, - [sample_input], + artifacts = generate_htp_profile_result( + artifact_dir=tmp_dir, + soc_id=self.chipset_table[self.soc_model], + pte_path=pte_path, + inputs=[sample_input], + adb=adb, + ) + for a in artifacts: + with open(a.chrometrace_json, "r") as f: + chrometrace = json.load(f) + for row in chrometrace["traceEvents"]: + self.assertIn("pid", row) + self.assertIsNotNone( + a.qhas_json, + "optrace mode should produce a valid QHAS JSON ", + ) + with open(a.qhas_json, "r") as f: + self.assertIn("data", json.load(f)) + + def test_qnn_backend_generate_hextimate(self): + if not self.enable_x86_64: + self.skipTest( + "Hextimate is host-side (compile-time); requires --enable_x86_64." + ) + if get_backend_type(self.backend) == QnnExecuTorchBackendType.kLpaiBackend: + self.skipTest("LPAI does not support hextimate generation.") + module = SimpleModel() # noqa: F405 + sample_input = (torch.ones(1, 32, 28, 28), torch.ones(1, 32, 28, 28)) + module = self.get_qdq_module(module, sample_input) + backend_options = generate_htp_compiler_spec(use_fp16=True) + + # Hextimate hard-requires online prepare (.dlc). No profile_level + # required — hextimate profiling is attached by the QNN CLI at + # context-binary-generation time. + compiler_spec = generate_qnn_executorch_compiler_spec( + soc_model=self.chipset_table[TestQNN.soc_model], + backend_options=backend_options, + online_prepare=True, + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + edge_prog_mgr = to_edge_transform_and_lower_to_qnn( + module, sample_input, compiler_spec + ).to_executorch() + pte_path = f"{tmp_dir}/model.pte" + with open(pte_path, "wb") as f: + edge_prog_mgr.write_to_file(f) + + artifacts = estimate_htp_profile_result( + artifact_dir=tmp_dir, + soc_id=self.chipset_table[self.soc_model], + pte_path=pte_path, + ) + for a in artifacts: + with open(a.chrometrace_json, "r") as f: + chrometrace = json.load(f) + for row in chrometrace["traceEvents"]: + self.assertIn("pid", row) + # QHAS JSON is truncated by an upstream SDK bug (division by + # zero on time_us=0). We surface this by setting qhas_json to + # None; the HTML report and chrometrace remain usable. + self.assertIsNone( + a.qhas_json, + "hextimate QHAS JSON is expected to be truncated by the " + "SDK bug; QnnTool should have detected it and returned " + "qhas_json=None.", ) - for _, (optrace, qhas) in binaries_trace.items(): - with open(optrace, "r") as optrace_file: - optrace_data = json.load(optrace_file) - # { - # header: - # { - # 'header_version': {'major': x, 'minor': y, 'patch': z}, - # 'version': {'major': x, 'minor': y, 'patch': z}, - # 'artifact_type': 'OP_TRACE' - # } - # traceEvents: - # {...} - # } - for row in optrace_data["traceEvents"]: - self.assertIn("pid", row) - with open(qhas, "r") as qhas_file: - qhas_data = json.load(qhas_file) - self.assertIn("data", qhas_data) + self.assertTrue(os.path.isfile(a.qhas_html)) def test_qnn_backend_seq_mse(self): from executorch.backends.qualcomm._passes.seq_mse import SeqMSE @@ -11439,9 +11510,12 @@ def test_custom_op_2(self): self.assertTrue(msg["is_close"]) def test_debugger_generate_optrace(self): + # This test drives the offline-prepare demo (profile_level=3, no + # --online_prepare). See qairt_visualizer_demo_online.py for the + # online path. cmds = [ "python", - f"{self.executorch_root}/examples/qualcomm/util_scripts/qairt_visualizer_demo.py", + f"{self.executorch_root}/examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_offline.py", "--artifact", self.artifact_dir, "--build_folder", @@ -11468,25 +11542,17 @@ def test_debugger_generate_optrace(self): msg = json.loads(conn.recv()) if "Error" in msg: self.fail(msg["Error"]) - else: - for _, (optrace, qhas) in msg["binaries_trace"].items(): - with open(optrace, "r") as optrace_file: - optrace_data = json.load(optrace_file) - # { - # header: - # { - # 'header_version': {'major': x, 'minor': y, 'patch': z}, - # 'version': {'major': x, 'minor': y, 'patch': z}, - # 'artifact_type': 'OP_TRACE' - # } - # traceEvents: - # {...} - # } - for row in optrace_data["traceEvents"]: - self.assertIn("pid", row) - with open(qhas, "r") as qhas_file: - qhas_data = json.load(qhas_file) - self.assertIn("data", qhas_data) + for a in msg["artifacts"]: + with open(a["chrometrace_json"], "r") as f: + chrometrace = json.load(f) + for row in chrometrace["traceEvents"]: + self.assertIn("pid", row) + self.assertIsNotNone( + a["qhas_json"], + "optrace mode should produce a valid QHAS JSON.", + ) + with open(a["qhas_json"], "r") as f: + self.assertIn("data", json.load(f)) def test_intermediate_debugger(self): cmds = [ diff --git a/backends/qualcomm/utils/utils.py b/backends/qualcomm/utils/utils.py index 64328b520f1..aa8a665f62a 100644 --- a/backends/qualcomm/utils/utils.py +++ b/backends/qualcomm/utils/utils.py @@ -201,10 +201,10 @@ def replace_linear(module: torch.nn.Module): return replace_linear(module) -def dump_context_from_pte(pte_path) -> List[str]: +def dump_context_from_pte(pte_path, output_dir=None) -> List[str]: """ - Dump compiled binaries under the same directory of pte_path. - For partitioned graph, there will be multiple files with names f"{method_name}_{index}". + Dump compiled binaries under output_dir, or the same directory as pte_path + when output_dir is not set. 'method_name' refers to the name of a method in the nn.Module that was traced to generate this program, while 'index' indicates the order of execution. @@ -220,7 +220,8 @@ def dump_context_from_pte(pte_path) -> List[str]: program = deserialize_pte_binary(program_data).program - ctx_path = os.path.dirname(pte_path) + ctx_path = output_dir or os.path.dirname(pte_path) + os.makedirs(ctx_path, exist_ok=True) dumpfiles = [] for execution_plan in program.execution_plan: for i, delegate in enumerate(execution_plan.delegates): @@ -1339,6 +1340,7 @@ def get_soc_to_chipset_map(): "SW6100": QcomChipset.SW6100, "QCM6490": QcomChipset.QCM6490, "SM8845": QcomChipset.SM8845, + "SA8540": QcomChipset.SA8540, } diff --git a/examples/qualcomm/util_scripts/README.md b/examples/qualcomm/util_scripts/README.md index 0fc45c2ced3..00d7f87ab7f 100644 --- a/examples/qualcomm/util_scripts/README.md +++ b/examples/qualcomm/util_scripts/README.md @@ -78,6 +78,14 @@ This tool aims for users who want to deploy models with ExecuTorch runtime. It's - `cli_example/execute_output/output_{data_index}_{output_index}.pt`.
`data_index` represents the sequence of dataset, `output_index` stands for the order of graph output. +## HTP Profiling Examples + +These examples demonstrate Qualcomm HTP profiling flows: + +* `htp_profiling_on_device_op_trace_online.py`: on-device OpTrace with `online_prepare=True`. +* `htp_profiling_on_device_op_trace_offline.py`: on-device OpTrace with `online_prepare=False` and `profile_level=3`. +* `htp_profiling_on_host_hextimate.py`: host-side Hextimate. + # Generate ET Record This section describes how to generate an ET record for a .pte program using the provided script. * Generate ET record for .pte using the provided script: diff --git a/examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_offline.py b/examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_offline.py new file mode 100644 index 00000000000..4ecb703f1d0 --- /dev/null +++ b/examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_offline.py @@ -0,0 +1,132 @@ +# Copyright (c) Qualcomm Innovation Center, Inc. +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +"""HTP profiling demo — on-device OpTrace with offline_prepare. + +This example exports a `.pte` with `QnnConfig.online_prepare=False`, generates +on-device HTP OpTrace results with `generate_htp_profile_result()`, and opens +the QHAS report with QAIRT Visualizer. + +Internal detail: offline_prepare embeds a finalized QNN context binary in the +`.pte`, so `qnn_config.profile_level=3` is required during export. + +For the online_prepare OpTrace route, see +htp_profiling_on_device_op_trace_online.py. For host-only Hextimate, see +htp_profiling_on_host_hextimate.py. +""" + +import json +import os +from multiprocessing.connection import Client + +import torch +from executorch.backends.qualcomm.debugger.utils import generate_htp_profile_result +from executorch.backends.qualcomm.export_utils import ( + build_executorch_binary, + QnnConfig, + setup_common_args_and_variables, + SimpleADB, +) +from executorch.backends.qualcomm.quantizer.quantizer import QuantDtype +from executorch.backends.qualcomm.tests.models import SimpleModel +from executorch.backends.qualcomm.utils.utils import get_soc_to_chipset_map + + +def main(args) -> None: + qnn_config = QnnConfig.load_config(args.config_file if args.config_file else args) + assert not qnn_config.online_prepare, ( + "This demo uses on-device OpTrace with offline_prepare; remove " + "--online_prepare (or set online_prepare=False in your config file). " + "For online prepare, use htp_profiling_on_device_op_trace_online.py." + ) + assert qnn_config.profile_level == 3, ( + "Offline-prepare requires qnn_config.profile_level=3 so that the AoT " + "HtpContext bakes optrace instrumentation into the context binary " + "before qnn_context_get_binary() dumps it. Pass --profile_level 3 " + "(or set profile_level=3 in your config file)." + ) + + model = SimpleModel() + example_inputs = [(torch.ones(1, 32, 28, 28), torch.ones(1, 32, 28, 28))] + + pte_filename = "qnn_simple_model" + os.makedirs(args.artifact, exist_ok=True) + + build_executorch_binary( + model=model, + qnn_config=qnn_config, + file_name=f"{args.artifact}/{pte_filename}", + dataset=example_inputs, + quant_dtype=QuantDtype.use_8a8w, + ) + + adb = SimpleADB( + qnn_config=qnn_config, + pte_path=f"{args.artifact}/{pte_filename}.pte", + workspace=f"/data/local/tmp/executorch/{pte_filename}", + ) + artifacts = generate_htp_profile_result( + artifact_dir=args.artifact, + soc_id=get_soc_to_chipset_map()[args.soc_model], + pte_path=f"{args.artifact}/{pte_filename}.pte", + inputs=example_inputs, + adb=adb, + ) + + if args.ip and args.port != -1: + with Client((args.ip, args.port)) as conn: + conn.send( + json.dumps( + { + "artifacts": [ + { + "binary_path": a.binary_path, + "mode": a.mode, + "prepare_mode": a.prepare_mode, + "chrometrace_json": a.chrometrace_json, + "qhas_json": a.qhas_json, + "qhas_html": a.qhas_html, + } + for a in artifacts + ], + } + ) + ) + return + + try: + import qairt_visualizer + except ImportError: + for a in artifacts: + print(f"QHAS HTML: {a.qhas_html}") + return + + # Offline-prepare emits .bin binaries, which do not support the graph + # view in qairt-visualizer — only the reports are shown. + for a in artifacts: + qairt_visualizer.view(reports=a.visualizer_reports()) + + +if __name__ == "__main__": + parser = setup_common_args_and_variables() + parser.add_argument( + "-a", + "--artifact", + type=str, + default="", + help="The folder to store the exported program", + ) + + args = parser.parse_args() + + try: + main(args) + except Exception as e: + if args.ip and args.port != -1: + with Client((args.ip, args.port)) as conn: + conn.send(json.dumps({"Error": str(e)})) + else: + raise Exception(e) diff --git a/examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_online.py b/examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_online.py new file mode 100644 index 00000000000..5a8d216883e --- /dev/null +++ b/examples/qualcomm/util_scripts/htp_profiling_on_device_op_trace_online.py @@ -0,0 +1,130 @@ +# Copyright (c) Qualcomm Innovation Center, Inc. +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +"""HTP profiling demo — on-device OpTrace with online_prepare. + +This example exports a `.pte` with `QnnConfig.online_prepare=True`, generates +on-device HTP OpTrace results with `generate_htp_profile_result()`, and opens +the QHAS report with QAIRT Visualizer. + +Internal detail: online_prepare stores a graph description in the `.pte`; QNN +tools create the profiled context and schematic during profiling. + +For the offline_prepare OpTrace route, see +htp_profiling_on_device_op_trace_offline.py. For host-only Hextimate, see +htp_profiling_on_host_hextimate.py. +""" + +import json +import os +from multiprocessing.connection import Client + +import torch +from executorch.backends.qualcomm.debugger.utils import generate_htp_profile_result +from executorch.backends.qualcomm.export_utils import ( + build_executorch_binary, + QnnConfig, + setup_common_args_and_variables, + SimpleADB, +) +from executorch.backends.qualcomm.quantizer.quantizer import QuantDtype +from executorch.backends.qualcomm.tests.models import SimpleModel +from executorch.backends.qualcomm.utils.utils import get_soc_to_chipset_map + + +def main(args) -> None: + qnn_config = QnnConfig.load_config(args.config_file if args.config_file else args) + assert qnn_config.online_prepare, ( + "This demo uses on-device OpTrace with online_prepare; pass " + "--online_prepare (or set online_prepare=True in your config file). " + "For offline prepare, use htp_profiling_on_device_op_trace_offline.py." + ) + + model = SimpleModel() + example_inputs = [(torch.ones(1, 32, 28, 28), torch.ones(1, 32, 28, 28))] + + pte_filename = "qnn_simple_model" + os.makedirs(args.artifact, exist_ok=True) + + # lower to QNN + build_executorch_binary( + model=model, + qnn_config=qnn_config, + file_name=f"{args.artifact}/{pte_filename}", + dataset=example_inputs, + quant_dtype=QuantDtype.use_8a8w, + ) + + adb = SimpleADB( + qnn_config=qnn_config, + pte_path=f"{args.artifact}/{pte_filename}.pte", + workspace=f"/data/local/tmp/executorch/{pte_filename}", + ) + artifacts = generate_htp_profile_result( + artifact_dir=args.artifact, + soc_id=get_soc_to_chipset_map()[args.soc_model], + pte_path=f"{args.artifact}/{pte_filename}.pte", + inputs=example_inputs, + adb=adb, + ) + + if args.ip and args.port != -1: + with Client((args.ip, args.port)) as conn: + conn.send( + json.dumps( + { + "artifacts": [ + { + "binary_path": a.binary_path, + "mode": a.mode, + "prepare_mode": a.prepare_mode, + "chrometrace_json": a.chrometrace_json, + "qhas_json": a.qhas_json, + "qhas_html": a.qhas_html, + } + for a in artifacts + ], + } + ) + ) + return + + try: + import qairt_visualizer + except ImportError: + for a in artifacts: + print(f"QHAS HTML: {a.qhas_html}") + return + + # Visualize each binary. Online-prepare emits .dlc, which supports the + # graph view; offline .bin binaries do not. + for a in artifacts: + if a.binary_path.endswith(".dlc"): + qairt_visualizer.view(a.binary_path, reports=a.visualizer_reports()) + else: + qairt_visualizer.view(reports=a.visualizer_reports()) + + +if __name__ == "__main__": + parser = setup_common_args_and_variables() + parser.add_argument( + "-a", + "--artifact", + type=str, + default="", + help="The folder to store the exported program", + ) + + args = parser.parse_args() + + try: + main(args) + except Exception as e: + if args.ip and args.port != -1: + with Client((args.ip, args.port)) as conn: + conn.send(json.dumps({"Error": str(e)})) + else: + raise Exception(e) diff --git a/examples/qualcomm/util_scripts/qairt_visualizer_demo.py b/examples/qualcomm/util_scripts/htp_profiling_on_host_hextimate.py similarity index 51% rename from examples/qualcomm/util_scripts/qairt_visualizer_demo.py rename to examples/qualcomm/util_scripts/htp_profiling_on_host_hextimate.py index 9d2ce7a8806..b3e9f16dcce 100644 --- a/examples/qualcomm/util_scripts/qairt_visualizer_demo.py +++ b/examples/qualcomm/util_scripts/htp_profiling_on_host_hextimate.py @@ -4,18 +4,26 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. +"""HTP profiling demo — Hextimate on host. + +This example exports a `.pte` with `QnnConfig.online_prepare=True`, estimates +HTP performance on host with `estimate_htp_profile_result()`, and opens the +QHAS report with QAIRT Visualizer. + +Hextimate is compile-time estimation, not an on-device OpTrace run. It does not +use sample inputs or adb during profiling. +""" + import json import os from multiprocessing.connection import Client -import qairt_visualizer import torch -from executorch.backends.qualcomm.debugger.utils import generate_optrace +from executorch.backends.qualcomm.debugger.utils import estimate_htp_profile_result from executorch.backends.qualcomm.export_utils import ( build_executorch_binary, QnnConfig, setup_common_args_and_variables, - SimpleADB, ) from executorch.backends.qualcomm.quantizer.quantizer import QuantDtype from executorch.backends.qualcomm.tests.models import SimpleModel @@ -24,17 +32,17 @@ def main(args) -> None: qnn_config = QnnConfig.load_config(args.config_file if args.config_file else args) + assert qnn_config.online_prepare, ( + "This demo uses host-only Hextimate; pass --online_prepare " + "(or set online_prepare=True in your config file)." + ) + model = SimpleModel() example_inputs = [(torch.ones(1, 32, 28, 28), torch.ones(1, 32, 28, 28))] pte_filename = "qnn_simple_model" os.makedirs(args.artifact, exist_ok=True) - assert ( - qnn_config.profile_level == 3 - ), "Please turn profile_level to 3 for the purpose of this tutorial." - - # lower to QNN build_executorch_binary( model=model, qnn_config=qnn_config, @@ -43,32 +51,43 @@ def main(args) -> None: quant_dtype=QuantDtype.use_8a8w, ) - # generate optrace and QHAS - adb = SimpleADB( - qnn_config=qnn_config, + artifacts = estimate_htp_profile_result( + artifact_dir=args.artifact, + soc_id=get_soc_to_chipset_map()[args.soc_model], pte_path=f"{args.artifact}/{pte_filename}.pte", - workspace=f"/data/local/tmp/executorch/{pte_filename}", - ) - binaries_trace = generate_optrace( - args.artifact, - get_soc_to_chipset_map()[args.soc_model], - adb, - f"{args.artifact}/{pte_filename}.pte", - example_inputs, ) if args.ip and args.port != -1: with Client((args.ip, args.port)) as conn: - conn.send(json.dumps({"binaries_trace": binaries_trace})) - else: - # Visualize the model and reports - for binary, (optrace, qhas) in binaries_trace.items(): - file_extension = os.path.splitext(binary)[-1] - if file_extension == ".bin": - qairt_visualizer.view(reports=[optrace, qhas]) - elif file_extension == ".dlc": - # We only show graph for dlc binary - qairt_visualizer.view(binary, reports=[optrace, qhas]) + conn.send( + json.dumps( + { + "artifacts": [ + { + "binary_path": a.binary_path, + "mode": a.mode, + "prepare_mode": a.prepare_mode, + "chrometrace_json": a.chrometrace_json, + "qhas_json": a.qhas_json, + "qhas_html": a.qhas_html, + } + for a in artifacts + ], + } + ) + ) + return + + try: + import qairt_visualizer + except ImportError: + for a in artifacts: + print(f"QHAS HTML: {a.qhas_html}") + return + + for a in artifacts: + qairt_visualizer.view(a.binary_path, reports=a.visualizer_reports()) + print(f"QHAS HTML: {a.qhas_html}") if __name__ == "__main__":