Workload AI e GPU su Kubernetes: Device Plugin e Job di Training
Nel 2026 il 66% dei cluster di inferenza AI gira su Kubernetes (CNCF Survey 2026). La ragione e semplice: Kubernetes risolve i problemi operativi piu difficili dei workload AI - scheduling intelligente delle GPU, scaling elastico dei job di training, integrazione con storage distribuito per i dataset, retry automatico in caso di node failure. Ma configurare Kubernetes per i workload GPU richiede competenze specifiche che vanno ben oltre il deployment di una normale applicazione web.
In questo articolo vedremo come configurare il NVIDIA Device Plugin per esporre le GPU al cluster, come schedulare Job di training distribuito con PyTorch e TensorFlow, come usare Karpenter per fare spot scaling delle GPU (riducendo i costi del 40-70%), e i pattern per ottimizzare l'utilizzo GPU nei workload di inferenza in produzione.
Cosa Imparerai
- Installazione e configurazione del NVIDIA Device Plugin per Kubernetes
- Schedulare Pod con richiesta GPU (nvidia.com/gpu resource)
- Training distribuito con PyTorchJob e TFJob (Kubeflow Training Operator)
- Karpenter NodePool per provisioning automatico di nodi GPU spot
- GPU time-slicing per condividere GPU tra piu Pod
- MIG (Multi-Instance GPU) per partizione di GPU A100/H100
- Monitoring GPU con DCGM Exporter e Grafana
- Pattern per inferenza ad alta throughput con TorchServe su K8s
Architettura GPU su Kubernetes
Kubernetes non conosce nativamente le GPU. Le GPU vengono esposte al cluster tramite il Device Plugin Framework: un DaemonSet che gira su ogni nodo con GPU, si registra presso il kubelet, e gestisce l'allocazione delle GPU ai container. Il NVIDIA Device Plugin e l'implementazione piu diffusa di questo framework.
Installazione NVIDIA Device Plugin
# Pre-requisiti: NVIDIA GPU drivers installati sui nodi
# Verifica driver sui nodi
kubectl get nodes -l accelerator=nvidia
kubectl describe node gpu-node-1 | grep -i nvidia
# Installa NVIDIA Device Plugin con Helm
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update
helm install nvdp nvdp/nvidia-device-plugin \
--namespace kube-system \
--version 0.16.0 \
--set failOnInitError=false
# Oppure con manifest diretto
kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.16.0/deployments/static/nvidia-device-plugin.yml
# Verifica che le GPU siano visibili nel cluster
kubectl get nodes -o json | jq '.items[].status.allocatable | select(."nvidia.com/gpu" != null)'
# Output: { "nvidia.com/gpu": "8" } per un nodo con 8 GPU A100
Installazione NVIDIA GPU Operator (Approccio Consigliato)
Per cluster in produzione, il GPU Operator di NVIDIA gestisce automaticamente tutti i componenti necessari: driver, device plugin, container runtime, DCGM Exporter per monitoring:
# Installa GPU Operator
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update
helm install gpu-operator nvidia/gpu-operator \
--namespace gpu-operator \
--create-namespace \
--version v24.9.0 \
--set driver.enabled=true \
--set mig.strategy=single \
--set dcgmExporter.enabled=true \
--set dcgmExporter.serviceMonitor.enabled=true
# Verifica installazione
kubectl get pods -n gpu-operator
# Attendi che tutti i pod siano Running
kubectl wait --for=condition=ready pod -l app=nvidia-device-plugin-daemonset -n gpu-operator --timeout=300s
# Verifica GPU allocabili
kubectl describe node gpu-node-1 | grep -A 5 "Allocatable:"
# nvidia.com/gpu: 8
Schedulare Pod con GPU
Una volta che il Device Plugin e attivo, puoi richiedere GPU nei manifest come qualsiasi altra risorsa Kubernetes. La differenza: non si usa requests, solo limits per le GPU (Kubernetes garantisce sempre esattamente il numero di GPU richiesto).
# pod-gpu-basic.yaml
apiVersion: v1
kind: Pod
metadata:
name: gpu-test
spec:
restartPolicy: OnFailure
containers:
- name: inference
image: nvcr.io/nvidia/pytorch:24.01-py3
command: ["python3", "-c"]
args:
- |
import torch
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"GPU count: {torch.cuda.device_count()}")
print(f"GPU name: {torch.cuda.get_device_name(0)}")
x = torch.rand(1000, 1000).cuda()
print(f"Tensor on GPU: {x.device}")
resources:
limits:
nvidia.com/gpu: "1" # richiedi 1 GPU
memory: "16Gi"
cpu: "4"
requests:
memory: "16Gi"
cpu: "4"
volumeMounts:
- name: model-storage
mountPath: /models
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: model-pvc
nodeSelector:
accelerator: "nvidia-a100" # schedule solo su nodi A100
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
Training Distribuito con Kubeflow Training Operator
Il training di modelli grandi richiede spesso piu GPU su piu nodi. Il Kubeflow Training Operator gestisce i job di training distribuito con PyTorchJob, TFJob, MXJob e MPIJob. Installa prima l'operator:
# Installa Training Operator
kubectl apply -k "github.com/kubeflow/training-operator/manifests/overlays/standalone?ref=v1.8.0"
# Verifica
kubectl get pods -n kubeflow
kubectl get crd | grep kubeflow
PyTorchJob per Training Multi-GPU Multi-Node
# pytorch-distributed-training.yaml
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
name: llm-finetuning-job
namespace: ml-training
spec:
pytorchReplicaSpecs:
Master:
replicas: 1
restartPolicy: OnFailure
template:
spec:
containers:
- name: pytorch
image: company.registry.io/training:llm-v2.1
command:
- python3
- -m
- torch.distributed.run
- --nproc_per_node=8
- --nnodes=4
- --node_rank=$(RANK)
- --master_addr=$(MASTER_ADDR)
- --master_port=23456
- train_llm.py
- --model=llama-7b
- --dataset=/data/training_set
- --batch-size=32
- --epochs=3
- --output=/models/finetuned
env:
- name: NCCL_DEBUG
value: "INFO"
- name: NCCL_SOCKET_IFNAME
value: "eth0"
resources:
limits:
nvidia.com/gpu: "8"
memory: "120Gi"
cpu: "32"
requests:
memory: "120Gi"
cpu: "32"
volumeMounts:
- name: training-data
mountPath: /data
- name: model-output
mountPath: /models
- name: shm
mountPath: /dev/shm
volumes:
- name: training-data
persistentVolumeClaim:
claimName: training-dataset-pvc
- name: model-output
persistentVolumeClaim:
claimName: model-output-pvc
- name: shm
emptyDir:
medium: Memory
sizeLimit: "64Gi" # shared memory per NCCL
nodeSelector:
accelerator: "nvidia-a100-80gb"
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
Worker:
replicas: 3 # 3 worker + 1 master = 4 nodi, 32 GPU totali
restartPolicy: OnFailure
template:
spec: # stesso spec del Master...
containers:
- name: pytorch
image: company.registry.io/training:llm-v2.1
resources:
limits:
nvidia.com/gpu: "8"
memory: "120Gi"
cpu: "32"
Karpenter per Provisioning di Nodi GPU Spot
Le GPU sono la risorsa piu costosa in cloud. Le istanze GPU spot costano 60-70% in meno rispetto alle on-demand. Karpenter gestisce il provisioning automatico di nodi GPU spot, con fallback su on-demand in caso di interruzione:
# karpenter-gpu-nodepool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: gpu-spot
spec:
template:
metadata:
labels:
role: gpu-worker
accelerator: nvidia
spec:
nodeClassRef:
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
name: gpu-nodeclass
requirements:
# Tipologie di istanze GPU AWS
- key: node.kubernetes.io/instance-type
operator: In
values:
- p4d.24xlarge # 8x A100 80GB
- p3.8xlarge # 4x V100
- g5.12xlarge # 4x A10G
- g4dn.12xlarge # 4x T4
# Preferisci spot
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- on-demand # fallback
- key: kubernetes.io/os
operator: In
values:
- linux
taints:
- key: nvidia.com/gpu
value: "true"
effect: NoSchedule
limits:
nvidia.com/gpu: 256 # max 256 GPU totali nel cluster
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30m # rimuovi nodi GPU spot quando il job finisce
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: gpu-nodeclass
spec:
amiFamily: AL2
role: KarpenterNodeRole
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "my-cluster"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "my-cluster"
instanceStorePolicy: RAID0 # usa i dischi NVMe locali per storage temporaneo
userData: |
#!/bin/bash
# Installa NVIDIA drivers al primo boot
/etc/eks/bootstrap.sh my-cluster
nvidia-smi # verifica GPU disponibili
GPU Time-Slicing: Condividere una GPU tra Piu Pod
Per workload di inferenza leggeri o development, una GPU intera e spesso sprecata. Il GPU time-slicing permette di condividere una GPU fisica tra piu Pod, ognuno dei quali vede una "GPU virtuale" con una fetta del tempo di calcolo:
# gpu-time-slicing-config.yaml
# Configura il Device Plugin per time-slicing
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config
namespace: gpu-operator
data:
any: |-
version: v1
flags:
migStrategy: none
sharing:
timeSlicing:
renameByDefault: false
failRequestsGreaterThanOne: false
resources:
- name: nvidia.com/gpu
replicas: 4 # ogni GPU fisica diventa 4 "GPU" logiche
---
# Applica il config all'operator
kubectl patch clusterpolicy gpu-cluster-policy \
-n gpu-operator \
--type merge \
-p '{"spec": {"devicePlugin": {"config": {"name": "time-slicing-config"}}}}'
# Verifica: ogni nodo con 1 GPU A100 ora mostra 4 GPU allocabili
kubectl describe node gpu-node-1 | grep nvidia.com/gpu
# Allocatable:
# nvidia.com/gpu: 4
# Pod che usa 1/4 di GPU
apiVersion: v1
kind: Pod
metadata:
name: inference-small
spec:
containers:
- name: model-server
image: company.registry.io/inference:v1
resources:
limits:
nvidia.com/gpu: "1" # ottiene 1/4 della GPU fisica
MIG: Multi-Instance GPU per A100 e H100
Le GPU NVIDIA A100 e H100 supportano MIG (Multi-Instance GPU), che partiziona la GPU in istanze hardware isolate (non solo time-sharing). Ogni istanza MIG ha memoria e compute garantiti e non interferisce con le altre:
# Configura MIG sul nodo (eseguito sul nodo GPU, non da kubectl)
# Richiede: driver NVIDIA >= 525, GPU A100 o H100
# Abilita MIG mode sulla GPU
sudo nvidia-smi -mig 1
# Crea 7 istanze MIG da 1/7 di A100 (1g.10gb)
sudo nvidia-smi mig -cgip -p 0,9 # Profile 9 = MIG 1g.10gb
# Verifica istanze create
sudo nvidia-smi mig -lgi
# +-------------------------------------------------------+
# | GPU instances: |
# | GPU Name Profile Instance Placement |
# | ID ID Start:Size |
# |=======================================================|
# | 0 MIG 1g.10gb 9 1 0:1 |
# | 0 MIG 1g.10gb 9 2 1:1 |
# | 0 MIG 1g.10gb 9 3 2:1 |
# ... (7 istanze totali)
# Nel cluster Kubernetes, configurare MIG Strategy nel GPU Operator
kubectl patch clusterpolicy gpu-cluster-policy \
-n gpu-operator \
--type json \
-p '[{"op":"replace","path":"/spec/mig/strategy","value":"mixed"}]'
# Pod che richiede specifica istanza MIG
apiVersion: v1
kind: Pod
metadata:
name: inference-mig
spec:
containers:
- name: model
image: nvcr.io/nvidia/pytorch:24.01-py3
resources:
limits:
nvidia.com/mig-1g.10gb: "1" # richiedi 1 istanza MIG 1g.10gb
Monitoring GPU con DCGM Exporter
Il monitoring delle GPU e fondamentale per capire se i job di training sono efficienti e per il FinOps. DCGM Exporter espone metriche GPU a Prometheus:
# DCGM Exporter viene installato automaticamente con GPU Operator
# Verifica che le metriche siano disponibili
kubectl port-forward svc/gpu-operator-dcgm-exporter 9400:9400 -n gpu-operator &
curl -s localhost:9400/metrics | grep DCGM_FI
# Metriche chiave da monitorare:
# DCGM_FI_DEV_GPU_UTIL - utilizzo GPU (0-100%)
# DCGM_FI_DEV_MEM_COPY_UTIL - utilizzo memoria GPU
# DCGM_FI_DEV_FB_USED - memoria GPU usata (MB)
# DCGM_FI_DEV_POWER_USAGE - consumo energetico (W)
# DCGM_FI_DEV_SM_CLOCK - clock streaming multiprocessor
# DCGM_FI_DEV_GPU_TEMP - temperatura GPU
# Alert: GPU sottoutilizzata (< 50% per 30 minuti = spreco)
- alert: GPUUnderutilized
expr: DCGM_FI_DEV_GPU_UTIL < 50
for: 30m
labels:
severity: warning
annotations:
summary: "GPU {{ $labels.gpu }} sul nodo {{ $labels.Hostname }} utilization < 50%"
description: "Valuta se il job puo essere terminato o ottimizzato"
# Dashboard Grafana: importa ID 12239 (NVIDIA DCGM Exporter Dashboard)
Deployment di Modelli per Inferenza con TorchServe
Per l'inferenza in produzione, serve un model server che gestisca il load balancing tra piu replica del modello, il batching delle richieste, e il versioning. TorchServe e la soluzione ufficiale PyTorch:
# torchserve-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: model-inference
namespace: ml-inference
spec:
replicas: 3 # 3 replica per alta disponibilita
selector:
matchLabels:
app: model-inference
template:
metadata:
labels:
app: model-inference
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8082" # TorchServe metrics port
spec:
containers:
- name: torchserve
image: pytorch/torchserve:0.11.0-gpu
args:
- torchserve
- --start
- --model-store=/models
- --models=text-classifier=bert-classifier.mar
- --ts-config=/config/config.properties
ports:
- containerPort: 8080 # inference API
- containerPort: 8081 # management API
- containerPort: 8082 # metrics
resources:
limits:
nvidia.com/gpu: "1"
memory: "16Gi"
cpu: "4"
requests:
memory: "8Gi"
cpu: "2"
readinessProbe:
httpGet:
path: /ping
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
livenessProbe:
httpGet:
path: /ping
port: 8080
initialDelaySeconds: 120
periodSeconds: 30
volumeMounts:
- name: model-store
mountPath: /models
- name: ts-config
mountPath: /config
volumes:
- name: model-store
persistentVolumeClaim:
claimName: model-store-pvc
- name: ts-config
configMap:
name: torchserve-config
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: torchserve-config
namespace: ml-inference
data:
config.properties: |
inference_address=http://0.0.0.0:8080
management_address=http://0.0.0.0:8081
metrics_address=http://0.0.0.0:8082
number_of_gpu=1
batch_size=32
max_batch_delay=100 # ms: attendi fino a 100ms per fare batching
max_response_size=6553500
install_py_dep_per_model=true
---
# HPA basato su latenza con KEDA (event-driven autoscaling)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: inference-scaler
namespace: ml-inference
spec:
scaleTargetRef:
name: model-inference
minReplicaCount: 1
maxReplicaCount: 10
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
metricName: torchserve_queue_latency_microseconds
threshold: "100000" # 100ms di coda = scala up
query: avg(torchserve_queue_latency_microseconds{model_name="bert-classifier"})
Best Practices per Workload AI su Kubernetes
Ottimizzazione dei Costi GPU
- Spot per training, on-demand per inferenza: Il training puo gestire interruzioni con checkpoint; l'inferenza deve essere sempre disponibile
- Checkpoint frequenti: Salva checkpoint ogni 30 minuti per riprendere il training dopo un'interruzione spot
- Time-slicing per development: Usa GPU time-slicing per i developer, MIG o GPU intere per produzione
- Scale-to-zero: Karpenter rimuove i nodi GPU spot quando il training finisce - non paghi per GPU idle
- Batching nella inferenza: TorchServe con batch_size=32 aumenta throughput 10-20x rispetto a richieste singole
- Profile prima di deployare: Usa NVIDIA Nsight per profilare il job di training e identificare inefficienze
Errori Comuni con GPU su Kubernetes
- Container senza taint toleration: I nodi GPU hanno il taint
nvidia.com/gpu=true:NoSchedule; senza toleration il Pod non viene schedulato sul nodo GPU - Mancato isolamento della memoria: La GPU non isola la memoria tra container come fa la CPU. Se allochi 1 GPU ma il modello usa piu memoria di quanto disponibile, il job crasha con CUDA OOM
- NCCL senza shared memory: Il training distribuito PyTorch usa NCCL che richiede /dev/shm grande (tipicamente 10-60GB); configura sempre un emptyDir con medium: Memory
- Non monitorare GPU utilization: Una GPU a 20% di utilizzo e uno spreco enorme. Il DCGM dashboard dovrebbe essere il primo posto da guardare dopo ogni deploy
Conclusioni e Prossimi Passi
Kubernetes e diventato la piattaforma standard per i workload AI/ML non per caso: la sua astrazione delle risorse, il sistema di scheduling avanzato e l'ecosistema di operator (Kubeflow, Training Operator) lo rendono il contesto ideale per orchestrare sia il training che l'inferenza a scala. Con Karpenter che gestisce automaticamente il provisioning di nodi GPU spot, il costo di un job di training puo essere ridotto del 40-70% rispetto all'uso di istanze on-demand.
Il prossimo passo e integrare questi workload con una pipeline MLOps completa: registrazione dei modelli con MLflow, gestione dei dataset con DVC, CI/CD per il retraining automatico. L'articolo FinOps per Kubernetes (articolo 9 di questa serie) approfondisce come misurare e ottimizzare il costo totale dei workload GPU nel cluster.
Prossimi Articoli della Serie Kubernetes at Scale
Serie Correlate
- MLOps e Machine Learning in Produzione - pipeline CI/CD per ML, model registry
- Deep Learning e Reti Neurali - fondamenti teorici dei modelli che si allenano su GPU
- Autoscaling in Kubernetes - KEDA per scaling basato su metriche personalizzate

Commenti
Caricamento commenti...
Accedi per commentare