# Parse and visualize results

Share the [published walkthrough](https://ambiqai.github.io/helia-estimator/guides/view-results/)
with evaluators. It opens without GitHub repository access. GitHub source and
release links require access while the repository is private; an unauthenticated
GitHub 404 does not mean the documentation or release is missing.

Start with the bundle, then choose a table, a model graph or a timeline. This
walkthrough uses the public keyword-spotting (KWS) examples. Commands assume a
source checkout and its Python 3.12 or 3.13 environment; installed-wheel users can omit
`uv run --frozen` when invoking `helx`.

## Read and verify a bundle

```bash
uv run --frozen helx run -c examples/at110/kws_vela_only.yaml
uv run --frozen helx show results/kws_vela_only
uv run --frozen helx validate bundle results/kws_vela_only
```

`run_summary.md` is the readable report. `run_summary.json` is the native
schema-version-4 contract. JSON measurements live under `measurements.samples[i]`.
`run_summary.csv` contains one row per measured invocation, with dotted columns
such as `measurement.index` and `measurement.cycles.total`. Without measured
samples it retains one row of shared facts. Use `helx export --fields statistics`
for aggregate rows. `manifest.json` binds the retained files to their sizes and SHA-256
hashes. Keep the whole bundle when sharing evidence, including `input_config.yaml`
and `artifacts/`. The original source model can live outside the bundle.

Check `fvp.stage`, PMU sanity, platform revision, values status and metric basis
before quoting numbers. A valid Vela-only result has no executed FVP counters.
Energy, power and efficiency remain experimental estimates. Integrity verification
does not establish numerical accuracy or silicon qualification.

For automation, the verified offline reader checks completion, schema, paths
and retained hashes before returning typed fields:

```python
from pathlib import Path
from helia_estimator.api import load_result, measurement_samples

result = load_result(Path("results/kws_vela_only"))
summary = result.summary
print(summary.run.model, summary.fvp.stage)
for sample in measurement_samples(result):
    if sample.cycles is not None:
        print("Measured sample", sample.index, "NPU total cycles:", sample.cycles.total)

for artifact in result.artifacts:
    print(artifact.relative_path, artifact.sha256)
```

Run a saved script with `uv run --frozen python your_script.py`. Missing values
are unavailable, not zero. To read a retained file with another integrity check,
pass its `ArtifactRef` to `result.read_artifact(artifact)`.
To list saved Model Explorer/Perfetto files with their experiment meaning, use
`helx results artifacts results/kws_vela_only`. The equivalent typed Python
catalog is `viewer_artifacts(result)`. Use `results export-artifact` or
`export_viewer_artifact` to copy a retained visualization with another hash and
completion check; see the [saved-visualization API](https://ambiqai.github.io/helia-estimator/guides/python-results/#discover-and-copy-saved-visualizations).
See the [Python API guide](https://ambiqai.github.io/helia-estimator/guides/python-results/) for rejected/incomplete bundles and
native exports, and the [result reference](https://ambiqai.github.io/helia-estimator/reference/results/) for every field.

For JSON and aggregate CSV:

```bash
uv run --frozen helx show results/kws_vela_only --json
uv run --frozen helx results inspect results/kws_vela_only --format json
uv run --frozen helx export results/ --format csv --fields all > runs.csv
```

`show --json` emits native schema 4. `results inspect/show/export` use a separate,
**draft portable mapping** after native verification; that mapping is not a
released wire API. Ordinary `helx export` makes a table; `--fields all` selects
all native leaves rather than the default curated columns. Write derived files
**outside** the bundle so they do not invalidate its manifest. Use `--strict`
on table export when any rejected input should fail the export.

## Explore the model in Google Model Explorer

Model Explorer shows a graph with HELX's per-original-operation overlays.
Install the optional viewer dependencies in the checkout:

```bash
uv sync --frozen --extra viz
```

Load the **original, uncompiled `.tflite`** used by this run. Overlay keys are
original output tensor names in graph `main`; the Vela-compiled or fenced model
will not give the same graph. Find the source in `run_summary.json` under
`source.cached_path` (or recover the exact original from `source.given`). A moved
bundle does not carry the model cache with it. The manifest verifies retained
artifacts, not an external cached source file.

For this example, copy the source into `local/kws-original.tflite` using a small
script saved as `local/copy_viewer_model.py` (create `local/` first):

```python
from pathlib import Path
from shutil import copyfile
from helia_estimator.api import load_result

summary = load_result(Path("results/kws_vela_only")).summary
source = Path(summary.source.cached_path)
copyfile(source, Path("local/kws-original.tflite"))
```

```bash
uv run --frozen python local/copy_viewer_model.py
uv run --frozen --extra viz model-explorer local/kws-original.tflite --node_data_paths=results/kws_vela_only/artifacts/model_explorer_overlay_mac_count.json
```

Follow the local URL printed by Model Explorer. Expand groups to inspect
operations; select **MAC Count** in the node data controls and use its eye icon
to color the graph. You can also start `model-explorer` without arguments,
select the original model, choose **View selected models**, then **Add per-node
data** to load an overlay JSON. These are Model Explorer data files, not
`run_summary.json`.

| Overlay in `artifacts/` | Meaning |
| --- | --- |
| `model_explorer_overlay_mac_count.json` | Vela-attributed MAC count |
| `model_explorer_overlay_sram.json` | Vela-attributed SRAM accesses |
| `model_explorer_overlay_cycles.json` | Vela cycle attribution scaled to the full-model FVP cycle total |

In HELX 0.7.0, a Vela-only run's **Calibrated Cycles overlay contains zeros**
because there is no FVP total to scale to. Use MAC Count or SRAM Accesses for that
run. To view calibrated cycles, use an executed FVP bundle, for example the
bus-trace run below, and select its cycles overlay with the same original model.
These are attributed cycles, **not independent per-operation measurements**.
Raw compiler estimates and attribution are also available in
`per_layer_breakdown.csv` and `per_original_op_breakdown.csv`.

For multi-subgraph models, or runs whose warmup excludes `CALL_ONCE`
initialization, HELX currently omits calibrated layer breakdowns and overlays
because Vela's table may describe a different execution scope. The raw
`vela_verbose_performance.log` remains available; `per_layer.unavailable_reason`,
`run_summary.md` and `helx show BUNDLE --layers 5` explain the omission.
Full-model PMU counters remain available. An overlay is also omitted when no
original operators can be mapped; consult the retained CSVs in that case.

![Public KWS graph with MAC Count overlay in Model Explorer](https://ambiqai.github.io/helia-estimator/images/results/results-model-explorer.png)

The screenshot uses the public KWS example's compiler output. Graph grouping and
controls may vary with the viewer version. If the overlay does not color nodes,
check the selected data series, graph `main` and original model identity first.
The optional TFLite adapter is platform dependent; if unavailable on your host,
run the viewer in a supported Linux environment. HELX's normal CLI does not
require Model Explorer. See Google's [user guide](https://github.com/google-ai-edge/model-explorer/wiki/2.-User-Guide)
and [command-line guide](https://github.com/google-ai-edge/model-explorer/wiki/3.-Command-Line-Guide).

## Open a bus timeline in Google Perfetto

After the normal `helx setup` prerequisites and Arm licence acceptance, run:

```bash
uv run --frozen helx run -c examples/at110/kws_bus_trace.yaml
uv run --frozen helx validate bundle results/kws_bus_trace
```

The example enables `defaults.fvp.bus_trace.enabled`. Bus tracing requires the
Corstone-320 / Ethos-U85 target; it is unavailable on Corstone-300 / U55 / U65.

Open [ui.perfetto.dev](https://ui.perfetto.dev/), choose **Open trace file**, and
select `results/kws_bus_trace/artifacts/bus_trace_perfetto.json`, or drag that
file into the page. Use the path recorded in
`artifacts.bus_trace_perfetto_json` for other bundles. No recording session or
browser extension is needed. Zoom with **W/S**, pan with **A/D**, and click a
track event to inspect its details.

Click **Expand all** in the track toolbar so **SRAM0**, **SRAM1** and **EXT**
are visible. Zoom into an active interval to inspect the three interfaces
together. The screenshot below has the memory-interface group expanded.

![Public KWS simulated bus timeline opened in Perfetto](https://ambiqai.github.io/helia-estimator/images/results/results-perfetto.png)

This view bins FVP bus transactions into a timeline normalized to NPU cycles.
It is a simulator diagnostic, not a silicon capture or a cycle-accurate CPU/bus
schedule. Keep `fvp.bus_trace` checks, time scaling and the configured NPU clock
with the interpretation. Companion `bus_trace_timeline.csv` and
`bus_trace_transfers.csv` support programmatic analysis. `keep_raw: false`
removes the raw transaction log after analysis, while retaining these outputs.
See Perfetto's [UI guide](https://perfetto.dev/docs/visualization/perfetto-ui).

## SRAM read and write diagnostics

The fenced Perfetto view focuses on PMU counters and verified bus traffic.
Use these figures to decide what to inspect next:

| Figure | Meaning and useful follow-up |
| --- | --- |
| **MAC active** | MAC-active cycles / NPU-active cycles. Low activity warrants checking memory pressure and the operator; it is not a percentage of peak MAC throughput. |
| **SRAM0 busy**, **SRAM1 busy**, **EXT busy** | Read + write beats / beats allowed at the configured port rate. A busy interface can motivate a memory-placement or bandwidth experiment. Separate SRAM interfaces require a verified bus trace. |
| **SRAM0 traffic share** | SRAM0's fraction of all SRAM read + write beats; 50% is even. Compare the two SRAM busy tracks before trying a port-balance change. No traffic means unknown. |
| **Mean SRAM read/write transfer** | Bytes / transfers, separately for each direction. Small transfers can motivate inspecting layout and burst splitting; a mean alone does not establish a cause. Requires a verified trace. |
| **Reads/writes queued**, **outstanding limit** | Counted request-stall events / NPU-active cycles. Use these with port usage to investigate queue pressure; events can overlap and ratios can exceed 100%. |

Uncounted metrics stay absent; a metric missing on some invocations has gaps
rather than a misleading zero. The selected profiles do not measure cache
hits/misses, so there is no reported hit rate. Vela cycle comparisons remain
available in `fenced_layers.csv` for compiler diagnosis and are omitted from
the timeline and selected-event details. Existing traces retain their original
tracks; rerun with the updated version to produce the new view.

For the separate SRAM usage, traffic-share and transfer-size figures, combine
`defaults.fvp.bus_trace.enabled: true` with per-operator fencing on U85, as
described in the methodology. The bus-only example above does not enable
per-operator fencing by itself. A PMU-only capture shows aggregate **SRAM busy**
when its counters suffice, assuming an even split across interfaces.

The expanded view below uses a BC-ResNet-8 proxy model with `port_stall`
counters and a verified bus trace. The top lanes show individual fenced NPU
invocations; the graphs below show their measured activity and derived traffic
figures. SRAM0/SRAM1 usage and traffic share include both reads and writes.
The read-only stall profile does not count write stalls; their absence is
unknown, rather than zero.

![BC-ResNet proxy fenced invocations with expanded MAC activity, memory-interface usage, traffic balance, transfer-size and request-stall tracks](https://ambiqai.github.io/helia-estimator/images/results/results-perfetto-metrics.png)

To generate this set of metrics for the public KWS model, copy
`examples/at110/kws_bus_trace.yaml` within its directory, give
`run.results_dir` a separate directory, add `pmu_profile: port_stall` under
`defaults.fvp`, and add `per_layer: {mode: per_op}` inside the model entry.
Run that config, then open its `artifacts/fenced_layers_perfetto.json`.
Use **Expand all** to reveal both groups and all their tracks. In each counter's
**Track options**, choose **Size → Medium (2x)** for larger graphs, and drag
the label-column divider to leave more room for the timeline. Model timings
and operator counts will differ from the proxy screenshot.

The screenshot reuses a verified v0.8.0 simulation with the updated renderer;
these metric tracks are available from helx 0.9.0. Capture and model identities
are recorded in the source repository's `docs/images/README.md`.

On the Ethos-U85, select `defaults.fvp.pmu_profile: sram_stall` to count SRAM
read/write queue cycles, read/write beats and MAC-active cycles together. The
complete example also enables per-operator fencing:

```bash
uv run --frozen helx run -c examples/at110/resnet_sram_stall.yaml
uv run --frozen helx show results/resnet_sram_stall
```

`measurements.samples[i].cycles.sram_wr_tran_req_stalled` counts write address requests held while
the memory interface is not ready (`AWVALID && !AWREADY`); it does not count
write-data backpressure. `sram_wr_stall_limit` counts stalls from the NPU's
outstanding-transaction limit. These are distinct events, not a count of
outstanding transactions or proof of the downstream queue depth.
The analogous `sram_rd_*` fields describe reads. Zero means counted zero;
an absent field means uncounted. The profile uses all eight event counters,
so EXT traffic and the complete measured energy estimate are unavailable.
Existing profiles remain available for other counter selections. U55/U65
`default` and `default_ext_rd_stall` also leave EXT write beats uncounted;
these runs cannot supply the complete counter-based energy estimate. A
previous parser substituted zero for that missing counter; new results
preserve it as unknown.

The fenced CSV and Perfetto timeline add **SRAM writes queued** and **SRAM
writes at the NPU's outstanding limit**. U85 SRAM stall events sum across AXI ports. The plotted ratio is the event
count divided by NPU-active cycles, shown as a percentage; it can exceed 100%
and is left unclipped. It is not the fraction of elapsed time a model stalled.
Read and write events may overlap each other and useful NPU work: do not add them or interpret them as recoverable runtime. The
**SRAM queue pressure** and **NPU engine** lanes are heuristics; missing
counters do not establish an unconstrained memory path.

Open `results/resnet_sram_stall/artifacts/fenced_layers_perfetto.json` in
Perfetto, click **Expand all**, and select an invocation to see its original
operators, NPU-active cycles and traffic. Expand both **layers (fenced run)**
and **per-layer figures**; the read and write request-stall tracks are separate.
The screenshot uses the shipped ResNet example on helx 0.8.0.

![ResNet read and write diagnostics with all Perfetto track groups expanded and an invocation selected](https://ambiqai.github.io/helia-estimator/images/results/results-perfetto-sram.png)

This profile does not count EXT traffic. Its absence in the details means
unknown, rather than zero. A **Not enough counted to tell** lane is expected
when the available counters cannot support a bottleneck classification.

Fencing changes scheduling and fusion. A percentage improvement in an
operator-isolated run is not the same improvement in the normal model.
Compare `measurements.samples[i].cycles` from matching unfenced runs for the model's NPU work;
these counters exclude CPU operators and are not end-to-end application latency.
FVP total/CCNT values have simulator step granularity, so small differences
are model sensitivity, not a precise silicon speedup;
use `per_segment` artifacts to investigate the separate fenced experiment.
Counter definitions: [Arm Ethos-U85 TRM, issue 05, PMU events](https://documentation-service.arm.com/static/67b5ba01ce2747241fce860f).

## Optional fenced layer timeline

To collect counters between operations, use the existing
`examples/what-if/corstone_300_u55/kws_per_op.yaml` example after
`helx setup --target corstone-300`:

```bash
uv run --frozen helx run -c examples/what-if/corstone_300_u55/kws_per_op.yaml
```

Alternatively, enable fencing on a copy of your own config. Set this **inside
its model entry**, alongside `source` and `memory_placement`:

```yaml
    per_layer:
      mode: per_op
```

Fenced profiling supports LiteRT, heliaRT and heliaAOT (1.3.0 or later). It inserts
`ETHOS_U_STREAM_SPLIT` boundaries, recompiles and reruns the model. Fences can change
fusion and scheduling; compare with the unfenced run explicitly.

Open the file recorded in `per_segment.layers_perfetto_json` (normally
`artifacts/fenced_layers_perfetto.json`) in Perfetto. Its time axis concatenates
fenced NPU-active intervals at the configured NPU clock; it omits CPU gaps and
is not application wall time. `per_segment.layers_csv` holds the segment table.
A segment is a fenced NPU invocation, not a guarantee of one original operation.
For additional stall diagnostics **on U85 only**, choose
`defaults.fvp.pmu_profile: port_stall`. On U55/U65, keep the example's default
supported profile; `port_stall` is unavailable. That U85 profile does not count
write beats, so absent write metrics remain unknown. Bus tracing can accompany
it on U85, but is not required for the layer timeline.

In Perfetto, click **Expand all** in the track toolbar, or expand the chevrons
beside **layers (fenced run)** and **per-layer figures**. The first group shows
NPU invocation intervals; the second shows the retained diagnostic series.
Click an invocation to inspect its duration and retained counter arguments.

![Public KWS fenced-layer trace with both track groups expanded in Perfetto](https://ambiqai.github.io/helia-estimator/images/results/results-perfetto-layers.png)

This U55 example does not collect every counter needed to classify a layer's
likely bottleneck. Its **Not enough counted to tell** lane preserves that
limitation; it is not a simulation failure.
This retained v0.8.0 image includes the former Vela comparison track; new
traces use the measured and derived view illustrated above.

If a visualization file is absent, check the summary's artifact references and
stage first: a compile-only run has no bus trace or fenced PMU timeline, and a
run without `per_layer` fencing has no fenced timeline. The overlay graph and
the fenced timeline answer different questions and need different inputs.

See [Tensor inputs and numerical verification](https://ambiqai.github.io/helia-estimator/guides/tensor-verification/) for fixed input bytes,
per-sample output artifacts and explicit reference/tolerance policies.

## SRAM transaction sizes

On U85, `defaults.fvp.pmu_profile: sram_transactions` counts accepted SRAM
read/write transactions with matching aggregate SRAM beats in the same capture:

```bash
uv run helx run -c examples/at110/resnet_sram_transactions.yaml
uv run helx show results/resnet_sram_transactions
```

The eight slots contain NPU activity, MAC activity, all four SRAM/EXT beat
totals, and SRAM read/write accepted transactions. The dedicated cycle counter
remains available. This preserves the traffic inputs for energy estimation,
but omits the default profile's per-interface read splits and does not count
queue stalls. Existing profiles and the default are unchanged.

Each native `measurements.samples[i].traffic_transactions` block has `sram_rd`
and `sram_wr` counts, in **transactions**. Zero is counted zero; null is
unavailable. The portable export retains these as namespaced transaction
observations and adds derived `burst_average` observations with source pointers.

For each direction, average beats per transaction is matching beats divided by
accepted transactions; bytes per transaction multiplies by the platform's
bytes per beat. Ratios are unavailable when either count is missing, the
transaction count is zero, or PMU overflow is nonzero or unknown. Byte ratios
also require a known byte-aligned beat width. These are whole-window aggregate
transfer averages, not a distribution of burst lengths, outstanding queue
depth, or a prediction of performance gains. No counters from different
profiles or invocations are combined.

The example also makes a separate per-operator fenced capture. Its segment CSV
contains the raw transaction counts and matching beat/byte averages. Segment
ratios use only that capture and are withheld if its totals do not reconcile.
Fencing changes scheduling; do not combine those counters with the normal run.
