Model Serving Patterns
Model Serving Patterns
Getting a trained model out of your experiment environment and into a path where it actually serves predictions to users is deceptively hard. The training job finished successfully, the evaluation metrics passed your thresholds, the model was promoted in the registry — and now the real work begins. Serving a model at production scale requires you to reason simultaneously about latency budgets, throughput ceilings, hardware cost, deployment topology, and failure semantics. This lesson covers the three canonical serving patterns you will encounter and the infrastructure decisions that determine whether each one fits your use case.
Real-Time Serving (Online Inference)
Real-time serving answers a prediction request synchronously within a latency budget — typically single-digit milliseconds for simple models to a few hundred milliseconds for large deep learning models. The client blocks on the response. This is the right pattern when the prediction is needed at request time: fraud detection on a payment, product recommendation on a page load, content moderation on a post submission.
The serving stack for real-time inference has three layers you already know from conventional microservices, plus a fourth that is ML-specific:
- Load balancer / API gateway: Routes prediction requests, enforces auth, applies rate limits, and provides the external HTTPS endpoint. No change from your existing services.
- Model server: The process that holds the model weights in memory (GPU VRAM or CPU RAM) and performs the forward pass. Production-grade model servers — Triton Inference Server, TorchServe, TF Serving, vLLM — expose gRPC and REST endpoints, support batching, handle multiple model versions concurrently, and emit Prometheus metrics. Do not wrap a raw Python script in a Flask app and call it a model server; that pattern collapses under any real load.
- Feature store (online path): For models that need pre-computed features not present in the request payload, the model server fetches them from a low-latency feature store (Redis, Feast online store, Tecton). This is the single biggest latency contributor outside the inference itself; keep it sub-5 ms.
- Model registry integration: The serving layer watches for new model versions in the registry and hot-swaps them without downtime. Triton supports this natively; TF Serving polls the model directory.
Batch Serving (Offline Inference)
Batch serving runs the model as a scheduled job over a large corpus of inputs and writes the results to a data store. Consumers then look up pre-computed predictions at query time instead of invoking the model. This pattern is correct when you can afford to pre-compute — personalized email content generated nightly, weekly churn scores on all customer accounts, overnight document classification across a document management system.
The trade-off is freshness. A batch job that runs every 24 hours means predictions are at most 24 hours stale. For many business problems that is fine. For fraud detection, it is not. Choose batch when:
- The input set is finite and enumerable (all users, all products, all documents).
- The prediction is valid for long enough that staleness does not cause harm.
- The model is too large or too slow to serve in real time within the latency budget.
- Cost is a constraint — batch inference on spot/preemptible GPU instances is typically 3-10x cheaper than always-on real-time endpoints.
At scale, batch inference runs on Spark (PySpark with pandas_udf to vectorize model calls), Ray Batch, or a simple Kubernetes Job that parallelizes over input shards. Outputs land in BigQuery, DynamoDB, Redis, or a feature store's offline path, depending on the lookup pattern.
Serving Frameworks in Production
NVIDIA Triton Inference Server is the industry default for GPU workloads. It supports TensorFlow SavedModel, ONNX, PyTorch TorchScript, TensorRT, and custom Python backends. Triton's ensemble scheduling allows you to chain pre-processing, inference, and post-processing as a single logical request. Dynamic batching aggregates requests arriving within a configurable window into a single GPU kernel invocation, dramatically improving GPU utilization without the client knowing.
vLLM is the standard for serving large language models. It implements PagedAttention — KV cache management borrowed from OS virtual memory — to eliminate memory waste from variable-length sequences. On a single A100 80GB, vLLM typically achieves 3-5x higher throughput than a naive HuggingFace generate() loop.
TF Serving is the canonical choice for TensorFlow SavedModel deployments. It is simpler than Triton and production-proven across Google's internal infrastructure. If your org is TF-heavy and you do not need multi-framework support, TF Serving is the lower-ops choice.
Ray Serve fills the gap when you need Python-first flexibility: custom pre/post-processing logic, ensemble models, or deployment graphs with branches. It integrates with the broader Ray ecosystem (Ray Train, Ray Tune) for end-to-end MLOps on a single cluster.
Autoscaling Inference
Inference workloads are bursty. A recommendation service sees 10x traffic spikes during peak shopping hours. A fraud model sees a surge whenever a large merchant runs a promotion. Static provisioning to handle peaks means spending GPU budget on idle capacity during off-peak hours. At $2–4/hr per A10G GPU, that idle cost accumulates fast.
Kubernetes Horizontal Pod Autoscaler (HPA) works for CPU-bound models but is blind to the GPU-specific signals that matter most: GPU utilization, batch queue depth, and pending request count. The production answer is KEDA (Kubernetes Event-Driven Autoscaling) combined with custom metrics from your model server:
For cost-sensitive or bursty workloads, consider scale-to-zero. Knative Serving and AWS SageMaker Serverless Inference both support true scale-to-zero: the deployment shrinks to zero replicas when idle and scales up on the first incoming request (cold-start latency: 1–30 seconds depending on model size). This is viable for models with infrequent traffic; it is catastrophically wrong for latency-sensitive paths where the cold start would breach your SLO.
resources.requests.nvidia.com/gpu equal to resources.limits.nvidia.com/gpu. GPU resources in Kubernetes are not compressible — the scheduler either fits the pod on a node with a free GPU or it does not. Setting a lower request than limit results in unpredictable scheduling and GPU sharing conflicts. Always set them equal.Load Testing Your Serving Stack
Never promote a model to real-time serving without a load test against a staging replica that matches production GPU type and memory. Triton's perf_analyzer is purpose-built for this; alternatively, k6 with a custom model request script works against any REST or gRPC endpoint.
nvidia-smi during load testing before setting maxReplicaCount. A rule of thumb: leave 10-15% VRAM headroom for KV cache growth, CUDA kernel overhead, and concurrent model versions.Canary Rollouts for Models
Deploying a new model version should follow the same canary discipline you apply to application code — but with the added complexity that model quality can degrade silently on subpopulations invisible in aggregate metrics. Use your service mesh (Istio, Linkerd) or your model server's traffic-splitting feature to route 5% of production traffic to the new version. Monitor both infrastructure metrics (latency, error rate) and model quality metrics (prediction score distribution, business KPIs) before promoting. If the new model's score distribution shifts significantly relative to the incumbent — measured with a KL divergence check in your monitoring pipeline — roll back immediately.
The combination of a model registry (lesson 3), a CI/CD pipeline that gates on evaluation metrics (lesson 5), a production serving layer with proper autoscaling, and drift monitoring (lesson 7) is what distinguishes a mature ML platform from a research project that happens to be running in production.