Skip to content
heliaPROFILER
User guide
HELIA

Adding an engine

Walk a contributor from an NSX module for a runtime to hpx profile --engine your-engine building, flashing and profiling firmware with it. Every step names the file it touches and the test that holds it to the contract.

Touch points for a new engineThe eight places a new engine changes, grouped by whether they are code, firmware or verification. The pipeline, capture and report packages are untouched.
  • Python
    • artifact typeengines/base.py
    • adapterengines/your_engine.py
    • registry entryengines/__init__.py
  • Firmware
    • template childmain_your_engine.cc.j2
    • template selectionfirmware/__init__.py
  • Verification
    • contract teststhree pinned inventories
    • adapter tests
    • user guide page
Text description of the diagram
  • Python: artifact type (engines/base.py), adapter (engines/your_engine.py), registry entry (engines/__init__.py)
  • Firmware: template child (main_your_engine.cc.j2), template selection (firmware/__init__.py)
  • Verification: contract tests (three pinned inventories), adapter tests, user guide page

You need an NSX module or source tree for the runtime, a way to run inference that can be instrumented per layer, and the Engine adapters page. Per-layer invocation is the hard requirement: PMU counters are read between layers, so a runtime that only exposes whole-model inference needs per-layer hooks added, or is profiled at whole-model granularity with that limitation documented.

Add an EngineArtifacts subclass in src/helia_profiler/engines/base.py. The base carries only what every engine produces; everything engine-specific goes on the subclass, required (no default) wherever the adapter always sets it, so consumers read it without a fallback:

@dataclass(frozen=True, kw_only=True)
class YourEngineArtifacts(EngineArtifacts):
"""YourEngine adapter outputs."""
engine_type: EngineType = EngineType.YOUR_ENGINE
your_engine_blob_name: str
your_engine_scratch_size: int
your_engine_manifest: list[dict[str, Any]] | None = None
_PINNED_ENGINE_TYPE: ClassVar[EngineType | None] = EngineType.YOUR_ENGINE

_PINNED_ENGINE_TYPE makes construction with any other engine_type a ValueError. If the engine resolves a backend, version, variant or toolchain tag, override the matching resolved_* property: those four are how deps/dependencies.py puts engine identity into the workspace fingerprint, and a value not surfaced there does not invalidate a cached workspace when it changes. Then add the pairing to ARTIFACT_TYPE_FOR_ENGINE in tests/contracts/test_engine_artifact_types.py; that map is asserted complete over EngineType.

Create src/helia_profiler/engines/your_engine.py implementing the EngineAdapter protocol: name, engine_type, prepare(config, work_dir), and the placement hooks (apply_arena_placement_override() as the identity unless the engine emits arena regions, check_psram_placement() for engine-specific PSRAM constraints). prepare() validates config.engine.*, creates any local NSX module under work_dir, and returns the artifact type fully populated:

def prepare(self, config: ProfileConfig, work_dir: Path) -> YourEngineArtifacts:
extra_modules = [
NsxModuleRef(
name="your-engine-module",
path=Path(),
local=False,
project="your-engine-project",
),
]
return YourEngineArtifacts(
engine_type=EngineType.YOUR_ENGINE,
extra_modules=extra_modules,
cmake_vars={"NSX_YOUR_ENGINE_OPTION": "value"},
engine_header="your_engine/api.h",
your_engine_blob_name="blob.bin",
your_engine_scratch_size=32 * 1024,
)

Three rules: return only the modules the engine adds (the board’s starter profile supplies the base set); state engine_header (TFLM’s if the firmware runs TFLM’s interpreter, as heliaRT does); and be idempotent, so two calls with the same inputs produce the same output.

In engines/__init__.py, add the EngineType member, a deferred factory function that imports the adapter module, and the factory’s entry in _ADAPTER_FACTORIES. get_adapter() needs no change.

If the engine runs through the interpreter path, main.cc.j2 already serves it. Otherwise write firmware/templates/main_your_engine.cc.j2 opening with {% extends "_main_base.cc.j2" %}; never a standalone main. The skeleton owns boot, the transport preamble, GPIO sync, the clean window, the PMU pass loop and teardown, and a standalone template drifts from it and loses features silently. Read the base’s prelude first: the render environment has trim_blocks and lstrip_blocks off, so the whitespace shape of every override is part of the contract.

Override the twelve required blocks (engine_file_header, engine_includes, engine_globals, engine_heartbeat_arm, engine_invoke, engine_iteration_setup, engine_pass_init, engine_print_csv, engine_profiler_off, engine_reset_inputs, engine_reset_inputs_warm, engine_start_metadata). Override an optional seam only where its default is wrong for the engine. engine_pmu_storage_sram_resident is a value seam that renders true (per-layer storage in shared SSRAM, the default) or false (storage in TCM); any other value is a render error. engine_clean_window deserves the most thought: its default brackets engine_invoke() with the window clock, which is right whenever the invoke is the inference (heliaRT, TFLM). ExecuTorch overrides it because its profiled call reloads the model and reports an execute-only cycle count, and heliaAOT enables input restoration inside the window. Whoever overrides it owns everything nested in it (engine_window_prologue, engine_window_restore, engine_profiler_on); overriding those separately as well is rejected by the block test.

Selection lives in _main_template() in firmware/__init__.py, which generate_app() and render_power_source() both call: main_aot.cc.j2 for EngineType.HELIA_AOT, main_executorch.cc.j2 for EngineType.EXECUTORCH, main.cc.j2 for everything else. Add the engine’s type there, and raise for power_only if the engine has no dedicated power binary (as ExecuTorch does).

  • tests/contracts/test_template_blocks.py: add the file to CHILDREN and the override set to test_child_override_sets_are_the_documented_ones.
  • tests/contracts/test_firmware_render_snapshots.py: add the engine to _ENGINES, give _render() a branch that renders the template with the variables production hands it (a missing branch raises rather than rendering main.cc.j2), narrow _ENGINE_SOCS if it does not run on every family, add it to _MATRIX_ENGINES only if it supports the dedicated power binary, then regenerate with HPX_UPDATE_SNAPSHOTS=1 and review the JSON diff.
  • tests/contracts/test_engine_artifact_types.py: the map from step 1.

Create tests/test_your_engine.py on the pattern of tests/test_tflm_adapter.py: prepare() returns the artifact type with the pinned engine_type and the expected module names, and creates any local module directory it promises.

Add the engine to Engines: what it is and when to use it, what it needs installed, its engine.config keys and its known limits. An engine without a page is not shippable.

  • artifact subclass with engine_type pinned and any resolved_* override
  • adapter implementing EngineAdapter; prepare() returns the subclass fully populated
  • ARTIFACT_TYPE_FOR_ENGINE entry
  • EngineType member and factory registered
  • template child of the skeleton with the required blocks, or the interpreter path documented
  • template selection in both render functions
  • block, snapshot and adapter tests green
  • user guide page
  • one end-to-end run on hardware with a real model
Block Path
artifact type src/helia_profiler/engines/base.py
adapter src/helia_profiler/engines/your_engine.py
registry src/helia_profiler/engines/__init__.py
template child src/helia_profiler/firmware/templates/main_your_engine.cc.j2
template selection src/helia_profiler/firmware/__init__.py
contract tests tests/contracts/test_engine_artifact_types.py, tests/contracts/test_template_blocks.py, tests/contracts/test_firmware_render_snapshots.py
adapter tests tests/test_your_engine.py

Firmware generation for the skeleton the template extends; Data capture for the stream the firmware has to print.