AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

ONNX: Model Interoperability Standard

📚 MLOps⏱️ 20 min read🎓 Grade 10
✍️ AI Computer Institute Editorial Team Updated: September 2026 CBSE-aligned · Peer-reviewed · 20 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

The UPI Moment for Machine Learning

Open your phone and pay a friend back for chai using whatever UPI app you have installed — PhonePe, Google Pay, Paytm, BHIM, it does not matter which. Your friend can be on a completely different app, with an account at a completely different bank, and the money still lands in seconds. This feels ordinary today, but it was not always possible. Before the National Payments Corporation of India (NPCI) launched the Unified Payments Interface (UPI) in 2016, sending money between two banks meant typing out account numbers and IFSC codes and waiting for NEFT or IMPS to process the transfer. Every bank had its own app, its own login, its own way of doing things, and none of them were built to talk to each other.

UPI did not force every bank to shut down its own app and merge into one. Instead, it defined a shared standard: a common address format (a Virtual Payment Address like name@bank), a common set of rules for how a payment request is made and confirmed, and a common message format that every participating bank and app agrees to speak. Because PhonePe, Google Pay, and your bank's own app all speak this one shared language, they interoperate freely, even though each was built by a different company, in a different codebase, with a different design philosophy.

Machine learning had exactly the same problem — and in 2017 it found its own version of UPI, called ONNX.

The N × M Problem: Why Interoperability Is Hard

Picture a data science team at an Indian company. The researchers prefer PyTorch because it is flexible and lets them experiment quickly. Another team on the same floor builds simpler models with scikit-learn, because a lot of their problems are solved well with decision trees and linear models. A legacy pipeline still trains some models in TensorFlow. Now imagine this company needs to deploy trained models to four very different places: a Python microservice in the cloud, a high-throughput Java backend, an Android app that must work offline, and a browser-based analytics dashboard.

Without a shared standard, every framework needs a hand-built bridge to every deployment target: a PyTorch-to-Java bridge, a PyTorch-to-Android bridge, a scikit-learn-to-browser bridge, a TensorFlow-to-Java bridge, and so on. With 3 frameworks and 4 targets, that is 3 × 4 = 12 separate converters, each written and maintained by hand, each a fresh opportunity for bugs, and each needing to be rewritten whenever the framework or the target changes.

This is the same shape of problem UPI solved for banking, and it has a name in computer science: the N × M interoperability problem. Whenever N producers need to talk to M consumers, and every single pair needs its own custom connector, the number of connectors grows by multiplication, not addition. Add a fourth framework or a fifth deployment target, and the count does not creep up gently — it jumps.

The fix is the one UPI used: put one common, agreed-upon format in the middle. If every framework only needs to know how to export to that one format, and every deployment target only needs to know how to read that one format, the number of integration points collapses from N × M to N + M. For our example, that is 3 exporters plus 4 runners: 7 pieces of work instead of 12 — and every new framework or target added after that needs one new integration, not a multiplying set of them. Compiler designers solved an almost identical problem decades ago with a shared intermediate representation that many programming languages compile down to before targeting many different processors; ONNX brought that same idea to machine learning.

What Exactly Is ONNX?

ONNX (Open Neural Network Exchange) is an open, framework-independent format for representing trained machine learning models. It was created by Facebook and Microsoft and announced in September 2017, with Amazon Web Services adding support soon after. Today ONNX is maintained as an open-source project under the LF AI & Data Foundation, part of the Linux Foundation, with contributions from hardware and software companies across the industry.

ONNX does not care which framework trained your model. It pins down two things precisely enough that any compliant tool can implement them:

  • A common file structure for describing a model as a computational graph — the sequence of mathematical operations the model performs, wired together.
  • A common, versioned set of operators: the individual mathematical building blocks, such as matrix multiplication, convolution, or an activation function, that every ONNX-compliant tool agrees to implement the same way.

It is fair to ask why a new format was needed at all — does every framework not already have its own way to save a model? PyTorch can save a model's learned parameters with torch.save, TensorFlow has its own SavedModel format, and scikit-learn models are commonly saved with Python's pickle or joblib. Each of these works well inside its own ecosystem, but none was built for anyone outside that ecosystem to read. A pickled scikit-learn model is not just framework-specific but Python-specific, since pickle stores a near-literal dump of Python objects — and loading a pickle file from a source you do not fully trust can execute arbitrary code on your machine, which is a real security concern in production. These native formats solve the save-and-reload problem inside one framework; ONNX solves the harder problem of handing a trained model to a completely different framework, programming language, or device, which is a genuinely different job.

A useful comparison is the PDF file format. A PDF can be created by Microsoft Word, Google Docs, or LaTeX, yet any PDF reader can open it correctly, because all of them agree on what a PDF file must contain and mean. ONNX plays the same role for trained models: PyTorch, TensorFlow, and scikit-learn can each export to it, and any ONNX-compliant runtime — on a phone, a server, or inside a browser — can load and execute it correctly, without ever needing to know which framework produced it.

Anatomy of an ONNX Model: Graphs, Nodes, and Operators

To use ONNX well, it helps to know exactly what is inside an .onnx file. Internally, ONNX stores a model using Protocol Buffers (protobuf) — a compact, efficient binary serialization format originally built at Google — organized into a small number of nested structures:

  • ModelProto: the outermost container. It stores metadata such as the producer's name, the ONNX version, and which opset (operator set) the model was built against.
  • GraphProto: the actual computation, stored as a directed acyclic graph (DAG) — a network of steps that only flows forward, with no step ever looping back on itself.
  • NodeProto: one entry per operation in the graph, such as a single matrix multiplication or a single activation function. Each node names its operator type, its input tensor names, and its output tensor names.
  • TensorProto: the raw numeric data. A model's learned weights and biases are stored as initializers — constant tensors baked directly into the graph — while data supplied at run time, like a new transaction record, arrives through the graph's declared inputs instead.

Every value flowing through this graph — inputs, outputs, weights — is a tensor: a multi-dimensional array of numbers with a fixed shape. A single number can be stored as a 1×1 tensor; a colour photograph might be a 224×224×3 tensor; a batch of 32 such photographs would be 32×224×224×3. ONNX records the shape and data type of every tensor in the graph, which is exactly what lets a runtime allocate memory correctly and catch shape mismatches before they cause silent errors later.

Because operators are versioned through opsets, an ONNX file always declares which opset it targets. This matters in production: a model exported against opset 17 behaves identically no matter which machine or which month it is loaded on, because the runtime looks up the exact opset-17 definition of every operator it sees — a small but important piece of the reproducibility that MLOps practice depends on.

Worked Example: Exporting and Tracing a Fare-Prediction Model

The best way to see all of this fit together is to build the smallest possible model, export it to ONNX, and trace the numbers by hand.

Suppose we train a model that estimates an auto-rickshaw fare from trip distance, and it has learned two numbers: a per-kilometre rate of ₹12.5, and a base fare of ₹30. As a single linear layer, this is fare = 12.5 * distance + 30. Here is that model built in PyTorch, with the learned numbers set directly so the trace below is exact:

import torch
import torch.nn as nn

class FareModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(1, 1)   # one input feature, one output

    def forward(self, x):
        return self.linear(x)

model = FareModel()

with torch.no_grad():
    model.linear.weight.fill_(12.5)     # rupees per km
    model.linear.bias.fill_(30.0)       # base fare in rupees

model.eval()
dummy_input = torch.tensor([[8.0]])     # a sample 8 km trip

torch.onnx.export(
    model,
    dummy_input,
    "fare_model.onnx",
    input_names=["distance_km"],
    output_names=["fare_rupees"],
    opset_version=17,
)

torch.onnx.export traces the model's forward pass and writes out a .onnx file. If you load that file back with the onnx package and list its nodes, this tiny model turns out to contain exactly one:

import onnx

onnx_model = onnx.load("fare_model.onnx")
for node in onnx_model.graph.node:
    print(node.op_type, list(node.input), "->", list(node.output))

# Gemm ['distance_km', 'linear.weight', 'linear.bias'] -> ['fare_rupees']

(A free tool called Netron will draw this same graph as boxes and arrows instead of printed text — handy once a model has hundreds of nodes instead of one.)

Gemm stands for General Matrix Multiply, a standard ONNX operator, borrowed from a decades-old naming convention in numerical computing libraries, that computes Y = alpha * A * B + beta * C. A linear layer's entire job — multiply the input by a weight and add a bias — is exactly this pattern, which is why PyTorch's exporter represents nn.Linear as a single Gemm node. In our exported graph, alpha = 1, beta = 1, and the weight is used transposed (transB = 1), so the node computes:

  • A = the input, distance_km = [[8.0]] (a 1×1 tensor: one example, one feature)
  • B = linear.weight = [[12.5]], used transposed
  • C = linear.bias = [30.0]

Tracing it by hand: A × B(transposed) = [[8.0]] × [[12.5]] = [[8.0 * 12.5]] = [[100.0]]. Then add the bias: [[100.0]] + [30.0] = [[130.0]]. No training, no gradients, no hidden steps — just one multiplication and one addition, exactly as the Gemm formula defines.

Now hand that same fare_model.onnx file to ONNX Runtime, with PyTorch nowhere in sight:

import onnxruntime as ort
import numpy as np

session = ort.InferenceSession("fare_model.onnx")
result = session.run(
    ["fare_rupees"],
    {"distance_km": np.array([[8.0]], dtype=np.float32)},
)
print(result[0])   # [[130.]]

The output matches the hand trace exactly: ₹130 for an 8 km ride. Notice what did not happen here — nothing in this second script imports torch or knows that PyTorch was ever involved. The .onnx file carried the entire computation, and a completely different piece of software reproduced it exactly. That is interoperability, made concrete: one exported file, understood correctly by a tool that has never heard of the framework that created it.

One more detail worth flagging before moving on: by default, the exported graph fixes the batch dimension at whatever size was used while tracing — here, one ride at a time. Feeding it two rides at once fails with a shape error until the export step is explicitly told which dimensions are allowed to vary. Shapes in ONNX are not a suggestion; the graph enforces exactly what it was told at export time, which is a safety feature once you know to expect it.

ONNX Runtime: The Universal Player

A .onnx file is only half the story — something has to load it and actually run the computation. That is the job of ONNX Runtime, an open-source inference engine released by Microsoft in 2018. Inference is the industry term for running a trained, frozen model on new input to get a prediction, as opposed to training, which is the earlier process of learning the weights in the first place.

ONNX Runtime ships with official APIs for Python, C++, C#, Java, and JavaScript, among others. This is precisely what makes the fare-prediction example useful beyond a toy: a data science team can train and export in Python, and a production backend written in Java can load that exact same fare_model.onnx file through ONNX Runtime's Java API and get identical numbers, with nobody hand-translating the model's logic into Java line by line.

ONNX Runtime can also target different hardware through pluggable Execution Providers: a default CPU provider that runs anywhere, a CUDA provider that hands the same graph to an NVIDIA GPU for speed, and others tuned for specific chips, mobile devices, and browsers. The graph stored in the .onnx file never changes — only which Execution Provider carries it out, which is exactly the separation MLOps needs between what a model computes and what hardware computes it. Before it even reaches an Execution Provider, ONNX Runtime can also apply graph-level optimizations — folding constant computations and fusing chains of small operators into faster combined ones — and companion tools support quantization, shrinking a model's numbers from 32-bit floats down to 8-bit integers so it runs faster on constrained, battery-powered devices, at a small and measurable cost in accuracy.

Why This Matters in MLOps

MLOps is the discipline of taking a model from a data scientist's notebook to a reliable, monitored, reproducible production system, and interoperability sits right at the centre of that journey. Consider an Indian fintech company building fraud detection for UPI transactions. The data science team trains candidate models in Python, iterating quickly with PyTorch or scikit-learn on historical transaction patterns. But the system that approves or declines a live UPI transaction has a strict latency budget of a few hundred milliseconds, and it is already written as a high-throughput Java service handling millions of transactions a day.

Without ONNX, deploying the model would mean a second engineering team reimplementing the trained model's logic by hand in Java — slow, error-prone, and something that has to be redone every time the data science team retrains and improves the model. With ONNX, the workflow becomes: train in Python, export once with torch.onnx.export or the equivalent for scikit-learn or TensorFlow, and load the resulting file directly into the Java service with ONNX Runtime. Retraining the fraud model next month means exporting a new .onnx file and swapping it in — no rewritten Java code, no redeployment of business logic, no new place for a translation bug to creep in.

In a mature MLOps pipeline, that exported .onnx file becomes a versioned build artifact in its own right, stored in a model registry alongside its opset version and a record of the exact training run that produced it — tracked with the same discipline a software team would apply to a compiled binary and the source commit it came from. That reduction in repeated, error-prone manual work, paired with clean version tracking, is exactly what good MLOps practice is chasing.

Where ONNX Has Limits

ONNX is not magic, and a careful MLOps engineer should know where it strains. Standard architectures — the layers used in most CNNs, ordinary feedforward networks, and most classical scikit-learn models — export cleanly, because their operations map neatly onto ONNX's built-in operator set. Highly custom model code, especially models whose computation branches differently depending on the input data at run time, can be harder to trace faithfully into a fixed graph: a PyTorch forward method containing an if statement whose condition depends on a tensor's actual values, for example, may export into a graph that only reflects whichever branch happened to run for that one example — a classic pitfall worth testing for explicitly, not assuming away. An operator that is brand new in some framework is sometimes not yet defined in the ONNX standard either, which occasionally forces a wait for the next opset, or a custom operator implementation. None of this erases the value of the standard; it simply means that, like any production tool, ONNX has to be verified on the specific model being exported, never assumed to work by default.

Back to UPI: The Takeaway

Return to that chai payment for a moment. NPCI never asked PhonePe, Google Pay, and every bank in India to shut down and merge into a single app. It asked them to agree on one shared language, so that money could move freely between apps and banks that would otherwise never have spoken to each other. ONNX makes machine learning frameworks the same offer. PyTorch keeps doing what it does best for research, scikit-learn keeps doing what it does best for classical models, TensorFlow keeps its own production pipelines, and ONNX carries the trained result between them, out of a data scientist's notebook and into a Java backend, an Android app, or a browser, without anyone hand-translating the model along the way. The mental model worth carrying out of this chapter is short enough to remember in an exam or a production incident alike: train anywhere, export once to ONNX, run anywhere.

Key Takeaways — Summary and Recap

Let us recap what we covered: the core ideas behind onnx: model interoperability standard, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.

← Edge Deployment: ML on DevicesContainerization with Docker: Packaging Applications for Production →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn