Profile and Power Pipeline Refactor Plan#
Goals#
Keep profiling and power measurement as distinct firmware products while giving them consistent host-side orchestration, checkpoints, error boundaries, and terminal reporting.
The design must:
- keep
hpx_profilerandhpx_profiler_powerseparate; - show useful progress before both firmware targets have built and run;
- preserve a linear, inspectable pipeline;
- pass typed, immutable outputs between major steps;
- keep power measurement free of transport/debug-domain overhead;
- enable diagnostics only after the measured GPIO window closes;
- recover useful status when GPIO observation fails;
- add narrow extension points without introducing a general event framework.
Non-goals#
- One firmware binary with profile and power modes.
- Multi-engine orchestration.
- Parallel profile and power execution.
- A persistent workflow engine, dependency graph, or plugin event bus.
- Making post-run telemetry a prerequisite for accepting a valid GPIO-gated measurement.
- Hiding build, flash, reset, or capture operations behind automatic retries.
Runtime products#
Profile firmware#
hpx_profiler owns PMU instrumentation and the selected capture transport. It
may consume substantially more code and RAM than power firmware. Its contract
is:
- initialize platform, transport, model, and PMU support;
- emit
HPX_STARTand startup metadata; - run clean timing and instrumented passes;
- emit structured PMU results and
HPX_END; - park until reset.
Power firmware#
hpx_profiler_power owns the minimal clean inference loop. It excludes PMU
storage, profile CSV machinery, and transport initialization from the measured
phase. Its contract is:
- initialize only the platform, model, timer, and GPIO synchronization needed for measurement;
- announce READY through GPIO and wait for GO when lockstep is enabled;
- raise GATE, execute the compiled fixed
N, and lower GATE; - after GATE is low, initialize the configured diagnostic transport;
- emit one terminal record and park.
Every recoverable power-firmware exit must converge on step 4. Error paths must force GATE low before enabling diagnostic/debug circuitry.
Host orchestration model#
Retain PipelineRunner as the sequential executor and PipelineContext as the
run-scoped carrier. Reduce the context's role as an unstructured state bag by
grouping major outputs into two immutable records.
@dataclass(frozen=True)
class ProfileRun:
firmware: FirmwareArtifact
deployment: DeploymentRecord | None = None
result: PmuResult | None = None
@dataclass(frozen=True)
class PowerRun:
plan: PowerRunPlan
firmware: FirmwareArtifact | None = None
deployment: DeploymentRecord | None = None
observation: PowerObservation | None = None
terminal: PowerTerminalRecord | None = None
PipelineContext continues to carry resolved platform, engine, model, and
memory information, plus profile_run and power_run. Existing path/result
fields can remain mirrored during migration and then be removed after callers
move to the grouped records.
Do not put methods that perform I/O on these records. They are immutable data passed between stages, reports, tests, and future API entry points.
Typed contracts#
Add only the contracts needed at major boundaries.
DeploymentRecord#
Records what was actually flashed rather than inferring deployment from a path.
@dataclass(frozen=True)
class DeploymentRecord:
firmware: FirmwareArtifact
target_id: str
deployed_at: str
RunCheckpoint#
A small value emitted when a durable or user-meaningful milestone is reached. This is not an event bus.
@dataclass(frozen=True)
class RunCheckpoint:
phase: Literal["setup", "profile", "power", "report"]
name: str
message: str
details: Mapping[str, object] = field(default_factory=dict)
PipelineRunner accepts one optional CheckpointSink callable. HpxConsole
implements the default sink. Tests can collect checkpoints in a list. Stages
emit checkpoints only for useful outcomes, not every internal operation.
The initial implementation uses the equivalent ProgressUpdate /
ProgressSink names because the same typed value represents transient status
and durable checkpoints. Rendering remains a console concern:
- default: current phase, stage, stage position, high-signal status, and ETA;
-v: stage timings plus durable checkpoints and outcomes;-vv: diagnostic details emitted withmin_verbosity=2through the same hook.
Stages must not format Rich markup or invent elapsed percentages. They may report real units (iterations, bytes, passes) and ETA only when backed by known work or measured timing.
Initial checkpoints:
- platform/model resolved;
- engine prepared;
- memory placement resolved;
- profile firmware built, with section sizes;
- profile firmware deployed;
- PMU capture complete, with layer count and clean latency;
- power run planned, with fixed
Nand expected runtime; - power firmware incrementally rebuilt;
- power firmware deployed;
- power GATE observed or GPIO fallback selected;
- terminal firmware status received;
- report paths written.
At default verbosity, checkpoints replace the inaccurate fixed-count progress bar and provide useful status before the second firmware is built. Verbose mode keeps stage timing lines and adds checkpoint details.
PowerObservation#
Separates instrument observation from firmware status.
@dataclass(frozen=True)
class PowerObservation:
mode: Literal["gpio_gated", "free_form"]
result: PowerResult
gate_rise_observed: bool
gate_fall_observed: bool
deadline_s: float
integrity: Literal["valid", "degraded", "invalid"]
A missing GPIO edge does not immediately raise. The capture backend continues sampling until the execution deadline and returns a free-form observation when possible.
PowerTerminalRecord#
Represents the post-GATE diagnostic protocol.
@dataclass(frozen=True)
class PowerTerminalRecord:
version: int
status: Literal["ok", "error"]
requested_count: int
completed_count: int
elapsed_us: int | None
final_phase: str
error_code: int
gate_asserted: bool
gate_lowered: bool
Use a versioned, line-oriented protocol with explicit start/end markers. Keep parsing separate from RTT/SWO/UART readers so the same record works across transports.
Target stage flow#
setup
preflight
ensure_board_powered
resolve_platform
resolve_jlink_probe
prepare_engine
analyze_model
plan_memory
profile
render_profile_firmware
build_profile_firmware
verify_profile_placement
deploy_profile_firmware
capture_profile
power (optional)
plan_power_run
render_power_firmware
build_power_firmware
verify_power_placement
deploy_power_firmware
capture_power_observation
collect_power_terminal
reconcile_power_run
report
generate_report
Rendering remains distinct from building. The initial profile build must not
compile the power target. After PMU capture determines N, the power source is
rendered and only hpx_profiler_power is built. This avoids doing hidden work
before the user receives profile status and makes each artifact's provenance
clear.
verify_power_placement is separate because power-only linking and late
transport support can change memory use independently of profile firmware.
Power diagnostic loop#
Firmware state machine#
BOOT
-> MODEL_INIT
-> READY
-> WAIT_GO
-> GATE_HIGH
-> RUN_FIXED_N
-> GATE_LOW
-> DIAGNOSTIC_INIT
-> TERMINAL_REPORT
-> PARK
Recoverable failures transition to a common finalizer:
FAILURE
-> FORCE_GATE_LOW
-> DIAGNOSTIC_INIT
-> TERMINAL_REPORT(status=error, phase, completed_N, code)
-> PARK
The finalizer must be idempotent. It must never raise GATE and must not enable transport before GATE is low.
Host state machine#
The host arms continuous power capture before reset, observes READY/GATE when available, and computes a deadline from the immutable plan:
planned_runtime = N * reference_inference_us
execution_deadline = boot_allowance + 2 * planned_runtime + fixed_margin
The exact multiplier and margins remain policy constants covered by tests, not firmware values.
Outcomes:
| GPIO observation | Terminal record | Result |
|---|---|---|
| Complete gate | Success | Accept gated result; terminal record corroborates N. |
| Complete gate | Error/missing | Preserve samples but fail reconciliation with explicit firmware-status error. |
| Missing edge | Success | Return free-form result with degraded integrity and GPIO diagnostic. |
| Missing edge | Error | Report firmware phase/error and retain diagnostic samples. |
| Missing edge | Missing at deadline | Treat target as stuck/catastrophic or diagnostic transport unavailable. |
The report must never present free-form energy as equivalent to a valid gated per-inference result. It may expose total trace statistics and clearly marked degraded estimates.
Error boundaries#
Each stage raises one typed error for its ownership boundary and attaches structured context where useful:
- engine preparation:
EngineError; - rendering:
FirmwareError; - build/deployment:
BuildError; - profile protocol/capture:
CaptureError; - instrument observation and terminal reconciliation:
PowerError; - output generation:
ReportError.
Add phase, artifact_role, and optional diagnostic details to errors without
creating a second exception hierarchy. The runner records the current stage and
last checkpoint before propagating the error to the CLI.
A failure should report:
- what completed successfully;
- which artifact was active;
- the last host checkpoint;
- the last firmware phase when available;
- the immediate corrective hint.
Extension touchpoints#
Keep extension points narrow and typed:
CheckpointSink: observe user-meaningful host milestones;PowerTerminalReader: read lines after GATE is low;PowerTerminalParser: parse the transport-independent terminal record;- existing engine adapters: produce engine artifacts;
- existing power drivers: collect samples and GPIO observations.
Do not expose arbitrary before/after-stage hooks. Add a new touchpoint only when two concrete implementations need it.
Delivery slices#
Slice 1: Honest artifacts and early progress#
- Stop building the power target in
BuildFirmwareStage. - Rename/clarify profile render/build/deploy stages.
- Introduce
ProfileRun,PowerRun, andDeploymentRecordwhile mirroring legacy context fields. - Add progress sink support and milestone console output. The typed hook, dynamic stage totals, phase display, verbosity filtering, and ETA rendering are implemented; artifact grouping and profile-only build separation remain.
- Keep existing capture behavior unchanged.
Status: progress hooks, phase/ETA rendering, profile-only initial builds,
EXCLUDE_FROM_ALL power-target declaration, stale-output rejection, and
artifact-only deployment boundaries are implemented. Immutable ProfileRun,
PowerRun, and DeploymentRecord state is now authoritative for runtime
orchestration, with strict role/provenance/order checks and stale-state
invalidation. Legacy result/artifact fields remain mirrored for reports and
the public API until Slice 4. RT KWS and AOT AD power hardware smoke tests pass.
Acceptance:
- non-power runs build only
hpx_profiler; - power runs show profile results before power rendering/build begins;
- existing software and profile hardware tests pass.
Slice 2: Power observation contract#
- Introduce
PowerObservation. - Split target preparation, instrument observation, and reconciliation inside the current power stage modules.
- On missing GPIO, continue to the plan-derived deadline and return free-form diagnostic capture rather than raising immediately.
- Preserve current valid-gate integrity checks unchanged.
Status: PowerObservation is implemented and authoritative in PowerRun.
Joulescope missing-rise/missing-fall cases retain whole-trace statistics as
explicitly degraded free-form observations when packets are available; empty
captures and duration-integrity mismatches remain hard failures. Reports,
console output, primary JSON, and hardware validation distinguish degraded
diagnostic data and suppress per-inference/TOPS efficiency metrics. Post-run
firmware terminal reconciliation remains Slice 3.
Acceptance:
- existing valid GPIO captures are numerically unchanged;
- missing-rise, missing-fall, short-pulse, and deadline tests have explicit outcomes;
- degraded results cannot produce unqualified per-inference energy.
Slice 3: Deferred firmware diagnostics#
- Add the common power-firmware finalizer to both interpreter and AOT templates.
- Initialize the selected diagnostic transport only after GATE low.
- Add the versioned terminal protocol and parser.
- Add
collect_power_terminalandreconcile_power_runstages.
Status: versioned PowerTerminalRecord parsing, late RTT initialization,
fresh retained-buffer reset, atomic terminal writes, non-reset J-Link attach,
target-specific RTT symbol resolution, explicit collect_power_terminal
reconciliation, report/console serialization, and common recoverable-error
finalization are implemented for dedicated RTT power firmware. UART, SWO, and
USB terminal collection remain skipped until equivalent post-GATE transports
are implemented. System-initialization failures before GPIO setup remain
catastrophic and are covered by the host deadline rather than terminal status.
The terminal path now uses a separate PowerTerminalTransport abstraction
rather than reusing profile capture transports, because terminal collection
must never reset the target. RTT, UART, SWO, and USB CDC adapters share one
collect(ctx, timeout_s) -> PowerTerminalEnvelope signature. All four are
hardware-validated on AP510 with matching fixed-N execution and energy.
PowerTerminalEnvelope can also carry an OnDevicePowerSummary for future
firmware-side monitors such as INA228. The payload uses integer base units and
requires source, fixed-N measurement scope, matching duration/count, overflow
state, and optional calibration/configuration identity. Internal mode skips
host-instrument capture and obtains its compatible PowerResult from this
terminal payload; external mode can retain both host observation and a
secondary on-device aggregate without conflating their provenance.
The shared types, parser, pipeline branch, and report conversion are complete;
the built-in ondevice driver remains rejected during planning until a real
firmware monitor producer emits the required envelope. A future INA228 adapter
must implement fixed-N monitor reset/start/stop, duration/count agreement,
overflow handling, and calibration identity before enabling that capability.
Acceptance:
- power during GATE remains statistically unchanged versus the transport-free baseline;
- success reports requested/completed
Nand elapsed time; - injected model-init and mid-loop failures lower GATE and report phase/code;
- disconnected GPIO with successful firmware returns degraded free-form status;
- a stuck firmware run fails at the calculated deadline.
Slice 4: Remove migration fields and update reports#
- Move reports and public API consumers to grouped run records.
- Remove mirrored firmware/result path fields from
PipelineContext. - Include checkpoints, deployment provenance, observation mode, terminal status, and integrity classification in JSON output.
- Update architecture and user power documentation.
Acceptance:
- no report schema silently changes; version new fields where needed;
- all existing CLI and API tests pass;
- profile-only and power-enabled hardware smoke matrices pass.
Validation strategy#
Every slice uses the cheapest local checks first, followed by full software validation. Hardware checks scale with risk:
- profile-only RT and AOT smoke after Slice 1;
- AP510 + JS320 valid/missing-GPIO cases after Slice 2;
- RT and AOT terminal success/failure injection after Slice 3;
- the existing KWS and AD power matrix after Slice 4.
Power acceptance compares current, gate duration, and energy per inference to a stored transport-free baseline. Late diagnostic initialization is acceptable only if it occurs after GATE low and does not alter measured-window statistics.