Skip to content
heliaAOT
Guides
HELIA HUB

Python API in practice

helia-aot convert is a thin wrapper around a Python API. Everything the command line does, it does by building a ConvertArgs and handing it to an AotConverter, so anything you can express on the command line you can express in a script.

Use the Python API to compose conversions in a build script or notebook, sweep configuration values, or register custom parsers, operators and transforms. The CLI also supports scripted conversions, but registry customization requires Python; there is no CLI flag for loading a custom operator.

Install helia-aot into the environment running your script:

Terminal window
python -m pip install helia-aot

For a complete runnable script, jump to A complete example.

Only the symbols in the package’s API manifest are public, and each one at the one import path the manifest lists for it. Everything else is an implementation detail and may change without a release note. A subpackage is public only through the names listed for it: import helia_aot does not promise helia_aot.aot or any other module object.

Symbol What it is Import from
ConvertArgs The whole configuration of one conversion, as a validated model helia_aot
AotConverter The converter; construct it with a config and call convert() helia_aot
ConversionResult What convert() returns: the output, the prefix, the plan and the report, and with result_views the operators, tensors, regions and placements of the generated module helia_aot
OperatorView, TensorView, RegionView, PlacementView, QuantizationView The records of a result’s views of the generated module helia_aot.views
RegistryContext The registries a conversion dispatches through helia_aot.registry
build_default_registry_context Builds a context from the built-ins, with your customizers applied helia_aot.registry
HeliaAotError and its subclasses The typed failures a conversion raises helia_aot.errors
OptimizationPlan, OptimizationReport Readers for the plan and the report a conversion writes helia_aot.optimization
SocPlatform A target’s cores, memories and capabilities (provisional) helia_aot.platforms
get_platform, list_platforms Look up or enumerate the registered targets (provisional) helia_aot.platforms
AirModel The graph representation a conversion works in helia_aot.air
custom_code_to_op_key Derives the canonical operator key from a LiteRT customCode helia_aot.air
create_interpreter Builds an interpreter for reference execution (provisional) helia_aot.interpreters

The Python API reference is generated from the source and carries every signature, parameter and docstring, on the page of the module that defines each name. This page is about how the pieces fit together.

Some manifest names are listed as provisional: they stay importable, but a minor release can change or remove them. The manifest’s provisional lists are the record; in short:

  • helia_aot.air: everything but the core graph types (AirModel, AirOperator, AirOperatorOptions, AirTensor, AirOpType, AirTensorKind, AirTensorType, TensorId, OpId) and the key helpers (custom_code_to_op_key, canonical_op_key). The per-operator options classes, their enums, the quantization and sparsity details, the shape-rule results and the arithmetic helpers are provisional.
  • helia_aot.optimization: the knob and setting registries (KNOBS, SETTINGS, KnobSpec, SettingSpec, knob, knob_names, default_knob_values) and the measurement records (MeasurementRecord, load_measurements, measurements_for). The plan and report readers are promised.
  • helia_aot.memory: the planner’s output types (MemoryPlan, TensorAllocation, TensorBinding, TensorLifetime, ArenaUsage, ArenaRole). The configuration types (MemoryConstraint, MemoryPlannerType, TensorAttributes) are promised; read a plan through the residency report.
  • helia_aot.aot.operators: the custom-operator plugin surface (AotOperator, CtxBufUsage, OpCapability, register_default_aot_operators). OperatorAttributes, the attribute keys an operators[] rule sets, is promised.
  • helia_aot.platforms: the platform model and its registration API (SocPlatform, get_platform, list_platforms, list_platform_names, register_platform, resolve_platform). The registered target names, platforms list and platforms show, and the memory and capability names that platform settings hold (MemoryType, SocCapability) are promised.
  • helia_aot.interpreters: AirInterpreter and create_interpreter.
  • helia_aot.registry: RegistryBundle, which build_default_registry_context builds for you. RegistryContext, Registry and build_default_registry_context are promised.

ConvertArgs is a validated model, which gives you two ways in.

From a YAML file, which is the same file the command line reads:

from helia_aot import ConvertArgs
config = ConvertArgs.from_yaml("kws.yaml")

from_yaml raises InvalidConfigError, a ConfigValueError, when the file does not exist or cannot be read, is not YAML or not a mapping, or a key or value does not validate. Its message lists every problem the way the command line prints them, and for a rejected key or value pydantic’s ValidationError is its __cause__. Its exit_code is 2, the status helia-aot convert --path exits with for the same file. Operator attributes are not checked here: convert() checks them against its registry context before the model loads, so a file from_yaml loads can still fail convert() and the command line for an attribute key or value. The config also keeps the path it was loaded from, on its own path field.

From a dictionary, which is what you want when the configuration is computed:

config = ConvertArgs.model_validate(
{
"model": {"path": "kws_ref.tflite", "name": "kws_ref"},
"module": {"path": "./out", "name": "kws_ref", "type": "cmake"},
"platform": {"name": "apollo510_evb"},
"memory": {"dump_residency_json": True},
}
)

Configuration types and field constraints are checked at construction, so an unknown module type fails here. Target lookup happens during conversion: a plain platform.name string is not proof that a registered target exists. An unregistered name can instead request a custom platform when its required fields are supplied. The keys are the YAML keys; Configuration is the generated list of all of them with their defaults and their command-line spellings.

An unknown key fails construction or model_validate with a pydantic ValidationError (also a ValueError) that carries the ConfigValueError naming the key, where it appeared and, when one is close, the nearest valid key; except ConfigValueError does not catch it there, while it does for from_yaml. The check runs on construction and validation only, not when an attribute of a built configuration is assigned later. The configuration does not know the registered planners and transforms, or the registry’s operators, so convert() checks memory.planner_options, each transforms[] entry and each operators[] rule’s attributes and raises an unknown key, a transform option value its type rejects, an unknown transform name, a transform listed more than once, or an attribute key or value no operator the rule reaches takes there as InvalidConfigError itself.

from helia_aot import AotConverter
converter = AotConverter(config)
result = converter.convert()

AotConverter(config, registry_context=None) takes the configuration and, optionally, the registries to dispatch through. Omit the context and the converter builds the default, frozen one, which is exactly what the command line does.

convert() runs the six stages in order (Load, Transform, Resolve, Plan, Emit and Export) and either writes the module or raises. It returns a frozen ConversionResult:

Field What it holds
output The module directory (module.path joined with module.name), or the .zip or .pack archive that module.path names
prefix The module.prefix the module’s C symbols and file names start with
plan The OptimizationPlan the module holds as <prefix>_plan.json
report The OptimizationReport the module holds as <prefix>_report.json
work_dir The temporary work directory, with the generated sources under module.name, when keep_work_dir kept it; otherwise None
operators With result_views, an OperatorView per generated operator, in execution order; otherwise None
tensors With result_views, a TensorView per tensor, in the order of the module’s tensor enumeration; otherwise None
regions With result_views, a RegionView per arena region, where regions[i].id == i; otherwise None
placements With result_views, each tensor’s PlacementView, keyed by tensor id; otherwise None

The plan records each operator’s kernel and knob choices and the report its figures and hints; the plan and the report says which of their fields are promised. The graph, the memory plan and the operator objects a conversion works with are internal and not part of the result.

Set result_views: True in the configuration to have convert() describe the module it generated. It is a Python API setting, so it has no command-line flag. The views are frozen records built for tools such as profilers; they pickle, copy and hash, and the generated module is the same with or without them.

config = ConvertArgs.model_validate({"model": {"path": "kws_ref.tflite"}, "result_views": True})
result = AotConverter(config).convert()
for op in result.operators:
print(op.layer_index, op.type, op.source_indices, op.macs, op.kernel_calls)
View Fields
OperatorView layer_index, symbol, type, source_indices, input_ids, output_ids, local_tensor_ids, kernel_calls, macs, parameters
TensorView id, name, shape, dtype, ctype, quantization, kind, input_index, output_index, nbytes
RegionView id, memory, source_memory, role, offset, size, alignment, symbol, source_symbol, source_offset, sidecar_file
PlacementView region, offset, size, source_memory, staged
  • Operator order and the callback. operators is in execution order. With module.schedule: table, the generated operator callback reports run_started and run_finished for the operators in that order, and its op argument is the operator’s layer_index, so operators[op] is the operator a callback reports.
  • type is the name of the operator in the model file: a LiteRT builtin operator name, such as CONV_2D or LESS, or a custom operator’s canonical key. It names the operator the module runs, so a depthwise convolution that DEPTHWISE_TO_CONV rewrites is CONV_2D.
  • source_indices is the index of the operator in the model file, in the converted subgraph, the operator was parsed from. A fused activation belongs to its LiteRT operator and adds no index, a transform that rewrites an operator keeps its index, and an operator a transform removes appears in no entry; one a transform creates has none. A LiteRT model hook that reorders the subgraph or returns a copy of the model keeps every index. Each rolled-GRU fusion replaces a WHILE loop and the reshapes around it with one operator, which lists those three indices; an operator a hook creates without stating the indices it stands for has none.
  • macs is multiply-accumulates per inference, a fused activation not counted: one per weight for each output row for CONV_2D, DEPTHWISE_CONV_2D and FULLY_CONNECTED; one per weight for each input row for TRANSPOSE_CONV, and for a CONV_2D rewritten from one; one per output element for each element of the contracted dimension for BATCH_MATMUL; and 0 for AVERAGE_POOL_2D and MAX_POOL_2D. parameters is the elements of the constant weights and bias (a BATCH_MATMUL’s constant operands), before lowering repacks or folds them, a weight a DEQUANTIZE of a constant produces counted as that constant. Both are None for an operator type with no defined count.
  • kind is input for a model input, otherwise output for a model output, otherwise the tensor’s storage: constant, persistent or scratch. input_index and output_index are its positions in the module’s input and output tables (ctx->inputs, ctx->outputs), so a tensor that is both keeps both roles.
  • regions use the ids of the module’s <prefix>_arena_region_t, which <prefix>_bind_arena takes and the residency report’s region_id uses. symbol is the C object the module emits for a region, or None when memory.allocate_arenas: false leaves the buffer to the application, and offset is the bytes from it to the region’s first usable byte. source_symbol holds a staged constant region’s bytes, starting source_offset bytes in, and sidecar_file is the file an external-arena module writes a constant region’s size bytes to.
  • nbytes and a placement’s size are the bytes the memory plan allocated, the sizes the residency report lists.

This converts a model, checks the conversion succeeded, and reads the memory layout back out of the residency report it wrote.

convert_kws.py
import json
import sys
from pydantic import ValidationError
from helia_aot import AotConverter, ConvertArgs
from helia_aot.errors import HeliaAotError
try:
config = ConvertArgs.model_validate(
{
"model": {"path": "kws_ref.tflite", "name": "kws_ref"},
"module": {"path": "./out", "name": "kws_ref", "type": "cmake"},
"platform": {"name": "apollo510_evb"},
"memory": {"dump_residency_json": True},
"force": True,
}
)
except ValidationError as error:
print(f"invalid configuration: {error}", file=sys.stderr)
raise SystemExit(2) from error
try:
result = AotConverter(config).convert()
except HeliaAotError as error:
print(f"conversion failed: {error}", file=sys.stderr)
raise SystemExit(error.exit_code) from error
print("wrote", result.output, "with", len(result.plan.operators), "operators")
report = json.loads((result.output / f"{result.prefix}_residency.json").read_text())
if report["schema_version"] != 5:
raise ValueError("Review the residency schema before reading this report")
print("schema", report["schema_version"], "plan", report["plan_hash"])
for role, arenas in report["arenas"].items():
for arena in arenas:
print(role, arena["memory"], arena["used"], "of", arena["total_size"])

Run this with kws_ref.tflite available relative to the process’s working directory and heliaAOT installed in that Python environment. Success writes out/kws_ref/, which is result.output, and prints the operator count, the report’s schema, plan hash and arena sizes. A failure exits with the status the command line would exit with for the same error. The separate tensor_layout_hash identifies the tensor layout; see memory reports. This example deliberately uses a directory output; a .zip or .pack export contains the report inside the archive instead of at this filesystem path.

force is set because a conversion refuses to write into a directory that already holds that module; in a script that reruns, say so deliberately rather than deleting the tree blindly.

HeliaAotError catches typed conversion failures, whose subclasses also inherit matching standard-library exception types. Configuration construction raises Pydantic validation errors separately. Every typed error carries the exit_code the command line exits with when that error ends a command: its class declares it, and the Errors reference lists each class’s code. A missing golden archive raises GoldenDataNotFoundError, which inherits both HeliaAotError and FileNotFoundError, so existing file-error handlers remain compatible; its code is 2. From Python, an unknown transform name or a transform listed more than once raises InvalidConfigError (a ConfigValueError, and so a ValueError) from convert(), with code 2, as does any setting rejected on its own, against the model or against the target; a ConfigValueError for a valid request the model cannot be converted with, such as an operator no kernel applies to, has code 1. A golden archive that cannot be read or does not match the model’s tensors raises GoldenDataError (also a ValueError), and one that lacks a key raises GoldenDataKeyError (also a KeyError); both have code 2. A model file that is missing or cannot be read raises ModelLoadError (an OSError), and an output that exists without force raises OutputExistsError (a FileExistsError); both have code 2 too. Keep unexpected tracebacks visible rather than turning every exception into a successful script exit. Troubleshooting maps each class to what to do about it.

Every dispatch a conversion makes (the LiteRT parser for a node, the operator class for a key, the transforms that run, the model hooks that run before parsing) goes through a RegistryContext. Build one, apply your customizers, and hand it to the converter:

from helia_aot.registry import build_default_registry_context
registry_context = build_default_registry_context(
customizers=[customize_registry],
)
AotConverter(config, registry_context=registry_context).convert()

The three keyword arguments are the whole contract of build_default_registry_context:

Argument Default What it controls
customizers None Callables invoked with the context, in order, after the built-ins are registered
allow_override False Whether a registration may replace a built-in entry
freeze True Whether the context is frozen once the customizers have run

Leave allow_override at its default when your customizer is only meant to add: a registration that collides then raises instead of quietly shadowing a built-in. Leave freeze at its default too, so nothing mutates the registries once a conversion is under way.

Custom operators is the full walkthrough, including the one canonical key that has to match across the parser, the AIR operator and the operator class. A custom operator also relies on helpers and a C contract outside the API manifest, so custom-operator plugins are experimental in 1.x; a stable plugin API is tracked in #829.

Every conversion writes <prefix>_plan.json next to the module, with each operator’s kernel and knob choices, and <prefix>_report.json, which explains them. Read them with OptimizationPlan and OptimizationReport from helia_aot.optimization. They follow the files’ schema_version rule, so they read a file from any release with the same schema_version, older or newer: a knob, value or member they do not know is kept as the string written, and a field they do not know is ignored. A file with another schema_version raises a Pydantic ValidationError that names both versions.

from pathlib import Path
from helia_aot.optimization import OptimizationPlan
plan = OptimizationPlan.model_validate_json(Path("out/kws_ref/aot_plan.json").read_text())
for operator in plan.operators:
print(operator.op_type, operator.kernel.entry if operator.kernel else None)

The residency report carries the memory layout. Set memory.dump_residency_json and the conversion writes <prefix>_residency.json next to the module, carrying a schema version, the arena envelope per role, every tensor with its role, memory, offset and size, and two hashes over the layout. A hash change says the layout changed; matching hashes do not check the model’s weight values or numerical behavior. Its fields can change in any release (see Compatibility), so check its schema_version before reading it, as the example above does.

Gate on it the way you would gate on a linker map: fail when an arena exceeds what the board offers, and treat a hash change as something to review. Testing and automation covers using it in a pipeline and Memory reads one field by field.

Automatic reference execution failures raise ReferenceExecutionError (also a RuntimeError), not GoldenDataError. Catch it separately from rejected supplied golden data. Its details retains an available backend traceback; an unmarked Python worker defect remains an unexpected RuntimeError. No backend exception name alone establishes that the model is invalid.