Operators
Every supported operator with its data types, kernel dependencies and restrictions, generated from the converter itself.
The catalog answers “is this operator supported, for these data types”. Support also depends on tensor roles, shape, attributes and target, so a model name alone cannot say whether it lowers. The rest of this page covers what the catalog cannot: the checks that apply to every model, and what to do when a node does not convert.
What “supported” means
Section titled “What “supported” means”A node that remains after the graph rewrites converts when three steps succeed:
- The LiteRT parser produces an AIR operator with a canonical key such as
CONV_2D. - That key resolves to a registered operator class.
- The class accepts the node’s tensor and target conditions and prepares its lowering: a kernel call, inline arithmetic, a copy, an alias or an accelerator invocation.
A catalog row summarizes the class’s data types, dependencies and restrictions. It is not a guarantee of every type and shape pairing, nor of an optimized arithmetic path. How heliaAOT works explains how a kernel is chosen for a target; the kernel library’s target build remains part of deployment.
Check a model before you convert
Section titled “Check a model before you convert”- Compare the model’s operators, tensor roles and types with the catalog.
- Read
helia-aot platforms show <target>for the hardware assumptions. - Convert with the intended configuration and inspect any typed error. No board is needed.
There is no separate coverage report. Conversion checks each remaining node before emission, after the applicable graph rewrites. Success proves the lowering for that configuration; building and comparing outputs establish the rest. Testing and automation covers that.
The rules every model meets
Section titled “The rules every model meets”Each rule below is checked before code generation. Use the node and tensor the error names to find the unsupported form.
| Rule | Applies to | What is rejected |
|---|---|---|
| Static shapes | every output | An extent that only a run-time value decides, such as a RESHAPE target read from an input; it cannot be allocated statically. A dynamic signature that resolves from the concrete input shapes is a different case and converts. |
| No INT4 | every tensor | Any INT4 tensor: no operator takes one. A model with INT4 weights is rejected naming the first operator that uses one. Re-export with int8 weights; INT4 is tracked in #701. |
| Per-tensor quantization | every int8, uint8 and int16 tensor an operator takes as an input or produces as an output, a constant input such as a DEQUANTIZE source included; not weights, state or parameters, which follow the per-operator rules below | More than one scale or more than one zero point. The emitted code reads only the first scale and zero point of such a tensor. Internal ETHOS_U_STREAM_SPLIT pairs are the exception and may carry per-axis metadata. |
| Quantized dimension | a tensor with several scales | A quantized dimension the tensor does not have, which LiteRT refuses to load; some exports give a rank-1 bias dimension 3. |
| Zero point present | every tensor with a scale, weights and parameters included, any dtype | A scale without a zero point, which LiteRT refuses to load. |
| Zero point in range | every int8, uint8, int16 and int32 tensor: model I/O, weights, state and parameters such as a PRELU alpha | A zero point outside the dtype’s range (-128..127, 0..255, -32768..32767 or the int32 range). An int8, uint8 or int16 zero point becomes an int32 offset or constant; an int32 tensor’s zero point is never read, only range-checked. |
| Finite positive scales | every int8, uint8 and int16 tensor | A scale that is zero, negative, infinite or NaN once narrowed to float32, except the zero-scale cases below. A LiteRT file stores float32 scales; a model built through the Python API can carry wider values. |
| Empty tensors | every operator input and output | A tensor with no elements, unless the operator accepts empty tensors of that dtype: CONCATENATION, PACK, UNPACK and SPLIT in float16 and float32; SQUEEZE, RESHAPE and EXPAND_DIMS in float16, float32, int32 and int64; SQRT, AVERAGE_POOL_2D and MAX_POOL_2D in float32. |
| Empty pools | AVERAGE_POOL_2D, MAX_POOL_2D | An empty input height or width whose output is not empty there, or a batch or channel count that differs from the input’s or is zero. |
| Declared output shapes | every output the model declares fully static | A declaration that differs from the shape the operator computes from its inputs and options; the model is rejected rather than converted with the declared size. An output with no recorded rank is checked as a scalar. Convolution and pool outputs follow LiteRT’s SAME or VALID size; a transposed convolution’s output is its output_shape input. |
| Shape-rule operands | ADD, SUB, MUL, DIV, MAXIMUM, MINIMUM, SQUARED_DIFFERENCE, the comparisons, SELECT_V2, PACK, DILATE, SCATTER_ND, UNPACK | Operands no valid model has, as LiteRT rejects them when it checks the operator: inputs that do not broadcast, PACK inputs of different shapes, a DILATE dilation that is not rank 1, a negative SCATTER_ND extent, an UNPACK axis or count that disagrees with the input. |
| int32 extents | every tensor, empty ones included | A dimension, or a product of nonzero dimensions, that does not fit int32. Declared shapes are checked before any shape rule runs and inferred shapes after propagation. A kernel’s own range can still overflow on a product that fits, which its validator reports. |
Shape propagation runs before any transform or memory planning and checks each declaration as soon as the operator’s inputs are known. Some rules need more words than a cell.
Unquantized tensors. An int8, uint8 or int16 tensor with no scale, and a tensor of any other dtype even when it carries a scale, such as a float tensor with a calibration range, is unquantized: its zero point is read as 0, as LiteRT’s kernels read it.
The I/O table. The generated I/O table lists the first scale and zero point of each int8, uint8 or int16 model input and output that has a scale, and scale 1 with zero point 0 for any other tensor, int32 included, since LiteRT’s kernels read no quantization of such a tensor.
LiteRT’s interpreter differs: it reports the scale and zero point of any tensor with a scale, whatever its dtype, and 0 and 0 for a tensor without one.
Zero scales. A constant may have scale exactly 0, as an all-zero weight channel does. So may data moved unchanged from such a constant.
-
Moved data is the output of an operator that only moves, compares or selects stored values, when that operator is its only writer, the output is not a variable, and every int8, uint8 or int16 input it moves has scale 0 and is such a constant or such data.
-
The operators that forward a zero scale are BATCH_TO_SPACE_ND, BROADCAST_TO, CONCATENATION, DEPTH_TO_SPACE, DILATE, DYNAMIC_UPDATE_SLICE, ETHOS_U_STREAM_SPLIT, EXPAND_DIMS, FILL, GATHER, GATHER_ND, MAXIMUM, MINIMUM, MIRROR_PAD, PACK, PAD, RESHAPE, RESIZE_NEAREST_NEIGHBOR, REVERSE_SEQUENCE, REVERSE_V2, SELECT_V2, SLICE, SPACE_TO_BATCH_ND, SPACE_TO_DEPTH, SPLIT, SQUEEZE, STRIDED_SLICE, TILE, TRANSPOSE and UNPACK; PADV2 and SPLIT_V forward too, as they lower to PAD and SPLIT. The padding ones write padding as a raw stored value.
-
Moved zero-scale data may feed an operator input, a parameter such as a PRELU alpha, or weights. An operator that precomputes from its weights at conversion, such as int8 CONV_2D and DEPTHWISE_CONV_2D on MVE targets or UNIDIRECTIONAL_SEQUENCE_LSTM, rejects moved weights.
-
Any other operator output, and every model input and output, needs a nonzero scale, and an operator whose arithmetic needs a nonzero input scale rejects a zero one.
-
LiteRT’s kernels reject or abort on a zero input scale, so without
test.golden_datathe generated test cannot take its expected outputs from LiteRT; supply them.
Shapes without a rule. A rule that cannot compute a shape, such as a RESHAPE target or FILL dims computed at run time, checks nothing. These operators have no shape rule: ARG_MAX, ARG_MIN, ASSIGN_VARIABLE, ETHOS_U, GATHER_ND, GRU, READ_VARIABLE, RESIZE_BILINEAR, RESIZE_NEAREST_NEIGHBOR, SHAPE, SQUEEZE, SVDF, UNIDIRECTIONAL_SEQUENCE_LSTM and WHERE, whose output the importer derives from its condition. Their own validators still apply.
Convolution biases. A CONV_2D, DEPTHWISE_CONV_2D or TRANSPOSE_CONV bias holds at least one value per output channel, since the kernels read one per channel; a quantized DEPTHWISE_CONV_2D bias holds exactly one. A longer bias converts, and LiteRT’s default interpreter runs it the same way.
TRANSPOSE_CONV. It checks its declared output itself as well, and a
run-time output_shape must hold the declared shape: the generated module
computes the declared shape and does not read the value.
-
Input and output channels must match the weights, as LiteRT’s contract states. A model with fewer output channels than its weights is rejected: LiteRT checks that only in debug builds, and its kernels then disagree.
-
With both strides at most 2 and more input channels than the
TRANSPOSE_REVERSE_CONVthreshold (16 by default), the transform, enabled by default, rewrites an int8 TRANSPOSE_CONV as a convolution: no output-size limit beyond the shape rule, but constant weights. -
Otherwise the int8 kernel computes at most
(input - 1) * stride + max(kernel, stride)rows and columns, so a larger output, padding included, is rejected. A Keraspadding='valid'export exceeds it whenoutput_paddingis abovemax(stride - kernel, 0);padding='same'exports stay within it. -
A shorter output converts, even one that leaves an input row’s whole filter outside it. Float32 converts every output size.
Per-operator extras. An operator can require more: ranks, constant weights, symmetric zero points, an int16 zero point of 0 for a kernel that takes no offset, activation functions, broadcasting, and signed kernel dimension and context ranges.
- Read the requirements per tensor role: an int16 LSTM cell state does not imply int16 activations, and index or mask tensors can differ from data tensors. The precision guide has the per-role details.
- Per-channel weights, state and parameters follow each operator’s own restrictions; SVDF reads the first channel of a per-channel tensor as its reference kernel does.
The catalog is the summary and the converter’s validators are the authority.
Stateful models
Section titled “Stateful models”LSTM and SVDF keep their state in persistent storage that aot_model_init
resets and aot_model_run carries forward. The supported rolled GRU exposes its
state as a model input and output that the caller carries. Resource variables
use persistent storage. Stateful models
covers where the state lives, the rolled GRU hook, and how a test run carries
state between iterations.
Operator contracts
Section titled “Operator contracts”Some operators have a contract worth reading before you rely on them: INT32 comparisons and selection, floating-point DIV, NEG and SQUARED_DIFFERENCE, and the Ethos-U command-stream fence. They are collected in Operator contracts. Boolean operators has its own page.
When an operator is missing
Section titled “When an operator is missing”First try a supported export or graph formulation. If the operator is needed as is, supply a LiteRT parser and an AOT operator class under the same canonical key and register them through a Python registry context. Custom operators walks through it.
When the kernel is not the one you want
Section titled “When the kernel is not the one you want”No configuration field selects a kernel by name. Selection follows the operator’s conditions, the tensor types and the platform.
- To change the capabilities, register a complete custom target or a Python platform; capability fields on a registered target name are ignored. Keep those declarations truthful and record the kernel library’s CPU and build flags. See Targets.
- A Python customizer can replace an operator class with
allow_override=True; keep the validation and scratch sizing, and test the numerics. See replacing a built-in operator.
Rewriting the graph before it is lowered
Section titled “Rewriting the graph before it is lowered”AIR transforms run after loading and before operator resolution. Configure them
in the transforms block, see
Configuring a conversion, or register
a custom transform under its NAME in a Python registry context.
- A custom transform declares its options with
OPTIONS: a pydantic model or pydantic dataclass, orNoOptions, the default, which rejects every option key. Its option keys, and while it is enabled its option values, are checked like the built-in ones. OPTIONS = Noneaccepts free-form options the transform checks itself, and its errors propagate as they are. Any otherOPTIONSis aTypeError.- The operator list is the execution order. A transform must leave every operator after the producers of the tensors it reads, constants, persistent state and model inputs excepted, or the conversion stops and names the transform.
LiteRT model hooks, such as the rolled-GRU fusion, run earlier over the source model. Pick the stage that still holds the structure you need; a multi-subgraph hook and an AIR transform are not interchangeable.