Model or dataset
onnxsim/onnxsim avatar
onnxsim/onnxsim

onnxsim: constant folding for ONNX graphs that grew too many nodes

Simplify your onnx model

4,403 stars434 forksPythonApache-2.0

At a glance

What is it?
onnxsim runs shape inference, graph optimization and constant folding to a fixed point over an ONNX model, then optionally checks the result against the original on random inputs. It is for people who exported a model and got a graph full of redundant operators, and who want that graph smaller without retraining anything.
Who is it for?
Adopt onnxsim when a framework exporter has emitted a graph whose static parts are still computed at runtime, and you want those parts replaced with constants before deployment. Skip it if your model is already minimal, if you depend on initializers staying tunable (check --initializers-as-non-constants), or if the opset rewrite in --target-opset would break a runtime you cannot re-test.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The exported reshape that should have been one node

The README opens with a concrete complaint. A PyTorch module whose forward pass is a single x.view((x.shape[0], x.shape[1], x.shape[3], x.shape[2])) is exported with torch.onnx.export on a dummy input of shape (2, 3, 4, 5). Because that input shape is static, the author expected the ONNX graph to contain one simple reshape. The repository's own screenshots show the opposite: a complicated graph, with the shape arithmetic decomposed into separate operators rather than folded into the reshape. That gap between what the exporter produced and what the operation actually is defines the problem onnxsim addresses. Anyone who has opened an exported model in Netron and found Shape, Gather, Unsqueeze, Concat and Reshape chains standing in for a single logical step is the intended audience. The tool is not a training-time optimizer and it does not change weights by default; it rewrites the graph.

Fixed-point folding, not a single pass

The mechanism is stated plainly in the README: onnxsim runs a fixed point of shape inference, graph optimization and constant folding until the model stops changing. Constant folding evaluates the constant regions of the graph and replaces the redundant operators with their computed outputs, so a subgraph that always produces the same tensor becomes that tensor. Shape inference is what makes more of the graph foldable: it propagates tensor shapes through the graph, and the README notes it includes partial shape evaluation via ONNX data propagation. Iterating matters because each round can expose new constants. A folded node may make its consumer's inputs fully known, which makes that consumer foldable in the next round. The loop terminates when a pass produces no further change, which also means the cost is not fixed: a graph that keeps collapsing under shape inference will take more rounds than one that is already close to minimal.

What counts as a constant, and why the flag matters

By default, initializers count as constants. That is the aggressive setting, and it is what lets the optimizer treat weights as known values and bake them into fused nodes. The README gives the escape hatch: pass --initializers-as-non-constants, or set initializers_as_constants=False in Python, to keep weights as tunable tensors. Under that setting, nodes rooted only at initializers are left untouched, and value-baking fusions such as fusing BatchNorm into Conv are skipped. This is a real fork in behaviour, not a cosmetic option. If you fold a BatchNorm into a Conv, the normalization is no longer a separate node you can edit or retrain in place. The default is the right choice for a deployment artifact that will only be run. It is the wrong choice if the simplified file is going to feed a further fine-tuning step that expects the original node structure. Read that flag before you decide whether the output of onnxsim is a final artifact or an intermediate one.

Optimization passes you can list, skip or add

Beyond folding, onnxsim runs onnx-optimizer's fusions and eliminations. The README names fuse BatchNorm into Conv as an example. The pass set is inspectable rather than hidden: onnxsim --list-default-optimizers prints it, and --skip-optimization [pass ...] removes all or some of it. Passes outside the default set, which the README describes as typically graph-shape rewrites rather than pure node reductions (a defusion is the example given), are not run unless requested with --enable-optimization pass [pass ...], or extra_optimizers= in Python. --list-other-optimizers lists those. This split is deliberate and worth respecting: a node-reduction pass is close to safe by construction, while a graph-shape rewrite can change what the model computes in ways that only a numerical check catches. If you enable a defusion, you have moved outside the set the project considers default, and the burden of validating the result is yours.

Correctness checking, and what it does not cover

onnxsim can validate the simplified model against the original on N random inputs. N is the positional check_n argument, with configurable tolerances --check-rtol and --check-atol. The inputs are generated and filled according to --input-fill (input_fill= in Python), whose options the README lists as random (uniform [0, 1), the default), ones, zeros and arange. Two things follow. First, the check is a numerical comparison on synthetic inputs, so it exercises the graph, not your data distribution. A model that simplifies correctly on uniform [0, 1) tensors can still misbehave on the actual input ranges of a deployment. Second, the tolerance values are yours to set, and a loose atol will pass a graph that a tighter one rejects. Treat a passing check as evidence that the rewrite did not obviously break the arithmetic, and pair it with a check on real inputs. Dynamic input shapes can be pinned for this purpose with --overwrite-input-shape and --test-input-shape, which is what makes the check meaningful for a model exported with a symbolic batch dimension.

Where the graph gets complicated: subgraphs, functions, custom ops

Three features address cases where a naive simplifier would stop at the top level or bail out entirely. Subgraph simplification, enabled with --include-subgraph, extends the work into If, Loop and Scan bodies, which otherwise stay untouched. Function inlining, enabled with --inline-functions (inline_functions=True in Python), flattens model-defined local functions into the main graph so that optimization, shape inference and constant folding can see through the calls; schema-defined built-in functions are left alone. Custom operators are preserved unchanged, whether they are TensorRT plugins, vendor-domain ops or custom ops declared in the default ONNX domain, and the README states that schemas registered through onnx.defs.register_schema are picked up automatically. That last point is the practical one for anyone shipping to a vendor runtime: the simplifier will not silently rewrite an operator it has no schema for, and it will use your registered schema when you provide one. Custom rewriters go further: you can plug your own rewriting logic into the fixed point with custom_rewriter, or express data-only FunctionProto rules that, per the README, also run from the C and Rust bindings.

Getting it running, and the flags that change the output

The package is on PyPI, so the install path is pip install onnxsim. The CLI is onnxsim, taking an input and output model path, and the README's examples of the flags are the fastest way to see what the tool will do to your file: onnxsim --list-default-optimizers and onnxsim --list-other-optimizers print the pass sets before you run anything. For the folding behaviour, --initializers-as-non-constants is the switch that decides whether weights are treated as known constants. For validation, the positional check_n count plus --check-rtol, --check-atol and --input-fill control the comparison. For shape-pinned models, --overwrite-input-shape and --test-input-shape set the shapes used during simplification and checking. For opset changes, --target-opset upgrades or downgrades the model's opset while simplifying, which is a rewrite of operator versions and therefore a change to verify against your runtime rather than assume. The Python API mirrors these: initializers_as_constants, extra_optimizers, inline_functions, input_fill, and a custom_rewriter hook. Recent releases add bindings beyond Python: v0.7.2 is titled npm package release and v0.7.3 is titled Rust publishing, so the C and Rust bindings mentioned in the README are part of the shipping surface, not a plan.

The wrong tool: when you should not simplify

onnxsim assumes the graph has redundancy to remove. If your exported model is already minimal, the fixed point has nothing to fold, and you pay the shape-inference and pass-running cost for a file that comes out the same size. The more serious failure mode is semantic rather than structural. A graph-shape rewrite such as a defusion, enabled explicitly through --enable-optimization, changes the shape of what flows between nodes; the default pass set avoids these, and the README's own framing of them as not pure node reductions is a warning. The second case is models whose value depends on node boundaries rather than on outputs. If a downstream step inspects or edits BatchNorm nodes, folding them into Conv under the default initializers-as-constants behaviour removes the thing you wanted to edit. The third case is custom operators with no registered schema: onnxsim preserves them, which is the correct behaviour, but it also means it cannot fold around them, so a model dominated by unregistered vendor ops will see little benefit. None of these are bugs. They are the boundaries of what constant folding can do.

Alternatives: onnxslim, and the opset-conversion route

The release notes for v0.7.1 are titled Dashboard improvements and catch up to onnxslim, which names the most direct alternative and implies a moving target. The difference in approach is one of scope rather than of folding technique. onnxsim's README describes a fixed point of shape inference, graph optimization and constant folding wrapped in export helpers: Transformers, diffusers, Detectron2 and SAM 2 exporters, safetensors and GGUF archive export and import, MLIR emission through torch-mlir or onnx-mlir, Core ML conversion, quantization-aware fine-tuning via apply_qat, and block-wise distillation via apply_block_finetune. A simplifier that only simplifies does less. If all you need is a smaller graph, the two are comparable by their output on your model, and the release note's phrasing suggests the project tracks that comparison. If you also want to go from a Hugging Face checkpoint to a deployment directory, or from a simplified graph into an MLIR compiler stack, onnxsim is doing work that a pure simplifier leaves to you. The honest position is that the folding core is the part to compare, and the surrounding exporters are the part that decides which tool fits your pipeline.

Licence and the cost of keeping up

onnxsim is Apache-2.0, which permits commercial use and modification with the usual conditions around notices and patent grant. That is a permissive licence and it is the same family as much of the ONNX tooling around it, so combining onnxsim with onnx and onnx-optimizer in one pipeline raises no obvious licence conflict. This is a description of the licence identifier, not legal advice; read the LICENSE file and the licences of onnx and onnx-optimizer yourself if you are redistributing. On maintenance cost, the release cadence visible here is roughly monthly across v0.7.1, v0.7.2 and v0.7.3 in August 2026, and the last push to master is dated 2026-09-10. The upgrade risk is concentrated in the pass set. Because --list-default-optimizers and --list-other-optimizers exist, a version bump can change which passes run by default, and a model that simplified cleanly on one release can come out differently on the next. Pin the onnxsim version in your build, and when you bump it, re-run the same model through the same flags and diff the node counts rather than assuming the output is unchanged. The opset-related flags, --target-opset in particular, are the other place where a version change can alter behaviour on a model you had already validated.

Editorial conclusion

Adopt onnxsim when a framework exporter has emitted a graph whose static parts are still computed at runtime, and you want those parts replaced with constants before deployment. Skip it if your model is already minimal, if you depend on initializers staying tunable (check --initializers-as-non-constants), or if the opset rewrite in --target-opset would break a runtime you cannot re-test. Verify first that the simplified model still passes your own accuracy check, not only onnxsim's built-in --check-n, because the README describes that check as running on random inputs.

Official sources

  1. License: Apache-2.0
  2. onnxsim/onnxsim on GitHub
  3. Project website
  4. README
  5. Releases
Community notes

Community notes