PROMETHEUS OS
NATIVE AI OPERATING SYSTEM
[INIT] Prometheus OS loading...
[CORE] Neural interface initializing...
[AI] LLM router connecting...
[MEM] Memory graph loading...
[AGT] Agent chains on standby...
[SEC] Security protocols active...
[NET] Provider mesh established...
[UI] Workspace rendering...
[DONE] All systems nominal.
Welcome to PROMETHEUS OS.
READY · OFFLINE · SECURE · PRIVATE
🎨 THEME ENGINE
PROMETHEUS OS · 23 THEMES
Trial version: Dark + Light themes only.
Full theme access with PROMETHEUS OS license.
Dark theme active
Experience all 23 themes in PROMETHEUS OS

Deploying Models: From Artifact to Production Service

Advanced 17 min· 5 exercises

Choose between batch, real-time and streaming inference, package a model safely (safetensors, ONNX, torch.export), serve it behind an API in containers on Kubernetes, and roll it out with shadow, canary and A/B releases that can be rolled back in minutes.

By the end of this lesson you will be able to
  • Compare batch, real-time and streaming inference and choose the cheapest pattern that meets a product's freshness and latency needs.
  • Explain the trade-offs between pickle checkpoints, safetensors, TorchScript, torch.export and ONNX, and implement an export with a numerical parity check.
  • Implement a minimal model-serving API and describe how containers, Kubernetes probes, autoscaling and GPU device plugins run it in production.
  • Calculate replica counts with Little's law and A/B test sample sizes from a minimum detectable effect.
  • Design a release plan that uses shadow, canary and A/B stages with automated gates and a rehearsed rollback.
  • Choose compression techniques (quantization, pruning, distillation) for an edge or on-device target.

Before this: The ML Lifecycle, Reproducibility and Experiment Tracking, Data Engineering for AI: Pipelines, Quality, Labeling, Features and Privacy

A trained model is only useful once something can call it. Deployment is the engineering that turns a weights file on a laptop into a service that answers fraud checks in 30 milliseconds, scores ten million customers overnight, or runs on a phone with no network. Most of the cost and risk in a production ML system sits here, not in the training notebook. Sculley et al. made the same point in their well-known "hidden technical debt" paper: the model code is a small box in the middle of a large system.

Intuition: a restaurant, not a recipe

Think of a trained model as a recipe. Deployment is opening the restaurant. You have to decide whether you cook to order (real-time), prep everything the night before (batch), or run a conveyor belt that cooks as ingredients arrive (streaming). You need a kitchen that can be copied exactly in another city (containers), a manager who adds cooks at the dinner rush (autoscaling), and a way to try a new dish on a few tables before it goes on every menu (canary and A/B tests). If the new dish makes people sick, you need to take it off the menu in minutes (rollback).

Three inference patterns

PatternWhen predictions are madeTypical latency budgetExampleMain engineering concern
Batch (offline)On a schedule, over a whole datasetMinutes to hours for the jobNightly churn scores written to a tableThroughput, cost, idempotent reruns
Real-time (online)On request, one or a few inputsRoughly 10–500 ms per requestCard fraud check at checkoutTail latency (p95/p99), availability
StreamingAs events arrive on a stream (e.g. Kafka)SecondsUpdating a user's risk score after each clickState, ordering, exactly-once semantics

Choose the cheapest pattern the product allows. If a prediction is only read the next morning, batch scoring is simpler, cheaper and easier to debug than a live service. A common hybrid precomputes expensive parts in batch (embeddings, candidate lists) and does a cheap final step online (re-ranking with fresh context). The deciding question is how fresh the inputs must be: if the prediction depends on something that happened two seconds ago, you need online or streaming inference.

A frequent bug in real-time systems is training–serving skew: features computed one way in the offline training pipeline and another way in the online service. A feature store, or at least one shared feature-computation library used by both paths, is the standard defence (see the data-engineering lesson).

Packaging the model: serialization formats

Before you can serve a model you must save it in a form another process can load safely and run quickly.

  • Pickle-based checkpoints (classic torch.save, joblib): convenient but unsafe. Unpickling can execute arbitrary code, so never load untrusted pickles. Since PyTorch 2.6, torch.load defaults to weights_only=True for this reason.
  • safetensors: a simple format that stores only tensors (a JSON header plus raw bytes). It cannot execute code and supports zero-copy, memory-mapped loading. It has become the default for sharing weights on the Hugging Face Hub. It stores weights only; you still need the model code to rebuild the architecture.
  • TorchScript (torch.jit.script / torch.jit.trace): PyTorch's older way to capture a model as a graph that can run without Python. PyTorch now describes TorchScript as nearing end of life and steers new work towards torch.export; treat it as legacy for new projects.
  • torch.export: captures a full, ahead-of-time graph (ExportedProgram) of a PyTorch model, with explicit dynamic shapes. It is the entry point for PyTorch's newer deployment paths (for example AOTInductor compilation and ExecuTorch for mobile and edge devices).
  • ONNX (Open Neural Network Exchange): a framework-neutral graph format with a versioned operator set ("opset"). Export once, then run in ONNX Runtime, TensorRT, OpenVINO or other engines on many kinds of hardware.
  • GGUF: the single-file format used by llama.cpp and related tools for quantized LLMs (covered in the local-runtimes lesson).

Rule of thumb: store training checkpoints as safetensors plus versioned code, and produce a separate inference artifact (ONNX, an exported program, or an engine-specific compiled file) in CI for each target runtime. Record the exact artifact hash in the model registry.

import torch, torch.nn as nn
from safetensors.torch import save_file, load_file

model = nn.Sequential(nn.Linear(20, 64), nn.ReLU(), nn.Linear(64, 1)).eval()
save_file(model.state_dict(), "fraud_v3.safetensors")      # weights only, no code execution on load
model.load_state_dict(load_file("fraud_v3.safetensors"))

x = torch.randn(4, 20)
torch.onnx.export(model, (x,), "fraud_v3.onnx",
                  input_names=["x"], output_names=["logit"],
                  dynamic_axes={"x": {0: "batch"}, "logit": {0: "batch"}})  # batch dim stays dynamic

ep = torch.export.export(model, (x,))                      # ahead-of-time graph capture
torch.export.save(ep, "fraud_v3.pt2")

import onnxruntime as ort, numpy as np
sess = ort.InferenceSession("fraud_v3.onnx", providers=["CPUExecutionProvider"])
onnx_out = sess.run(None, {"x": x.numpy()})[0]
assert np.allclose(onnx_out, model(x).detach().numpy(), atol=1e-5)   # parity check

The last line is the most important: always check numerical parity between the training framework and the exported artifact on real inputs before shipping. Exporters change between PyTorch releases (since PyTorch 2.9, torch.onnx.export defaults to a torch.export-based path, which needs the onnxscript package and prefers a dynamic_shapes argument over the older dynamic_axes), so pin versions in your build and let the parity test catch surprises.

Serving over REST and gRPC

A model server wraps the artifact in a network API.

  • REST/JSON over HTTP is universal, easy to debug with curl, and fine for most workloads.
  • gRPC uses HTTP/2 and Protocol Buffers: binary payloads, a strict schema, streaming, and lower per-call overhead. It suits high-volume service-to-service calls and large tensor payloads.

You can write a small server yourself or use a dedicated inference server (NVIDIA Triton Inference Server, TorchServe, KServe on Kubernetes, Ray Serve, or vLLM and similar engines for LLMs; see Inference serving at scale). Dedicated servers add dynamic batching: they briefly hold incoming requests and run them through the GPU together, trading a few milliseconds of latency for much higher throughput.

A minimal FastAPI service:

from contextlib import asynccontextmanager
import numpy as np, onnxruntime as ort
from fastapi import FastAPI
from pydantic import BaseModel

MODEL_VERSION = "fraud_v3"
state = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
    state["sess"] = ort.InferenceSession(f"{MODEL_VERSION}.onnx",
                                         providers=["CPUExecutionProvider"])
    yield                                   # load once at startup, not per request
    state.clear()

app = FastAPI(lifespan=lifespan)

class PredictRequest(BaseModel):
    features: list[list[float]]             # batch of 20-dim feature vectors

@app.post("/v1/predict")
def predict(req: PredictRequest):           # sync def: FastAPI runs it in a thread pool
    x = np.asarray(req.features, dtype=np.float32)
    logits = state["sess"].run(None, {"x": x})[0]
    probs = 1 / (1 + np.exp(-logits))
    return {"model_version": MODEL_VERSION, "scores": probs.ravel().tolist()}

@app.get("/healthz")
def healthz():
    return {"ready": "sess" in state}

Run it with uvicorn app:app --workers 2. Points to notice: the model loads once at startup; the response includes the model version (essential for debugging and for joining predictions to labels later); a health endpoint lets the orchestrator know when the replica is ready. In production you would also validate input ranges, enforce a request size limit, log a sample of inputs and outputs, and export latency metrics.

Containers and Kubernetes basics

A container image packages the code, the Python environment, system libraries (and CUDA user-space libraries for GPU work) and often the model artifact, so the same thing runs on a laptop, in CI and in production. Good images are small (slim base image, multi-stage builds), pinned (exact dependency versions, base image digest), and run as a non-root user.

Kubernetes runs containers across a cluster. The objects you meet first:

  • Pod: one or more containers scheduled together; the unit that runs.
  • Deployment: declares "run N replicas of this pod template" and performs rolling updates.
  • Service: a stable network name and load balancer in front of a set of pods.
  • HorizontalPodAutoscaler (HPA): changes the replica count based on a metric (CPU, or custom metrics such as requests per second or queue depth).
  • Readiness and liveness probes: readiness says "send me traffic" (only after the model has loaded); liveness says "restart me if I hang".

GPU scheduling. GPUs are exposed to Kubernetes through vendor device plugins, and pods request them as extended resources (for example nvidia.com/gpu: 1 under resources.limits). By default a GPU is allocated whole and is not shared between containers. Ways to share are vendor specific: NVIDIA offers Multi-Instance GPU (MIG) partitioning on supported data-center GPUs, plus time-slicing. Use node labels and taints so that only GPU workloads land on expensive GPU nodes. Model loading is slow (gigabytes from storage into GPU memory), so set generous readiness delays, and scale on queue depth or in-flight requests rather than CPU utilisation, which says little about a GPU-bound server.

Worked example: sizing a real-time service

A fraud service must handle a peak of λ=200\lambda = 200 requests per second with mean service time W=0.15W = 0.15 s per request. Little's law says the average number of requests in flight is

L=λW=200×0.15=30.L = \lambda W = 200 \times 0.15 = 30.

Load tests show one replica handles 8 concurrent requests before its p99 latency exceeds the 250 ms budget. Running replicas at 100% of that is fragile, so target 70% utilisation:

replicas=⌈L8×0.7⌉=⌈5.36⌉=6.\text{replicas} = \left\lceil \frac{L}{8 \times 0.7} \right\rceil = \lceil 5.36 \rceil = 6.

Add at least one more so that losing a node or doing a rolling update does not breach the SLO: plan for 7, and let the autoscaler go higher if traffic exceeds the forecast.

Releasing safely: shadow, canary, A/B, rollback

A new model that scored better offline can still fail online: the features differ slightly, the latency is worse, or users react differently. Release strategies limit the blast radius.

  • Shadow (dark) deployment: the new model receives a copy of live traffic, but its outputs are only logged, never returned to users. It tests latency, errors and prediction distributions on real inputs at zero user risk. It cannot measure user-facing outcomes, because users never see its outputs.
  • Canary release: send a small fraction of real traffic (say 1%, then 5%, 25%, 100%) to the new version, comparing error rate, latency and key business metrics against the old version at each step. Automate the gates: advance only if every metric is within its threshold.
  • A/B test: randomly assign users (not requests, so each user has a consistent experience) to the old or new model for long enough to measure a business outcome with statistical power. A canary asks "is it safe?"; an A/B test asks "is it better?"
  • Blue/green: run the old (blue) and new (green) stacks side by side and switch the router all at once; switching back is instant.
  • Rollback: always keep the previous model version deployed or instantly deployable, and make "roll back" a one-command or automatic action. Roll back first, investigate second.

Worked example: how long must an A/B test run?

The current model's conversion rate is p=0.10p = 0.10. You want to detect an absolute improvement of δ=0.01\delta = 0.01 (to 0.11) with significance level α=0.05\alpha = 0.05 (two-sided) and power 1−β=0.81-\beta = 0.8. A standard approximation for users needed per arm is

n≈2 (z1−α/2+z1−β)2 p(1−p)δ2,n \approx \frac{2\,(z_{1-\alpha/2} + z_{1-\beta})^2\, p(1-p)}{\delta^2},

where zqz_q is the qq quantile of the standard normal distribution: z0.975=1.960z_{0.975} = 1.960 and z0.8=0.842z_{0.8} = 0.842.

  1. zz sum: 1.960+0.842=2.8021.960 + 0.842 = 2.802; squared: 7.857.85.
  2. Variance term: p(1−p)=0.10×0.90=0.09p(1-p) = 0.10 \times 0.90 = 0.09.
  3. Numerator: 2×7.85×0.09=1.4132 \times 7.85 \times 0.09 = 1.413.
  4. Divide by δ2=0.0001\delta^2 = 0.0001: n≈14,130n \approx 14{,}130 users per arm.

With 4,000 eligible users a day split 50/50, that is about 7 days. Run full weeks anyway, because behaviour differs by weekday. Decide the duration before starting and do not stop early when the dashboard looks good: repeatedly peeking and stopping at the first significant result inflates the false-positive rate. (This formula is a simplification that uses pp for both arms. Use a proper power calculator, or sequential testing methods designed for peeking, in real work.)

Edge and on-device deployment

Running the model on the user's device (phone, browser, car, camera, laptop) removes network latency, works offline, keeps data local, and moves compute cost off your servers. The price is tight memory, power and thermal budgets, a wide spread of hardware, and slow update cycles. Typical runtimes include ONNX Runtime (including mobile and web builds), TensorFlow Lite (now branded LiteRT by Google), Core ML on Apple devices, ExecuTorch for PyTorch models, and llama.cpp for LLMs.

Compression makes models fit:

  • Quantization: store weights (and often activations) in 8-bit or 4-bit integers instead of 32-bit floats. Post-training quantization needs only a small calibration set; quantization-aware training recovers more accuracy. See the quantization lesson.
  • Pruning: remove weights or whole channels with little effect. Unstructured sparsity rarely speeds up ordinary hardware; structured pruning (whole channels or heads) and hardware-supported patterns such as 2:4 sparsity do.
  • Knowledge distillation: train a small "student" model to match a large "teacher" model's output distribution (Hinton, Vinyals and Dean, 2015).
  • Architecture choice: start from a model designed for mobile (depthwise-separable convolutions, small transformers).

Worked size example: a ResNet-50 has about 25.6 million parameters. At FP32 (4 bytes each) that is about 25.6M×4=10225.6\text{M} \times 4 = 102 MB; at INT8 about 26 MB, a 4× reduction before any pruning. Always re-measure accuracy on the target device and runtime, not just in Python on a workstation: operator support and numerical behaviour differ.

Common pitfalls

  • Training–serving skew: different feature code, different preprocessing (image resize, tokenizer version) or different default values online versus offline. Test with recorded production inputs.
  • Loading the model per request or lazily on the first request, which causes multi-second cold starts and timeouts. Load at startup and gate readiness on it.
  • No model version in logs or responses, making it impossible to tell which model produced a bad prediction.
  • Scaling GPU services on CPU utilisation. Use queue depth, in-flight requests or GPU utilisation.
  • Skipping the parity test after export or quantization, then finding a silent accuracy drop in production.
  • Unpinned dependencies and latest image tags, so a rebuild changes behaviour without anyone changing code.
  • Calling an A/B test early or randomising by request instead of by user.
  • No tested rollback path. A rollback that has never been rehearsed often fails when needed.
  • Loading untrusted pickles. Prefer safetensors and weights_only loading.

In practice

  • Pick the simplest inference pattern the product allows; batch is underrated.
  • Treat models as versioned, immutable artifacts in a registry, with the training data snapshot, code commit, metrics and export hash attached.
  • Build inference artifacts in CI with an automated parity test and a latency benchmark on the target hardware.
  • Ship every model through shadow, then a staged canary with automated metric gates, then (if the change targets a business metric) an A/B test.
  • Define SLOs (for example, p99 below 250 ms and 99.9% availability) and alert on them; the monitoring lesson covers model-quality signals.
  • Keep N−1 deployed or instantly deployable, and practise rollbacks.
  • For edge targets, budget memory, latency and energy up front, and test on the slowest device you support.

Key takeaways

  • Use the simplest inference pattern the product allows: batch scoring is cheaper and easier to run than a live service when predictions need not be fresh.
  • Store weights in a safe format such as safetensors, build runtime-specific artifacts (ONNX, exported programs) in CI, and always test numerical parity after export.
  • A model server should load once at startup, expose readiness, return the model version and be scaled on queue depth or in-flight requests, not CPU.
  • Shadow deployments test safety with no user risk, canaries limit the blast radius, and A/B tests measure whether the new model is actually better.
  • Rollback must be a fast, rehearsed, preferably automatic action, with the previous version kept ready.
  • Edge deployment trades server cost and latency for tight device budgets; quantization, pruning and distillation make models fit.

Terms in this lesson

Batch inference
Scoring a whole dataset on a schedule and storing the predictions for later use, rather than answering individual requests.
Training–serving skew
A difference between how inputs or features are computed during training and during serving that degrades a model's live performance.
safetensors
A tensor-only file format with a JSON header and raw data that cannot execute code on load and supports memory-mapped reading.
ONNX
An open, framework-neutral format that represents a model as a graph of versioned operators so it can run in many inference engines.
torch.export
A PyTorch API that captures a model ahead of time as a full graph (an ExportedProgram) for compilation and deployment without Python-level tracing at run time.
Dynamic batching
A server technique that groups requests arriving within a short window into one batch to raise accelerator throughput at a small latency cost.
Readiness probe
A Kubernetes check that tells the cluster a container is ready to receive traffic, typically only after its model has loaded.
Shadow deployment
Running a new model on a copy of live traffic while discarding its outputs, so its behaviour can be measured without affecting users.
Canary release
Gradually shifting a small, increasing fraction of real traffic to a new version while comparing its health metrics with the current version.
Little's law
The relation L = λW between the average number of items in a stable system, their arrival rate and the average time each spends in it.

Check your understanding

  1. 1.

    A retailer recomputes product recommendations for every user once a night and shows them the next day. Which inference pattern fits best?

  2. 2.

    Why is safetensors preferred over pickle-based checkpoints for sharing model weights?

  3. 3.

    A service receives 400 requests per second at peak with a mean service time of 0.05 s. By Little's law, how many requests are in flight on average?

  4. 4.

    You want to check a new model's latency and output distribution on real traffic without any user ever seeing its predictions. What should you use?

  5. 5.

    Your GPU inference pods are overloaded and latency is climbing, but the HPA keeps them at 2 replicas because CPU utilisation is 20%. What is the best fix?

Exercises

Work each problem before opening the solution.

  1. Exercise 1Core

    For each scenario choose batch, real-time or streaming inference and give one sentence of justification:

    1. Monthly credit-limit review for all cardholders.
    2. Blocking a fraudulent card payment before it is authorised.
    3. Updating a delivery driver's estimated arrival time every few seconds from GPS events.
    4. Tagging 3 million archived photos with labels.
    Show solution
    1. Batch. The decision runs on a monthly schedule over every account, so a scheduled job is cheapest.
    2. Real-time. The decision must be returned within the payment's latency budget, before authorisation.
    3. Streaming. Predictions update continuously as events arrive and depend on recent state (the latest positions).
    4. Batch. It is a one-off bulk job with no user waiting; run it as a high-throughput offline job, ideally with large batches on GPUs.
  2. Exercise 2Core

    A model service must handle 120 requests per second at peak. The mean latency per request is 0.2 s, and one replica serves 6 concurrent requests within the latency SLO. Using a 70% target utilisation and one extra replica for resilience, how many replicas should you plan for?

    Show solution

    Little's law: L = λW = 120 × 0.2 = 24 requests in flight.

    Capacity per replica at 70% utilisation: 6 × 0.7 = 4.2.

    Replicas = ⌈24 / 4.2⌉ = ⌈5.71⌉ = 6.

    Add one for resilience: 7 replicas, with the autoscaler allowed to go higher if traffic exceeds forecast.

  3. Exercise 3Core

    Write a Python function parity_check(torch_model, onnx_path, n=256, atol=1e-4) that feeds the same random batch of shape (n, 20) to a PyTorch model and to an ONNX Runtime session whose input is named x, and returns the maximum absolute difference, raising an AssertionError if it exceeds atol.

    Show solution
    import numpy as np, torch, onnxruntime as ort
    
    def parity_check(torch_model, onnx_path, n=256, atol=1e-4):
        torch_model.eval()
        x = torch.randn(n, 20)
        with torch.no_grad():
            ref = torch_model(x).numpy()
        sess = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
        out = sess.run(None, {"x": x.numpy()})[0]
        max_diff = float(np.max(np.abs(out - ref)))
        assert max_diff <= atol, f"parity failed: max diff {max_diff:.2e} > {atol}"
        return max_diff
    

    Notes: eval() disables dropout and uses running batch-norm statistics, so both runs are deterministic. torch.no_grad() avoids building an autograd graph. In CI you should also run the check on a sample of recorded production inputs, because random Gaussian inputs may not exercise edge cases (very large values, zeros, padding).

  4. Exercise 4Challenge

    Your baseline click-through rate is 4%. Product wants to detect an absolute lift of 0.4 percentage points (to 4.4%) with α = 0.05 two-sided and 80% power. (a) Estimate the users needed per arm with n≈2(z0.975+z0.8)2p(1−p)/δ2n \approx 2(z_{0.975}+z_{0.8})^2 p(1-p)/\delta^2. (b) With 10,000 eligible users per day split evenly, how many days must the test run? (c) Name two reasons not to stop the test on day 3 even if the result already looks significant.

    Show solution

    (a) z0.975+z0.8=1.960+0.842=2.802z_{0.975}+z_{0.8} = 1.960 + 0.842 = 2.802; squared = 7.85.

    p(1−p)=0.04×0.96=0.0384p(1-p) = 0.04 \times 0.96 = 0.0384.

    Numerator: 2×7.85×0.0384=0.6032 \times 7.85 \times 0.0384 = 0.603.

    δ2=0.0042=0.000016\delta^2 = 0.004^2 = 0.000016.

    n≈0.603/0.000016≈37,700n \approx 0.603 / 0.000016 \approx 37{,}700 users per arm.

    (b) 5,000 users per arm per day → 37,700 / 5,000 ≈ 7.5 days; round up to 8 days, and in practice run two full weeks (14 days) to cover weekday effects evenly.

    (c) (1) Repeatedly checking and stopping at the first significant result inflates the false-positive rate well above 5% unless you use a sequential testing method designed for it. (2) Early days are unrepresentative: novelty effects, weekday mix, and the users who arrive first differ from the overall population. The planned sample size also guarantees the power you designed for.

  5. Exercise 5Challenge

    Design a release plan for replacing a production fraud model (real-time, 99.9% availability SLO, p99 < 250 ms). Specify the stages, the traffic share and duration of each, the metrics that gate each stage, and the rollback trigger and mechanism. Fraud labels (chargebacks) arrive 30–60 days after a transaction.

    Show solution

    A sound plan:

    1. Offline gate (CI): parity test of the exported artifact, latency benchmark on production hardware, evaluation on a recent hold-out window, and fairness or segment checks. Fail any gate and the model does not deploy.
    2. Shadow, 100% mirrored traffic, about 1 week: compare p50/p99 latency, error rate, score distribution (PSI against the current model's scores), and the decline rate the new model would have produced at the chosen threshold. Gate: p99 within SLO, errors ≤ baseline, would-be decline rate within an agreed band (e.g. ±10% relative) unless the change is intended.
    3. Canary 1% → 5% → 25%, each step at least 24 h: gate on availability, latency, actual decline rate, manual-review queue volume and customer complaints or contacts. The promotion between steps is automated when all gates pass.
    4. 100% rollout, with the old model kept deployed (blue/green) for at least the first weeks.
    5. Delayed-label evaluation: because chargebacks take 30–60 days, confirm the true fraud-capture gain later by comparing the arms' outcomes, and use proxy signals meanwhile (manual-review confirmations, early fraud reports).

    Rollback trigger: any SLO breach for 5 minutes, decline rate outside the band, or review-queue growth beyond a threshold. Mechanism: a router or traffic-split switch back to the old version (automatic for SLO breaches), followed by an incident review.

Further reading

Go further

Sources

Fact-checked and last reviewed 2026-11-05

Stay in the loop
Prometheus OS™ · Native AI OS · Built for the future © 2025 - 2026