\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,300 wordsUpdated Apr 4, 2026

Autore: Riley Debug – specialista nel debuggare IA e ingegnere ML ops

Il frustrante “CUDA out of memory” è un ostacolo comune per chiunque lavori con modelli di apprendimento profondo in PyTorch. Avete progettato meticulosamente il vostro modello, preparato i vostri dati e iniziato l’allenamento, solo per trovarvi di fronte a questo messaggio frustrante. È un chiaro segnale che la vostra GPU non ha abbastanza memoria per contenere tutti i tensori e i calcoli necessari per l’operazione attuale. Non è semplicemente un fastidio; rallenta i vostri progressi, fa perdere un tempo prezioso e può costituire un importante collo di bottiglia nello sviluppo di soluzioni IA potenti.

Questa guida è progettata per fornirvi una comprensione approfondita delle ragioni per cui si verificano questi errori in PyTorch e, ancora più importante, offrirvi un insieme pratico di strategie per superarli. Esploreremo varie tecniche, dai semplici aggiustamenti a considerazioni architettoniche più avanzate, in modo da assicurarvi di poter gestire efficacemente le vostre risorse GPU e mantenere i vostri pipeline di allenamento in buone condizioni. Esploriamo come diagnosticare, prevenire e correggere gli errori di CUDA out of memory in PyTorch, permettendovi di costruire e allenare modelli più grandi e complessi.

Comprendere l’uso della memoria GPU in PyTorch

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

Tensori e parametri del modello

Ogni tensore nel vostro modello, comprese le dati di input, le attivazioni intermedie e i parametri apprendibili del modello (pesi e bias), si trovano sulla GPU se li avete trasferiti lì. La dimensione di questi tensori è direttamente legata all’uso della memoria. I 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 comporteranno tensori di input più grandi.

Attivazioni intermedie (passaggio avanti)

Durante il passaggio avanti, PyTorch deve memorizzare le attivazioni di ogni strato. Questi valori intermedi sono essenziali per il calcolo dei gradienti durante il passaggio all’indietro (retropropagazione). Per le reti profonde, l’accumulo di queste attivazioni può essere sostanziale. Ad esempio, un ResNet con molti blocchi genererà molte mappe delle caratteristiche che devono essere mantenute in memoria.

Gradienti (passaggio all’indietro)

Quando inizia il passaggio all’indietro, i gradienti vengono calcolati per ogni parametro. Questi gradienti occupano anch’essi memoria GPU. Il motore di differenziazione automatica di PyTorch (Autograd) gestisce questo processo, ma la memoria allocata per i gradienti può essere significativa, in particolare per i modelli con un numero elevato di parametri.

Stato degli ottimizzatori

Ottimizzatori come Adam, RMSprop o Adagrad mantengono stati interni per ogni parametro (cioè, buffer dei momenti, stime di varianza). Questi stati sono spesso grandi quanto i parametri stessi, raddoppiando o triplicando in realtà la memoria richiesta solo per i parametri.

Dimensione del lotto

Forse il fattore più semplice da considerare è la dimensione del lotto. Una dimensione del lotto maggiore significa che più campioni di input e le loro attivazioni intermedie corrispondenti vengono elaborati simultaneamente. Sebbene lotti più grandi possano talvolta portare a stime di gradienti più stabili e a una convergenza di allenamento più rapida, rappresentano un fattore principale di consumo di memoria GPU.

Overhead interni di PyTorch

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

Diagnostica iniziale e correzioni rapide

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

SVuotare la cache CUDA di PyTorch

A volte, l’allocatore di memoria di PyTorch può mantenere in memoria cache anche dopo che i tensori non sono più utilizzati, causando frammentazione o una visione imprecisa della memoria disponibile. Svuotare esplicitamente la cache può liberare spazio.


import torch

torch.cuda.empty_cache()
 

È consigliabile chiamarlo periodicamente, soprattutto dopo aver rimosso grandi tensori o prima di allocarne di nuovi. Si noti che questo svuota solo la cache interna di PyTorch, non la memoria attivamente utilizzata dai tensori.

Ridurre la dimensione del lotto

Questo è spesso il primo passo più efficace e semplice. Una dimensione del lotto più piccola riduce direttamente il numero di campioni elaborati simultaneamente, diminuendo quindi la memoria necessaria per gli input, le attivazioni intermedie e i gradienti.


# Dimensione del lotto originale
batch_size = 64
# Se OOM, provare
batch_size = 32
# O anche
batch_size = 16
 

Riducete la vostra dimensione del lotto a metà in modo iterativo finché l’errore non scompare. Siate consapevoli che una dimensione del lotto molto piccola potrebbe influenzare la stabilità dell’allenamento o la velocità di convergenza, quindi è un compromesso.

Eliminare tensori e variabili non necessari

Assicuratevi di non mantenere in memoria grandi tensori o variabili che non sono più necessari. Il garbage collector di Python alla fine li libererà, ma rimuoverli esplicitamente può liberare memoria prima. Non dimenticate di spostarli sulla CPU o di scollegarli se fanno parte del grafo computazionale e volete conservare i loro dati ma non la loro storia di gradienti.


# Esempio: Se avete un grande tensore 'temp_data' che non è più necessario
del temp_data
# Chiamate anche esplicitamente il garbage collector
import gc
gc.collect()
torch.cuda.empty_cache() # Chiamate di nuovo dopo la rimozione
 

Monitorare l’uso della memoria GPU

Strumenti come nvidia-smi (nel vostro terminale) o le funzioni di report della memoria integrate di PyTorch possono darvi spunti sul consumo di memoria della vostra GPU. Questo aiuta a identificare se un altro processo sta consumando memoria o se il vostro script PyTorch è l’unico colpevole.


nvidia-smi
 

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


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

Questo fornisce una suddivisione della memoria allocata rispetto alla memoria riservata e può talvolta indicare una frammentazione.

Tecniche avanzate di ottimizzazione della memoria

Quando le correzioni rapide non sono sufficienti, o quando è necessario allenare modelli realmente grandi, sono necessarie tecniche più sofisticate. Questi metodi comportano spesso compromessi tra memoria, tempo di calcolo e complessità del codice.

Accumulo di gradienti

L’accumulo di gradienti consente di simulare una dimensione di lotto efficace più grande senza aumentare l’impronta di memoria di un singolo passaggio avanti/indietro. Invece di aggiornare i pesi dopo ogni lotto, accumulati i gradienti su più piccoli lotti, poi effettuate un singolo aggiornamento.


model = MyModel().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
accumulation_steps = 4 # Accumulare 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 # Normalizzare la perdita per l'accumulo

 loss.backward() # Accumulare i gradienti

 if (i + 1) % accumulation_steps == 0:
 optimizer.step() # Effettuare l'aggiornamento dell'ottimizzatore
 optimizer.zero_grad() # Azzerare i gradienti

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

Questa tecnica è potente per l’allenamento con grandi dimensioni di lotto effettive su GPU con memoria limitata.

Punto di controllo dei gradienti (punto di controllo delle attivazioni)

Come discusso, le attivazioni intermedie occupano una memoria significativa. Il checkpointing dei gradienti affronta questo problema non memorizzando tutte le attivazioni intermedie durante il forward pass. Invece, le ricalcola durante il backward pass per i segmenti che richiedono gradienti. Questo riduce notevolmente la memoria ma aumenta il tempo di calcolo, poiché alcune parti del forward pass 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):
 # Usare il checkpoint per il forward pass di questo strato
 return checkpoint.checkpoint(self.layer, x)

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

È particolarmente utile per reti molto profonde in cui memorizzare tutte le attivazioni è impossibile.

Allenamento in precisione mista (FP16/BF16)

L’allenamento in precisione mista consiste nell’eseguire alcune operazioni in precisione inferiore (FP16 o BF16) mantenendo altre in FP32. Questo può ridurre della metà l’impronta di memoria per i pesi, le attivazioni e i gradienti, e spesso accelera l’allenamento su GPU moderne (come le architetture NVIDIA Volta, Turing, Ampere, Ada Lovelace) che hanno core Tensor progettati per calcoli FP16.

Il modulo torch.cuda.amp di PyTorch facilita l’implementazione di ciò:


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 saranno eseguite in FP16 quando possibile
 outputs = model(inputs)
 loss = criterion(outputs, labels)

 scaler.scale(loss).backward() # Scala la perdita per evitare il sovraccarico dei gradienti FP16
 scaler.step(optimizer) # Descale dei gradienti e aggiornamento dei pesi
 scaler.update() # Aggiorna lo scaler per l'iterazione successiva
 

La precisione mista è una tecnica potente che offre spesso sia risparmi di memoria che guadagni di prestazioni.

Scarico verso la CPU (CPU Offloading)

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


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

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

Per i livelli del modello, ciò implica spesso di suddividere il modello e spostare blocchi sequenziali tra i dispositivi.

Considerazioni Architettoniche e di Progettazione del Codice

Oltre alle tecniche specifiche di ottimizzazione della memoria, il modo in cui progetti il tuo modello e scrivi il tuo codice PyTorch può avere un impatto significativo sull’uso della memoria GPU.

Architetture di Modello Efficienti

Alcune architetture di modelli sono intrinsecamente più affamate di memoria rispetto ad altre. Ad esempio, i modelli con strati molto larghi o quelli che generano molte mappe di caratteristiche ad alta risoluzione (ad esempio, nei compiti di segmentazione) consumeranno più memoria. Prendi in considerazione l’utilizzo di alternative più parsimoniose in memoria, se possibile:

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

Operazioni In-place

Le operazioni PyTorch creano spesso nuovi tensori per il loro output. Le operazioni in-place (indicate da un underscore finale, ad esempio, x.add_(y) anziché 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, in particolare quando vengono utilizzate su tensori che richiedono gradienti.


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

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

Evita Cloni/Copie di Tensors Inutili

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 è davvero necessaria una copia profonda, altrimenti lavora con le viste quando 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()
 

Utilizzare torch.no_grad() per l’Inferred/Valutazione

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


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

È una pratica fondamentale per chiunque lavori con PyTorch e può spesso prevenire gli errori OOM durante le fasi di validazione.

Profilazione dell’Uso della Memoria

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


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

# Esempio di profilazione di un singolo forward/backward pass
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))
 

L’output del profiler mostrerà una 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