Ga naar hoofdinhoud

ONNX: Train in Any Framework, Deploy on Any Hardware

Leer hoe je modellen converteert naar ONNX-indeling, ze optimaliseert met quantization en uitrolt op elk platform - van edge-apparaten tot cloudservers - zonder vendor lock-in.
· 15 min lezen

Verken met AI

Dus je hebt een model getraind in PyTorch, maar je productiesysteem draait op TensorFlow. Moet je dan alles vanaf nul herschrijven?

Nee, dat hoeft niet.

Modelinteroperabiliteit kan hoofdpijn veroorzaken voor machine learning engineers. Verschillende frameworks gebruiken verschillende formaten, en converteren ertussen kan je model breken of onverwacht gedrag introduceren. Dat wil je niet, maar je wilt ook geen meerdere versies van hetzelfde model onderhouden.

ONNX (Open Neural Network Exchange) lost dit op door een universeel formaat voor machinelearningmodellen te bieden. Het laat je in het ene framework trainen en in een ander deployen zonder code te herschrijven of met bugs te worstelen.

In dit artikel laat ik je zien hoe je modellen converteert naar ONNX-indeling, inference draait met ONNX Runtime, modellen optimaliseert voor productie en ze uitrolt op diverse platforms, van edge devices tot cloudservers.

Aan de slag met ONNX

Je kunt ONNX niet gebruiken zonder eerst je ontwikkelomgeving in te richten.

In deze sectie neem ik je mee door alles wat je nodig hebt - van softwarevereisten tot het creëren van een schone, reproduceerbare werkruimte. Ik laat je zien hoe je ONNX en ONNX Runtime installeert op Windows, Linux en macOS.

Vereisten

ONNX werkt op alle grote besturingssystemen.

Je hebt Python 3.8 of hoger nodig op je machine. Dat is het voor de basis. ONNX ondersteunt Python's ABI3 (Application Binary Interface), wat betekent dat je vooraf gebouwde binaire pakketten kunt gebruiken zonder vanaf de bron te compileren.

Dit ga je installeren:

  • uv (haal het van astral.sh/uv)

  • Python 3.8+ (uv regelt dit voor je)

  • pip (wordt meegeleverd met uv)

Wil je meer leren over hoe uv werkt en waarom het je standaardkeuze zou moeten zijn, lees dan onze gids over de snelste Python-package manager.

Voor Windows-gebruikers kun je uv installeren met PowerShell:

powershell -c "irm <https://astral.sh/uv/install.ps1> | iex"

Krijg je een fout over het uitvoeringsbeleid, voer PowerShell dan eerst uit als administrator.

Linux-gebruikers kunnen uv installeren met deze shell-opdracht:

curl -LsSf <https://astral.sh/uv/install.sh> | sh

Dit werkt op elke Linux-distributie.

Tot slot kunnen macOS-gebruikers uv installeren met Homebrew, of via een curl-opdracht:

brew install uv

# or

curl -LsSf <https://astral.sh/uv/install.sh> | sh

De omgeving instellen

Zo richt je een project in met uv:

mkdir onnx-project
cd onnx-project

# Initialize a uv project with Python 3.10
uv init --python 3.13

uv maakt een .venv-map en een pyproject.toml-bestand. De virtuele omgeving wordt automatisch geactiveerd wanneer je opdrachten via uv uitvoert.

Afbeelding 1 - uv-projectinstelling

Installeer nu ONNX en ONNX Runtime:

uv add onnx onnxruntime

# Verify the installation
uv run python -c "import onnx; print(onnx.__version__)"
uv run python -c "import onnxruntime; print(onnxruntime.__version__)"

De verificatie-opdrachten horen versienummers te printen:

Afbeelding 2 - ONNX- en ONNX Runtime-versies

Je afhankelijkheden staan al vastgepind. uv schrijft automatisch alle pakketversies naar uv.lock. Zo worden je builds reproduceerbaar - iedereen kan jouw exacte omgeving opnieuw creëren door uv sync uit te voeren.

Zo ziet je pyproject.toml eruit:

[project]
name = "onnx-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "onnx>=1.19.1",
    "onnxruntime>=1.23.2",
]

En dat was het, de omgeving is geconfigureerd! Laten we verdergaan met de basis.

De basis van ONNX begrijpen

Je moet begrijpen hoe ONNX werkt voordat je modellen gaat converteren.

In deze sectie splits ik het ONNX-formaat uit en leg ik de grafiekgebaseerde architectuur uit. Ik laat je zien wat er in een ONNX-bestand zit en hoe de gegevens erdoorheen stromen.

Het ONNX-modelformaat

Een ONNX-model is een bestand dat de structuur en gewichten van je neuraal netwerk bevat.

Zie het als een blauwdruk. Het bestand beschrijft welke operaties uitgevoerd moeten worden, in welke volgorde en met welke parameters. Je getrainde gewichten worden samen met deze blauwdruk opgeslagen, zodat het model klaar is om te draaien zonder extra bestanden.

ONNX werd in 2017 gelanceerd als een samenwerking tussen Microsoft en Facebook (nu Meta). Het doel was simpel: stop met het herschrijven van modellen telkens wanneer je van framework wisselt. PyTorch, TensorFlow en andere frameworks bleven verbeteren, maar modellen konden niet zonder handmatige conversie tussen hen bewegen.

ONNX veranderde dat. Versie 1.0 ondersteunde basisoperaties voor computervisie en eenvoudige neurale netwerken. De huidige ONNX ondersteunt transformers, grote taalmodellen en complexe architecturen die in 2017 niet bestonden.

Het gebruikt Protocol Buffers (protobuf) om deze gegevens te serialiseren. Protobuf is een binair formaat ontwikkeld door Google voor efficiënte gegevensopslag en -overdracht. Het is veel sneller te lezen en te schrijven dan JSON of XML, en de bestanden zijn kleiner. Win-win.

Dit zit er in een ONNX-model:

  • Graph: De netwerkarchitectuur
  • Weights: Getrainde modelparameters
  • Metadata: Versie-informatie, producentnaam en modeldocumentatie
  • Opset-versie: Welke operaties beschikbaar zijn

De opset-versie is belangrijk. ONNX evolueert in de tijd, en blijft nieuwe operaties toevoegen en bestaande verbeteren. Je modelbestand specificeert welke opset-versie het gebruikt, zodat de runtime weet hoe het uit te voeren.

Belangrijke concepten en architectuur

ONNX representeert je model als een computationele graaf.

Een graaf heeft knooppunten en randen. Knooppunten zijn operaties (zoals matrixvermenigvuldiging of activatiefuncties), en randen zijn tensors (je data) die tussen die operaties stromen. Zo beschrijft ONNX "vermenigvuldig deze matrices, pas dan ReLU toe, vermenigvuldig dan opnieuw".

Hier is een eenvoudig voorbeeld:

import onnx
from onnx import helper, TensorProto

# Create input and output tensors
input_tensor = helper.make_tensor_value_info(
    "input", TensorProto.FLOAT, [1, 3, 224, 224]
)
output_tensor = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1, 1000])

# Create a node (operation)
node = helper.make_node(
    "Relu",  # Operation type
    ["input"],  # Input edges 
    ["output"],  # Output edges
)

# Create the graph
graph = helper.make_graph(
    [node],  # List of nodes
    "simple_model",  # Graph name
    [input_tensor],  # Inputs
    [output_tensor],  # Outputs
)

# Create the model
model = helper.make_model(graph)
onnx.save(model, "simple_model.onnx")

Afbeelding 3 - Eenvoudig ONNX-voorbeeld

Elk knooppunt voert één operatie uit. Het operatietype (zoals Relu, Conv of MatMul) komt uit de ONNX-operatorset. Je kunt niet zomaar operatienamen verzinnen - ze moeten bestaan in de opset-versie die je gebruikt.

Randen verbinden knooppunten en dragen tensors. Een tensor heeft een vorm en een datatype. Wanneer je [1, 3, 224, 224] definieert, zeg je "deze tensor heeft 4 dimensies met deze groottes". De runtime gebruikt deze informatie om geheugen toe te wijzen en de graaf te valideren.

De graaf stroomt in één richting. Data komt binnen via inputknooppunten, gaat door operaties en verlaat via outputknooppunten. Geen cycli toegestaan - ONNX ondersteunt geen recursieve verbindingen direct. Je moet lussen uitrollen of specifieke operaties voor sequenties gebruiken.

Deze graafstructuur maakt frameworkinteroperabiliteit mogelijk. PyTorch denkt in termen van eager execution, TensorFlow gebruikt statische grafen en scikit-learn heeft een heel andere abstractie. Maar ze kunnen allemaal naar hetzelfde graafformaat exporteren.

De graaf maakt ook gedeelde optimalisatie mogelijk. Een ONNX-optimizer kan operaties fuseren (meerdere knooppunten combineren tot één), dode code elimineren (ongebruikte knooppunten verwijderen) of trage operaties vervangen door snellere equivalenten. Deze optimalisaties werken ongeacht welk framework het model heeft gemaakt.

Straks laat ik je stap voor stap zien hoe je modellen converteert naar ONNX-indeling.

Modellen converteren naar ONNX-indeling

In deze sectie laat ik je zien hoe je modellen exporteert vanuit PyTorch, TensorFlow en scikit-learn. Ik loop door het conversieproces en laat je zien hoe je controleert of je model na conversie correct werkt.

Frameworkondersteuning en tools

ONNX ondersteunt de populairste machinelearningframeworks.

PyTorch heeft ingebouwde ONNX-export via torch.onnx.export(). Het is de meest volwassen converter omdat zowel PyTorch als ONNX door Meta worden onderhouden.

TensorFlow gebruikt tf2onnx voor conversie. Het is een aparte bibliotheek die TensorFlow-grafen converteert naar ONNX-indeling. Installeer het met uv add tf2onnx, en je bent klaar om te gaan.

scikit-learn converteert via skl2onnx. Deze bibliotheek behandelt traditionele ML-modellen zoals random forests, lineaire regressie en SVM's. Deep learning-frameworks krijgen meer aandacht, maar scikit-learn-modellen werken net zo goed.

Dit zijn de drie populairste, maar je kunt elk van deze andere ondersteunde frameworks gebruiken - installeer gewoon de afhankelijkheid via uv add <name>:

  • Keras: Gebruik tf2onnx (Keras is onderdeel van TensorFlow)

  • XGBoost: Gebruik onnxmltools

  • LightGBM: Gebruik onnxmltools

  • MATLAB: Ingebouwde ONNX-export

  • Paddle: Gebruik paddle2onnx

Elk framework heeft zijn eigen converter omdat ze modellen verschillend representeren. PyTorch gebruikt dynamische computationele grafen, TensorFlow gebruikt statische grafen en scikit-learn gebruikt helemaal geen grafen. De converters vertalen deze representaties naar het graafformaat van ONNX.

Je kunt ook modellen gebruiken die door anderen zijn getraind. De ONNX Model Zoo host vooraf getrainde modellen die klaar zijn voor gebruik. Je vindt er computervisiemodellen (ResNet, YOLO, EfficientNet), NLP-modellen (BERT, GPT-2) en meer. Download ze van de officiële GitHub-repository.

Hugging Face host ook ONNX-modellen. Zoek naar modellen met "onnx" in de naam of filter op ONNX-indeling. Veel populaire transformermodellen zijn beschikbaar in ONNX, al geoptimaliseerd voor inference.

Voer voordat je verdergaat deze opdracht uit om alle vereiste bibliotheken met uv op te halen:

uv add torch torchvision tensorflow tf2onnx scikit-learn skl2onnx onnxscript

Stapsgewijs conversieproces

Zo converteer je een PyTorch-model naar ONNX:

import torch
import torch.nn as nn

# Define a simple model
class SimpleModel(nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.fc = nn.Linear(10, 5)

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

# Create and prepare the model
model = SimpleModel()
model.eval()  # Set to evaluation mode

# Create dummy input with the same shape as your real data
dummy_input = torch.randn(1, 10)

# Export to ONNX
torch.onnx.export(
    model,  # Model to export
    dummy_input,  # Example input
    "simple_model.onnx",  # Output file
    input_names=["input"],  # Name for the input
    output_names=["output"],  # Name for the output
    dynamic_shapes={"x": {0: "batch_size"}},  # Allow variable batch size
)

Nieuw in PyTorch? Laat dat je niet tegenhouden. Onze Deep Learning met PyTorch-cursus behandelt de basis in een paar uur.

De dummy-input is belangrijk. ONNX moet de inputvorm weten om de graaf te bouwen. Als je model variabele-lengtesequenties of verschillende batchgroottes accepteert, gebruik dan dynamic_shapes om die dimensies als dynamisch te markeren.

Zet je model altijd in evaluatiemodus met model.eval() vóór export. Dit schakelt het trainingsgedrag van dropout en batchnormalisatie uit. Als je deze stap overslaat, komt je geconverteerde model niet overeen met het origineel.

Dynamische vormen zorgen soms voor problemen. Als je converter klaagt over onbekende dimensies, zorg dan dat je dynamic_shapes correct specificeert of geef concrete vormen op.

Overschakelend naar TensorFlow, hier is alternatieve code die je kunt draaien:

import numpy as np
# Temporary compatibility fixes for tf2onnx
if not hasattr(np, "object"):
    np.object = object
if not hasattr(np, "cast"):
    np.cast = lambda dtype: np.asarray

import tensorflow as tf
import tf2onnx

# Define a simple Keras model (same spirit as your PyTorch one)
inputs = tf.keras.Input(shape=(10,), name="input")
outputs = tf.keras.layers.Dense(5, name="output")(inputs)
model = tf.keras.Model(inputs, outputs)

# Save the model (optional, if you want a .h5 file)
model.save("simple_tf_model.h5")

# Define a dynamic input spec (None = dynamic batch dimension)
spec = (tf.TensorSpec((None, 10), tf.float32, name="input"),)

# Convert to ONNX
model_proto, _ = tf2onnx.convert.from_keras(
    model, input_signature=spec, output_path="simple_tf_model.onnx"
)

Nog nooit met TensorFlow gewerkt? We raden onze cursus Introductie tot TensorFlow in Python aan.

Helaas is er wat conflict met de nieuwste versie van Numpy, dus ik heb een tijdelijke fix toegevoegd. Hopelijk kun je tegen de tijd dat je dit leest het fragment draaien zonder de eerste zes regels.

En tot slot, zo werkt ONNX-conversie voor scikit-learn-modellen:

from sklearn.ensemble import RandomForestClassifier
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

# Train a simple model
model = RandomForestClassifier(n_estimators=10)
model.fit(X_train, y_train)

# Define input type and shape
initial_type = [('float_input', FloatTensorType([None, 4]))]

# Convert to ONNX
onnx_model = convert_sklearn(model, initial_types=initial_type)

# Save the model
with open("sklearn_model.onnx", "wb") as f:
    f.write(onnx_model.SerializeToString())

Net als voor TensorFlow heeft DataCamp ook een gratis Supervised Learning met scikit-learn-cursus - bekijk die als je het fragment hierboven verwarrend vindt.

En dat was het!

Je kunt nu dit fragment draaien om je geconverteerde model te valideren:

import onnx
import onnxruntime as ort
import numpy as np

# Load and check the ONNX model
onnx_model = onnx.load("simple_model.onnx")
onnx.checker.check_model(onnx_model)

# Run inference with ONNX Runtime
session = ort.InferenceSession("simple_model.onnx")
input_name = session.get_inputs()[0].name

# Create test input
test_input = np.random.randn(1, 10).astype(np.float32)

# Get prediction
onnx_output = session.run(None, {input_name: test_input})

# Compare with original model
with torch.no_grad():
    original_output = model(torch.from_numpy(test_input))

# Check if outputs match (within floating point tolerance)
np.testing.assert_allclose(
    original_output.numpy(),
    onnx_output[0],
    rtol=1e-3,
    atol=1e-5
)
print("Model conversion successful - outputs match!")

Afbeelding 4 - Validatie van ONNX-model

Voer deze validatie elke keer uit wanneer je een model converteert. Kleine numerieke verschillen zijn normaal (zwevendekommagetallen zijn niet exact), maar grote verschillen betekenen dat er iets misging tijdens de conversie.

Om af te ronden, hier zijn twee belangrijke kanttekeningen waar je van op de hoogte moet zijn:

  • Aangepaste operaties worden niet geconverteerd: Als je aangepaste PyTorch-operaties of TensorFlow-lagen hebt geschreven, weet de converter niet hoe hij die moet vertalen. Je moet ze ofwel herschrijven met standaardoperaties of aangepaste ONNX-operators registreren.
  • Versiemismatches breken dingen: Je frameworkversie, converterversie en ONNX-versie moeten compatibel zijn. Controleer de documentatie van de converter voor ondersteunde versiecombinaties.

Dan nu ONNX Runtime.

Inference draaien met ONNX Runtime

In deze sectie laat ik je zien hoe je ONNX Runtime gebruikt om modellen te laden en voorspellingen te doen. Ik behandel hardwareversnelling en laat zien waar ONNX Runtime in productie uitblinkt.

Introductie tot ONNX Runtime

ONNX Runtime is een cross-platform inference-engine. Microsoft ontwikkelde het om ONNX-modellen snel te draaien op elke hardware - CPU's, GPU's, mobiele chips en gespecialiseerde AI-accelerators.

Zo werkt het: je laadt een ONNX-model, en ONNX Runtime bouwt een uitvoeringsplan. Het analyseert de computationele graaf, past optimalisaties toe en bepaalt de beste manier om operaties op jouw hardware uit te voeren. Vervolgens draait het inference met het geoptimaliseerde plan.

De architectuur scheidt de uitvoeringslogica van hardware-specifieke code. Execution providers verzorgen de hardware-interface. Wil je op NVIDIA GPU's draaien? Gebruik de CUDA execution provider. Wil je Apple Silicon? Je onderliggende code blijft hetzelfde.

ONNX Runtime heeft deze execution providers:

  • CPUExecutionProvider: Standaardprovider, werkt overal

  • CUDAExecutionProvider: NVIDIA GPU's met CUDA

  • TensorRTExecutionProvider: NVIDIA GPU's met TensorRT-optimalisatie

  • CoreMLExecutionProvider: Apple-apparaten (macOS, iOS)

  • DmlExecutionProvider: DirectML voor Windows (werkt met elke GPU)

  • OpenVINOExecutionProvider: Intel CPU's en GPU's

  • ROCMExecutionProvider: AMD GPU's

Elke provider vertaalt ONNX-operaties naar hardware-specifieke instructies. De CPU-provider gebruikt standaard CPU-operaties. De CUDA-provider gebruikt cuDNN en cuBLAS. De TensorRT-provider compileert je model naar geoptimaliseerde GPU-kernels, enzovoort.

Een model instellen en uitvoeren

Je kunt een ONNX-model laden met drie regels code:

import onnxruntime as ort

# Create an inference session
session = ort.InferenceSession("simple_model.onnx")

# Get input and output names
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name

De InferenceSession doet het zware werk. Het laadt het model, valideert de graaf en bereidt voor op inference. Standaard gebruikt het de CPU execution provider.

Draai nu inference:

import numpy as np
import onnxruntime as ort

# Create an inference session
session = ort.InferenceSession("simple_model.onnx")

# Get input and output names
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name

# Prepare input data
input_data = np.random.randn(1, 10).astype(np.float32)

# Run inference
outputs = session.run(
    [output_name],
    {input_name: input_data},
)

# Get predictions
predictions = outputs[0]
print(predictions.shape)

Afbeelding 5 - Een PyTorch-model draaien met ONNX

De run()-methode neemt twee argumenten. Eerst een lijst met outputnamen (of None voor alle outputs). Ten tweede een dictionary die inputnamen koppelt aan numpy-arrays.

Inputvormen moeten overeenkomen. Als je model [1, 10] verwacht en jij geeft [1, 10, 3] door, dan faalt de inference.

Wil je GPU-versnelling, specificeer dan execution providers bij het aanmaken van de sessie:

# For NVIDIA GPUs with CUDA
session = ort.InferenceSession(
    "model.onnx",
    providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
)

# For Apple Silicon
session = ort.InferenceSession(
    "model.onnx",
    providers=['CoreMLExecutionProvider', 'CPUExecutionProvider']
)

# For Intel hardware
session = ort.InferenceSession(
    "model.onnx",
    providers=['OpenVINOExecutionProvider', 'CPUExecutionProvider']
)

Zet providers in volgorde van prioriteit. ONNX Runtime probeert de eerste provider, en valt terug op de volgende als die niet beschikbaar is. Neem altijd CPUExecutionProvider als laatste fallback op.

Je kunt controleren welke provider daadwerkelijk wordt gebruikt:

import numpy as np
import onnxruntime as ort

# Create an inference session
session = ort.InferenceSession(
    "simple_model.onnx", providers=["CoreMLExecutionProvider", "CPUExecutionProvider"]
)

# Get input and output names
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name

# Prepare input data
input_data = np.random.randn(1, 10).astype(np.float32)

# Run inference
outputs = session.run(
    [output_name],
    {input_name: input_data},
)

# Get predictions
predictions = outputs[0]
print(predictions.shape)
print("Providers:")
print(session.get_providers())

Afbeelding 6 - Een provider wijzigen

Zie je ["CPUExecutionProvider"] terwijl je een GPU verwacht, dan is de GPU-provider niet beschikbaar. Vaak komt dat door ontbrekende drivers, een verkeerd ONNX Runtime-pakket of een incompatibele GPU.

De configuratie stopt hier niet. Je kunt de execution provider verder optimaliseren voor betere prestaties:

providers = [
    (
        "TensorRTExecutionProvider",
        {
            "device_id": 0,
            "trt_max_workspace_size": 2147483648,  # 2GB
            "trt_fp16_enable": True,  # Use FP16 precision
        },
    ),
    "CUDAExecutionProvider",
    "CPUExecutionProvider",
]

session = ort.InferenceSession("simple_model.onnx", providers=providers)

Raadpleeg de officiële documentatie van ONNX Runtime voor een lijst met alle beschikbare opties voor de provider van jouw keuze.

Toepassingen van ONNX Runtime

Het indrukwekkende aan ONNX Runtime is dat het overal draait - servers, browsers en mobiele apparaten.

ONNX Runtime Web brengt inference naar browsers met WebAssembly en WebGL. Je model draait client-side zonder data naar servers te sturen. Dit werkt uitstekend voor privacygevoelige toepassingen zoals medische beeldanalyse of documentverwerking.

Hier is een eenvoudig voorbeeld van hoe je ONNX Runtime in een simpele HTML-file kunt gebruiken:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>ONNXRuntime Web Demo</title>
</head>
<body>
  <h1>ONNXRuntime Web Demo</h1>

  <!-- Load ONNXRuntime-Web from CDN -->
  <script src="<https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js>"></script>

  <script>
    async function runModel() {
      // Create inference session
      const session = await ort.InferenceSession.create('./simple_model.onnx');

      // Prepare input
      const inputData = new Float32Array(10).fill(0).map(() => Math.random());
      const tensor = new ort.Tensor('float32', inputData, [1, 10]);

      // Run inference
      const results = await session.run({ input: tensor });

      const output = results.output;
      console.log('Output shape:', output.dims);
      console.log('Output values:', output.data);
    }

    runModel();
  </script>
</body>
</html>

Afbeelding 7 - ONNX Runtime in JavaScript

Voor mobiele uitrol kun je ONNX Runtime Mobile gebruiken - een lichtgewicht versie, geoptimaliseerd voor iOS en Android. Het stript onnodige features en verkleint de binaire grootte.

Dit is een voorbeeldconfiguratie voor iOS:

import onnxruntime_objc

let modelPath = Bundle.main.path(forResource: "simple_model", ofType: "onnx")!
let session = try ORTSession(modelPath: modelPath)

let inputData = // Your input as Data
let inputTensor = try ORTValue(tensorData: inputData,
                               elementType: .float,
                               shape: [1, 10])

let outputs = try session.run(withInputs: ["input": inputTensor])

Of, als je Python gebruikt om een REST API te bouwen, kun je ONNX Runtime aan een van je endpoints toevoegen met deze aanpak:

from fastapi import FastAPI
import onnxruntime as ort
import numpy as np

app = FastAPI()
session = ort.InferenceSession("simple_model.onnx")

@app.post("/predict")
async def predict(data: dict):
    input_data = np.array(data["input"]).astype(np.float32)
    outputs = session.run(None, {"input": input_data})
    return {"prediction": outputs[0].tolist()}

En dat was het! Andere omgevingen gebruiken een vergelijkbare aanpak, maar je snapt het punt - ONNX Runtime is eenvoudig te gebruiken. Laten we nu optimalisatie bespreken.

ONNX-modellen optimaliseren

In deze sectie laat ik zien hoe je ONNX-modellen sneller en kleiner maakt via quantization en graafoptimalisaties. Ik behandel de technieken die het meest tellen voor productie.

Quantization-technieken

Quantization verlaagt de precisie van je model om het sneller en kleiner te maken.

In plaats van gewichten als 32-bits floats op te slaan, sla je ze op als 8-bits integers. Dat vermindert het geheugen met 75% en versnelt inference omdat integerrekenwerk op de meeste hardware sneller is dan floating point.

Dynamische quantization converteert gewichten naar lagere precisie, maar houdt activaties (intermediaire waarden tijdens inference) in volle precisie. Het is de gemakkelijkste methode omdat je geen calibratiedata nodig hebt.

Zo pas je dynamische quantization toe:

from onnxruntime.quantization import quantize_dynamic, QuantType

# Quantize the model
quantize_dynamic(
    model_input="model.onnx",
    model_output="model_quantized.onnx",
    weight_type=QuantType.QUInt8  # 8-bit unsigned integers
)

Dat is alles. Je gequantizeerde model gebruikt nu 8-bits unsigned integers en is klaar voor gebruik.

Statische quantization converteert zowel gewichten als activaties naar lagere precisie. Het is agressiever en sneller dan dynamische quantization, maar je hebt representatieve calibratiedata nodig om activatiebereiken te meten.

Hier is een algemene strategie om statische quantization toe te passen:

from onnxruntime.quantization import quantize_static, CalibrationDataReader
import numpy as np

# Create a calibration data reader
class DataReader(CalibrationDataReader):
    def __init__(self, calibration_data):
        self.data = calibration_data
        self.index = 0

    def get_next(self):
        if self.index >= len(self.data):
            return None
        batch = {"input": self.data[self.index]}
        self.index += 1
        return batch

# Load calibration data (100-1000 samples from your dataset)
calibration_data = [np.random.randn(1, 10).astype(np.float32)
                    for _ in range(100)]
data_reader = DataReader(calibration_data)

# Quantize
quantize_static(
    model_input="model.onnx",
    model_output="model_static_quantized.onnx",
    calibration_data_reader=data_reader
)

De calibratiedata moet je daadwerkelijke inference-werkbelasting representeren, gebruik dus geen willekeurige data zoals hierboven getoond. Gebruik echte samples uit je dataset.

In het algemeen heeft quantization voordelen en trade-offs waar je als machine learning engineer van op de hoogte moet zijn. Dit zijn de voordelen:

  • 4x kleinere modellen: INT8 gebruikt een kwart van het geheugen van FP32
  • 2-4x snellere inference: Integeroperaties draaien sneller op CPU's
  • Lager energieverbruik: Minder berekening betekent minder energie
  • Betere cachebenutting: Kleinere modellen passen in de CPU-cache

En dit zijn de trade-offs:

  • Kleine nauwkeurigheidsverlies: Meestal 1-2% voor INT8, acceptabel voor de meeste toepassingen
  • Niet alle operaties quantizen goed: Batchnormalisatie en softmax zijn lastig
  • Hardwareafhankelijk: GPU-snelheidswinsten variëren, sommige GPU's versnellen INT8 niet

Quantization blinkt ook uit voor grote taalmodellen en generatieve AI. Een model met 7B parameters in FP32 neemt 28GB geheugen in. Gequantizeerd naar INT8 daalt dat naar 7GB. Naar INT4 is het 3,5GB - klein genoeg om op consumentenhardware te draaien.

Graafoptimalisaties

Graafoptimalisaties herschrijven de computationele graaf van je model om het sneller te maken.

Het concept van node-fusie combineert meerdere operaties in één operatie. Als je model een Conv-laag heeft gevolgd door BatchNorm en daarna ReLU, fuseert de optimizer ze tot één ConvBnRelu-knooppunt. Dit vermindert geheugentransport en versnelt de uitvoering.

Zo ziet de graaf eruit vóór fusie:

Input -> Conv -> BatchNorm -> ReLU -> Output

Hij is vereenvoudigd na fusie:

Input -> ConvBnRelu -> Output

Er zijn nog twee concepten die je moet kennen bij graafoptimalisatie:

  • Constant folding precomputeert operaties die niet van inputdata afhangen. Als je model een gewichtenmatrix met een constante vermenigvuldigt, doet de optimizer die vermenigvuldiging één keer tijdens optimalisatie in plaats van bij elke inference.
  • Dead code elimination verwijdert ongebruikte operaties. Als je model takken heeft die nooit worden uitgevoerd of outputs die je nooit gebruikt, snijdt de optimizer ze weg.

Zo kun je graafoptimalisaties inschakelen in ONNX Runtime:

import onnxruntime as ort

# Set optimization level
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL

# Create session with optimizations
session = ort.InferenceSession(
    "simple_model.onnx",
    sess_options,
    providers=["CPUExecutionProvider"]
)

ONNX Runtime heeft de volgende optimalisatieniveaus:

  • ORT_DISABLE_ALL: Geen optimalisaties (handig voor debuggen)

  • ORT_ENABLE_BASIC: Veilige optimalisaties die numerieke resultaten niet veranderen

  • ORT_ENABLE_EXTENDED: Agressievere optimalisaties kunnen kleine numerieke verschillen geven

  • ORT_ENABLE_ALL: Alle optimalisaties, inclusief layouttransformaties

Gebruik ORT_ENABLE_ALL voor productie. De snelheidswinst is de kleine numerieke verschillen waard.

Je kunt dit fragment draaien om geoptimaliseerde modellen op te slaan voor hergebruik:

sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
sess_options.optimized_model_filepath = "model_optimized.onnx"

# This creates and saves the optimized model
session = ort.InferenceSession("simple_model.onnx", sess_options)

Graafoptimalisaties werken ongeacht welk framework je model heeft gemaakt. Een PyTorch Conv-BN-ReLU-patroon en een TensorFlow Conv-BN-ReLU-patroon worden op dezelfde manier gefuseerd. Dit is het voordeel van gedeelde optimalisatie - je schrijft de optimalisatie één keer en past die toe op modellen uit elk framework.

Laten we nu deployment bespreken.

Deploymentscenario's

In deze sectie behandel ik drie deploymentscenario's - edge-apparaten, cloudinfrastructuur en webbrowsers. Ik laat de uitdagingen en oplossingen voor elk zien.

Deployment op edge-apparaten

De kernuitdaging van edge-apparaten is dat ze beperkte resources hebben.

Je smartphone heeft 4-16GB RAM. Een Raspberry Pi heeft nog minder. IoT-apparaten hebben misschien 512MB of minder. Je kunt niet zomaar een model met 7B parameters op deze apparaten zetten en verwachten dat het werkt.

Begin met quantization. INT8-modellen zouden je basis moeten zijn. Ga zelfs naar INT4 als je het nauwkeurigheidsverlies kunt tolereren. Zo krijg je je model klein genoeg om in het geheugen te passen.

Voor deployment op edge-apparaten is ONNX Runtime Mobile je vriend. Het stript serverfeatures die je niet nodig hebt en optimaliseert voor batterijduur. De binary is kleiner, de opstart sneller en het stroomverbruik lager.

Zo voeg je het toe aan je mobiele app-project:

# iOS
pod 'onnxruntime-mobile-objc'

# Android
implementation 'com.microsoft.onnxruntime:onnxruntime-android:latest.version'

Execution providers handelen hardwareverschillen af. Android-apparaten gebruiken ARM-CPU's, sommige hebben GPU's van verschillende leveranciers en nieuwere telefoons hebben NPU's (Neural Processing Units). Je wilt geen code voor elke chip schrijven.

Gebruik dit fragment om de juiste execution provider te kiezen:

# iOS - use CoreML for Apple Silicon optimization
session = ort.InferenceSession(
    "simple_model.onnx",
    providers=["CoreMLExecutionProvider", "CPUExecutionProvider"]
)

# Android - use NNAPI for hardware acceleration
session = ort.InferenceSession(
    "simple_model.onnx",
    providers=["NnapiExecutionProvider", "CPUExecutionProvider"]
)

CoreML op iOS gebruikt de Neural Engine (de NPU van Apple). NNAPI op Android gebruikt welke accelerator je apparaat ook heeft - GPU, DSP of NPU. De provider abstraheert de hardware zodat je code hetzelfde blijft. Wanneer er een nieuwe, snellere chip uitkomt, hoef je alleen de execution provider te updaten en draait je app sneller zonder codewijzigingen.

Clouddeployment

Clouddeployment geeft je onbeperkte schaal en de nieuwste hardware.

Je kunt ONNX-modellen op elk cloudplatform deployen - AWS, Azure, GCP of je eigen servers. Het patroon is hetzelfde - containeriseer je inference-service en zet die achter een load balancer.

Hier is een kleine Python FastAPI-voorbeeld:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import onnxruntime as ort
import numpy as np
import logging

app = FastAPI()
logger = logging.getLogger(__name__)

# Load model at startup
session = None

@app.on_event("startup")
async def load_model():
    global session
    session = ort.InferenceSession(
        "simple_model.onnx",
        providers=[
            "CUDAExecutionProvider",
            "CoreMLExecutionProvider",
            "CPUExecutionProvider",
        ],
    )
    logger.info("Model loaded successfully")

class PredictionRequest(BaseModel):
    input: list

class PredictionResponse(BaseModel):
    prediction: list
    inference_time_ms: float

@app.post("/predict", response_model=PredictionResponse)
async def predict(request: PredictionRequest):
    try:
        import time

        start = time.time()

        # Input shape: (1, 10)
        input_data = np.array(request.input, dtype=np.float32).reshape(1, 10)
        input_name = session.get_inputs()[0].name
        outputs = session.run(None, {input_name: input_data})

        inference_time = (time.time() - start) * 1000

        return PredictionResponse(
            prediction=outputs[0].tolist(), inference_time_ms=inference_time
        )
    except Exception as e:
        logger.error(f"Prediction failed: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/health")
async def health():
    return {"status": "healthy", "model_loaded": session is not None}

Dit FastAPI-voorbeeld, hoewel klein, is niet het makkelijkst te doorgronden voor machine learning engineers. Je kunt de basis onder de knie krijgen met onze gratis Introductie tot FastAPI-cursus.

Je kunt het vervolgens containerizen met Docker:

FROM python:3.13-slim

WORKDIR /app

# Install dependencies
RUN pip install onnx onnxruntime onnxscript fastapi pydantic numpy uvicorn

# Copy model and code
COPY simple_model.onnx .
COPY main.py .

# Expose port
EXPOSE 8000

# Run the service
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Elke machinelearningliefhebber moet de basis van Docker kennen. Volg onze Introductie tot Docker-cursus om eindelijk te leren hoe je een machinelearningmodel deployt.

Je krijgt een applicatie draaiend op poort 8000, waarvan je de documentatie direct kunt openen en het prediction-endpoint kunt uitproberen:

Afbeelding 8 - ONNX-model gedeployed met FastAPI

Als je Azure Machine Learning gebruikt, is het goed om te weten dat het direct integreert met ONNX. Je kunt je model in drie stappen deployen:

from azureml.core import Workspace, Model
from azureml.core.webservice import AciWebservice, Webservice

# Register model
ws = Workspace.from_config()
model = Model.register(
    workspace=ws, model_path="simple_model.onnx", model_name="my-onnx-model"
)

# Deploy
deployment_config = AciWebservice.deploy_configuration(cpu_cores=1, memory_gb=1)

service = Model.deploy(
    workspace=ws,
    name="onnx-service",
    models=[model],
    deployment_config=deployment_config,
)

Azure verzorgt schalen, monitoring en updates. Je krijgt automatische logging, request tracing en health checks.

Ben je nieuw met Azure Machine Learning, lees dan onze Beginner's Guide om snel de basis te leren.

Als we het hebben over MLOps-workflows heb je deze componenten nodig:

  • Versiebeheer: Volg modelversies met DVC of MLflow
  • CI/CD: Automatiseer testen en deployment met GitHub Actions of Azure Pipelines
  • Monitoring: Volg inference-latency, throughput en foutpercentages
  • A/B-testen: Vergelijk modelversies in productie
  • Rollback: Ga terug naar eerdere versies wanneer er problemen optreden

Hier is een voorbeeldfragment dat je kunt gebruiken:

import prometheus_client as prom
import onnxruntime as ort
import numpy as np

# Define metrics
inference_duration = prom.Histogram(
    "model_inference_duration_seconds", "Time spent processing inference"
)

inference_count = prom.Counter("model_inference_total", "Total number of inferences")

# Load model
session = ort.InferenceSession("simple_model.onnx")

@app.post("/predict")
@inference_duration.time()
async def predict(request: PredictionRequest):
    inference_count.inc()
    # Input shape: (1, 10)
    input_data = np.array(request.input, dtype=np.float32).reshape(1, 10)
    input_name = session.get_inputs()[0].name
    outputs = session.run(None, {input_name: input_data})
    return {"prediction": outputs[0].tolist()}

Vanaf daar kun je horizontaal schalen door meerdere service-instanties achter een load balancer te draaien. ONNX Runtime is stateless, dus elke instantie kan elke request afhandelen.

Inference in de browser

ONNX Runtime Web draait modellen rechtstreeks in browsers met WebAssembly en WebGL.

In plaats van data naar een server te sturen, stuur je het model één keer naar de browser en draai je inference lokaal. Gebruikers krijgen directe respons zonder netwerklatentie.

Begin eenvoudig door een HTML-bestand te maken met een inline JS-script:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <script src="<https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js>"></script>
  </head>
  <body>
    <script>
      async function run() {
        try {
          const session = await ort.InferenceSession.create("simple_model.onnx", {
            executionProviders: ["wasm"]
          });
          console.log("Session loaded:", session);

          // Inspect names
          console.log("Inputs:", session.inputNames);
          console.log("Outputs:", session.outputNames);

          const inputName = session.inputNames[0];

          const inputData = new Float32Array(10).fill(0).map(() => Math.random());
          const tensor = new ort.Tensor("float32", inputData, [1, 10]);

          const feeds = {};
          feeds[inputName] = tensor;

          const results = await session.run(feeds);

          const outputName = session.outputNames[0];
          console.log("Prediction:", results[outputName].data);

        } catch (err) {
          console.error("ERROR:", err);
        }
      }

      run();
    </script>
  </body>
</html>

Je ziet de output in de console zodra je het bestand met live server opent:

Afbeelding 9 - ONNX Runtime met JavaScript

En tot slot bespreken we een paar geavanceerde onderwerpen.

Geavanceerde onderwerpen

Standaard ONNX-operaties dekken de meeste use-cases, maar als je meer nodig hebt, is dit de sectie voor jou.

Ik behandel aangepaste operators en deployment van grote taalmodellen. Ik leg uit wanneer je deze geavanceerde features nodig hebt en welke trade-offs ze meebrengen.

Aangepaste operators

ONNX wordt geleverd met honderden operaties, maar jouw model kan iets unieks gebruiken.

Aangepaste operators laten je operaties definiëren die niet in de standaard ONNX-operatorset bestaan. Je schrijft de operatielogica zelf en vertelt ONNX Runtime hoe die uit te voeren. Dit breidt ONNX uit voorbij de ingebouwde mogelijkheden.

Wanneer heb je aangepaste operators nodig? Wanneer je vooroplopende research gebruikt die ONNX nog niet ondersteunt. Of wanneer je propriëtaire operaties hebt gebouwd die specifiek zijn voor jouw domein. Of wanneer je hardware-specifieke optimalisaties nodig hebt die standaard-ops niet kunnen bieden.

Maar hier zit 'm de crux - aangepaste operators breken portabiliteit. Je model draait niet op systemen die jouw implementatie van de aangepaste operator niet hebben. Als je een PyTorch-model met custom ops naar ONNX exporteert, moet je die ops apart verpakken en registreren bij ONNX Runtime.

Deployment wordt lastiger. Elke omgeving heeft jouw bibliotheek met aangepaste operators nodig. Edge-apparaten, cloudservers en browsers hebben allemaal dezelfde implementatie nodig. Je verliest het grootste voordeel van ONNX: write once, run anywhere.

Hier is een klein werkend voorbeeld van aangepaste operators, zodat je ziet hoe ze werken:

import numpy as np
import onnxruntime as ort
from onnxruntime import InferenceSession, SessionOptions

# Define the custom operation
def custom_square(x):
    return x * x

# Register the custom op with ONNX Runtime
# This tells ONNX Runtime how to execute "CustomSquare" operations
class CustomSquareOp:
    @staticmethod
    def forward(x):
        return np.square(x).astype(np.float32)

# Create a simple ONNX model with a custom op using the helper
from onnx import helper, TensorProto
import onnx

# Create a graph with custom operator
node = helper.make_node(
    "CustomSquare",  # Custom operation name
    ["input"],
    ["output"],
    domain="custom.ops",  # Custom domain
)

graph = helper.make_graph(
    [node],
    "custom_op_model",
    [helper.make_tensor_value_info("input", TensorProto.FLOAT, [1, 10])],
    [helper.make_tensor_value_info("output", TensorProto.FLOAT, [1, 10])],
)

model = helper.make_model(graph)
onnx.save(model, "custom_op_model.onnx")

# To use this model, you'd need to implement the custom op in C++
# and register it with ONNX Runtime - Python-only custom ops aren't supported
print("Custom operator model created - requires C++ implementation to run")

De meeste teams vermijden aangepaste operators waar mogelijk. Probeer eerst combinaties van standaard-ops te gebruiken. Ga alleen custom als standaard-ops je operatie niet kunnen uitdrukken of wanneer performance het vereist.

Omgaan met grote taalmodellen

Geheugen is een probleem bij LLM's. Een model met 7B parameters heeft 28GB nodig in FP32, 14GB in FP16 en 7GB in INT8. De meeste consumentenharde waar kan dat niet aan. Je hebt quantization nodig, vaak agressief naar INT4 of INT3.

Ben je nieuw met LLM's en wil je meer leren, dan is onze Large Language Models (LLMs) Concepts-cursus een goed startpunt.

KV-cachebeheer is belangrijk voor autoregressieve generatie. Elk token dat je genereert moet alle voorgaande tokens kunnen refereren. De cache groeit mee met de sequentielengte. Lange gesprekken eten snel geheugen. ONNX Runtime GenAI bevat geoptimaliseerd KV-cachebeheer dat geheugen hergebruikt en overhead vermindert.

Inferentiesnelheid hangt af van je hardware en modelgrootte. Kleinere modellen (onder 3B parameters) draaien goed op consumentengpu's. Grotere modellen hebben meerdere GPU's of gespecialiseerde inference-accelerators nodig. ONNX Runtime ondersteunt multi-GPU-inference via execution providers, maar je moet je model handmatig over apparaten splitsen.

Flash Attention versnelt het attentiemechanisme in transformers. Standaard attentie is O(n²) in sequentielengte - dat wordt snel traag. Flash Attention vermindert geheugentransfers en verbetert de snelheid zonder resultaten te veranderen. ONNX Runtime bevat Flash Attention-optimalisaties voor ondersteunde hardware.

LLM's trainen met ONNX Runtime is mogelijk maar zeldzaam. De meeste teams trainen met PyTorch of JAX en exporteren daarna naar ONNX voor alleen inference. ONNX Runtime Training bestaat voor fine-tuning en verdere pretraining, maar het ecosysteem rond PyTorch-training is volwassener.

Generatieve AI-workloads krijgen speciale behandeling in ONNX Runtime. De GenAI-extensies bieden high-level API's voor tekstgeneratie, het behouden van state tussen calls en het beheren van beam search of samplingstrategieën. Zo hoef je de generatielogica niet zelf te implementeren.

Het LLM-landschap verandert snel, en ONNX Runtime-updates doen hun best om bij te blijven. De operatorset en optimalisaties worden regelmatig bijgewerkt, maar er is altijd een vertraging tussen research en production-ready ondersteuning.

Conclusie

Je weet nu hoe je modellen uit elk framework converteert, ze optimaliseert voor productie en ze overal uitrolt.

ONNX haalt de frictie weg tussen training en deployment. Train in PyTorch omdat het geweldig is voor research. Deploy met ONNX Runtime omdat het snel is en overal draait. Quantize naar INT8 en halveer je inferencekosten. Schakel over van CPU naar GPU zonder code te wijzigen. Verplaats modellen tussen cloudproviders zonder vendor lock-in.

ONNX maakt het mogelijk.

Het ecosysteem blijft verbeteren. Ondersteuning voor generatieve AI wordt met elke release beter - snellere LLM-inference, beter geheugengebruik en nieuwe optimalisatietechnieken. Hardware-ondersteuning breidt uit naar meer accelerators en edge-apparaten. De community bouwt tools die ONNX makkelijker maken. Je kunt meedoen door nieuwe functies te testen, issues te melden of code bij te dragen via de GitHub-repositories.

Klaar voor de volgende stap? Beheers Explainable Artificial Intelligence (XAI) Concepts en stop eindelijk met modellen als black boxes te zien.

Dario Radečić

Auteur

Dario Radečić

Senior Data Scientist, gevestigd in Kroatië. Top Tech-schrijver met meer dan 700 gepubliceerde artikelen en meer dan 10 miljoen weergaven. Auteur van het boek Machine Learning Automation with TPOT.

FAQs

Wat is ONNX en waarom zou ik het gebruiken?

ONNX (Open Neural Network Exchange) is een universeel formaat voor machinelearningmodellen dat over verschillende frameworks werkt. Het laat je een model trainen in PyTorch of TensorFlow en het overal deployen zonder code te herschrijven. Je krijgt frameworkonafhankelijkheid, betere prestaties via optimalisaties en de mogelijkheid om modellen op elke hardware te draaien, van edge-apparaten tot cloudservers.

Hoe verbetert ONNX de modelprestaties?

ONNX Runtime past graafoptimalisaties toe zoals node-fusie en constant folding die inference versnellen. Quantization verkleint het model met tot 75% en maakt inference 2-4x sneller door gewichten van 32-bits floats naar 8-bits integers te converteren. Deze optimalisaties werken automatisch en vereisen niet dat je je model handmatig afstemt.

Kan ONNX op mobiele apparaten en in browsers draaien?

Ja. ONNX Runtime Mobile draait op iOS- en Android-apparaten met optimalisaties voor batterijduur en geheugengebruik. ONNX Runtime Web draait modellen rechtstreeks in browsers met WebAssembly en WebGL, zodat je client-side inference kunt doen zonder data naar servers te sturen. Beide opties ondersteunen hardwareversnelling via execution providers zoals CoreML voor Apple-apparaten en NNAPI voor Android.

Wat is het verschil tussen dynamische en statische quantization?

Dynamische quantization converteert gewichten naar lagere precisie maar houdt activaties in volle precisie, en vereist geen calibratiedata. Statische quantization converteert zowel gewichten als activaties naar lagere precisie, wat sneller is maar representatieve calibratiedata vereist om activatiebereiken te meten. Statische quantization geeft betere prestaties maar vergt meer setup.

Hoe ga ik om met modellen met aangepaste operaties bij het converteren naar ONNX?

Aangepaste operaties vereisen C++-implementaties die bij ONNX Runtime zijn geregistreerd - alleen Python-custom ops worden niet ondersteund voor productiegebruik. Je moet de aangepaste operator in C++ implementeren, als gedeelde bibliotheek compileren en registreren bij ONNX Runtime in elke deploymentomgeving. De meeste teams vermijden custom ops en gebruiken combinaties van standaard ONNX-operaties, omdat custom ops portabiliteit tussen platforms breken.

Onderwerpen