Skip to content
heliaAOT
Guides
HELIA HUB

Configuring a conversion

One configuration object, ConvertArgs, describes a conversion. You can write it as YAML, pass it as command-line flags, or build it in Python. All three reach the same validated object, and every field is documented in the Configuration reference.

Use a model that already converts on your chosen target. Keep the model path, target and output directory in one file before changing individual controls:

convert.yaml
model:
path: ./model.tflite
module:
path: ./out
name: my_model
platform:
name: apollo510_evb
memory:
dump_residency_json: true
Terminal window
helia-aot convert --path convert.yaml

A successful conversion writes out/my_model/, including the generated source, a README describing the model and its integration, and aot_residency.json. When residency reporting is enabled, the success log names the exported file, or the archive and its report member, after export completes. This location remains available after the temporary working directory is removed.

If a scratch tensor does not fit, the memory hint distinguishes each configured max_size budget from the target’s capacity, shows the bytes occupied by constant and persistent arenas, and states the scratch budget with byte units. That budget is not a guarantee that the tensor fits: live scratch, fixed offsets and alignment also affect the usable gaps. Keep the YAML file and any explicit CLI overrides with this output. Review configuration warnings and the placement report before building the firmware. If that module already exists, select a new output directory or use --force deliberately. Conversion success does not establish numerical correctness; follow Testing and automation before tuning.

A YAML file is passed with --path and must be a mapping. The command line is deep-merged over it, and only flags you actually typed take part: every option defaults to nothing rather than to the field’s default, so naming a flag is what overrides YAML, not accepting a default.

Terminal window
helia-aot convert --path convert.yaml --module.path ./out --force

The merge is per key, not per block, so a command-line --platform.memories entry replaces that one memory and leaves the rest of the YAML block standing. Nested fields become dotted, kebab-cased flags (--memory.allocate-arenas), booleans become a pair (--test.enabled and --no-test.enabled), and list options accept repeated values, space-separated values, or a bare flag meaning an explicit empty list.

In Python the equivalent is ConvertArgs.from_yaml(path), then AotConverter(config).convert().

Three sources can supply a value, and they are consulted in one order: a flag you typed, then the YAML file, then the field’s own default. Take this file:

convert.yaml
model:
path: kws_ref.tflite
module:
path: ./out
type: zephyr

and this command:

Terminal window
helia-aot convert --path convert.yaml --module.type nsx
Setting Flag YAML Default Value used Why
module.type nsx zephyr cmake nsx A flag you typed beats the file.
module.path not given ./out output.zip ./out The file beats the default.
module.prefix not given not given aot aot Nothing overrode the default.
module.schedule not given not given table table The same, and the reason a flag you did not type cannot silently reimpose a default.

Omitted flags do not replace YAML values. A wrapper script can override one setting while retaining the rest of the saved configuration.

An unknown key at the top level, in a nested block, in a list entry such as transforms[] or memory.constraints[], or under memory.tensors[].attributes fails validation. The command line exits 2; ConvertArgs.from_yaml raises InvalidConfigError, the ConfigValueError whose exit_code is 2; constructing ConvertArgs or calling ConvertArgs.model_validate raises a pydantic ValidationError that wraps it. The message names up to 20 unknown keys of the block, each with where it appeared and, when one is close, the nearest valid key, then counts the rest, and the hint lists the valid keys. A YAML file with planer: greedy under memory stops with:

error: 1 validation error while parsing arguments
memory: Value error, unknown key 'planer' (did you mean 'planner'?) in memory configuration (hint: Valid keys are allocate_arenas, constraints, dump_residency_json, planner, planner_options, tensors.)

The YAML file, the command line and the Python API check the same keys; only the exception that carries the error differs, as above. A removed key, such as memory.auto_hydrate_constants, fails instead with an error whose hint names its replacement. A deprecated setting is accepted with a warning that names its replacement, and so is the former key of a renamed setting, in every block, including transform options, planner options, operator attributes and tensor attributes. Each warning is printed once per conversion; the configuration reference marks both, and which settings are experimental.

memory.planner_options is checked against the selected planner, and each transforms[] entry’s name and options against the registered transform: the option keys always, and, for an enabled transform, the values, which must be of a type the option accepts (range limits are the transform’s own business). The platform is resolved too: an unknown name without the fields of a custom platform, a platform.memories override naming a memory the target lacks, and any platform, custom or overridden, with a negative memory size or a preferred_memory_order naming a memory its memories does not size are all rejected. An unknown name or absent override memory is reported first; otherwise negative sizes and missing preferred memories are reported together. The last two rules belong to SocPlatform itself, so constructing one from Python, as for register_platform, raises a ValueError for them too. A platform, registered or custom, that declares the FP16 capability on a core known to lack it, such as cortex-m4, is rejected once it has none of those errors. The command line does this after validation and before the conversion starts, and an error there also exits 2. For transforms it reports the first failing entry only, and for a rejected value the first error with a count of the rest. A name listed more than once is reported before any entry’s unknown name or option error, but after the errors validation finds in an entry itself, such as enabled: 3. Each transform, and "*", may appear once. operators[].attributes keys and values are checked against the operators each rule reaches; see Operator attributes. A custom transform takes no options unless its class declares OPTIONS; one that sets OPTIONS = None checks its own options, and its errors propagate as they are, so anything other than a ConfigValueError keeps its traceback.

A failed validation exits with code 2 and prints one message per error. A validation error is led by the field or block it concerns, except an unknown top-level key, which has no lead. When the transforms are built, their errors take three shapes: an unknown option key starts with unknown key and names transforms[NAME].options, a rejected value is led by transforms[NAME].options, and an unknown or repeated name is led by transforms[]. An unknown key ends validation of the block that holds it, and an unknown top-level key ends validation of the whole file, so other errors there appear only after you fix the key and rerun. memory.tensors[].attributes keys are the exception: they are checked only once the rest of memory validates, so an error elsewhere in memory is reported first.

Before converting, the command also checks what needs only the configuration and the target: the platform’s required fields, the operator attributes, the transforms, the planner options and memory.constraints. A mistake there exits 2 as well. A memory.tensors pin is judged on each tensor’s merged rules once the model is loaded, so a rule that a later rule overrides is not an error, and a rule that matches no tensor logs a warning that names it. A tensor pinned to a memory the target lacks, one memory.constraints leaves out, or a read-only memory for a tensor that must be writable (scratch, persistent, or a staging destination) raises InvalidConfigError and exits 2 too. So does every other setting the loaded model rejects: an out-of-range model.subgraph, a test.state_feedback pair that is out of range, mismatched or drives one input twice, a test.tolerance the output dtype cannot hold, golden data that is missing or does not match, a model file that is missing or cannot be read, an output location that cannot be written or already holds the module without --force, and a target that cannot run the model (float16 tensors without FP16 support, an Ethos-U operator without an NPU). A valid request that fails while converting, such as an operator no kernel applies to or memory that does not fit, exits 1. Each of these errors carries its code as exit_code, so a Python caller of ConvertArgs.from_yaml or convert() sees the same status the command line exits with for the same error. A file with more than one mistake can stop the two at different ones: the command line runs the checks above before it loads the model, and convert() checks only the operator attributes before it loads the model.

Block What it sets Reference
model Input path, subgraph index, name, description, version ModelArgs
module Output path, module type, name, prefix, schedule mode ModuleArgs
test Generated test case: tolerance, golden data, iterations, state feedback TestArgs
transforms Which graph rewrites run, and with what options TransformSpec
memory Planner, constraints, tensor rules, arena ownership, residency report MemoryArgs
platform Registered target name, or a complete custom target definition PlatformArgs
operators Attribute rules and optimization knob overrides applied to operators OperatorRuleset
optimization Goal, approximation gate and knobs; see Performance and accuracy options OptimizationArgs
documentation The experimental offline HTML site DocumentationArgs
top level verbose, force, keep_work_dir, log_file ConvertArgs

Attribute rules tune how the model is compiled and laid out without changing the graph. They come in two lists: operators at the top level, and memory.tensors.

A rule has three keys:

  • type - the entity type. For operators, an operator name such as CONV_2D; for tensors, a kind: constant, persistent or scratch. Types match in any case, so conv_2d and CONSTANT work too. * matches everything.
  • id - one entity id, or a list of them, as strings. An operator rule matches the operator’s node id or its stable id, <subgraph>:<output tensor name>, which <prefix>_plan.json records. Prefer the stable id: it comes from the model’s own tensor names, so it survives edits that shift operator positions. Node ids and tensor ids are numbered by the importer and by transforms, so a model change or a heliaAOT release can renumber them. A tensor rule matches the tensor id the residency report shows.
  • attributes - the settings to apply.

All matching rules are collected, sorted by specificity, then merged field by field in that order, so the more specific rule wins. Specificity, lowest first:

  1. neither type nor id (or type: "*" with no id), the catch-all
  2. type only
  3. id only
  4. both type and id, the most specific

A rule that names an id selects particular entities, so it outranks a rule that names only their type, wherever each appears in the file. Two rules of equal specificity are settled by position: the later one in the file wins. A rule that other rules override on every entity it matches has no effect at all, and the conversion logs one warning that names it, the rules that win and the entities, for operator and tensor rules alike. A rule that keeps any setting somewhere is in effect, and a catch-all rule never warns, so the default-plus-exceptions pattern below converts without a warning. The resolved attributes are not printed at any verbosity; check the result instead. Code placement shows only in the generated code: a code_placement: ITCM rule puts <PREFIX>_PUT_CODE_IN_ITCM on the operator’s run function in its header. <prefix>_plan.json records a few other choices under fixed: RSQRT lut_mode, HARD_SWISH use_lut and compat_variant, and the float weight_layout. A rule whose type or id matches no operator or tensor in the model logs a warning that names it, and the conversion continues; after an upgrade, check for these warnings, since a renumbered id no longer matches.

To make a per-node exception to a type rule, name the node’s id:

operators:
- type: "*"
attributes: { code_placement: MRAM }
- type: CONV_2D
attributes: { code_placement: ITCM }
- id: "1"
attributes: { code_placement: MRAM }

For a CONV_2D node whose id is "1", the id rule wins and code stays in MRAM; every other CONV_2D node gets ITCM. Rules merge individual attributes; they do not replace the whole attribute dictionary.

code_placement controls the generated operator run code. MRAM keeps the default placement; ITCM requests the target’s ITCM section. A value that is not a memory name is a configuration error. Initialization and the model schedule keep their default placement, and external kernel-library functions follow your linker script. See ITCM placement for module-specific macros, memory budgets and verification.

operators:
- type: "*"
attributes:
code_placement: MRAM
- type: FULLY_CONNECTED
attributes:
code_placement: ITCM

An operator rule can also carry optimization, which overrides the model-wide optimization knobs for the operators it matches, with the same precedence as attributes. The accumulation knob, for example, selects the direct precise entry for eligible packed FP16 fully connected layers, while fast retains its legacy routing. Ineligible layers keep standard weights. On FP16 depthwise layers fast selects the float16-lane _acc16 entry. See Performance and accuracy options for every knob, the goal and the approximation gate.

optimization:
accumulation: fast
operators:
- type: FULLY_CONNECTED
optimization:
accumulation: precise

To place scratch buffers, use memory.tensors rules with type: scratch and an appropriate memory attribute; the removed scratch_placement attribute is an error that says so. Operator code placement does not move model tensors.

Some operators take attributes of their own through the same mechanism. The operator catalog does not list them; these are all of them:

Operator Attribute Values Effect
RSQRT lut_mode per_op (default), universal INT16 only. per_op builds a table for each operator; universal shares one table between them and may differ from LiteRT by 1 to 2 LSB.
HARD_SWISH use_lut true (default), false INT8 only. Computes the activation through a lookup table.
HARD_SWISH compat_variant true (default), false INT8 without the table. Selects the kernel variant that matches heliaRT and TFLM.
ETHOS_U require_platform_npu true (default), false false allows a target that declares no NPU, such as a simulator that provides one.

An operator takes only the attributes in that table and code_placement. Before the model loads, each rule is checked against the operators its type can reach in the conversion’s registry: a rule naming a type against that operator, and a type: "*" rule against every operator. A key that none of them takes is an error that names the nearest valid key, so code_placment: ITCM is rejected rather than silently keeping code in MRAM. The key problems of every rule, or else each invalid value, are named in one configuration error. A type: "*" rule with no id applies each attribute to the operators that take it; a rule naming an id that sets an attribute its operator does not take logs a warning. A custom operator class can accept attributes it does not declare; see Custom operators.

Tensor rules control placement: memory selects the memory a tensor is read from, and constant_destination_memory selects a different runtime memory to stage a constant into. A tensor whose rules set no memory is placed by the planner, as if no rule matched it; for a constant staged by constant_destination_memory alone, the planner chooses the source memory. A value that is not a memory name is a configuration error, and so is constant_destination_memory on a scratch or persistent rule, since only constants are staged. Whether a destination can be staged into is decided after the rules merge, so a rule may name a read-only destination that another rule’s memory turns into a read in place.

memory:
tensors:
- type: constant
attributes:
memory: MRAM
- type: scratch
attributes:
memory: DTCM

Memory covers what the placements mean and when staging is worth it.

For a registered target such as apollo510_evb, set platform.name and use its declared capabilities and memory map. platform.memories replaces the sizes it names for that conversion; other platform fields do not override a registered target, and the converter warns and ignores them. To limit the memory available to this model, use platform.memories or memory.constraints.

An unregistered name requests a conversion-local custom target. It requires cpu, memories, preferred_memory_order and min_alignment; speeds is deprecated, and no conversion reads it. Set capabilities to what the hardware actually supports. Start from the Targets reference and review the whole definition. Declaring a capability cannot add it to the silicon. Target resolution occurs during conversion, after configuration construction.

Leave transforms absent for the built-in pipeline. All five passes are enabled and run in this order, using these exact registry names:

Name Purpose
FOLD_STATIC_SHAPE_EXPRESSIONS Fold supported shape-building expressions whose values are known at conversion time.
DEPTHWISE_TO_CONV Rewrite eligible single-input-channel depthwise convolutions as convolutions.
PRUNE_IDENTITY_OPS Remove supported operations that leave their inputs unchanged.
TRANSPOSE_REVERSE_CONV Rewrite eligible integer transpose convolutions as convolutions.
SCHEDULE_CONSTANT_DEQUANTIZE Run each DEQUANTIZE of a constant just before the first operator that uses its output.

SCHEDULE_CONSTANT_DEQUANTIZE matters for weight-only reduced-precision models, such as FP16 weights feeding FP32 kernels. Without it every weight is widened before the first layer runs and all the widened copies are live at once; with it each copy is live only around the layers that use it, so the planner can reuse that memory. The same operators run on every inference and the outputs are unchanged; the transform changes their order and therefore the memory plan, which can also move tensors between memories and change timing. Disable it to restore the model’s original order.

To isolate the effect of a rewrite, disable it explicitly, then reconvert, validate the outputs and compare the emitted module:

transforms:
- name: PRUNE_IDENTITY_OPS
enabled: false
options: {}

Names are case-sensitive; Python class names such as PruneIdentityOps are not registry names. A wildcard entry (name: "*") sets the default enabled state, and named entries override it. Changing list order does not reorder the pipeline. Put options on a named entry; options on the wildcard entry are an error.

DEPTHWISE_TO_CONV accepts output_channel_threshold (default 1) and TRANSPOSE_REVERSE_CONV accepts input_channel_threshold (default 16); the other built-in transforms take no options. A candidate must also satisfy the rewrite’s shape and dtype conditions. These thresholds control eligibility, not an assurance of faster inference.

module.schedule picks how _model_run invokes each operator.

  • table - a function-pointer table walked by a loop, with the per-node callback seam intact. Choose this when an RTOS or a profiler needs to observe or interpose on each node. This is the default.
  • static - a straight-line sequence of direct calls. No table, no loop, no indirect dispatch, and no callback seam. Choose this when you want the compiler to see the whole sequence.

Both modes emit the same public API, the same status codes, the same lifecycle latch and the same constant hydration. Only the dispatch differs.

Every conversion writes a LICENSE and a README.md into the module root. The README describes model I/O, arenas, operator distribution, library requirements and integration. It does not contain the conversion configuration as YAML; retain your configuration file and explicit CLI overrides separately.

documentation.html adds an offline HTML site to that:

documentation:
html: true

It writes a self-contained docs/index.html in the module root. Open the file in a browser or share it on its own; styles, scripts and the operator graph are embedded, so viewing needs no server or network connection. Python renders the report directly, without MkDocs, Node.js or a separate site build.

The report includes the model interface, execution composition, selected kernel entries, memory banks and arenas, integration instructions and conversion records. Charts summarize planned bytes and estimated MACs, not measured hardware usage or timing. The configuration section serializes validated configuration values; configured fields ignored during target resolution can still appear there. Resolved target facts appear separately.

The field is marked experimental in DocumentationArgs and defaults to false. README and JSON generation remain available when HTML is off.

verbose accepts 0 to 3. Levels 2 and 3 both enable debug logging; level 3 also expands AIR operator details. The Results summary remains visible at every level.

Level What you see
0 Errors and the Results summary; progress is suppressed.
1 Stage milestones. The default.
2 Debug details: the operators as loaded, the transforms applied, operator resolution, tensor lifetimes and planner diagnostics. Resolved attributes are not printed.
3 Debug output plus expanded AIR tensor and option details.

Set log_file to also write the log to a file. Durable results print to standard output; progress, logs and errors go to standard error, so a script can capture one without the other.