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.
Prerequisites
Section titled “Prerequisites”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.
Which instrument
Section titled “Which 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.
How a power run works
Section titled “How a power run works”- 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.
Series supply wiring, the per-board GPIO pins for gate, state and GO,
Vref on JS220 and JS320, io_voltage, and the lock-step handshake.
Wiring a Power Monitor Click or an Adafruit breakout, choosing a shunt, what the on-device summary contains, and its failure modes.
The dedicated power firmware, window sizing and duration, reset strategies, and the advanced power floors.
The power block of summary.json, the health signals, the window
clock checks, bring-up diagnostics and troubleshooting.
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.
Expected output
Section titled “Expected output”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.
What next
Section titled “What next”Start with Joulescope setup and wiring if you have the instrument, or INA228 on the target if you do not.
Fixed-input captures
Section titled “Fixed-input captures”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.
Rail energy through the run API
Section titled “Rail energy through the run API”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 failedguard 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_secondsthat 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.
Optional fixture RAM diagnostics
Section titled “Optional fixture RAM diagnostics”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.