Simplify, optimize, and inspect ONNX models with onnxsim and onnx-optimizer, compiled to WebAssembly. Everything runs locally in your browser — models are never uploaded.
Runs full onnxsim.simplify(), or onnxsim's native
quantization passes on their own; see
docs/wasm_pyodide.md
for the toolchain story behind it.
The form onnx.parser.parse_graph / parse_model accept.
It is parsed into a model by the WebAssembly module (a bare graph is
wrapped into a model with a default-domain opset import) and shown in
the Before Netron pane. Local function definitions
(<domain: "..."> name (...) => (...) {...} blocks after
the graph) are parsed too, and the wrapped model gains an opset import
for each function's domain so it validates. With convert after
parsing on, it is also run straight through the currently selected
convert mode below (Simplify / Optimize / Inline functions, or a single
debug pass). Extract from loaded model runs this in reverse: it
reads back whatever model is currently shown in the Before Netron
pane below and fills this box with its textual representation, so it can
be inspected and -- after editing -- reparsed.
.onnx.safetensors or .onnx.gguf archive
(e.g. one downloaded from the format selector below) -- it is decoded back to ONNX before converting.
onnxmodelzoo org; any owner/repo
or direct .onnx URL works too. Picking a model (or
entering a repo id / URL) shows its download size first, so you
can see how big it is before fetching. When a repo holds more than
one .onnx, the largest is auto-detected; use the
file selector to convert a different one.
onnxmodelzoo) don't need one. Create one at
huggingface.co/settings/tokens
("read" access is enough). It is sent straight from your
browser to huggingface.co as an
Authorization header for size checks, file
listing, and the download itself — never anywhere else, and
never included in the "Copy shareable link" URL above. With
remember off (the default) it lives only in this
page's memory and is gone on reload; checking it saves the
token in this browser's local storage (still local to your
machine) so it survives a reload.
?model=onnxmodelzoo/resnet18d_Opset18 or
?model=https://…/foo.onnx&optimizer=optimize&cf=0.
It loads and converts automatically (add &autoload=0
to only prefill). Keys: model
(hf/url), graph (an ONNX text
graph to parse), optimizer (a.k.a. mode/processor; the
convert mode —
simplify/optimize/optimize_fixed/inline/infer_shapes/data_propagation/fold_constant),
constant_fold/cf,
shape_inference/si,
inline_functions/inline,
tensor_size_threshold/tst,
target_opset/opset,
autorun/autoinfer (auto-run inference
after each conversion),
backend (prefills the backend-test URL).
@huggingface/hub, reconstructing the
file from content-addressed chunks and caching them in
IndexedDB so repeated / overlapping loads reuse them. On by
default; falls back to a direct download automatically if the
Xet path isn't available (e.g. a non-Xet repo or a CORS block).
XetBlob)
fetches multiple content-addressed blocks at once and adapts
the connection count to measured throughput, up to
max connections, to saturate your link — this is what
makes Xet fast enough to be the default (~10 or fewer is
usually best; 1 forces a single serial
connection). Xet also gives chunk dedup / cache reuse across
repeated or overlapping loads.
nanochat_wasm_demo.onnx (bundled with this page,
34KB, no network fetch beyond this page itself) -- a tiny
nanochat-style GPT (rotary embeddings, QK norm, Group-Query
Attention, relu² MLP) exported unsimplified from
scripts/nanochat
(nanochat itself has no ONNX export path of its own -- see that
directory's README for why). Runs the same Simplify / Optimize /
Netron / Run-inference path as an uploaded file.
model.onnx and the test data
(test_data_set_N/ of input_*.pb /
output_*.pb) from GitHub, runs the model through
onnxruntime-web with the test inputs, and checks each output
against the expected tensor within tolerance. Pick a preset above or
paste a GitHub tree / raw.githubusercontent.com
URL; listing uses GitHub's public (rate-limited) API. With
convert the model on, the fetched model is also run through the
selected convert mode below (and shown in the Netron panes).
Quantizes a model with onnxsim's WebAssembly module, entirely in
your browser -- nothing is uploaded. Its result is kept
independent of the plain Convert section above (it never
overwrites the "after" Netron pane or converted
inference source) so you can inspect all three stages side by
side: original -- onnxsim-simplified (Convert
section, optional) -- quantized (its own "Quantized"
Netron pane and quantized inference source below).
Pick what to quantize:
Check whether INT8 (Dynamic / Static / QOperator's scheme) looks numerically safe for this model -- a static analysis of its own weights and shapes, no execution or calibration data needed, so it runs instantly.
Pick a method:
DynamicQuantizeLinear. No calibration
needed.{-1, 0, 1} × scale), e.g. BitNet-style models.
No calibration needed.QuantizeLinear/DequantizeLinear
pairs using a calibrated activation range; the graph
still computes in float32, and a QDQ-aware runtime fuses the
pattern into an integer kernel. Needs calibration (below).QLinearMatMul, computing directly in int8; also
needs a calibrated output range, not just the
activation. Needs calibration (below).Cast keeps the model's own external
input/output types float32) is simply rounded to a
narrower floating-point format, not an integer scheme, so
no calibration data is needed at all. Float16/BFloat16
halve storage; Float8 (E4M3 or E5M2, below) quarters it,
at the cost of the least precision of any format here.
Calibration (Static/QOperator only) runs the model over a
few batches of synthetic random input through
onnxruntime-web
(downloaded on demand, same as the "Run inference" panel below)
to observe each quantizable tensor's actual value range -- the
same approach onnxsim.calibrate() uses in Python,
just run here in the browser instead. Random data is a
reasonable stand-in when representative data isn't at hand, but
real calibration data (e.g. via the Python API) usually gives
tighter, more accurate ranges.
Checks a model against a static, offline heuristic for
TI's TIDL
(the inference engine for the C7x-MMA accelerator on
Jacinto/Sitara SoCs), entirely in your browser — nothing is
uploaded. Ported from
scripts/edgeai/tidl_ops.py,
checked against edgeai-tidl-tools' own published
docs/operators.md — control flow
(If/Loop/Scan),
Sequence/Optional ops,
NonMaxSuppression (host-only), QOperator-format
quantized ops (QLinearConv and friends — TIDL
only supports the QDQ form), and any input with a dynamic
(symbolic or unranked) shape. This is a heuristic, not a
real compile — see the note below the report.
Target precision (informational — TIDL's C7x-MMA runs 8-bit or 16-bit fixed-point, or a per-layer mix of the two; there is no lower-precision mode, and this selector does not change the check above):
To produce a QDQ-quantized model matching TIDL's documented
per-layer scheme (per-channel symmetric weights, per-tensor
activations), use the Static (QDQ) method in the
Quantize section above — the same
onnxsim.calibration.quantize_static this
page's WASM module runs.
scripts/edgeai/quantize_for_tidl.py
checks that match on the Python side, and its docstring
records a real, reproduced TIDL compiler crash on that
exact QDQ output via
advanced_options:prequantized_model=1 — so
treat that output as inspection-only for now, not something
confirmed to compile.
Recovers some of the accuracy quantization cost, by training
the quantized model in your browser. This is
onnxsim.apply_qat, reachable without Python — see
docs/qat.md.
The float model is the teacher, the quantized one is the student, and each block's weights are optimized by Adam so the student reproduces the teacher's own activations at that block's output. The WebAssembly module emits one optimizer step as a plain ONNX inference graph (a hand-derived backward pass is just dataflow -- no autograd, no training runtime) and onnxruntime-web runs that graph in a loop, so the whole training step reaches WebGPU (and WebNN) through the same execution providers the inference panel uses.
What it can and cannot do. The objective is
"reproduce what the teacher produced", so it cannot exceed the
float model and it is not a way to train on new data or a new
task -- it fixes a model something else made worse. With
fine-tune only below, the quantizer comes out of the
middle and the student's own float weights are trained instead
(apply_block_finetune), which is for a pruned or
otherwise altered model; the two models have to differ, or there
is nothing to learn. Training runs on a handful of calibration
rows, so treat the result as a demonstration of the machinery
rather than a production tuning run.
Recovers accuracy an output-preserving algorithm change cost a
model -- quantization, or a Resize node's mode
swapped for one a deployment accelerator supports. This is
onnxsim.correct_bias /
onnxsim.correct_spatial_bias, reachable without
Python -- see
bias_correction.py's
own module docstring for exactly when it can and cannot recover
an algorithm change's error.
By measuring, on synthetic calibration data run through onnxruntime-web, a systematic shift between the float model and the modified one, and adding a constant to cancel it. No training, no gradients -- only ordinary forward passes, so it is much cheaper than fine-tuning, and much more limited.
Per-channel vs per-position. Per-channel (the default) fits a single number per output channel and suits quantization rounding and similar shifts. A Resize mode/ coordinate-transformation swap's error is instead spatially-varying, so tick per-position (spatial) below for that case -- it only helps when the calibration images share real spatial structure (a fixed-mount camera, a consistent framing), and is verified against held-out calibration data before being applied at all: on generic, independent images it correctly does nothing rather than risk making things worse.
Injects a trainable low-rank (X @ A @ B) branch
around each eligible MatMul/Gemm/Conv
layer and trains only A/B by Adam,
with every base weight frozen -- in your browser, with no
server. This is onnxsim.inject_lora +
onnxsim.train_lora -- see
onnxsim/lora_entry.h.
Reachable without Python: the WebAssembly module performs the injection and emits one optimizer step as a plain ONNX inference graph, and onnxruntime-web runs that graph in a loop, exactly as the QAT panel above does for its own step graph.
What it can and cannot do. The adapter is trained to
reproduce a reference model's own output on the same
inputs, so the two models have to differ or there is nothing
to learn -- a natural pairing is base = onnxsim-simplified
against reference = the original upload, patching back
whatever simplification changed with a small trainable branch
instead of a full re-tune. It trains on a handful of
calibration rows, so treat the result as a demonstration of
the machinery rather than a production adapter. QLoRA
(composing this with an NF4-quantized base) and
caller-supplied training targets (rather than a
reference model) are not wired into this panel yet -- see
lora_finetune.mjs's own top comment. Exporting to
ONNX Runtime's native .onnx_adapter format
(export_lora_adapter) needs its Python
onnxruntime.AdapterFormat API, which has no C++
counterpart to reach from wasm, so this panel exports the
trained full model only.
Not a tool for your own model -- a small, fixed, self-contained demo of the machinery above, with WebGPU required (no wasm/CPU fallback).
A tiny two-layer network,
onnxsim.graph_grad/qat_graph's own
gradient/Adam step graph, trained on real data fetched live
from Hugging Face -- this fails loudly on a browser
without WebGPU, rather than silently training somewhere else.
See
webgpu_hf_demo.mjs
and test/make_step_graph_fixtures.py
for exactly what each demo trains and why.
Renders the model graph with a self-hosted Netron: the original model on the left and the simplified/optimized result on the right. Nothing is uploaded.
The model bytes are posted straight into the embedded Netron in your
browser, so there is no model-size limit. Use export SVG to save
the rendered graph as an SVG image.
The quantized result has its own Netron preview inside the
Quantize panel above instead of a third
pane here, so it never crowds this before/after comparison.
Netron is driven here purely as an embeddable model-preview component
over a postMessage protocol; that idea and protocol are
being discussed upstream in
lutzroeder/netron#1591.
Lists the model's symbolic axis names (ONNX
dim_params) on its graph inputs and outputs —
before and after simplify/optimize. Read straight from
the model bytes in your browser (nothing is uploaded); symbolic axes
are shown highlighted.
Xenova/all-MiniLM-L6-v2 to see dim_params — details
load
Xenova/all-MiniLM-L6-v2
(a ~21 MB quantized MiniLM sentence-transformer) — its inputs
and output carry the named dynamic axes batch_size and
sequence_length, so they show up here on both the
before and after sides. Most image models (e.g. the
onnxmodelzoo ResNets in the dropdown) are fully static
and list no dim_params.
Lists every node that carries a custom WebGPU kernel metadata — before and after simplify/optimize. Nothing is uploaded. Every node in the model, of any op type — whether or not it already carries a kernel — gets an opt-in Tune this kernel… button that can generate and dispatch alternative kernels live in this page — see below.
Already-attached kernels come from onnxsim's WebGPU kernel metadata
(hand-written, or generated by
onnxsim.webgpu_tinygrad_codegen for a gap
onnxsim.webgpu_target flags, e.g. a 3-D Conv
or an align_corners Resize), since a
conversion can rename or fuse away an annotated node. Read straight
from the model bytes in your browser; each step's WGSL source and
bindings are shown collapsed — click a step to expand it.
This panel does not detect gaps itself — it only displays what a
model already carries. That's rare in practice (most models never
go through that server-side flagging at all), so every node
gets its own opt-in Tune this kernel… button, not just
already-flagged ones — any op type tinygrad's own ONNX importer
supports, not only Conv, so a memory-bound op like
Gather/Concat/Pad/Slice
can get its own tuned kernel too. It runs
onnxsim's
browser-timed kernel tuning live in this page (via
Pyodide
running tinygrad, a real multi-second/multi-megabyte download —
nothing loads until you click it) and lets you download a model
with the fastest candidate it found attached in place of the
original kernel — there's no cap on how many candidates get
tried, every one tinygrad's own kernel-opt search finds is
dispatched and timed. A node whose isolated computation needs no
dedicated kernel at all (a pure view op, e.g. a contiguous
Reshape/Transpose) is reported as
"nothing to tune", not an error. A Tune full graph… button
(shown once per side, above the per-node list) runs the same loop
for every node in sequence and folds every winner into one
export — handy for a model with more than a couple of tunable
nodes, though each node's own dispatch loop still takes real GPU
time, so a big model can take a while (the button reports live
per-node progress; a node that fails to tune is skipped, not
fatal to the rest of the batch). A Profile graph… button
runs first, cheaply (one baseline kernel per node, no search), and
picks out the smallest set of the slowest nodes whose combined
time covers most of the model's own measured latency — the
resulting Tune dominant ops only button spends a full
tuning search only on those, useful when a handful of
memory-bound ops (rather than every Conv) actually
dominate a model's real runtime.
.log file. Report an issue opens a pre-filled
GitHub issue
with the selected options, browser info, and the tail of the
console output. Please also attach your model (and the downloaded
log) so the problem can be reproduced.
Profiling is on by default (“profile simplification” above): convert a model with Simplify and the fixed-point transforms (shape inference, onnx-optimizer, constant folding) — with ONNX Runtime's own per-operator constant-folding spans merged in — render below as a flame graph. Download the JSON, or click Embed in Perfetto to open the full Perfetto timeline inline on this page.
How many nodes the graph held right after every round of each simplification fixed-point loop, until it stopped changing (a fixed point) or hit the round cap. One chart per loop — Optimize (shape inference + onnx-optimizer passes) usually does the most rounds; FoldConstant is the outer pipeline wrapping it.
Runs a model for a few iterations to check it executes, using dummy inputs generated from the model's input shapes. compare (before vs after conversion) — the default — runs both the original upload and the converted result on the same input and reports how far their outputs diverge plus the speed difference.
The model
picker chooses what to run: compare (before vs after
conversion) — the default — runs both the original upload
and the converted result on the same deterministic input and
reports how far their outputs diverge (max |Δ|) plus the speed
difference, so you can confirm the simplify/optimize step preserved
the model's numerics and see how much faster it got. You can also run
just the converted result or the original upload on
their own. The batch control sizes the model's dynamic batch
(first) axis; it has no effect on inputs whose first dimension is
fixed. The warmup control runs that many untimed passes before
the timed iterations, so one-time costs (kernel/program compilation,
buffer allocation) don't add noise to the reported latency; set it to
0 to time every run. WebGPU falls back to WebAssembly when
unavailable. Tick auto-run after each conversion to run this
panel automatically whenever a conversion finishes (with the options
selected here), instead of clicking Run inference each time.
The input fill picker's sample data option feeds real
data instead of synthetic numbers: a photo from
uoft-cs/cifar10
for an image-shaped input, or a tokenized sentence from
stanfordnlp/sst2
for a recognized NLP model's input_ids/
attention_mask/token_type_ids — both
fetched live from Hugging Face for each run. Any input that doesn't
match one of those shapes, or an NLP model whose tokenizer family
isn't recognized, falls back to random fill (logged in the output
below). With sample data selected, a run also shows what it
actually fed the model and got back — the fetched image/sentence
next to the model's raw output, decoded as top-5 ImageNet-1k
predictions when the output looks like a standard 1000-way
classifier (a best-effort guess, not verified per model).
The execution-provider picker also offers
WebNN
(GPU / NPU / CPU), which runs the model through the browser's own
ML stack. WebNN is experimental and ships behind a flag
(Chrome/Edge: chrome://flags/#web-machine-learning-neural-network),
with the broadest support on Windows — the WebNN status line
below the controls reports whether it is usable in your browser, and
any WebNN run falls back to WebAssembly when the chosen device or an
operator is unsupported.
If the model carries onnxsim's MAC/FLOP metrics in its
metadata_props (onnxsim
PR #527,
e.g. annotated via onnxsim.model_info.annotate_metadata),
they are shown below with the achieved throughput (GFLOP/s = model
FLOPs ÷ average latency). With annotate model info on (the
default), the converter bakes these metrics into both the
converted result and the original upload, so you can run each
and compare their MACs alongside the measured inference speed.
With profiling on (default), the run captures an onnxruntime-web
trace: on WebGPU, one span per GPU kernel invocation; on WASM (or as
a fallback), one span per session.run iteration. It
renders below in the same viewer, with an Embed in Perfetto
button to open the full timeline inline.
Streams a physical onnx-cardputer-flash board's own printed output straight into this page over Web Serial — the exact same lines the runtime already prints to a serial terminal or the board's own screen, just rendered here too. This only reads and displays what the board is already printing; it does not run or compare against onnxruntime-web above, and it doesn't flash anything — that's the onnx-cardputer-flash flasher's job (a real page, deployed alongside this one — see its README for the full pipeline). The same project's onnx-k210-flash flasher flashes Kendryte K210 boards (Sipeed Maix Amigo, M5StickV) the same way — see its own README for status; there's no live monitor panel for it on this page.
Parses the runtime's own two print formats
(tools/onnx-cardputer-flash/firmware/runtime/src/main.cpp's
RunAndReportInference()/printOutputValues()):
a test inference: OK (N ms) /
test inference: FAILED (N ms) status+latency line,
followed by each output tensor's
out[i] raw: ... (quantized) and
out[i] dequant: ... (real-unit, int8/uint8 only)
or out[i] values: ... (float32) lines. Everything
else the board prints (boot messages, mic ready,
errors) is shown in the raw log below but not specially
parsed. The latest result panel above the log re-renders
on each new status line using whatever out[]
lines followed the previous one.
This board's native USB-Serial/JTAG peripheral (not a
separate USB-UART chip) genuinely disconnects and
re-enumerates on every hardware reset — a real property of
the chip, not a bug here. A read error is treated as an
expected disconnect, not a fatal one: the status line says
so and the Connect button re-enables; reconnecting
after a reset needs one more click, the same way this page's
own Web Serial permission model requires a user gesture for
each new requestPort() call.