Skip to content
heliaAOT
Examples
HELIA HUB

Custom operator

Lower a LiteRT custom operator end to end: a parser that turns the node into AIR, an operator class that emits its C, and the one key that ties the two together. Nothing here patches the compiler; it all goes through a registry customizer.

Field Value
Model one node whose customCode is custom-noop, int8
Target apollo510_evb
Shows RegistryContext, a LiteRT parser, an AotOperator subclass, and the Python-only path a custom operator takes
CI Conversion and host compilation are declared in the examples CI job

The model is built by make_model.py as a controlled byte-copy fixture. No downloaded model is required.

Use a repository checkout and activate its environment after uv sync --frozen --group ci. The model-building script imports the repository’s Python helpers; an isolated CLI installation alone is not enough. See Running examples.

Terminal window
./run.sh

A second run stops with Output path ... already exists and exits 2, because the first run’s module is still in out/. convert.py prints a rejected conversion’s error and exits with its code, as the command line does. Run ./run.sh --force, which it accepts like the command line, to replace it.

There is no helia-aot call. The command line always builds the default, frozen registry, so a custom operator has no flag and no configuration key: convert.py is the entry point, and config.yaml is an ordinary conversion configuration it loads.

registry_context = build_default_registry_context(customizers=[customize_registry])
AotConverter(config, registry_context=registry_context).convert()

The fixture only adds a new key, so allow_override stays at its default, False, and the registrations do not pass overwrite=True. A customizer that replaced a built-in would make context construction fail; deliberate replacement needs allow_override=True, a separate and more permissive choice.

custom_noop.py derives the key from the customCode with custom_code_to_op_key, which replaces hyphens and spaces with underscores, then trims and uppercases, so custom-noop becomes CUSTOM_NOOP. The same string is used for the parser’s registry key, for the op_type the parser writes onto the AIR operator, and for the operator class’s registry key. A mismatch prevents the parser or emitter from resolving the custom node.

out/custom_noop/src/aot_custom_noop_0.c is what the operator class wrote:

int32_t
aot_custom_noop_0_run(aot_model_context_t *ctx)
{
const int8_t *__restrict input = (const int8_t *)ctx->tensor_ptrs[aot_tensor_0];
int8_t *__restrict output = (int8_t *)ctx->tensor_ptrs[aot_tensor_1];
if (input == output) { return 0; }
arm_memcpy_s8(output, input, 16);
return 0;
}

The fixture copies raw bytes; its equal-byte-count check is not a general proof of dtype or quantization compatibility. The supplied synthetic model uses matching int8 input/output metadata. When adapting it, validate the semantic contract instead of assuming every equally sized tensor is a no-op.

The module dispatches <prefix>_<name>_run and, when has_init is true, <prefix>_<name>_init. Operators without initialization can declare has_init = False; generated model code omits their init call. Tensors are reached through ctx->tensor_ptrs with the enumerator the module generated for that tensor, which is what lets the planner place the buffer wherever it likes.

The header the class writes keeps its #include outside the extern "C" guard, so a C++ consumer links against the C definition.

From here the next steps are a typed options class so the parser can carry a decoded payload, real kernel calls instead of the copy, and tests: one for parser dispatch, one for resolve and emit, and one full conversion with the customizer applied.

Conversion should create out/custom_noop/ with its model and custom-node sources. The host compile check compiles each generated translation unit against the selected headers. It does not link the firmware or verify numerical custom semantics. For numerical validation, provide trusted outputs or an independent implementation; label verification-skipped runs as smoke checks.