Skip to content
heliaPROFILER
User guide
HELIA

Power measurement

Measure the energy an inference costs, not just its cycles. hpx captures current, voltage and energy around a GPIO-gated clean window: the firmware runs a dedicated, uninstrumented inference loop and drives a sync GPIO high for exactly that window, while an instrument integrates charge and energy and the host reads the result. Two instruments are supported, and the guides below cover setup, measurement windows and verification.

A working PMU profile of the model (First profile), and one of:

  • a Joulescope JS110, JS220 or JS320 in series with the board’s supply, with at least one GPIO wire from the board to the instrument; or
  • a TI INA228 current monitor on the board’s own I2C bus, for whole-window energy without a bench instrument.
Joulescope (power.driver: joulescope, mode: external) INA228 (power.driver: ina228, mode: internal)
What it is A bench instrument in series with the supply, watching a GPIO gate A monitor chip on the target rail, read by the firmware over I2C
Streaming samples yes no; hardware accumulators
Whole-window energy yes, HPX accumulates instrument statistics over the selected gate yes, integrated on the chip
Current distribution (median, p95, p99) yes no
Host wiring series supply plus one to three GPIO wires none; target I2C
Powers and resets the target rail yes (relay, power-cycle recovery) no
Adds to the measured current no yes: the IOM that talks to the chip stays powered inside the measurement
Pick it when You need external current statistics and GPIO-gated energy You need on-target aggregate energy without a Joulescope

power.driver names the chip or instrument the firmware and host talk to. The registered drivers are joulescope and ina228 (src/helia_profiler/power/__init__.py); any other name raises PowerError with the list of available drivers. Use ina228 for an on-target measurement. Runs made with different instruments carry different measurement_scope values (gpio_gated_clean_window for the Joulescope, on_device_gated_inference for the INA228), and hpx compare omits power deltas between them.

A power runThe dedicated power binary runs a clean window; the instrument integrates it; the host verifies the window before it publishes a number.
  • Power firmwareno transport
    • Warm-up
    • READY, GOlock-step
    • Clean windowgate high
  • Instrument
    • Joulescopegate on INPUT0
    • INA228I2C, accumulators
  • Host
    • Window checkstwo clocks agree
    • summary.jsonpower block
Text description of the diagram
  • Power firmware: no transport
    • Warm-up
    • READY, GO: lock-step
    • Clean window: gate high
  • Instrument
    • Joulescope: gate on INPUT0
    • INA228: I2C, accumulators
  • Host
    • Window checks: two clocks agree
    • summary.json: power block

In order: hpx flashes a second, transport-free build of the firmware (power.firmware: dedicated, the default) that does model init, warm-up, the three-wire lock-step handshake and the gated clean window, then parks. With a Joulescope, the host watches the gate on INPUT0, sums the statistics packets that fall inside it, and divides the gated energy by the firmware-reported clean inference count. With an INA228, the firmware resets the chip’s accumulators, runs a fixed number of inferences, reads energy and charge back, and reports them through the same terminal record. Either way the host cross-checks the firmware’s own window clock against the instrument before it publishes per-inference numbers.

Two topics other pages link to by their old anchors live on the Windows page: the dedicated power firmware and AOT input preparation.

The power block’s every key, type and default is on the generated configuration reference; the pages above explain what the keys do.

summary.json gains a power block with measurement_scope, average and peak current, average power, the current percentiles (Joulescope), energy per inference, inferences per joule, and the health signals the verification page lists; the terminal prints a compact power table. The power examples provide configurations and explain how to read the output. Their validation sections identify which steps have captured evidence; they do not provide a validated power benchmark.

Start with Joulescope setup and wiring if you have the instrument, or INA228 on the target if you do not.

The run API builds fixed-input firmware without opening an instrument. hpx.capabilities().targets lists each board’s supported engines; combine that list with the engine/dtype capabilities when selecting a fixture. AP510B supports heliaAOT and LiteRT. Timing-only captures read the complete output and timing terminal. For measured rail energy, build with a FixturePowerSync method and run with a FixtureRailPowerRequest (Rail energy through the run API). The firmware signals READY after initialization and warmups, acknowledges GO, gates the entire restore-and-invoke batch, and signals DONE after publishing its terminal.

The host retains the statistics and both packed GPIO streams. One shared native sample-counter transaction identifies the selected gate, its integral and typed completion. Missing transitions, stream gaps, multiple eligible transactions or incomplete batch evidence cannot yield accepted energy. The caller-selected settle_seconds bounds startup and completion separately, each at most 60 seconds for rail measurements. Timing-only fixture captures accept an explicit finite budget up to 300 seconds; the ordinary 60-second caller default is unchanged. The station permit must cover that budget plus setup and readback. The timing terminal must also fit the requested budget. Flat fixture images are bounded by the canonical target application flash region, with full ELF segment, sink and image readback validation. Independent firmware and sensor clocks remain separate observations; a sample boundary uncertainty is not instrument calibration.

Results expose gross measured joules, joules per completed iteration, scope, instrument identity and retained evidence. Repeats, comparison thresholds and calibration acceptance belong to the calling experiment.

The run API measures one power route: the gross energy of the MCU rail over the timed restore-and-invoke loop, with nothing subtracted. It is admitted for heliaAOT and LiteRT builds on the apollo510_evb under Lifecycle.CARRY. HPX refuses any other engine, board, rail or lifecycle before it compiles or touches the board, and says why.

Compile the power firmware with the GPIO wiring, then capture with the instrument, the rail and the station’s authority over them:

import helia_profiler as hpx
wiring = hpx.FixturePowerSync(sync_gpio_pin=29, state_gpio_pin=36, go_gpio_pin=14)
method = hpx.FixtureMethod(hpx.FixtureTimingScope.RESTORE_AND_INVOKE, power_sync=wiring)
compiled = hpx.build(plan, fixture, method=method, lifecycle=hpx.Lifecycle.CARRY)
result = hpx.run_build(
compiled,
guard=station.board_guard,
jlink_serial=station.probe_serial,
evidence_dir=output_dir,
settle_seconds=60,
power=hpx.FixtureRailPowerRequest(
serial=station.instrument_serial,
rail="MCU",
io_voltage=1.8, # the AP510 EVB GPIO level
sync_gpio_pin=29,
state_gpio_pin=36,
go_gpio_pin=14,
),
power_guard=station.power_guard,
)
energy = result.energy # None when the capture failed

guard vouches for the board and probe; power_guard authorizes the instrument and rail (verify_power_device) and keeps checking ownership. These are refused before the board is touched:

  • request GPIO pins that differ from the build’s;
  • a build without wiring, or a power build run without a rail request;
  • a missing power_guard;
  • a settle_seconds that is not a finite number above 0, or is above 60.

build also refuses a method without power_sync for a plan frozen with power enabled, rather than building timing-only firmware.

result.energy holds:

  • the measured iterations, seconds, watts, amperes, joules and joules per iteration;
  • the rail and the instrument serial;
  • the hash-pinned raw instrument record;
  • completion: the protocol-2 READY, GO, gate and DONE sample IDs that bound the window.

result.identity.run.energy states the method: scope, rail, subtraction and completion protocol, with the requested rail also when the capture failed. Each run_build call is one capture; run repeats as separate calls into separate evidence directories. The plan sets the warmup and iteration counts. result.save(path, roots=..., build=...) saves the run with its raw instrument record pinned; its portable view reports energy per inference and mean power (A saved run result).

For a timing capture through a configured supply, pass power=hpx.FixtureBoardPowerRequest(driver, serial, rail) and a power_guard with a build compiled without power_sync. It enables the supply before the probe attaches or flashes, powers the board and measures no energy; a supply failure stops before the target is touched. The power_guard must authorize that instrument and rail. It is not limited to the energy route’s engines, boards or lifecycles, and its settle_seconds may reach 300. Pass no power for an independently powered timing station: no power driver is resolved or opened. hpx run takes no power options.

For a timing-only build, pass observe_ram=True to hpx.build; a build with power_sync refuses it. The option instruments the compiled image, so it cannot be enabled for an existing build at run time. It defaults to False.

Successful diagnostic captures include a hashed ram.json file in result.capture.artifacts. Read it through FixtureFile.read() to verify its digest. The JSON has schema_version: 1 and these quantities:

Field Meaning
stack_reserved_bytes Linked stack capacity in bytes.
stack_written_high_water_lower_bound_bytes Observed written stack depth in bytes; a lower bound, not an exact stack peak.
baseline_active_stack_bytes Active stack depth when painting began, in bytes.
heap_sbrk_backing_high_water_bytes Optional linked allocator backing maximum in bytes.
heap_system_memory_high_water_bytes Optional allocator system-memory backing maximum in bytes; overlaps the preceding counter.
heap_peak_bytes null: peak live heap payload is not measured.

Missing allocator counters are null, with a corresponding <field>_reason value of allocator_counter_not_linked. A present counter reporting zero is not missing evidence. heap_peak_reason is not_instrumented. A disabled or failed capture must not be interpreted as zero RAM usage.

The stack window covers model initialization, warmups and invokes, including diagnostic overhead and interrupts. It excludes boot/system initialization. Paint collisions and stack reservations without writes can underestimate depth. Allocator counters cover backing obtained since reset; they overlap and must not be added together or to an entire reserved heap region. The JSON also retains measurement_window, limitations and heap_counter_basis strings. These observations do not establish total peak RAM.

AOT arena painting is a separate hpx.build option. Keep observe_aot_arenas=False for carry-state workloads unless the selected emitted arenas are proven scratch-only and contain no initialized state. Static arena accounting does not require painting.

A saved build omits a disabled observe_ram; an enabled one records true.