Output files
Know what each file in a result bundle is, which numbers it holds, and what each number measures. The bundle is designed for progressive disclosure: a summary and a per-layer table by default, deeper projections on request, and a manifest that lets a consumer verify the whole set.
Prerequisites
Section titled “Prerequisites”A results/ directory from First profile.
Which file
Section titled “Which file”| File | Schema | Role | When present |
|---|---|---|---|
result_manifest.json |
hpx.result-manifest v1 (results/manifest.py) |
the bundle envelope: identity, validity, issues, provenance, comparability, and every artifact’s path, media type, size and SHA-256 | always; written last |
summary.json |
hpx.run-summary v19 (results/run_summary.py) |
headline metrics and health signals; read this first | always |
profile_results.csv or .json |
hpx.profile-results v1 for the JSON form (report/contracts.py); the CSV carries the semantic name without a published schema |
one row per layer, one column per counter | always; one of the two per output.format |
run_metadata.json |
hpx.run-metadata v1 (report/contracts.py) |
the resolved configuration, platform, model, toolchain, build images | always |
nsx.lock |
neuralSPOT-X lock file | the exact module lock the firmware was built from | always |
aot_operator_manifest.json |
engine extension | the operators the AOT compiler emitted with tensor placement | helia-aot runs |
aot_memory_layers.csv |
engine extension | the same placement data flattened for a spreadsheet | helia-aot runs |
model_explorer/me_overlay_<COUNTER>.json |
Model Explorer overlay format | one overlay per counter | unless output.model_explorer: false |
detailed/memory.json, detailed/profile_<group>.csv, detailed/profile_<preset>.csv, detailed/power_summary.csv |
projections and diagnostics | counters merged per compute-unit group and per PMU preset, memory breakdown, per-window power | output.detailed: true |
result.json in a run directory’s evidence-<N>/ |
hpx.run-result v1 (runs/record.py) |
one run-API capture, saved and verifiable; see A saved run result | hpx run, RunResult.save |
The manifest’s artifact entries carry role (core, projection,
extension, export, diagnostic), name, producer, optional, and
schema plus schema_version only where a published schema exists. A
complete profile bundle requires the core artifacts summary.json,
run_metadata.json and the selected primary result, plus nsx.lock
when the run’s dependency provenance recorded one.
load_result_manifest(path, verify=True) rejects missing, modified or
path-escaping artifacts; verify defaults to False, so a loader that
omits it reads the manifest without checking the files. The packaged JSON Schemas are under
helia_profiler/data/ (result_manifest.schema.v1.json,
run_summary.schema.v1.json, run_metadata.schema.v1.json,
profile_results.schema.v1.json); they fix the stable root fields and stay
open to additive extension, and the typed models in
helia_profiler.results are the authoritative shape.
1. summary.json
Section titled “1. summary.json”Written by report/summary.py from the RunSummary model. The file opens
with its identity; abridged here, the full file also carries binary,
latency, memory, compatibility, dependencies, identity, top_layers and the
nested memory and analysis tables described below:
{ "schema": "hpx.run-summary", "schema_version": 5, "engine": "helia-rt", "layers": 13, "total_cycles": 2060239.0, "overflow_detected": false, "validity": "valid", "issues": []}(the engine, layer and cycle values are from the shipped bundle
examples/results/hardware-validation-2026-09-16/apollo510_evb-kws-rt-ns-arm-none-eabi-gcc-rtt-auto/,
which retains schema version 5. New runs use schema version 19; use the
typed loader to read historical bundles without rewriting them). Top-level
fields in new runs:
| Field | Type | Unit | What it measures |
|---|---|---|---|
schema, schema_version |
string, int | none | hpx.run-summary, version 19 |
engine |
string | none | the engine that ran |
layers |
int | count | operators in the profiled graph |
total_cycles |
number | CPU cycles | the sum of the per-layer instrumented cycle counts (report/summary.py), so it includes the per-layer counter reads; the clean window is latency.device_clean_infer_avg_cycles |
overflow_detected |
bool | none | any counter saturated its 32-bit accumulator in any pass |
top_layers[] |
op, cycles, pct |
cycles, percent of total_cycles |
the heaviest layers |
validity |
valid, degraded, invalid |
none | the run’s verdict from the same evaluation the manifest records |
issues[] |
code, severity, message, context | none | the structured reasons; every code is on the issue codes page |
memory (from MemorySection):
| Field | Unit | What it measures |
|---|---|---|
arena_size |
bytes | the arena requested (model.arena_size or the engine default) |
allocated_arena |
bytes | what the runtime used of it; the number to size the next run from |
model_size |
bytes | the model file |
num_tensors, input_size, output_size |
count, bytes, bytes | graph tensors and the I/O buffers |
binary (from BinarySection, using the profile ELF’s size-tool totals
and section headers for reserved-memory reconciliation; the power-only
binary is not measured):
| Field | Unit | What it measures |
|---|---|---|
text |
bytes | size-tool code plus read-only data; not just the ELF section named .text |
data, bss |
bytes | initialised writable data and zero-initialised state, respectively |
reserved |
bytes | linker-reserved NOBITS capacity separated from statically allocated state (Apollo5 linker scripts fill the remaining DTCM as heap); omitted when zero |
total |
bytes | the size tool’s inclusive sum; text + data + bss + reserved reconciles to it |
latency (from LatencySection; the boundary of each number is in its
name):
| Field | Unit | Boundary |
|---|---|---|
capture_duration_s, hpx_start_latency_s, protocol_duration_s, boot_phases_s |
seconds | host-side timing of the whole capture, not of inference |
device_profiled_infer_count, device_profiled_infer_total_us, device_profiled_infer_avg_us |
count, microseconds | the instrumented per-layer passes, on the device’s clock |
device_clean_infer_count, device_clean_infer_total_cycles, device_clean_infer_avg_cycles, device_clean_infer_avg_us |
count, cycles, microseconds | the clean window: warm, back to back, no per-layer instrumentation |
device_clean_stalled_iters, device_clean_partial_iters, device_clean_ref_cycles, device_clean_dwt_rate_cyc, device_clean_dwt_rate_us, device_clean_attach_wait_us |
counts, cycles, microseconds | the clean-window clock checks described on Verifying a capture |
clean_workload |
string | identifies what the clean window ran (for AOT, aot_raw_zero_refill_included_v1 means input restoration is inside the timing) |
cache: the aggregated memory-group counters (ARM_PMU_L1D_CACHE,
ARM_PMU_L1D_CACHE_RD, ARM_PMU_L1D_CACHE_REFILL,
ARM_PMU_L1D_CACHE_MISS_RD, ARM_PMU_DTCM_ACCESS, ARM_PMU_MEM_ACCESS
among others; report/memory.py holds the list) as event counts over the profiled passes, plus l1d_hit_rate_pct derived
as on PMU counters.
power (from PowerSection, present only when power was captured):
| Field | Unit | Boundary |
|---|---|---|
measurement_scope |
gpio_gated_clean_window, on_device_gated_inference, free_form_capture, whole_capture_window |
what the numbers below were integrated over (Windows) |
avg_current_a, median_current_a, p95_current_a, p99_current_a, peak_current_a |
amperes | inside the scope; percentiles exist for the Joulescope only |
avg_power_w, median_power_w |
watts | inside the scope |
energy_j, energy_per_inference_j, inferences_per_joule |
joules, joules, 1/joule | the scope’s integrated energy and its division by the counted inferences |
capture_duration_s, capture_window_s, gated_window_expected_duration_s, gated_window_duration_ratio |
seconds, seconds, seconds, ratio | the measured window against the expectation (Verifying a capture) |
gated_window_count, gated_window_duration_suspect, gated_window_reference_drift, per_inference_metrics_omitted, short_gate_pulses_ignored |
count, bool, string, string, count | health signals |
power_firmware, observation_mode, integrity, gate_failure, gating_method, sync, sync_timing_s, terminal, on_device_summary, window_clock_ceiling, gate_duration_integrity, firmware_code_fingerprint, clean_workload, power_plan, target_lifecycle |
mixed | how the capture was produced; firmware_code_fingerprint and clean_workload are comparability inputs |
active_window_estimated_* |
seconds, joules | an estimate for the active portion, labelled by active_window_estimate_method |
memory_plan and memory_regions answer two different questions and are
kept apart on purpose:
memory_planis the decision record computed before any compiler ran: per region,capacity,used(the sum of what hpx placed) and the namedconsumers. It carries nofreeoroverflow, because only a measurement can answer those.memory_regionsis the measured truth from the linked ELF, classified into the per-SoC windows:link_family,linker_profile, and per regionwindow,app_window,used,reserved,free,load_image,window_provenance,app_provenance, plusunattributedfor sections outside every window.usedincludes the live stack on GNU links;freeisapp_window.length - used; GNU’s fill-to-end heap sits inside the app window (in bothreservedandfree), armlink’s fixed heap and stack sit outside it, sousedandfreecompare only within a link family. The block is absent whenever it cannot be true: a custom SoC, a non-defaultlinker_profile, a failed tool probe or a partial section inventory.memory_reconciliation, when the symbol table is readable, holds the two against each other: per consumerplanned_size,status(matchedwithmeasured_sizeanddelta,missing, orunmatchablewhen no sized symbol can exist) andmatched_symbols; per region planusedagainst measuredused.
The schema version history matters for comparisons: v2 split reserved
out of bss; v3 removed free and overflow from memory_plan; v4
re-sourced the gate-duration verdict (power.gate_duration_integrity
with gated_window_reference_drift); v5 preferred the driver’s packet
duration for gated power; v6 added clean_workload. hpx compare reports
a version difference as informative and reads artifacts through
load_run_summary, whose PowerSection.gate_duration_unarbitrated_failure
property applies the v4 arbitration.
identity (from helia_profiler.identity.RunIdentity, built by
profile_identity.py) states what ran in the vocabulary fixture builds use, so
a profile run and a fixture build of the same firmware can be compared. A fact
the run did not record is {"unknown": "<reason>"} rather than absent or
guessed:
| Field | What it states |
|---|---|
runtime |
engine (litert, helia-rt, helia-aot or executorch), repository, version and commit: the runtime module from the lock, the installed heliaAOT package, or the ExecuTorch runtime record |
kernels |
library (reference, cmsis-nn or helia-core), repository, version, commit: the one CMSIS-NN module the lock resolved, never the backend label; reference only for LiteRT’s reference backend, which links none |
build |
toolchain, toolchain_version, board, cpu_hz, clock_profile, arena_location, weights_location, arena_bytes |
baseline_id, dependency_lock_sha256, producer |
the compatibility baseline, the lock’s sha256 and the HPX release, from the dependency workspace |
model_sha256, inputs_hash |
the model file’s sha256, and an hpx.inputs semantic hash of each pinned input’s tensor name and data sha256; profile firmware feeds zero-filled inputs, so inputs_hash is unknown |
run |
how it ran and how the clean window was timed: the fields below |
run.lifecycle, run.instance_boundary |
carry with boot: the engine initializes once and keeps its state across invocations; single with invocation for ExecuTorch, whose firmware reloads and initializes the program on every call |
run.iterations, run.warmups |
the configured counts; the clean window always runs at least one warmup |
run.first_invoke_initializer, run.first_invoke_window |
whether the first invocation of an instance runs the model’s one-time initializer, and where it falls. LiteRT and heliaRT run a CALL_ONCE subgraph inside the first Invoke(), so this is whether the source model has one; heliaAOT runs it in model_init(), so false; unknown for ExecuTorch. The window is warmup, except measured for ExecuTorch, where every invocation is its instance’s first |
run.timing_scope |
restore_and_invoke for heliaAOT’s clean window, else invoke_only |
run.timing_clock, run.timing_clock_hz |
the clock that times the clean window and its configured rate: stimer at 32,768 Hz for the busy_loop probe; otherwise cpu cycles at the CPU clock for ExecuTorch (its execute-cycle count) on every SoC, stimer on Apollo5, and cpu (DWT) cycles elsewhere |
run.timing_statistic |
mean: the clean window reports the mean per invocation (latency.device_clean_infer_avg_us); per-layer counters use profiling.aggregation |
run.energy |
the method the run’s energy is measured with: null for a profile without power.enabled, and unknown for one with it, since a profile does not state its energy method. A run-API rail capture states its scope, rail, subtraction and completion_protocol here, also when the capture failed |
build_hash |
an hpx.build semantic hash of the flashed .bin image, what the lock resolved, and the model; it ignores the HPX release and the lock’s timestamps, so it changes only when what runs changes |
observation_hash |
an hpx.observation semantic hash of the PMU and device timing results, without host paths; two captures of the same firmware can differ |
intent_hash |
the frozen plan’s hpx.intent semantic hash for a run built from one; null for an ordinary profile |
Semantic hashes
Section titled “Semantic hashes”build_hash, inputs_hash, observation_hash and intent_hash, and a
compiled build’s binding, are semantic hashes:
hpx.<kind>/<version>:sha256:<hex>, with 64 lowercase hex digits. The kind
says what was hashed and the version says how. The identity’s *_sha256
fields stay bare hex: each is the sha256 of one file’s bytes (the model, the
dependency lock, the prepared runtime archive).
The hashed bytes, the basis, are the value’s JSON as Python’s json.dumps
writes it with sort_keys=True, separators=(",", ":") and
ensure_ascii=True: keys sorted, no whitespace, every non-ASCII character
escaped as \uXXXX (a surrogate pair beyond U+FFFF) so the bytes are
ASCII, and each float in Python’s shortest round-trip repr (0.1,
1e-05). A non-finite float (NaN or an infinity) is refused, except in an
observation basis, where a capture can report a NaN counter and it is
written as the string "nan", "inf" or "-inf". Each kind has one basis:
| Kind | Basis |
|---|---|
hpx.intent/1 |
what a frozen plan asks for: its settings without host paths, the model hash, baseline, qualification, overrides, local inputs, toolchain, environment and engine runtime |
hpx.build/1 |
the flashed image’s sha256, what the dependency lock resolved, the linked runtime archive’s sha256 and the model’s sha256 |
hpx.observation/1 |
one capture’s measured facts, with files reduced to their sha256 and no host paths or error text |
hpx.binding/1 |
a compiled build’s plan intent_hash, lifecycle, timing scope, state policy, power wiring, prepared runtime manifest and what it observes |
hpx.inputs/1 |
each pinned input’s [tensor name, data sha256], in model order |
hpx.target-config/1 |
retained board, probe device type, load address, configured clock profile/CPU rate and arena/weights placements; no workload or arena capacity |
Changing what goes into a kind’s basis bumps that kind’s version. Compare two
hashes only when their producer, kind and version all match.
helia_profiler.SemanticHash.parse checks a string’s form and returns its
producer, kind, version and hex; it refuses bare hex, uppercase hex,
a missing version and any algorithm but sha256.
These examples pin the encoding:
| Kind | Basis bytes | Hash |
|---|---|---|
intent |
{"settings":{"engine":{"type":"helia-aot"}},"toolchain":["atfe","21.1.1"]} |
hpx.intent/1:sha256:a74a43acef82134a3774a67959b8da1232a38fb88d7ea6f93e55e3e7662786c8 |
build |
{"image":"ab12","lock":{},"model":"cd34","runtime_archive":null} |
hpx.build/1:sha256:6c416b863ba7e576075dca7ff677be4449aa90e879738cb4591dcb323d9583f5 |
observation |
{"cycles":[120,80],"error":false,"mean_us":"nan"} |
hpx.observation/1:sha256:8f2796b5d85db5099f58b011ce8cb5c0b7c38f22d36d462cf80e6e9b77eba605 |
binding |
{"lifecycle":"carry","power_sync":null} |
hpx.binding/1:sha256:7b9198891413a5779f01bb8aed3d0a68125d87cf983fddec759efbfcb58201ed |
inputs |
[["input_1","cd34"],["input_2","ef56"]] |
hpx.inputs/1:sha256:4a9320c8ecb5b4deede7d46e4dd9f2ad11ecf27e7626385dc7e7e03d84f2c9cc |
target-config |
{"arena_location":"sram","board":"apollo510_evb","clock_profile":"lp","cpu_hz":96000000,"device":"AP510NFA-CBR","load_address":4259840,"weights_location":"mram"} |
hpx.target-config/1:sha256:41d565d1ce07c353df4a6156c30885773b692ad5b6224fd58b5a918d26ad04e0 |
intent |
{"note":"\u00b5s \u2713","ratio":0.1,"scale":1e-05} |
hpx.intent/1:sha256:78888b62d415f66c54322c1aa3ea08858c0ffd0df737e142b8c13424825ca312 |
The last example is {"note": "µs ✓", "scale": 1e-05, "ratio": 0.1}: its
non-ASCII characters are escaped and its floats keep their shortest form.
Four more top-level sections appear when the run produced them and are
owned by other pages: compatibility and dependencies (the qualified
baseline and the lock provenance, Toolchains), psram
(Memory placement) and model_analysis
(cycles_per_mac and cycles_per_op from model analysis,
Analysis and comparison).
Version 12 uses compiled-graph costs for AOT profiles when the compiler supplies
the transformed graph. model_analysis.engine identifies the analyzer and
index_space is execution for compiled costs or source for original-graph
costs. Original-graph fallback and older reports retain source-index joins.
Version 13 reports the planned MRAM capacity as the linker’s application
window for the selected toolchain, which is smaller than the whole MRAM part
(4,128,768 of 4,194,304 bytes on Apollo510), wherever the SoC has a linked
memory map.
Fusions can change totals and derived rates; compare like analysis bases.
Version 14 adds identity. The change is additive: no existing field changes
meaning.
Version 16 adds identity.intent_hash and the run fields from
instance_boundary to timing_statistic. An ExecuTorch run’s
run.lifecycle becomes single; nothing else changes meaning.
Version 17 renames the engine tflm to litert in engine and wherever
else a summary names the engine; nothing else changes. Read a summary that
says tflm with the HPX release that wrote it.
Version 18 adds identity.run.energy; nothing else changes meaning.
Version 19 writes identity.build_hash, observation_hash and intent_hash
as semantic hashes instead of bare hex, and renames
identity.inputs_sha256 to inputs_hash, also a semantic hash; the hex part
of each is unchanged. The verified result reader refuses a version 19 summary
whose identity is not an object, omits one of these four, or states one that
is neither a hash of its kind nor, where allowed, {"unknown": "<reason>"}
or null. It reads no other summary version: one is refused as
unsupported_schema, naming its version, and is read with the HPX release
that wrote it.
2. profile_results.csv
Section titled “2. profile_results.csv”One row per layer, written from the merged per-layer table:
| Column | Unit | Meaning |
|---|---|---|
id |
index | the firmware’s execution position: the sequential layer index for the interpreter engines, the post-compilation position for AOT (which skips operators such as VAR_HANDLE without renumbering) |
op |
string | operator type; DEPTHWISE_CONV_2D:1 on AOT carries the original index |
source_index |
index | AOT only: the original operator index retained as provenance; compiled-graph costs join on execution position |
kernel_calls |
role=symbol pairs |
AOT only, when the installed helia-aot records them: the CORE entries the generated operator calls directly, ;-separated and sorted by role. run calls execute inside the measured window; init calls run in the operator’s initialisation before it; other keys name shared helpers. none when the compiler recorded no CORE call; empty when it recorded nothing for that operator. Interpreter engines select kernels at run time and have no column. A symbol linked into the image is not evidence that it ran |
<COUNTER> columns |
event counts | each requested counter, aggregated across iterations by profiling.aggregation |
cycles |
cycles | the dedicated cycle counter |
overflow |
bool | any counter of this layer saturated; its values are a lower bound |
macs, ops, cycles_per_mac |
count, count, cycles per MAC | present when the model could be analysed (the analysis extra) |
The JSON form (output.format: json) carries the measured rows under
layers (id, op, counters, cycles, cycles_pct, overflow) without
the attribution columns, with metadata, summary, presets,
overflow_detected and power at the root, per
profile_results.schema.v1.json. aot_operator_manifest.json keeps the
per-operator provenance for either format.
3. run_metadata.json
Section titled “3. run_metadata.json”The provenance of the run, from report/contracts.py:
| Field | Meaning |
|---|---|
hpx_version, run_id, timestamp |
the tool and the run |
config |
the fully resolved configuration after the merge (Configuration) |
platform |
board, soc, core, link_family (gnu for GCC and ATfE, armlink for armclang), the resolved clock tier |
model |
name, size in bytes, sha256 |
toolchain |
compiler, compiler_version, cmake_version as found on the host |
engine |
type and the measured runtime version where one resolves (heliaRT, heliaAOT) |
build_images[] |
one entry per built target (profile, and power on a power run): target_name, binary_name, sha256 of the linked ELF, flash_sha256 of the flat .bin image the probe writes (no host paths; null when the build left none), size_bytes, architecture_flags counted from the configured build tree’s compile_commands.json, translation_units |
firmware |
arena_size, allocated_arena, model_size as the firmware reported them |
| dependency provenance | the typed registry hash, baseline identity and fingerprint, workspace fingerprint, lock mode and update state, requested refs and tags, peeled commits, content hashes and explicit overrides |
architecture_flags counts instruction-set flags such as
-mcpu=cortex-m55 against -mcpu=cortex-m55+nomve across the configured
tree; it is an inventory of the configured units, not proof each was linked,
and precompiled libraries contribute nothing. hpx compare shows it as an
informative dimension.
4. AOT artifacts
Section titled “4. AOT artifacts”aot_operator_manifest.json records every operator the AOT compiler emitted
after graph transforms, in execution order, each with inputs, outputs and
local tensors (weights, weight sums, per-operator scratch). idx is the
execution position. id is the operator’s .tflite index, or null when
a transform created it or fused several (source_indices lists every index
it came from); the firmware labels such a layer <OP_TYPE>:-1, where
<OP_TYPE> is op_type upper-cased with any character outside A-Z, 0-9
and _ replaced by _.
kernel_calls is the compiler’s record of the CORE entries the operator’s
generated code calls, keyed by role or shared helper, or null when nothing
was recorded for that operator or it was malformed. The compiler’s own
<prefix>_report.json, beside the generated module under aot_output/ in
the work directory, carries the module-wide list with its unrecorded
operators. Tensor entries carry id, the model’s name when it has one,
dtype, ctype, kind (input, output, constant, persistent or
scratch), nbytes and shape. Placed tensors add memory (dtcm,
itcm, sram, mram, psram), source_memory, staged, arena_role
(scratch, persistent, constant), arena_region_id, offset and
allocation_size, all in bytes where they are sizes. Manifests from
hpx 0.2.x recorded model inputs and outputs as scratch and named tensors
tensor_<id>, so hpx compare against such a run reports their placement
as changed.
aot_memory_layers.csv is the same data as one row per tensor with
layer_idx, layer_id, op_type, op_name, tensor_role, tensor_id,
tensor_name, tensor_kind, memory, source_memory, staged,
arena_role, arena_region_id, offset, size, shape.
5. detailed/
Section titled “5. detailed/”With output.detailed: true: memory.json (binary sections, the arena
block, per_layer_memory with each layer’s memory-group counters,
cache_totals, and memory_symbols: the top 32 sized symbols per region),
profile_<group>.csv merged per compute-unit group and, when the run
needed more than one PMU pass, profile_<preset>.csv per preset (presets
are named <group>_<index>, for example cpu_0), and power_summary.csv with one row per gated window plus a
whole_capture_window reference row.
6. The terminal summary
Section titled “6. The terminal summary”Every run prints the arena, model and binary sizes, the layer count, total
cycles, the top layers with percentages, the cache totals and the
validity footer: VALID, DEGRADED with one code: message line per
warning, or INVALID with the errors first. An invalid run still exits 0
and writes its bundle unless output.fail_on_invalid (--fail-on-invalid)
is set, in which case it exits 3.
7. A saved run result
Section titled “7. A saved run result”hpx run saves each capture as result.json in its evidence directory.
From Python, RunResult.save(path, roots=..., build=...) writes one and
helia_profiler.load_run(path, roots=...) reads it back as a RunResultRecord
(path, size_bytes, sha256, hpx_version, result, document). Paths are
{root, path} under named roots, as in a saved compiled build, so the run
directory moves as a unit.
| Field | Holds |
|---|---|
location |
where the result itself sits, under its root |
compiled_build |
the saved build’s root, path, sha256 and size_bytes |
capture |
the capture’s facts (state, timing, memory, rail_energy, error and the rest); every file it references (outputs, terminal snapshots, the rail trace) as root, path, sha256 and size_bytes |
power |
null, or the request with kind rail or supply |
verification |
null, or the reference origin, tolerance, matched, the per-tensor facts and reference_files, the reference outputs written beside the result as <stem>.reference_<index>.<publication>.bin |
identity |
the run’s RunIdentity |
hpx_version |
the heliaPROFILER that wrote the result |
Loading checks the evidence: every pinned file matches its sha256 and size,
the compiled build is the one recorded and verifies, the identity and the
output comparison recompute from the build and the capture, and the power
request matches the build’s GPIO wiring, protocol and supported power route.
A successful capture must include timing whose iteration and warmup counts match
the compiled build; both saving and loading enforce this check. Failed captures
may retain no timing.
Retained energy must agree with the request’s instrument and rail, the build’s
iteration counts, and a valid batch completion record. Timing-only and
supply-only runs cannot carry rail energy. An unknown identity fact matches an unknown fact
whatever its reason text. Free text, such as a failed capture’s error, is
recorded, not verified. The checks catch accidents, such as an edited or
mixed-up file, not tampering. A failed capture saves and loads too; its
outcome is data. A failed rail capture may have no energy. Error text may be
null or a string; its wording is not verified.
Saving uses a new set of reference filenames for each publication. If a replacement save fails, the previous JSON and its reference files remain readable. Unreferenced files may remain after an interrupted save.
A refusal is a helia_profiler.RunResultError, a ConfigError whose
code names the failed check:
| Code | Meaning |
|---|---|
invalid_json |
not strict JSON: unparsable, a repeated key or a non-finite number |
unsupported_schema |
another schema, or another hpx.run-result version |
invalid_path |
unusable roots, or a result that is not where it records itself |
artifact_changed |
the build, an evidence file or a reference output is missing or changed |
runtime_missing |
the build’s prepared runtime is not in the HPX cache; the hint names the hpx runtimes prepare command |
identity_mismatch |
the identity or the output comparison does not recompute |
invalid_evidence |
anything else: an unreadable or malformed result, a build that does not verify, or a power request the build cannot take |
helia_profiler.api.load_result takes a result.json or its evidence
directory, dispatches on the file’s schema, and verifies it under the root
it records itself under and this host’s HPX cache, raising a
ResultReadError with the same codes. It adds publication_changed for a
result that changed while it was read, and ambiguous_location for a
directory holding both result.json and result_manifest.json; name the
file instead. observations(record)
maps it to the portable result view (1.0-draft, mapping hpx-run-2);
hpx results show and hpx results export print and write it.
| Metric | Unit | Scope | Statistic | Basis |
|---|---|---|---|---|
latency.invoke.v1 or latency.restore_invoke.v1 |
ms | the run’s timing_scope |
mean over the measured iterations (sample_count) |
ticks / timer_hz / iterations * 1000; clocks are the configured timing_clock_hz and the reported timer_hz, domain timer for the STIMER |
energy.per_inference.v1 |
uJ | gross_rail_restore_and_invoke |
mean per measured iteration | energy_per_iteration_j * 1e6; domain system, since the MCU rail feeds more than the CPU |
power.mean.v1 |
mW | gross_rail_restore_and_invoke |
time mean over the gated window | mean_power_w * 1e3 |
memory.arena_capacity.v1 |
B | arena_capacity |
size | the capture’s memory terminal |
memory.arena_used_after_invoke.v1 |
B | allocator_after_invoke |
size | the capture’s memory terminal |
A timing-only or supply-only run states energy unavailable
(energy_not_measured_timing_only, energy_not_measured_supply_only), and a
failed capture states every measurement unavailable (capture_failed).
producer.version is the hpx_version that wrote the result. workload
holds the pinned model file’s name, its sha256, the inputs_hash and the
lifecycle. verification.checks names the output comparison only when the
run compared its outputs; otherwise a limitation says they were not
compared.
engine.version follows the engine, and extensions.helia-profiler names
its basis:
| Engine | engine.version |
engine_version_basis |
|---|---|---|
helia-aot |
the heliaAOT release that generated the module | aot_provider_release |
helia-rt, litert, executorch |
the linked runtime release | linked_runtime_release |
An unknown version stays null, with its reason in
engine_version_unavailable_reason. extensions.helia-profiler also holds
the build-time producer (build_producer), the output comparison, the
semantic hashes and the run facts
(instance_boundary, first_invoke_initializer, first_invoke_window,
timing_statistic, warmups). The view does not qualify the engine
numerically or authenticate the result.
examples/results/run-api-synthetic/ holds a synthetic saved result and
its portable.json.
Expected output
Section titled “Expected output”A bundle whose manifest verifies, whose summary says valid, and whose
per-layer table has one row per operator and one column per counter you
asked for. Parsing outputs reads all of it from
Python and jq.
Controlling output
Section titled “Controlling output”| Key | Flag | Effect |
|---|---|---|
output.dir |
--output-dir |
the bundle directory (results by default; a second run into it overwrites in place) |
output.format |
--output-format csv or json |
the primary per-layer artifact |
output.model_explorer |
--no-model-explorer |
skip the overlays |
output.detailed |
--detailed |
write detailed/ |
output.fail_on_invalid |
--fail-on-invalid |
exit 3 on an invalid run |
What next
Section titled “What next”Parsing outputs for scripts against these files, Analysis and comparison for two bundles side by side, Model Explorer overlays for the graph view.
Schema v7 uses the INA228 accumulation interval for internal-mode average power and current. Energy and energy per inference are unchanged. The GPIO gate interval is power.terminal.gate_elapsed_us; the whole firmware interval remains power.terminal.elapsed_us. Comparing v6 and v7 reports an informative schema difference.
Schema v8 corrects MAC counts for TRANSPOSE_CONV, non-flat FULLY_CONNECTED, and BATCH_MATMUL. Models using these operators can have changed total MACs, total operations, TOPS, and cycles per MAC.
Schema v9 publishes power.energy_per_inference_j and power.inferences_per_joule for on-device gated-inference summaries when the probe ran inferences and the run has no power error; power.per_inference_metrics_omitted names the errors otherwise. Earlier summaries lacked these fields for that scope, so comparing v8 and v9 reports an informative schema difference.
Schema v10 counts SQRT, RSQRT, ABS, and SQUARED_DIFFERENCE as one operation per output element in the heliaAOT analysis, matching the TFLite analysis. helia-aot models using these operators can have changed total operations and TOPS; MACs are unchanged.
Schema v11 renames the firmware gate interval in the power.window_observer_mismatch and power.window_clock_mismatch issue contexts from elapsed_us / elapsed_s to gate_elapsed_us / gate_elapsed_s, so one key name no longer means two intervals. elapsed_us in power.window_clock_ceiling and power.window_clock_frozen remains the whole firmware window. Measured values are unchanged.
Retained target configuration and CPU clocks
Section titled “Retained target configuration and CPU clocks”Saved-run mapping hpx-run-2 exposes target.identity as
hpx.target-config/1:sha256:<hex> when all seven retained facts are known:
board, device, load_address, clock_profile, cpu_hz, arena_location
and weights_location. The producer extension target_config exposes that flat
hash basis, per-field artifact/JSON-pointer sources and unavailable reasons.
Any unknown or invalid included fact makes the identity null; known fields stay
visible, and reason text is never hashed. Values come from the verified saved
result and compiled build, never current board definitions or installed pins.
This is an HPX configuration identity, not a board serial, silicon qualification, or equivalence to a HELX simulation target. It excludes arena capacity, workload, runtime/kernel/toolchain, firmware image, lifecycle, measurements and host paths. Relocation does not change it. Equal identities describe only these seven facts; comparison eligibility remains the consumer’s decision.
Latency bases retain timing-source clocks and add CPU-domain configured and
firmware-reported rates, with distinct sources. STIMER frequency is a timer fact,
not CPU frequency. The cpu_clocks producer extension (hpx.cpu-clocks, version 1)
preserves both rates and explicit reasons when unavailable; disagreements remain
visible. Firmware reports an assigned SystemCoreClock, not an independently
measured or calibrated frequency. If the timing source is CPU, its existing
CPU-domain clock entries are kept without duplicates. Failed captures do not gain
successful latency observations from these configuration facts.
The native saved-result and compiled-build formats are unchanged. This mapping version applies to saved run results; the separate profile mapping is unchanged.