RFC: fx_viewer — A Lightweight FX Graph Viewer for Debugging
Status: Draft for review
Related: Observatory RFC (RFC-B), Draft PR #19288
Demo reports (Observatory): xnnpack/mobilebert · qualcomm/mobilenet_v2 · cross-backend compare
Demo reports (fx_viewer standalone): MV2 XNNPACK ETRecord compare (2-pane) · MV2 QNN ETRecord compare (3-pane) · 3-graph sync demo
Walkthrough video: https://youtu.be/NQuj-2LvhAc
1. Summary
We propose adding executorch/devtools/fx_viewer/ — a small, self-contained FX graph viewer that produces standalone HTML files with no server required.
Its primary job is one that the existing visualization tools do not cover well: debugging a model across multiple compile stages at the same time — viewing ATen IR, quantized graph, Edge IR, and device graph side by side, with automatic cross-graph node synchronization and programmatic analysis overlays.
fx_viewer is complementary to Model Explorer, not a replacement. Each tool has a clear best-fit scenario, described in §3.
A working proof-of-concept is available in draft PR #19288. All demo HTML files linked above were generated from that implementation.
▶ Feature walkthrough video (start at 0:21 — the CLI wrapper shown in the first 20s is part of Observatory, not fx_viewer):

2. Motivation: What the Existing Tools Cannot Do
ExecuTorch already ships two visualization paths:
devtools/visualization/ — wraps Model Explorer (ME) for general model browsing.
model-explorer raw API — the upstream Python API from google-ai-edge.
Both are good at browsing a single model's structure. But when debugging a compile pipeline, developers run into four concrete gaps.
Gap 1 — You cannot view more than 2 graphs at once
What you need: When debugging quantization, you typically need to look at 4 stages together — float model, calibrated model, quantized model, and edge/device graph. Seeing only 2 at a time means you lose context.
What ME provides: A fixed 2-pane split view. You can load multiple models, but the compare UI only shows 2 at a time.
What fx_viewer provides: An N-way grid. Any number of graphs in one view, in one HTML file.
from executorch.devtools.fx_viewer import FXGraphCompareExporter, FXGraphExporter
from collections import OrderedDict
FXGraphCompareExporter(
OrderedDict([
("Float", FXGraphExporter(float_gm)),
("Quantized", FXGraphExporter(quantized_gm)),
("Edge IR", FXGraphExporter(edge_gm)),
("Device", FXGraphExporter(device_gm)),
]),
sync_mode="auto",
title="Quantization Pipeline",
).export_html("pipeline_compare.html")
Live demo: MV2 XNNPACK ETRecord compare (2-pane) — click any node in the Aten pane and watch the Edge dialect pane sync automatically.
Gap 2 — Cross-graph node sync requires manual work
What you need: Click a node in the quantized graph → the corresponding node in the float graph highlights automatically. This is essential for tracing where accuracy degrades.
What ME provides:
- Mode 1: "Match node id" — node ids can change after quantization or lowering passes, and the matching does not handle many-to-many transformations (e.g. one ATen op decomposing into multiple quantized ops).
- Mode 2: Generate and upload a mapping JSON file — you need to produce this file separately and upload it through the GUI for cross-graph sync to work.
What fx_viewer provides: Automatic many-to-many sync — no mapping file needed.
- PyTorch's
Interpreter base class (used by ExportPass and all ExecuTorch PassBase subclasses) records node provenance in fx_node.meta["from_node"] as each pass executes. This is a linked list of NodeSource entries tracing back to the original node.
FXGraphExporter walks this chain to extract from_node_root — the name of the original ancestor node before any passes ran.
- The JS compare engine tries sync in order:
from_node_root → debug_handle set-intersection → node id. This handles 1-to-many (decomposition), many-to-1 (fusion), and many-to-many cases automatically.
Click "quantized_conv2d_1" in Quantized graph
→ auto-highlights "conv2d_1" in Float graph
→ auto-highlights "conv2d_1" in Edge IR graph
→ auto-highlights "conv2d_1_lowered" in Device graph
Live demo: 3-graph sync demo — shows reference vs. decomposed (1→many) vs. fused (many→1) sync behavior.
Future work: We plan to add support for QNN backend graph format (the device-side graph produced after QNN compilation) as an additional pane. This would enable end-to-end node sync from ATen IR all the way through to the on-device execution graph, covering the full compile-to-deploy pipeline in a single view.
Gap 3 — You cannot overlay your own analysis results on the graph
What you need: After computing per-layer accuracy (PSNR, MSE, cosine similarity), you want to see which nodes are "red" (bad) directly on the graph — not in a separate table.
What ME provides:
- The raw ME API has a
node_data_builder that supports per-node numeric overlays.
- Limitation 1: ME's ETRecord adapter (including Arm's
etrecord-adapter-model-explorer) extracts only the edge dialect graph — a single flattened graph of op nodes. Placeholder, output, and get_attr nodes are collapsed into synthetic "Graph Inputs" / "Graph Outputs" boundary nodes and are not individually addressable for overlay. The overlay API therefore covers the same set of nodes that the adapter exposes.
- Limitation 2: The workflow requires: (1) build a
ModelNodeData object, (2) save it to a JSON file, (3) start the ME server, (4) upload the JSON through the GUI.
- Limitation 3: The
devtools/visualization/ wrapper (visualize_with_clusters) only exposes namespace grouping — it does not expose the node_data_builder API at all.
- Limitation 4: Overlay data is scoped to a single graph. ME has no mechanism to attach the same overlay across multiple graphs simultaneously.
What fx_viewer provides: Programmatic overlay on any node type, baked into the HTML at export time.
from executorch.devtools.fx_viewer import GraphExtension, NumericColorRule
ext = GraphExtension(id="accuracy", name="Per-Layer Accuracy")
for node_id, metrics in per_layer_results.items():
ext.add_node_data(node_id, {
"severity_score": metrics["severity"],
"max_abs_err": metrics["max_abs"],
"cosine_sim": metrics["cosine"],
})
# Color nodes red-to-green by severity
ext.set_color_rule(NumericColorRule(attribute="severity_score", cmap="reds"))
# Show key metrics as labels on each node
ext.set_label_formatter(lambda d: [
f"sev={d['severity_score']:.2e}",
f"max={d['max_abs_err']:.2e}",
])
# Show full details in the tooltip panel
ext.set_tooltip_formatter(lambda d: [
f"cosine_sim: {d['cosine_sim']:.4f}",
f"max_abs_err: {d['max_abs_err']:.6e}",
])
exporter.add_extension(ext)
exporter.export_html("accuracy_overlay.html")
Note: The overlay API is not limited to compile-time data. Runtime artifacts such as ETDump profiling results (latency, event counts) can be parsed and attached as a GraphExtension in exactly the same way — see §3 for a comparison with Arm's ETDump overlay approach.
Multiple overlay layers can be stacked and toggled independently:
exporter.add_extension(partition_ext) # which backend owns each op
exporter.add_extension(accuracy_ext) # per-layer accuracy
exporter.add_extension(latency_ext) # profiling data
exporter.export_html("multi_layer.html")
# → user can toggle each layer on/off in the HTML
Gap 4 — The output requires a server to view or share
What you need: Attach a graph view to a GitHub issue, send it in an email, or upload it as a CI artifact. The recipient should be able to open it without installing anything.
What ME provides: Either a live server session (blocking subprocess), or a JSON file that still requires a running ME server to open. There is no standalone output.
What fx_viewer provides: A single .html file. Open in any browser, no server, no installation.
exporter.export_html("debug_report.html")
# → one file, works offline, shareable anywhere
This also makes fx_viewer CI-friendly: the HTML can be uploaded as a build artifact and opened directly from the CI dashboard.
The viewer is also embeddable — export_js(container_id) returns a self-contained JS snippet that mounts the full interactive viewer (canvas, minimap, search, overlay toggles) into any DOM element. This is how Observatory embeds fx_viewer graphs into its unified HTML report.
Gap 5 — The viewer cannot be modified by ExecuTorch contributors
What you need: Add a new overlay type, change the layout algorithm, or support a new backend-specific node attribute. This should be a normal ExecuTorch PR.
What ME provides: ME is maintained by an external team (google-ai-edge). ExecuTorch cannot land viewer changes directly. Any feature request requires an upstream PR and waits for their release cycle.
What fx_viewer provides: Lives in executorch/devtools/fx_viewer/. The frontend is ~4,500 lines of plain JavaScript on HTML5 Canvas — no framework, no build step. Any backend team can read and modify it directly.
|
Model Explorer |
fx_viewer |
| Frontend stack |
Angular + three.js + d3 (~50k LoC) |
Plain JS on Canvas (~4.5k LoC) |
| Ownership |
External (google-ai-edge) |
In-tree (devtools/fx_viewer/) |
| To add a feature |
Open upstream PR, wait for release |
Normal ExecuTorch PR |
| Layout computation |
Browser-side (slow on large graphs) |
Python pre-computed, baked into HTML |
3. Positioning: Complementary, Not a Replacement
fx_viewer and Model Explorer serve different jobs. The table below shows when to use each.
| Scenario |
Use Model Explorer |
Use fx_viewer |
| "What does my model look like?" |
✅ Rich module hierarchy, collapsible layers |
— |
| "Which ops belong to which nn.Module?" |
✅ Namespace grouping |
— |
| "What changed between ATen and Edge IR?" |
— |
✅ N-way compare, auto sync |
| "Which layer lost accuracy after quantization?" |
⚠️ node_data_builder (single graph, GUI upload, see §2 Gap 3) |
✅ Programmatic overlay, multi-graph, baked into HTML |
| "I need to attach this to a GitHub issue" |
— |
✅ Standalone HTML |
| "CI needs to check for graph regressions" |
— |
✅ HTML artifact, no server |
| "I need to extend the viewer for my backend" |
— |
✅ In-tree, small codebase |
Why not extend Model Explorer instead?
We considered pushing these features upstream into ME. The blockers are architectural:
- N-way compare requires redesigning ME's split-pane UI (currently hardcoded to 2 panes).
- Automatic node sync requires ExecuTorch-specific knowledge (
from_node meta, debug_handle) that does not belong in a general-purpose viewer.
- Standalone HTML requires removing ME's server dependency, which is a fundamental architectural change.
- In-tree extensibility is the point — backend teams need to iterate quickly without waiting for an external team's release cycle.
Relationship to Arm's executorch-extension-model-explorer
Arm recently released executorch-extension-model-explorer. Based on the source code, it provides:
- ETRecord adapter — extracts the edge dialect graph only (via
gen_graphs_from_etrecord → EDGE_DIALECT_GRAPH_KEY), renders it in ME with delegate namespace grouping and node metadata.
- ETDump data provider — overlays runtime latency metrics (avg/p50/min/max ms, event count) onto the edge dialect graph by matching
debug_handle values from the ETDump.
- PTE adapter — visualizes
.pte program files.
The same capabilities are achievable with fx_viewer:
# ETRecord → multi-pane compare (Aten + edge dialect + backend overlay)
export_etrecord_to_html("mv2.etrecord", "mv2_compare.html")
# ETDump latency overlay — parse ETDump, attach as GraphExtension
from executorch.devtools import Inspector
inspector = Inspector(etdump_path="out.etdump", etrecord="mv2.etrecord")
latency_ext = GraphExtension(id="latency", name="Runtime Latency")
for event in inspector.event_blocks[0].events:
if event.debug_handles:
latency_ext.add_node_data(
event.name, {"avg_ms": event.perf_data.avg}
)
latency_ext.set_color_rule(NumericColorRule(attribute="avg_ms", cmap="reds"))
Key differences:
- Arm's extension shows one graph (edge dialect only);
fx_viewer shows N graphs (Aten + intermediate + edge) in one view with automatic cross-pane sync.
- Arm's ETDump overlay is limited to the ME node data API (op nodes in the edge graph);
fx_viewer's GraphExtension overlay works on any node in any graph, and is not limited to compile-time data — runtime artifacts like ETDump can be overlaid in the same way.
- Arm's extension requires the ME server;
fx_viewer produces standalone HTML.
fx_viewer can produce the same visualizations as Arm's extension (single edge-dialect graph with latency overlay) and additionally supports multi-graph compare, automatic node sync, and standalone HTML. The one capability Arm's extension provides that fx_viewer does not is integration with Model Explorer's hierarchical layer browsing UI.
4. Feature Comparison Matrix
| Feature |
devtools/visualization/ (ME wrapper) |
Raw ME API |
fx_viewer |
| Single graph view |
✅ |
✅ |
✅ |
| GraphModule as direct input |
✅ |
✅ |
✅ |
| ETRecord as direct input |
❌ |
❌ |
✅ (one-liner) |
| Per-node custom data overlay |
❌ (not exposed) |
✅ (edge graph, op nodes) |
✅ (any graph, any node) |
| Programmatic color mapping |
❌ |
✅ |
✅ |
| Minimap for large graph navigation |
❌ |
❌ |
✅ |
| Multiple overlay layers (toggle) |
❌ |
✅ |
✅ |
| Runtime artifact overlay (e.g. ETDump) |
❌ |
✅ (edge graph only) |
✅ (any graph) |
| Multi-graph compare |
❌ |
✅ (2 graphs) |
✅ (N graphs) |
| Cross-graph node sync |
❌ |
✅ (manual mapping file) |
✅ (automatic) |
| Many-to-many node mapping |
❌ |
✅ (manual mapping file) |
✅ (automatic) |
| Backend assignment overlay |
❌ |
❌ |
✅ (from ETRecord) |
| Embeddable JS API for custom web pages |
❌ |
❌ |
✅ |
| Standalone HTML output |
❌ |
❌ |
✅ |
| No server required |
❌ |
❌ |
✅ |
| CI-friendly artifact |
❌ |
❌ |
✅ |
| Module hierarchy / collapsible layers |
❌ |
✅ |
❌ |
| In-tree, modifiable by ExecuTorch |
❌ |
❌ |
✅ |
| External Python dependency |
model-explorer |
model-explorer |
fast-sugiyama (see §5) |
| Python pre-computed layout |
❌ |
❌ |
✅ |
5. API Reference
5.1 Single-graph export
from executorch.devtools.fx_viewer import (
FXGraphExporter,
GraphExtension,
NumericColorRule,
CategoricalColorRule,
)
# Build exporter from any GraphModule in the compile pipeline
exporter = FXGraphExporter(graph_module)
# Optional: customize the base layer
exporter.set_base_label_formatter(lambda node: node.info.get("target", ""))
exporter.set_base_color_rule(CategoricalColorRule(attribute="op"))
# Add one or more overlay extensions
ext = GraphExtension(id="my_analysis", name="My Analysis")
ext.add_node_data("node_name", {"metric": 0.95, "status": "pass"})
ext.set_color_rule(NumericColorRule(attribute="metric", cmap="red_green"))
ext.set_label_formatter(lambda d: [f"metric={d['metric']:.2f}"])
ext.set_sync_key("debug_handle") # enables cross-graph sync on this field
exporter.add_extension(ext)
# Export
exporter.export_html("graph.html") # standalone HTML
exporter.export_json("graph.json") # raw JSON payload
js = exporter.export_js("container-id") # embeddable JS snippet
5.2 Multi-graph compare: FXGraphCompareExporter
The new FXGraphCompareExporter class renders N FXGraphExporter instances into one standalone HTML page. Column order follows insertion order.
from executorch.devtools.fx_viewer import FXGraphCompareExporter, FXGraphExporter
from collections import OrderedDict
viewers = OrderedDict([
("ATen IR", FXGraphExporter(aten_gm)),
("Quantized", FXGraphExporter(quantized_gm)),
("Edge IR", FXGraphExporter(edge_gm)),
])
FXGraphCompareExporter(
viewers,
title="Quantization Pipeline Compare",
sync_mode="auto", # from_node_root → debug_handle → id
active_extensions=["accuracy"],
color_by="accuracy",
).export_html("compare.html")
Sync modes:
"auto" (default) — tries from_node_root first, then debug_handle set-intersection, falls back to node id.
"id" — node id match only.
"layer" — match by a specific extension field (requires sync_layer + sync_field).
"none" — no propagation.
5.3 ETRecord → compare HTML (new)
The new etrecord_adapter module turns an .etrecord bundle into a compare view with a single function call. This is the primary entry point for users who already use the ETRecord workflow.
from executorch.devtools.fx_viewer import export_etrecord_to_html
# One-liner: path or in-memory ETRecord → standalone HTML
export_etrecord_to_html("mv2.etrecord", "mv2_compare.html")
Panes are added automatically based on what the bundle contains:
- Aten — from
rec.exported_program. If from_node / debug_handle are missing (common for bundles produced by torch.export.export), the adapter re-runs run_decompositions({}) and DebugHandleGeneratorPass automatically.
- Extra stored programs — one pane per entry in
rec.graph_map. For example, the QNN pipeline writes graph_map["edge_after_transform/forward"] when to_edge_transform_and_lower_to_qnn is used — this becomes a third pane automatically.
- Edge dialect — from
rec.edge_dialect_program, with a backend overlay derived from _debug_handle_map × _delegate_map when the bundle names a backend.
Live demos:
For callers who want to attach extra extensions or override settings before writing:
from executorch.devtools.fx_viewer import build_compare_from_etrecord
# Returns FXGraphCompareExporter — attach more extensions before export
exporter = build_compare_from_etrecord("mv2.etrecord", title="My Compare")
exporter.export_html("mv2_compare.html")
5.4 Inspector integration (new)
When an Inspector is already available with an ETRecord attached, a single method produces the compare HTML:
from executorch.devtools import Inspector
inspector = Inspector(etdump_path="out.etdump", etrecord="mv2.etrecord")
# One-liner: same pane layout as export_etrecord_to_html
inspector.export_fx_viewer_html("mv2_compare.html")
This keeps the ETRecord-based workflow intact — users who already use Inspector get fx_viewer output without changing their setup.
5.5 Dependencies
fx_viewer requires the fast-sugiyama package for graph layout computation:
pip install 'fast-sugiyama[all]' # requires Python >= 3.11
Note: fast-sugiyama has known layout bugs in certain graph topologies. We plan to replace it with a pure-Python layout implementation that eliminates this external dependency entirely. Until then, fast-sugiyama is a required install-time dependency.
5.6 Stable API surface
The following are proposed as stable, change-controlled contracts:
| API |
Description |
FXGraphExporter |
Single-graph export |
FXGraphCompareExporter |
N-graph compare export |
export_etrecord_to_html() |
ETRecord → compare HTML one-liner |
build_compare_from_etrecord() |
ETRecord → FXGraphCompareExporter (for customization) |
Inspector.export_fx_viewer_html() |
Inspector hook for ETRecord-based users |
GraphExtension |
Overlay layer definition |
ColorRule subclasses |
Color mapping rules |
6. Future Work
The following items are planned but out of scope for this RFC:
- QNN backend graph format support — Add a pane for the device-side graph produced after QNN compilation, enabling end-to-end node sync from ATen IR through to the on-device execution graph.
- Pure-Python layout engine — Replace
fast-sugiyama with a dependency-free implementation to eliminate the external package requirement and fix known layout bugs.
- Runtime delegated accuracy — Per-layer accuracy comparison for delegated (on-device) execution, not just compile-time simulation.
7. Open Questions
Q1 — Should fx_viewer live in devtools/fx_viewer/ or somewhere else?
Current proposal: devtools/fx_viewer/ as a sibling to devtools/visualization/. The two serve different jobs and have different dependencies. Alternative: nest under devtools/visualization/ as a second backend. We prefer the sibling layout for clarity.
Q2 — Should fx_viewer provide an extension surface for additional graph formats?
Currently fx_viewer only accepts torch.fx.GraphModule and ETRecord bundles. Backend teams may want to visualize other graph representations — for example, the QNN backend graph (post-compilation device graph), nn.Module hierarchy, or TOSA IR. Should we define a formal GraphFormatAdapter protocol that third parties can implement to add new pane types, or keep the input surface narrow and add formats case-by-case?
Q3 — Should single/multi-GraphModule visualization be added to the Inspector API?
The current Inspector.export_fx_viewer_html() hook only works when an ETRecord is attached (it reads the full bundle). Users who have individual GraphModule objects — for example, from a custom pass pipeline — cannot use this path. Should Inspector gain a method like Inspector.export_fx_viewer_html_from_graphs({"Aten": gm1, "Edge": gm2}), or should users call FXGraphCompareExporter directly?
Q4 — Should the JSON payload format be exposed as a stable public API?
GraphPayload and GraphExtensionPayload are currently internal to fx_viewer. Exposing them as a stable schema would allow users to export JSON once and re-render HTML later without re-running the compiler, and would enable third-party tooling to consume the format. The cost is schema versioning and backward-compatibility maintenance. Should we expose the JSON format as a stable API, or keep it internal and only support HTML as the stable output?
Q5 — Should the JS embedding API (export_js / FXGraphViewer.create) be formalized as a stable contract?
Currently export_js(container_id) returns a JS snippet that mounts the full viewer into a DOM element. Observatory and other tools (e.g. custom dashboards, Jupyter/Colab notebooks) rely on this to embed interactive graphs. Should the JS mount interface (FXGraphViewer.create({ payload, mount })) and programmatic methods (e.g. selectNode, setColorBy, setLayers) be documented and versioned as a stable API surface, or kept internal with only the Python export_html / export_js as the stable boundary?
RFC:
fx_viewer— A Lightweight FX Graph Viewer for Debugging1. Summary
We propose adding
executorch/devtools/fx_viewer/— a small, self-contained FX graph viewer that produces standalone HTML files with no server required.Its primary job is one that the existing visualization tools do not cover well: debugging a model across multiple compile stages at the same time — viewing ATen IR, quantized graph, Edge IR, and device graph side by side, with automatic cross-graph node synchronization and programmatic analysis overlays.
fx_vieweris complementary to Model Explorer, not a replacement. Each tool has a clear best-fit scenario, described in §3.A working proof-of-concept is available in draft PR #19288. All demo HTML files linked above were generated from that implementation.
▶ Feature walkthrough video (start at 0:21 — the CLI wrapper shown in the first 20s is part of Observatory, not fx_viewer):
2. Motivation: What the Existing Tools Cannot Do
ExecuTorch already ships two visualization paths:
devtools/visualization/— wraps Model Explorer (ME) for general model browsing.model-explorerraw API — the upstream Python API fromgoogle-ai-edge.Both are good at browsing a single model's structure. But when debugging a compile pipeline, developers run into four concrete gaps.
Gap 1 — You cannot view more than 2 graphs at once
What you need: When debugging quantization, you typically need to look at 4 stages together — float model, calibrated model, quantized model, and edge/device graph. Seeing only 2 at a time means you lose context.
What ME provides: A fixed 2-pane split view. You can load multiple models, but the compare UI only shows 2 at a time.
What
fx_viewerprovides: An N-way grid. Any number of graphs in one view, in one HTML file.Live demo: MV2 XNNPACK ETRecord compare (2-pane) — click any node in the Aten pane and watch the Edge dialect pane sync automatically.
Gap 2 — Cross-graph node sync requires manual work
What you need: Click a node in the quantized graph → the corresponding node in the float graph highlights automatically. This is essential for tracing where accuracy degrades.
What ME provides:
What
fx_viewerprovides: Automatic many-to-many sync — no mapping file needed.Interpreterbase class (used byExportPassand all ExecuTorchPassBasesubclasses) records node provenance infx_node.meta["from_node"]as each pass executes. This is a linked list ofNodeSourceentries tracing back to the original node.FXGraphExporterwalks this chain to extractfrom_node_root— the name of the original ancestor node before any passes ran.from_node_root→debug_handleset-intersection → node id. This handles 1-to-many (decomposition), many-to-1 (fusion), and many-to-many cases automatically.Live demo: 3-graph sync demo — shows reference vs. decomposed (1→many) vs. fused (many→1) sync behavior.
Gap 3 — You cannot overlay your own analysis results on the graph
What you need: After computing per-layer accuracy (PSNR, MSE, cosine similarity), you want to see which nodes are "red" (bad) directly on the graph — not in a separate table.
What ME provides:
node_data_builderthat supports per-node numeric overlays.etrecord-adapter-model-explorer) extracts only the edge dialect graph — a single flattened graph of op nodes. Placeholder, output, and get_attr nodes are collapsed into synthetic "Graph Inputs" / "Graph Outputs" boundary nodes and are not individually addressable for overlay. The overlay API therefore covers the same set of nodes that the adapter exposes.ModelNodeDataobject, (2) save it to a JSON file, (3) start the ME server, (4) upload the JSON through the GUI.devtools/visualization/wrapper (visualize_with_clusters) only exposesnamespacegrouping — it does not expose thenode_data_builderAPI at all.What
fx_viewerprovides: Programmatic overlay on any node type, baked into the HTML at export time.Multiple overlay layers can be stacked and toggled independently:
Gap 4 — The output requires a server to view or share
What you need: Attach a graph view to a GitHub issue, send it in an email, or upload it as a CI artifact. The recipient should be able to open it without installing anything.
What ME provides: Either a live server session (blocking subprocess), or a JSON file that still requires a running ME server to open. There is no standalone output.
What
fx_viewerprovides: A single.htmlfile. Open in any browser, no server, no installation.This also makes
fx_viewerCI-friendly: the HTML can be uploaded as a build artifact and opened directly from the CI dashboard.The viewer is also embeddable —
export_js(container_id)returns a self-contained JS snippet that mounts the full interactive viewer (canvas, minimap, search, overlay toggles) into any DOM element. This is how Observatory embedsfx_viewergraphs into its unified HTML report.Gap 5 — The viewer cannot be modified by ExecuTorch contributors
What you need: Add a new overlay type, change the layout algorithm, or support a new backend-specific node attribute. This should be a normal ExecuTorch PR.
What ME provides: ME is maintained by an external team (
google-ai-edge). ExecuTorch cannot land viewer changes directly. Any feature request requires an upstream PR and waits for their release cycle.What
fx_viewerprovides: Lives inexecutorch/devtools/fx_viewer/. The frontend is ~4,500 lines of plain JavaScript on HTML5 Canvas — no framework, no build step. Any backend team can read and modify it directly.fx_viewergoogle-ai-edge)devtools/fx_viewer/)3. Positioning: Complementary, Not a Replacement
fx_viewerand Model Explorer serve different jobs. The table below shows when to use each.fx_viewerWhy not extend Model Explorer instead?
We considered pushing these features upstream into ME. The blockers are architectural:
from_nodemeta,debug_handle) that does not belong in a general-purpose viewer.Relationship to Arm's
executorch-extension-model-explorerArm recently released
executorch-extension-model-explorer. Based on the source code, it provides:gen_graphs_from_etrecord→EDGE_DIALECT_GRAPH_KEY), renders it in ME with delegate namespace grouping and node metadata.debug_handlevalues from the ETDump..pteprogram files.The same capabilities are achievable with
fx_viewer:Key differences:
fx_viewershows N graphs (Aten + intermediate + edge) in one view with automatic cross-pane sync.fx_viewer'sGraphExtensionoverlay works on any node in any graph, and is not limited to compile-time data — runtime artifacts like ETDump can be overlaid in the same way.fx_viewerproduces standalone HTML.fx_viewer can produce the same visualizations as Arm's extension (single edge-dialect graph with latency overlay) and additionally supports multi-graph compare, automatic node sync, and standalone HTML. The one capability Arm's extension provides that fx_viewer does not is integration with Model Explorer's hierarchical layer browsing UI.
4. Feature Comparison Matrix
devtools/visualization/(ME wrapper)fx_viewermodel-explorermodel-explorerfast-sugiyama(see §5)5. API Reference
5.1 Single-graph export
5.2 Multi-graph compare:
FXGraphCompareExporterThe new
FXGraphCompareExporterclass renders NFXGraphExporterinstances into one standalone HTML page. Column order follows insertion order.Sync modes:
"auto"(default) — triesfrom_node_rootfirst, thendebug_handleset-intersection, falls back to node id."id"— node id match only."layer"— match by a specific extension field (requiressync_layer+sync_field)."none"— no propagation.5.3 ETRecord → compare HTML (new)
The new
etrecord_adaptermodule turns an.etrecordbundle into a compare view with a single function call. This is the primary entry point for users who already use the ETRecord workflow.Panes are added automatically based on what the bundle contains:
rec.exported_program. Iffrom_node/debug_handleare missing (common for bundles produced bytorch.export.export), the adapter re-runsrun_decompositions({})andDebugHandleGeneratorPassautomatically.rec.graph_map. For example, the QNN pipeline writesgraph_map["edge_after_transform/forward"]whento_edge_transform_and_lower_to_qnnis used — this becomes a third pane automatically.rec.edge_dialect_program, with abackendoverlay derived from_debug_handle_map × _delegate_mapwhen the bundle names a backend.Live demos:
For callers who want to attach extra extensions or override settings before writing:
5.4 Inspector integration (new)
When an
Inspectoris already available with an ETRecord attached, a single method produces the compare HTML:This keeps the ETRecord-based workflow intact — users who already use
Inspectorgetfx_vieweroutput without changing their setup.5.5 Dependencies
fx_viewerrequires thefast-sugiyamapackage for graph layout computation:5.6 Stable API surface
The following are proposed as stable, change-controlled contracts:
FXGraphExporterFXGraphCompareExporterexport_etrecord_to_html()build_compare_from_etrecord()FXGraphCompareExporter(for customization)Inspector.export_fx_viewer_html()GraphExtensionColorRulesubclasses6. Future Work
The following items are planned but out of scope for this RFC:
fast-sugiyamawith a dependency-free implementation to eliminate the external package requirement and fix known layout bugs.7. Open Questions
Q1 — Should
fx_viewerlive indevtools/fx_viewer/or somewhere else?Current proposal:
devtools/fx_viewer/as a sibling todevtools/visualization/. The two serve different jobs and have different dependencies. Alternative: nest underdevtools/visualization/as a second backend. We prefer the sibling layout for clarity.Q2 — Should
fx_viewerprovide an extension surface for additional graph formats?Currently
fx_vieweronly acceptstorch.fx.GraphModuleand ETRecord bundles. Backend teams may want to visualize other graph representations — for example, the QNN backend graph (post-compilation device graph),nn.Modulehierarchy, or TOSA IR. Should we define a formalGraphFormatAdapterprotocol that third parties can implement to add new pane types, or keep the input surface narrow and add formats case-by-case?Q3 — Should single/multi-GraphModule visualization be added to the
InspectorAPI?The current
Inspector.export_fx_viewer_html()hook only works when an ETRecord is attached (it reads the full bundle). Users who have individualGraphModuleobjects — for example, from a custom pass pipeline — cannot use this path. ShouldInspectorgain a method likeInspector.export_fx_viewer_html_from_graphs({"Aten": gm1, "Edge": gm2}), or should users callFXGraphCompareExporterdirectly?Q4 — Should the JSON payload format be exposed as a stable public API?
GraphPayloadandGraphExtensionPayloadare currently internal tofx_viewer. Exposing them as a stable schema would allow users to export JSON once and re-render HTML later without re-running the compiler, and would enable third-party tooling to consume the format. The cost is schema versioning and backward-compatibility maintenance. Should we expose the JSON format as a stable API, or keep it internal and only support HTML as the stable output?Q5 — Should the JS embedding API (
export_js/FXGraphViewer.create) be formalized as a stable contract?Currently
export_js(container_id)returns a JS snippet that mounts the full viewer into a DOM element. Observatory and other tools (e.g. custom dashboards, Jupyter/Colab notebooks) rely on this to embed interactive graphs. Should the JS mount interface (FXGraphViewer.create({ payload, mount })) and programmatic methods (e.g.selectNode,setColorBy,setLayers) be documented and versioned as a stable API surface, or kept internal with only the Pythonexport_html/export_jsas the stable boundary?