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.
Prerequisites
Section titled “Prerequisites”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.
Run it
Section titled “Run it”./run.shThere 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], allow_override=True,)AotConverter(config, registry_context=registry_context).convert()This checked-in script enables allow_override=True, which permits a
registration to replace a built-in. The fixture only adds a new key, so an
addition-only application should omit that argument to retain replacement
protection. Deliberate replacement is a separate, more permissive choice.
One key, three places
Section titled “One key, three places”custom_noop.py derives the key from the customCode with
custom_code_to_op_key, which trims, uppercases and replaces hyphens, 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.
What to look at
Section titled “What to look at”out/custom_noop/src/aot_custom_noop_0.c is what the operator class wrote:
int32_taot_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.
- Custom operators for the registries a context holds and what else they take.