ONNX import¶
Load an ONNX model and run it on the Apple Neural Engine. The importer targets the CNN-classifier op subset (the ops a torch-exported ResNet/VGG/MobileNet emits), builds an ANEForge graph from the ONNX node list, and compiles it through the e5rt path.
API¶
Two entry points, both top-level (import aneforge as af):
import aneforge as af
import numpy as np
net = af.load_onnx("resnet18.onnx") # import + compile -> runnable Model
y = net(np.zeros((1, 3, 224, 224), np.float16))
af.load_onnx(path, fuse_attention=False, compile_kwargs)- import and compile to a runnableModel.fuse_attention=Truerewrites thesoftmax(Q@K^T*scale [+ causal mask])@Vpattern ontoaf.sdpa; a causal mask routes to the native fused-attention layer (a graph cut), so GPT-style decoder attention runs on the dedicated SDPA hardware. Extra keyword arguments pass straight toaf.compile(target=,opt=,compress=, ...).pathis a filename or an in-memoryonnx.ModelProto.af.onnx_to_tensor(path)- import only, returning(graph_inputs, output)ANEForge tensors so you can inspect or splice the graph before compiling it yourself.af.onnx_to_features(path)- import a classifier and return(inputs, features)wherefeaturesis the input to the final linear layer. Compile it as a frozen feature extractor and train a fresh head on the ANE for transfer learning (seeexamples/onnx_finetune.py).
An end-to-end example (export a torchvision model, import it, validate against
onnxruntime) is in examples/onnx_import.py:
python3 examples/onnx_import.py [model.onnx].
Supported operators¶
The importer raises NotImplementedError("ONNX op 'X' not supported") for anything
outside this set, so an unsupported model fails loudly with the offending op named.
| Category | ONNX ops |
|---|---|
| Activations | Relu, Sigmoid, Tanh, Clip ((0,6)->relu6, (0,inf)->relu), Elu, Selu, Celu, Mish, Softsign, ThresholdedRelu, LeakyRelu, PRelu, Gelu (exact + tanh-approximate), Erf, HardSigmoid, HardSwish |
| Elementwise | Add, Sub, Mul, Div, Pow, Exp, Log, Sqrt (constant operands supported), Abs, Neg, Sign, Reciprocal, Sin, Cos, Atan, Softplus, Floor, Ceil, Round, Min, Max, Sum, Mean (variadic), Where, Tile, Shrink |
| Comparison / logic | Equal, Greater, GreaterOrEqual, Less, LessOrEqual, Not |
| Convolution / pooling | Conv, MaxPool, AveragePool, GlobalAveragePool, GlobalMaxPool |
| Linear | Gemm (full alpha/beta/transA/transB), MatMul, Einsum (matmul-reducible equations) |
| Attention | Attention (opset 23) - 4D [1,H,S,D], onto the native fused-attention layer |
| Normalization | BatchNormalization, InstanceNormalization, LayerNormalization, GroupNormalization, RMSNormalization, LpNormalization (p=1/2) |
| Shape / layout | Reshape, Flatten, Transpose, Squeeze (incl. squeeze-all), Unsqueeze, Concat, Split, Expand, SpaceToDepth, DepthToSpace, Slice (step 1; step -1 as a full-axis flip), Shape, Trilu, Pad (constant/edge/reflect/wrap) |
| Normalization (cross-channel) | LRN |
| Resampling | Resize (nearest / linear) |
| Reduction | ReduceMax, ReduceMin, ReduceSum, ReduceMean, ReduceL1, ReduceL2, ReduceLogSum, ReduceLogSumExp, ReduceSumSquare, CumSum |
| Indexing | Gather (static indices), ArgMax, ArgMin, TopK (2D, values only) |
| Quantization | DequantizeLinear, QuantizeLinear (QDQ int8) |
| Misc | Softmax, LogSoftmax, Constant, ConstantOfShape, Range, EyeLike, Identity, Dropout (inference no-op), Cast (import-level), OneHot (constant depth/values) |
| Control flow | If (constant condition), Loop (static trip count, unrolled) |
| Recurrent | LSTM, GRU, RNN (unrolled; forward/reverse/bidirectional, default activations) |
Export at opset_version=13 with constant folding on (the default), which resolves the
Shape/Gather/dynamic-Reshape plumbing into static initializers before import.
Caveats¶
- fp16 compute. ANEForge casts weights to fp16 at compile and computes in fp16, so the match against onnxruntime (fp32) is a cosine-similarity check, not bit-exact. A torch-exported ResNet-18 matches onnxruntime at cosine ~0.99999 end to end.
- fp16 input.
af.inputis fp16, so feed an fp16 array (cast withx.astype(np.float16)). - Static shapes only. Every program compiles for a concrete shape; a dynamic or symbolic dim raises at import. Data-dependent value ops (on-engine gather / index by tensor data) have no ANE path - see capabilities.md.
- NCHW. Channels-first layout, matching ONNX's convolution convention.
- Uniform/symmetric conv params. Per-axis strides, dilations, and pads must be uniform and symmetric; a non-uniform value raises rather than mis-lowering.
Limitations¶
These attribute forms fall outside the supported subset and raise rather than mis-lower:
- Constant weights/params only. Weight/parameter inputs (Conv/Gemm/MatMul/
BatchNormalization/InstanceNormalization/PRelu weights, reduce
axes, TopKk, Gather indices) must be constant initializers; a data-dependent (computed) weight or param has no ANE path and raisesNotImplementedError. - Gemm: only
alpha=1,beta=1,transA=0(transBis honored). - Conv: explicit
padsonly (auto_padraises); asymmetric per-side pads are supported (mapped to the native per-side MIL convpad). - Shape subgraph.
Shapeand the dynamic-reshape plumbing (Slice/Gather/Concat/ arithmetic over shape vectors) constant-fold at import on the static input shape, so patterns like ShuffleNet's channel shuffle (Reshape->Transpose->Reshape) import as static ops.Sliceon an activation lowers toslice_by_size(step 1 only). - Pooling:
ceil_mode(both modes) andAveragePoolcount_include_pad(0/1) map to the native MILceil_mode/exclude_padding_from_averageparams;MaxPoolrejectsdilations != 1. - Elementwise.
Add/Sub/Mul/Div/Powaccept either two tensors or a tensor and a constant: a constant operand (scalar, per-channel, or broadcastable) bakes in as a fusedconst_array(a MILconst, folded to the ANE's gain-offset epilogue for affine ops), soMul(x, c),Add(x, c),Pow(x, 2), and the like need no graph cut. This makesHardSigmoid/HardSwishand the scale/shift ops in MobileNetV3 and ConvNeXt importable. - Quantized (QDQ int8). A statically-quantized ONNX model imports: an int8 weight
DequantizeLinearfolds to its dequantized constant, and an activationQuantizeLinear/DequantizeLinearpair becomes a fp16clipto the quant range (which carries any relu/saturation the quantizer fused in). Weights run at fp16 by default; passcompress="int8"to keep them on the ANE int8 weight datapath. Matches onnxruntime on-device (cosine ~1.0). - Gelu: exact erf-gelu, plus the tanh approximation decomposed to primitives (#92).
- Recurrent layers unroll.
LSTM/GRU/RNNunroll over the static sequence length into one program (two fused gate matmuls per step). Forward, reverse, and bidirectional; both GRUlinear_before_resetforms. Constant weights and initial states; default activations only;clip, peepholes (P), per-samplesequence_lens, andlayout=1raise. - Control flow folds at import.
Ifrequires a constant condition (the taken branch imports inline; the other is never built) andLoopa constant trip count with a constant-true condition (the body unrolls into one program; scan outputs stack along a new leading axis). Data-dependent branching/termination has no ANE path: a compiled program is one static graph. - Boolean algebra has no ANE path.
And/Or/Xorcannot be implemented: MIL'slogical_and/logical_or/logical_xor(and generalcast, which would allow a bool->fp16 workaround) do not compile for the ANE backend (see the on-device MIL vocabulary sweep).IsNaNis also out: fp16 NaN payloads do not survive the datapath, sox != xreads false on-device. Comparisons andNotare the supported boolean surface. - Cast folds constants and treats float->float on an activation as identity (the
engine computes fp16 regardless); float->int truncates toward zero as
sign(x)*floor(|x|), exact within fp16 integer range. - PRelu: slope is a per-channel initializer (
[C],[C,1,1], or scalar, flattened to[C]); input must be rank>=3[N,C,...]. - InstanceNormalization:
[N,C,H,W]input withscale/Binitializers[C]. - LRN: the ANE
local_response_normfolds thealpha/sizescaling internally, so ONNXalphamaps straight through andbiasmaps tok(validated on-device against onnxruntime, cosine ~1.0).size/betapass through unchanged. - DepthToSpace:
DCRmode only (the ONNX default, which the ANE op matches exactly);CRDchannel ordering raises. - Resize:
[N,C,H,W], opset 11+, target fromsizes(preferred) orscales. Only the coordinate conventions the ANE matches are accepted, each validated on-device (cosine ~1.0); every other config raises rather than mis-resize:mode="nearest"requirescoordinate_transformation_mode="asymmetric"andnearest_mode="floor"(the ANE samples withfloor; the ONNX defaultround_prefer_floorand the other round/ceil modes raise).mode="linear"requiresasymmetric(-> half-pixel-off bilinear) oralign_corners.half_pixel/pytorch_half_pixelsampling andmode="cubic"are not matched by the ANE resamplers and raise. The ONNX defaultcoordinate_transformation_modeishalf_pixel, so aResizemust setasymmetric/align_cornersexplicitly - or passload_onnx(path, approx_resize=True)to map a half-pixelResizeto the closest ANE bilinear (an opt-in approximation, ~0.99 cosine on smooth maps). This lets segmentation models (FCN, DeepLabV3) - whose final upsample is half-pixel - import.
- Reductions (
ReduceMax/ReduceMin/ReduceSum/ReduceMean):axesmay be an attribute (opset < 18; ReduceSum < 13) or an input initializer (later opsets); an absent/emptyaxesreduces over every axis.keepdims=1(the default) keeps reduced dims as size 1;keepdims=0squeezes them afterward.noop_with_empty_axes=1with no axes (the identity edge) raises.ReduceMeanandReduceMaxare validated on-device against onnxruntime (cosine ~1.0). - Gather: static (constant-initializer) integer indices only - a data-dependent
(tensor) index raises, since on-engine gather-by-data has no ANE path. Indices must be
scalar or 1-D; a scalar index drops the gathered axis (ONNX rank rule) and a >1-D index
raises. Lowers to
slice_by_size+concat; validated on-device (cosine ~1.0). - ArgMax: 2-D
[C,W]inputs only (the ANEGlobalArgMinMaxbridge),keepdims=1default (keepdims=0squeezes the axis);select_last_index=1is unsupported. Indices are fp16-encoded and the bridge cuts the graph, so it is shipped shape-validated only. - TopK: 2-D
[C,W]inputs, last axis (axisin{-1, 1}) only. Returns the values output only - ONNX's second (indices) output is unsupported, so a model consuming the indices fails when that name is later looked up.kis read from the (opset 10+) input;kin{3, 4}is rejected by the ANE itself. Shipped shape-validated only. - Attention (opset 23): maps onto
af.sdpa, the native fused-attention layer, so it accepts only what that layer expresses: 4-D[1,H,S,D]Q/K/V with batch 1 and matching head counts, thescaleattribute (ONNX's1/sqrt(D)default isaf.sdpa's too),is_causal, and anattn_maskthat is either a runtime[1,1,Sq,Skv]additive plane or a constant that is exactly the causal upper-triangular-inf(folded to the native causal path).Sq < Skv(KV-cache decode) works withis_causal=0. Rejected withNotImplementedError: the 3-D packed Q/K/V form, grouped-query attention (q_num_heads != kv_num_heads- expand K/V in the graph),past_key/past_value, thepresent_key/present_value/qk_matmul_outputoutputs,softcap,qk_matmul_output_mode,softmax_precision, boolean masks, and arbitrary constant masks. Outside the reliable native regimeaf.sdpa's own tiled decomposition takes over for the non-causal case and raises for the causal one - see capabilities.