ML Deployment Research¶
📊 Data & AI Track · Level 7
When you'd use this
Triton server, BentoML, Ray Serve, ONNX, containerization and production inference.
Serve trained models in production — packaging, inference servers, batching — so models deliver predictions reliably.
Deployment landscape¶
Deployment landscape in ML Deployment — what it is and when to use it.
| Tool | Best for | Latency | Throughput |
|---|---|---|---|
| FastAPI + uvicorn | Simple models, prototypes | Low | Moderate |
| BentoML | Packaging + serving | Low | High |
| Ray Serve | Scalable, multi-model | Low | Very high |
| Triton Inference Server | GPU inference at scale | Very low | Very high |
| TorchServe | PyTorch-specific | Low | High |
| TFServing | TensorFlow-specific | Low | High |
Model export: ONNX¶
Export a trained model to a portable format for cross-framework serving.
Convert models to a portable format:
import torch
import torch.onnx
model = MyModel()
model.eval()
# Export to ONNX
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
dummy_input,
"model.onnx",
input_names=["image"],
output_names=["prediction"],
dynamic_axes={"image": {0: "batch_size"}, "prediction": {0: "batch_size"}},
opset_version=17,
)
# Verify
import onnx
model_onnx = onnx.load("model.onnx")
onnx.checker.check_model(model_onnx)
# Inference with ONNX Runtime
import onnxruntime as ort
import numpy as np
session = ort.InferenceSession("model.onnx")
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
outputs = session.run(None, {"image": input_data})
print(outputs[0].shape) # (1, 1000)
BentoML — package and serve¶
Bundle a model with its dependencies into a servable artifact.
# save_model.py
import bentoml
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(n_estimators=100)
model.fit(X_train, y_train)
# Save to BentoML model store
saved_model = bentoml.sklearn.save_model("churn_classifier", model)
print(f"Saved: {saved_model.tag}") # churn_classifier:abc123
# service.py
import bentoml
import numpy as np
from bentoml.io import NumpyNdarray, JSON
runner = bentoml.sklearn.get("churn_classifier:latest").to_runner()
svc = bentoml.Service("churn_service", runners=[runner])
@svc.api(input=NumpyNdarray(), output=JSON())
async def predict(input_array: np.ndarray):
prediction = await runner.predict.async_run(input_array)
return {"prediction": prediction.tolist()}
# Serve locally
bentoml serve service.py:svc --reload
# Build container
bentoml build
bentoml containerize churn_service:latest
# Deploy
docker run -p 3000:3000 churn_service:latest
Ray Serve — scalable serving¶
Scale model inference across a cluster with autoscaling.
from ray import serve
from ray.serve.handle import DeploymentHandle
import numpy as np
@serve.deployment(num_replicas=2, ray_actor_options={"num_gpus": 0.5})
class ModelDeployment:
def __init__(self):
import joblib
self.model = joblib.load("model.joblib")
async def __call__(self, request):
data = await request.json()
features = np.array(data["features"]).reshape(1, -1)
prediction = self.model.predict(features)
probability = self.model.predict_proba(features).max()
return {
"prediction": int(prediction[0]),
"confidence": float(probability),
}
# Compose multiple models
@serve.deployment
class Ensemble:
def __init__(self, model_a: DeploymentHandle, model_b: DeploymentHandle):
self.model_a = model_a
self.model_b = model_b
async def __call__(self, request):
# Run both models in parallel
ref_a = self.model_a.remote(request)
ref_b = self.model_b.remote(request)
result_a, result_b = await ref_a, await ref_b
# Average predictions
return {"prediction": (result_a["prediction"] + result_b["prediction"]) / 2}
# Deploy
app = ModelDeployment.bind()
serve.run(app, host="0.0.0.0", port=8000)
Monitoring and drift detection¶
Watch live predictions for drift and quality drops.
import numpy as np
from scipy.stats import ks_2samp
class DriftDetector:
def __init__(self, reference_data: np.ndarray, threshold=0.05):
self.reference = reference_data
self.threshold = threshold
def check_drift(self, current_data: np.ndarray) -> dict:
results = {}
for i in range(current_data.shape[1]):
stat, p_value = ks_2samp(self.reference[:, i], current_data[:, i])
results[f"feature_{i}"] = {
"statistic": float(stat),
"p_value": float(p_value),
"drift_detected": p_value < self.threshold,
}
return results
# Usage
detector = DriftDetector(X_train)
drift_report = detector.check_drift(X_new_batch)
drifted = [k for k, v in drift_report.items() if v["drift_detected"]]
if drifted:
print(f"⚠️ Drift detected in: {drifted}")
# Trigger retraining pipeline
A/B testing models¶
Compare model versions on real traffic before full rollout.
import random
class ModelRouter:
def __init__(self, models: dict, traffic_split: dict):
self.models = models # {"v1": model_v1, "v2": model_v2}
self.traffic_split = traffic_split # {"v1": 0.9, "v2": 0.1}
def predict(self, features):
# Route based on traffic split
r = random.random()
cumulative = 0
for version, weight in self.traffic_split.items():
cumulative += weight
if r <= cumulative:
model = self.models[version]
prediction = model.predict(features)
self._log_prediction(version, features, prediction)
return {"prediction": prediction, "model_version": version}
def _log_prediction(self, version, features, prediction):
# Log to tracking system for later analysis
pass
router = ModelRouter(
models={"v1": model_old, "v2": model_new},
traffic_split={"v1": 0.9, "v2": 0.1}, # 10% to new model
)
Docker deployment pattern¶
Containerize a model server for reproducible deploys.
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY model/ ./model/
COPY serve.py .
EXPOSE 8000
HEALTHCHECK --interval=30s CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "serve:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
# docker-compose.yml
services:
model-api:
build: .
ports: ["8000:8000"]
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
Practice Exercises¶
- Export a PyTorch model to ONNX and compare inference speed with ONNX Runtime vs native PyTorch.
- Build a BentoML service with preprocessing, prediction and postprocessing.
- Implement drift detection using KS-test on each feature and trigger alerts.
- Set up A/B testing with 90/10 traffic split and log metrics for comparison.
- Dockerize a model API with health checks, proper logging and GPU support.
- Implement a model registry that tracks versions, metrics and deployment status.
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.