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:
python -m pip install helia-aotFor a complete runnable script, jump to A complete example.
The surface
Section titled “The surface”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.
Provisional names
Section titled “Provisional names”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 anoperators[]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 listandplatforms show, and the memory and capability names thatplatformsettings hold (MemoryType,SocCapability) are promised.helia_aot.interpreters:AirInterpreterandcreate_interpreter.helia_aot.registry:RegistryBundle, whichbuild_default_registry_contextbuilds for you.RegistryContext,Registryandbuild_default_registry_contextare promised.
Building a configuration
Section titled “Building a configuration”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.
Running a conversion
Section titled “Running a conversion”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.
The views
Section titled “The views”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.
operatorsis in execution order. Withmodule.schedule: table, the generated operator callback reportsrun_startedandrun_finishedfor the operators in that order, and itsopargument is the operator’slayer_index, sooperators[op]is the operator a callback reports. typeis the name of the operator in the model file: a LiteRT builtin operator name, such asCONV_2DorLESS, or a custom operator’s canonical key. It names the operator the module runs, so a depthwise convolution thatDEPTHWISE_TO_CONVrewrites isCONV_2D.source_indicesis 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 aWHILEloop 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.macsis multiply-accumulates per inference, a fused activation not counted: one per weight for each output row forCONV_2D,DEPTHWISE_CONV_2DandFULLY_CONNECTED; one per weight for each input row forTRANSPOSE_CONV, and for aCONV_2Drewritten from one; one per output element for each element of the contracted dimension forBATCH_MATMUL; and 0 forAVERAGE_POOL_2DandMAX_POOL_2D.parametersis the elements of the constant weights and bias (aBATCH_MATMUL’s constant operands), before lowering repacks or folds them, a weight aDEQUANTIZEof a constant produces counted as that constant. Both areNonefor an operator type with no defined count.kindisinputfor a model input, otherwiseoutputfor a model output, otherwise the tensor’s storage:constant,persistentorscratch.input_indexandoutput_indexare its positions in the module’s input and output tables (ctx->inputs,ctx->outputs), so a tensor that is both keeps both roles.regionsuse the ids of the module’s<prefix>_arena_region_t, which<prefix>_bind_arenatakes and the residency report’sregion_iduses.symbolis the C object the module emits for a region, orNonewhenmemory.allocate_arenas: falseleaves the buffer to the application, andoffsetis the bytes from it to the region’s first usable byte.source_symbolholds a staged constant region’s bytes, startingsource_offsetbytes in, andsidecar_fileis the file an external-arena module writes a constant region’ssizebytes to.nbytesand a placement’ssizeare the bytes the memory plan allocated, the sizes the residency report lists.
A complete example
Section titled “A complete example”This converts a model, checks the conversion succeeded, and reads the memory layout back out of the residency report it wrote.
import jsonimport sys
from pydantic import ValidationError
from helia_aot import AotConverter, ConvertArgsfrom 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.
Extending the compiler
Section titled “Extending the compiler”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.
Reading the plan
Section titled “Reading the plan”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.