Skip to main content
PyTorch advanced Lesson 7 of 11

PyTorch Deployment with ONNX

Export PyTorch models to ONNX, optimize for inference, serve with ONNX Runtime, and convert to TorchScript for production.

Real-World Scenario

A fraud detection model needs to run inside a Java-based payment processing service with sub-5ms latency. PyTorch eager mode adds ~15ms Python overhead per call. Exporting to ONNX and running with ONNX Runtime drops inference to 1.2ms — a 12x speedup — and removes the Python dependency entirely.

Exporting a Model to ONNX

import torch
import torch.nn as nn
import torch.onnx
from pathlib import Path

# Define a model
class FraudDetector(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(20, 128), nn.ReLU(), nn.Dropout(0.2),
            nn.Linear(128, 64), nn.ReLU(),
            nn.Linear(64, 1), nn.Sigmoid(),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.net(x)

model = FraudDetector()
model.eval()   # CRITICAL: always set eval() before export (affects Dropout/BN)

# Create a dummy input with the correct shape and dtype
dummy_input = torch.randn(1, 20)  # (batch_size, features)

# Export to ONNX
output_path = "fraud_detector.onnx"
torch.onnx.export(
    model,
    dummy_input,
    output_path,
    export_params=True,          # include trained weights
    opset_version=17,            # ONNX opset — use latest stable
    input_names=["features"],    # name the inputs for clarity
    output_names=["fraud_prob"],
    dynamic_axes={
        "features":   {0: "batch_size"},  # variable batch dimension
        "fraud_prob": {0: "batch_size"},
    },
)
print(f"Model exported to {output_path}")

# Verify the exported model
import onnx
onnx_model = onnx.load(output_path)
onnx.checker.check_model(onnx_model)
print("ONNX model is valid")
print(f"Graph inputs:  {[i.name for i in onnx_model.graph.input]}")
print(f"Graph outputs: {[o.name for o in onnx_model.graph.output]}")

Running Inference with ONNX Runtime

import onnxruntime as ort
import numpy as np
import time

# Load the ONNX model
session = ort.InferenceSession(
    "fraud_detector.onnx",
    providers=["CUDAExecutionProvider", "CPUExecutionProvider"],  # GPU first, fallback CPU
)

# Print runtime info
print(f"Providers available: {ort.get_available_providers()}")
print(f"Providers in use:    {session.get_providers()}")
print(f"Input names:         {[i.name for i in session.get_inputs()]}")
print(f"Input shapes:        {[i.shape for i in session.get_inputs()]}")

# Single prediction
features = np.random.randn(1, 20).astype(np.float32)  # ONNX Runtime needs float32
result = session.run(
    output_names=["fraud_prob"],
    input_feed={"features": features},
)
fraud_prob = result[0][0, 0]
print(f"Fraud probability: {fraud_prob:.4f}")

# Batch prediction
batch = np.random.randn(1000, 20).astype(np.float32)
t0 = time.perf_counter()
result = session.run(None, {"features": batch})
latency = (time.perf_counter() - t0) * 1000
print(f"Batch of 1000: {latency:.1f}ms ({latency/1000:.3f}ms per sample)")

# Compare with PyTorch eager mode
import torch
torch_model = FraudDetector()   # same model, not loaded from ONNX
torch_model.eval()
torch_batch = torch.tensor(batch)
t0 = time.perf_counter()
with torch.no_grad():
    torch_result = torch_model(torch_batch)
torch_latency = (time.perf_counter() - t0) * 1000
print(f"PyTorch eager:   {torch_latency:.1f}ms")
print(f"ONNX Runtime:    {latency:.1f}ms")
print(f"Speedup:         {torch_latency/latency:.1f}x")

TorchScript Export

import torch
import torch.nn as nn

class SentimentClassifier(nn.Module):
    def __init__(self, vocab_size: int, embed_dim: int, hidden: int, n_classes: int):
        super().__init__()
        self.embedding  = nn.Embedding(vocab_size, embed_dim)
        self.lstm       = nn.LSTM(embed_dim, hidden, batch_first=True)
        self.classifier = nn.Linear(hidden, n_classes)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        embedded = self.embedding(x)
        _, (h_n, _) = self.lstm(embedded)
        return self.classifier(h_n[-1])

model = SentimentClassifier(5000, 64, 128, 2)
model.eval()

# Method 1: torch.jit.trace — fastest, records the execution path
# Use when model has no data-dependent control flow
dummy = torch.zeros(1, 20, dtype=torch.long)
traced_model = torch.jit.trace(model, dummy)
traced_model.save("sentiment_traced.pt")

# Method 2: torch.jit.script — compiles the model with type inference
# Use when model has conditional branches on input values
@torch.jit.script
def classify_text(model: torch.jit.ScriptModule, token_ids: torch.Tensor) -> torch.Tensor:
    logits = model(token_ids)
    return torch.softmax(logits, dim=-1)

# Load and run without needing the original class definition
loaded = torch.jit.load("sentiment_traced.pt")
loaded.eval()

test_input = torch.randint(0, 5000, (1, 20))
probs = torch.softmax(loaded(test_input), dim=-1)
print(f"Sentiment probs: positive={probs[0,1]:.3f}, negative={probs[0,0]:.3f}")

Production Inference Class

import onnxruntime as ort
import numpy as np
from pathlib import Path
import threading

class ONNXModelServer:
    """Thread-safe ONNX model server with warmup and metrics."""

    def __init__(self, model_path: str, n_threads: int = 4):
        opts = ort.SessionOptions()
        opts.intra_op_num_threads  = n_threads
        opts.inter_op_num_threads  = n_threads
        opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL

        self.session    = ort.InferenceSession(model_path, sess_options=opts)
        self.input_name = self.session.get_inputs()[0].name
        self._lock      = threading.Lock()
        self._call_count = 0
        self._total_ms   = 0.0

        # Warmup — first call is slow due to JIT compilation
        warmup_input = np.zeros((1, 20), dtype=np.float32)
        for _ in range(3):
            self.session.run(None, {self.input_name: warmup_input})
        print(f"Model warmed up: {model_path}")

    def predict(self, features: np.ndarray) -> np.ndarray:
        """Run inference. Thread-safe."""
        if features.dtype != np.float32:
            features = features.astype(np.float32)
        if features.ndim == 1:
            features = features.reshape(1, -1)

        import time
        t0 = time.perf_counter()

        with self._lock:
            result = self.session.run(None, {self.input_name: features})[0]
            elapsed = (time.perf_counter() - t0) * 1000
            self._call_count += 1
            self._total_ms   += elapsed

        return result

    def stats(self) -> dict:
        return {
            "calls":      self._call_count,
            "avg_latency_ms": round(self._total_ms / max(self._call_count, 1), 2),
        }


# Usage
server = ONNXModelServer("fraud_detector.onnx")

# Single prediction
features = np.random.randn(20).astype(np.float32)
prob = server.predict(features)[0, 0]
print(f"Fraud probability: {prob:.4f}")

# Simulate concurrent requests
import concurrent.futures
requests = [np.random.randn(20).astype(np.float32) for _ in range(100)]

with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
    futures = [executor.submit(server.predict, r) for r in requests]
    results = [f.result()[0, 0] for f in futures]

print(f"Processed 100 requests: {server.stats()}")
print(f"Mean predicted prob: {np.mean(results):.4f}")

Frequently Asked Questions

What is ONNX and why export to it?
ONNX (Open Neural Network Exchange) is a universal model format that decouples training from inference. Export once from PyTorch, then run anywhere — on CPU with ONNX Runtime, on mobile with CoreML or TFLite, on edge hardware with OpenVINO or TensorRT. ONNX Runtime typically gives 2-5x faster CPU inference than PyTorch eager mode.
What is TorchScript and when should I use it instead of ONNX?
TorchScript compiles a PyTorch model to a static graph that can run without a Python interpreter — useful for C++ deployment or when your model uses Python control flow that ONNX can't represent. Use ONNX for broad runtime compatibility; use TorchScript for pure PyTorch deployment in C++ or mobile (iOS/Android via PyTorch Mobile).