From 324907fdfe05a6efd14aa85aec35901aede76217 Mon Sep 17 00:00:00 2001 From: LucaCorra02 Date: Fri, 20 Feb 2026 10:41:59 +0100 Subject: [PATCH 1/6] add raw note --- chapters/Lezione11.typ | 121 +++++++++++++++++++++++++++++++++++++++++ main.typ | 1 + 2 files changed, 122 insertions(+) create mode 100644 chapters/Lezione11.typ diff --git a/chapters/Lezione11.typ b/chapters/Lezione11.typ new file mode 100644 index 0000000..6df02c5 --- /dev/null +++ b/chapters/Lezione11.typ @@ -0,0 +1,121 @@ +#import "../template.typ": * + += PyTorch on GPU + +``` python +tensor_gpu = tensor_cpu.to(torch.device('cuda:0')) +tensor_gpu = tensor_cpu.cuda(0) #versione corta +``` + +Si tratta di un trasferimento di dati esplicito da CPU a GPU, il device GPU viene specificato tramite una stringa. Il problema è che possono fare delle operazioni miste, in particolare se vogliamo risparmiare operazioni possiamo concatenarle: +```py +x = x.to(device='cuda',dtype=np.float32) +``` +Tramite la variabile `torch.set_default_device('cuda')`, posso cambiare il device di default (CPU). Le opraziomi `rand` e `zeros` ora eseguono una cuda malloc. + +Scrivere le factory function, empty, zeros, ecc +Ci sono alcune operazioni che lavorano in place, ad esempio ```py +x = torch.rand(100,100,device='cuda:0') +y = torch.rand(100,device='cuda:0') + +x.add_(y)#avviene un broadcasting implicito gestito da cuda +z = torch.rand(100,device='cpu') +#x.add_(z) z e x sono su due device diversi +``` +La gestione del garbage avviene automaticamente ed è gestita a runtime (non dal programmatore). + +I dati del modello possono essere spostati sulla gpu +```py +model = nn.Sequential( + nn.Linear(784,256), + nn.ReLu +) +model = model.to() +``` +Allo stesso modo i dati nel training vengono elaborati a batch, i batch anche essi devono essere caricati sulla GPU. I singoli batch vengono trasferiti su device sia input che target +```py +for input,target in dataLoader: + inputs = inputs.to(device) + targets = targets.to(device) + output = model(inputs) #risultati su GPU, vanno ritrasferiti +``` + +#nota()[ + Tutto il codice è molto portatile, può essere scalato per più cpu, più thread delle cpu in modo trasparente +] + +Solitamente la memoria riservata e poco di più di quella allocata. + +//aggiungere summary + += Stream + +Solitamente ci sono una serie di _corsie_ su cui smalitre il traffico overo gli stream. Si possono sovrappore delle richieste di trasferimento e calcolo vero e proprio. + +Esiste anche qui il default stream (di default le cose lanciate su GPU vanno qua), inoltre il default stream è mutualmente esclusivo, blocca tutti gli altri stream (non bello). + +Usiamo la classe `torch.cuda.Stream` per la costruzione configurazione di stream. +```python + +stream = torch.cuda.Stream( + device = True, + priority = 0, #priorità a livello di stream nello scheuling +) +``` +#esempio()[ + ```py + x = torch.randn(1000,1000, device='cuda') + y = torch.matmul(x,x) + + stream1 = torch.cuda.Stream('cuda') + stream2 = torch.cuda.Stream('cuda') + with torch.cuda.stream(stream1):#passo l'esecuzione a stream1 + #op stream1 + with torch.cuda.stream(stream2): + #op stream2 + ``` + Le esecuzioni di stream1 e stream2 sono davvero concorrenti +] +Possiamo avere anche degli stream annidati (creazione di stream dentro il contesto di un altro stream). Lo stream interno vive solamente nel contesto del with interno, successivamente il contesto passa a quello esterno + +Esiste un metodo `wait.stream` che permette di sincronizarci con gli altri stream. Oltre agli eventi + +== Trasferimento dati efficiente + +Esistono due meccanismi ; +- `pinned_memory` trasferire i dati in pinned_memory +- `non_blocking` in modo asincrono, l'host non attende su quel trasferimento + +=== Pin memory + +`tensor.to(device, non_blocking)`. La pin memory può essere applicata ad un tensore per trasferirlo nella pinned. +//aggiunge pinned memory immage + +Il codice che viene esegutio si blocca e aspetta `pin_memory` è quindi *blocking* per l'host. +Possono essere creati anche dei tensori direttamente in pin memory già inizializzati come vogliamo + +== Eventi + +All'interno di un kernel può essere messo un marker, ovvero `torch.cuda.Event()` e posso sincronizzare gli stream su un certo evento. Posso creare un interdipendenza tra eventi + +`torch.cuda.syncronize` = sincronizza su tutto, host e strami, solitamente non si usa + +solitamente di una `strea.sync` o `event.sync`. + +```py +x_cpu = torch.rand(pinend=True) +x_gpu = device.to(non_blocking=True) #trasferimento asincrono +``` + +//aggiungere esempio sync tra stream + +//aggiungere esercizio + +Esempio in cui nelle fase di training uso degli stream in modo tale che se il batch N sta venendo computato, trasferisco il batch n+1 in GPU + +Ho un punto unico di uso dei dati model. Devo sgnaciare solamente l'esecuzione del trasferimento su un altro stream, ne bastano due. + +In questo caso non possiamo elaborare un chunk di dati su uno stream in maniera indipendente. Qui non va bene in quanto il model deve consumare tutti i dati che passano + +Intando che il model elabora un cnhunk paralelamente carichiamo in memoria GPU (asincrono) + diff --git a/main.typ b/main.typ index ce1bdc9..d95487a 100644 --- a/main.typ +++ b/main.typ @@ -41,3 +41,4 @@ #part("PyTorch") #include "chapters/Lezione-7.typ" #include "chapters/Lezione10.typ" +#include "chapters/Lezione11.typ" From bd6dab7adb9fd88400e94c92413eb15dfcda99f8 Mon Sep 17 00:00:00 2001 From: LucaCorra02 Date: Mon, 23 Feb 2026 09:18:42 +0100 Subject: [PATCH 2/6] grammar LLM Check --- chapters/Lezione11.typ | 130 +++++++++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 43 deletions(-) diff --git a/chapters/Lezione11.typ b/chapters/Lezione11.typ index 6df02c5..b84460a 100644 --- a/chapters/Lezione11.typ +++ b/chapters/Lezione11.typ @@ -11,16 +11,22 @@ Si tratta di un trasferimento di dati esplicito da CPU a GPU, il device GPU vien ```py x = x.to(device='cuda',dtype=np.float32) ``` -Tramite la variabile `torch.set_default_device('cuda')`, posso cambiare il device di default (CPU). Le opraziomi `rand` e `zeros` ora eseguono una cuda malloc. +Tramite la variabile `torch.set_default_device('cuda')`, posso cambiare il device di default (CPU). Le operazioni `rand` e `zeros` ora eseguono una cuda malloc. -Scrivere le factory function, empty, zeros, ecc -Ci sono alcune operazioni che lavorano in place, ad esempio ```py +Le factory functions (`empty`, `zeros`, `ones`, `rand`, ecc.) supportano il parametro `device` per creare tensori direttamente su GPU: +```py +x = torch.zeros(100, 100, device='cuda:0') +y = torch.rand(100, 100, device='cuda:0') +``` + +Ci sono alcune operazioni che lavorano in place, ad esempio: +```py x = torch.rand(100,100,device='cuda:0') y = torch.rand(100,device='cuda:0') -x.add_(y)#avviene un broadcasting implicito gestito da cuda +x.add_(y) # avviene un broadcasting implicito gestito da cuda z = torch.rand(100,device='cpu') -#x.add_(z) z e x sono su due device diversi +# x.add_(z) # ERRORE: z e x sono su due device diversi ``` La gestione del garbage avviene automaticamente ed è gestita a runtime (non dal programmatore). @@ -28,38 +34,35 @@ I dati del modello possono essere spostati sulla gpu ```py model = nn.Sequential( nn.Linear(784,256), - nn.ReLu + nn.ReLU() ) -model = model.to() +model = model.to(device) # sposta il modello su GPU ``` Allo stesso modo i dati nel training vengono elaborati a batch, i batch anche essi devono essere caricati sulla GPU. I singoli batch vengono trasferiti su device sia input che target ```py -for input,target in dataLoader: +for inputs, targets in dataLoader: inputs = inputs.to(device) targets = targets.to(device) - output = model(inputs) #risultati su GPU, vanno ritrasferiti + output = model(inputs) # risultati su GPU ``` #nota()[ - Tutto il codice è molto portatile, può essere scalato per più cpu, più thread delle cpu in modo trasparente + Tutto il codice è molto portabile, può essere scalato per più GPU, più thread delle CPU in modo trasparente. ] -Solitamente la memoria riservata e poco di più di quella allocata. - -//aggiungere summary +Solitamente la memoria riservata è poco di più di quella allocata. PyTorch gestisce automaticamente la memoria GPU tramite un allocatore interno. = Stream -Solitamente ci sono una serie di _corsie_ su cui smalitre il traffico overo gli stream. Si possono sovrappore delle richieste di trasferimento e calcolo vero e proprio. +Solitamente ci sono una serie di _corsie_ su cui smaltire il traffico ovvero gli stream. Si possono sovrapporre delle richieste di trasferimento e calcolo vero e proprio. Esiste anche qui il default stream (di default le cose lanciate su GPU vanno qua), inoltre il default stream è mutualmente esclusivo, blocca tutti gli altri stream (non bello). -Usiamo la classe `torch.cuda.Stream` per la costruzione configurazione di stream. +Usiamo la classe `torch.cuda.Stream` per la costruzione e configurazione di stream. ```python - stream = torch.cuda.Stream( - device = True, - priority = 0, #priorità a livello di stream nello scheuling + device='cuda:0', + priority=0 # priorità a livello di stream nello scheduling (range: -1 a 0) ) ``` #esempio()[ @@ -67,55 +70,96 @@ stream = torch.cuda.Stream( x = torch.randn(1000,1000, device='cuda') y = torch.matmul(x,x) - stream1 = torch.cuda.Stream('cuda') - stream2 = torch.cuda.Stream('cuda') - with torch.cuda.stream(stream1):#passo l'esecuzione a stream1 - #op stream1 + stream1 = torch.cuda.Stream() + stream2 = torch.cuda.Stream() + with torch.cuda.stream(stream1): # passo l'esecuzione a stream1 + # operazioni eseguite su stream1 + a = torch.randn(1000, 1000, device='cuda') with torch.cuda.stream(stream2): - #op stream2 + # operazioni eseguite su stream2 + b = torch.randn(1000, 1000, device='cuda') ``` Le esecuzioni di stream1 e stream2 sono davvero concorrenti ] -Possiamo avere anche degli stream annidati (creazione di stream dentro il contesto di un altro stream). Lo stream interno vive solamente nel contesto del with interno, successivamente il contesto passa a quello esterno +Possiamo avere anche degli stream annidati (creazione di stream dentro il contesto di un altro stream). Lo stream interno vive solamente nel contesto del `with` interno, successivamente il contesto passa a quello esterno. -Esiste un metodo `wait.stream` che permette di sincronizarci con gli altri stream. Oltre agli eventi +Esiste un metodo `wait_stream()` che permette di sincronizzarci con gli altri stream, oltre agli eventi. == Trasferimento dati efficiente -Esistono due meccanismi ; -- `pinned_memory` trasferire i dati in pinned_memory -- `non_blocking` in modo asincrono, l'host non attende su quel trasferimento +Esistono due meccanismi: +- `pin_memory`: trasferire i dati in pinned memory +- `non_blocking`: trasferimento in modo asincrono, l'host non attende su quel trasferimento === Pin memory -`tensor.to(device, non_blocking)`. La pin memory può essere applicata ad un tensore per trasferirlo nella pinned. -//aggiunge pinned memory immage +`tensor.to(device, non_blocking=True)`. La pin memory può essere applicata ad un tensore per trasferirlo nella memoria pinned. -Il codice che viene esegutio si blocca e aspetta `pin_memory` è quindi *blocking* per l'host. -Possono essere creati anche dei tensori direttamente in pin memory già inizializzati come vogliamo +La pinned memory è una memoria RAM che non può essere spostata dalla memoria virtuale (page-locked). Questo permette trasferimenti DMA (Direct Memory Access) più veloci tra CPU e GPU, poiché il driver non deve prima copiare i dati in un buffer intermedio. + +Il codice che viene eseguito si blocca e aspetta. `pin_memory` è quindi *blocking* per l'host. +Possono essere creati anche dei tensori direttamente in pin memory già inizializzati: +```py +x = torch.randn(100, 100, pin_memory=True) +``` == Eventi -All'interno di un kernel può essere messo un marker, ovvero `torch.cuda.Event()` e posso sincronizzare gli stream su un certo evento. Posso creare un interdipendenza tra eventi +All'interno di un kernel può essere messo un marker, ovvero `torch.cuda.Event()` e posso sincronizzare gli stream su un certo evento. Posso creare un'interdipendenza tra eventi. -`torch.cuda.syncronize` = sincronizza su tutto, host e strami, solitamente non si usa +`torch.cuda.synchronize()` sincronizza su tutto, host e stream, solitamente non si usa. -solitamente di una `strea.sync` o `event.sync`. +Solitamente si usa `stream.synchronize()` o `event.synchronize()`. ```py -x_cpu = torch.rand(pinend=True) -x_gpu = device.to(non_blocking=True) #trasferimento asincrono +x_cpu = torch.rand(100, 100, pin_memory=True) +x_gpu = x_cpu.to('cuda', non_blocking=True) # trasferimento asincrono ``` -//aggiungere esempio sync tra stream +#esempio()[ + Sincronizzazione tra stream usando eventi: + ```py + stream1 = torch.cuda.Stream() + stream2 = torch.cuda.Stream() + event = torch.cuda.Event() + + with torch.cuda.stream(stream1): + x = torch.randn(1000, 1000, device='cuda') + event.record() # registra l'evento su stream1 -//aggiungere esercizio + with torch.cuda.stream(stream2): + stream2.wait_event(event) # attende che stream1 raggiunga l'evento + y = x + 1 # usa il risultato di stream1 + ``` +] -Esempio in cui nelle fase di training uso degli stream in modo tale che se il batch N sta venendo computato, trasferisco il batch n+1 in GPU +=== Esempio: Sovrapposizione di trasferimento e computazione + +Esempio in cui nella fase di training uso degli stream in modo tale che se il batch N sta venendo computato, trasferisco il batch N+1 in GPU: + +```py +stream_compute = torch.cuda.Stream() +stream_transfer = torch.cuda.Stream() + +for i, (inputs, targets) in enumerate(dataloader): + # Trasferisco il batch corrente su GPU in modo asincrono + with torch.cuda.stream(stream_transfer): + inputs_gpu = inputs.to('cuda', non_blocking=True) + targets_gpu = targets.to('cuda', non_blocking=True) + + # Attendo che il trasferimento sia completato + stream_compute.wait_stream(stream_transfer) + + # Eseguo il forward pass + with torch.cuda.stream(stream_compute): + output = model(inputs_gpu) + loss = criterion(output, targets_gpu) + loss.backward() +``` -Ho un punto unico di uso dei dati model. Devo sgnaciare solamente l'esecuzione del trasferimento su un altro stream, ne bastano due. +Ho un punto unico di uso dei dati (model). Devo sganciare solamente l'esecuzione del trasferimento su un altro stream, ne bastano due. -In questo caso non possiamo elaborare un chunk di dati su uno stream in maniera indipendente. Qui non va bene in quanto il model deve consumare tutti i dati che passano +In questo caso non possiamo elaborare un chunk di dati su uno stream in maniera indipendente. Qui non va bene in quanto il model deve consumare tutti i dati che passano. -Intando che il model elabora un cnhunk paralelamente carichiamo in memoria GPU (asincrono) +Intanto che il model elabora un chunk, parallelamente carichiamo in memoria GPU (asincrono) il successivo. From 4eb8454078d9dc5d48e8bc057d25d2c8b94f8e8e Mon Sep 17 00:00:00 2001 From: LucaCorra02 Date: Mon, 23 Feb 2026 10:01:19 +0100 Subject: [PATCH 3/6] update first part --- chapters/Lezione11.typ | 127 ++++++++++++++++++++++++++++++++--------- 1 file changed, 99 insertions(+), 28 deletions(-) diff --git a/chapters/Lezione11.typ b/chapters/Lezione11.typ index b84460a..cfd22cd 100644 --- a/chapters/Lezione11.typ +++ b/chapters/Lezione11.typ @@ -2,53 +2,124 @@ = PyTorch on GPU -``` python -tensor_gpu = tensor_cpu.to(torch.device('cuda:0')) -tensor_gpu = tensor_cpu.cuda(0) #versione corta +A differenza degli array `numpy` che possono vivere solamente nella memoria host, i `tensori` possono esssere allocati anche su device. + +Di *default* il device su cui vengono allocati i tensori è la memoria host. Per cambiare il device possiamo: +```py + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(device) + torch.set_default_device('cuda') # Lo setta globalmente ``` +#nota()[ + La stringa `cuda` si riferisce al device di defualt, tipicamente `cuda:0`. Se ci sono più device sulla macchina possiamo selezionarli come `cuda:1,cuda:2, ecc..` +] -Si tratta di un trasferimento di dati esplicito da CPU a GPU, il device GPU viene specificato tramite una stringa. Il problema è che possono fare delle operazioni miste, in particolare se vogliamo risparmiare operazioni possiamo concatenarle: +è possibile anche cambiare device solo in un determinato contesto tramite `torch.cuda.device(device_id)`. Tale metodo infatti fornisce uno scope temporaneo in cui viene utilizzato il device indicato: ```py -x = x.to(device='cuda',dtype=np.float32) +with torch.cuda.device(0): + tensor_gpu1 = torch.randn(1000,1000) # On gpu + result = some_computation(tensor_gpu1) + +#Device reverts to previous setting ``` -Tramite la variabile `torch.set_default_device('cuda')`, posso cambiare il device di default (CPU). Le operazioni `rand` e `zeros` ora eseguono una cuda malloc. -Le factory functions (`empty`, `zeros`, `ones`, `rand`, ecc.) supportano il parametro `device` per creare tensori direttamente su GPU: +== Trasferimento CPU -> GPU + +Il trasferimenti avviene attraverso il metodo `.to()`. Esso può cambiare: +- Device +- Dype +- Layout +- Memory Format + +Il trasferimento dati avviene in maniera *esplicita* da CPU a GPU (e viceversa), il device GPU viene specificato tramite una stringa: + ```py -x = torch.zeros(100, 100, device='cuda:0') -y = torch.rand(100, 100, device='cuda:0') +tensor_cpu = torch.randn(1000, 1000) +print(tensor_cpu.device) + +# Pattern 1: Explicit device object +tensor_gpu = tensor_cpu.to(torch.device('cuda:0')) +print(tensor_gpu.device) + +# Pattern 2: String specification +tensor_gpu = tensor_cpu.to('cuda:0') +print(tensor_gpu.device) + +# Pattern 3: Legacy .cuda() method (still supported) +tensor_gpu = tensor_cpu.cuda(0) +print(tensor_gpu.device) + ``` +Possiamo andare anche ad effettuare delle operazioni aggiuntive durante il trasferimento, concatenandole tra di loro: -Ci sono alcune operazioni che lavorano in place, ad esempio: ```py -x = torch.rand(100,100,device='cuda:0') -y = torch.rand(100,device='cuda:0') + x = x.to(device='cuda',dtype=torch.float16) +``` -x.add_(y) # avviene un broadcasting implicito gestito da cuda -z = torch.rand(100,device='cpu') -# x.add_(z) # ERRORE: z e x sono su due device diversi +Le *factory functions* (`empty`, `zeros`, `ones`, `rand`, ecc.) supportano il parametro `device` per creare tensori direttamente su GPU: +```py + x = torch.zeros(100, 100, dtype=torch.float32 ,device='cuda:0') + y = torch.rand(100, 100, device='cuda:0') ``` -La gestione del garbage avviene automaticamente ed è gestita a runtime (non dal programmatore). +#nota()[ + Le device function sono molto utili in quanto permettono di rimuovere trasferimenti CPU->GPU non necessari. Allocazione in un'unica operazione. +] + +== Operazioni -I dati del modello possono essere spostati sulla gpu +Alcune operazioni PyTorch possono essere *in-place* (`add_`,`mul_`,`relu_`,ecc). Esse modificano direttamente il tensore in memoria senza crearne uno nuovo: ```py -model = nn.Sequential( - nn.Linear(784,256), - nn.ReLU() -) -model = model.to(device) # sposta il modello su GPU + x = torch.randn(100, 100, device='cuda:0') + y = torch.randn(100, device='cuda:0') # Broadcastable shape, same device + + x.add_(y) # Valid: same device, broadcastable + + z = torch.randn(100, device='cpu') + # x.add_(z) # RuntimeError: expected device cuda:0 but got cpu ``` -Allo stesso modo i dati nel training vengono elaborati a batch, i batch anche essi devono essere caricati sulla GPU. I singoli batch vengono trasferiti su device sia input che target +In questo caso il *broadcasting* (se possibile) viene automaticamente gestito da pytorch. + +La gestione del garbage collector avviene in maniera automatica, gestita a runtime (*non* dal programmatore). + +== Modello su GPU + +Possiamo trasferire un modello deep da CPU a GPU nel seguente modo. Internamente. +- Tutti i parametri (pesi e bias), vengono trasferiti sulla memoria della GPU +- Tutti i buffer (`BatchNorm`) vengono trasferiti +- La memoria allocata sul device per il modello rimane fino alla prossima passata del garbage collector + ```py -for inputs, targets in dataLoader: - inputs = inputs.to(device) - targets = targets.to(device) - output = model(inputs) # risultati su GPU + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + model = nn.Sequential( + nn.Linear(784, 256), + nn.ReLU(), + nn.Linear(256, 10) + ) + model = model.to(device) ``` #nota()[ - Tutto il codice è molto portabile, può essere scalato per più GPU, più thread delle CPU in modo trasparente. + PyTorch *non* permette operazioni tra host e device. Se il modello è allocato sulla GPU mentre l'input è sulla CPU verrà generato un `RunTimeError`. + + La *best practice* è utilizzare una variabile device definita globalmente. + ```py + model.to(device) + tensor.to(device) + ``` ] +Per questo motivo è sempre necessario trasferire i dati sullo stesso device del modello. I dati nel training vengono elaborati a batch, anche essi devono essere caricati sulla GPU: + +```py + for inputs, targets in dataLoader: + inputs = inputs.to(device) + outputs = outputs.to(device) + + outputs = model(inputs) + loss = criterion(outputs, targets) + #Risultati sulla GPU +``` + +== Memoria della GPU Solitamente la memoria riservata è poco di più di quella allocata. PyTorch gestisce automaticamente la memoria GPU tramite un allocatore interno. From d20618c2417ab0880009f4cd5cf93d3317dfedaf Mon Sep 17 00:00:00 2001 From: LucaCorra02 Date: Mon, 23 Feb 2026 11:38:01 +0100 Subject: [PATCH 4/6] update pinned --- chapters/Lezione11.typ | 223 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 197 insertions(+), 26 deletions(-) diff --git a/chapters/Lezione11.typ b/chapters/Lezione11.typ index cfd22cd..9fe46dd 100644 --- a/chapters/Lezione11.typ +++ b/chapters/Lezione11.typ @@ -121,58 +121,229 @@ Per questo motivo è sempre necessario trasferire i dati sullo stesso device del == Memoria della GPU -Solitamente la memoria riservata è poco di più di quella allocata. PyTorch gestisce automaticamente la memoria GPU tramite un allocatore interno. +Possiamo vedere lo stato della memoria tramite due direttive: +- `torch.cuda.memory_allocated()`: Mostra la memoria allocata, utilizzata dai tensori +- `torch.cuda.memory:reserved()`; Mostra la memoria riservata -= Stream +Inoltre, tramite la funzione `torch.cuda.empty_cache()` è possibile rilasciare la cache che la GPU non sta utilizzando, per renderla di nuovo disponibile. Questa operazione *non* ha effetto sulla memoria che sta venendo utilizzata dai tensori. -Solitamente ci sono una serie di _corsie_ su cui smaltire il traffico ovvero gli stream. Si possono sovrapporre delle richieste di trasferimento e calcolo vero e proprio. +== Stream -Esiste anche qui il default stream (di default le cose lanciate su GPU vanno qua), inoltre il default stream è mutualmente esclusivo, blocca tutti gli altri stream (non bello). +Le moderne architetture dispongono di code hardware indipendenti su cui possiamo spalmare il traffico da smaltire. In pytorch uno *stream* definisce a quale di queste code di esecuzione hardware verrà inviata la successiva operazione CUDA. + +PyTorch assegna di *default* le operazioni a uno _stream di default_. Tutte le operazioni lanciate su GPU di default si incanalano qui. Il $mr("problema")$ principale è che questo strem è *mutualmente esclusivo* viene sincronizzato implicitamente con tutti gli altri, creando cosi delle barriere di sincronizzazione che *riducono la concorrenza*. + +In particolare, se non si creano nuovi stream si rischoa di serealizzare delle operazioni che potrebbero avvenire in parallelo come: +- Trasferimenti Host->Device +- Il lancio di kernel +- Trasferimenti Device->Host + +I casi d'uso principale includono: +- *Overlap Copia - Calcolo*: Separando le operazioni in stream diversi, il motore DMA (responsabile dei trasferimenti via PCIe o NVLink) e il motore di calcolo (che esegue i kernel sui multiprocessori) possono operare simultaneamente e in modo indipendente. + +- *Triple-Buffering*: Un pattern molto efficiente consiste nel dedicare tre stream separati: uno per i trasferimenti di dati verso la GPU (H2D), uno per i kernel di calcolo, e un terzo per riportare i risultati sulla CPU (D2H). Questa separazione abilita pipeline in cui l'upload, il calcolo e il download avvengono contemporaneamente. + +- *Stram Based Prefetch*: Durante l'*addestramento*, uno stream dedicato alla copia può avviare il trasferimento del batch $N+1$ (dalla CPU alla GPU), proprio mentre lo stream di calcolo sta ancora elaborando il batch $N$. Questa tecnica incrementa notevolmente il throughput complessivo. + +=== Creazione degli Stream + +In pytorch viene utilizzata la classe `torch.cuda.Stream` per la costruzione e configurazione di stream. La creazione di stream è limitata dai limiti hardware della GPU, solitamente $32-64$ kernel concorrenti. -Usiamo la classe `torch.cuda.Stream` per la costruzione e configurazione di stream. ```python -stream = torch.cuda.Stream( - device='cuda:0', - priority=0 # priorità a livello di stream nello scheduling (range: -1 a 0) -) + stream = torch.cuda.Stream( + device='cuda:0', # Target device + priority=0, # Priority level (lower = higher priority) + ) ``` + #esempio()[ + Nell'esempio è il calcolo di $a$ e di $b$ avvengono in modo parallelo: ```py x = torch.randn(1000,1000, device='cuda') y = torch.matmul(x,x) - stream1 = torch.cuda.Stream() - stream2 = torch.cuda.Stream() + stream1 = torch.cuda.Stream('cuda') + stream2 = torch.cuda.Stream('cuda') + with torch.cuda.stream(stream1): # passo l'esecuzione a stream1 # operazioni eseguite su stream1 - a = torch.randn(1000, 1000, device='cuda') + a = torch.matmul(x,x) + with torch.cuda.stream(stream2): # operazioni eseguite su stream2 - b = torch.randn(1000, 1000, device='cuda') + b = torch.matmul(x,x) ``` - Le esecuzioni di stream1 e stream2 sono davvero concorrenti + Le esecuzioni di stream1 e stream2 sono concorrenti. +] + +Possiamo avere anche degli *stream annidati* (creazione di stream dentro il contesto di un altro stream). Lo stream interno vive solamente nel contesto del `with` interno, successivamente il contesto passa a quello esterno: +```py +outer_stream = torch.cuda.Stream('cuda') +with torch.cuda.stream(outer_stream): + # Opereazioni eseguite sullo stream outer + x = torch.randn(1000, 1000) + inner_stream = torch.cuda.Stream('cuda') + with torch.cuda.stream(inner_stream): + # Operazioni eseguite sul inner_stream + y = torch.matmul(x, x) + + # Controllo passato all'outer_stream + z = x + y # Nota: y può non essere pronta, serve sincronizzazione ! +``` +#nota()[ + Nell'esempio sopra $y$ potrebbe non essere pronta, richiede sincronizzazione. ] -Possiamo avere anche degli stream annidati (creazione di stream dentro il contesto di un altro stream). Lo stream interno vive solamente nel contesto del `with` interno, successivamente il contesto passa a quello esterno. -Esiste un metodo `wait_stream()` che permette di sincronizzarci con gli altri stream, oltre agli eventi. +La *sincronizzazione* avviene tramite il metodo `wait_stream()` che permette di sincronizzarci con gli altri stream, oltre agli eventi. In particolare per creare una dipendenza tra due stream: +```py + torch.cuda.current_stream().wait_stream(copy_stream) +``` == Trasferimento dati efficiente -Esistono due meccanismi: -- `pin_memory`: trasferire i dati in pinned memory -- `non_blocking`: trasferimento in modo asincrono, l'host non attende su quel trasferimento +Il trasferimento efficiente tra CPU e GPU è essenziale. Potrebbe diventare un bottleneck se non fatto in maniera adeguata. PyTorch fornisce due meccanismi: +- *Pinned Memory* `tensor.pin_memory()` +- *Trasferimenti asincroni* `tensor.to(device, non_blocking=True)`. L'host non attende in questo tipo di trasferimento + +#attenzione()[ + Usare la pinned_memory non sempre più efficiente, deve essere utilizzata con cautela. +] === Pin memory -`tensor.to(device, non_blocking=True)`. La pin memory può essere applicata ad un tensore per trasferirlo nella memoria pinned. +In python esistono due tipi di memorie: +- *Pinned Memory*: La GPU può accedere direttamente alla memoria del host. Si tratta di un'area della RAM della CPU che viene bloccata, in modo che il sistema operatico non possa spostarla su disco. -La pinned memory è una memoria RAM che non può essere spostata dalla memoria virtuale (page-locked). Questo permette trasferimenti DMA (Direct Memory Access) più veloci tra CPU e GPU, poiché il driver non deve prima copiare i dati in un buffer intermedio. +- *Pageable Memory*: CUDA deve prima creare una copia del dato nella memoria pinned, successivamente procedere con il trasferimento. + +*`.to(device)`* = Effettua il classico trasferimento da CPU a GPU. Il tensore inzialmente risiede in RAM nella parte *paginata*. Tuttavia questo trasferimento presenta il problema dello $mr("stagin")$: La GPU non può leggere direttamente dalla memoria paginabile. CUDA, sottobanco, crea una copia temporanea del tensore nell'area pinned. Solo dopo il tensore può essere trasferito sulla GPU. Si tratta di un'*operazione bloccante* (La CPU attende che il trasferimento sia completo) + + +*`.pin_memory()`* presenta due caratteristiche principali: +- *Accesso diretto*: Un tensore che risiede in questa zona di RAM può essere copiato dal motore DMA, senza bisogno dello stagin +- *Bloccante*: Si tratta di un'operazione bloccante per la CPU. + +#nota()[ + Solitamente l'uso della pinned memory viene utilizzato con il parametro*`non_blocking=True`*. PyTorch cede il trasferimento dei dati su uno stream CUDA e restituire immediatamente il controllo alla CPU. + + Richiede che i dati di partenza che si vuole trasferire siano già nella `pinned_memory`. +] + +#align(center)[ + #import "@preview/cetz:0.3.2": canvas, draw + #canvas(length: 1cm, { + import draw: * + + // Virtual Memory box + rect((0, 0), (6, 7), stroke: 2pt, name: "vm") + content((3, 6.5), text(11pt, weight: "bold")[Virtual memory]) + + // Disk area + rect((0.5, 4.5), (5.5, 6), fill: rgb("#f4e5a0"), stroke: 1pt, name: "disk") + content((3, 5.5), [Disk]) + + // RAM area + rect((0.5, 0.5), (5.5, 4.2), fill: rgb("#f4e5a0"), stroke: 1pt, name: "ram") + content((3, 3.8), [RAM (pageable)]) + + // Pinned memory area inside RAM + rect((2.5, 1), (5, 3.2), fill: rgb("#b8e6b8"), stroke: 1.5pt, name: "pinned") + content((3.75, 2.9), text(9pt)[pinned memory]) + + // GPU box + rect((8, 2), (11, 5), stroke: 2pt, name: "gpu") + content((10.3, 4.5), text(11pt, weight: "bold")[GPU]) + + // ========== PERCORSO BLU (senza pinned memory - percorso lento) ========== + // Tensori blu su disco + circle((1.2, 5.5), radius: 0.15, fill: rgb("#7eb3e6"), stroke: 1pt) + circle((1.5, 5.5), radius: 0.15, fill: rgb("#7eb3e6"), stroke: 1pt) + + // Tensori blu in RAM pageable + circle((1.2, 3), radius: 0.15, fill: rgb("#7eb3e6"), stroke: 1pt) + circle((1.5, 3), radius: 0.15, fill: rgb("#7eb3e6"), stroke: 1pt) + + // Tensori blu in pinned memory (copia intermedia CUDA) + circle((3.2, 2), radius: 0.15, fill: rgb("#7eb3e6"), stroke: 1pt) + circle((3.5, 2), radius: 0.15, fill: rgb("#7eb3e6"), stroke: 1pt) + + // Tensori blu in GPU + rect((9, 4.2), (9.6, 4.6), fill: rgb("#7eb3e6"), stroke: 1pt) + + // Freccia blu: Disco → RAM + line((1.35, 4.5), (1.35, 3.3), stroke: (paint: rgb("#7eb3e6"), thickness: 1.5pt), mark: (end: "stealth")) + content((0.4, 3.9), text(7pt, fill: rgb("#7eb3e6"), weight: "bold")[]) + + // Freccia blu: RAM → Pinned (copia intermedia) + line((1.8, 3), (3, 2.2), stroke: (paint: rgb("#7eb3e6"), thickness: 1.5pt), mark: (end: "stealth")) + content((2.3, 2.4), text(7pt, fill: rgb("#7eb3e6"), weight: "bold")[]) + + + // ========== PERCORSO ARANCIONE (con pinned memory) ========== + // Tensori arancioni su disco + circle((4.2, 5.5), radius: 0.15, fill: rgb("#e87030"), stroke: 1pt) + circle((4.5, 5.5), radius: 0.15, fill: rgb("#e87030"), stroke: 1pt) + + // Tensori arancioni in RAM pageable + circle((4.2, 3.5), radius: 0.15, fill: rgb("#e87030"), stroke: 1pt) + circle((4.5, 3.5), radius: 0.15, fill: rgb("#e87030"), stroke: 1pt) + + // Tensori arancioni in pinned memory + circle((4.2, 2.2), radius: 0.15, fill: rgb("#e87030"), stroke: 1pt) + circle((4.5, 2.2), radius: 0.15, fill: rgb("#e87030"), stroke: 1pt) + + // Tensori arancioni in GPU + rect((9, 3.3), (9.6, 3.7), fill: rgb("#e87030"), stroke: 1pt) + + // Freccia arancione: Disco → RAM + line((4.35, 4.5), (4.35, 3.8), stroke: (paint: rgb("#e87030"), thickness: 1.5pt), mark: (end: "stealth")) + content((5.2, 4.2), text(7pt, fill: rgb("#e87030"), weight: "bold")[]) + + // Freccia arancione: RAM → Pinned Memory + line((4.35, 3.2), (4.35, 2.5), stroke: (paint: rgb("#e87030"), thickness: 1.5pt), mark: (end: "stealth")) + content((5.2, 2.9), text(7pt, fill: rgb("#e87030"), weight: "bold")[]) + + // Freccia arancione: Pinned → GPU (DMA) + line((5, 2.2), (8, 3.5), stroke: (paint: rgb("#e87030"), thickness: 2pt), mark: (end: "stealth")) + content((6.5, 2.5), text(7pt, fill: rgb("#e87030"), weight: "bold")[DMA]) + + // Legend + let legend_y = -1.5 + + // Pageable memory + rect((0, legend_y), (0.8, legend_y + 0.5), fill: rgb("#f4e5a0"), stroke: 1pt) + content((2.2, legend_y + 0.25), anchor: "west", text(9pt)[Pageable memory]) + + // Non-pageable memory + rect((0, legend_y - 0.8), (0.8, legend_y - 0.3), fill: rgb("#b8e6b8"), stroke: 1pt) + content((2.2, legend_y - 0.55), anchor: "west", text(9pt)[Non-pageable (page-locked)]) + + // Percorsi + content((0, legend_y - 1.4), anchor: "west", text(10pt, weight: "bold")[Percorsi di trasferimento:]) + + line( + (0.2, legend_y - 1.9), + (0.8, legend_y - 1.9), + stroke: (paint: rgb("#7eb3e6"), thickness: 1.5pt), + mark: (end: "stealth"), + ) + + content((2.2, legend_y - 1.9), anchor: "west", text(9pt)[`pinned_memory()`]) + + line( + (0.2, legend_y - 2.5), + (0.8, legend_y - 2.5), + stroke: (paint: rgb("#e87030"), thickness: 2pt), + mark: (end: "stealth"), + ) + content((2.2, legend_y - 2.5), anchor: "west", text(9pt)[`.to_device()`]) + + // Transfer annotations + content((1.6, 5), text(8pt, fill: rgb("#7eb3e6"), weight: "bold")[`pinned_memory()`]) + content((4.2, 5), text(8pt, fill: rgb("#e87030"), weight: "bold")[`.to_device()`]) + }) +] -Il codice che viene eseguito si blocca e aspetta. `pin_memory` è quindi *blocking* per l'host. -Possono essere creati anche dei tensori direttamente in pin memory già inizializzati: -```py -x = torch.randn(100, 100, pin_memory=True) -``` == Eventi From c658b54ba7f9e47f64733c1e6cb5df95c7264c80 Mon Sep 17 00:00:00 2001 From: LucaCorra02 Date: Mon, 23 Feb 2026 11:59:41 +0100 Subject: [PATCH 5/6] add events --- chapters/Lezione11.typ | 64 +++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/chapters/Lezione11.typ b/chapters/Lezione11.typ index 9fe46dd..788c7b1 100644 --- a/chapters/Lezione11.typ +++ b/chapters/Lezione11.typ @@ -347,34 +347,60 @@ In python esistono due tipi di memorie: == Eventi -All'interno di un kernel può essere messo un marker, ovvero `torch.cuda.Event()` e posso sincronizzare gli stream su un certo evento. Posso creare un'interdipendenza tra eventi. +All'interno di un kernel può essere messi dei *marker*, ovvero dei `torch.cuda.Event()`. Essi possono essere usati come delle barriere di *sincronizzazione* molto leggere, per gestire l'overlap delle operazioni sulla GPU. Gli eventi creano una vera dipendenza tra *operazioni su stream diversi*, *senza però bloccare l'host* (la CPU). -`torch.cuda.synchronize()` sincronizza su tutto, host e stream, solitamente non si usa. +#esempio()[ + Quando il lavoro viene diviso su più stream (ad esempio, uno stream per copiare i dati e un altro per fare i calcoli), è necessario coordinarli. Lo stream di calcolo non può iniziare a elaborare i dati finché lo stream di copia non ha effettivamente finito di trasferirli. +] -Solitamente si usa `stream.synchronize()` o `event.synchronize()`. - -```py -x_cpu = torch.rand(100, 100, pin_memory=True) -x_gpu = x_cpu.to('cuda', non_blocking=True) # trasferimento asincrono -``` +Il meccanismo si basa su due azioni principali: +- *`record()`*: Viene registrato l'evento che si deve aspettare `event.record` (ad esempio la copia dei dati) +- *`wait_event()`*: Lo stream aspetta fino a quando il marker non viene raggiunto `compute_stream.wait(event)` (ad esempio, per iniziare la computazione il trasferimento dati deve essere finito) #esempio()[ - Sincronizzazione tra stream usando eventi: + L'esempio mostra la creazione di due stream: + - il primo per il trasferimento dei dati + - il secondo per la loro computazione + + La computazione non può iniziare se il trasferimento non è completato. ```py - stream1 = torch.cuda.Stream() - stream2 = torch.cuda.Stream() - event = torch.cuda.Event() + event = torch.cuda.Event() + with torch.cuda.stream(copy_stream): + x_gpu = x_cpu.to("cuda", non_blocking=True) + + event.record() + with torch.cuda.stream(compute_stream): + compute_stream.wait_event(event) # Attesa + y = model(x_gpu) #Computazion + ``` +] - with torch.cuda.stream(stream1): - x = torch.randn(1000, 1000, device='cuda') - event.record() # registra l'evento su stream1 +=== Livelli di sincronizzazione - with torch.cuda.stream(stream2): - stream2.wait_event(event) # attende che stream1 raggiunga l'evento - y = x + 1 # usa il risultato di stream1 - ``` +In PyTorch esistono diversi meccanismi di sincronizzazione con diversi livelli di granularità e impatto sulle prestazioni: + +#align(center)[ + #table( + columns: (auto, auto, 1fr), + align: (center, center, left), + stroke: 0.5pt, + inset: 8pt, + table.header([*API*], [*Ambito (Scope)*], [*Descrizione*]), + [`torch.cuda.synchronize()`], + [Globale], + [Ferma la CPU finché tutte le operazioni su tutti gli stream non sono terminate. È molto sicuro ma uccide il parallelismo.], + + [`stream.synchronize()`], [Stream], [Ferma la CPU finché un singolo stream specifico non ha finito il suo lavoro.], + + [`event.synchronize()`], [Evento], [Ferma la CPU finché non viene raggiunto quello specifico evento (segnalibro).], + + [`stream.wait_event()`], + [Cross-stream], + [*Non blocca la CPU*. Dice semplicemente a uno stream della GPU di aspettare che venga raggiunto un evento registrato su un altro stream.], + ) ] + === Esempio: Sovrapposizione di trasferimento e computazione Esempio in cui nella fase di training uso degli stream in modo tale che se il batch N sta venendo computato, trasferisco il batch N+1 in GPU: From af2337fe226253b74b90990e648a6e2115852a70 Mon Sep 17 00:00:00 2001 From: LucaCorra02 Date: Mon, 23 Feb 2026 12:12:33 +0100 Subject: [PATCH 6/6] add example --- chapters/Lezione11.typ | 99 +++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 29 deletions(-) diff --git a/chapters/Lezione11.typ b/chapters/Lezione11.typ index 788c7b1..b384e2e 100644 --- a/chapters/Lezione11.typ +++ b/chapters/Lezione11.typ @@ -401,33 +401,74 @@ In PyTorch esistono diversi meccanismi di sincronizzazione con diversi livelli d ] -=== Esempio: Sovrapposizione di trasferimento e computazione - -Esempio in cui nella fase di training uso degli stream in modo tale che se il batch N sta venendo computato, trasferisco il batch N+1 in GPU: - -```py -stream_compute = torch.cuda.Stream() -stream_transfer = torch.cuda.Stream() - -for i, (inputs, targets) in enumerate(dataloader): - # Trasferisco il batch corrente su GPU in modo asincrono - with torch.cuda.stream(stream_transfer): - inputs_gpu = inputs.to('cuda', non_blocking=True) - targets_gpu = targets.to('cuda', non_blocking=True) - - # Attendo che il trasferimento sia completato - stream_compute.wait_stream(stream_transfer) - - # Eseguo il forward pass - with torch.cuda.stream(stream_compute): - output = model(inputs_gpu) - loss = criterion(output, targets_gpu) - loss.backward() -``` - -Ho un punto unico di uso dei dati (model). Devo sganciare solamente l'esecuzione del trasferimento su un altro stream, ne bastano due. - -In questo caso non possiamo elaborare un chunk di dati su uno stream in maniera indipendente. Qui non va bene in quanto il model deve consumare tutti i dati che passano. - -Intanto che il model elabora un chunk, parallelamente carichiamo in memoria GPU (asincrono) il successivo. +== Esempio: CUDA Prefetcher +#esempio()[ + In questo esempio viene costruito un `CUDAPrefetcher`. Questa tecnica permette di calcolare il batch $N$ sulla GPU nel mentre viene trasferito il batch $N+1$ sulla GPU. + + #nota()[ + L'esempio assume che il `DataLoader` originale sia configurate per lavorare sulla `pin_memory=True`. + ] + + Classe `CudaPrefetcher`: + ```python + import torch + class CUDAPrefetcher: + def __init__(self, loader, device): + self.loader = iter(loader) # DataLoader + self.device = device + # Stream dedicato alla copia + self.stream = torch.cuda.Stream(device=device) + self.next_batch = None + self._preload() # Caricamento del primo batch + + def _to_device(self, batch): + if isinstance(batch, (tuple, list)): + return [t.to(self.device, non_blocking=True) for t in batch] + return batch.to(self.device, non_blocking=True) + + """Preleva il prossimo batch dalla CPU e lo accoda per la copia sulla GPU.""" + def _preload(self): + try: + batch = next(self.loader) + except StopIteration: + self.next_batch = None + return + # Copia affidata allo stram + with torch.cuda.stream(self.stream): + self.next_batch = self._to_device(batch) + + def __iter__(self): + return self + + """Restituisce il batch corrente e avvia il prefetch del successivo.""" + def __next__(self): + if self.next_batch is None: + raise StopIteration + # SINCRONIZZAZIONE: Lo stream di default (calcolo) deve aspettare che lo stream di copia abbia finito + torch.cuda.current_stream(self.device).wait_stream(self.stream) + batch = self.next_batch + # Avviamo il trasferimento del prossimo batch in background + self._preload() + return batch + ``` + Il meotodo `__next__` viene utilizzato nel training loop. Quando il ciclo di training chiede il batch successivo, usiamo `wait_stream`. Lo stream di calcolo corrente si ferma solo se il batch *non* ha ancora finito di viaggiare sul cavo PCIe, garantendo che i dati siano pronti all'uso. Subito dopo, avvia il preloading del batch successivo e passa i dati correnti al training loop. + + Esempio di training Loop: + ```python + my_dataloader = DataLoader(dataset, batch_size=32, shuffle=True, pin_memory=True) + device = torch.device('cuda:0') + #Wrap del dataloader + prefetcher = CUDAPrefetcher(my_dataloader, device) + + for inputs, targets in prefetcher: + # A questo punto, 'inputs' e 'targets' sono GIA' sulla GPU. + # Non è necessario .to_device() + outputs = model(inputs) + loss = criterion(outputs, targets) + optimizer.zero_grad() + loss.backward() + optimizer.step() + ... + ``` +]