Scaling ML su Kubernetes: Deployment e Orchestrazione in Produzione
Il tuo modello di machine learning ha superato tutti i test offline, le metriche sono eccellenti, il serving via FastAPI funziona perfettamente in locale. Poi arriva il momento critico: devi gestire 10.000 richieste al secondo, scalare dinamicamente in base al carico, garantire alta disponibilità e zero downtime durante gli aggiornamenti. Un singolo container non e più sufficiente.
Kubernetes e diventato lo standard de facto per l'orchestrazione di workload ML in produzione, con il 78% delle organizzazioni enterprise che lo utilizzano per il deployment di modelli secondo il CNCF Survey 2025. Non e pero sufficiente mettere il modello in un Pod: occorre gestire GPU scheduling, autoscaling event-driven, resource quotas, canary deployments e monitoring specializzato per l'inferenza. Il mercato MLOps, che varra $4.38 miliardi nel 2026, con un CAGR del 39.8%, ha in Kubernetes il suo motore di scaling principale.
In questo articolo esploriamo l'architettura completa per portare modelli ML su Kubernetes in produzione: da KServe e Seldon Core per l'inference serving, al GPU scheduling con NVIDIA Device Plugin, all'autoscaling intelligente con HPA, VPA e KEDA, fino al monitoring con Prometheus e Grafana.
Cosa Imparerai
- perchè Kubernetes e lo standard per ML in produzione e quando usarlo
- GPU scheduling e sharing con NVIDIA Device Plugin e MIG
- KServe: deploy di InferenceService con canary rollout e scale-to-zero
- Seldon Core v2: pipeline composabili e multi-model serving
- Autoscaling avanzato: HPA, VPA e KEDA per workload ML event-driven
- Resource management: requests, limits, priority classes e node affinity
- Monitoring con Prometheus + Grafana specializzato per ML inference
- Cost optimization e best practices per cluster GPU
perchè Kubernetes per ML in Produzione
Prima di immergersi nella configurazione tecnica, e fondamentale capire perchè Kubernetes si e affermato come piattaforma di riferimento per i workload ML, superando soluzioni come bare metal, VM dedicate o servizi cloud proprietari.
I workload ML hanno caratteristiche uniche rispetto alle applicazioni web tradizionali. Il training richiede GPU massicce per ore o giorni, poi le risorse devono essere rilasciate. L'inferenza ha picchi imprevedibili e latenza critica. I modelli devono essere aggiornati senza downtime. I dataset possono essere enormi e richiedono storage specializzato. Kubernetes affronta tutti questi scenari con primitive native: Pod scheduling su nodi GPU specifici, Persistent Volumes per dataset, Jobs per training batch, HorizontalPodAutoscaler per scaling dell'inferenza.
Kubernetes vs Alternativa Cloud-Managed
Kubernetes self-managed o managed (EKS, GKE, AKS): controllo completo,
portabilita multi-cloud, costi ottimizzabili, ma complessità operativa elevata.
SageMaker / Vertex AI / Azure ML: setup rapido, integrazione cloud-native,
ma vendor lock-in, costi più alti a lungo termine e meno flessibilità per architetture custom.
Regola pratica: team <5 persone o budget limitato? Inizia con managed ML.
Team >5 con modelli multipli in produzione? Kubernetes ripaga l'investimento in 6-12 mesi.
Architettura di Riferimento
Un cluster Kubernetes per ML in produzione si struttura tipicamente su tre layer distinti, ognuno con responsabilità ben definite:
- Infrastructure Layer: nodi CPU per serving leggero e orchestrazione, nodi GPU per training e inferenza pesante, nodi di storage per dataset e artefatti. Le GPU pool sono separate con node labels specifici.
- Platform Layer: KServe o Seldon Core per l'inference serving, Kubeflow per pipeline di training, MLflow per experiment tracking (vedi articolo 4), Argo Workflows per orchestrazione complessa.
- Observability Layer: Prometheus per metriche, Grafana per dashboard, Jaeger per distributed tracing, Loki per log aggregation.
# Namespace structure per ML cluster
# Separare ambienti e responsabilità
kubectl create namespace ml-training # Job di training
kubectl create namespace ml-serving # Inference services
kubectl create namespace ml-monitoring # Prometheus, Grafana
kubectl create namespace mlflow # Experiment tracking
kubectl create namespace kubeflow # Pipeline orchestration
# Label nodi per GPU scheduling
kubectl label nodes gpu-node-1 accelerator=nvidia-a100
kubectl label nodes gpu-node-2 accelerator=nvidia-t4
kubectl label nodes cpu-node-1 workload=inference-cpu
GPU Scheduling e Sharing
Le GPU sono la risorsa più costosa del cluster ML. Gestirle male significa sprecare decine di
migliaia di euro al mese. Kubernetes espone le GPU come risorse schedulabili tramite il
NVIDIA Device Plugin, un DaemonSet che rileva automaticamente le GPU sui nodi
e le registra come nvidia.com/gpu nel kubelet.
# Installazione NVIDIA Device Plugin via Helm
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update
helm upgrade -i nvdp nvdp/nvidia-device-plugin \
--namespace kube-system \
--set failOnInitError=false
# Verifica GPU disponibili sui nodi
kubectl describe nodes | grep -A5 "Allocatable:"
# Output atteso:
# nvidia.com/gpu: 8
# cpu: 96
# memory: 768Gi
# Pod che richiede 1 GPU intera
apiVersion: v1
kind: Pod
metadata:
name: ml-training-job
spec:
containers:
- name: trainer
image: pytorch/pytorch:2.5.0-cuda12.4-cudnn9-runtime
resources:
limits:
nvidia.com/gpu: 1 # Richiede 1 GPU intera
cpu: "8"
memory: "32Gi"
requests:
nvidia.com/gpu: 1
cpu: "4"
memory: "16Gi"
nodeSelector:
accelerator: nvidia-a100 # Forza scheduling su A100
Per workload di inferenza che non richiedono una GPU intera, NVIDIA offre due strategie di GPU sharing: Time-Slicing e Multi-Instance GPU (MIG).
# Configurazione Time-Slicing (per GPU T4/V100, sharing software)
# Ogni GPU fisica viene divisa in N repliche logiche
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config
namespace: kube-system
data:
any: |-
version: v1
flags:
migStrategy: none
sharing:
timeSlicing:
replicas: 4 # 4 pod condividono 1 GPU fisica
failRequestsGreaterThanOne: false
# Apply al device plugin
kubectl patch clusterpolicies/cluster-policy \
-n gpu-operator --type merge \
-p '{"spec": {"devicePlugin": {"config": {"name": "time-slicing-config"}}}}'
# Configurazione MIG per A100/H100 (hardware isolation)
# Partiziona A100 in 7 istanze MIG da 10GB ciascuna
nvidia-smi mig -cgi 9,9,9,9,9,9,9 -C
# Pod che usa una slice MIG
resources:
limits:
nvidia.com/mig-1g.10gb: 1 # Usa 1 istanza MIG da 10GB
Time-Slicing vs MIG: Quando Usare Quale
Time-Slicing: adatto per workload di inferenza leggeri (modelli <2GB VRAM), introduce latenza da context switching. Funziona su qualsiasi GPU NVIDIA. MIG: isolamento hardware completo, memoria dedicata garantita, zero interference tra workload. Disponibile solo su A100, A30, H100. Ideale per SLA di latenza stringenti. Non combinare mai i due approcci sullo stesso nodo.
KServe: Inference Serving Nativo su Kubernetes
KServe (ex KFServing) e lo standard CNCF per l'inference serving su Kubernetes,
nato dalla collaborazione tra Google, IBM, Bloomberg e altri. Fornisce un'astrazione
InferenceService che nasconde la complessità del deployment, gestendo automaticamente
canary rollout, scale-to-zero, autoscaling basato su richieste, e supporto per molteplici framework
(PyTorch, TensorFlow, scikit-learn, XGBoost, ONNX, Hugging Face).
# Installazione KServe (versione 0.13+)
kubectl apply -f https://github.com/kserve/kserve/releases/download/v0.13.0/kserve.yaml
# Verifica installazione
kubectl get pods -n kserve
# kserve-controller-manager-xxx Running
# kserve-gateway-xxx Running
# InferenceService per modello scikit-learn
apiVersion: "serving.kserve.io/v1beta1"
kind: "InferenceService"
metadata:
name: "churn-predictor"
namespace: ml-serving
annotations:
serving.kserve.io/enable-prometheus-scraping: "true"
spec:
predictor:
minReplicas: 1
maxReplicas: 10
scaleTarget: 50 # Target: 50 req/sec per replica
scaleMetric: rps # Scala in base a requests-per-second
sklearn:
storageUri: "gs://my-ml-bucket/models/churn-model/v3"
runtimeVersion: "1.5.2"
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
Uno dei punti di forza di KServe e il supporto nativo per i Canary Rollout: puoi inviare una percentuale del traffico alla nuova versione del modello mentre il resto continua a usare quella stabile, esattamente come l'A/B testing che abbiamo visto nell'articolo precedente di questa serie.
# Canary Rollout: 20% traffico alla nuova versione
apiVersion: "serving.kserve.io/v1beta1"
kind: "InferenceService"
metadata:
name: "churn-predictor"
namespace: ml-serving
spec:
predictor:
# Versione stabile (80% traffico)
minReplicas: 2
maxReplicas: 8
sklearn:
storageUri: "gs://my-ml-bucket/models/churn-model/v3"
runtimeVersion: "1.5.2"
canaryTrafficPercent: 80
# Versione canary (20% traffico)
predictor:
# NOTA: nella specifica KServe, il canary si gestisce con
# l'annotation traffic-split
# Ecco la sintassi corretta tramite Knative revisions:
---
# Alternativa: InferenceGraph per traffic splitting esplicito
apiVersion: "serving.kserve.io/v1alpha1"
kind: "InferenceGraph"
metadata:
name: "churn-ab-split"
namespace: ml-serving
spec:
nodes:
root:
routerType: WeightedEnsemble
routes:
- serviceName: churn-predictor-v3
weight: 80
- serviceName: churn-predictor-v4
weight: 20
# Test dell'endpoint
curl -X POST \
http://churn-predictor.ml-serving.svc.cluster.local/v1/models/churn-predictor:predict \
-H 'Content-Type: application/json' \
-d '{"instances": [[35, 12000, 2, 1, 0.8, 3]]}'
La feature di scale-to-zero di KServe (basata su Knative Serving) e particolarmente preziosa per modelli usati occasionalmente: il pod si spegne dopo un periodo di inattivita configurabile e si riavvia automaticamente alla prima richiesta, con un cold start tipicamente inferiore ai 30 secondi per modelli pre-cached.
# Configurazione scale-to-zero con timeout personalizzato
apiVersion: "serving.kserve.io/v1beta1"
kind: "InferenceService"
metadata:
name: "batch-analyzer"
namespace: ml-serving
annotations:
# Scale-to-zero dopo 5 minuti di inattivita
autoscaling.knative.dev/scaleToZeroGracePeriod: "300s"
# Window per calcolo scale-up
autoscaling.knative.dev/window: "60s"
# Utilization target (in percentuale)
autoscaling.knative.dev/targetUtilizationPercentage: "70"
spec:
predictor:
minReplicas: 0 # Abilita scale-to-zero
maxReplicas: 5
pytorch:
storageUri: "gs://my-ml-bucket/models/analyzer/v1"
runtimeVersion: "2.5.0"
Seldon Core v2: Pipeline ML Composabili
Mentre KServe eccelle nel serving di singoli modelli, Seldon Core v2 brilla nella gestione di architetture ML complesse: pipeline multi-step, ensemble di modelli, A/B routing con business logic, e integrazione con Kafka per stream processing. Seldon v2 utilizza MLServer come runtime di inferenza, compatibile con il protocollo V2 (KFServing Inference Protocol), e supporta natively PyTorch, scikit-learn, XGBoost, Hugging Face e modelli custom.
# Installazione Seldon Core v2 via Helm
helm repo add seldonio https://storage.googleapis.com/seldon-charts
helm install seldon-core-v2 seldonio/seldon-core-v2 \
--namespace seldon-mesh \
--create-namespace \
--set controller.clusterwide=true
# Model: singolo modello XGBoost
apiVersion: mlops.seldon.io/v1alpha1
kind: Model
metadata:
name: churn-xgb
namespace: ml-serving
spec:
storageUri: "gs://my-ml-bucket/models/churn-xgb/v2"
requirements:
- xgboost
memory: 100Mi
# Pipeline: preprocessing + prediction + postprocessing
apiVersion: mlops.seldon.io/v1alpha1
kind: Pipeline
metadata:
name: churn-pipeline
namespace: ml-serving
spec:
steps:
- name: preprocessor
inputs:
- churn-pipeline.inputs
- name: churn-xgb
inputs:
- preprocessor.outputs
- name: postprocessor
inputs:
- churn-xgb.outputs
output:
steps:
- postprocessor
# Autoscaling basato su RPS
replicas: 1
scaling:
replicas:
minReplicas: 1
maxReplicas: 8
metric: rps
target: 100
KServe vs Seldon Core: Quale Scegliere
- KServe: migliore per modelli singoli, integrazione con Knative e Istio, scale-to-zero nativo, comunita CNCF. Ideale per team che iniziano con K8s ML.
- Seldon Core v2: migliore per pipeline complesse, ensemble, integrazione Kafka, multi-model serving. Ideale per architetture ML avanzate con routing business logic.
- Entrambi: supportano il protocollo V2, monitoring Prometheus, Triton per GPU serving. Non sono mutualmente esclusivi, alcune organizzazioni li usano insieme per casi d'uso diversi.
Autoscaling Intelligente: HPA, VPA e KEDA
Il scaling dei workload ML e più complesso rispetto alle applicazioni web tradizionali. Le metriche CPU e memoria spesso non riflettono accuratamente il carico reale di un modello: un modello GPU-bound può saturare la scheda grafica mentre la CPU e all'80% di idle. Kubernetes offre tre meccanismi di autoscaling complementari che, usati correttamente insieme, coprono tutti gli scenari ML.
# HPA (Horizontal Pod Autoscaler): scala il numero di repliche
# Configurazione per inference service basata su custom metrics
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: churn-predictor-hpa
namespace: ml-serving
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: churn-predictor
minReplicas: 2
maxReplicas: 20
metrics:
# Scala su CPU (fallback)
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# Scala su custom metric: inference latency P99
- type: Pods
pods:
metric:
name: inference_request_duration_p99
target:
type: AverageValue
averageValue: "500m" # 500ms P99 latency target
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # Reazione rapida al traffico
policies:
- type: Percent
value: 100
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300 # Lento a rimuovere pod (warm models)
# VPA (Vertical Pod Autoscaler): ottimizza resources requests/limits
# NOTA: non usare VPA e HPA sulle stesse metriche CPU/Memory!
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: batch-trainer-vpa
namespace: ml-training
spec:
targetRef:
apiVersion: batch/v1
kind: Job
name: model-training
updatePolicy:
updateMode: "Off" # Solo raccomandazioni, non applica auto (Off | Initial | Recreate)
resourcePolicy:
containerPolicies:
- containerName: trainer
minAllowed:
cpu: "1"
memory: 4Gi
maxAllowed:
cpu: "16"
memory: 128Gi
controlledResources: ["cpu", "memory"]
# Leggi le raccomandazioni VPA
kubectl describe vpa batch-trainer-vpa
# Output:
# Recommendation:
# Container Recommendations:
# Container Name: trainer
# Lower Bound: cpu: 2, memory: 8Gi
# Target: cpu: 6, memory: 32Gi
# Upper Bound: cpu: 12, memory: 64Gi
KEDA (Kubernetes Event-Driven Autoscaler, CNCF graduated project) e lo strumento più potente per i workload ML event-driven: scala i pod basandosi su eventi da code di messaggi, database, metriche Prometheus o trigger HTTP, permettendo anche lo scale-to-zero per batch processing.
# KEDA: scala i worker ML in base alla coda di inference requests
# Installazione
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --namespace keda --create-namespace
# ScaledObject per batch ML processing da RabbitMQ
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: ml-batch-processor-scaler
namespace: ml-serving
spec:
scaleTargetRef:
name: batch-ml-processor
minReplicaCount: 0 # Scale-to-zero quando coda vuota
maxReplicaCount: 30 # Max 30 worker per GPU cluster
pollingInterval: 15 # Controlla la coda ogni 15 secondi
cooldownPeriod: 60 # Aspetta 60s prima di scale-down
triggers:
- type: rabbitmq
metadata:
host: amqp://rabbitmq.ml-serving.svc.cluster.local
queueName: inference-requests
mode: QueueLength
value: "10" # 1 pod ogni 10 messaggi in coda
# Trigger alternativo: Prometheus metric
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc.cluster.local:9090
metricName: inference_queue_depth
query: sum(inference_queue_depth{namespace="ml-serving"})
threshold: "50" # 1 replica ogni 50 richieste pendenti
Pattern Autoscaling per ML: Raccomandazione
Combina i tre meccanismi con responsabilità distinte:
- HPA su latency/RPS per inference online (reattivo, veloce)
- VPA in modalità Off per ottimizzare le requests di training job (consulta e aggiorna manualmente)
- KEDA per batch processing e pipeline event-driven (scale-to-zero incluso)
Non usare HPA e VPA sulla stessa risorsa (CPU/Memory) contemporaneamente: i conflitti
di scaling causano oscillazioni imprevedibili e sprechi di risorse.
Resource Management e Priority Classes
Su un cluster condiviso tra team ML diversi, la gestione delle risorse e fondamentale per evitare che un job di training blocchi l'inferenza in produzione o che un esperimento consumi tutte le GPU disponibili. Kubernetes offre tre strumenti: ResourceQuota, LimitRange e PriorityClass.
# ResourceQuota: limita risorse per namespace
apiVersion: v1
kind: ResourceQuota
metadata:
name: ml-serving-quota
namespace: ml-serving
spec:
hard:
requests.cpu: "40"
requests.memory: 160Gi
limits.cpu: "80"
limits.memory: 320Gi
requests.nvidia.com/gpu: "4" # Max 4 GPU per inference namespace
limits.nvidia.com/gpu: "4"
pods: "50"
---
# LimitRange: imposta defaults e limiti per singolo container
apiVersion: v1
kind: LimitRange
metadata:
name: ml-container-limits
namespace: ml-serving
spec:
limits:
- type: Container
default: # Default limits se non specificati
cpu: "2"
memory: 4Gi
defaultRequest: # Default requests se non specificati
cpu: "500m"
memory: 1Gi
max: # Massimo per container
cpu: "8"
memory: 32Gi
nvidia.com/gpu: "2"
min: # Minimo per container
cpu: "100m"
memory: 256Mi
---
# PriorityClass: garantisce che l'inferenza non venga preemptata dal training
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: ml-inference-critical
value: 1000000 # Alta priorità per serving
globalDefault: false
description: "Inference services critici - non preemptabili"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: ml-training-batch
value: 100000 # Bassa priorità per training
preemptionPolicy: PreemptLowerPriority
description: "Training jobs - preemptabili se necessario"
Il Node Affinity e i Taints/Tolerations permettono di controllare con precisione su quali nodi vengono schedulati i workload ML, garantendo che i job di training non competano con l'inferenza sulle stesse GPU:
# Taint nodi GPU dedicati all'inferenza
kubectl taint nodes gpu-inference-1 dedicated=inference:NoSchedule
kubectl taint nodes gpu-inference-2 dedicated=inference:NoSchedule
# Solo pod con toleration possono usare questi nodi
# InferenceService spec con affinity e toleration
spec:
predictor:
tolerations:
- key: dedicated
operator: Equal
value: inference
effect: NoSchedule
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: accelerator
operator: In
values:
- nvidia-a100
- nvidia-h100
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: churn-predictor
topologyKey: kubernetes.io/hostname
# Distribuisce repliche su host diversi per HA
Monitoring con Prometheus e Grafana per ML
Il monitoring di un sistema ML su Kubernetes richiede metriche a due livelli: le metriche infrastrutturali standard di Kubernetes (CPU, memoria, rete) e le metriche specifiche dell'inferenza ML (latenza per modello, throughput, error rate, data drift signal). KServe e Seldon espongono automaticamente metriche Prometheus nel formato standard.
# Configurazione Prometheus per scraping KServe metrics
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: kserve-inference-monitor
namespace: ml-monitoring
labels:
release: prometheus
spec:
namespaceSelector:
matchNames:
- ml-serving
selector:
matchLabels:
serving.kserve.io/inferenceservice: "true"
endpoints:
- port: metrics
interval: 15s
path: /metrics
honorLabels: true
# Metriche KServe esposte automaticamente:
# kserve_inference_request_total{model_name, namespace, status_code}
# kserve_inference_request_duration_seconds{model_name, quantile}
# kserve_inference_request_size_bytes{model_name}
# kserve_inference_response_size_bytes{model_name}
# PrometheusRule: alert per latenza elevata
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: ml-inference-alerts
namespace: ml-monitoring
spec:
groups:
- name: ml-inference.rules
rules:
- alert: HighInferenceLatency
expr: |
histogram_quantile(0.99,
rate(kserve_inference_request_duration_seconds_bucket[5m])
) > 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "P99 latency > 1s per {{ $labels.model_name }}"
description: "Modello {{ $labels.model_name }} ha latenza P99 di {{ $value }}s"
- alert: ModelErrorRateHigh
expr: |
rate(kserve_inference_request_total{status_code!="200"}[5m])
/ rate(kserve_inference_request_total[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "Error rate > 5% per {{ $labels.model_name }}"
# Dashboard Grafana: query principali per ML inference monitoring
# (da importare come JSON o configurare manualmente)
# 1. Throughput modello (req/sec)
rate(kserve_inference_request_total[5m])
# 2. Latenza P50, P95, P99
histogram_quantile(0.50, rate(kserve_inference_request_duration_seconds_bucket[5m]))
histogram_quantile(0.95, rate(kserve_inference_request_duration_seconds_bucket[5m]))
histogram_quantile(0.99, rate(kserve_inference_request_duration_seconds_bucket[5m]))
# 3. GPU Utilization per nodo (richiede NVIDIA DCGM Exporter)
DCGM_FI_DEV_GPU_UTIL{namespace="ml-serving"}
# 4. GPU Memory in uso
DCGM_FI_DEV_FB_USED{namespace="ml-serving"} /
DCGM_FI_DEV_FB_TOTAL{namespace="ml-serving"} * 100
# 5. Numero repliche attive per modello
kube_deployment_status_replicas_available{
namespace="ml-serving",
deployment=~".*-predictor.*"
}
# 6. Scaling events (utile per debug autoscaler)
kube_horizontalpodautoscaler_status_desired_replicas{
namespace="ml-serving"
}
Cost Optimization per Cluster ML
Le GPU sono la voce di costo dominante di un cluster ML: una NVIDIA A100 SXM4 costa circa $2-3/ora su cloud, un H100 $4-5/ora. Su un cluster di 20 GPU, il costo mensile può superare i $100.000. L'ottimizzazione dei costi non e opzionale.
Strategie di Cost Optimization
- Spot/Preemptible instances per training: risparmio del 60-80% per job tolleranti alle interruzioni. Usa checkpoint frequenti e Argo Workflows per resume automatico.
- Scale-to-zero per modelli occasionali: KServe con minReplicas=0 azzera il costo delle GPU quando il modello non riceve traffico.
- GPU Time-Slicing per inferenza leggera: 4-8 modelli per GPU fisica riducono il costo per modello di 4-8x.
- Cluster Autoscaler con node pools misti: nodi GPU aggiunti/rimossi automaticamente in base al carico reale del cluster.
- Node consolidation con Karpenter: consolida pod su meno nodi prima di terminare i nodi vuoti (risparmio 20-40% su cluster con utilizzo variabile).
# Cluster Autoscaler per node pool GPU
# (esempio GKE, ma concettualmente uguale per EKS/AKS)
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-config
namespace: kube-system
data:
config.yaml: |
nodeGroups:
- name: gpu-a100-pool
minSize: 0 # Scala a zero se nessun workload GPU
maxSize: 10 # Max 10 nodi A100
machineType: a2-highgpu-1g
expander: least-waste # Usa il nodo che spreca meno risorse
scaleDownUnneededTime: 10m # Rimuovi nodo inutilizzato dopo 10 min
scaleDownUtilizationThreshold: 0.5 # Scala down se utilizzo < 50%
skipNodesWithSystemPods: false
# Cost tracking con labels obbligatorie su tutti i workload
# Ogni Job/Deployment deve avere questi labels per tracking
metadata:
labels:
cost-center: "data-science"
project: "churn-prediction"
environment: "production"
model-version: "v3"
Deploy Completo: Pipeline End-to-End
Vediamo come integrare tutti i componenti in un deployment Helm completo per un modello di produzione, combinando KServe, Prometheus monitoring e autoscaling con KEDA:
# Helm Chart structure per ML service completo
# charts/ml-inference-service/
# ├── Chart.yaml
# ├── values.yaml
# └── templates/
# ├── inference-service.yaml
# ├── hpa.yaml
# ├── service-monitor.yaml
# └── network-policy.yaml
# values.yaml
model:
name: "churn-predictor"
version: "v3"
storageUri: "gs://my-ml-bucket/models/churn-model/v3"
framework: "sklearn"
runtimeVersion: "1.5.2"
scaling:
minReplicas: 2
maxReplicas: 20
targetRPS: 50
targetLatencyP99: "500m"
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
monitoring:
enabled: true
prometheusNamespace: "ml-monitoring"
grafanaDashboard: true
canary:
enabled: false
weight: 0
# Deploy
helm upgrade --install churn-predictor ./charts/ml-inference-service \
--namespace ml-serving \
--values production-values.yaml \
--wait --timeout 10m
# Verifica deployment
kubectl get inferenceservice -n ml-serving
# NAME URL READY PREV LATEST ...
# churn-predictor http://churn-predictor True 0 100 ...
Best Practices e Anti-Pattern
Dopo aver visto l'implementazione tecnica, ecco le lezioni apprese da deployment ML su Kubernetes in ambienti enterprise:
Best Practices
- Sempre specificare resource requests E limits: senza requests, il scheduler non può piazzare correttamente i pod. Senza limits, un modello OOM può destabilizzare l'intero nodo.
- Usa readiness probe specifiche per ML: un container può essere "running" ma il modello ancora in fase di loading. La readiness probe deve verificare che il modello sia effettivamente pronto a servire.
- Pre-pull delle immagini sui nodi GPU: le immagini PyTorch con CUDA superano spesso i 10GB. Usa DaemonSet o image pre-caching per evitare cold start elevati.
- Separa namespace per ambienti: staging e production su namespace diversi con ResourceQuota distinte evita interferenze accidentali.
- Implementa circuit breaker: se il modello ha error rate > 10%, interrompi il traffico automaticamente con Istio o un sidecar proxy.
- Versioning esplicito dei modelli: ogni InferenceService deve avere un tag di versione nel nome o nelle label. Mai usare "latest" in produzione.
Anti-Pattern da Evitare
- Training su nodi di inferenza: i job di training saturano CPU/GPU e causano latency spike sui modelli in produzione. Usa sempre node pools separati con taints.
- HPA su CPU per modelli GPU-bound: la CPU può essere bassa mentre la GPU e satura. Usa sempre metriche custom (latency, RPS, GPU utilization) per GPU workloads.
-
Nessun graceful shutdown: i pod ML devono completare le richieste in
corso prima di terminare. Configura sempre
terminationGracePeriodSeconds>= 30s. - Modelli in immagini Docker: includere i pesi del modello nell'immagine Docker rende gli aggiornamenti lenti e le immagini enormi. Usa model storage separato (S3, GCS).
-
Nessun budget di disruption: senza
PodDisruptionBudget, un aggiornamento del cluster può rimuovere tutte le repliche del modello simultaneamente.
Budget per PMI: Iniziare con Meno di 5.000 EUR/Anno
Non serve un cluster enterprise da $100K/mese per iniziare con ML su Kubernetes. Ecco uno stack realistico per una PMI con budget limitato:
- Cluster K3s su VM cloud (2 nodi, 8 vCPU, 32GB RAM): circa 150-200 EUR/mese. K3s e la distribuzione Kubernetes lightweight di Rancher, perfetta per cluster piccoli.
- 1 nodo GPU NVIDIA T4 (spot instance): 0.35-0.50 EUR/ora, circa 120-180 EUR/mese se usato 12 ore/giorno. Scale-to-zero quando non serve.
- KServe + MLflow + Prometheus: tutti gratuiti, open-source, installabili con Helm in 30 minuti.
- Storage S3-compatible (MinIO self-hosted): 0 costi di licensing, solo storage. 100GB di modelli e dataset: circa 2-5 EUR/mese su object storage cloud.
Totale stimato: 300-400 EUR/mese, meno di 5.000 EUR/anno per un ambiente production-ready con GPU, monitoring completo e autoscaling. Per confronto, SageMaker con configurazione equivalente costerebbe 3-5x di più.
Conclusioni
Kubernetes e lo standard industriale per il deployment di modelli ML in produzione non per caso: offre la combinazione unica di GPU scheduling, autoscaling event-driven, isolamento dei workload e ecosystem di tool specializzati (KServe, Seldon, KEDA) che nessun'altra piattaforma può eguagliare in termini di flessibilità e costo a lungo termine.
Il percorso ottimale per chi inizia: configura un cluster K3s con un nodo GPU, installa KServe per il serving del primo modello, aggiungi Prometheus e Grafana per il monitoring, e solo quando il cluster cresce oltre 5-10 modelli in produzione investi in KEDA e Seldon Core per architetture più complesse. La complessità di Kubernetes si ripaga solo quando il volume di workload la giustifica.
Nel prossimo articolo della serie esploriamo la Governance ML: come garantire compliance con l'AI Act EU, implementare explainability con SHAP e LIME, gestire audit trail e fairness dei modelli in produzione.
Articoli Correlati in Questa Serie
- Serving Modelli: FastAPI + Uvicorn in Produzione - Il modello prima di Kubernetes
- A/B Testing di Modelli ML - Canary rollout e traffic splitting
- Governance ML: Compliance, Audit, Ethics - Prossimo articolo
- Model Drift Detection e Retraining Automatico - Monitoring avanzato
Cross-Serie
- Serie Deep Learning Avanzato - Training di modelli complessi da deployare su K8s
- Serie Computer Vision - Modelli CV ottimizzati per GPU inference







