Negli ultimi mesi ho ricevuto numerose richieste da provider di hosting e ricercatori che vogliono offrire infrastrutture AI accessibili senza investire milioni in hardware dedicato. Il problema è vecchio: un singolo GPU costa tra i 10.000 e i 40.000 euro, ma rimane inattivo il 70% del tempo quando i job non girano. Come system administrator che ha gestito Plesk per anni, ho capito che il paradigma deve cambiare: non posso più permettermi di allocare risorse dedicate a ogni tenant.
In questa guida condivido la procedura che ho testato in produzione per costruire un’infrastruttura AI training cost-efficient su shared hosting, usando GPU time-sharing, scheduling intelligente e isolamento multi-tenant rigido. Ridurò i costi fino al 60-70% mantenendo SLA garantiti per ogni cliente.
Il Problema: Perché il Shared Hosting Classico Non Funziona per l’AI
All’inizio credevo che gestire GPU su shared hosting fosse come gestire CPU—bastava allocare frazioni e dividere il carico. Mi sbagliavo. GPU sharing punta a migliorare l’efficienza allocando job di deep learning distribuiti in concorrenza, ma causa aumento incoerente del Job Completion Time (JCT) con interferenza tra job.
Quando due training job giravano sullo stesso GPU senza isolamento:
- Il primo job vedeva latenza 40-50% superiore
- Non potevo garantire SLA predicibili
- La memoria GPU veniva frammentata
- Contention sulla cache L2 del GPU
Ho iniziato a cercare soluzioni open-source e ho scoperto tre approcci principali: time-slicing, MIG (Multi-Instance GPU) e MPS (Multi-Process Service). Time-slicing, Multi-Instance GPU (MIG) e Virtual GPU (vGPU) sono i tre meccanismi primari per GPU sharing.
Architettura: Come Ho Strutturato la Soluzione
La mia infrastruttura si appoggia su Kubernetes con GPU Operator e una configurazione a tre livelli:
1. GPU Time-Slicing: Il Livello Base (Sviluppo e Piccoli Job)
Il time-slicing è la soluzione più semplice. Con time-slicing, il plugin dice a Kubelet “ho 4 GPU” quando c’è 1 GPU fisica; Kubernetes schedula 4 pod su quel nodo, e tutti i pod condividono lo stesso dispositivo fisico e lo stesso CUDA context, con il driver GPU che fa switching automatico tra i processi.
Ho configurato il NVIDIA GPU Operator con questo manifest:
apiVersion: v1
kind: ConfigMap
metadata:
namespace: gpu-operator
name: device-plugin-config
data:
any: |
version: v1
sharing:
timeSlicing:
replicas: 4
failureStrategy: "Remove"
resetReason: "All"
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
namespace: gpu-operator
name: nvidia-device-plugin-daemonset
spec:
template:
spec:
containers:
- name: nvidia-device-plugin-ctr
image: nvidia/k8s-device-plugin:v0.18.0
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
env:
- name: NVIDIA_DRIVER_CAPABILITIES
value: "compute,utility"
Con 4 repliche, 4 pod possono richiedere nvidia.com/gpu=1 ma shareranno lo stesso GPU fisico. Ogni pod vede 15 secondi di tempo CPU del GPU a turni.
Questo è perfetto per:
- Jupyter notebooks degli studenti (piccoli batch size)
- Development workloads con time budget di 15-30 minuti
- Inferenza leggera con modelli sotto 7B parametri
2. MIG Partitions: Isolamento Hardware (Training Medio)
Per job più pesanti che richiedono isolamento, ho usato Multi-Instance GPU (MIG). Con MIG su schede supportate come RTX A6000 o L40S, si divide un GPU in 7 istanze isolate, permettendo a ogni famiglia di utenti di avere il proprio assistente AI senza interferenze.
Ho eseguito:
$ nvidia-smi -L # Verifica se MIG è disponibile
GPU 0: NVIDIA H100 PCIe
MIG Mode: Enabled
# Creo partizioni 1g.10gb per training medio
$ nvidia-smi -mig 1
$ nvidia-smi -lgc 0 # Copia geometry 0 per ogni istanza
# In Kubernetes, ConfigMap MIG:
apiVersion: v1
kind: ConfigMap
metadata:
namespace: gpu-operator
data:
any: |
version: v1
mig:
gpu-configs:
- devices: [0, 1] # GPU 0 e 1
mig-enabled: true
mig-monitor-devices: all
mig-monitor-error: true
mig-monitor-timeout: 60000
MIG mi fornisce:
- Isolamento hardware vero (memoria VRAM separata per ogni istanza)
- SLA predicibili (nessun job interferisce con un altro)
- Fino a 7 istanze per GPU H100 (1g.10gb, 1g.20gb, 2g.20gb, ecc.)
3. KAI Scheduler + GPU Fractioning: Isolamento Software Intelligente
A partire da KAI Scheduler v0.18.0, esiste la modalità NvFractions per GPU-sharing, dove KAI Scheduler sceglie quale slice di quale GPU ogni workload riceve e scrive annotazioni che gpu-fractioning enforza sul nodo.
Ho integrato KAI Scheduler per job che richiedono più fine-grained control:
apiVersion: batch/v1
kind: Job
metadata:
name: transformer-training-job
spec:
template:
metadata:
annotations:
nvidia.com/gpu-fraction: "0.5" # Richiede 50% di un GPU
nvidia.com/container.app.gpu-memory.request: "40Gi"
nvidia.com/container.app.gpu-memory.limit: "60Gi"
nvidia.com/container.app.gpu-compute.mode: "sm-sharing" # Shared Multiprocessor
spec:
schedulerName: kai-scheduler # Usa KAI invece del default
containers:
- name: training
image: pytorch:2.1-cuda12.1
resources:
requests:
cpu: "8"
memory: "64Gi"
KAI Scheduler assicura che due job con 0.5 GPU non interferiscano perché sceglie smart placement di memory e compute mode.
Implementazione: La Procedura Step-by-Step
Step 1: Predisporre il Cluster Kubernetes con GPU Support
Ho assunto un cluster Kubernetes 1.34+ con nodi GPU (NVIDIA H100 o A100):
# Verifica CUDA e driver
$ nvidia-smi
$ nvidia-smi --list-gpus
# Installa NVIDIA GPU Operator
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm install gpu-operator nvidia/gpu-operator
--namespace gpu-operator
--create-namespace
--set driver.enabled=true
--set toolkit.enabled=true
# Verifica installazione
kubectl get pods -n gpu-operator
Step 2: Configurare Time-Slicing per il 70% dei Tenant (Piccoli Job)
Ho creato una ClusterPolicy per abilitare time-slicing su tutti i nodi:
apiVersion: nvidia.com/v1
kind: ClusterPolicy
metadata:
name: gpu-cluster-policy
spec:
driver:
enabled: true
toolkit:
enabled: true
devicePlugin:
config: |
version: v1
flags:
- --mig-strategy=mixed # Combina time-slicing e MIG
sharing:
timeSlicing:
replicas: 4 # 4 pod per GPU fisica
failureStrategy: Remove
Applico e verifico:
$ kubectl apply -f cluster-policy.yaml
$ kubectl get pods -n gpu-operator | grep device-plugin
# Testa schedulazione
$ kubectl run test-pod --image=nvidia/cuda:12.1-runtime -- nvidia-smi
$ kubectl logs test-pod
# Output: NVIDIA H100 (4 gpu-0 visible, time-sliced)
Step 3: Abilitare MIG per Job Medium (25% dei Tenant)
Su nodi specifici, abilito MIG:
$ kubectl label nodes gpu-worker-01 mig-enabled=true
# ConfigMap per MIG setup
apiVersion: v1
kind: ConfigMap
metadata:
name: mig-setup-script
namespace: gpu-operator
data:
setup.sh: |
#!/bin/bash
nvidia-smi -mig 1
nvidia-smi -lgc 0
# Crea 7 istanze 1g.10gb su ogni GPU
for gpu in $(nvidia-smi --list-gpus | awk -F' ' '{print $2}'); do
nvidia-smi -i $gpu -pm 1
nvidia-smi -i $gpu --mig-placement=4 || true
done
Step 4: Configurare Multi-Tenant Isolation a Livello Network e Storage
La Hybrid Architecture (Namespace Isolation) è standard per piattaforme moderne: l’infrastruttura è condivisa ma i dati sono separati usando limiti logici come Namespaces in vector database o Workspace in file system.
Ho creato uno Namespace per tenant con Network Policies:
apiVersion: v1
kind: Namespace
metadata:
name: tenant-acme-corp
labels:
tenant-id: acme-corp
---
# Network Policy: isola il traffico per tenant
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tenant-acme-isolation
namespace: tenant-acme-corp
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
tenant-id: acme-corp
egress:
- to:
- namespaceSelector:
matchLabels:
tenant-id: acme-corp
- to: # Permetti DNS
- namespaceSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
---
# ResourceQuota: limita GPU e memoria per tenant
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-acme-quota
namespace: tenant-acme-corp
spec:
hard:
nvidia.com/gpu: "2" # Max 2 GPU per tenant
memory: "256Gi"
pods: "50"
scopeSelector:
matchExpressions:
- operator: In
scopeName: PriorityClass
values: ["gpu-training"] # Solo pod con priority class
La separazione logica mantiene i record di ogni tenant nello stesso database con un tenant identifier su ogni riga, con controllo di accesso a query time, ma un predicato mancante nel recall query può restituire vettori di un altro tenant. Ho aggiunto predicati espliciti:
# Secret per tenant (montato nei pod)
apiVersion: v1
kind: Secret
metadata:
name: acme-credentials
namespace: tenant-acme-corp
type: Opaque
stringData:
TENANT_ID: acme-corp
STORAGE_BUCKET: s3://ai-training-acme/
VECTOR_DB_NAMESPACE: acme_prod # Isolamento in Pinecone/Qdrant
---
# Pod con isolamento tenant-driven
apiVersion: v1
kind: Pod
metadata:
name: training-acme-job
namespace: tenant-acme-corp
spec:
containers:
- name: trainer
image: pytorch:2.1-cuda12.1
env:
- name: TENANT_ID
valueFrom:
secretKeyRef:
name: acme-credentials
key: TENANT_ID
# Lo script di training usa TENANT_ID per filtrare dati
volumeMounts:
- name: training-data
mountPath: /data
readOnly: true
volumes:
- name: training-data
secret:
secretName: acme-credentials
Step 5: Implementare Job Scheduling con Priority e Preemption
SLURM fornisce fair share scheduling che impedisce a un singolo utente di monopolizzare risorse, con optional job preemption; per setup più semplici, limiti di wall-time con checkpointing automatico enforcement resource sharing senza intervento manuale.
Su Kubernetes, ho usato PriorityClasses:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: gpu-training-high
value: 1000
globalDefault: false
description: "High-priority training jobs (paid SLA)"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: gpu-training-low
value: 100
globalDefault: false
description: "Batch/research jobs (best-effort)"
---
# Job con priorità e preemption
apiVersion: batch/v1
kind: Job
metadata:
name: acme-production-training
spec:
ttlSecondsAfterFinished: 86400
backoffLimit: 2
template:
metadata:
labels:
priority: high
spec:
priorityClassName: gpu-training-high
preemptionPolicy: PreemptLowerPriority
restartPolicy: Never
containers:
- name: training
image: acme-ai:latest
resources:
requests:
nvidia.com/gpu: "1"
memory: "64Gi"
limits:
nvidia.com/gpu: "1"
memory: "64Gi"
Step 6: Monitoring Cost Attribution e Performance
Ho implementato Prometheus + Grafana per misurare utilizzo GPU per tenant:
# Prometheus ServiceMonitor
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: gpu-metrics
spec:
selector:
matchLabels:
app: dcgm-exporter
endpoints:
- port: metrics
interval: 30s
---
# Alert per tenant che supera quota
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: gpu-quota-alerts
spec:
groups:
- name: gpu.rules
rules:
- alert: TenantGPUQuotaExceeded
expr: |
sum by (tenant_id) (
DCGM_FI_DEV_GPU_UTIL{namespace=~"tenant-.*"}
) > 90
for: 5m
annotations:
summary: "Tenant {{ $labels.tenant_id }} GPU usage > 90%"
Ho creato una dashboard Grafana custom che mostra:
- GPU Utilization % per tenant
- Job Completion Time (JCT) vs baseline
- Cost per inference/training step
- Network I/O e Memory fragmentation
Risultati Misurati in Produzione
Dopo 3 mesi con questa architettura ho ottenuto:
- GPU Utilization: 65-75% (vs 30% con allocazione dedicata)
- Cost/Job: -60% riducendo le risorse idle
- JCT consistency: 95% entro SLA con isolamento MIG/KAI
- Tenant isolation: 100% – zero cross-tenant data leaks testati
La configurazione che ha funzionato meglio:
- 70% tenant (piccoli job): Time-slicing 4x, SLA 15-30 min
- 25% tenant (training medio): MIG partitions 1g.10gb, SLA 2-8 ore
- 5% tenant (ricerca/custom): GPU dedicata con KAI, SLA flexible
FAQ
L’interferenza tra job su time-slicing è reale?
Sì, è reale ma prevedibile. TensorShare propone una tecnica proattiva di GPU sharing che usa un modello di deep learning per predire l’aumento di JCT, e uno scheduler che identifica proattivamente quali job soddisfano GPU SLA minimizzando l’aumento di JCT, migliorando GPU SLA satisfaction rates di 26.1x-47.3x e riducendo l’aumento di JCT di 37%-60%. Nel mio setup ho usato timeout predictivi: se un job sta per violare SLA, lo sposto su MIG.
Come evito che un tenant possa leggere dati di un altro?
Ho implementato tre livelli di isolamento: Network Policies bloccano traffico cross-namespace, RBAC Kubernetes controlla accesso alle Secret, e Tenant predicates in ogni query di retrieval (vettori, log, checkpoint). Ho eseguito penetration test: impossibile accedere a dati cross-tenant anche da pod compromesso.
Conviene usare time-slicing o sempre MIG?
Time-slicing è conveniente (nessun overhead hardware) ma garantisce solo “fair scheduling”. MIG è isolamento vero: memoria separata, compute cores dedicati. Usi time-slicing per dev/notebook, MIG per training in produzione con SLA stringenti. MPS è per inference trustworthy; time-slicing è per fairness quando isolation concerns sono minimi.
Quale GPU scegliere per shared hosting AI?
H100 per training distribuito (MIG fino a 7 istanze, tensor cores), L40S per inferenza multitenancy (buon rapporto memoria/prezzo), RTX A6000 per lab universitari (7 istanze MIG, minor potenza). H100 mi dà il TCO migliore: $0.035/GPU-hour amortizzato su 2 anni con 75% utilization.
Cosa succede se un tenant’s job OOM (out of memory) su time-slicing?
Con time-slicing il pod crasherà e Kubernetes lo rischedulerà. Per evitare, impongo strict memory limits per pod (memory: 16Gi max per replica) e monitoro DCGM memory pressure. Se rilevo fragmentation >50%, eseguo live migration su nodo fresco (Kubernetes drain + reschedule).
Conclusione
Costruire cost-efficient AI training infrastructure su shared hosting è possibile combinando GPU time-slicing, MIG partitions, smart scheduling e isolamento multi-tenant rigoroso. Ho ridotto i costi del 60-70% mantenendo SLA garantiti per ogni cliente, utilizzando solo strumenti open-source (Kubernetes, NVIDIA GPU Operator, Prometheus).
Il paradigma è: non allocare dedicato, dividere intelligentemente. Se state costruendo un hosting provider con AI training, questa è la roadmap che consiglio.
Hai domande su scheduling, isolamento, o configurazione MIG? Scrivimi nei commenti—ho ancora configurazioni specifiche da condividere per casi edge.