Skip to content
heliaPROFILER
User guide
HELIA

Engine adapters

The registry selects an adapter, and the preparation stage obtains its typed artifacts through prepare() or, for a supplied prepared runtime, prepare_runtime(). Shared build stages consume those artifacts. Specialized model-analysis and frozen-plan code also consult engine-specific helpers; the import rule below prevents dependencies between adapters, not every engine-specific import elsewhere in hpx.

Engine dispatchFrom the engine.type key to the objects the rest of the pipeline reads. Adapters share helpers and hand typed artifacts to the build stages.
  • Registryengines/__init__.py
    • EngineType
    • adapter factories
    • get_adapter
  • Adapters
    • litert
    • helia_rt
    • helia_aot
    • executorch
  • Shared helpers
    • cmsis_nn
    • ethos_u
  • Artifactsengines/base.py
    • EngineArtifacts
    • LiteRtArtifacts
    • HeliaRtArtifacts
    • HeliaAotArtifacts
    • ExecutorchArtifacts
  • Consumers
    • firmware render
    • memory planning
    • workspace fingerprint
    • model analysis
Text description of the diagram
  • Registry (engines/__init__.py): EngineType, adapter factories, get_adapter
  • Adapters: litert, helia_rt, helia_aot, executorch
  • Shared helpers: cmsis_nn, ethos_u
  • Artifacts (engines/base.py): EngineArtifacts, LiteRtArtifacts, HeliaRtArtifacts, HeliaAotArtifacts, ExecutorchArtifacts
  • Consumers: firmware render, memory planning, workspace fingerprint, model analysis

EngineType is a StrEnum with four members, litert, helia-rt, helia-aot and executorch, so YAML and Jinja compare against the hyphen form without unwrapping. _ADAPTER_FACTORIES maps each member to a deferred import, so registering an engine does not import its package until get_adapter(engine_type) asks for it.

EngineAdapter in engines/base.py has a name, an engine_type, and prepare(config, work_dir) returning that engine’s artifact subtype. Three hooks let an engine shape memory placement: apply_arena_placement_override() for engines that emit typed arena regions; placement_config(), which returns the engine settings a requested placement implies (fixture builds apply it before hashing their inputs); and check_psram_placement() together with psram_weights_source, which is how the PSRAM preflight on Memory placement is enforced per engine.

A lifecycle says how a model’s internal state behaves across the invocations of one run. carry initializes the engine once and keeps its internal state through warmups and timed invocations, restoring only the fixed inputs each time. single invokes the model once, so no state carries between invocations. Each adapter declares the lifecycles its own firmware implements in lifecycles; require_lifecycle() refuses any other with the adapter’s lifecycle_refusal() reason. HPX never inspects the model graph to decide. LiteRT, heliaRT and heliaAOT implement both. ExecuTorch implements single only, because its firmware reloads the program on every invocation. Further lifecycles, such as resetting state before each invocation, arrive per engine as each firmware implements them.

Each adapter also states its instance_boundary (boot, or invocation for ExecuTorch) and answers first_invoke_initializer(): whether the first invocation of an instance runs the model’s one-time initializer. LiteRT and heliaRT run a CALL_ONCE subgraph inside the first Invoke(), so the answer is whether the model has one; heliaAOT runs it in model_init(), so false; ExecuTorch answers unknown. Run identities record both, with where the first invocation falls (see Inference and layer timing).

Engine Module Steps Modules it adds
litert engines/litert.py resolves the nsx-tflite-micro module and the reference or CMSIS-NN backend nsx-tflite-micro (plus the CMSIS-NN provider for that backend)
helia-rt engines/helia_rt/ validates the variant and toolchain; with no override returns the pinned registry module; source_path installs a local tree as a module; dist_path or a release source verifies the matching core and toolchain archive and generates a wrapper (nsx_module.py, artifacts.py, download.py) nsx-helia-rt
helia-aot engines/helia_aot/ maps the board to a compiler platform and runs the helia-aot compiler (compile.py); reads the operator manifest, arena regions and memory plan from the compiler’s result views (manifest.py); vendors the generated model as a local module named by engine.config.module_name (default hpx_model); resolves the CMSIS-NN provider nsx-cmsis-nn, the model module
executorch engines/executorch.py resolves the nsx-executorch checkout at the commit the compatibility baseline pins (auto-cloned into the hpx cache, or engine.config.source_path); reads the optional <model>.pte.json sidecar for the provider, op lists, planned arena and I/O sizes; resolves the region of each static RAM buffer; generates a wrapper module and emits the provider module before it the CMSIS-NN provider, the nsx-executorch wrapper

The shared helpers sit beside the adapters: engines/cmsis_nn.py resolves the nsx-cmsis-nn module reference and its CMake variables for every engine that links it, and engines/ethos_u.py holds the NSX module identity of the Ethos-U backend. Support for each engine and backend per SoC is a registry question, answered on Engines and Boards and targets, not here.

EngineArtifacts carries what every engine produces: engine_type, engine_header (no default; each adapter states its own), extra_modules, cmake_vars and memory_plan, plus two preparation facts any engine may report: source (the installed package that generated the model code) and report_files (files the engine wrote describing the preparation, which fixture builds pin as evidence). Engine-specific fields live on the subclass, so reading another engine’s field is an AttributeError at the access site rather than a silent None; each subclass pins its engine_type and raises ValueError if constructed with another. Four resolved_* properties (backend, version, variant, toolchain_tag) are how the dependency workspace fingerprint records engine identity without knowing which engine it holds. tests/contracts/test_engine_artifact_types.py asserts the adapter-to-artifact map is complete over EngineType.

Consumer Reads Module
firmware render engine_header, cmake_vars, extra_modules, the subtype’s fields (AOT operator manifest and arena regions, ExecuTorch buffer sizes) firmware/context.py, firmware/project.py
memory planning memory_plan, the adapter’s placement hooks stages/plan_memory.py, placement.py
workspace fingerprint the resolved_* properties deps/dependencies.py
model analysis the engine’s view of the graph for hpx analyze --engine evaluation/engine_analysis.py
Engine Main template Module graph on top of the board’s starter profile
litert main.cc.j2 nsx-tflite-micro, with the CMSIS-NN provider first for that backend
helia-rt main.cc.j2 nsx-helia-rt
helia-aot main_aot.cc.j2 nsx-cmsis-nn, then the generated model module
executorch main_executorch.cc.j2 the CMSIS-NN provider, then nsx-executorch

The provider is ordered before nsx-executorch on purpose: NSX must configure exactly one CMSIS-NN provider before the runtime creates its bridge target. The binary section sizes in summary.json record what the engine choice cost in flash and RAM for the run at hand.

Block Path under src/helia_profiler/
registry engines/__init__.py
protocol and artifacts engines/base.py
adapters engines/litert.py, engines/helia_rt/, engines/helia_aot/, engines/executorch.py
shared helpers engines/cmsis_nn.py, engines/ethos_u.py, engines/semver.py
stage stages/prepare_engine.py
contract test tests/contracts/test_engine_artifact_types.py

tests/test_package_layout.py classifies every engine Python module and checks absolute and relative imports, including lazy imports. Adapters may use their own internals and shared helpers; only the registry imports across adapters. Classify a new adapter or helper there when adding it.

Firmware generation for what the render context does with the artifacts; Adding an engine for the touch points a fifth adapter has to cover.