# 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](https://github.com/AmbiqAI/helia-aot/blob/cf2246a7ad439fa38d57462118e7840e5497a685/examples/custom-operator/make_model.py) as a controlled byte-copy
fixture. No downloaded model is required.

## 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](https://ambiqai.github.io/helia-aot/examples/).

## Run it

```sh
./run.sh
```

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](https://github.com/AmbiqAI/helia-aot/blob/cf2246a7ad439fa38d57462118e7840e5497a685/examples/custom-operator/convert.py) is the entry point, and
[config.yaml](https://github.com/AmbiqAI/helia-aot/blob/cf2246a7ad439fa38d57462118e7840e5497a685/examples/custom-operator/config.yaml) is an ordinary conversion configuration it loads.

```python
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

[custom_noop.py](https://github.com/AmbiqAI/helia-aot/blob/cf2246a7ad439fa38d57462118e7840e5497a685/examples/custom-operator/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

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

```c
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.

## Next

- [Custom operators](https://ambiqai.github.io/helia-aot/guide/custom-operators/)
  for the registries a context holds and what else they take.
