Read enough of our source tours and a pattern emerges: MIGraphX starts by parsing an ONNX file, IREE compiles from ONNX inputs, Triton Inference Server loads ONNX backends, and ONNX Runtime is literally named after it. The onnx repository itself is the quiet room at the center of all that noise — a protobuf specification, a C++ core that validates and understands models, Python tooling that authors them, a NumPy reference implementation, and the test harness that every accelerator vendor’s “ONNX compatible” claim is measured against.
What surprises most readers is how little code the interchange layer actually needs. The format’s authority comes from four protobuf messages; everything else in the repo exists to make those messages correct (the checker), understandable (the schema registry and shape inference), convenient to write (the Python helpers), and demonstrably executable (the reference runtime and test machine). If you have ever wondered what exactly a runtime promises when it says it supports ONNX, this repository is the contract in source form.
The map below follows that story: the spec at the center, the C++ core that interprets it, the Python layer that produces it, and the reference and test machinery that proves it runs.
Overview of the onnx repository: helper.py builds protobufs per the spec; the C++ core (checker, schema registry, shape inference) validates and understands them; numpy_helper bridges tensors; the pure-NumPy ReferenceEvaluator executes models; and the backend test suite replays thousands of cases against any runtime that implements the Backend contract.
Reading the overview from left to right:
- The spec lives in onnx.in.proto, L449, where
ModelProtowrapsGraphProto(L564), nodes (L223), and tensors (L607). - The Python authoring layer — helper.py, L297 and numpy_helper.py, L408 — turns dictionaries and arrays into those messages and validates them via checker.py, L120.
- The C++ core registers every versioned op in schema.cc, L110 — the tensor family, L22 being the largest — and consults those schemas while checker.cc, L1508 validates structure.
- A complete model semantics reference is the ReferenceEvaluator, L28, which runs any model in pure NumPy.
- The Backend contract, L69 plus the Runner, L61 form the test machine that real runtimes — from vLLM’s export paths to ONNX Runtime itself — are measured with.
Four Messages That Define a Model
Everything starts in the protobuf definitions. TensorProto (L607) is the most instructive to read because it encodes a pragmatic compromise: large tensors live in raw_data as raw bytes, while small ones use typed repeated fields; external data can point at a file entirely outside the model. NodeProto (L223) is minimal on purpose — an op type string, a domain, input and output names, and attributes — because all the real semantics live in the schema registry, not the message. GraphProto (L564) adds the dataflow wiring: named edges, initializers, and value infos; and it nests, which is how subgraphs for control flow work. ModelProto (L449) is the envelope carrying the opset imports, producer metadata, and (crucially) the training and function extensions.
This is a dataflow graph, not a program: nodes only name their inputs, cycles are illegal outside the nesting rules, and nothing specifies how an op computes. That deliberate thinness is why the format survived — every runtime from llama.cpp’s converter ecosystem to TensorRT-LLM workflows can agree on four messages while disagreeing completely about kernels.
The format’s extensibility deserves its own note. Node attributes carry compile-time parameters — axis choices, epsilon values, even whole subgraphs for the control-flow ops If and Loop. And function protos let the standard grow without breaking the wire format: newer composite ops are defined as labeled subgraphs of older primitives, so a runtime that implements the primitives can expand the composite on the fly, while one that knows it natively skips that step. This is how the opset count keeps climbing without forcing every runtime to hard-code every entry.
The C++ Core: Schemas, Checking, Shapes
The opset is the semantic heart. schema.cc, L110 is a registry keyed by (domain, op type, version), and every op family declares itself through macros like ONNX_OPERATOR_SET_SCHEMA, L22 — a declarative block specifying inputs, outputs, type constraints, and shape rules. Node lookup (GetSchema, L908) resolves a node’s op against the opset versions the model imported, which is what makes opset evolution survivable: old models keep working because old versions remain registered.
Two subsystems build on that registry. The checker — check_value_info, L163, check_tensor, L349, and the model-level walk at L1508 — validates that a model is structurally honest, including cycle detection in function calls (L1343). Shape inference composes per-op rules from building blocks like propagateElemTypeFromInputToOutput, L97 and mergeInShapeInfo, L143 so runtimes can know output shapes without executing. The version converter, L17 rounds it out by rewriting models across opset versions through the C++ up/down machinery (L39).
Shape inference deserves a sentence of praise for its honesty: it is optional, best-effort, and conservative. When it cannot decide a dimension, it leaves the value unknown rather than guessing, and the checker still passes. Exporters run it once before saving so downstream tools get static shapes for free, and runtimes treat its output as hints, never promises — a contract written exactly the way cross-vendor software should be.
The division of labor matters: the checker rejects malformed models, the schema defines legal ones, and shape inference predicts unexecuted ones. Runtimes with strict ingestion paths — our IREE and MIGraphX tours both trip over malformed files loudly — are essentially consumers of exactly these two guarantees.
Authoring in Python: From Dicts to Deployable Models
The Python surface is what most people actually touch. helper.py, L136 builds nodes, make_graph, L203 wires them, and make_model, L297 wraps everything with opset imports — running the checker in the process, which means a model that constructs cleanly is almost always one that loads cleanly. Tensor translation goes through numpy_helper’s to_array, L234 and from_array, L408, which decide between raw bytes and typed fields and handle external data paths.
Serialization got a quiet redesign: serialization.py, L94 registers format-specific serializers — protobuf, text proto, JSON — into a registry (L51) probed at load time, so onnx.load_model sniffs the file and picks the right reader. For anyone writing exporters — the unglamorous glue inside PyTorch, TensorFlow, and Executorch conversion paths — these three files are the entire contract.
The Reference Runtime and the Test Machine
The reference directory is the repo’s most underrated asset. ReferenceEvaluator, L28 compiles a model into a list of Python op handlers, and its run, L560 executes them with NumPy — slowly, but with textbook fidelity. The ops inherit from op_run.py’s base classes, which snapshot the C++ schemas so Python implementations self-check against the spec, and _op_list.py, L283 registers the hundreds of covered ops. When an accelerator vendor disagrees with a spec reading, this code is the referee.
Reading one implementation is worth a thousand spec paragraphs. The reference handlers keep broadcasting rules, rank behavior for matrix products, and the exact arithmetic of quantized types right beside the code that implements them, so a disagreement between vendor behavior and the spec resolves in minutes rather than weeks. The evaluators also double as documentation for the awkward corners where the written spec is dense but Python is readable — which is exactly why both runtime authors and benchmark authors keep the reference on hand.
The test machine industrializes that referee role. A backend implements the Backend contract — prepare, L91, run_model, L102, run_node, L110 — and the Runner, L61 replays the full corpus of node and model cases as unittest suites (L159), with include/exclude/xfail filters (L102) for honest partial support. Per-op cases like abs.py generate the protobuf test data on the fly. This is why “passes the ONNX backend tests” is a meaningful sentence in vendor documentation — the corpus, the expectations, and the harness are all public.
From Install to Your First Model
The Python package installs from PyPI and needs nothing else for authoring and reference execution:
pip install onnx
python - <<'PY'
import numpy as np
import onnx
from onnx import helper, numpy_helper, TensorProto
X = helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 2])
Y = helper.make_tensor_value_info("y", TensorProto.FLOAT, [2, 2])
node = helper.make_node("Relu", ["x"], ["y"])
graph = helper.make_graph([node], "relu", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 21)])
onnx.checker.check_model(model)
onnx.save_model(model, "relu.onnx")
ref = onnx.reference.ReferenceEvaluator(model)
print(ref.run(None, {"x": np.array([[-1, 2], [3, -4]], dtype=np.float32)}))
PY
That snippet touches every layer of the map: helper builds the protos, check_model runs the C++ checker, and the ReferenceEvaluator proves the artifact computes before any accelerator sees it. For production you would hand relu.onnx to a real runtime — and the whole ecosystem of them, from SGLang frontends to KTransformers integration paths, speaks the same four messages.
Honest Limits and Where ONNX Fits
Limits first, because the format’s weaknesses are as architectural as its strengths. ONNX is a graph interchange, not an execution plan: dynamic control flow, training loops, and custom hardware features routinely escape the standard opset, and the escape hatch — custom op domains — quietly destroys portability, since a model with vendor ops only runs on that vendor’s runtime. Coverage of frontier LLM architectures also lags by design: a new attention variant usually ships in runtime-native form long before its opset counterpart lands, which is why the serving stacks we toured (vLLM, SGLang, TensorRT-LLM) increasingly treat ONNX as one ingestion path among several rather than the only one. And quantization has historically fragmented across proposals — a reminder that interchange standards move at the speed of consensus.
As for fit: this repository is the right read when you write an exporter, debug a model a runtime rejects, or need a ground-truth implementation to argue numerical correctness. It is also the quiet prerequisite behind half our inference overview catalog — and understanding it makes every other stack tour easier, because after this one you know exactly what the words “ONNX model” promise: four protobuf messages, validated against a versioned op registry, shippable with a test suite everyone shares. Enjoyed this post? Never miss out on future posts by following us