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.
Diagram
Section titled “Diagram”- 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
The blocks
Section titled “The blocks”Registry
Section titled “Registry”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.
The adapter protocol
Section titled “The adapter protocol”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.
State lifecycles
Section titled “State lifecycles”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).
What each adapter’s prepare() does
Section titled “What each adapter’s prepare() does”| 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.
Artifacts
Section titled “Artifacts”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.
Consumers
Section titled “Consumers”| 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 |
How the engine shapes the firmware
Section titled “How the engine shapes the firmware”| 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.
Where the code lives
Section titled “Where the code lives”| 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.
What next
Section titled “What next”Firmware generation for what the render context does with the artifacts; Adding an engine for the touch points a fifth adapter has to cover.