Model converter

Simplify, optimize, and inspect ONNX models with onnxsim and onnx-optimizer, compiled to WebAssembly. Everything runs locally in your browser — models are never uploaded.

Also experimental: a Pyodide (Python-in-the-browser) demo — details

Runs full onnxsim.simplify(), or onnxsim's native quantization passes on their own; see docs/wasm_pyodide.md for the toolchain story behind it.

versionsloading…

Parse a text graph

Paste an ONNX textual representation graph or model — details

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.

Convert a model

⏳ Loading WebAssembly runtime… the file picker is disabled until it is ready.
Also accepts a standalone .onnx.safetensors or .onnx.gguf archive (e.g. one downloaded from the format selector below) -- it is decoded back to ONNX before converting.
How loading from Hugging Face works — details
Fetches the model straight from the Hugging Face Hub in your browser (nothing is uploaded), then runs it through the same Simplify / Optimize path as an uploaded file. Bare names default to the 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.
Only needed for gated/private repos — how the token is used
Optional — only needed to load a gated or private repo you have access to; public repos (like 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.
Shareable link: the input model and options can be set from the URL. Setting an input updates this URL so it stays shareable (uploads excepted).
URL query keys and what "Copy shareable link" does
e.g. ?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).
Copy shareable link builds a link that reproduces the current input and all the conversion options below (processor, constant fold, shape inference, tensor size threshold, target opset) and copies it to your clipboard. Uploaded local files have no URL, so share a Hugging Face model / URL, a backend test case, or a pasted text graph.
What Xet does and how "max connections" works
Downloads via @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).
The in-browser Xet client (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.
What the nanochat demo model is
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.
What "Run test case" fetches and checks
Fetches 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).

Convert mode & options


single pass (debug):








Quantize a model

How this panel relates to Convert / Fine-tune / Correct bias — details

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 quantization risk before picking a method — details

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:

What each method does, and when it needs calibration — details

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.



Quantized preview

Check Edge AI (TI TIDL) compatibility

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.

Fine-tune a quantized model (QAT)

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.

How it works, and what it can/cannot do — details

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.

Correct bias

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.

How it works, and per-channel vs per-position — details

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.

Fine-tune with a LoRA adapter

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.

How it works, and what it can/cannot do — details

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.

inject into:

WebGPU training demo (Hugging Face data)

Not a tool for your own model -- a small, fixed, self-contained demo of the machinery above, with WebGPU required (no wasm/CPU fallback).

What this demo trains, and why WebGPU is required — details

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.

Visualize with Netron (before / after)

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.

Model-size limit, the quantized preview, and the Netron embedding — details

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.

Before
After

Dynamic dimensions (dim_param)

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.

Try it: load 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.

Custom WebGPU kernels

Lists every node that carries a custom WebGPU kernel metadatabefore 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.

Where this metadata comes from, how the WGSL is shown, and what "Tune this kernel"/"Profile graph" do — details

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.

Console outputs:

Download log saves the console output above as a .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.

onnxsim profiling

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.

Node reduction per loop

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.

Run inference (onnxruntime-web)

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.

What each control does (model picker, batch, warmup, input fill, WebNN, MACs) — details

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.

WebNN status: probing…

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.

Live Cardputer output (Web Serial)

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.

What this reads, and reconnect behavior — details

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.