Known Limitations¶
This page summarizes the main support boundaries for jax2onnx.
jax2onnx is an export tool for JAX-derived callables and model code. It is
primarily intended to produce ONNX inference artifacts.
Unsupported Primitives¶
jax2onnx lowers traced JAXPR primitives through registered plugins.
If a traced callable uses a primitive without a registered lowering, conversion fails with an explicit error. In many cases, support can be added through the plugin system.
See Plugin System for extension details.
Dynamic Shapes¶
Symbolic dimensions such as "B" are supported for common dynamic-batch export
patterns.
Not every JAX shape-polymorphic expression can necessarily be represented directly in ONNX. For validation and debugging, prefer starting with concrete input shapes and then introducing symbolic dimensions where needed.
Inference Behavior¶
The exported ONNX model represents the traced callable behavior.
For modules with dropout, batch normalization, mutable state, or RNG-dependent behavior, make the intended inference behavior explicit before export. Pass runtime flags as explicit inputs only when those flags should remain part of the ONNX model interface.
Runtime Compatibility¶
ONNX Runtime compatibility depends on:
- the operators emitted by the export,
- the target opset,
- the ONNX Runtime version,
- the execution provider,
- whether the model is intended for Python, browser/WASM, or another deployment target.
For browser/WASM deployment, use export_mode="web" and the Web validation
workflow.
Numerical Differences¶
Small numerical differences can occur across JAX and ONNX Runtime because of implementation details, dtype handling, precision settings, or runtime kernels.
Use allclose(...) with tolerances appropriate for the model and dtype. For
deployment checks, validate representative inputs rather than only zero-valued
inputs.
Normalization is especially sensitive when a group has zero or near-zero variance. JAX and ONNX runtimes can produce different floating-point residuals while implementing the same normalization formula, and later layers may amplify that roundoff. Use strict parity checks on representative, nonconstant inputs; for degenerate normalization inputs, also check finiteness and apply a tolerance specific to the model, dtype, and runtime.
Float64 cosine¶
Float32 cosine uses native ONNX Cos. For float64, jnp.cos and lax.cos
use the equivalent expression 1 - 2 * Sin(x / 2)². This supports older
ONNX Runtime CPU versions without a double-precision Cos kernel and avoids
losing the phase shift in Sin(x + π/2) for large arguments. The half-angle
formula can lose relative accuracy near cosine zeros; its regression checks
use an absolute tolerance of 2e-15 on selected finite float64 inputs, including
large arguments. NaN and infinity retain their NaN cosine results. These checks
cover ONNX Runtime CPU; validate the actual deployment provider separately.
log1p, expm1, and related functions¶
ONNX has no Log1p or Expm1 operator. jnp.log1p, lax.log1p,
jnp.expm1, lax.expm1, and jax.nn.log1mexp use compensated forms of
Log(1 + x) and Exp(x) - 1; the direct forms return zero or lose most digits
for small |x|. Two functions build on them. jax.nn.log_sigmoid uses JAX's
min(x, 0) - log1p(exp(-|x|)); Log(Sigmoid(x)) returns -inf from x = -20
down on ONNX Runtime. jnp.sinh uses the expm1-based libm form instead of
(Exp(x) - Exp(-x)) / 2 for every floating dtype, so it also runs on CUDA,
which has no Sinh kernel. lax.sinh keeps Sinh for float32 and uses the
same form for other floating dtypes, which have no ONNX Runtime float64 or
bfloat16 Sinh kernel. The compensated forms compute in the input dtype with
standard operators and rely on the runtime's Log being accurate near one.
bfloat16 is the exception: it is computed in float32 and rounded back, because
ONNX Runtime has no bfloat16 Log kernel; the model keeps its bfloat16 inputs
and outputs. With enable_double_precision=True, float16 and bfloat16 models
also keep their dtype. Their regression checks compare against a float64
reference: at most 8 ulp for float16, float32, and float64 sweeps with and
without graph optimizations and for every float16 input, and at most 1 ulp for
every bfloat16 input, with enable_double_precision off and on. They also
cover subnormal inputs, NaN, infinities, the log1p domain boundary at -1,
and sinh up to its overflow threshold. ONNX Runtime's CPU Where returns
+0.0 when it selects -0.0, so exported expm1(-0.0) is +0.0;
log1p(-0.0) and sinh(-0.0) keep their sign. These checks cover ONNX Runtime
CPU; validate the actual deployment provider separately.
Normalization export policy¶
normalization_mode="auto" follows a two-step policy. First, choose the
implementation with the best reproducible accuracy in a defined test
environment and lock its acceptance bounds. Then prefer a native ONNX operator
if it can replace that choice while satisfying the same bounds. Export applies
the plugin's current selection; it does not benchmark implementations for each
model. The current selection is an explicit graph for Flax NNX and Linen
GroupNorm, and Equinox, Flax NNX, and Linen RMSNorm and LayerNorm. Fixed
comparative accuracy bounds currently cover only the selected LayerNorm cases
below. Equivalent GroupNorm and RMSNorm bounds have yet to be established,
so the LayerNorm results must not be read as guarantees for those operators or
for other models, inputs, or hardware.
normalization_mode="prefer_native" requests a standard ONNX operator when
the plugin can map the operation, the builder supports the operator, and the
selected opset defines it: LayerNormalization from opset 17, fast-variance
GroupNormalization from opset 21, and RMSNormalization from opset 23. The
plugin falls back to its explicit graph when these conditions do not hold.
This is a representation choice: export does not measure numerical error or
fall back based on the "auto" accuracy limits. The selected native LayerNorm
cases have their own, looser acceptance limits in the table below. Masks,
distributed axis settings, unsupported reduction layouts, and some dtype
configurations can bypass the specialized module plugin and trace framework
operations instead.
Slow-variance GroupNorm remains explicit in every mode; native GroupNorm also
requires a supported floating dtype and a shape without statically known empty
dimensions. For symbolic dimensions that may become zero at runtime, select
normalization_mode="force_decomposed". That mode chooses the explicit
primitive graph at export. Native operators can produce smaller graphs and may
be accelerated by a runtime, but they can also produce larger numerical
differences on high-offset or otherwise ill-conditioned inputs.
This choice controls the exported graph. A runtime may subsequently
optimize or fuse that graph. The explicit LayerNorm and RMSNorm paths square
with Mul instead of Pow; targeted tests check that ONNX Runtime does not
re-fuse these patterns into its native normalization kernels with
ORT_ENABLE_ALL. Other runtimes may rewrite the graphs differently. In the
tested CPU environment, the native LayerNormalization path has larger errors
on the selected rows with large outlier activations than the explicit graph.
GPU measurements are supplementary to the CPU acceptance gate. ONNX
GroupNormalization does not model Flax's negative-variance clamp or
reduction order, and deep models may amplify normalization roundoff. Validate
the chosen representation on representative inputs and the actual deployment
runtime.
ONNX Runtime 1.30 adds AVX2 kernels for native float32 LayerNorm and RMSNorm. The LayerNorm kernel uses two-pass statistics on eligible x86 CPUs; native accuracy therefore depends on the CPU, runtime version, and normalized width. The selected native LayerNorm cases still exceed the explicit graph's stricter acceptance bounds below. To measure the model-specific latency and JAX parity tradeoff, use the decoder benchmark.
The specialized explicit LayerNorm uses a two-pass variance for Equinox and
Flax NNX with use_fast_variance=False. Its constant-row safeguard preserves
exact centered zeros for constant rows when the computed mean is finite; the
final affine bias still applies. Fast-variance Flax NNX and Linen LayerNorm
instead follow the clamped E[x²] - E[x]² formula, which is sensitive to
large offsets. Flax Linen LayerNorm with use_fast_variance=False traces the
original JAX computation in every mode and does not use that constant-row
safeguard. It is outside the fixed comparative limits below. The specialized
explicit LayerNorm computes float16 and bfloat16 statistics in float32. For
float32 inputs, only the slow-variance reduction accumulates its squared centered
values in float64 and rounds the variance back to float32; the mean, squares,
epsilon, normalization, and affine math stay float32. The widening is
unconditional (independent of enable_double_precision), LayerNorm-specific, and
improves accuracy portability across runtime reduction orders without
guaranteeing identical results. The fixed comparisons below cover float32 inputs;
validate other configurations for the model's inputs.
Scope of the fixed LayerNorm limits¶
The LayerNorm accuracy
test
exports at opset 23 and runs the same float32 inputs through ONNX Runtime's
CPUExecutionProvider with ORT_ENABLE_ALL and ORT_DISABLE_ALL. It uses
257 rows of 384 float32 features from np.random.default_rng(0): standard
normal samples with channel 7 shifted by +1700, channel 123 by -900, and
channel 300 by +300 or -300 using the same generator. Epsilon is 1e-5, with
the tested modules' default unit scale and zero bias. JAX x64 is disabled for
the tested functions; the independent reference uses float64. The
variants are Equinox LayerNorm, Flax NNX LayerNorm with slow or default fast
variance, and Flax Linen LayerNorm with default fast variance. For each case,
the test computes the maximum absolute error over all outputs against two
separate comparators: an independent float64 two-pass LayerNorm reference and
the corresponding JAX output. JAX parity therefore does not stand in for error
against the float64 reference.
The following values are fixed acceptance limits, not errors measured by a particular CI run. Both limits in each row must hold for both optimization settings:
| Export mode | LayerNorm variant | Maximum error vs. float64 reference | Maximum error vs. JAX |
|---|---|---|---|
auto, force_decomposed |
Equinox, Flax NNX slow variance | 4.1e-6 | 3.9e-6 |
auto, force_decomposed |
Flax NNX and Linen fast variance | 4.1e-6 | 5.8e-6 |
prefer_native |
Equinox, Flax NNX slow variance | 1.2e-5 | 1.2e-5 |
prefer_native |
Flax NNX and Linen fast variance | 1.2e-5 | 1.4e-5 |
The GroupNorm stability
tests
and RMSNorm policy
tests
use case-specific JAX parity tolerances: for example, representative float32
GroupNorm cases use rtol=atol=1e-5; selected Equinox and NNX float32 RMSNorm
cases use rtol=atol=5e-5; and the selected float16 RMSNorm cases use
rtol=atol=2e-3. These checks do not compare against an independent float64
reference and do not establish fixed comparative bounds for GroupNorm or
RMSNorm.
The limits were set from measurements with ONNX Runtime 1.29 on an AMD Ryzen 9 9950X3D CPU using JAX 0.10.2 and 0.11.1, then rounded up to two significant digits. The current lockfile's CPU accuracy jobs target Python 3.12 with JAX and JAXLIB 0.10.2, Flax 0.12.8, and NumPy 2.4.6; and Python 3.13 with JAX and JAXLIB 0.11.2, Flax 0.12.10, and NumPy 2.5.4. Both use Equinox 0.13.8, ONNX 1.23.2, ONNX IR 1.0.0, and ONNX Runtime 1.31.0. Each GitHub Actions accuracy job records its actual dependency versions, runner image, and CPU hardware in its job summary; use that record when interpreting a run. The CPU jobs are the authoritative numerical acceptance gate. Changes to limits, reference calculation, inputs, or test conditions require explicit justification and before/after evidence. Tests must never derive or rewrite their own acceptance bounds from the current run.
The opset only selects the ONNX schema contract; it does not assert support in a
particular runtime version. Validate the chosen opset and normalization mode
against the actual deployment runtime.
For example, TensorRT 10.9's GroupNormalization-21 importer
internally uses a fixed rank-4 normalization core. Native GroupNorm export
therefore temporarily extends rank-2/3 inputs with singleton dimensions and
flattens the spatial dimensions of higher-rank inputs before restoring the
original shape. The standard GroupNormalization node and its channel-wise
(C) parameters remain visible and schema-conformant. TensorRT 10.15's newer
NormalizationV2 path does not require this physical-shape adaptation, but
parses the same rank-canonicalized models.
TensorRT's NormalizationV2 importer nevertheless has a separate correctness
defect for GroupNormalization(num_groups=1): through at least TensorRT
10.16.1 it normalizes each channel independently instead of reducing over the
single group containing all channels. Use
normalization_mode="force_decomposed" for that TensorRT case until a fixed
version is verified; see TensorRT #4756
and the still-open onnx-tensorrt fix.
The ONNX schema function for native GroupNorm cannot execute zero-sized batch
or spatial dimensions in the checked runtimes. Statically known empty shapes
therefore fall back to the explicit graph even in "prefer_native" mode. If a
symbolic dimension may become zero at runtime, export with
normalization_mode="force_decomposed".
Training Is Out of Scope¶
jax2onnx exports ONNX artifacts for inference-style execution. It does not
attempt to preserve JAX training loops, optimizer state, automatic
differentiation behavior, or Python-side training control flow.
Coverage Pages¶
For current coverage information, see: