\n\n\n\n Correzione degli errori di memoria insufficiente CUDA in PyTorch: Una guida completa - AiDebug \n

Correzione degli errori di memoria insufficiente CUDA in PyTorch: Una guida completa

📖 12 min read2,275 wordsUpdated Apr 4, 2026

Autore: Riley Debug – specialista in debug AI e ingegnere operazioni ML

L’odiosa errore “CUDA out of memory” è un ostacolo comune per chiunque lavori con modelli di deep learning in PyTorch. Hai progettato con cura il tuo modello, preparato i tuoi dati e iniziato ad addestrare, solo per essere accolto da questo messaggio frustrante. È un chiaro segnale che la tua GPU non ha abbastanza memoria per contenere tutti i tensori e i calcoli necessari per l’operazione attuale. Questo non è solo un fastidio; blocca i tuoi progressi, fa perdere tempo prezioso e può rappresentare un notevole collo di bottiglia nello sviluppo di soluzioni AI potenti.

Questa guida è progettata per fornire una comprensione approfondita del perché si verificano questi errori in PyTorch e, più importante, darti un kit di strumenti pratici di strategie per superarli. Esploreremo varie tecniche, da semplici aggiustamenti a considerazioni architettoniche più avanzate, assicurandoti di poter gestire efficacemente le risorse della tua GPU e mantenere fluide le tue pipeline di addestramento. Esploriamo come diagnosticare, prevenire e risolvere gli errori di memoria CUDA esaurita in PyTorch, permettendoti di costruire e addestrare modelli più grandi e complessi.

Comprendere l’uso della memoria GPU in PyTorch

Prima di poter risolvere gli errori “CUDA out of memory”, è fondamentale comprendere cosa consuma memoria GPU durante un’esecuzione di addestramento in PyTorch. Diversi componenti contribuiscono all’impronta totale di memoria e identificare i principali colpevoli è il primo passo verso un’ottimizzazione efficace.

Tensori e Parametri del Modello

Ogni tensore nel tuo modello, inclusi i dati di input, le attivazioni intermedie e i parametri apprendibili del modello (pesi e bias), risiede sulla GPU se li hai spostati lì. La dimensione di questi tensori è direttamente correlata all’uso della memoria. Modelli più grandi con più strati e parametri richiedono naturalmente più memoria. Allo stesso modo, immagini di input a risoluzione più alta o lunghezze di sequenza più lunghe porteranno a tensori di input più grandi.

Attivazioni Intermedie (Passaggio Avanti)

Durante il passaggio avanti, PyTorch deve memorizzare le attivazioni da ciascun strato. Questi valori intermedi sono essenziali per calcolare i gradienti durante il passaggio all’indietro (backpropagation). Per reti profonde, l’accumulo di queste attivazioni può essere sostanziale. Ad esempio, un ResNet con molti blocchi genererà numerose mappe di attivazione che devono essere mantenute in memoria.

Gradienti (Passaggio all’Indietro)

Quando inizia il passaggio all’indietro, i gradienti vengono calcolati per ciascun parametro. Anche questi gradienti occupano memoria GPU. Il motore di differenziazione automatica di PyTorch (Autograd) gestisce questo processo, ma la memoria allocata per i gradienti può essere significativa, specialmente per modelli con un gran numero di parametri.

Stati dell’Ottimizzatore

Ottimizzatori come Adam, RMSprop o Adagrad mantengono stati interni per ciascun parametro (ad esempio, buffer di momentum, stime della varianza). Questi stati sono spesso tanto grandi quanto i parametri stessi, raddoppiando o triplicando effettivamente la memoria necessaria solo per i parametri.

Dimensione del Batch

Forse il fattore più semplice è la dimensione del batch. Una dimensione del batch più grande significa che più campioni di input e le loro corrispondenti attivazioni intermedie vengono elaborati simultaneamente. Sebbene batch più grandi possano talvolta portare a stime di gradienti più stabili e a una convergenza più rapida dell’addestramento, sono un fattore principale di consumo di memoria GPU.

Overhead Interno di PyTorch

Oltre ai dati specifici del tuo modello, PyTorch stesso ha un certo overhead interno per gestire i contesti CUDA, gli allocatori di memoria e altri componenti operativi. Sebbene generalmente sia più piccolo rispetto alla memoria dei tensori, fa parte dell’uso totale.

Diagnostica Iniziale e Soluzioni Rapide

Quando si verifica l’errore “CUDA out of memory”, non farti prendere dal panico. Inizia con questi passaggi immediati per diagnosticare e potenzialmente risolvere rapidamente il problema.

Ripulire la Cache CUDA di PyTorch

A volte, l’allocatore di memoria di PyTorch potrebbe mantenere in memoria cache anche dopo che i tensori non sono più in uso, portando a frammentazione o a una visualizzazione imprecisa della memoria disponibile. Pulire esplicitamente la cache può liberare spazio.


import torch

torch.cuda.empty_cache()
 

È buona pratica chiamarlo periodicamente, specialmente dopo aver eliminato grandi tensori o prima di allocarne di nuovi. Nota che questo ripulisce solo la cache interna di PyTorch, non la memoria attivamente utilizzata dai tensori.

Ridurre la Dimensione del Batch

Questo è spesso il primo passo più efficace e semplice. Una dimensione del batch più piccola riduce direttamente il numero di campioni elaborati contemporaneamente, diminuendo così la memoria necessaria per input, attivazioni intermedie e gradienti.


# Dimensione del batch originale
batch_size = 64
# Se OOM, prova
batch_size = 32
# O addirittura
batch_size = 16
 

Riduci iterativamente la dimensione del batch fino a quando l’errore non scompare. Fai attenzione che una dimensione del batch molto piccola potrebbe influenzare la stabilità dell’addestramento o la velocità di convergenza, quindi è un compromesso.

Elimina Tensori e Variabili Non Necessari

Assicurati di non conservare tensori o variabili grandi che non sono più necessari. Il garbage collector di Python li libererà eventualmente, ma eliminarli esplicitamente può rilasciare memoria prima. Ricorda di spostarli sulla CPU o di scollegarli se fanno parte del grafo di computazione e desideri mantenere i loro dati ma non la loro cronologia dei gradienti.


# Esempio: se hai un grande tensore 'temp_data' che non è più necessario
del temp_data
# Inoltre, chiama esplicitamente il garbage collector
import gc
gc.collect()
torch.cuda.empty_cache() # Richiama di nuovo dopo l'eliminazione
 

Monitora l’Uso della Memoria GPU

Strumenti come nvidia-smi (nel tuo terminale) o le funzioni di reporting della memoria integrate in PyTorch possono darti informazioni sul consumo di memoria della tua GPU. Questo aiuta a identificare se un altro processo sta consumando memoria o se il tuo script PyTorch è l’unico colpevole.


nvidia-smi
 

All’interno di PyTorch, puoi ottenere statistiche dettagliate sulla memoria:


print(torch.cuda.memory_summary(device=None, abbreviated=False))
 

Questo fornisce una suddivisione della memoria allocata rispetto a quella riservata, e può a volte suggerire la frammentazione.

Tecniche Avanzate di Ottimizzazione della Memoria

Quando le soluzioni rapide non sono sufficienti, o hai bisogno di addestrare modelli davvero grandi, sono richieste tecniche più sofisticate. Questi metodi comportano spesso compromessi tra memoria, tempo di calcolo e complessità del codice.

Accumulazione dei Gradienti

L’accumulazione dei gradienti consente di simulare una dimensione del batch efficace più grande senza aumentare l’impronta di memoria di un singolo passaggio avanti/indietro. Invece di aggiornare i pesi dopo ogni batch, accumuli i gradienti su diversi piccoli batch e poi esegui un singolo aggiornamento.


model = MyModel().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
accumulation_steps = 4 # Accumula i gradienti su 4 mini-batch

for epoch in range(num_epochs):
 for i, (inputs, labels) in enumerate(dataloader):
 inputs, labels = inputs.to(device), labels.to(device)

 outputs = model(inputs)
 loss = criterion(outputs, labels)
 loss = loss / accumulation_steps # Normalizza la perdita per l'accumulo

 loss.backward() # Accumula i gradienti

 if (i + 1) % accumulation_steps == 0:
 optimizer.step() # Esegui il passo di ottimizzazione
 optimizer.zero_grad() # Pulisci i gradienti

 # Assicurati che i gradienti accumulati restanti vengano applicati alla fine dell'epoca
 if (i + 1) % accumulation_steps != 0:
 optimizer.step()
 optimizer.zero_grad()
 

Questa tecnica è potente per l’addestramento con grandi dimensioni di batch efficaci su GPU con memoria limitata.

Checkpointing dei Gradienti (Checkpointing delle Attivazioni)

Come discusso, le attivazioni intermedie occupano una significativa memoria. Il checkpointing dei gradienti affronta questo problema non memorizzando tutte le attivazioni intermedie durante il passaggio avanti. Invece, vengono ricalcolate durante il passaggio all’indietro per i segmenti che richiedono gradienti. Questo riduce drasticamente la memoria ma aumenta il tempo di calcolo, poiché parti del passaggio avanti vengono eseguite due volte.


import torch.utils.checkpoint as checkpoint

class CheckpointBlock(torch.nn.Module):
 def __init__(self, layer):
 super().__init__()
 self.layer = layer

 def forward(self, x):
 # Usa il checkpointing per il passaggio avanti di questo strato
 return checkpoint.checkpoint(self.layer, x)

# Esempio di utilizzo: avvolgi un grande blocco sequenziale
model = MyLargeModel()
# Sostituisci una grande parte sequenziale con una versione checkpointed
# Ad esempio, se il tuo modello ha `self.encoder = nn.Sequential(...)`
# Potresti avvolgere l'encoder:
# self.encoder = CheckpointBlock(nn.Sequential(*encoder_layers))
 

Questo è particolarmente utile per reti molto profonde dove memorizzare tutte le attivazioni è impossibile.

Addestramento a Precisione Mista (FP16/BF16)

L’addestramento a precisione mista comporta l’esecuzione di alcune operazioni a bassa precisione (FP16 o BF16) mantenendo altre in FP32. Questo può dimezzare l’impronta di memoria per pesi, attivazioni e gradienti, e spesso accelera l’addestramento su GPU moderne (come architetture NVIDIA Volta, Turing, Ampere, Ada Lovelace) progettate per i calcoli FP16.

Il modulo torch.cuda.amp di PyTorch rende questo facile da implementare:


from torch.cuda.amp import autocast, GradScaler

model = MyModel().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
scaler = GradScaler() # Per la stabilità FP16

for epoch in range(num_epochs):
 for inputs, labels in dataloader:
 inputs, labels = inputs.to(device), labels.to(device)

 optimizer.zero_grad()

 with autocast(): # Le operazioni all'interno di questo contesto verranno eseguite in FP16 dove possibile
 outputs = model(inputs)
 loss = criterion(outputs, labels)

 scaler.scale(loss).backward() # Scala la perdita per prevenire l'underflow nei gradienti FP16
 scaler.step(optimizer) # Annulla la scala dei gradienti e aggiorna i pesi
 scaler.update() # Aggiorna lo scaler per la prossima iterazione
 

La precisione mista è una tecnica potente che spesso fornisce sia risparmi di memoria che miglioramenti delle prestazioni.

Scaricamento sulla CPU (CPU Offloading)

Per modelli estremamente grandi o tensori intermedi, potresti considerare di spostare parti del tuo modello o specifici tensori sulla CPU quando non vengono utilizzati attivamente e poi riportarli sulla GPU quando necessario. Questo è più complesso da gestire e introduce un sovraccarico significativo a causa del trasferimento dei dati tra CPU e GPU, ma può essere un’ultima risorsa per modelli che altrimenti non ci starebbero.


# Esempio: Sposta un grande tensore sulla CPU dopo il suo utilizzo
large_tensor_on_gpu = torch.randn(10000, 10000).to(device)
# ... calcoli usando large_tensor_on_gpu ...

# Quando non è più necessario sulla GPU
large_tensor_on_gpu = large_tensor_on_gpu.cpu()
# O semplicemente elimina se non necessario affatto
del large_tensor_on_gpu
torch.cuda.empty_cache()
 

Per i layer del modello, questo implica spesso di suddividere il modello e spostare blocchi sequenziali tra i dispositivi.

Considerazioni sull’Architettura e sul Design del Codice

Oltre a tecniche specifiche di ottimizzazione della memoria, come progetti il tuo modello e scrivi il tuo codice PyTorch possono avere un impatto significativo sull’uso della memoria GPU.

Architetture di Modello Efficiente

Alcune architetture di modelli sono inherentemente più affamate di memoria rispetto ad altre. Ad esempio, i modelli con layer molto ampi o quelli che generano molte mappe di caratteristiche ad alta risoluzione (ad es., nei compiti di segmentazione) consumeranno più memoria. Considera di utilizzare alternative più efficienti in termini di memoria se possibile:

  • Convoluzioni Separabili in Profondità: Spesso usate in architetture mobili (ad es., MobileNet), queste possono ridurre significativamente parametri e calcoli rispetto alle convoluzioni standard.
  • Condivisione dei Parametri: Riutilizzare i pesi in diverse parti della rete può risparmiare memoria.
  • Potatura e Quantizzazione: Anche se tipicamente applicate dopo l’allenamento, queste possono essere considerate per il deployment e potrebbero influenzare le scelte design per ambienti con vincoli di memoria.

Operazioni In-place

Le operazioni PyTorch spesso creano nuovi tensori per il loro output. Le operazioni in-place (indicate da un trattino basso finale, ad es., x.add_(y) invece di x = x + y) modificano direttamente il tensore senza allocare nuova memoria per il risultato. Anche se possono risparmiare memoria, usale con cautela poiché possono rompere il grafo di calcolo se non gestite correttamente, specialmente quando utilizzate su tensori che richiedono gradienti.


# Risparmio di memoria (in-place)
x.relu_() # Modifica x direttamente

# Crea un nuovo tensore
x = torch.relu(x)
 

Evitare Clone/Copie di Tensori Non Necessari

Fai attenzione alle operazioni che creano implicitamente copie di tensori. Ad esempio, il ritaglio di un tensore potrebbe a volte creare una vista, ma altre operazioni potrebbero creare una copia completa. Usa esplicitamente .clone() solo quando è veramente necessaria una copia profonda, altrimenti, lavora con le viste dove possibile.


# Crea una vista (nessuna nuova memoria per i dati)
view_tensor = original_tensor[0:10]

# Crea un nuovo tensore (nuova memoria)
cloned_tensor = original_tensor.clone()
 

Usare torch.no_grad() per Inferenza/Evaluazione

Durante la valutazione o l’inferenza, non è necessario calcolare o memorizzare i gradienti. Racchiudere il tuo codice di inferenza nel gestore di contesto torch.no_grad() impedisce a Autograd di costruire il grafo di calcolo, il che risparmia una quantità significativa di memoria non memorizzando attivazioni intermedie per il backpropagation.


model.eval() # Imposta il modello in modalità di valutazione
with torch.no_grad():
 for inputs, labels in val_dataloader:
 inputs, labels = inputs.to(device), labels.to(device)
 outputs = model(inputs)
 # ... calcola le metriche ...
model.train() # Ripristina il modello in modalità di allenamento
 

Questa è una pratica fondamentale per chi lavora con PyTorch e può spesso prevenire errori OOM durante i passaggi di validazione.

Profilatura dell’Uso della Memoria

Per casi complessi, PyTorch fornisce potenti strumenti di profilatura che possono individuare esattamente quali operazioni consumano più memoria. Il modulo torch.profiler (o il più vecchio torch.autograd.profiler) può registrare le allocazioni di memoria CUDA.


import torch
from torch.profiler import profile, record_function, ProfilerActivity

# Esempio di profilatura di un singolo passaggio forward/backward
model = MyModel().to(device)
inputs = torch.randn(4, 3, 224, 224).to(device)
labels = torch.randint(0, 10, (4,)).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = torch.nn.CrossEntropyLoss()

with profile(activities=[
 ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapes=True) as prof:
 with record_function("model_inference"):
 outputs = model(inputs)
 with record_function("loss_computation"):
 loss = criterion(outputs, labels)
 with record_function("backward_pass"):
 loss.backward()
 with record_function("optimizer_step"):
 optimizer.step()
 optimizer.zero_grad()

print(prof.key_averages().table(sort_by="cuda_memory_usage", row_limit=10))
 

Il risultato del profiler mostrerà la memoria dettagliata

Articoli Correlati

🕒 Published:

✍️
Written by Jake Chen

AI technology writer and researcher.

Learn more →
Browse Topics: ci-cd | debugging | error-handling | qa | testing
Scroll to Top