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.
Start with a repeatable conversion
Section titled “Start with a repeatable conversion”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:
model: path: ./model.tflitemodule: path: ./out name: my_modelplatform: name: apollo510_evbmemory: dump_residency_json: truehelia-aot convert --path convert.yamlA 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
--forcedeliberately. - Conversion success does not establish numerical correctness; follow Testing and automation before tuning.
YAML and the command line
Section titled “YAML and the command line”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.
helia-aot convert --path convert.yaml --module.path ./out --forceThe 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().
Which value wins
Section titled “Which value wins”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:
model: path: kws_ref.tflitemodule: path: ./out type: zephyrand this command:
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.
When a key is wrong
Section titled “When a key is wrong”The YAML file, the command line and the Python API check the same keys; only
the carrier of the error differs. 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.
| Mistake | What happens | Exit |
|---|---|---|
Unknown key, at the top level, in a block, in a list entry such as transforms[] or memory.constraints[], or under memory.tensors[].attributes |
Validation fails. 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; the hint lists the valid keys. An unknown key ends validation of its block, 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. | 2 |
Removed key, such as memory.auto_hydrate_constants |
An error whose hint names the replacement. | 2 |
| Deprecated setting, or the former key of a renamed one, in any block including transform options, planner options, operator attributes and tensor attributes | Accepted with one warning per conversion that names the replacement. The configuration reference marks both, and which settings are experimental. | 0 |
memory.planner_options key the selected planner does not take |
Rejected against the selected planner. | 2 |
transforms[] entry with an unknown name, a repeated name (each name, and "*", may appear once), an unknown option key, or for an enabled transform a value of a type the option does not accept |
The first failing entry is reported; for a rejected value, the first error with a count of the rest. A repeated name is reported before an entry’s unknown name or option, but after errors found in the entry itself, such as enabled: 3. Range limits are the transform’s own business. |
2 |
platform with an unknown name and none of the fields of a custom target, a platform.memories override naming a memory the target lacks, or any platform, custom or overridden, with a negative memory size or a preferred_memory_order naming a memory memories does not size |
The unknown name or absent override memory is reported first; otherwise negative sizes and missing preferred memories together. The last two rules belong to SocPlatform, so constructing one from Python, as for register_platform, raises ValueError for them too. |
2 |
memory.constraints naming a memory the target lacks, a max_size above the target’s capacity, or an empty list |
Rejected; the empty list fails validation on every carrier, the other two before the command line starts converting. | 2 |
platform, registered or custom, declaring the FP16 capability on a core known to lack it, such as cortex-m4 |
Rejected once the target has none of the errors above. | 2 |
operators[].attributes key or value that no operator the rule reaches takes |
A configuration error naming the nearest valid key; see Operator attributes. | 2 |
memory.tensors[].attributes key that is wrong |
Checked only once the rest of memory validates, so an error elsewhere in memory is reported first. |
2 |
memory.tensors pin 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) |
InvalidConfigError, judged on each tensor’s merged rules once the model is loaded. A rule a later rule overrides is not an error; a rule that matches no tensor logs a warning that names it. |
2 |
A 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 missing or mismatched, a model file missing or unreadable, an output location that cannot be written or already holds the module without --force, a target that cannot run the model (float16 tensors without FP16 support, an Ethos-U operator without an NPU) |
InvalidConfigError. |
2 |
| A valid request that fails while converting, such as an operator no kernel applies to or memory that does not fit | A typed conversion error. | 1 |
How the errors read:
- A failed validation prints one message per error, each led by the field or block it concerns, except an unknown top-level key, which has no lead.
- Transform errors take three shapes: an unknown option key starts with
unknown keyand namestransforms[NAME].options, a rejected value is led bytransforms[NAME].options, and an unknown or repeated name is led bytransforms[]. - A custom transform takes no options unless its class declares
OPTIONS; one that setsOPTIONS = Nonechecks its own options, and its errors propagate as they are, so anything other than aConfigValueErrorkeeps its traceback. - Each error carries its code as
exit_code, so a Python caller ofConvertArgs.from_yamlorconvert()sees the same status the command line exits with. - Before converting, the command line checks everything that needs only the
configuration and the target: the platform’s required fields, the operator
attributes, the transforms, the planner options and
memory.constraints. The tensor pins and the settings the loaded model rejects are judged once it is loaded, and a model file that is missing or unreadable is the first of those to fail. convert()checks only the operator attributes before loading, so a file with more than one mistake can stop the two at different ones.
A YAML file with planer: greedy under memory stops with:
error: 1 validation error while parsing argumentsmemory: 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 blocks
Section titled “The blocks”| 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 model report | DocumentationArgs |
| top level | verbose, force, keep_work_dir, log_file |
ConvertArgs |
Attribute rules
Section titled “Attribute rules”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 asCONV_2D; for tensors, a kind:constant,persistentorscratch. Types match in any case, soconv_2dandCONSTANTwork 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.jsonrecords; a tensor rule matches the tensor id the residency report shows.- 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.
attributes- the settings to apply.
Precedence
Section titled “Precedence”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:
- neither
typenorid(ortype: "*"with noid), the catch-all typeonlyidonly- both
typeandid, 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, 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.
- A rule whose
typeoridmatches 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. - The resolved attributes are not printed at any verbosity; check the result
instead. Code placement shows only in the generated code: a
code_placement: ITCMrule puts<PREFIX>_PUT_CODE_IN_ITCMon the operator’s run function in its header.<prefix>_plan.jsonrecords a few other choices underfixed: RSQRTlut_mode, HARD_SWISHuse_lutandcompat_variant, and the floatweight_layout.
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.
Operator attributes
Section titled “Operator attributes”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: ITCMAn operator rule can also carry optimization, which overrides the model-wide
optimization knobs for the operators it matches, with the same precedence as
attributes. See
Performance and accuracy options for every knob,
the goal and the approximation gate.
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, and on FP16 depthwise layers
fast selects the float16-lane _acc16 entry.
optimization: accumulation: fastoperators: - type: FULLY_CONNECTED optimization: accumulation: preciseTo 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
typecan reach in the conversion’s registry: a rule naming a type against that operator, and atype: "*"rule against every operator. - A key that none of them takes is an error that names the nearest valid key,
so
code_placment: ITCMis 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 noidapplies each attribute to the operators that take it; a rule naming anidthat 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 attributes
Section titled “Tensor attributes”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
memoryis placed by the planner, as if no rule matched it; for a constant staged byconstant_destination_memoryalone, the planner chooses the source memory. - A value that is not a memory name is a configuration error, and so is
constant_destination_memoryon ascratchorpersistentrule, 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
memoryturns into a read in place.
memory: tensors: - type: constant attributes: memory: MRAM - type: scratch attributes: memory: DTCMMemory covers what the placements mean and when staging is worth it.
Choosing a target
Section titled “Choosing a target”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.
Transforms
Section titled “Transforms”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, into RESHAPE targets, BROADCAST_TO shapes and FILL dimensions. |
FOLD_INVARIANT_VARIABLES |
Replace resource variables that provably never change with the constant they start at. |
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.
FOLD_INVARIANT_VARIABLES removes the persistent copy of a resource variable
whose value can never change.
- A variable qualifies when the main graph never assigns it, or when its every assignment stores a MAXIMUM/MINIMUM clip of the variable’s own read against constant bounds, under one quantization, that returns the initial value unchanged (the write-back a Keras weight constraint exports).
- Each read then uses the initial value as a constant in place, so the module
no longer copies it into persistent memory at initialization or out again
on every invocation. The report’s
folded_variableslists each one. - A variable that is a model input or output, whose read is a model input or output, or whose assignment is anything else is left as it is.
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.
Schedule mode
Section titled “Schedule mode”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.
The documentation block
Section titled “The documentation block”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 model report to that:
documentation: html: trueIt 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.
Local paths in the configuration section are shown relative to the
configuration file’s directory, or to the output directory when the conversion
had no configuration file. A path outside that directory shows only its file
name after .../, so the shown configuration names no other local directory.
The section’s <prefix>_config.yaml download, embedded in the same file, keeps
every path as the conversion read it, so a shared report still carries them;
loading it reproduces the conversion’s settings.
The field is marked experimental in DocumentationArgs and defaults to false. README and JSON generation remain available when HTML is off.
Verbosity and logs
Section titled “Verbosity and logs”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.