Vai al contenuto principale

ONNX: allena in qualsiasi framework, distribuisci su qualsiasi hardware

Scopri come convertire i modelli in formato ONNX, ottimizzarli con la quantizzazione e distribuirli su qualsiasi piattaforma, dai dispositivi edge ai server cloud, senza lock-in del fornitore.
· 15 min leggi

Scopri con l'IA

Hai addestrato un modello in PyTorch, ma il tuo sistema in produzione esegue TensorFlow. Devi riscrivere tutto da zero?

No, non serve.

L’interoperabilità dei modelli può dare grattacapi agli ingegneri di machine learning. I diversi framework usano formati differenti, e convertirli può rompere il modello o introdurre comportamenti inattesi. Non lo vuoi, ma non vuoi nemmeno mantenere più versioni dello stesso modello.

ONNX (Open Neural Network Exchange) risolve il problema fornendo un formato universale per i modelli di machine learning. Ti permette di addestrare in un framework e distribuire in un altro senza riscrivere codice o inseguire bug.

In questo articolo ti mostrerò come convertire i modelli in formato ONNX, eseguire inferenza con ONNX Runtime, ottimizzare i modelli per la produzione e distribuirli su varie piattaforme, dai dispositivi edge ai server cloud.

Primi passi con ONNX

Non puoi usare ONNX senza prima configurare l’ambiente di sviluppo.

In questa sezione ti guiderò in tutto ciò che ti serve, dai requisiti software alla creazione di uno spazio di lavoro pulito e riproducibile. Ti mostrerò come installare ONNX e ONNX Runtime su Windows, Linux e macOS.

Prerequisiti

ONNX funziona su tutti i principali sistemi operativi.

Ti serve Python 3.8 o superiore installato sulla macchina. Questo è tutto per le basi. ONNX supporta l’ABI3 (Application Binary Interface) di Python, il che significa che puoi usare pacchetti binari precompilati senza compilare dai sorgenti.

Ecco cosa installerai:

  • uv (scaricalo da astral.sh/uv)

  • Python 3.8+ (uv lo gestisce per te)

  • pip (viene con uv)

Se vuoi capire meglio come funziona uv e perché dovrebbe essere la tua scelta principale, leggi la nostra guida al più veloce gestore di pacchetti Python.

Per chi usa Windows, puoi installare uv con PowerShell:

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

Se ricevi un errore di criteri di esecuzione, avvia prima PowerShell come amministratore.

Gli utenti Linux possono installare uv con questo comando da shell:

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

Funziona su qualsiasi distribuzione Linux.

Infine, gli utenti macOS possono installare uv con Homebrew o con un comando curl:

brew install uv

# or

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

Configurare l’ambiente

Ecco come impostare un progetto con uv:

mkdir onnx-project
cd onnx-project

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

uv crea una directory .venv e un file pyproject.toml. L’ambiente virtuale si attiva automaticamente quando esegui comandi tramite uv.

Image 1 - uv project setup

Ora installa ONNX e 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__)"

I comandi di verifica dovrebbero stampare i numeri di versione:

Image 2 - ONNX and ONNX Runtime. versions

Le dipendenze sono già bloccate. uv scrive automaticamente tutte le versioni dei pacchetti in uv.lock. Questo rende le build riproducibili: chiunque può ricreare il tuo identico ambiente eseguendo uv sync.

Ecco come appare il tuo pyproject.toml:

[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",
]

E questo è tutto, l’ambiente è configurato! Passiamo ai fondamentali.

Capire le basi di ONNX

Devi capire come funziona ONNX prima di iniziare a convertire modelli.

In questa sezione analizzerò il formato ONNX e spiegherò la sua architettura basata su grafi. Ti mostrerò cosa c’è dentro un file ONNX e come i dati scorrono al suo interno.

Il formato del modello ONNX

Un modello ONNX è un file che contiene la struttura e i pesi della tua rete neurale.

Pensalo come un progetto. Il file descrive quali operazioni eseguire, in che ordine e con quali parametri. I pesi addestrati sono memorizzati accanto a questo progetto, quindi il modello è pronto per l’esecuzione senza file aggiuntivi.

ONNX è stato lanciato nel 2017 come collaborazione tra Microsoft e Facebook (oggi Meta). L’obiettivo era semplice: smettere di riscrivere i modelli ogni volta che si cambia framework. PyTorch, TensorFlow e altri framework continuavano a migliorare, ma i modelli non potevano muoversi tra di loro senza conversioni manuali.

ONNX ha cambiato le carte in tavola. La versione 1.0 supportava operazioni di base per la computer vision e reti semplici. L’ONNX di oggi supporta transformer, large language model e architetture complesse che nel 2017 non esistevano.

Usa Protocol Buffers (protobuf) per serializzare i dati. Protobuf è un formato binario sviluppato da Google per un’archiviazione e trasmissione dati efficienti. È molto più veloce da leggere e scrivere rispetto a JSON o XML, e i file sono più piccoli. Un doppio vantaggio.

Ecco cosa c’è dentro un modello ONNX:

  • Grafo: l’architettura della rete
  • Pesi: i parametri del modello addestrato
  • Metadati: informazioni di versione, nome del produttore e documentazione del modello
  • Versione dell’opset: quali operazioni sono disponibili

La versione dell’opset conta. ONNX evolve nel tempo e continua ad aggiungere nuove operazioni e a migliorare quelle esistenti. Il file del modello specifica quale opset utilizza, così il runtime sa come eseguirlo.

Concetti chiave e architettura

ONNX rappresenta il tuo modello come un grafo computazionale.

Un grafo ha nodi e archi. I nodi sono operazioni (come moltiplicazione di matrici o funzioni di attivazione) e gli archi sono tensori (i tuoi dati) che scorrono tra queste operazioni. È così che ONNX descrive: "moltiplica queste matrici, poi applica ReLU, poi moltiplica di nuovo".

Ecco un semplice esempio:

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")

Image 3 - Simple ONNX example

Ogni nodo esegue un’operazione. Il tipo di operazione (come Relu, Conv o MatMul) proviene dall’insieme di operatori ONNX. Non puoi inventarti nomi di operazioni: devono esistere nella versione dell’opset che stai usando.

Gli archi collegano i nodi e trasportano i tensori. Un tensore ha una forma e un tipo di dato. Quando definisci [1, 3, 224, 224], stai dicendo "questo tensore ha 4 dimensioni con queste dimensioni". Il runtime usa queste informazioni per allocare memoria e validare il grafo.

Il grafo scorre in una sola direzione. I dati entrano dai nodi di input, passano attraverso le operazioni ed escono dai nodi di output. Niente cicli: ONNX non supporta direttamente connessioni ricorrenti. Devi svolgere i cicli o usare operazioni specifiche per le sequenze.

Questa struttura a grafo abilita l’interoperabilità tra framework. PyTorch ragiona in termini di esecuzione eager, TensorFlow usa grafi statici e scikit-learn ha un’astrazione completamente diversa. Ma possono tutti esportare nello stesso formato a grafo.

Il grafo abilita anche ottimizzazioni condivise. Un ottimizzatore ONNX può fondere operazioni (combinare più nodi in uno), eliminare codice morto (rimuovere nodi inutilizzati) o sostituire operazioni lente con equivalenti più veloci. Queste ottimizzazioni funzionano a prescindere dal framework che ha creato il modello.

A seguire, ti mostro come convertire i modelli nel formato ONNX.

Convertire modelli in formato ONNX

In questa sezione ti mostrerò come esportare modelli da PyTorch, TensorFlow e scikit-learn. Ti guiderò nel processo di conversione e ti mostrerò come validare che il modello funzioni correttamente dopo la conversione.

Supporto dei framework e strumenti

ONNX supporta i framework di machine learning più popolari.

PyTorch ha l’esportazione ONNX integrata tramite torch.onnx.export(). È il convertitore più maturo perché PyTorch e ONNX sono entrambi mantenuti da Meta.

TensorFlow usa tf2onnx per la conversione. È una libreria separata che converte i grafi TensorFlow in formato ONNX. Installala con uv add tf2onnx e sei pronto.

scikit-learn converte tramite skl2onnx. Questa libreria gestisce modelli di machine learning tradizionali come random forest, regressione lineare e SVM. I framework di deep learning ricevono più attenzione, ma i modelli scikit-learn funzionano altrettanto bene.

Questi tre sono i più popolari, ma puoi usare anche altri framework supportati: installa semplicemente la dipendenza con uv add <name>:

  • Keras: usa tf2onnx (Keras fa parte di TensorFlow)

  • XGBoost: usa onnxmltools

  • LightGBM: usa onnxmltools

  • MATLAB: esportazione ONNX integrata

  • Paddle: usa paddle2onnx

Ogni framework ha il proprio convertitore perché rappresenta i modelli in modo diverso. PyTorch usa grafi di calcolo dinamici, TensorFlow usa grafi statici e scikit-learn non usa affatto grafi. I convertitori traducono queste rappresentazioni nel formato a grafo di ONNX.

Puoi anche usare modelli addestrati da altri. L’ONNX Model Zoo ospita modelli pre-addestrati pronti all’uso. Troverai modelli di computer vision (ResNet, YOLO, EfficientNet), modelli NLP (BERT, GPT-2) e altro. Scaricali dal repository ufficiale su GitHub.

Hugging Face ospita anche modelli ONNX. Cerca modelli con "onnx" nel nome o filtra per formato ONNX. Molti popolari modelli transformer sono già disponibili in formato ONNX e ottimizzati per l’inferenza.

Prima di procedere, esegui questo comando per recuperare tutte le librerie richieste con uv:

uv add torch torchvision tensorflow tf2onnx scikit-learn skl2onnx onnxscript

Processo di conversione passo passo

Ecco come convertire un modello PyTorch in 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
)

Nuovo a PyTorch? Non lasciare che ti fermi. Il nostro corso Deep Learning con PyTorch copre le basi in poche ore.

Il dummy input è importante. ONNX deve conoscere la forma dell’input per costruire il grafo. Se il tuo modello accetta sequenze di lunghezza variabile o batch size diversi, usa dynamic_shapes per contrassegnare quelle dimensioni come dinamiche.

Imposta sempre il modello in modalità valutazione con model.eval() prima dell’esportazione. Questo disabilita il comportamento di training di dropout e batch normalization. Se dimentichi questo passaggio, il modello convertito non corrisponderà all’originale.

Le forme dinamiche possono creare problemi. Se il convertitore si lamenta di dimensioni sconosciute, assicurati di specificare correttamente dynamic_shapes o fornisci forme concrete.

Passando a TensorFlow, ecco un codice alternativo che puoi eseguire:

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"
)

Se non hai mai lavorato con TensorFlow, ti consigliamo il nostro corso Introduzione a TensorFlow in Python.

Sfortunatamente, c’è un piccolo conflitto con l’ultima versione di Numpy, quindi ho aggiunto una correzione temporanea. Al momento in cui leggi, speriamo tu possa eseguire lo snippet senza le prime sei righe.

E infine, vediamo la conversione ONNX per i modelli scikit-learn:

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())

Come per TensorFlow, anche DataCamp ha un corso gratuito Supervised Learning con scikit-learn: dagli un’occhiata se trovi lo snippet sopra poco chiaro.

E questo è tutto!

Ora puoi eseguire questo snippet per validare il tuo modello convertito:

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!")

Image 4 - ONNX model validation

Esegui questa validazione ogni volta che converti un modello. Piccole differenze numeriche sono normali (la matematica in floating point non è esatta), ma grandi differenze significano che qualcosa è andato storto durante la conversione.

Per concludere, ecco due importanti avvertenze di cui devi essere consapevole:

  • Le operazioni personalizzate non si convertono: se hai scritto operazioni PyTorch personalizzate o layer TensorFlow personalizzati, il convertitore non sa come tradurli. Devi riscriverli usando operazioni standard o registrare operatori ONNX personalizzati.
  • Le incongruenze di versione rompono le cose: la versione del framework, del convertitore e di ONNX devono essere compatibili. Controlla la documentazione del convertitore per le combinazioni di versione supportate.

A seguire, parliamo di ONNX Runtime.

Eseguire inferenza con ONNX Runtime

In questa sezione ti mostrerò come usare ONNX Runtime per caricare modelli e fare predizioni. Ti guiderò tra le opzioni di accelerazione hardware e ti mostrerò dove ONNX Runtime dà il meglio in produzione.

Introduzione a ONNX Runtime

ONNX Runtime è un motore di inferenza multipiattaforma. Microsoft lo ha sviluppato per eseguire velocemente i modelli ONNX su qualsiasi hardware: CPU, GPU, chip mobile e acceleratori AI specializzati.

Ecco come funziona: carichi un modello ONNX e ONNX Runtime costruisce un piano di esecuzione. Analizza il grafo computazionale, applica ottimizzazioni e decide il modo migliore per eseguire le operazioni sul tuo hardware. Poi esegue l’inferenza usando il piano ottimizzato.

L’architettura separa la logica di esecuzione dal codice specifico dell’hardware. Gli execution provider gestiscono l’interfaccia con l’hardware. Vuoi eseguire su GPU NVIDIA? Usa il provider CUDA. Vuoi Apple Silicon? Il tuo codice sottostante resta lo stesso.

ONNX Runtime ha questi execution provider:

  • CPUExecutionProvider: provider predefinito, funziona ovunque

  • CUDAExecutionProvider: GPU NVIDIA con CUDA

  • TensorRTExecutionProvider: GPU NVIDIA con ottimizzazione TensorRT

  • CoreMLExecutionProvider: dispositivi Apple (macOS, iOS)

  • DmlExecutionProvider: DirectML per Windows (funziona con qualsiasi GPU)

  • OpenVINOExecutionProvider: CPU e GPU Intel

  • ROCMExecutionProvider: GPU AMD

Ogni provider traduce le operazioni ONNX in istruzioni specifiche dell’hardware. Il provider CPU usa operazioni standard su CPU. Il provider CUDA usa cuDNN e cuBLAS. Il provider TensorRT compila il modello in kernel GPU ottimizzati, e così via.

Impostare ed eseguire un modello

Puoi caricare un modello ONNX con tre righe di codice:

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

L’InferenceSession fa tutto il lavoro pesante. Carica il modello, valida il grafo e si prepara all’inferenza. Per impostazione predefinita usa il provider di esecuzione su CPU.

Ora esegui l’inferenza:

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)

Image 5 - Running a PyTorch model with ONNX

Il metodo run() accetta due argomenti. Per primo, un elenco di nomi di output (o None per tutti gli output). Secondo, un dizionario che mappa i nomi di input ad array numpy.

Le forme degli input devono corrispondere. Se il tuo modello si aspetta [1, 10] e tu passi [1, 10, 3], l’inferenza fallirà.

Se vuoi l’accelerazione GPU, specifica gli execution provider quando crei la sessione:

# 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']
)

Elenca i provider in ordine di priorità. ONNX Runtime prova il primo e ripiega sul successivo se non è disponibile. Includi sempre CPUExecutionProvider come ultimo fallback.

Puoi controllare quale provider viene effettivamente usato:

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())

Image 6 - Changing a provider

Se vedi ["CPUExecutionProvider"] quando ti aspetti una GPU, il provider GPU non è disponibile. Le cause comuni potrebbero essere driver mancanti, un pacchetto ONNX Runtime errato o una GPU incompatibile.

La configurazione non finisce qui. Puoi ottimizzare ulteriormente l’execution provider per ottenere prestazioni migliori:

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)

Puoi fare riferimento alla documentazione ufficiale di ONNX Runtime per ottenere l’elenco completo delle opzioni disponibili per il provider che preferisci.

Applicazioni di ONNX Runtime

La cosa impressionante di ONNX Runtime è che gira ovunque: server, browser e dispositivi mobili.

ONNX Runtime Web porta l’inferenza nei browser usando WebAssembly e WebGL. Il tuo modello gira lato client senza inviare dati ai server. Funziona benissimo per applicazioni sensibili alla privacy come l’analisi di immagini mediche o l’elaborazione di documenti.

Ecco un semplice esempio di come puoi usare ONNX Runtime in un semplice file HTML:

<!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>

Image 7 - ONNX Runtime in JavaScript

Per la distribuzione mobile puoi usare ONNX Runtime Mobile, una versione leggera ottimizzata per iOS e Android. Elimina funzionalità non necessarie e riduce la dimensione del binario.

Questo è un esempio di configurazione 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])

Oppure, se usi Python per costruire una REST API, puoi includere ONNX Runtime in uno dei tuoi endpoint seguendo questo approccio:

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()}

Ed è tutto! Altri ambienti useranno un approccio simile, ma il succo è chiaro: ONNX Runtime è facile da usare. A seguire, parliamo di ottimizzazione.

Ottimizzare i modelli ONNX

In questa sezione ti mostrerò come rendere i modelli ONNX più veloci e più piccoli tramite quantizzazione e ottimizzazioni del grafo. Ti guiderò attraverso le tecniche più importanti per le distribuzioni reali.

Tecniche di quantizzazione

La quantizzazione riduce la precisione del modello per renderlo più veloce e più piccolo.

Invece di memorizzare i pesi come float a 32 bit, li memorizzi come interi a 8 bit. Questo riduce l’uso di memoria del 75% e accelera l’inferenza perché l’aritmetica intera è più veloce della floating point sulla maggior parte dell’hardware.

La quantizzazione dinamica converte i pesi a bassa precisione ma mantiene le attivazioni (valori intermedi durante l’inferenza) a piena precisione. È il metodo più semplice perché non richiede dati di calibrazione.

Ecco come applicare la quantizzazione dinamica:

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
)

È fatta. Il tuo modello quantizzato ora usa interi senza segno a 8 bit ed è pronto all’uso.

La quantizzazione statica converte sia pesi che attivazioni a bassa precisione. È più aggressiva e più veloce della dinamica, ma servono dati di calibrazione rappresentativi per misurare gli intervalli delle attivazioni.

Ecco una strategia generale per applicare la quantizzazione statica:

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
)

I dati di calibrazione dovrebbero rappresentare il tuo carico di lavoro reale in inferenza, quindi non usare dati casuali come ho mostrato sopra. Usa campioni reali del tuo dataset.

In generale, la quantizzazione ha alcuni vantaggi e compromessi che devi conoscere come ingegnere di machine learning. Ecco i vantaggi:

  • Modelli 4 volte più piccoli: INT8 usa un quarto della memoria rispetto a FP32
  • Inferenza 2-4 volte più veloce: le operazioni intere sono più veloci su CPU
  • Consumo energetico inferiore: meno calcolo significa meno energia
  • Miglior utilizzo della cache: modelli più piccoli entrano nella cache della CPU

Ecco invece i compromessi:

  • Leggera perdita di accuratezza: di solito 1-2% per INT8, accettabile per la maggior parte delle applicazioni
  • Non tutte le operazioni si quantizzano bene: batch normalization e softmax sono complicate
  • Dipendenza dall’hardware: le accelerazioni su GPU variano, alcune GPU non accelerano INT8

La quantizzazione brilla anche per i large language model e la generative AI. Un modello da 7B parametri in FP32 richiede 28GB di memoria. Quantizzato a INT8 scende a 7GB. Quantizzato a INT4 arriva a 3,5GB: abbastanza piccolo da girare su hardware consumer.

Ottimizzazioni del grafo

Le ottimizzazioni del grafo riscrivono il grafo computazionale del modello per renderlo più veloce.

Il concetto di fusione dei nodi combina più operazioni in un’unica operazione. Se il tuo modello ha un layer Conv seguito da BatchNorm e poi ReLU, l’ottimizzatore li fonde in un unico nodo ConvBnRelu. Questo riduce il traffico di memoria e velocizza l’esecuzione.

Ecco come appare il grafo prima della fusione:

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

È semplificato dopo la fusione:

Input -> ConvBnRelu -> Output

Ci sono altri due concetti da conoscere quando si parla di ottimizzazione del grafo:

  • Il constant folding pre-calcola operazioni che non dipendono dai dati di input. Se il tuo modello moltiplica una matrice di pesi per una costante, l’ottimizzatore esegue quella moltiplicazione una sola volta in fase di ottimizzazione invece che a ogni inferenza.
  • La dead code elimination rimuove operazioni inutilizzate. Se il tuo modello ha rami che non vengono mai eseguiti o output che non usi, l’ottimizzatore li elimina.

Ecco come abilitare le ottimizzazioni del grafo 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 ha i seguenti livelli di ottimizzazione:

  • ORT_DISABLE_ALL: nessuna ottimizzazione (utile per il debug)

  • ORT_ENABLE_BASIC: ottimizzazioni sicure che non cambiano i risultati numerici

  • ORT_ENABLE_EXTENDED: ottimizzazioni aggressive che possono introdurre piccole differenze numeriche

  • ORT_ENABLE_ALL: tutte le ottimizzazioni, incluse le trasformazioni di layout

Usa ORT_ENABLE_ALL in produzione. L’aumento di velocità vale le minuscole differenze numeriche.

Puoi eseguire questo snippet per salvare modelli ottimizzati da riutilizzare:

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)

Le ottimizzazioni del grafo funzionano indipendentemente dal framework che ha creato il modello. Un pattern Conv-BN-ReLU di PyTorch e uno di TensorFlow vengono fusi allo stesso modo. Questo è il vantaggio dell’ottimizzazione condivisa: scrivi l’ottimizzazione una volta e la applichi a modelli di qualsiasi framework.

A seguire, parliamo della distribuzione.

Scenari di distribuzione

In questa sezione analizzerò tre scenari di distribuzione: dispositivi edge, infrastruttura cloud e browser web. Ti mostrerò le sfide e le soluzioni per ciascun ambiente.

Distribuzione su dispositivi edge

La sfida principale dei dispositivi edge è che hanno risorse limitate.

Il tuo smartphone ha 4-16GB di RAM. Un Raspberry Pi ancora meno. I dispositivi IoT possono avere 512MB o meno. Non puoi semplicemente buttare un modello da 7B parametri su questi dispositivi e aspettarti che funzioni.

Parti dalla quantizzazione. I modelli INT8 dovrebbero essere la base. Spingiti fino a INT4 se puoi tollerare la perdita di accuratezza. Così il modello diventa abbastanza piccolo da entrare in memoria.

Quando si tratta di distribuire su dispositivi edge, ONNX Runtime Mobile è il tuo alleato. Elimina le funzionalità server non necessarie e ottimizza per la durata della batteria. Il binario è più piccolo, l’avvio è più rapido e il consumo energetico è inferiore.

Ecco come puoi aggiungerlo al tuo progetto mobile:

# iOS
pod 'onnxruntime-mobile-objc'

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

Gli execution provider gestiscono le differenze hardware. I dispositivi Android usano CPU ARM, alcuni hanno GPU di vendor diversi e i telefoni più recenti hanno NPU (Neural Processing Unit). Non vuoi scrivere codice per ogni chip.

Fai riferimento a questo snippet per scegliere l’execution provider giusto:

# 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 su iOS usa la Neural Engine (la NPU di Apple). NNAPI su Android usa qualunque acceleratore abbia il dispositivo: GPU, DSP o NPU. Il provider astrae l’hardware così il tuo codice resta lo stesso. Quando esce un nuovo chip più performante, aggiorna l’execution provider e la tua app va più veloce senza cambiare codice.

Distribuzione sul cloud

La distribuzione sul cloud ti dà scalabilità illimitata e l’hardware più recente.

Puoi distribuire modelli ONNX su qualsiasi piattaforma cloud: AWS, Azure, GCP o server propri. Il pattern è lo stesso: containerizza il servizio di inferenza e distribuiscilo dietro un bilanciatore di carico.

Ecco un piccolo esempio in Python con FastAPI:

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}

Questo esempio FastAPI, per quanto piccolo, non è il più facile da comprendere per gli ingegneri di machine learning. Puoi padroneggiare le basi con il nostro corso gratuito Introduzione a FastAPI.

Poi puoi containerizzarlo con 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"]

Ogni appassionato di machine learning deve conoscere le basi di Docker. Segui il nostro corso Introduzione a Docker per imparare finalmente a distribuire un modello di machine learning.

Otterrai un’applicazione in esecuzione sulla porta 8000, per la quale puoi aprire direttamente la documentazione e provare l’endpoint di predizione:

Image 8 - ONNX model deployed with FastAPI

Se usi Azure Machine Learning, ti farà piacere sapere che si integra direttamente con ONNX. Puoi distribuire il tuo modello in tre passaggi:

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 gestisce scalabilità, monitoraggio e aggiornamenti. Ottieni logging automatico, tracciamento delle richieste e health check.

Se sei nuovo ad Azure Machine Learning, leggi la nostra Guida per principianti per apprendere rapidamente i fondamenti.

Se parliamo di workflow MLOps servono questi componenti:

  • Controllo di versione: traccia le versioni dei modelli con DVC o MLflow
  • CI/CD: automatizza test e distribuzione con GitHub Actions o Azure Pipelines
  • Monitoraggio: traccia latenza di inferenza, throughput e tassi di errore
  • A/B testing: confronta versioni di modello in produzione
  • Rollback: torna a versioni precedenti quando si verificano problemi

Ecco un esempio di snippet che puoi usare:

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()}

Da lì, puoi scalare orizzontalmente eseguendo più istanze del servizio dietro un bilanciatore di carico. ONNX Runtime è stateless, quindi qualsiasi istanza può gestire qualsiasi richiesta.

Inferenza nel browser

ONNX Runtime Web esegue i modelli direttamente nei browser usando WebAssembly e WebGL.

Invece di inviare i dati a un server, invii il modello una volta al browser ed esegui l’inferenza in locale. Gli utenti ottengono risposte istantanee senza latenza di rete.

Per iniziare, crea un file HTML che usa uno script JS inline:

<!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>

Puoi vedere l’output nella console non appena apri il file con live server:

Image 9 - ONNX runtime with JavaScript

E infine, discutiamo un paio di argomenti avanzati.

Argomenti avanzati

Le operazioni standard ONNX coprono la maggior parte dei casi d’uso, ma se ti serve di più, questa è la sezione per te.

Tratterò gli operatori personalizzati e la distribuzione di large language model. Spiegherò quando ti servono queste funzionalità avanzate e quali compromessi comportano.

Operatori personalizzati

ONNX include centinaia di operazioni, ma il tuo modello potrebbe usare qualcosa di unico.

Gli operatori personalizzati ti permettono di definire operazioni che non esistono nell’insieme standard di operatori ONNX. Scrivi tu stesso la logica dell’operazione e dici a ONNX Runtime come eseguirla. Questo estende ONNX oltre le sue capacità integrate.

Quando servono gli operatori personalizzati? Quando usi ricerca all’avanguardia che ONNX non supporta ancora. Oppure quando hai costruito operazioni proprietarie specifiche del tuo dominio. O quando ti servono ottimizzazioni specifiche dell’hardware che le operazioni standard non possono fornire.

Ma c’è un problema: gli operatori personalizzati rompono la portabilità. Il tuo modello non girerà su sistemi che non hanno la tua implementazione dell’operatore personalizzato. Se esporti un modello PyTorch con operatori personalizzati in ONNX, devi impacchettare quegli operatori separatamente e registrarli con ONNX Runtime.

La distribuzione diventa più difficile. Ogni ambiente ha bisogno della tua libreria di operatori personalizzati. Dispositivi edge, server cloud e browser necessitano tutti della stessa implementazione. Perdi il vantaggio più grande di ONNX: scrivi una volta, esegui ovunque.

Ecco un piccolo esempio funzionante di operatori personalizzati, giusto per vedere come funzionano:

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")

La maggior parte dei team evita gli operatori personalizzati quando possibile. Prova prima a usare combinazioni di operatori standard. Passa al personalizzato solo quando gli operatori standard non possono esprimere la tua operazione o quando le prestazioni lo richiedono.

Gestire i large language model

La memoria è un problema quando si tratta di LLM. Un modello da 7B parametri richiede 28GB in FP32, 14GB in FP16 e 7GB in INT8. La maggior parte dell’hardware consumer non può contenerlo. Serve la quantizzazione, spesso aggressiva a INT4 o INT3.

Se sei alle prime armi con gli LLM e vuoi saperne di più, il nostro corso sui concetti dei Large Language Models (LLM) è un ottimo punto di partenza.

La gestione della cache KV è importante per la generazione autoregressiva. Ogni token che generi deve fare riferimento a tutti i token precedenti. La cache cresce con la lunghezza della sequenza. Conversazioni lunghe consumano rapidamente memoria. ONNX Runtime GenAI include una gestione ottimizzata della cache KV che riusa memoria e riduce l’overhead.

La velocità di inferenza dipende dal tuo hardware e dalla dimensione del modello. Modelli più piccoli (sotto i 3B parametri) girano bene su GPU consumer. Modelli più grandi richiedono più GPU o acceleratori di inferenza specializzati. ONNX Runtime supporta l’inferenza multi-GPU tramite execution provider, ma devi suddividere il modello tra i dispositivi manualmente.

Flash Attention accelera il meccanismo di attenzione nei transformer. L’attenzione standard è O(n²) in funzione della lunghezza della sequenza: diventa lenta rapidamente. Flash Attention riduce lo spostamento di memoria e migliora la velocità senza cambiare i risultati. ONNX Runtime include ottimizzazioni di Flash Attention per l’hardware supportato.

Addestrare LLM con ONNX Runtime è possibile ma raro. La maggior parte dei team addestra con PyTorch o JAX, poi esporta in ONNX solo per l’inferenza. ONNX Runtime Training esiste per il fine-tuning e il pre-training continuato, ma l’ecosistema attorno al training con PyTorch è più maturo.

I carichi di lavoro di generative AI ricevono un trattamento speciale in ONNX Runtime. Le estensioni GenAI forniscono API di alto livello per la generazione di testo, il mantenimento dello stato tra le chiamate e la gestione di beam search o strategie di campionamento. Questo ti evita di implementare da zero la logica di generazione.

Il panorama degli LLM cambia rapidamente e gli aggiornamenti di ONNX Runtime fanno del loro meglio per stare al passo. L’insieme di operatori e le ottimizzazioni vengono aggiornati regolarmente, ma c’è sempre un ritardo tra la ricerca e il supporto pronto per la produzione.

Conclusione

Ora sai come convertire modelli da qualsiasi framework, ottimizzarli per la produzione e distribuirli ovunque.

ONNX rimuove l’attrito tra training e distribuzione. Allena in PyTorch perché è ottimo per la ricerca. Distribuisci con ONNX Runtime perché è veloce e gira ovunque. Quantizza a INT8 e dimezza i costi di inferenza. Passa da CPU a GPU senza cambiare codice. Sposta i modelli tra provider cloud senza lock-in del fornitore.

ONNX è ciò che lo rende possibile.

L’ecosistema continua a migliorare. Il supporto alla generative AI diventa migliore a ogni release: inferenza LLM più veloce, migliore gestione della memoria e nuove tecniche di ottimizzazione. Il supporto hardware si estende a più acceleratori e dispositivi edge. La community costruisce strumenti che rendono ONNX più facile da usare. Puoi partecipare testando nuove funzionalità, segnalando problemi o contribuendo con codice tramite i repository GitHub.

Pronto per il livello successivo? Padroneggia i Concetti di Explainable Artificial Intelligence (XAI) per smettere finalmente di considerare i modelli come scatole nere.

Dario Radečić

Autore

Dario Radečić

Senior Data Scientist con base in Croazia. Top Tech Writer con oltre 700 articoli pubblicati, per più di 10 milioni di visualizzazioni. Autore del libro Machine Learning Automation with TPOT.

FAQs

Cos’è ONNX e perché dovrei usarlo?

ONNX (Open Neural Network Exchange) è un formato universale per modelli di machine learning che funziona tra framework diversi. Ti permette di addestrare un modello in PyTorch o TensorFlow e distribuirlo ovunque senza riscrivere il codice. Ottieni indipendenza dal framework, migliori prestazioni grazie alle ottimizzazioni e la possibilità di eseguire i modelli su qualsiasi hardware, dai dispositivi edge ai server cloud.

Come ONNX migliora le prestazioni del modello?

ONNX Runtime applica ottimizzazioni del grafo come fusione dei nodi e constant folding che velocizzano l’inferenza. La quantizzazione riduce la dimensione del modello fino al 75% e rende l’inferenza 2-4 volte più veloce convertendo i pesi da float a 32 bit a interi a 8 bit. Queste ottimizzazioni funzionano automaticamente e non richiedono di ottimizzare manualmente il modello.

ONNX può girare su dispositivi mobili e browser?

Sì. ONNX Runtime Mobile gira su dispositivi iOS e Android con ottimizzazioni per la durata della batteria e l’uso della memoria. ONNX Runtime Web esegue i modelli direttamente nei browser usando WebAssembly e WebGL, così puoi fare inferenza lato client senza inviare dati ai server. Entrambe le opzioni supportano l’accelerazione hardware tramite execution provider come CoreML per dispositivi Apple e NNAPI per Android.

Qual è la differenza tra quantizzazione dinamica e statica?

La quantizzazione dinamica converte i pesi a bassa precisione ma mantiene le attivazioni a piena precisione e non richiede dati di calibrazione. La quantizzazione statica converte sia pesi che attivazioni a bassa precisione, è più veloce ma richiede dati di calibrazione rappresentativi per misurare gli intervalli delle attivazioni. La quantizzazione statica offre prestazioni migliori ma richiede più configurazione.

Come gestisco i modelli con operazioni personalizzate durante la conversione a ONNX?

Le operazioni personalizzate richiedono implementazioni in C++ registrate con ONNX Runtime: gli operatori personalizzati solo in Python non sono supportati per l’uso in produzione. Devi implementare l’operatore personalizzato in C++, compilarlo come libreria condivisa e registrarlo con ONNX Runtime in ogni ambiente di distribuzione. La maggior parte dei team evita gli operatori personalizzati quando possibile e usa combinazioni di operazioni ONNX standard, poiché gli operatori personalizzati compromettono la portabilità tra piattaforme diverse.

Argomenti

Correlato