














Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Riassunto del corso di Architetture Avanzate di Nicola Bombieri
Tipologia: Appunti
1 / 22
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!















La Shared Memory può essere di due tipi diversi: UMA se ho processori identici e lo stesso tempo di accesso alla memoria; NUMA se ho la connessio- ne di 2+ Symmetric Multi Processors e il tempo di accesso non è lo stesso. I vantaggi della Shared Memory sono una facile programmazione alla memoria ed una condivisione dati tra task veloce ed uniforme, ma presenta anche svataggi tra i quali assenza di scalabilità, costo elevato ed il programmatore è responsa- bile della sincronizzazione. Nella Distributed Memory invece è richiesta una connessione di rete per connettere i vari sistemi processore-memorie, ma ogni processore ha la propri memoria ed opera indipendentemente, per cui ho vantaggi in termini di scala- bilità, interferenze e problemi di coerenza della cahe, ma con una più difficile mappatura dei dati e un modello NUMA.
Esistono 3 modi principali per trasformare un lavoro sequenziale in un lavoro parallelo:
Gli Hazards sono situazioni che prevengono l’esecuzione della prossima istru- zione nello stream di esecuzione nel designato ciclo di clock e possono essere di 3 tipi:
Lo SpeedUp della Pipeline è ora P ipelineDeath/(1+BranchF requency∗BranchP enalty). Ma allora cosa rende così difficile l’implementazione della Pipeline? Le inter- ruzioni, poichè un’istruzione completa il N cicli di clock ma questo è il minimo intervallo per lanciare un’interruzione, per cui bisogna avere la capacità di can- cellare un’istruzione mentre sta eseguendo. Queste interruzioni possono essere in più stadi della pipeline (IF, ID, EX, MEM), perciò l pipeline deve poter essere spenta in modo sicuro ed il PC deve essere salvato per ripartire da quel punto. Perciò quando si presenta un’eccezione:
Un’eccezione viene chiamata precise se lascia la macchina in uno stato consi- stente con il modello sequenziale di esecuzione; per essere tale tutte le istruzioni prima della fault sono completate, tutte quelle dopo non sono completate e la fault esegue correttamente.
Poichè è impossibile pretendere che tutte le operazioni completino in un ciclo di clock, si usa una pipeline modificata dove il ciclo EX viene ripetuto tante volte quante ne sono necessarie per completare l’operazione. Le unità funzionali separate sono quattro: integer unit, FP (Floating Point) e Integer multiplier, FP adder, FP e Integer divider. I possibili problemi sono che operazioni senza pipeline aumentano la possibi- lità di hazard strutturali, differenti latenze possono richiedere registri multi- pli su cui scrivere, le istruzioni finiscano in ordine diverso da quello previsto.
La predizione statica sul branch è fatta a tempo di compilazione, ma dicendo che un branch verrà preso abbiamo uno sbaglio che varia dal 59% al 9% (troppo). Quello dinamico (decisioni cambiano durante il tempo) viene fatto dall’hardware e si basa sul passato: sia F una funzione che esprime il risultato di una predizione su branch, siano x1..xn parametri che influenzano F, allora F(x1..xn) è preso se > 0.5. Più semplicemente, posso usare una tabella che salva la storia dei branch: durante IF accedo alla BHT(Branch History Table) per predire il branch e durante ID verifico se è un branch (implementata come una cache), di solito usa 2-bit per la predizione
L’HW riarrangia l’esecuzione delle istruzioni per ridurre gli stalli mantenendo il comportamento delle eccezioni e del flusso di dati. Permette alla CPU di tollerare ritardi impredicibili come mancanza di cache e di compilare codice che era pensato con una sola pipeline di funzionare efficaciemente in pipeline diffe- renti, ma non si può cambiare l’ordine delle istruzioni ordinate e non si sfrutta lo scheduling run-time -> dynamic scheduling! Nelle pipeline tradizionali, un hazard manda in stallo istruzioni che non sono affatte da tale hazard, per cui si cerca di completare questa operazione; facendo cosi però c’è il rischio di introdurre hazard di tipo WAR e WAW e problemi di imprecisioni degli errori. Distinguiamo l’inizio dell’esecuzione di un’istruzione dalla sua fine, per cui tra questi due tempi essa è in esecuzione. Assumiamo inoltre che la pipeline permet- te l’esecuzione multipla di più istruzioni in contemporanea e che il processore ha più unità funzionali -> dividiamo la fase ID in 2 stadi: Issue che decodifica istruzioni e controlla per hazard strutturali e Read Operands, che aspetta fino a che non ho più data hazard. poi legge gli operandi; uno stage IF precede lo stage Issue e può portare entrambi nell’IR oppure in una coda di istruzioni pendenti. Viene usato l’approccio di Tomasulo: evita WAR e WAW rinominando i regi- stri ed evita RAW eseguendo istruzioni solo quando gli operandi sono disponibili. Introduce le stazioni di prenotazione,che mandano gli operandi delle istru- zioni ad operare nell’Issue, eliminando il bisogno di prendere gli operandi dai registri (posso avere più stazioni di prenotazioni che registri reali). Ognuna ha 7 campi: Op (operazione da eseguire), Qj Qk (stazioni che producono l’ope- rando corrispondente), Vj Vk (valori operandi), A (info per calcolo memoria di store/load), Busy, Qi (numero di stazioni che contengono l’operazione il cui risultato deve essere salvato). Compie 3 passi base:
tutti gli operandi sono disponibili l’istruzione viene eseguita dalla corri- spondente unità funzionale
I vantaggi quindi di questo approccio sono che se un singolo risultato atteso da più istruzioni questo viene propagato e l’esecuzione riprende subito e che WAR e WAW sono risolvi semplicemente rinominando i registri
Sfruttare più parallelismo richiede che superiamo il limite delle dipendenze di controllo. Un’idea è quella di permettere alle istruzioni di eseguire non in or- dine ma forzarle a registrarsi in ordine e di prevenire ogni azione irrimediabile, separando il processo di esecuzione completata da quello di istruzione registrata usando un buffer di riordine (ROB): questo tiene i risultati di un’istruzione tra i due tempi.
l’host e chiamabile solo dall’host). Per compilare un programma CUDA si prende il codice scritto in C con le esten- sioni del CUDA, si usa il compilatore NVCC che compila il codice diversamente a seconda che sia Host o Device e poi il tutto viene rimesso assieme.
L’hardware è libero di assegnare i blocchi ad ogni processore quando vuole, men- tre un kernel scala attraverso qualsiasi numero di processori paralleli. Ogni bloc- co può eseguire in qualsiasi ordine relativamente agli altri blocchi. Ogni blocco viene eseguiro come un Warp a 32 thread. I thread sono assegnati a Streaming Multiprocessors (MP) in blocchi granulari e possono essere eseguiti concor- rentemente. I blocchi di thread sono partizionati in warp (che mantengono lo ID consistenti) e questo partizionamento è lo stesso sempre. Thread all’interno di un singolo warp possono prendere strade differenti e queste diverse esecuzioni sono serializzate nelle GPU correnti, quindi è da evitare -> if (threadIdx.x / WARPSIZE > 2) e non if (threadIdx.x > 2).
Ogni thread può leggere/scrivere: registri per-thread (1 ciclo), shared memory per-blocco (5 cicli), global memory per-grid (500 cicli). Le variabili senza qua- lificatore (shared o constant) risiedono in un registro, eccetto che per array di thread che stanno nella memoria globale. Le variabili vanno dichiarate in base alla possibilità dell’host di accedervi, per cui è globale se può ed è all’esterno di ogni funzione, è shared/local/automatica se non può ed è all’interno del kernel. Un modo vantaggioso di migliorare le performance è di partizionare dati in sot- toinsiemi che possono stare nella shared memory e gestire ognuno di questi con un blocco di thread caricandolo dalla global memory sulla shared memory e poi ripassarlo (buono quando i thread hanno lo stesso tempo di accesso). Per esempio con una lunghezza di 16, posso avere 1616 = 256 thread, che vuol dire 2256=512 load da global memory per 256216=8192 operazioni +/; quindi nella shared memory da 16KB, ogni blocco usa 2256*4B=2KB di shared me- mory, ciò vuole 8 potenziali blocchi attivi contemporaneamente. Per la sincronizzazione si usa __syncthreads(), che è una sorta di barriera dove tutti i blocchi dello stesso thread devono arrivare prima di poter prosegui- re. Si può interrogare il device chiedendone le proprietà tramite: cudaGetDevice- Count(&devcount) per il numero di device nel sistema, cudaGetDevicePro(&devprop), devprop.maxThreadsPerBlock, devprop.sharedMemoryPerBlock. La struttura tipica di un programma CuDA è dunque questa:
Per l’utente ha importanza il tempo di esecuzione (latenza), mentre per un ge- store interessa principalmente il throughput, ossia l’ammontare complessivo di lavoro svolto in un dato tempo (bandwith). Il tempo di esecuzione è il reciproco della prestazione, quindi T empoesecuzioney T empoesecuzionex =^
P restazionex P restazioney =^ 1 +^
n 100 Si deve favorire il caso più frequente a discapito del più raro e, per la legge di Amdahl, il miglioramento di prestazione che può essere ottenuto usando alcune modalità di esecuzione più veloci è limitato dalla frazione di tempo nella quale tali modalità possono venire impiegate. Lo speedup fornisce informazioni su quanto più velocemente un lavoro verrà eseguito usando la macchina con la mi- glioria rispetto alla macchina originale: speedup = tempoesecuzionenomiglioriatempoesecuzionesimigliora. Il tempo è la misura delle prestazioni di un computer, dove il tempo di risposta rappresenta la latenza per il completamento di un lavoro includendo accessi al disco, memoria e I/O, mentre il tempo di CPU corrisponde a cicli CPU per un programma * tempo CPU sistema, il CPI è il numero di cicli di clock diviso il numero di istruzioni. Il tempo di CPU dipende dal ciclo di clock (tecnologia HW ed organizzazione), dal ciclo di clock per istruzione (organizzazione ed ar- chitettura set istruzioni) e dal numero di istruzioni (architettura set istruzioni e tecnologia compilatori).
cudaMemcpy serializza il traferimento dei dati e quindi anche la computazio- ne della GPU poichè usa una sola direzione di svolgimento. Ma alcuni device CUDA supportano il device overlap, ossia l’esecuzione simultanea di kernel mentre è in corso una processo di copiatura tra device e host. CUDA supporta esecuzione parallela di kernel e cudaMemcpy con Streams, ossia code di operazioni (lanco di kernel e cudaMemcpy), dove operazioni (task)
OpenCl è un acronimo per "Open Computin Language", che sta ad indicare la sua natura aperta e senza royalty, permette la programmazione parallela tra più piattaforme e può usare tutte le risorse di computazione (CPU, GPU, .. ). Consiste di 3 componenti principali:
Rispetto a CUDA non supporta certe funzioni hardware specifiche a favore della portabilità, permette la compilazione run-time ma non la texture memory. Di simile hanno i modelli e la sintassi.
La cache è il nome dato al primo livello di gerarchia della memoria incontrato quando l’indirizzo lascia il processore. Posso avere cache hit e cache miss; il tempo richiesto per il cache miss dipende dalla latenza (tempo di trovare la prima parola) e dalla bandwidth (tempo di trovare il blocco) della memoria.
Posso avere 3 modalità di piazzamento del blocco nella cache:
In generale, un indirizzo è partizionato in t bits per il TAG, s bits per il SET INDEX e b bits per l’OFFSET, che vuol dire che la cache consiste in 2 s^ classi equivalenti, ogni set consiste in t entries e che ogni entry consiste in 2 b^ bytes. Il dato comunque viene validato attraverso un valid bit finale.
Con la Direct Mapping, viene verificato solo un blocco per hit e quindi posso sostituire solo quello; con le altre posso usare politiche Random, LRU o FIFO.
Due possibili scelte di scrittura: write through con la quale l’informazione viene scritta sia in cache che in memoria per avere consistenza, oppure write back con la quale l’informazione viene scritta solo nella cache e modifico il blocco scritto in memoria solo quando nella cache viene rimpiazzato. Con un write miss, posso decidere di usare due strategie: write allocate per cui write miss corrisponde a read miss, oppure no-write allocate che non carica in cache ma modifica il blocco nel livello più basso
Il tempo di accesso alla memoria medio AMAT = Hit time + (Miss Rate x Miss Penalty), dove Hit Time è il tempo di accedere alla cache e trovare il dato, la Miss Rate è la possibilità di fare miss e il Miss Penalty è quanto mi costa il Miss. Per migliorare le performance si possono seguire 3 strade: ridurre il miss rate, ridurre il miss penalty oppure ridurre il time hit. I miss che si possono avere sono: Compulsory (all’inizio quando ancora non ho niente nella cache), Capacity (oltre tot informazioni non posso salvarle), Conflict (un blocco può essere rimosso e poi rimesso).
La memoria virtuale è quella vista dal programma, mentre quella fisica è quella vista dalla memoria, con necessità di traduzione da una all’altra fatta per ogni accesso in memoria tramite paging o segmentatio.