Quando la VRAM della GPU finisce: cosa fare
La VRAM della GPU è il collo di bottiglia per carichi AI e grafici. Come funzionano davvero memoria unificata, offloading in VRAM e swap su NVMe.

Il messaggio d'errore più comune nel machine learning non è un traceback Python né un mismatch di dimensioni. È CUDA out of memory. Il modello è troppo grande, il batch size è troppo alto, oppure le attivazioni intermedie non entrano nella VRAM della GPU. Una RTX 4090 ha 24 GB. Un modello da 70 miliardi di parametri in mezza precisione richiede circa 140 GB. I conti non tornano, e buttare soldi su GPU più grandi serve solo a rimandare il problema: una H100 con 80 GB non riesce comunque a contenere i modelli più grandi su una singola scheda.
Ma se si potesse estendere in modo trasparente la memoria della GPU usando la RAM di sistema o persino lo storage NVMe? Strumenti come Greenboost di NVIDIA e progetti simili fanno esattamente questo: sfruttano la gerarchia di memoria (VRAM → RAM di sistema → SSD) per eseguire carichi di lavoro che sulla tua macchina non dovrebbero entrare. I compromessi sulle prestazioni ci sono, ma sono sorprendentemente gestibili per molti casi d'uso. Per capire come funziona serve capire la gerarchia di memoria della GPU stessa.
Perché la memoria della GPU è diversa da quella della CPU
Memoria CPU e GPU servono pattern di accesso fondamentalmente diversi. I carichi di lavoro della CPU sono sensibili alla latenza: un singolo thread ha bisogno di un dato e si blocca finché non arriva. Le cache della CPU sono progettate per minimizzare la latenza con accessi casuali.
I carichi di lavoro della GPU sono sensibili al throughput: migliaia di thread hanno ciascuno bisogno dei propri dati, e la GPU può passare da un thread all'altro per nascondere la latenza. La memoria della GPU (HBM sulle schede data center, GDDR su quelle consumer) è progettata per la banda: consegnare enormi quantità di dati al secondo, anche se il singolo accesso richiede più tempo di un cache hit della CPU.
Memory bandwidth comparison (approximate):
RTX 4090 GDDR6X: 1,000 GB/s
H100 HBM3: 3,350 GB/s
DDR5 System RAM: 50 GB/s
PCIe 5.0 x16: 64 GB/s (theoretical max)
NVMe SSD: 7 GB/s
The bandwidth cliff between VRAM and system RAM is ~20x.
Between VRAM and NVMe it's ~140x.
This is why naive offloading to system RAM kills performance —
you're trying to feed a 1,000 GB/s appetite through a 50 GB/s straw.
Questo divario di banda è il motivo per cui non funziona semplicemente rendere la memoria della GPU «virtuale», paginando i dati nella RAM di sistema come fa la CPU con il disco. La CPU tollera i page fault con un rallentamento forse di 10x. Una GPU che legge dalla RAM di sistema invece che dalla VRAM subisce una riduzione di banda di 20x, che per i carichi limitati dalla banda (cioè la maggior parte dell'inferenza ML) significa un rallentamento di 20x.
Come funziona davvero l'offloading sulla VRAM
Il trucco non è trattare la RAM di sistema come una VRAM lenta. Si tratta di precaricare i dati dalla RAM di sistema nella VRAM prima che la GPU ne abbia bisogno, nascondendo la latenza dietro il calcolo. È l'intuizione chiave alla base di ogni tecnica pratica di estensione della VRAM.
Durante l'inferenza di una rete neurale, il calcolo procede in sequenza attraverso i layer. Mentre la GPU elabora il layer 5, sa che il prossimo è il layer 6. Un sistema di offloading intelligente può iniziare a trasferire i pesi del layer 6 dalla RAM di sistema alla VRAM mentre il layer 5 è in calcolo. Se il calcolo richiede più tempo del trasferimento (cosa frequente con i layer grandi), il trasferimento è completamente nascosto e la GPU non si ferma mai.
# Conceptual overlap of compute and transfer
# (simplified pseudocode)
def inference_with_offloading(model, input_data):
# Only 2 layers fit in VRAM at a time
# Rest are in system RAM
for i, layer in enumerate(model.layers):
# Start async transfer of NEXT layer while computing current
if i + 1 < len(model.layers):
async_transfer_to_gpu(model.layers[i + 1])
# Compute on current layer (GPU is busy, transfer happens in parallel)
output = layer.forward(input_data)
# Evict current layer from VRAM (it's done)
transfer_to_ram(layer)
# Wait for next layer transfer to complete (usually already done)
sync_transfer()
input_data = output
return output
Questo approccio a pipeline funziona bene per l'inferenza perché il grafo di calcolo è prevedibile: si sa esattamente quali pesi serviranno dopo. L'addestramento è più difficile, perché i backward pass richiedono le attivazioni del forward pass, creando pattern di movimento dei dati più complessi.
Gli approcci nella pratica
CUDA Unified Memory
La CUDA Unified Memory di NVIDIA crea un unico spazio di indirizzi che copre sia la VRAM della GPU sia la RAM di sistema. Il runtime CUDA migra automaticamente le pagine tra le due in base ai pattern di accesso. Quando la GPU accede a una pagina nella RAM di sistema, scatta un page fault e la pagina viene migrata nella VRAM.
Il vantaggio: è trasparente per l'applicazione. Il tuo codice CUDA non deve gestire il posizionamento dei dati. Lo svantaggio: i page fault sono costosi, e le euristiche di migrazione del runtime non sempre coincidono con il pattern di accesso dell'applicazione. Per carichi prevedibili come l'inferenza di reti neurali, la gestione esplicita batte la migrazione automatica.
Offloading layer per layer
Strumenti come Hugging Face Accelerate, DeepSpeed ZeRO-Inference e il flag --mmap di llama.cpp implementano un offloading esplicito dei layer. Tengono in VRAM solo i layer attivi e trasmettono il resto dalla RAM di sistema o dal disco. Il modello non deve entrare interamente nella VRAM: basta che ne entrino uno o due layer alla volta.
È così che l'esecuzione di modelli da 70B su hardware consumer funziona davvero. Un modello da 70B quantizzato a 4 bit richiede circa 40 GB in totale, ma un singolo layer ne richiede solo 1-2 GB. Con 24 GB di VRAM e il prefetching, puoi far girare il modello con un impatto modesto sulle prestazioni, forse un 30-50% più lento rispetto a un caricamento interamente in VRAM.
NVMe come memoria estesa
L'approccio più aggressivo usa gli SSD NVMe come terzo livello di memoria della GPU. La banda è pessima rispetto alla VRAM (~7 GB/s contro ~1.000 GB/s), ma la capacità è sostanzialmente illimitata. Un NVMe da 4 TB costa 200 dollari e può ospitare decine di modelli grandi contemporaneamente.
Progetti come Greenboost di NVIDIA implementano questo approccio in modo trasparente: il sistema di memoria della GPU si estende su NVMe, con un prefetching intelligente per ridurre al minimo l'impatto del limite di banda. Per i carichi di inferenza in cui la GPU passa parecchio tempo a calcolare (non solo a spostare dati), la latenza di NVMe può essere completamente nascosta dalla sovrapposizione con il calcolo.
Le prestazioni dipendono molto dal carico di lavoro. Le operazioni vincolate dal calcolo (grandi moltiplicazioni di matrici) nascondono bene la latenza di trasferimento. Quelle vincolate dalla memoria (meccanismi di attention con contesto ampio) no. In pratica, l'offloading su NVMe funziona meglio per l'inferenza batch di modelli grandi con batch piccoli: esattamente il caso d'uso dell'inferenza locale di LLM.
Il vantaggio della memoria unificata di Apple
L'architettura di memoria unificata di Apple Silicon segue un approccio completamente diverso: eliminare la distinzione tra VRAM e RAM. CPU e GPU condividono lo stesso pool di memoria fisica. Non c'è nessun «offloading» perché non c'è separazione: la GPU accede alla stessa memoria che usa la CPU.
Questo non elimina il problema della banda: la banda di memoria della serie M (~400 GB/s per l'M4 Max) è inferiore a quella di una GPU dedicata, ma elimina il collo di bottiglia del PCIe che rende lento l'offloading su GPU discrete. Un Mac con 128 GB di memoria unificata può contenere un modello da 70B interamente in memoria accessibile alla GPU, senza alcun overhead di offloading.
Il compromesso: un throughput di picco più basso per i carichi che entrano interamente nella VRAM di una GPU dedicata, ma prestazioni nettamente migliori per quelli che non ci entrano. Per l'inferenza di modelli grandi, quando il modello supera la VRAM della GPU discreta, Apple Silicon è spesso più veloce di una GPU discreta con offloading, nonostante abbia meno potenza di calcolo grezza.
Cosa significa per gli sviluppatori
Se stai costruendo applicazioni che usano GPU (inferenza ML, grafica, calcolo scientifico), il vincolo della VRAM influisce sulle decisioni di architettura in modi concreti.
- Conosci la dimensione del tuo working set. Profila l'uso della memoria della GPU. Non l'allocazione di picco, ma il working set in ogni momento. Se il picco è 48 GB ma nessuna singola operazione richiede più di 8 GB di dati attivi, l'offloading funzionerà bene. Se una singola operazione richiede davvero 48 GB contemporaneamente, ti serve una GPU più grande.
- Scegli la strategia di offloading in base al pattern di accesso. L'accesso sequenziale (inferenza layer per layer) funziona benissimo con il prefetching. L'accesso casuale (attention su contesti ampi) no. Sappi quale pattern segue il tuo carico di lavoro.
- La quantizzazione di solito costa meno dell'offloading. Passare il modello da FP16 a INT4 riduce la memoria di 4x con un impatto modesto sulla qualità. L'offloading sulla RAM di sistema aggiunge latenza senza alcun impatto sulla qualità, ma con risparmi di memoria limitati. Prima quantizza, poi valuta l'offloading.
- Il batch size è la tua manopola di regolazione. Batch più grandi richiedono più memoria ma ammortizzano meglio l'overhead. Batch più piccoli richiedono meno memoria ma elaborano meno elementi al secondo. Quando sei vicino al limite della VRAM, ridurre il batch size è la soluzione più semplice.
- Monitora la frammentazione della memoria. L'allocazione di memoria CUDA può frammentare la VRAM nel tempo, soprattutto con input di lunghezza variabile. Potresti avere 8 GB liberi in totale ma nessun blocco contiguo da 2 GB.
torch.cuda.memory_stats()di PyTorch mostra la frammentazione.torch.cuda.empty_cache()può aiutare, anche se non è la panacea.
La memoria della GPU sarà sempre il collo di bottiglia per i carichi di calcolo su larga scala. I modelli crescono più in fretta della VRAM. Ma gli strumenti per gestire questo collo di bottiglia (memoria unificata, offloading intelligente, caching multi-livello) stanno diventando abbastanza validi da far sì che «non entra nella VRAM» non sia più una barriera insormontabile. È un compromesso sulle prestazioni, e sempre più spesso, un compromesso gestibile.


