Skip to content

Commit 1094ea2

Browse files
quic-boyucclaude
andcommitted
observatory: promote observatory & fx_viewer to shared devtools
Move the Observatory debugging framework and fx_viewer graph visualization from backends/qualcomm/ to devtools/ so all ExecuTorch backends can use them. Backend-specific patches (QNN ptq_calibrate, XNNPACK quantize, QNN dataset capture) are extracted into their respective backend directories with a register_backend_patches() hook mechanism. Each backend maintains its own CLI runner (--accuracy opt-in flag): - python -m executorch.devtools.observatory (generic) - python -m executorch.backends.qualcomm.debugger.observatory (QNN) - python -m executorch.backends.xnnpack.debugger.observatory (XNNPACK) Includes RFC document at devtools/observatory/RFC_OBSERVATORY_SHARED_FRAMEWORK.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 22e1c72 commit 1094ea2

85 files changed

Lines changed: 1245 additions & 492 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backends/qualcomm/debugger/observatory/Questions.md

Lines changed: 0 additions & 18 deletions
This file was deleted.

backends/qualcomm/debugger/observatory/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,3 @@
33
#
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
6-
7-
from .observatory import Observatory
8-
9-
__all__ = ["Observatory"]

backends/qualcomm/debugger/observatory/cli.py

Lines changed: 43 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -4,277 +4,75 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7-
"""Observatory CLI — run mode and visualize mode.
7+
"""Qualcomm Observatory CLI — QNN-specific lens configuration.
88
9-
Run mode (default):
10-
python -m backends.qualcomm.debugger.observatory.cli \\
11-
[--no-accuracy] [--json-only] [--report-title TITLE] \\
12-
[--report-dir DIR] [--report-html PATH] [--report-json PATH] \\
13-
SCRIPT [SCRIPT_ARGS...]
9+
Graph collection only (default):
10+
python -m executorch.backends.qualcomm.debugger.observatory SCRIPT [ARGS...]
1411
15-
Visualize mode (JSON -> HTML, no script execution):
16-
python -m backends.qualcomm.debugger.observatory.cli visualize \\
17-
--input report.json --output report.html [--title "My Report"]
12+
With accuracy debugging:
13+
python -m executorch.backends.qualcomm.debugger.observatory --accuracy SCRIPT [ARGS...]
1814
19-
See backends/qualcomm/debugger/observatory/USAGE.md for full workflow docs.
15+
Visualize mode (JSON -> HTML):
16+
python -m executorch.backends.qualcomm.debugger.observatory visualize \\
17+
--input report.json --output report.html [--title "My Report"]
2018
"""
2119

2220
from __future__ import annotations
2321

24-
import logging
25-
import os
26-
import runpy
2722
import sys
2823

29-
logging.basicConfig(level=logging.INFO, format="%(message)s")
30-
31-
32-
def _usage_run() -> str:
33-
return (
34-
"Usage: python -m backends.qualcomm.debugger.observatory.cli "
35-
"[--no-accuracy] [--json-only] [--no-report] "
36-
"[--report-title TITLE] [--report-dir DIR] "
37-
"[--report-html PATH] [--report-json PATH] "
38-
"SCRIPT [SCRIPT_ARGS...]"
39-
)
40-
41-
42-
def _usage_visualize() -> str:
43-
return (
44-
"Usage: python -m backends.qualcomm.debugger.observatory.cli visualize "
45-
"--input report.json --output report.html [--title TITLE]"
46-
)
47-
48-
49-
def _read_value(argv: list[str], index: int, flag_name: str) -> tuple[str, int]:
50-
"""Read next token as value for a flag and return (value, next_index)."""
51-
if index + 1 >= len(argv):
52-
print(f"Missing value for {flag_name}")
53-
print(_usage_run())
54-
sys.exit(1)
55-
return argv[index + 1], index + 1
5624

57-
58-
def _parse_visualize_args() -> tuple[str, str, str]:
59-
"""Parse argv for: cli visualize --input X --output Y [--title T]"""
60-
argv = sys.argv[2:] # skip 'visualize' token
61-
input_path = None
62-
output_path = None
63-
title = "Observatory Report"
64-
65-
i = 0
66-
while i < len(argv):
67-
arg = argv[i]
68-
if arg in ("--input", "-i"):
69-
input_path, i = _read_value(argv, i, arg)
70-
elif arg.startswith("--input="):
71-
input_path = arg.split("=", 1)[1]
72-
elif arg in ("--output", "-o"):
73-
output_path, i = _read_value(argv, i, arg)
74-
elif arg.startswith("--output="):
75-
output_path = arg.split("=", 1)[1]
76-
elif arg == "--title":
77-
title, i = _read_value(argv, i, arg)
78-
elif arg.startswith("--title="):
79-
title = arg.split("=", 1)[1]
80-
elif arg in ("-h", "--help"):
81-
print(_usage_visualize())
82-
sys.exit(0)
83-
else:
84-
print(f"Unknown flag for visualize: {arg}")
85-
print(_usage_visualize())
86-
sys.exit(1)
87-
i += 1
88-
89-
if not input_path or not output_path:
90-
print("visualize requires --input and --output")
91-
print(_usage_visualize())
92-
sys.exit(1)
93-
94-
return input_path, output_path, title
95-
96-
97-
def _run_visualize() -> None:
98-
input_path, output_path, title = _parse_visualize_args()
99-
100-
if not os.path.isfile(input_path):
101-
logging.error("[Observatory CLI] Input JSON not found: %s", input_path)
102-
sys.exit(1)
103-
104-
from .observatory import Observatory
105-
106-
Observatory.clear()
107-
Observatory.generate_html_from_json(input_path, output_path, title=title)
108-
logging.info(
109-
"[Observatory CLI] visualize: html=%s from json=%s", output_path, input_path
25+
def main():
26+
from executorch.devtools.observatory.cli import (
27+
parse_observatory_args,
28+
run_observatory,
29+
run_visualize,
11030
)
11131

112-
113-
def _parse_observatory_args():
114-
"""Extract observatory flags from argv before SCRIPT, then pass all remaining args through."""
115-
116-
obs_flags = {
117-
"--no-accuracy": False,
118-
"--no-report": False,
119-
"--json-only": False,
120-
"--report-title": None,
121-
"--report-dir": None,
122-
"--report-html": None,
123-
"--report-json": None,
124-
}
125-
script_path = None
126-
script_argv = []
127-
argv = list(sys.argv[1:])
128-
129-
i = 0
130-
while i < len(argv):
131-
arg = argv[i]
132-
if script_path is not None:
133-
script_argv.append(arg)
134-
i += 1
135-
continue
136-
137-
if arg == "--":
138-
if i + 1 >= len(argv):
139-
print(_usage_run())
140-
sys.exit(1)
141-
script_path = argv[i + 1]
142-
script_argv.extend(argv[i + 2 :])
143-
break
144-
if arg == "--no-accuracy":
145-
obs_flags["--no-accuracy"] = True
146-
elif arg == "--no-report":
147-
obs_flags["--no-report"] = True
148-
elif arg == "--json-only":
149-
obs_flags["--json-only"] = True
150-
elif arg in ("--report-title", "--obs-report-title"):
151-
value, i = _read_value(argv, i, arg)
152-
obs_flags["--report-title"] = value
153-
elif arg.startswith("--report-title=") or arg.startswith("--obs-report-title="):
154-
obs_flags["--report-title"] = arg.split("=", 1)[1]
155-
elif arg in ("--report-dir", "--obs-report-dir"):
156-
value, i = _read_value(argv, i, arg)
157-
obs_flags["--report-dir"] = value
158-
elif arg.startswith("--report-dir=") or arg.startswith("--obs-report-dir="):
159-
obs_flags["--report-dir"] = arg.split("=", 1)[1]
160-
elif arg in ("--report-html", "--obs-report-html"):
161-
value, i = _read_value(argv, i, arg)
162-
obs_flags["--report-html"] = value
163-
elif arg.startswith("--report-html=") or arg.startswith("--obs-report-html="):
164-
obs_flags["--report-html"] = arg.split("=", 1)[1]
165-
elif arg in ("--report-json", "--obs-report-json"):
166-
value, i = _read_value(argv, i, arg)
167-
obs_flags["--report-json"] = value
168-
elif arg.startswith("--report-json=") or arg.startswith("--obs-report-json="):
169-
obs_flags["--report-json"] = arg.split("=", 1)[1]
170-
elif arg.startswith("-"):
171-
print(f"Unknown observatory flag before SCRIPT: {arg}")
172-
print(_usage_run())
173-
sys.exit(1)
174-
else:
175-
script_path = arg
176-
i += 1
177-
178-
if script_path is None:
179-
print(_usage_run())
180-
sys.exit(1)
181-
182-
return obs_flags, script_path, script_argv
183-
184-
185-
def _infer_report_dir(script_argv: list[str]) -> str:
186-
"""Infer output directory from target script args (artifact/output_dir), else cwd."""
187-
keys = ("-a", "--artifact", "-o", "--output_dir")
188-
for idx, arg in enumerate(script_argv):
189-
if arg in keys and idx + 1 < len(script_argv):
190-
return script_argv[idx + 1]
191-
if arg.startswith("--artifact="):
192-
return arg.split("=", 1)[1]
193-
if arg.startswith("--output_dir="):
194-
return arg.split("=", 1)[1]
195-
return "."
196-
197-
198-
def _resolve_report_paths(obs_flags: dict[str, object], script_argv: list[str]) -> tuple[str, str]:
199-
report_dir = obs_flags["--report-dir"] or _infer_report_dir(script_argv)
200-
report_html = obs_flags["--report-html"]
201-
report_json = obs_flags["--report-json"]
202-
203-
if report_html is None:
204-
report_html = os.path.join(str(report_dir), "observatory_report.html")
205-
if report_json is None:
206-
if report_html.endswith(".html"):
207-
report_json = report_html[:-5] + ".json"
208-
else:
209-
report_json = os.path.join(str(report_dir), "observatory_report.json")
210-
211-
return str(report_html), str(report_json)
212-
213-
214-
def main():
21532
if len(sys.argv) > 1 and sys.argv[1] == "visualize":
216-
_run_visualize()
33+
run_visualize()
21734
return
21835

21936
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
220-
print(_usage_run())
221-
print()
222-
print(_usage_visualize())
37+
print(
38+
"Usage: python -m executorch.backends.qualcomm.debugger.observatory "
39+
"[--accuracy] [--json-only] [--no-report] "
40+
"[--report-title TITLE] [--report-dir DIR] "
41+
"[--report-html PATH] [--report-json PATH] "
42+
"SCRIPT [SCRIPT_ARGS...]"
43+
)
22344
sys.exit(0)
22445

225-
obs_flags, script_path, script_argv = _parse_observatory_args()
46+
usage = (
47+
"Usage: python -m executorch.backends.qualcomm.debugger.observatory "
48+
"[--accuracy] [--json-only] [--no-report] "
49+
"[--report-title TITLE] [--report-dir DIR] "
50+
"[--report-html PATH] [--report-json PATH] "
51+
"SCRIPT [SCRIPT_ARGS...]"
52+
)
53+
obs_flags, script_path, script_argv = parse_observatory_args(usage_str=usage)
54+
55+
from executorch.devtools.observatory.observatory import Observatory
56+
from executorch.devtools.observatory.lenses.pipeline_graph_collector import (
57+
PipelineGraphCollectorLens,
58+
)
22659

227-
from .observatory import Observatory
228-
from .lenses.pipeline_graph_collector import PipelineGraphCollectorLens
60+
from .lenses.qnn_patches import install_qnn_patches
22961

23062
Observatory.clear()
231-
Observatory.register_lens(PipelineGraphCollectorLens)
232-
233-
if not obs_flags["--no-accuracy"]:
234-
from .lenses.accuracy import AccuracyLens
23563

236-
Observatory.register_lens(AccuracyLens)
64+
PipelineGraphCollectorLens.register_backend_patches(install_qnn_patches)
65+
Observatory.register_lens(PipelineGraphCollectorLens)
23766

238-
sys.argv = [script_path] + script_argv
67+
if obs_flags["--accuracy"]:
68+
from executorch.devtools.observatory.lenses.accuracy import AccuracyLens
23969

240-
config: dict = {}
241-
report_html, report_json = _resolve_report_paths(obs_flags, script_argv)
70+
from .lenses.qnn_dataset_patches import install_qnn_dataset_patches
24271

243-
title = obs_flags["--report-title"] or f"Observatory: {os.path.basename(script_path)}"
72+
AccuracyLens.register_dataset_patches(install_qnn_dataset_patches)
73+
Observatory.register_lens(AccuracyLens)
24474

245-
try:
246-
with Observatory.enable_context(config=config):
247-
runpy.run_path(script_path, run_name="__main__")
248-
except SystemExit:
249-
pass
250-
except Exception as exc:
251-
logging.error("[Observatory CLI] Script raised: %s", exc)
252-
finally:
253-
if not obs_flags["--no-report"]:
254-
os.makedirs(os.path.dirname(report_html) or ".", exist_ok=True)
255-
os.makedirs(os.path.dirname(report_json) or ".", exist_ok=True)
256-
Observatory.export_json(report_json)
257-
collected = Observatory.list_collected()
258-
if not obs_flags["--json-only"]:
259-
Observatory.export_html_report(report_html, title=title, config=config)
260-
if collected:
261-
if obs_flags["--json-only"]:
262-
logging.info(
263-
"[Observatory CLI] Reports: json=%s (%d records: %s)",
264-
report_json,
265-
len(collected),
266-
", ".join(collected),
267-
)
268-
else:
269-
logging.info(
270-
"[Observatory CLI] Reports: html=%s json=%s (%d records: %s)",
271-
report_html,
272-
report_json,
273-
len(collected),
274-
", ".join(collected),
275-
)
276-
else:
277-
logging.warning("[Observatory CLI] No records collected")
75+
run_observatory(obs_flags, script_path, script_argv, Observatory)
27876

27977

28078
if __name__ == "__main__":

backends/qualcomm/debugger/observatory/examples/demo_etrecord_auto_collect.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828

2929
import torch
3030

31-
from executorch.backends.qualcomm.debugger.observatory import Observatory
32-
from executorch.backends.qualcomm.debugger.observatory.lenses.pipeline_graph_collector import (
31+
from executorch.devtools.observatory import Observatory
32+
from executorch.devtools.observatory.lenses.pipeline_graph_collector import (
3333
PipelineGraphCollectorLens,
3434
)
3535
from executorch.devtools.etrecord import ETRecord

backends/qualcomm/debugger/observatory/examples/demo_graphview_accuracy_compare.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232

3333
import torch
3434

35-
from executorch.backends.qualcomm.debugger.observatory import Observatory
36-
from executorch.backends.qualcomm.debugger.observatory.interfaces import (
35+
from executorch.devtools.observatory import Observatory
36+
from executorch.devtools.observatory.interfaces import (
3737
AnalysisResult,
3838
Frontend,
3939
Lens,
@@ -44,7 +44,7 @@
4444
TableRecordSpec,
4545
ViewList,
4646
)
47-
from executorch.backends.qualcomm.utils.fx_viewer import (
47+
from executorch.devtools.fx_viewer import (
4848
GraphExtensionNodePayload,
4949
GraphExtensionPayload,
5050
)

backends/qualcomm/debugger/observatory/examples/generate_ui_test_harness.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
import json
1515
import os
1616

17-
from executorch.backends.qualcomm.debugger.observatory.html_template import get_html_template
18-
from executorch.backends.qualcomm.utils.fx_viewer.exporter import FXGraphExporter
17+
from executorch.devtools.observatory.html_template import get_html_template
18+
from executorch.devtools.fx_viewer.exporter import FXGraphExporter
1919

2020

2121
def _sample_graph_assets() -> dict:

0 commit comments

Comments
 (0)