Università degli Studi di Napoli Federico II Facoltà di Scienze MM.FF.NN. Corso di Laurea in Informatica Tesi sperimentale di Laurea Triennale Sviluppo di un codice parallelo su GPU per la ricostruzione di particelle: Analisi dei tempi di esecuzione e confronto con codice sequenziale Relatori Candidato Prof. Guido Russo Dr. Guglielmo De Nardo Dr. Silvio Pardi Luigi Perillo matr. 566/2699 Anno Accademico 2011-2012 Ai miei genitori, che con mille sacrifici e rinunce mi hanno permesso di studiare e portare a termine questo percorso. A mio fratello Vincenzo mia sorella Rachele ad Ignazio e Marianna, che hanno sempre avuto parole confortevoli e hanno sempre creduto in me. Ai miei nipotini, che mi hanno regalato a loro insaputa tanti sorrisi nei periodi difficili. Ai pochi amici sinceri, presenze costanti nelle mie giornate. Al mio miglior amico Felice, capace di supportarmi e sopportarmi anche a chilometri di distanza. ...un semplice grazie che racchiude tutto il bene che provo per voi ma che non so dimostrarvi. Luigi Perillo 566/2699 Pagina 2 di 110 Indice generale Introduzione...............................................................................................................7 1) Il Progetto SuperB.................................................................................................9 1.1) L’acceleratore di Particelle..............................................................................10 2) Le architetture GPU nel calcolo ad alte prestazioni.............................................13 2.1) Architetture GPU e paradigma GPGPU.......................................................14 2.2) Confronto tra architetture CPU e GPU.........................................................15 2.3) nVIDIA Tesla S2050 Computing System........................................................19 3) Ambiente di programmazione: CUDA C.............................................................24 3.1) Primi passi in CUDA...................................................................................24 3.2) Il Kernel.......................................................................................................27 3.3) Tipi e funzioni per la Gestione Memorie in CUDA......................................29 3.4) Funzioni.......................................................................................................35 3.5) Operazioni Atomiche...................................................................................37 3.6) Funzioni di Errore........................................................................................37 3.7) Funzioni di Eventi........................................................................................38 3.8) Compilazione CUDA C................................................................................39 4) Il problema della ricostruzione di eventi.............................................................41 4.1) Algoritmo per la ricostruzione di eventi.......................................................41 4.2) Strutture dati utilizzate.................................................................................43 4.3) Gestione dati reali per Pioni Π+, Π- e Gamma.............................................45 4.4) Descrizione strategia sequenziale.................................................................47 4.4.1) Sequenziale modulo D0(k+Π-Π0; k-Π+Π0) ricostruito.......................47 Luigi Perillo 566/2699 Pagina 3 di 110 4.4.2) Sequenziale modulo D0(k+Π-Π-Π+; k-Π+Π+Π-) ricostruito..............49 4.5) Descrizione strategia parallela .....................................................................52 4.5.1) Parallelo modulo D0(k+Π-Π0; k-Π+Π0) ricostruito.............................54 4.5.2)Parallelo modulo D0(k+Π-Π-Π+; k-Π+Π+Π-) ricostruito.....................56 5) Test codice e valutazione architettura GPU.........................................................59 5.1) Test prestazioni della funzione Kernel.........................................................61 5.2) Test esecuzione funzione D0(k+Π-Π0; k-Π+Π0) ........................................63 5.3) Test esecuzione funzione D0(k+Π-Π-Π+; k-Π+Π+Π-)................................65 5.4) Test sull'analisi dei tempi di esecuzione degli algoritmi ............................67 Conclusioni.............................................................................................................. 70 Appendice................................................................................................................72 A.1. Codice versione sequenziale........................................................................73 A.1.1. prototype.h ..........................................................................................73 A.1.2. function.c .............................................................................................75 A.1.2. main.c ..................................................................................................80 A.2. Codice versione parallela.............................................................................85 A.2.1. prototype.h ..........................................................................................85 A.2.2. cudaprototype.h....................................................................................86 A.2.3. function.c .............................................................................................89 A.2.4. cudafunction.cu ...................................................................................90 A.2.5. main.cu ..............................................................................................101 Bibliografia............................................................................................................110 Luigi Perillo 566/2699 Pagina 4 di 110 Indice delle illustrazioni Illustrazione 1: Sede acceleratore SuperB................................................................10 Illustrazione 2: Ambiente d'esperimento SuperB.....................................................11 Illustrazione 3: Confronto tra CPU e GPU in termini di floating-point...................16 Illustrazione 4: Schemi strutture CPU e GPU a confronto.......................................17 Illustrazione 5: CPU e GPU a confronto in termini di bandwidth............................18 Illustrazione 6: nVidia Tesla S2050.........................................................................19 Illustrazione 7: Architettura GPU nVidia Fermi......................................................20 Illustrazione 8: Architettura Tesla S2050.................................................................21 Illustrazione 9: Cavo PCI Express x16....................................................................22 Illustrazione 10: Connessione di un sistema Tesla a due Host.................................23 Illustrazione 11: Scalabilità automatica programmi CUDA.....................................25 Illustrazione 12: Schema di un programma CUDA C..............................................26 Illustrazione 13: Struttura griglia, blocchi e thread..................................................28 Illustrazione 14: Gerarchia di accesso alle memorie................................................29 Illustrazione 15: Grafico dei tempi di esecuzione funzione CudaMalloc()..............32 Illustrazione 16: Grafico dei tempi di esecuzione funzione cudaMemcpy()............34 Illustrazione 17: Compilazione di codice scritto in linguaggio CUDA C................40 Illustrazione 18: Stadi della ricostruzione completa del mesone B..........................42 Illustrazione 19: Snapshot di esempio del file di output dopo l'esecuzione di un evento...................................................................................................................... 45 Illustrazione 20: Snapshot di esempio del file di output dopo l'esecuzione di un evento .....................................................................................................................46 Illustrazione 21: Diagramma suddivisione lavoro CPU GPU..................................53 Illustrazione 22: Server Dell PowerEdge R510.......................................................60 Illustrazione 23: Grafico dei tempi di esecuzione della funzione Kernel.................62 Luigi Perillo 566/2699 Pagina 5 di 110 Illustrazione 24: Grafico dei tempi di esecuzione funzione D0(k+Π-Π0; k-Π+Π0) 64 Illustrazione 25: Grafico dei tempi di esecuzione funzione D0(k+Π-Π-Π+; kΠ+Π+Π-).................................................................................................................66 Illustrazione 26: Grafico tempi di esecuzione degli algoritmi su numero variabile di eventi....................................................................................................................... 69 Indice delle tabelle Tabella 1: Tempi funzione cudaMalloc().................................................................32 Tabella 2: Tempi di esecuzione funzione cudaMemcpy()........................................33 Tabella 3: Tempi di esecuzione della funzione Kernel espressi in millisecondi.......61 Tabella 4: Tempi di esecuzione funzione createD0KPIPI0() e ScreateD0KPIPI0() espressi in millisecondi............................................................................................63 Tabella 5: Tempi di esecuzione funzioni createD0K3PI() e ScreateD0K3PI() espressi in millisecondi............................................................................................65 Tabella 6: Tempi di esecuzione degli algoritmi su numero variabile di eventi.........68 Luigi Perillo 566/2699 Pagina 6 di 110 Introduzione Il suddetto lavoro di tesi si propone di illustrare le attività di tirocinio, svolte presso il data center SCoPE dell’Università degli Studi di Napoli “Federico II”, con l’obbiettivo principale di valutare le architetture GPGPU (General Purpose Graphic Processing Unit), all’interno del progetto SuperB, nell’ambito del calcolo scientifico ad alte prestazioni (High Performance Computing). Nelle pagine che seguono, si è cercato di capire quali sono i vantaggi, nell’utilizzare le GPU per scopi differenti dalla grafica virtuale, mediante l’uso di una tecnologia di nuova generazione sviluppata dalla nVIDIA, ovvero l’architettura CUDA (Compute Unified Device Architecture). Per la valutazione e il testing di questa tecnologia, è stato scritto un algoritmo parallelo per la ricostruzione dei decadimenti dei mesoni B, ovvero per la ricostruzione di particelle subatomiche. Va sottolineato che attualmente esistono già applicazioni che, su grosse quantità di input, attraverso acceleratori quali BaBar e LHC, studiano il processo di ricostruzione dei decadimenti delle particelle del mesone B. Questi software devono essere di precisione altissima, in quanto valutano tutte le possibile combinazioni di decadimento avvenute; implicando una gran quantità di lavoro, nella ripetitività delle operazioni da svolgere su un numero molto grande di dati. La tesi si snoda nei seguenti capitoli: Capitolo 1: Propone una panoramica sul Progetto SuperB, illustrando brevemente l’entità dell’acceleratore di particelle e le esigenze di calcolo derivanti dall’analisi dei risultati degli esperimenti effettuati. Capitolo 2: Mostra l’architettura GPU, nel calcolo ad alte prestazioni, soffermandosi sul nuovo modello GPGPU; si propone un confronto tra l’utilizzo nel calcolo computazionale ad alte prestazioni dell’architettura GPU rispetto alle CPU tradizionali. Luigi Perillo 566/2699 Pagina 7 di 110 Capitolo 3: Illustra l’ambiente di lavoro CUDA, con le principali caratteristiche, soffermandosi principalmente sul linguaggio C. Propone una panoramica generale, delle prime istruzioni per scrivere un listato da parallelizzare, descrivendo la struttura del codice e le variabili di sistema principalmente utilizzate. Capitolo 4: Fornisce una visione dettagliata dei moduli dell'algoritmo seriale e parallelo sviluppato. Definisce gli indici e i cicli sviluppati per le varie versioni proposte, e la suddivisione del carico di lavoro tra i diversi threads dei blocchi per la versione parallela. È inoltre riportata la metodologia usata, per la gestione dei dati reali in input al sistema. Capitolo 5: Fornisce una rappresentazione ed un'analisi accurata delle prestazioni dei moduli e dell'algoritmo sviluppato ed eseguito su una GPU, confrontando questi con una normale versione seriale eseguita su CPU. Appendice: Fornisce le informazioni sui listati sviluppati, le modalità utilizzate per la scrittura e la prevenzione agli errori in fase di scrittura ed infine i codici delle funzioni sviluppate, sia nella versione seriale che in quella parallela. Luigi Perillo 566/2699 Pagina 8 di 110 1) Il Progetto SuperB Il progetto SuperB proposto dall’Istituto Nazionale di Fisica Nucleare (INFN), ha come obiettivo principe la costruzione di un nuovo acceleratore di particelle a base italiana e a partecipazione planetaria, che il Ministero dell’Istruzione Università e Ricerca ha deciso di valorizzare e finanziare come progetto bandiera per l'Italia. Quest’ultimo sarà situato nell’area dell’Università degli Studi di Roma, sito in Tor Vergata. L’esperimento avrà come punto cardine lo studio degli eventi naturali, grazie ai principi della fisica quantistica, con possibilità di studiare le collisioni della materia e l’anti-materia così come avvenuto 13,7 milioni di anni fa, ossia all’origine dell’universo, con l’esplosione del Big Bang. Il progetto SuperB sarà basato su idee sviluppate e sperimentate dalla divisione degli acceleratori dei Laboratori Nazionali di Frascati dell’INFN, il tutto con un’intensità 100 volte superiore, rispetto a quelle raggiunte finora dagli altri acceleratori posti nel mondo. Le collisioni fra materia e anti-materia fornirà nuovi indizi sulle questioni fondamentali della fisica moderna, e cioè: 1. origine dell’universo a partire dal Big Bang; 2. origine della massa; 3. evoluzione dell’universo fino allo stato attuale; 4. natura ed evoluzione della materia oscura. Quest’acceleratore come detto in precedenza sarà situato nei pressi dell’Università di Roma, in Tor Vergata. La scelta dell’area geografica laziale è da attribuire a tre fattori principali: • Un’enorme area priva di reperti archeologici. • Distanza non eccessiva (3Km) dai laboratori LNF di Frascati. Luigi Perillo 566/2699 Pagina 9 di 110 • Area geografica priva di vibrazioni rilevanti. Illustrazione 1: Sede acceleratore SuperB. 1.1) L’acceleratore di Particelle La fase sperimentale e progettuale del SuperB, richiede lo studio e l’analisi di una grossa mole di dati d’input, oltre alle informazioni riguardanti le collisioni che si verificano. Luigi Perillo 566/2699 Pagina 10 di 110 Illustrazione 2: Ambiente d'esperimento SuperB. L’esperimento sarà complementare, in termini di informazioni prodotte, a quelle sviluppate presso il CERN con LHC (Large Hadron Collider) ed, in futuro con ILC (International Linear Collider). LHC è anch’esso un acceleratore di particelle ed è utilizzato per ricerche sperimentali nel campo della fisica delle particelle. ILC invece è ancora in fase di realizzazione, il suo lavoro sarà complementare a quello dell’LHC. La cooperazione tra LHC e SuperB sarà vista nel seguente modo, secondo la teoria della Nuova Fisica chiamata “Supersimmetria”, LHC produrrà delle particelle super-simmetriche fino ad una massa collegata alla massima energia raggiungibile con l’acceleratore. SuperB invece valuterà i contributi virtuali di tali particelle andando a sfruttare le fluttuazioni quantistiche derivanti dal principio d’indeterminazione; portando cosi la sensibilità di SuperB ben oltre l’energia raggiungibile con LHC. Come detto il nuovo acceleratore SuperB, si differenzierà da quelli già esistenti, per la mole di informazioni trattate, avvicinandosi a ben più potenti acceleratori esistenti, quali ATLAS e CMS, con sede al CERN di Ginevra. Luigi Perillo 566/2699 Pagina 11 di 110 Ad oggi si prevede che, per ogni anno di attività, ci sarà bisogno di risorse computazionali, decisamente superiori a quelle richieste dagli esperimenti svolti al CERN, con ordine di grandezza quasi duplicate. Con una previsione di mole di lavoro che si dovrebbe aggirare intorno a: • 1700 KHep-Spec06, di potenza computazionale, • 50 PB annui, per il data storage. A grandi linee, l’acceleratore SuperB ha lo stesso funzionamento e la stessa componentistica degli altri acceleratori per la ricerca delle collisioni. La componentistica principale del SuperB è caratterizzata da: • Iniettore lineare (LINAC). • Due anelli. • Un accumulatore. Il primo elemento, l'iniettore lineare LINAC dove sono prodotti cinquanta miliardi di particelle elementari in un secondo, cioè gli elettroni e i positroni. Il LINAC, inietta con una frequenza di 50Hz, i pacchetti prodotti dalle collisioni, nell’anello chiamato Accumulatore, nelle quale sono fatti ruotare, per far perdere loro energia, sotto forma di luce e consentire la compattazione delle particelle. Infine trasferiti in due anelli, che li fanno viaggiare in direzione opposte, facendole scontrare in un punto (punto di collisione), nel quale è situato un rilevatore, che acquisisce e analizza le informazioni prodotte dalle collisioni delle particelle. Nei due anelli di collisione, continua il processo di emissione di luce, qui le particelle riacquistano l’energia persa in precedenza, grazie a delle cavità a radiofrequenza. La luce di sincrotrone prodotta da SuperB (luminosita > 1036cm-2s-1) ha delle caratteristiche molto vicine a quelle dei più innovativi e potenti acceleratori dedicati alla produzione della sola luce. Questo rende SuperB, un progetto capace di produrre eventi da studiare, in frontiere Luigi Perillo 566/2699 Pagina 12 di 110 ancora inesplorate, nel mondo delle particelle subatomiche. 2) Le architetture GPU nel calcolo ad alte prestazioni Come illustrato nel capitolo precedente, la grossa mole di calcolo, dovuta alla sperimentazione, del progetto SuperB, richiede un’ingente potenza di calcolo. Per l’evoluzione di tale progetto saranno messe a disposizione circa 10.000 CPU su base multi-core, interconnesse tra di loro, da Infiniband a 10Gbit/s. Presso il data center SCoPE dell’Universita degli Studi di Napoli “Federico II”, ad esempio, si trovano armadietti rack di espansione cablati, raffreddati a liquido, già pronti per essere messi in funzione, gestiti e monitorati da un Control Room di altissima affidabilità. Vista l’ingente quantità di calcolo, cui saranno sottoposte le CPU, per far fronte al lavoro quotidiano nel progetto, si è alla ricerca di alternative valide, per aumentare la potenza di calcolo e diminuire il tempo di computazione dei risultati previsti. Ecco quindi che negli ultimi anni, sta prendendo sempre più strada, l’utilizzo dell’architettura GPU come coprocessore matematico. Esse garantiscono una potenza di calcolo notevole, massively parallel, consentendo un consumo ridotto di energia e una poca dissipazione di calore, con conseguente miglioramento delle prestazioni di calcolo e poca usura delle architetture. Luigi Perillo 566/2699 Pagina 13 di 110 2.1) Architetture GPU e paradigma GPGPU Con l’acronimo GPGPU intendiamo General-Purpose Computing on Graphics Processing Units; ovvero prendere un’unità prettamente grafica (le GPU poste nelle schede grafiche), ed espandere il proprio uso oltre l’utilizzo standard. L’esecuzione di software General-Purpose (uso generale) su un’architettura da 1 TeraFLOPS (GPU), anziché da 100 GigaFLOPS (CPU), consente un guadagno, oltre che nell’ordine di tempo, anche nei costi di produzione e nel risparmio energetico. Tuttavia anche le GPU, presentano delle limitazioni, che rendono comunque impossibile il loro utilizzo esclusivo, a discapito delle CPU. Le GPU, sono dotate di un numero elevato di core, ma con poca potenza di calcolo, il che consente di distribuire il carico di lavoro in parallelo, sfruttando più core contemporaneamente. Le CPU, invece, sono strutturate con un numero di core limitato (nell’ordine della decina), dotata ognuna, di una potenza di calcolo nettamente superiore rispetto ai core delle GPU. Il modello GPGPU, si propone quindi di usare, la potenza di calcolo parallela dei core GPU, producendo software parallelizzati, al fine di massimizzare l’efficienza delle GPU. L’utilizzo del modello GPGPU, trova per ora pochi sbocchi applicativi. In prevalenza in questo momento, è utilizzato maggiormente, in ambiente grafico nel calcolo scientifico. Anche i software in grado di sfruttare le GPU sono limitati tutt’ora, ma in progressivo aumento. Alcuni esempi sono: il sistema di finestre Windows Aero (su Windows 7), il motore di rendering di Internet Explorer 9 e Firefox 4, la codifica e la decodifica video di molti software di montaggio e riproduzione video, software di ricerca password per archivi compressi e software per audio digitale. Alla base del parallelismo delle GPU, vi è il modello SIMT (Single Instruction Luigi Perillo 566/2699 Pagina 14 di 110 Multiple Thread), modello nel quale le unità fondamentali, risultano i thread eseguiti dallo Streaming Processor. Questo modello prevede che gli Streaming Multiprocessor che lavorano con i thread, li creino, li gestiscano e li schedulino in gruppi di 32 thread paralleli chiamati warp. Questi thread partono contemporaneamente dallo stesso indirizzo di programma, ma ognuno ha il proprio Instruction Address Counter, ed il proprio registro di stato, in modo da lavorare indipendentemente. I warp sono strutturati in modo da eseguire un’istruzione comune per volta, in modo da massimizzare l’efficienza di tutti i thread che compongono un warp sullo stesso path di esecuzione. Se i path da eseguire sono differenti, sono eseguiti in seriale, disabilitando i thread che non lavorano su quello specifico path, quando infine tutti i path sono stati completati, i thread convergono nuovamente sullo stesso path esecutivo. 2.2) Confronto tra architetture CPU e GPU I processori basati su singola unità di calcolo (CPU), nel giro di pochi decenni hanno subito un notevole sviluppo, in termini di prestazione e rapporto qualità prezzo. Tuttavia a causa di problemi riscontrati nello sviluppo forzato delle prestazioni, si è dovuta trovare una strada alternativa, per consentire alle CPU di svolgere il loro compito senza una gran dissipazione del calore, limitando quindi l’aumento della frequenza di clock, nelle attività di calcolo. Da qui nasce l’esigenza di usare l’architettura GPU, fino a quel momento sfruttata solo in ambito grafico. La differenza sostanziale tra CPU e GPU, si trova nella composizione hardware che le contraddistingue; ad oggi le tecniche di sviluppo principalmente usate sono: 1. Multi-Core 2. Many-Core. Luigi Perillo 566/2699 Pagina 15 di 110 I multi-core, sono sviluppati in modo da cercare di mantenere la velocità di esecuzione di programmi sequenziali, eseguendoli su core multipli. Un esempio sono i processori multi-core, quad-core della famiglia Intel. I many-core concentrano il oro sviluppo sul throughput di esecuzione di applicazioni parallele per unità di tempo. Un esempio è dato dalla famiglia delle schede video nVIDIA GeForce dalla serie 8 in poi. Con queste premesse, di conseguenza lo sviluppo della potenza di calcolo delle CPU ha ricevuto, un notevole rallentamento, a differenza delle GPU, che continua ad avere miglioramenti, tanto che ormai il rapporto tra many-core e multi-core per quanto riguarda il throughput del calcolo floating-point è di circa 10 a 1, e costantemente tutt’ora in aumento. Illustrazione 3: Confronto tra CPU e GPU in termini di floating-point. Le motivazioni di tale divario tra le CPU e l’architettura GPU, risiede sostanzialmente, nel fatto che l’architettura CPU è ottimizzata, nel aumentare le prestazioni di codice sequenziale. Le CPU sono costituite da strutture logico-aritmetiche (ALU) più evolute e da una Luigi Perillo 566/2699 Pagina 16 di 110 cache memory capace di ridurre la latenza di accesso ai dati e alle istruzioni da parte delle applicazioni. Le GPU invece sono caratterizzate da larghezze di banda superiore a quella delle CPU, di circa dieci volte. La larghezza di banda è alla base delle filosofie progettuali delle GPU, cioè modelli più semplici e con meno vincoli. Illustrazione 4: Schemi strutture CPU e GPU a confronto. Di conseguenza, dalle definizioni date, è chiaro che l’utilizzo delle GPU nel calcolo numerico, diventa imprescindibile e con il passare del tempo e con successive evoluzioni future, sarà impossibile non sfruttare la loro potenza di calcolo parallelo nell’ambito scientifico. Ciò però non esclude la duttilità e l’utilizzo delle CPU che consentono, come visto, un gran mezzo per il calcolo sequenziale. Lo scopo e l’obiettivo finale, quindi sarà quello di un uso combinato di entrambe le architetture, suddividendo i compiti a seconda delle sezioni di codice sequenziali o parallele. Questo è il motivo principale che ha indotto la nVIDIA, ad introdurre un nuovo modello computazionale, CUDA (Compute Unified Device Architecture), cioè Luigi Perillo 566/2699 Pagina 17 di 110 supportare la collaborazione tra CPU e GPU nell’esecuzione di applicativi software. Illustrazione 5: CPU e GPU a confronto in termini di bandwidth. Luigi Perillo 566/2699 Pagina 18 di 110 2.3) nVIDIA Tesla S2050 Computing System Negli ultimi anni, i maggiori produttori di chip grafici, si sono specializzati nella realizzazione di hardware dedicato al calcolo scientifico. Un esempio è rappresentato dalla serie TESLA della famiglia nVIDIA. Una soluzione che prevede l’implementazione di più schede video ottimizzate per il GPGPU computing, utilizzando nVIDIA CUDA. La soluzione adottata dal data center SCoPE, è quella di utilizzare due server equipaggiati con una NVIDIA Tesla S2050, da destinare al progetto SuperB. L’nVIDIA tesla S2050 si presenta come una chassis di dimensione 4,40cm X 44,45cm X 72,39 cm, il tutto predisposto per struttura da 19”. Illustrazione 6: nVidia Tesla S2050. La struttura interna contiene, quattro GPU nVIDIA Fermi, ciascuna con le seguenti caratteristiche tecniche: • 14 Streaming Multiprocessor (SM); • 32 core per SM per un totale di 448 core; Luigi Perillo 566/2699 Pagina 19 di 110 • processor core clock: 1.15 Ghz; • memory clock: 1.546 Ghz; • L2 cache da 768 KB; • 3 GB di memoria GDDR5; • shared memory configurable (48 KB o 16 KB); • spazio di indirizzamento a 64 bit; • 16 fused multiply-add operations a doppia precisione per SM ad ogni ciclo di lock; Illustrazione 7: Architettura GPU nVidia Fermi Luigi Perillo 566/2699 Pagina 20 di 110 L'intero sistema nVIDIA Tesla S2050 ha le seguenti caratteristiche: • 4 GPU NVIDIA con architettura Fermi • 12 GB di memoria DRAM di tipo GDDR5 • Consumo di corrente pari 900W Illustrazione 8: Architettura Tesla S2050. La connessione del sistema ad un host avviene tramite cavo PCI Express. Luigi Perillo 566/2699 Pagina 21 di 110 Illustrazione 9: Cavo PCI Express x16 Ogni cavo PCI Express collega due delle quattro GPU, l'intero sistema (4 GPU) può essere collegato ad uno oppure due host. La Tesla S2050 connessa a due sistemi host, può accedere soltanto a due GPU ognuno. Nella figura che segue è mostrato lo schema di collegamento dell'intero sistema a due host, mediante due cavi PCI Express x16. Luigi Perillo 566/2699 Pagina 22 di 110 Illustrazione 10: Connessione di un sistema Tesla a due Host. Luigi Perillo 566/2699 Pagina 23 di 110 3) Ambiente di programmazione: CUDA C Come si è detto nei capitoli precedenti, negli ultimi anni, il GPU Computing, sta assumendo sempre maggiore importanza, soprattutto nel settore del calcolo scientifico. Tutto ciò, attraverso la collaborazione tra CPU e GPU, nel modello di programmazione parallela adottato dalla nVIDIA, cioè CUDA. Il modello CUDA è utilizzabile, in diversi linguaggi di programmazione tradizionali: C, C++, Fortran; applicando delle opportune estensioni. Ai fini della tesi, per la scrittura del codice, è stato utilizzato il linguaggio C, con l’estensione CUDA. A questo punto è necessario comprendere tutte le componenti che entrano in gioco nello sviluppo di un programma, al fine di massimizzare lo sfruttamento della potenza di calcolo che le moderne GPU mettono a disposizione. 3.1) Primi passi in CUDA CUDA è costituito da: un modello architetturale parallelo general purpose, un insieme di Application Programming Interface (API) per sfruttare tale architettura, ed infine una serie di estensioni al linguaggio C per descrivere algoritmi paralleli in grado di girare sulle GPU che adottano questo modello. Quanto segue, sarà una panoramica generale introduttiva, per iniziare a conoscere, e a muovere i primi passi, nello sviluppo di codice. La struttura interna di questo modello, è costituita da tre componenti principali: • Gerarchia di gruppi di Threads. • Memorie Condivise. • Barriere di sincronizzazione. Queste componenti, guidano il programmatore, a strutturare e partizionare il lavoro in sotto problemi, rendendo il codice scalabile e indipendente dal numero di blocchi Luigi Perillo 566/2699 Pagina 24 di 110 di threads. Ogni sotto problema a sua volta, è ulteriormente suddiviso in altri sotto problemi che possono essere risolti in parallelo da tutti i threads all'interno del medesimo blocco. Proprio la suddivisione del problema in blocchi, affidati in fase di esecuzione ai diversi gruppi di thread, assicura la scalabilità automatica. Ogni blocco di threads può essere schedulato su uno qualsiasi degli Streaming Multiprocessor della GPU, sia parallelamente o sequenzialmente, così che un programma compilato in CUDA possa essere eseguito su un numero generico di cores, lasciando alla fase di runtime il compito di riconoscere l'effettivo numero (fisico) di cores in dotazione. In sostanza un programma multi-thread è partizionato in blocchi di threads che eseguono indipendentemente gli uni dagli altri, così che una GPU con più core automaticamente eseguirà il programma in meno tempo, di una GPU che ha meno core a disposizione; ciò è illustrato in figura 11. Illustrazione 11: Scalabilità automatica programmi CUDA. La scrittura di un programma in CUDA C è composta di parti di codice che vengono processate normalmente dalla CPU (host) e da altre che vengono gestite dalla GPU Luigi Perillo 566/2699 Pagina 25 di 110 (device). Queste parti di codice, Host e Device accedono a memorie differenti e la gestione è trattata dalla CPU. La parte di codice dell’host, è strutturata principalmente in tre punti: • Allocazione della memoria del device. • Trasferimento da e per la memoria del device. • Stampa dei risultati. È facile intuire che le parti di codice seriali, si trovano all’interno delle CPU, mentre quelle parallele in GPU. La GPU è vista come un coprocessore per la CPU, ha una propria memoria (la device memory) ed esegue molti threads in parallelo. Proprio questa caratteristica, garantisce le prestazioni elevate e il loro utilizzo a discapito dell’uso esclusivo delle CPU. Questi thread sono utilizzati sul device, tramite funzioni particolari, chiamate Kernel. La particolarità di queste funzioni è che, una volta chiamate, sono eseguite ripetutamente in parallelo, secondo il numero di threads a disposizione del device; tutto ciò a differenza delle normali funzioni C che sono eseguite una sola volta. Illustrazione 12: Schema di un programma CUDA C. Luigi Perillo 566/2699 Pagina 26 di 110 3.2) Il Kernel Le funzioni Kernel sono definite usando la dichiarazione __global__ seguita dal numero di threads utilizzati per l’esecuzione; la sintassi utilizzata è la seguente: <<<numero_di_blocchi,numero_di_threads_per_blocco>>>. Ad ogni thread che esegue nel Kernel, sarà associato un unico thread-Id univoco, accessibile nel Kernel tramite la variabile threadIdx. Quest’ultima è in realtà un vettore a tre dimensioni di un nuovo tipo introdotto da CUDA C, cioè dim3. È utilizzata per identificare, con più naturalezza, elementi quali vettori, matrici o volumi. Con quest’ultima si può accedere alle varie dimensioni aggiungendo a threadIdx “.x”, “.y” o “.z”. Come esempio, è posto un listato, che mostra quanto detto finora. Questo esempio, esegue la somma di due vettori A e B di dimensione N, con risultato posto in un nuovo vettore chiamato Somma. //Definizione funzione Kernel_Somma __global__ void Kernel_Somma (float *A, float *B, float *Somma){ int i = threadIdx.x; Somma[i] = A[i] + B[i]; } //Funzione Main int main() { … //chiamata alla funzione Kernel_Somma utilizzando N threads Kernel_Somma <<< 1, N >>> (A, B, Somma);} Il listato proposto, produce in output un array (Somma), in cui ciascuno degli N thread del singolo blocco utilizzato, produce una somma delle componenti. Questi kernel sono eseguiti su una griglia, composta da blocchi di thread. Queste Luigi Perillo 566/2699 Pagina 27 di 110 griglie possono assumere dimensione massima di 65535 blocchi e possono essere o monodimensionali o bidimensionali per un totale di blocchi in questo caso di 4294836225 (65535 blocchi X 65535blocchi). A sua volta ogni blocco della griglia può assumere forma monodimensionale, bidimensionale o tridimensionale, con un totale di 1024 thread sulle tre dimensioni. Segue quindi che configurazioni del tipo: (32, 16, 2) sono valide, mentre (32, 16, 16) non sono consentite. I Illustrazione 13: Struttura griglia, blocchi e thread. l numero di threads per blocco, ed il numero di blocchi per griglia possono essere di tipo int oppure dim3. La dimensione del blocco è accessibile all'interno del Kernel attraverso la variabile di sistema blockDim. I diversi threads all'interno di un blocco, possono cooperare mediante la condivisione di dati, attraverso una memoria condivisa e sincronizzando la loro esecuzione al fine di coordinare gli accessi in tale memoria. E’ possibile specificare all’interno del listato del kernel, delle barriere di Luigi Perillo 566/2699 Pagina 28 di 110 sincronizzazione, attraverso la sintassi: __synchthreads(). Questa barriere fa si che tutti i thread dello stesso blocco, siano bloccati, finche i restanti threads del medesimo blocco non arrivino alla barriera. I threads CUDA, durante l’esecuzione, possono accedere a dati collocati in diversi spazi di memoria, seguendo una ben specifica gerarchia. 3.3) Tipi e funzioni per la Gestione Memorie in CUDA Ogni singolo thread, ha una memoria locale (local memory) privata. Ogni blocco ha una shared memory visibile a tutti i threads del blocco stesso (questa memoria ha stessa durata dei thread). Infine tutti i threads hanno accesso alla medesima memoria globale (global memory). Ci sono inoltre due ulteriori spazi di memoria a sola lettura accessibili da tutti i threads: la constant memory e la texture memory. Illustrazione 14: Gerarchia di accesso alle memorie. Luigi Perillo 566/2699 Pagina 29 di 110 La global memory, e le due memorie appena descritte di sola lettura sono spazi di memorie persistenti per Kernel lanciati nella stessa applicazione. CUDA fornisce delle funzioni che permettono di allocare memoria sul device e di trasferire dati e informazioni da host a device e viceversa. Queste funzioni, nel dettaglio sono le seguenti: • cudaMemcpy (void *dst, void *src, size_t nbytes, enum direction): utilizzata per il trasferimento di dati tra host e device. In questa funzione va specificato il puntatore all'area di memoria sorgente, il puntatore all'area di memoria destinazione, la grandezza in byte dei dati da copiare e la direzione dell'operazione di copia. La direzione può essere uno dei seguenti tre tipi: i. cudaMemcpyDeviceToHost, imposta la direzione di copia dal device all'host ii. cudaMemcpyHostToDevice, imposta la direzione di copia da host al device iii. cudaMemcpyDeviceToDevice, imposta la direzione di copia da device a device. • cudaMalloc(void** pointer, size_t n_bytes): funzione utilizzata per l'allocazione di memoria sul device, nella quale va specificato il nome del puntatore all'area di memoria che si sta allocando e la grandezza in bytes dell'area da allocare. • cudaFree(void *pointer): funzione per la deallocazione della memoria allocata in precedenza, nella quale basta specificare il puntatore all'area di memoria da liberare sul device o sull’host. • cudaMallocPitch(void **devPtr, size_t *pitch, size_t *width, size_t*height): funzione per l'allocazione di matrici o volumi. Questa funzione permette di aumentare la coalescenza della memoria limitando il numero di transazioni. ll pitch esprime la lunghezza (in byte) che dovrebbe avere la linea della matrice per raggiungere l'allineamento di Luigi Perillo 566/2699 Pagina 30 di 110 memoria, che riduca il più possibile il numero di transazioni per il caricamento dei dati. Con quest’ultima la memoria è allocata con le righe della lunghezza “giusta” per ridurre il numero di transazioni. Questo significa che nella matrice allocata nella memoria del device, ogni nuova linea inizierà dopo un “pitch” numero di byte. • cudaMemset(void *pointer, int value, size_t count): funzione per l'inizializzazione di un'area di memoria (segnata da pointer) di grandezza count e di valore value. Un programma quindi scritto nel linguaggio CUDA C, conterrà al suo interno una funzione destinata ad essere eseguita sulla GPU. Il listato sarà sicuramente composto di tre parti fondamentali: • Allocazione di memoria sul device; • Trasferimento di dati dalla CPU alla GPU e viceversa; • Lancio di una funzione Kernel. Risulta quindi molto importante, conoscere i tempi impiegati per poter eseguire queste tre fasi, per avere una stima dell'overhead, della sola applicazione scritta in CUDA C. La funzione cudaMalloc(), definita poco sopra, è in generale la prima funzione che si incontra in un codice scritto in CUDA C, poiché la maggior parte degli algoritmi necessitano di allocare memoria sulla GPU. Dai test sviluppati, nei precedenti lavori di tesi, riguardanti le allocazioni di memoria sulle GPU, si è stimato che, la cudaMalloc() impiega lo stesso tempo di esecuzione per allocare elementi fino alle 100.000 unita. Tale osservazione fa evidenziare che allocare pochi elementi sulla GPU e un'operazione abbastanza dispendiosa, in relazione alla quantità realmente allocata; converrebbe quindi se è possibile, e richiesto dal listato in fase di sviluppo, allocare una quantità di memoria Luigi Perillo 566/2699 Pagina 31 di 110 sufficientemente elevata in una sola chiamata alla funzione, poiché l'overhead diventa consistente con più chiamate a cudaMalloc(). Float Tempo (ms) 0 0,008 1 0,139 10 0,139 100 0,139 1000 0,139 10000 0,139 100000 0,139 1000000 0,147 10000000 0,245 100000000 24,781 500000000 170,518 680000000 243,123 Tabella 1: Tempi funzione cudaMalloc(). Illustrazione 15: Grafico dei tempi di esecuzione funzione CudaMalloc() Luigi Perillo 566/2699 Pagina 32 di 110 Successivamente l'allocazione della memoria sulle GPU, occorre eseguire la fase di caricamento dati, cioè trasferire i dati contenuti sulle strutture della CPU alla GPU (e viceversa).Sempre da test svolti in precedenza, sviluppati sul trasferimento di diverse quantità di bytes sulla GPU, si è ricavato che per la funzione cudaMemcpy() possono essere fatte le stesse considerazioni poste per la funzione cudaMalloc(), cioè che trasferire pochi byte alla volta non risulta conveniente, poiché il tempo di computazione resta invariato nel caso in cui si trasmettono poche decine di Bytes o alcune migliaia, fino ad arrivare ad un tempo limite di 1332,238 ms per l'invio di 2,55 GB, (cioè il limite massimo per una singola GPU Fermi). Byte Tempo (ms) 8 0,051 16 0,051 24 0,053 32 0,053 800 0,053 8000 0,056 80000 0,125 100000 0,145 1000000 0,756 10000000 4,654 100000000 42,283 1000000000 482,838 2400000000 1136,858 2720000000 1178,041 2740715520 1332,238 Tabella 2: Tempi di esecuzione funzione cudaMemcpy(). Luigi Perillo 566/2699 Pagina 33 di 110 Illustrazione 16: Grafico dei tempi di esecuzione funzione cudaMemcpy(). Luigi Perillo 566/2699 Pagina 34 di 110 3.4) Funzioni Nel linguaggio di programmazione CUDA C, le funzioni che costituiscono il Kernel portante, vanno identificate con dei qualificatori, da anteporre al nome della funzione, nel momento della loro dichiarazione. Queste sono: • __global__: identifica la funzione definita, e invocata nell’host, ovvero la funzione Kernel, che verrà eseguita in parallelo sul device, dai threads. • __device__: indica le funzioni che sono chiamate sul device, ed eseguite dalle GPU; sono normali funzioni C, ma che sono eseguite esclusivamente sul device e non sull’host. • __host__: indica le funzioni che sono chiamate sull’host, ed eseguite dalle CPU; sono normali funzioni C, ma che sono eseguite esclusivamente sull’host e non sul device. Queste ultime due, possono essere fuse, in modo tale da poter eseguire codice, in cui ci siano parti da eseguire in parallelo sulle GPU e parti da eseguire in seriale su CPU. Una funzione kernel (definita __global__), deve essere eseguita con una configurazione di esecuzione, come vista in precedenza. Quindi attraverso, la configurazione di una griglia e del numero di threads per blocco. Esempio: __global__ void Function(...) {…} int main(//paramentri funzione main) {… dim3 DimGrid(2,2); //Griglia da 4 blocchi dim3 DimBlock(16,4,2); //128 threads per blocco Function<<<DimGrid,DimBlock>>>(//parametri Function); …} Luigi Perillo 566/2699 Pagina 35 di 110 Il codice scritto lancia il kernel “Function” su di una griglia composta da 4 blocchi ognuno contenente 128 threads; questo implica il lancio della funzione 4*128=512 volte. Nella programmazione CUDA C, ogni thread, ha bisogno di accedere ad un differente elemento della struttura dati su cui si sta lavorando. A tal fine, il supporto a runtime, mette a disposizione di ogni thread le seguenti strutture dati predefinite. • threadIdx.x – threadIdx.y – threadIdx.z: per identificare il thread ID dentro un blocco; • blockIdx.x – blockIdx.y: per identificare il block ID nella griglia; • blockDim.x – blockDim.y – blockDim.z: numero di threads nelle direzioni del blocco; • gridDim.x – gridDim.y: dimensioni della griglia in numero di blocchi. Com'è intuibile, con “x” si identifica l'asse orizzontale, con “y” quello verticale e con “z” quello della profondità. Per determinare l'indice di uno specifico thread all'interno di un kernel, è usata all'interno della funzione kernel, la seguente formula: TID=threadIdx.x+threadIdx.y*blockDim.x+threadIdx.z*blockDim.y*blockDim.x. È possibile inoltre, definire all’interno del programma CUDA, più di una funzione kernel. Queste sono eseguite indipendentemente l’una dall’altra, definendo una nuova sorta di parallelismo che riguarda questa volta le funzioni kernel. L’utilizzo dell’istruzione cudaThreadSynchronize() invece, dopo la dichiarazione __global__ del Kernel, permette l’esecuzione in sequenziale delle diverse funzioni kernel definite. Luigi Perillo 566/2699 Pagina 36 di 110 3.5) Operazioni Atomiche Il linguaggio di programmazione CUDA C permette anche l'utilizzo di operazioni atomiche (eseguite senza interruzioni, in modo seriale). Una funzione atomica esegue in sequenza una lettura-modifica-scrittura su di una variabile che risiede nella memoria globale o in una memoria condivisa. La modalità di esecuzione è la seguente: si legge in una variabile in un certo indirizzo in memoria globale o condivisa, e a questa si aggiunge un numero, che verrà aggiornato allo stesso indirizzo. Tuttavia queste non sono consigliate, perché bloccano il parallelismo. Queste operazioni sono svolte su interi (signed/unsigned). Alcune di queste operazioni sono: add, sub, min, max, and, or, xor, increment, decrement, compare e swap. Le operazioni atomiche sono richiamabili dalla funzione atomicX(), sostituendo al posto delle X l'operazione che si vuole effettuare. Esempio: atomicAdd(*punt, (un-)signed int); atomicSub(*punt, (un-)signed int)); atomicMin(*punt, (un-)signed int)); 3.6) Funzioni di Errore Tutte le chiamate CUDA C, fatta eccezione per i lanci del Kernel, ritornano un valore che identifica un codice di errore del tipo cudaError_t. Il linguaggio CUDA C mette a disposizione dell’utente una funzione, capace di riportare l'errore: cudaError_t cudaGetLastError(void). Ritorna il codice dell'ultimo errore, determinato dall'esecuzione di un determinato kernel. Tale funzione combinata con: char* cudaGetErrorString(cudaError_t code) ritorna, una stringa di caratteri che descrive l'errore. Questa sarà utilizzata in una stampa (printf), e si fornirà all'utente una descrizione indicativa del problema. Luigi Perillo 566/2699 Pagina 37 di 110 Esempio: printf(“%s\n”, cudaGetErrorString(cudaGetLastError())). 3.7) Funzioni di Eventi In CUDA C sono inserite delle funzioni per il calcolo del tempo impiegato dal device; cioè per il calcolo accurato del tempo di esecuzione dell’applicazione CUDA. Ciò avviene inserendo in particolari punti del codice delle funzioni, che in seguito ad una sincronizzazione, eseguono il calcolo in questione. Ciò che segue servirà a comprendere meglio quanto detto: cudaEvent_t inizio, fine; cudaEventCreate(&inizio); cudaEventCreate(&fine); cudaEventRecord(inizio, 0); // codice da monitorare cudaEventRecord(fine, 0); cudaEventSynchronize(fine); float time; cudaEventElapsedTime(&time, inizio, fine); Dai due eventi di tipo cudaEvent_t, “inizio” e “fine”, creati dall'apposita funzione cudaEventCreate(*cudaEvent_t); la funzione cudaEventRecord(cudaEvent_t, int) eseguirà una registrazione del tempo di inizio e fine del codice da monitorare. Tutto sincronizzato dalla chiamata alla funzione cudaEventSynchronize(cudaEvent_t). Infine cudaEventElapsedTime(*float, cudaEvent_t, cudaEvent_t) memorizzerà il tempo trascorso tra i due eventi in una variabile di tipo float “time”. Luigi Perillo 566/2699 Pagina 38 di 110 3.8) Compilazione CUDA C Le applicazioni scritte in linguaggio CUDA C, possono essere composte da file con codice C standard, con estensione “.c” e file con codice CUDA con estensioni “.cu”. I file con solo codice scritto esclusivamente in C, possono essere dati in pasto ad un compilatore standard, mentre i file che contengono anche estensioni CUDA devono essere compilati sotto NVCC. Un particolare compilatore fornito da NVIDIA per compilare codice scritto in CUDA C. In entrambi i casi, sono generati dei file oggetto, che poi il linker integrerà; permettendo di ottenere un singolo eseguibile con i binari per CPU e GPU. Nei file “.cu” è contenuto codice C per la CPU e per la GPU. NVCC, che è un compiler driver, separa i due codici, lanciando il compilatore di sistema (Visual C per Windows, o GCC per Linux/Unix) per la parte destinata alla CPU e invocando il CUDA compiler per la parte destinata alla GPU. Il CUDA compiler genera un binario di tipo PTX (Parallel Thread eXecution), che rappresenta l'Instruction Set Virtual per i chip grafici NVIDIA; definendo anche il modello di programmazione, le risorse di esecuzione e lo stato. Un ulteriore traslatore trasforma il codice PTX nel codice binario dell'architettura fisica target. Luigi Perillo 566/2699 Pagina 39 di 110 Illustrazione 17: Compilazione di codice scritto in linguaggio CUDA C. Tutti gli eseguibili scritti con codice CUDA richiedono le librerie CUDA core library (cuda) e CUDA runtime library (cudart). La sintassi utilizzata per la compilazione in una shell di tipo unix è la seguente: nvcc [opzioni] sorgente.cu –o Eseguibile. Tra le varie opzioni messe a disposizione da NVCC va citata quella per stabilire la compute capability del device dove sarà fatto girare il codice, la sintassi è la seguente: -arch sm_xy (dove xy indica la compute capability; esempio: compute capability 2.0 => nvcc -arch sm_20 sorgente.cu –o Eseguibile). Di default è impostata la compute capability 1.0. Luigi Perillo 566/2699 Pagina 40 di 110 4) Il problema della ricostruzione di eventi Nella fisica dello studio delle particelle, con il nome di mesone è indicata una famiglia di particelle subatomiche instabili composte da un quark e un antiquark. Come visto in precedenza il progetto SuperB produrrà una mole di dati elevata, dunque l'esigenza di utilizzare algoritmi veloci ed efficienti è tra gli obiettivi principali del progetto. Il compito dell'acceleratore di particelle, del progetto SuperB, come visto in precedenza, sarà quello di far collidere pacchetti di elettroni e positroni e di ricostruire i mesoni partendo dai loro decadimenti. La difficoltà principale della ricostruzione dei mesoni, è quella di prendere qualsiasi evento e cercare di ricostruire uno dei mesoni B, in uno delle migliaia di canali di decadimento diversi, con lo scopo di ottenere la massima efficienza. 4.1) Algoritmo per la ricostruzione di eventi Arrivati a questo punto, abbiamo tutte le conoscenze tecniche, per poter valutare e confutare i vantaggi derivanti dal calcolo parallelo mediante GPGPU-computing. Per valutare appieno l'architettura nVIDIA Tesla S2050, come ampiamente descritto in precedenza, è stato scritto un algoritmo per la ricostruzione delle collisioni tra particelle di Pioni Π+, Pioni Π- e Gamma. Per lo sviluppo del lavoro di tesi, è stato scritto dapprima un normale codice sequenziale in linguaggio C e successivamente si è valutato quali parti di questo codice potevano essere parallelizzate, per essere poi implementate in linguaggio CUDA C. La logica utilizzata per questa implementazione, proprio per la definizione date in precedenza è: lasciare la gestione alla CPU per le parti di codice “poco ripetitive”, mentre lasciare operazioni su grandi quantità di dati alle GPU, per il calcolo parallelizzato. Luigi Perillo 566/2699 Pagina 41 di 110 I programmi sviluppati, ricevono entrambi in input particelle Gamma, Pioni Π+ e Pioni Π-, da un file contenente un numero elevatissimo di eventi (circa 100.000), le quali vengono combinate tra loro per fornire in output (file outlog.txt) le ricostruzioni dei decadimenti. L'algoritmo implementato (sia la versione parallela che quella seriale) si occupa nello specifico dei seguenti punti: • Sviluppo di un modulo, che prende come input un numero di eventi reali (circa 100.000), per le particelle di: Pioni Π+, Pioni Π- e Gamma, li processa separatamente, fornendo in output, un file di testo “outlog.txt” contenente i risultati delle elaborazioni. • Un modulo per la ricostruzione di mesoni D0, da un insieme di Pioni Π0, Kaoni K+, Kaoni K-, Pioni Π+ e Pioni Π-; che indicheremo con D 0 (k+ΠΠ0; k- Π+ Π0). • Un modulo per la ricostruzione di mesoni D0, a partire da un insieme di: Kaoni K+, Pioni Π-, Pioni Π-, Pioni Π+; e Kaoni K-, Pioni Π+, Pioni Π+, Pioni Π-; che indicheremo con D0 (k+Π-Π-Π+; k-Π+Π+Π-). Illustrazione 18: Stadi della ricostruzione completa del mesone B. Luigi Perillo 566/2699 Pagina 42 di 110 4.2) Strutture dati utilizzate Gli algoritmi sviluppati, (sia quello seriale, che la versione parallela) ricevono in input degli insiemi di particelle, ognuna delle quali è costituita da: • Componenti spaziali: x, y e z. • Un valore per l’Energia. I dati di ciascuna particella, quindi, sono memorizzati in una struttura contenente: • quattro campi in cui memorizzare i dati di una particella (le componeti spaziali e l’energia) • un campo per memorizzare il genere della particella (tipo K (kaone) o non K). La struttura utilizzata, in sostanza si presenta così: typedef struct { float x; // float y; // Componenti spaziali float z; // float Ene; //Energia char gen; //Genere } QVECT; Il modulo per la ricostruzione del mesone D 0 (k+Π-Π0; k- Π+ Π0) richiede, l'utilizzo di una struttura dati supplementare, derivante dall'elaborazione del modulo per la determinazione dei mesoni Π0 , (modulo ricavato da precedenti studi di tesi). La struttura è costituita dalle componenti standard del quadrivettore di input, con due variabili intere, che determinano le particelle genitrici, del particolare decadimento Π0. typedef struct { Luigi Perillo 566/2699 Pagina 43 di 110 float x; // float y; // Componenti spaziali float z; // float Ene; //Energia char gen; //Genere int g1; //Particella Genitrice G1 G2 int g2; } PI_0; I moduli che sono stati sviluppati, sfruttano una nuova struttura dati differente da quella dei quadrivettori di input, in quanto devono tener traccia, di ulteriori componenti, come i tre e i quattro indice rispettivamente, delle particelle genitrici di D0(k+Π-Π0; k-Π+Π0) e D0(k+Π-Π-Π+; k-Π+Π+Π-), oltre alle componenti proposte dal quadrivettore QVEC. La struttura utilizzata è: typedef struct { float x; // float y; // Componenti spaziali float z; // float Ene; //Energia char gen; //Genere int g1; // Particella Genitrice: G1 G2 G3 G4 int g2;// int g3;// int g4;// } D0KPI_PI0; Luigi Perillo 566/2699 Pagina 44 di 110 4.3) Gestione dati reali per Pioni Π+, Π- e Gamma Il primo punto risolto per la corretta stesura delle versioni dell'algoritmo, è stato l’acquisizione dei dati reali, letti da un file di testo. Il file con le informazioni necessarie per i quadrivettori di Pioni Π+, Pioni Π-, i rispettivi Kaoni+, Kaoni- e le Gamma, segue la sintassi: X Y Z Energia; per una composizione come quella riportata in figura: Illustrazione 19: Snapshot di esempio del file di output dopo l'esecuzione di un evento Luigi Perillo 566/2699 Pagina 45 di 110 Per un numero massimo di eventi, (nel caso dei test effettuati), dell’ordine dei 100 mila eventi distinti. I risultati delle varie elaborazioni, dovute ai differenti eventi letti da file di input, vengono scritte su file di output (outlog.txt), nello stesso formato in cui si presentano le componenti nel file di input; con la sola aggiunta dell’indice delle particelle genitrici. La sintassi sarà: X Y Z En G1 G2 (G3 G4, in quanto le particelle genitrici G3 e G4 sono opzionali solo di alcuni moduli). Illustrazione 20: Snapshot di esempio del file di output dopo l'esecuzione di un evento Luigi Perillo 566/2699 Pagina 46 di 110 L’acquisizione delle informazioni, è sviluppata in modo semplice, sfruttando la sintassi C per la lettura da file di testo. Il codice che esamina i moduli sviluppati, viene richiamato, in questo caso 100 mila volte, per ogni singolo evento. 4.4) Descrizione strategia sequenziale I moduli sviluppati, come detto in precedenza sono stati dapprima scritti in una versione seriale, in linguaggio C, per poi rendere la conversione nella versione parallela in CUDA C molto più semplice. Esaminiamo i singoli moduli sviluppati singolarmente: 1. Modulo ricostruzione D0(k+Π-Π0; k-Π+Π0). 2. Modulo ricostruzione D0(k+Π-Π-Π+; k-Π+Π+Π-). 4.4.1) Sequenziale modulo D0(k+Π-Π0; k-Π+Π0) ricostruito Come primo passo, per la stesura di questo modulo, sono state introdotte le strutture dati di input, alla base della ricostruzione di tale punto. Sono stati identificati i seguenti punti: quadrivettori di Pioni Π+, Pioni Π-, e modulo di ricostruzione dei Pioni Π0 (sviluppato nei precedenti lavori di tesi). La composizione dei quadrivettori e del modulo del Pione Π 0, è stata definita nel paragrafo precedente (4.2). L'algoritmo riceve in input un determinato numero di quadrivettori Π+, Π- ricavati dalla lettura dell'evento da file d'input, e Π 0 frutto di una precedente elaborazione dei dati dell'evento caricato. Il codice seriale è strutturato nel seguente modo: 1. Allocazione dinamica della memoria per le strutture dati utilizzate. 2. Caricamento dei dati in input per gli array utilizzati e allocati. 3. Invocazione della funzione per la ricostruzione del mesone D0(k+Π-Π0; kΠ+Π0). Luigi Perillo 566/2699 Pagina 47 di 110 4. Scrittura su file dei dati dell'elaborazione, e stampa a video dei tempi di elaborazione. Focalizziamo l'attenzione sulla funzione ScreateD0KPIPI0, che ha il compito di generare i candidati corretti per la ricostruzione dei mesoni D0(k+Π-Π0; k-Π+Π0). Questa funzione accetta come parametri d'input i vettori dei Π+, Π- e dei Π0, le dimensioni effettive di questi tre vettori, la massa ed il delta di riferimento; e restituisce infine al programma chiamante l'array dei D 0(k+Π-Π0; k-Π+Π0) generati, con la dimensione effettiva. La ricerca delle particelle da combinare, viene gestita da un triplo ciclo for innestato a cascata. Il primo “for” è utilizzato per gestire gli indici dei Π+, il secondo per la gestione degli indici dei Π- ed il terzo per la gestione degli indici dei Π0. Le componenti dei pioni, vengono sommate tra di loro, generando un candidato per il D0(k+Π-Π0; k-Π+Π0), del quale si andrà a calcolare la massa, seguendo la seguente formula: massa=√ Energia2 − x 2 − y 2 − z 2 . La particella appena calcolata, verrà aggiunta al vettore dei risultati D 0(k+Π-Π0; kΠ+Π0) solo nel caso in cui la massa appena calcolata rientrerà in un determinato range, identificato secondo i parametri massa e delta di riferimento, passati come valori d'input alla funzione. Il programma chiamante, riceverà alla fine della computazione, il vettore dei D0(k+Π-Π0; k-Π+Π0), e stamperà a video il tempo di esecuzione della funzione sviluppata, e copierà su file di output i risultati dell'elaborazione. for(i=0;i<N;i++) for(j=0;j<M;j++) for(k=0;k<dimPI0;k++){ if(((PIp[i].gen=='k')&&(PIm[j].gen!='k'))||((PIp[i].gen!='k')&& (PIm[j].gen=='k')){ Luigi Perillo 566/2699 Pagina 48 di 110 //Somma delle componenti ... //Calcolo della massa della particella candidata ... //Controllo sulla massa, assegnazione Candidato ad array risultati if(massa>M0-del && massa<M0+del){ ... } }} 4.4.2) Sequenziale modulo D0(k+Π-Π-Π+; k-Π+Π+Π-) ricostruito Il primo passo, per la stesura di questo modulo, è stato quello di introdurre le strutture dati di input, alla base della ricostruzione di tale punto. Sono state identificati i seguenti punti: quadrivettori di Pioni Π+, Pioni Π- (rispettivamente utilizzati anche per identificare anche i Kaoni+ Kaoni -). La struttura dell'array di output è la stessa per il modulo sopra citato, in questo caso però viene utilizzato un'ulteriore indice per tener traccia di tutti e quattro le componenti genitrici per la ricostruzione del mesone D0(k+Π-Π-Π+; k-Π+Π+Π-). L'algoritmo che riceve in input un determinato numero di quadrivettori Π+, Π- (e rispettivamente i Kaoni+ (K+) e i Kaoni- (K-)) ricavati dalla lettura dell'evento da file d'input. Il codice seriale è strutturato nel seguente modo: 1. Allocazione dinamica della memoria per le strutture dati utilizzate. 2. Caricamento dei dati in input per gli array utilizzati e allocati. 3. Invocazione della funzione per la ricostruzione del mesone D0(k+Π-Π-Π+; kΠ+Π+Π-). 4. Scrittura su file dei dati dell'elaborazione, e stampa a video dei tempi di elaborazione. Luigi Perillo 566/2699 Pagina 49 di 110 La funzione ScreateD0K3PI, ha il compito di generare i candidati corretti per la ricostruzione dei mesoni D0(k+Π-Π-Π+; k-Π+Π+Π-). Questa funzione accetta come parametri d'input: i vettori dei Π+, Π- (e con un controllo sul genere, identifica anche le K+, K-), le dimensioni effettive di questi 2 vettori, la massa ed il delta di riferimento; restituendo infine al programma chiamante l'array dei D 0(k+Π-Π-Π+; k-Π+Π+Π-) generati, con dimensione effettiva. La ricerca delle particelle da combinare, viene gestita da quattro “ciclo for” innestati in cascata. Il primo “for” è utilizzato per gestire gli indici dei Kaoni K+ (e del reciproco Kaone K-), il secondo per la gestione degli indici dei Π- (il reciproco Π+) il terzo per la gestione degli indici dei Π- (e per il reciproco Π+) ed infine il quarto indice per la gestione degli indici dei Π+ (e per il reciproco dei Π-). Per ogni calcolo di: K+ Π- Π- Π+ (e del reciproco K- Π+ Π+ Π-) è impostato un controllo che, impedisce la somma delle componenti delle stesse particelle del medesimo array. Le componenti dei Pioni e dei Kaoni, vengono sommate tra di loro, generando un candidato per il D0(k+Π-Π-Π+; k-Π+Π+Π-), del quale si andrà a calcolare la massa, seguendo la stessa formula del modulo precedente. La particella appena calcolata, verrà aggiunta al vettore dei risultati D 0(k+Π-Π-Π+; k-Π+Π+Π-), solo nel caso in cui la massa appena calcolata rientra nel range, derivante dai parametri di massa e delta di riferimento passati come valori d'input alla funzione. Infine il programma chiamante riceverà il vettore dei D0(k+Π-Π-Π+; k-Π+Π+Π-), stamperà a video il tempo d'esecuzione della funzione sviluppata, e copierà su file di output i risultati elaborati. for(i=0;i<N;i++) for(j=0;j<M;j++) for(k=0;k<M;k++) for(z=0;z<N;z++){ //controllo per somma K+ PI- PI- PI+ if((PIp[i].gen=='k')&&(PIm[j].gen!='k')&&(PIm[k].gen!='k')&&(PIp[z].gen!='k') Luigi Perillo 566/2699 Pagina 50 di 110 &&(j>k)){ //Somma delle componenti … //Calcolo della massa della particella candidata ... //Controllo sulla massa, assegnazione Candidato ad array risultati if(massa>M0-del && massa<M0+del){ ... } } //controllo per somma K- PI+ PI+ PI-, if((PIm[i].gen=='k')&&(PIp[j].gen!='k')&&(PIp[k].gen!='k')&&(PIm[z].gen!='k') &&(j>k)){ //Somma delle componenti ... //Calcolo della massa della particella candidata ... //Controllo sulla massa, assegnazione Candidato ad array risultati if(massa>M0-del && massa<M0+del){ ... } } Luigi Perillo 566/2699 Pagina 51 di 110 4.5) Descrizione strategia parallela Ogni modulo, che sviluppa una funzione di processo, per la ricostruzione degli eventi è rappresentato da una funzione di tipo __device__, richiamata all'interno di una singola funzione Kernel che è di tipo __global__. La funzione per l’elaborazione degli “n” eventi reali contenuti sul file d’input, è una funzione completamente sviluppata sulle CPU. Le specifiche che saranno elencate di seguito, sono inerenti all’analisi di un singolo evento, tra gli “n” contenuti nel file d’input, in quanto le operazioni saranno ripetute per ogni evento preso in considerazione. Ogni funzione CUDA C sviluppata, si compone dei seguenti punti: • Estrazione dei dati reali, da “gpufile.txt” per le particelle Gamma, Pioni Π+ e Π-. • Allocazione della memoria sul device • Trasferimento dati dall'host al device • Esecuzione della funzioni Kernel • Trasferimento dei dati dal device all'host • Stampa dei dati ricevuti, su file “outlog.txt” L’ordine seguito per sviluppare queste operazioni è indicato nel diagramma, a seguire. In particolare sono riportate, le fasi in cui è richiesto l’uso della GPU, con richiamo da parte della CPU, l’operazione interessata, la modalità e la sintassi utilizzata. Luigi Perillo 566/2699 Pagina 52 di 110 Illustrazione 21: Diagramma suddivisione lavoro CPU GPU. Luigi Perillo 566/2699 Pagina 53 di 110 4.5.1) Parallelo modulo D0(k+Π-Π0; k-Π+Π0) ricostruito Il primo modulo sviluppato, per la ricostruzione del mesone D0, nella versione parallela, riceve in ingresso tre insiemi: • un insieme composto da: Pioni Π+ e da Kaoni K+. • un insieme composto da: Pioni Π- e da Kaoni K-. • un insieme composto da: Pioni Π0 . Ogni CUDA thread si occupa del calcolo di un Mesone D0 attraverso la composizione di un Kaone K+, di un Pione Π- e di un Pione Π0, oppure di un Kaone K-, di un Pione Π+ e di un Pione Π0. L'assegnazione di una di queste terne di particelle ad un thread, avviene nel seguente modo: • calcolo del thread ID, relativo al blocco • calcolo del thread ID, relativo all'intera griglia di blocchi (tid). • calcolo degli indici i, j e k con i quali accedere ai dati in input. Gli indici i, j e k vengono calcolati nel modo seguente: tid2 = int(tid /dimPI0); if ( tid < Max_dim){ i = int (tid2 / M); //i-esima particella PI+ j = tid2 -( (M-1) * i) - i ; //j-esima particella PIk = tid % dimPI0; //k-esima particella PI0 …} In cui: • Max_dim è il numero massimo possibile di combinazioni di terne del tipo: Ki+ Πj- Πk0 e Ki- Πj+ Πk0, corrispondente al prodotto tra le Luigi Perillo 566/2699 Pagina 54 di 110 dimensioni dell'array dei Pioni Π+ , dell'array dei Pioni Π- e dell'array dei Pioni Π0 • M è la dimensione dell'array dei Pioni Π- • dimPI0 è la dimensione dell'array dei Pioni Π0. Dopo aver determinato, quale thread deve gestire l’opportuno indice dell’array degli input alla funzione, inizia la fase di calcolo del Mesone D0. Ogni elaborazione produce un calcolo di un Mesone D0(k+Π-Π0; k-Π+Π0) candidato, ottenuto sommando elemento per elemento un Kaone e due Pioni. Successivamente di questo mesone appena calcolato, viene identificata la massa (secondo la formula precedente), e confrontata con una soglia ricavata dalla massa e dal delta di riferimento, passati in input alla funzione (per questa ricostruzione: Massa=1,864; delta=0,020). Le particelle di questi array di candidati che supereranno questo controllo di massa, verranno assegnati all'array dei risultati dei D0(k+Π-Π0; kΠ+Π0) ed infine restituiti alla funzione chiamante, che eseguirà una copia dei risultati dell'elaborazione su un file di output, e la stampa a video dei tempi impiegati per l'elaborazione della funzione che calcola il modulo. Il modulo è sviluppato da una funzione __device__ con la seguente lista di parametri: __device__ void createD0KPIPI0 ( //DATI IN INPUT QVECT* , //array dei Pioni PI+ QVECT* , //array dei Pioni PIPI_0* , //array dei Pioni PI0 unsigned int , //dimensione array PI+ unsigned int, //dimensione array PIunsigned int , //dimensione array PI0 unsigned int,//numero totale combinazioni //DATI IN OUTPUT D0KPI_PI0* , //array dei risultati Luigi Perillo 566/2699 Pagina 55 di 110 unsigned int* //dimensione array risultati long) ; //dimensione massima toy array 4.5.2)Parallelo modulo D0(k+Π-Π-Π+; k-Π+Π+Π-) ricostruito Il secondo modulo sviluppato, riceve invece, in ingresso due insiemi: • un insieme composto da: Pioni Π+ e da Kaoni K+. • un insieme composto da: Pioni Π- e da Kaoni K-. Ogni CUDA thread si occupa del calcolo di un Mesone D0 attraverso la composizione di un Kaone K+, di un Pione Π- di un altro Pione Π- (differente dal precedente) e da un Pione Π+; oppure di un Kaone K-, di un Pione Π+ di un altro Pione Π+ (differente dal precedente) e di un Pione Π-. L'assegnazione di una di queste quaterne di particelle ad un thread avviene in questo modo: • calcolo del thread ID, relativo al blocco • calcolo del thread ID, relativo all'intera griglia di blocchi (tid). • calcolo degli indici i, j, k e z con i quali accedere ai dati in input. Gli indici i, j, k e z sono calcolati nel modo seguente: int dim_1, dim_2=N*M, dim_3; if (tid<Max_dim){ if(N==M){ dim_1=N; dim_3=M*M*N; i=tid/dim_3; j=(tid-(i*dim_3))/dim_2; k=(tid-((i*dim_3)+(j*dim_2)))/dim_1; z=(tid-((i*dim_3)+(j*dim_2)+(k*dim_1))); …}else{ Luigi Perillo 566/2699 Pagina 56 di 110 if(N<M){ dim_1=N; dim_3=M*M*N; i=tid/dim_3; j=(tid-(i*dim_3))/dim_2; k=(tid-((i*dim_3)+(j*dim_2)))/dim_1; z=(tid-((i*dim_3)+(j*dim_2)+(k*dim_1))); if((i<=N)&&(z<=N)){ ….}else{ if((j<=N)&&(k<=N)){ …..} }else{ dim_1=M; dim_3=N*N*M; if((i<=M)&&(z<=M)){ ….}else{ if((j<=M)&&(k<=M)){ ….}…} In cui: • Max_dim è il numero massimo possibile di combinazioni di terne del tipo: Ki+ Πj- Πk- Πz+ e Ki- Πj+ Πk+ Πz-, corrispondente al prodotto tra le dimensioni dell'array dei Pioni Π+, dell'array dei Pioni Π-. • M è la dimensione dell'array dei Pioni Π- • N è la dimensione dell'array dei Pioni Π+ • dim_1, dim_2 e dim_3 sono variabili usate per identificare il TID che eseguirà la somma corrispondente delle particelle. Il calcolo degli indici per questa funzione, è alquanto articolato, poiché bisogna tener conto di tutte le possibili combinazioni degli array di Pioni Π+ e Π-; Luigi Perillo 566/2699 Pagina 57 di 110 facendo attenzione alle dimensioni di quest’ultimi, per non eccedere ad aree di memoria in cui non sono contenute le informazioni da sommare. Dopo aver determinato, quale thread deve gestire l’opportuno indice dell’array degli input alla funzione, inizia la fase di calcolo del Mesone D0. Viene eseguito il calcolo di un Mesone D0(k+Π-Π-Π+; k-Π+Π+Π-) candidato, ottenuto dalla somma componente per componente di un Kaone e per i tre Pioni. Successivamente viene calcolata la massa di questo D0(k+Π-Π-Π+; k-Π+Π+Π-) candidato, ed eseguito un controllo con la soglia ottenuta dalla massa e dal delta di riferimento, passati in input al modulo (per questa ricostruzione: Massa=1,864; delta=0,020). Questo controllo fa si, che all'array dei risultati D0(k+Π-Π-Π+; kΠ+Π+Π-) siano assegnati esclusivamente solo quelle combinazioni che superano il controllo di massa, ed infine restituiti alla funzione chiamante, che eseguirà una copia dei risultati dell'elaborazione su un file di output, e la stampa a video dei tempi impiegati per l'elaborazione della funzione che calcola il modulo D0(k+Π-ΠΠ+; k-Π+Π+Π-). Il modulo sviluppato è una funzione __device__ con la seguente lista di parametri: __device__ void createD0K3PI ( //DATI IN INPUT QVECT *, //array dei PIp QVECT *, //array dei PIm unsigned int , //dim PIp unsigned int , //dim PIm unsigned int , //dimensione massima D0K3PI //DATI IN OUTPUT D0KPI_PI0 *, //array dei risultati unsigned int *, //dimensione array risultati long); //dimensione massima toy array. Luigi Perillo 566/2699 Pagina 58 di 110 5) Test codice e valutazione architettura GPU Nel capitolo precedente e stata fatta una panoramica sull'algoritmo di ricostruzione del mesone B, sulla scomposizione del problema mediante un modello gerarchico e sulle particelle coinvolte nel processo di ricostruzione. In particolare e stata mostrata una parte dell'algoritmo sviluppato, sia nella versione seriale scritta in C e sia la versione parallela scritta in CUDA C, evidenziando i punti più importanti per i due moduli sviluppati. La stesura dei due differenti codici, si è resa necessaria, per valutare gli aspetti positivi e negativi dell'utilizzo dell'architettura GPU, effettuando dei test di performance su entrambe le versioni e mettendole a confronto. La versione parallela, vista in precedenza, è una versione ibrida, in quanto la parte non parallelizzabile del codice, viene eseguita interamente su CPU; mentre la versione sequenziale descritta in precedenza, viene completamente eseguita sulle CPU. I test sono stati svolti sulle macchine in dotazione del data Center ScoPe, con le seguenti caratteristiche: • una nVIDIA Tesla S2050, di cui si è già ampiamente parlato, usata per il codice parallelo in CUDA C. • un server Dell PowerEdge R510, usato per la versione seriale, dalle seguenti caratteristiche: 1. 32 GB DDR3 fino a 1666 Mhz; 2. Processore eight-core Intel Xeon serie E5506 @2,13 Ghz con 4096 KB di chace size; 3. 8 dischi rigidi SATA (7200 rpm) da 3,5”, con capacità singola di 500 GB. Luigi Perillo 566/2699 Pagina 59 di 110 Illustrazione 22: Server Dell PowerEdge R510. Attualmente gli stessi algoritmi vengono usati da esperimenti già esistenti quali BaBar e LHC; questi codici però, sono scritti in logica sequenziale, e integrano parte del framework ROOT e un ambiente sviluppato in C++, progettato per l'analisi dei dati nel campo della fisica delle particelle. Lo scopo primario dei test effettuati quindi è quello di valutare se il lavoro della GPU utilizzate come coprocessore matematico, porterà o meno miglioramenti alle performance di computazione. Per questo motivo, si è richiesto quindi lo sviluppo di un algoritmo sequenziale, da poter confrontare con uno parallelo scritto in CUDA C. Dagli esiti di queste elaborazioni, si potrà valutare se risulta opportuno eseguire un upgrade, degli algoritmi esistenti, in versioni parallele da portare nell'ambito dell'esperimento SuperB. I test di computazione svolti, eseguiti su entrambi i listati, misurano il tempo di esecuzione dell'intero kernel e delle funzioni sviluppate. La funzione utilizzata per eseguire questi test, è “cudaEventRecord(...)” ampiamente descritta in precedenza. I test che seguiranno sono stati eseguiti, su un singolo evento in input, facendo variare il numero di Pioni Π+, Pioni Π– e particelle Gamma, dalle poche decine alle Luigi Perillo 566/2699 Pagina 60 di 110 migliaia di elementi, sviluppando delle considerazioni rilevanti sul codice steso e considerazioni rilevanti per eventuali modifiche e migliorie future al codice. 5.1) Test prestazioni della funzione Kernel Uno dei test sicuramente, più importante, che da rilevanza al lavoro sviluppato, è sicuramente quello che prende in esame, il tempo d'esecuzione dell'intero blocco Kernel, eseguito sulle variazioni di input, senza alcuna considerazione dell'overhead delle funzioni di allocazione dei dati, per cui i tempi sono stati riportati in precedenza. Questo test dimostra effettivamente quanto tempo viene impiegato per la sola elaborazione dell'output, all'interno delle funzioni sviluppate, modificando esclusivamente solo le dimensioni degli input serviti ai listati. Il risultato delle verifiche sviluppate è espresso in millisecondi (la stessa unità è valida per tutti i successivi casi di test). Π+ Π- Gamma Seriale Parallelo 10 10 20 0,24 0,96 50 50 100 46,44 4,83 100 100 200 775,79 49,49 500 500 1000 296610,53 24153,75 1000 1000 2000 2759512,69 218623,76 Tabella 3: Tempi di esecuzione della funzione Kernel espressi in millisecondi. Luigi Perillo 566/2699 Pagina 61 di 110 Tempi (ms) Grafico funzione KERNEL 3000000 Sequenziale Parallelo 2759512,69 2500000 2000000 1500000 1000000 500000 218623,76 296610,53 46,44 0,24 0,96 0 10 10 20 775,79 4,83 49,49 50 50 100 100 100 200 24153,75 Input 500 500 1000 1000 1000 2000 Illustrazione 23: Grafico dei tempi di esecuzione della funzione Kernel Dai valori osservati, durante la fase di test, è stato possibile sviluppare alcune considerazioni molto importanti. L'algoritmo per un input molto piccolo (si intende il numero di particelle di Pioni Π+, Pioni Π- e Gamma) risulta più performante dell'algoritmo parallelo. Appena l'input comincia ad aumentare le prestazioni del codice sequenziale peggiorano vistosamente mentre quelle del parallelo restano pressoché invariate, nell'ordine di pochi minuti d'esecuzione. Nel caso estremo che è stato provato (1000 Pioni Π+ e Π-, e 2000 particelle Gamma), si è verificata una differenza molto significativa tra le prestazioni, infatti, l'algoritmo parallelo, ha terminato la sua elaborazione dell'output, in circa 4'; tempo nettamente inferiore rispetto al listato sequenziale che ha elaborato l'output, in circa 46', mettendo in risalto una differenza sostanziale di poco più di 40 minuti. Possiamo quindi affermare che le due strategie implementate, risultano entrambe molto utili, a seconda del campo applicativo, cioè, l'algoritmo sequenziale è Luigi Perillo 566/2699 Pagina 62 di 110 performante quando si lavora su pochi dati o su operazioni poco ripetitive, quello parallelo è ottimo ad elaborare grosse moli di dati e nell'eseguire operazioni cicliche di grandi dimensioni. 5.2) Test esecuzione funzione D0(k+Π-Π0; k-Π+Π0) Ora passiamo ad esaminare il tempo effettivo impiegato dai listati, per eseguire i singoli moduli sviluppati. Il primo test sviluppato riguarda, quanto tempo viene impiegato per l'esecuzione del modulo che esegue D0KPIPI0, per un singolo evento, con la sola variazioni del numero di elementi per i Pioni Π+, Pioni Π- e le particelle Gamma e tenendo presente il tempo impiegato per le allocazioni delle sole strutture dati (nel codice sequenziale) e del trasferimento dei dati dall'Host al Device (nel codice parallelo). Quindi quanto tempo impiegano i due codici per eseguire le funzioni createD0KPIPI0 (parallela) e ScreateD0KPIPI0 (seriale). Π+ Π- Gamma Seriale Parallelo 10 10 20 0,03 0,39 50 50 100 0,33 0,91 100 100 200 61,24 11,75 500 500 100 12230,66 8495,02 1000 1000 2000 505123,18 16292,53 Tabella 4: Tempi di esecuzione funzione createD0KPIPI0() e ScreateD0KPIPI0() espressi in millisecondi. Luigi Perillo 566/2699 Pagina 63 di 110 Tempi (ms) Grafico D0KPIPI0 600000 Seriale Parallelo 505123,18 500000 400000 300000 200000 100000 0,33 0,03 0,39 0 10 10 20 61,24 0,91 11,75 50 50 100 100 100 200 12230,66 16292,53 8495,02 Input 500 500 1000 1000 1000 2000 Illustrazione 24: Grafico dei tempi di esecuzione funzione D0(k+Π-Π0; k-Π+Π0) Per le misurazioni effettuate, si deve tener presente, che le funzioni sono soggette a particolari overhead. In particolare la versione parallela risulta maggiormente penalizzata quando si deve effettuare un trasferimento di piccole quantità di dati da e verso il Device, per l'esecuzione della funzione kernel, che elabora i risultati. Su input di piccola entità, infatti è possibile notare come sia proprio l'overhead ad influenzare le tempistiche di esecuzione del modulo, proprio come ampiamente descritto nel paragrafo 3.4 per lo studio dei tempi delle allocazioni e delle inizializzazioni delle strutture dati sulle GPU. La situazione cambia notevolmente, quando si fa uso di un parallelismo massimo. Infatti dai dati elaborati, su dimensioni di input nell'ordine delle migliaia, si nota come le prestazioni del listato sequenziale, peggiorino esponenzialmente rispetto a quelle del codice parallelo, in cui le prestazioni restano pressoché lineari. Si calcola Luigi Perillo 566/2699 Pagina 64 di 110 infatti che per un input del tipo 1000-1000-2000 (Pioni Π+ e Π-, e Gamma), il tempo di esecuzione del modulo in versione sequenziale si aggira intorno a poco più di 8' di elaborazione, mentre nella rispettiva versione parallela del modulo, il tempo si aggira intorno a circa 16”. Con un risparmio in termini di tempo totale di esecuzione di circa 7 minuti. 5.3) Test esecuzione funzione D0(k+Π-Π-Π+; k-Π+Π+Π-) Il test che segue, riguarda invece il tempo che viene impiegato per eseguire il secondo modulo sviluppato, ponendosi nelle stesse condizioni precedenti sia per il numero di eventi esaminati, sia per il numero di elementi per i Pioni Π+, Pioni Π- e Gamma e sia per la presenza degli overhead di allocazione e inizializzazione. Quindi i test seguenti verificano, quanto tempo viene impiegato dai due codici per eseguire le funzioni createD0K3PI (parallela) e ScreateD0K3PI (seriale). Π+ Π- Gamma Seriale Parallelo 10 10 20 0,19 0,43 50 50 100 42,44 1,17 100 100 200 702,86 26,24 500 500 1000 205061,81 11813,94 1000 1000 2000 1319761,64 145623,75 Tabella 5: Tempi di esecuzione funzioni createD0K3PI() e ScreateD0K3PI() espressi in millisecondi. Luigi Perillo 566/2699 Pagina 65 di 110 Tempi (ms) Seriale Parallelo 1319761,64 Grafico D0K3PI 1400000 1200000 1000000 800000 600000 400000 205061,81 200000 42,44 0,43 1,17 0,19 0 10 10 20 145623,75 702,86 26,24 11813,94 Input 50 50 100 100 100 200 500 500 1000 1000 1000 2000 Illustrazione 25: Grafico dei tempi di esecuzione funzione D0(k+Π-Π-Π+; k-Π+Π+Π-) Come è possibile riscontrare dai dati elaborati ci troviamo nella stessa condizione del modulo precedentemente esaminato. Per una dimensione di input, di poco conto (poche decine di elementi per ogni struttura dati), l'algoritmo seriale risulta molto più performante dell'algoritmo parallelo. La situazione cambia notevolmente quando la mole di lavoro dovuta all'aumento della dimensione degli input cresce, nell'ordine delle migliaia. Il modulo D0(k+Π-Π-Π+; k-Π+Π+Π-) sviluppato, risulta il modulo che maggiormente influisce le prestazioni di entrambi i listati, sia in versione seriale che in versione parallela, in quanto esaminandoli separatamente, si riscontra: 1. Nella versione seriale, la presenza di 4 cicli for in cascata, che su dimensione del tipo 1000-1000-2000, eseguirà circa 10004 operazioni cicliche, molto dispendiose, indipendentemente dall'architettura hardware su cui si lancia il Luigi Perillo 566/2699 Pagina 66 di 110 codice. 2. Nella versione parallela, anche se le prestazioni risultano nettamente migliori rispetto alla versione sequenziale, il tempo di esecuzione di questo modulo influisce il tempo totale di esecuzione del Kernel, (è possibile notare ciò, nel paragrafo 5.1). Questo perché, se anche non sono presenti cicli for annidati, in questo modulo parallelo, lavoreranno il numero massimo di thread. Ciò è dovuto dal fatto che una componente dell'array dei risultati, risulta essere costituita da quattro elementi differenti (Kaoni+ e Kaoni-, Pioni Π+ e Pioni Π-), e di conseguenza si richiederanno 4 indici differenti, per poter accedere alla struttura dati corrispondente, per l'elaborazione del risultato. Di conseguenza, proprio il ritardo dovuto alla restituzione della componente corrispondente calcolata, secondo la combinazioni degli input, comporterà l'aggravarsi del tempo di elaborazione della funzione. Si riscontra in termini semplici, una differenza in fase di elaborazione dei due listati, per il modulo in questione, di circa 20 minuti, in quanto la versione seriale impiega circa 22 minuti per l'elaborazione del modulo, mentre quella parallela poco più di 2' e 30”. 5.4) Test sull'analisi dei tempi di esecuzione degli algoritmi Come ultimo obbiettivo, si è ritenuto opportuno misurare i tempi di esecuzione, per l'elaborazione degli output, per entrambi i listati su numero variabile di eventi. Ci troviamo nelle condizioni di eseguire i codici, al variare del numero di eventi, con tetto massimo di 100000, lanciati singolarmente. In queste condizioni verrà fatto variare il numero medio delle dimensioni degli input per i Pioni Π+, Pioni Π– e per le particelle Gamma. Dal seguente test, ci si aspetta di poter confutare, per future elaborazioni di eventi di dati reali, quale sia il punto di svolta che rende la strategia parallela e quindi Luigi Perillo 566/2699 Pagina 67 di 110 l'utilizzo delle GPU preferibile alla strategia sequenziale eseguita sulle CPU. Dall'analisi dei tempi dei listati, si sono riscontrati i seguenti valori: Eventi Pioni Π+ Pioni Π- Gamma 1 10 1000 100000 Sequenziale Parallelo 10 10 20 0,43 1,87 50 50 100 48,80 5,83 100 100 200 785,85 54,28 10 10 20 5,65 18,26 50 50 100 503,06 56,43 100 100 200 7740,83 578,34 10 10 20 545,43 1634,59 50 50 100 51213,32 5242,40 100 100 200 734953,95 53609,05 10 10 20 56230,16 153403,32 50 50 100 5121332,41 524239,52 100 100 200 *73495394,51 5360905,45 NOTA: (*) valori stimati Tabella 6: Tempi di esecuzione degli algoritmi su numero variabile di eventi Luigi Perillo 566/2699 Pagina 68 di 110 Seriale Grafico Eventi/Input tempo esecuzione Tempo (ms) Parallelo 80000000 70000000 60000000 50000000 40000000 30000000 20000000 10000000 0 Input 100 100 200 50 50 100 1000 10 10 20 100 100 200 50 50 100 10 10 10 20 100 100 200 50 50 100 10 10 20 100 100 200 50 50 100 10 10 20 1 100000 Eventi Illustrazione 26: Grafico tempi di esecuzione degli algoritmi su numero variabile di eventi Dalla tabella precedente, nascono le seguenti considerazioni: Per input di dimensioni dell'ordine delle poche decine, lanciare i listati, su un numero elevato di eventi, risulta al quanto dispendioso per la strategia parallela, in quanto gli overhead che intervengono nel trasferimento dei pochi dati sulle GPU, rallentano l'elaborazione dei risultati, a differenza del listato sequenziale che risponde con prestazioni elevate all'esecuzione dei blocchi. Nel punto di soglia in cui le dimensioni assumono grandezza dell'ordine delle 50 particelle per i Pioni Π+ e Pioni Π- e 100 particelle per le Gamma, l'algoritmo parallelo, anche su un numero di eventi elevato, risulta molto più performante del listato sequenziale. Si nota infatti, anche se gli ultimi valori sono stati stimati, che per un esecuzione di entrambi i codici su un input elevato, per un numero di eventi dell'ordine dei 100000, il codice sequenziale dovrebbe impiegare circa 20 ore, mentre quello Luigi Perillo 566/2699 Pagina 69 di 110 parallelo terminare l'esecuzione in circa 90 minuti. Conclusioni Nella fase di studio, della ricostruzione dei decadimenti del mesone B per il progetto superB, è nata l'esigenza di sperimentare e sviluppare, nuovi tipi di approcci più performanti, per lo sviluppo e l'elaborazione dell'enorme quantità di informazioni richieste. La direzione e stata quella di adottare il calcolo parallelo, della tecnologia CUDA introdotta da nVIDIA. Ciò ha portato dapprima a svolgere dei test, per la valutazione dell'ambiente di calcolo e lo studio degli overhead minimi da valutare, quando si vuole eseguire una conversione di un codice sequenziale in uno parallelo, da eseguire su GPU. Da questi test è emerso che, poiché si lavora su una mole di informazioni elevata e quindi sono necessarie allocazioni e trasferimenti di dati da Host a Device e viceversa, bisogna ricorrere al massively parallel. Per verificare ciò, sono stati sviluppati nel linguaggio CUDA C, alcune fasi della ricostruzione dei decadimenti del mesone B, al fine di testare l'efficienza del GPU computing in questo determinato ambito scientifico. Per una corretta analisi, di questa nuova tecnologia di programmazione, è stata realizzata una versione sequenziale dello stesso algoritmo, in modo da stabilire i punti e le circostanze, in cui risulta preferibile l'utilizzo di un approccio sequenziale o di uno parallelo. Da questa doppia implementazione di codice è emerso, che se ci troviamo a dover lavorare con applicativi che utilizzano pochi elementi in input, allora gli algoritmi scritti per l'esecuzione esclusiva su CPU risultano molto performanti, a differenza degli algoritmi paralleli, che esprimono la loro utilità, in presenza di una grosse mole di dati da elaborare. Le motivazioni, di tale considerazione, risiedono principalmente nella presenza degli overhead dell'ambiente di calcolo, in quanto questi, incidono pesantemente sulla velocità di elaborazione dell'hardware. Luigi Perillo 566/2699 Pagina 70 di 110 Ciò che però risulta molto importante, è l'efficienza dimostrata dal listato parallelo in presenza di un'elevata quantità di dati in input. Infatti si prevede che per le future ricostruzioni, su valori reali, dei decadimenti del mesone B, le dimensioni delle strutture d'input, risulteranno essere dell'ordine delle poche decine, quindi, ideali per il listato sequenziale. Da queste, nasce una nuova idea, che potrebbe rendere l'algoritmo parallelo, migliore anche nel caso in cui le dimensioni degli input, siano in media dell'ordine delle poche decine, indipendentemente dal numero di eventi da elaborare. Una possibile soluzione, potrebbe essere quella di sfruttare la Global Memory per il salvataggio delle informazioni sulle GPU, in modo da poter caricare più eventi di piccole dimensioni e di lanciare quindi un numero ridotto di volte le funzioni Kernel, con numero ridotto di trasferimenti da Host a Device e viceversa. Ciò potrebbe riduce significativamente gli overhead per i trasferimenti e quindi i tempi di elaborazione risulteranno notevolmente ridotti, anche in presenza di piccole dimensioni di input. CUDA C, è risultato un linguaggio di programmazione, molto intuitivo e utile, per la coesistenza tra le mentalità sequenziali e parallele, ponendo però come prerequisito, la corretta conoscenza dell'architettura GPU su cui si lavora. L'utilità di CUDA, è stata proprio questa, cioè permetterci di scrivere applicativi che sfruttino contemporaneamente i vantaggi offerti da entrambe le architetture CPU e GPU. Proprio la coesistenza, è uno dei punti principi di questo paradigma implementativo, infatti, il programmatore, deve riuscire ad intuire, per una corretta parallelizzazione del codice, quali parti vanno delegate all'Host, e quali riservate al Device, per poter sfruttare appieno le potenze di calcolo computazionale. Possiamo a questo punto affermare, che il co-processing CPU-GPU può, essere considerato il futuro per quanto riguarda il calcolo ad alte prestazioni, nell'ambito scientifico. Luigi Perillo 566/2699 Pagina 71 di 110 Appendice Nelle pagine che seguiranno, è stato riportato il codice sviluppato, sia nella versione seriale che in quella parallela. In particolare, per una corretta lettura, comprensione e rilevazione di errori di sintassi durante la stesura del codice, si è ritenuto opportuno suddividere i listati seguenti, in diversi file. I codici in particolare sono suddivisi nel seguente modo: 1. Codice sequenziale: • Header file contenente le librerie, le strutture dati utilizzate e i prototipi alle funzioni: prototype.h. • File contenente i codici delle funzioni sviluppate, in particolare i codici dei moduli per la ricostruzione dei decadimenti in versione seriale, con le funzioni per la lettura e la scrittura dei file: function.c. • File main, contenente le chiamate alle funzioni sviluppate e le funzioni per il calcolo del tempo di esecuzione dell'intero algoritmo e dei singoli moduli: main.c. 2. Codice parallelo: • Header file contenente le librerie e i prototipi delle funzione svolte interamente sulle CPU: prototype.h. • Header file contenente le librerie cuda, e i prototipi alle funzione svolte interamente sulle GPU: cudaprototype.cuh. • File contenente il codice delle funzioni svolte sulle CPU: function.c • File contenente il codice delle funzione svolte sulle GPU, in particolare il codice della funzione __device__kernel(..) e i vari moduli sviluppati: cudafunction.cu. • File contenente il main, con le chiamate alle funzioni sia Host che Device, con funzioni per il calcolo del tempo di esecuzione dei vari Luigi Perillo 566/2699 Pagina 72 di 110 moduli, e blocco di istruzioni per determinare la dimensione della Griglia su cui eseguire il codice sulle GPU: main.cu. I codici seguenti, sono comprensivi solo delle funzioni e delle operazioni inerenti alle funzioni sviluppate, e non degli interi listati. A.1. Codice versione sequenziale A.1.1. prototype.h #ifndef PROTOTYPE_H #define PROTOTYPE_H //definizioni masse e soglie delta #define mPI0 0.1349 #define dPI0 0.020 #define mD0KPIPI0 1.864 #define dD0KPIPI0 0.020 #define mD0K3PI 1.864 #define dD0K3PI 0.020 ... //Definizioni strutture dati typedef struct { float Ene; //Energia float x; //componente X float y; //componente y float z; //componente z char gen; //Genere } QVECT; //Quadrivettore typedef struct { float Ene; float x; float y; float z; char gen; int g1; //Gamma 1 oppure PI+ Luigi Perillo 566/2699 Pagina 73 di 110 int g2; //Gamma 2 oppure PI} PI_0; //PI_0 typedef struct { float Ene; float x; float y; float z; char gen; int g1; //PI+ int g2; //PIint g3; //PI0 int g4; //utilizzato nel calcolo di D0K3PI } D0KPI_PI0; //D0KPI_PI0 … ... void ScreatePI0( QVECT *, unsigned int, float , float , unsigned int *); //array delle particelle gamma //numero di particelle gamma //massa di riferimento //delta //dimensione effettiva array dei risultati void ScreateD0KPIPI0( QVECT *, QVECT *, PI_0 *, unsigned int , unsigned int , unsigned int , float , float , unsigned int *); //array dei PIp //array dei PI//array delle particelle PI0 //dim PIp //dim PIm //dim PI0 //massa di riferimento //delta //dimensione effettiva array dei risultati void ScreateD0K3PI( QVECT *, QVECT *, unsigned int , unsigned int , float , float , unsigned int *); //array dei PIp //array dei PI//dim PIp //dim PIm //massa di riferimento //delta //dimensione effettiva array dei risultati … Luigi Perillo 566/2699 Pagina 74 di 110 … #endif A.1.2. function.c #include <stdio.h> #include <stdlib.h> #include <math.h> #include "prototype.h" //Dichiarazione variabili globali su CPU PI_0 *PI0_host; //Aos contenente le PI0 D0KPI_PI0 *D0KPIPI0_host; //Aos contenente le D0KPIPI0 D0KPI_PI0 *D0K3PI_host; //Aos contenente le D0K3PI //Funzione per assegnazione delle componenti ai quadri-vettori d'input void add(char buf[100],QVECT *vect,unsigned int num, unsigned int s) { char stoy[100]; int i=0,k=0,j=0; //assegnazione dei valori ai quadrivettori di input while(j<4) { while((buf[i]!=' ')&&(buf[i]!='\0')) { stoy[k]=buf[i]; i++; k++; } stoy[k]='\0'; i++; k=0; j++; if(j==1) vect[num].x=atof(stoy); else if(j==2) vect[num].y=atof(stoy); else if(j==3) vect[num].z=atof(stoy); else if(j==4) vect[num].Ene=atof(stoy); Luigi Perillo 566/2699 Pagina 75 di 110 if((s==4)||(s==5)) vect[num].gen='k'; else vect[num].gen='a'; } } … ... void ScreatePI0( QVECT *Gamma, //array delle particelle gamma unsigned int N, //numero di particelle gamma float M0, //massa di riferimento float del, //delta unsigned int *dimPI0) //dimensione effettiva array dei risultati { int i, j; //indice Gamma //indice Gamma float massa; //massa del quadrivettore candidato QVECT Candidato; //quadrivettore di appoggio for(i=0;i<N-1;i++) for(j=i+1;j<N;j++){ Candidato.x= Gamma[i].x + Gamma[j].x; Candidato.y= Gamma[i].y + Gamma[j].y; Candidato.z= Gamma[i].z + Gamma[j].z; Candidato.Ene= Gamma[i].Ene + Gamma[j].Ene; //Calcolo della massa della particella candidata massa=(Candidato.Ene*Candidato.Ene) – (Candidato.x*Candidato.x) – (Candidato.y*Candidato.y) - (Candidato.z*Candidato.z); //Controllo sulla massa if(sqrt(massa)>M0-del && sqrt(massa)<M0+del) { PI0_host=(PI_0*)realloc(PI0_host, ((*dimPI0)+1)*sizeof(PI_0)); PI0_host[*dimPI0].x=Candidato.x; PI0_host[*dimPI0].y=Candidato.y; PI0_host[*dimPI0].z=Candidato.z; PI0_host[*dimPI0].Ene=Candidato.Ene; PI0_host[*dimPI0].g1=i; PI0_host[*dimPI0].g2=j; *dimPI0=(*dimPI0)+1; } } Luigi Perillo 566/2699 Pagina 76 di 110 } void ScreateD0KPIPI0( QVECT *PIp, //array dei PIp QVECT *PIm, //array dei PIPI_0 *PI0, //array delle particelle PI0 unsigned int N, //dim PIp unsigned int M, //dim PIm unsigned int dimPI0,//dim PI0 float M0, //massa di riferimento float del, //delta unsigned int *dimD0KPIPI0)//dimensione effettiva array dei risultati { int i, //indice PIp j, //indice PIm k; //indice PI0 float massa; //massa del quadrivettore candidato QVECT Candidato; //quadrivettore di appoggio for(i=0;i<N;i++) for(j=0;j<M;j++) for(k=0;k<dimPI0;k++) { if(((PIp[i].gen=='k')&&(PIm[j].gen!='k'))||((PIp[i].gen!='k')&&(PIm[j].gen=='k'))) { //Somma delle componenti della i-esima particella PIpiu genere k e della j-esima PImeno k Candidato.x= PIp[i].x + PIm[j].x + PI0[k].x; Candidato.y= PIp[i].y + PIm[j].y + PI0[k].y; Candidato.z= PIp[i].z + PIm[j].z + PI0[k].z; Candidato.Ene= PIp[i].Ene + PIm[j].Ene + PI0[k].Ene; //Calcolo della massa della particella candidata massa=(Candidato.Ene*Candidato.Ene) - (Candidato.x*Candidato.x) – (Candidato.y*Candidato.y) - (Candidato.z*Candidato.z); //Controllo sulla massa if(sqrt(massa)>M0-del && sqrt(massa)<M0+del) { D0KPIPI0_host=(D0KPI_PI0*)realloc(D0KPIPI0_host, ((*dimD0KPIPI0)+1)*sizeof(D0KPI_PI0)); Luigi Perillo 566/2699 Pagina 77 di 110 D0KPIPI0_host[*dimD0KPIPI0].x=Candidato.x; D0KPIPI0_host[*dimD0KPIPI0].y=Candidato.y; D0KPIPI0_host[*dimD0KPIPI0].z=Candidato.z; D0KPIPI0_host[*dimD0KPIPI0].Ene=Candidato.Ene; if(PIp[i].gen == 'k') { D0KPIPI0_host[*dimD0KPIPI0].g1=i; //Particella genitrice PIpiu D0KPIPI0_host[*dimD0KPIPI0].g2=-j; //Particella genitrice PImemo D0KPIPI0_host[*dimD0KPIPI0].g3=k; //Particella genitrice PI0 }else{ D0KPIPI0_host[*dimD0KPIPI0].g1=-i; //Particella genitrice PIpiu D0KPIPI0_host[*dimD0KPIPI0].g2=j; //Particella genitrice PImemo D0KPIPI0_host[*dimD0KPIPI0].g3=k; //Particella genitrice PI0 } *dimD0KPIPI0=(*dimD0KPIPI0)+1; } } } } void ScreateD0K3PI( QVECT *PIp, //array dei PIp QVECT *PIm, //array dei PIunsigned int N, //dim PIp unsigned int M, //dim PIm float M0, //massa di riferimento float del, //delta unsigned int *dimD0K3PI)//dimensione effettiva array dei risultati { int i,j,k,z; float massa; QVECT Candidato; for(i=0;i<N;i++) for(j=0;j<M;j++) for(k=0;k<M;k++) for(z=0;z<N;z++){ if((PIp[i].gen=='k')&&(PIm[j].gen!='k')&&(PIm[k].gen!='k')&&(PIp[z].gen!='k') &&(j>k)) { Candidato.x= PIp[i].x + PIm[j].x + PIm[k].x + PIp[z].x; Candidato.y= PIp[i].y + PIm[j].y + PIm[k].y + PIp[z].y; Candidato.z= PIp[i].z + PIm[j].z + PIm[k].z + PIp[z].z; Luigi Perillo 566/2699 Pagina 78 di 110 Candidato.Ene= PIp[i].Ene + PIm[j].Ene + PIm[k].Ene + Pip[z].Ene; massa=(Candidato.Ene*Candidato.Ene) – (Candidato.x*Candidato.x)(Candidato.y*Candidato.y) – (Candidato.z*Candidato.z); //Controllo sulla massa if(massa>M0-del && massa<M0+del) { D0K3PI_host=(D0KPI_PI0*)realloc(D0K3PI_host,((*dimD0K3PI)+1) *sizeof(D0KPI_PI0)); D0K3PI_host[*dimD0K3PI].x=Candidato.x; D0K3PI_host[*dimD0K3PI].y=Candidato.y; D0K3PI_host[*dimD0K3PI].z=Candidato.z; D0K3PI_host[*dimD0K3PI].Ene=Candidato.Ene; D0K3PI_host[*dimD0K3PI].g1=i; D0K3PI_host[*dimD0K3PI].g2=-j; D0K3PI_host[*dimD0K3PI].g3=-k; D0K3PI_host[*dimD0K3PI].g4=z; *dimD0K3PI=(*dimD0K3PI)+1; } } if((PIm[i].gen=='k')&&(PIp[j].gen!='k')&&(PIp[k].gen!='k')&&(PIm[z].gen !='k') &&(j>k)) { Candidato.x= PIm[i].x + PIp[j].x + PIp[k].x + PIm[z].x; Candidato.y= PIm[i].y + PIp[j].y + PIp[k].y + PIm[z].y; Candidato.z= PIm[i].z + PIp[j].z + PIp[k].z + PIm[z].z; Candidato.Ene= PIm[i].Ene + PIp[j].Ene + PIp[k].Ene + Pim[z].Ene; massa=(Candidato.Ene*Candidato.Ene) – (Candidato.x*Candidato.x)(Candidato.y*Candidato.y) - (Candidato.z*Candidato.z); //Controllo sulla massa if(sqrt(massa)>M0-del && sqrt(massa)<M0+del){ D0K3PI_host=(D0KPI_PI0*)realloc(D0K3PI_host, ((*dimD0K3PI)+1) *sizeof(D0KPI_PI0)); D0K3PI_host[*dimD0K3PI].x=Candidato.x; D0K3PI_host[*dimD0K3PI].y=Candidato.y; D0K3PI_host[*dimD0K3PI].z=Candidato.z; Luigi Perillo 566/2699 Pagina 79 di 110 D0K3PI_host[*dimD0K3PI].Ene=Candidato.Ene; D0K3PI_host[*dimD0K3PI].g1=i; D0K3PI_host[*dimD0K3PI].g2=-j; D0K3PI_host[*dimD0K3PI].g3=-k; D0K3PI_host[*dimD0K3PI].g4=z; *dimD0K3PI=(*dimD0K3PI)+1; } } } } … … A.1.2. main.c #include <stdio.h> #include <stdlib.h> #include <math.h> #include "prototype.h" #include "function.c" #include <time.h> #include <cuda_runtime.h> int main(int argc, char **argv){ FILE *fd2, *fd; //Dati di input QVECT *Gamma_host; //Aos contenente le particelle Gamma QVECT *PIm_host; //AoS contenente le PImeno QVECT *PIp_host; //Aos contenente le PIpiu char buf[100], *res; srand(time(NULL)); long int i=0, j=0, evento=0, s=1; unsigned int numpim=0, //numero di particelle PImeno numpip=0, //numero di particelle PIpiu numgamma=0, //numero di particelle Gamma prod_cart, //dimensione massima dell'array delle K0s e D0KPI coefBin, //dimensione massima dell'array dei PI0 Luigi Perillo 566/2699 Pagina 80 di 110 *dimPI0_host, *dimD0KPIPI0_host, *dimD0K3PI_host; … //dimensione effettiva array PI0 //dimensione effettiva array D0KPIPI0 //dimensione effettiva array D0K3PI remove("outlogseq.txt"); //cancellazione vecchio file di outlog.txt /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ cudaEvent_t start, stop, start2, stop2; float time, time2; cudaEventCreate(&start); cudaEventCreate(&stop); cudaEventRecord(start, 0); fd=fopen("gpufile2.txt", "r"); if( fd==NULL ) { perror("Errore in apertura del file di input"); exit(1); } //apertura file di outlog.txt fd2=fopen("outlogseq.txt", "a"); if( fd2==NULL ) { perror("Errore in apertura del file di log"); exit(1); } //lettura di file input res=fgets(buf, 100, fd); while((res!=NULL)) { //Allocazione vettori considerando dimensioni massime PIm_host=(QVECT*)malloc(numpim*sizeof(QVECT)); PIp_host=(QVECT*)malloc(numpip*sizeof(QVECT)); Gamma_host=(QVECT*)malloc(numgamma*sizeof(QVECT)); PI0_host=(PI_0*)malloc(0); D0KPIPI0_host=(D0KPI_PI0*)malloc(0); D0K3PI_host=(D0KPI_PI0*)malloc(0); ... //Allocazione dimensioni effettive dimPI0_host=(unsigned int*)malloc(sizeof(unsigned int)); Luigi Perillo 566/2699 Pagina 81 di 110 *dimPI0_host=0; dimD0KPIPI0_host=(unsigned int*)malloc(sizeof(unsigned int)); *dimD0KPIPI0_host=0; dimD0K3PI_host=(unsigned int*)malloc(sizeof(unsigned int)); *dimD0K3PI_host=0; //lettura da file dati di un EVENTO "gamma" "pi_plus" "pi_minus" “k_minus” k_plus” fino a "ENDEVENT" while((strncmp(buf,"ENDEVENT",8)!=0)) { if(strncmp(buf,"gamma",5)==0) s=1; else if(strncmp(buf,"pi_plus",7)==0) s=2; else if(strncmp(buf,"pi_minus",8)==0) s=3; else if(strncmp(buf,"k_plus",6)==0) s=4; else if(strncmp(buf,"k_minus",7)==0) s=5; else { if(s==1){ //buf è una particella, addala in gamma Gamma_host=(QVECT*)realloc(Gamma_host,((numgamma)+1) *sizeof(QVECT)); add(buf,Gamma_host,numgamma,s); numgamma++; }else if(s==2){ //buf è una particella, addala in pip Pip_host=(QVECT*)realloc(PIp_host,((numpip)+1)* sizeof(QVECT)); add(buf,PIp_host,numpip,s); numpip++; }else if(s==3){ //buf è una particella, addala in pim Pim_host=(QVECT*)realloc(PIm_host,((numpim)+1) *sizeof(QVECT)); add(buf,PIm_host,numpim,s); numpim++; }else if(s==4){ //buf è una particella k+, addala in pip Pip_host=(QVECT*)realloc(PIp_host,((numpip)+1) *sizeof(QVECT)); add(buf,PIp_host,numpip,s); numpip++; }else if(s==5){ //buf è una particella, k- addala in pim Pim_host=(QVECT*)realloc(PIm_host,((numpim)+1) *sizeof(QVECT)); add(buf,PIm_host,numpim,s); numpim++; } Luigi Perillo 566/2699 Pagina 82 di 110 } res=fgets(buf, 100, fd); } //fine lettura EVENTO corrente prod_cart=numpip*numpim; coefBin=(numgamma*(numgamma-1))/2; //Chiamante alle funzioni che costituiscono il contenuto del Kernel nella Parallela … … ScreatePI0( Gamma_host, numgamma, mPI0, dPI0, dimPI0_host); ScreateD0KPIPI0( PIp_host, PIm_host, PI0_host, numpip, numpim, *dimPI0_host, mD0KPIPI0, dD0KPIPI0, dimD0KPIPI0_host); ScreateD0K3PI( PIp_host, PIm_host, numpip, numpim, mD0K3PI, dD0K3PI, dimD0K3PI_host); … ... //Scrittura su file outlogseq.txt dei risultati elaborazione Evento corrente … … fprintf(fd2,"PI0\n"); for(i=0; i<(*dimPI0_host) ;i++) {fprintf(fd2,"%f %f %f %f %d %d\n",PI0_host[i].x, Luigi Perillo 566/2699 Pagina 83 di 110 PI0_host[i].y, PI0_host[i].z, PI0_host[i].Ene, PI0_host[i].g1, PI0_host[i].g2);} fprintf(fd2,"D0KPIPI0\n"); for(i=0; i<(*dimD0KPIPI0_host) ;i++) {fprintf(fd2,"%f %f %f %f %d %d %d\n",D0KPIPI0_host[i].x, D0KPIPI0_host[i].y, D0KPIPI0_host[i].z, D0KPIPI0_host[i].Ene, D0KPIPI0_host[i].g1, D0KPIPI0_host[i].g2, D0KPIPI0_host[i].g3);} fprintf(fd2,"D0K3PI\n"); for(i=0; i<(*dimD0K3PI_host) ;i++) {fprintf(fd2,"%f %f %f %f %d %d %d %d\n",D0K3PI_host[i].x, D0K3PI_host[i].y, D0K3PI_host[i].z, D0K3PI_host[i].Ene, D0K3PI_host[i].g1, D0K3PI_host[i].g2, D0K3PI_host[i].g3, D0K3PI_host[i].g4);} … ... //Tag fine evento elaborato fprintf(fd2,"ENDEVENT\n"); //Rilascio memoria strutture e dimensioni sull'host free(PIm_host); free(PIp_host); free(Gamma_host); free(PI0_host); free(D0KPIPI0_host); free(D0K3PI_host); free(dimPI0_host); free(dimD0KPIPI0_host); free(dimD0K3PI_host); //reset dimensioni particelle input per singolo evento Luigi Perillo 566/2699 Pagina 84 di 110 numpip=0; numpim=0; numgamma=0; res=fgets(buf, 100, fd); evento++; } fclose(fd); fclose(fd2); printf("Numero Eventi:%d\n",evento); /*timeline*/ cudaEventRecord(stop, 0); /*timeline*/ cudaEventSynchronize(stop); /*timeline*/ cudaEventElapsedTime(&time, start, stop); /*timeline*/ printf("[tempo totale sequenziale] Time: %f ms\n\n", time); } A.2. Codice versione parallela A.2.1. prototype.h #ifndef PROTOTYPE_H #define PROTOTYPE_H typedef struct { //Quadrivettore float Ene; //Energia float x; //componente X float y; //componente y float z; //componente z char gen; //Genere } QVECT; typedef struct { //PI 0 float Ene; float x; float y; float z; char gen; int g1; //Gamma 1 oppure PI+ int g2; //Gamma 2 oppure PI} PI_0; Luigi Perillo 566/2699 Pagina 85 di 110 typedef struct { //D0KPIPI0 float Ene; float x; float y; float z; char gen; int g1; //PI+ int g2; //PIint g3; //PI0 int g4; //utilizzato nel calcolo di D0K3PI } D0KPI_PI0; … ... void add(char [100], QVECT *, unsigned int, unsigned int); void stampa_risultati(PI_0 *, PI_0 *, PI_0 *, D0KPI_PI0 *, QVECT *, QVECT *, D0KPI_PI0 *, D0KPI_PI0 *, D0 *, unsigned int *, unsigned int *, unsigned int *, unsigned int *, unsigned int *, unsigned int *, unsigned int*); #endif A.2.2. cudaprototype.h #ifndef PROTOTYPE #define PROTOTYPE //definizioni masse e soglie delta #define mPI0 0.1349 #define dPI0 0.020 #define mD0KPIPI0 1.864 #define dD0KPIPI0 0.020 Luigi Perillo 566/2699 Pagina 86 di 110 #define mD0K3PI 1.864 #define dD0K3PI 0.020 … //Definizione firme funzioni __global__ void kernel( //DATI DI INPUT QVECT *, //array delle PIpiu QVECT *, //array delle PImeno QVECT *, //array delle Gamma unsigned int, //dimensione array PIpiu unsigned int, //dimensione array PImeno unsigned int, //dimensione array Gamma unsigned int, //dimensione massima array dei risultati unsigned int, //dimensione massima array dei risultati //DATI DI OUTPUT PI_0 *, //array delle particelle k0s risultanti PI_0 *, //array delle particelle risultanti PI_0 *, //array delle particelle PI0 risultanti D0KPI_PI0 *, //array delle particelle risultanti unsigned int *, //dimensione effettivan array delle K0s unsigned int *, //dimensione effettiva array delle D0KPI unsigned int *, //dimensione effettiva array delle PI0 unsigned int *, //dimensione effettiva array delle D0KPIPI0 unsigned int, //dimensione massima array D0KPIPI0 D0KPI_PI0 *, //array delle particelle risultanti unsigned int *, //dimensione effettiva array delle D0Ks2PI unsigned int, //dimensione massima array D0Ks2PI D0KPI_PI0 *, //array delle particelle D0K3PI risultanti unsigned int *, //dimensione effettiva array D0K3PI unsigned int, //dimensione massima array D0K3PI D0 *, //array delle particelle Ds0D0PI0 unsigned int *, //dimensione effettiva array Ds0D0PI0 long, long, long, long, long, long, long); //dimensione massima TOY array __device__ void createPI0( Luigi Perillo 566/2699 //DATI DI INPUT Pagina 87 di 110 QVECT *, //array delle particelle Gamma unsigned int, //numero di particelle Gamma) unsigned int, //coefficiente binomiale float, //massa di riferimento float, //delta //DATI DI OUTPUT PI_0 *, //array delle particelle PI0 risultanti unsigned int *, long); //dimensione massima TOY array risultati __device__ void createD0KPIPI0( //DATI IN INPUT QVECT*, //array PI+ QVECT*, //array PIPI_0 *, //array delle particelle PI0 unsigned int, //dimensione array PIpiu unsigned int, //dimensione array PImeno unsigned int, //dimensione PI0 unsigned int, //dimensione massima array risultati float, //massa di riferimento float, //delta //DATI IN OUTPUT D0KPI_PI0 *, //array delle particelle risultanti unsigned int *, long); //dimensione massima TOY array risultati __device__ void stampa_input ( //DATI IN INPUT QVECT*, //array PI+ QVECT*, //array PIPI_0 *, //array delle particelle PI0 unsigned int, //dimensione array PIpiu unsigned int, //dimensione array PImeno unsigned int, //dimensione PI0 unsigned int, //dimensione massima array risultati float, //massa di riferimento float, //delta //DATI IN OUTPUT D0KPI_PI0 *, //array delle particelle risultanti unsigned int *);//dimensione effettiva array dei risultati __device__ void createD0K3PI ( QVECT *, QVECT *, unsigned int , Luigi Perillo 566/2699 //array dei PIp //array dei PIm //dim PIp Pagina 88 di 110 unsigned int , //dim PIm unsigned int , //dimensione massima D0K3PI float , //massa di riferimento float , //delta D0KPI_PI0 *, //array delle particelle risultanti unsigned int *, long); //dimensione massima TOY array risultati … ... #endif A.2.3. function.c #include <stdlib.h> #include <math.h> #include "prototype.h" void add(char buf[100],QVECT *vect,unsigned int num, unsigned int s) { char stoy[100]; int i=0,k=0,j=0; //calcolo componenti dei quadrivettori di input while(j<4){ while((buf[i]!=' ')&&(buf[i]!='\0')) { stoy[k]=buf[i]; i++; k++; } stoy[k]='\0'; i++; k=0; j++; if(j==1) vect[num].x=atof(stoy); else if(j==2) vect[num].y=atof(stoy); else if(j==3) vect[num].z=atof(stoy); Luigi Perillo 566/2699 Pagina 89 di 110 else if(j==4) vect[num].Ene=atof(stoy); if((s==4)||(s==5)) vect[num].gen='k'; else vect[num].gen='a'; } } … … A.2.4. cudafunction.cu #include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> #include <cuda_runtime.h> #include "prototype.h" #include "cudaprototype.cuh" __global__ void kernel( Luigi Perillo 566/2699 QVECT *PIp, //array delle PIpiu QVECT *PIm, //array delle PImeno QVECT *Gamma, //array delle Gamma unsigned int N, //dimensione array PIpiu unsigned int M, //dimensione array PImeno unsigned int L, //dimensione array Gamma unsigned int Max_dim, //dimensione massima K0s e D0KPI unsigned int Max_dim2,//dimensione massima array PI0 PI_0 *K0s, //array delle particelle k0s risultanti PI_0 *D0KPI, //array delle particelle D0KPI PI_0 *PI0, //array delle particelle PI0 D0KPI_PI0 *D0KPIPI0, //array delle particelle D0KPIPI0 unsigned int *dimk0s, //dimensione effettiva K0s unsigned int *dimD0KPI, //dimensione effettiva D0KPI unsigned int *dimPI0, //dimensione effettiva PI0 unsigned int *dimD0KPIPI0, //dimensione effettiva D0KPIPI0 unsigned int max_D0KPIPI0, //dimensione massima D0KPIPI0 Pagina 90 di 110 D0KPI_PI0 *D0Ks2PI, //array delle particelle D0Ks2PI unsigned int *dimD0Ks2PI, //dimensione effettiva D0Ks2PI unsigned int max_D0Ks2PI, //dimensione massima D0Ks2PI D0KPI_PI0 *D0K3PI, //array delle particelle D0K3PI unsigned int *dimD0K3PI, //dimensione effettiva D0K3PI unsigned int max_D0K3PI, //dimensione massima D0K3PI D0 *Ds0D0PI0, //array delle particelle DS0DOPI0 unsigned int *dimDs0D0PI0, //dimensione effettiva DS0D0PI0 long Tmax_K0s, long Tmax_D0KPI, long Tmax_PI0, long Tmax_D0KPIPI0, long Tmax_D0Ks2PI, long Tmax_D0K3PI, long Tmax_Ds0D0PI0)//dimensione massima TOY array { ... //Invoco la funzione per il calcolo delle PI0 createPI0(Gamma,L,Max_dim2,mPI0,dPI0,PI0,dimPI0,Tmax_PI0); __syncthreads(); if((*dimPI0)!=0) { max_D0KPIPI0=Max_dim*(*dimPI0); //Invoco la funzione per il calcolo delle D0KPIPI0 createD0KPIPI0(PIp,PIm,PI0,N,M,*dimPI0,max_D0KPIPI0,mD0KPIPI0,dD0KPIP I0,D0KPIPI0,dimD0KPIPI0,Tmax_D0KPIPI0); } max_D0K3PI=N*(M*M*N); //Invoco la funzione per il calcolo delle D0K3PI createD0K3PI(PIp,PIm,N,M,max_D0K3PI,mD0K3PI,dD0K3PI,D0K3PI,dimD0K3 PI,Tmax_D0K3PI); __syncthreads(); Luigi Perillo 566/2699 Pagina 91 di 110 int tot_el=(*dimD0KPI)+(*dimD0KPIPI0)+(*dimD0Ks2PI)+(*dimD0K3PI); … ... } __device__ void createPI0(QVECT *Gamma, //array delle particelle gamma unsigned int N, //numero di particelle gamma unsigned int N2, //coefficiente binomiale float M0, //massa di riferimento float del , //delta PI_0 *Pi0, //array delle particelle PI0 risultanti unsigned int *dimPI0, long Tmax_PI0)//dimensione massima TOY risultati { //Dichiarazioni variabili locali sul device int linIdx, i, z, j, loc, sh, tid; float massa; QVECT Candidato; //Calcolo del thread ID sh=threadIdx.x+blockDim.x*threadIdx.y; tid = blockDim.x*blockDim.y*(blockIdx.x + gridDim.x* blockIdx.y) + sh; //Algoritmo per il calcolo degli indici giusti in base al thread if (tid<N2){ linIdx=N2-tid; i=int(N - 0.5 - sqrt(0.25 - 2 * (1 - linIdx))); z=(N+N-1-i)*i; j=tid - z/2 + 1 + i; if (i==j){ i=i-1; j=N-1; } //Somma di due quadrivettori Candidato.x=Gamma[i].x+Gamma[j].x; Candidato.y=Gamma[i].y+Gamma[j].y; Candidato.z=Gamma[i].z+Gamma[j].z; Candidato.Ene=Gamma[i].Ene+Gamma[j].Ene; //Controllo massa Luigi Perillo 566/2699 Pagina 92 di 110 massa=(Candidato.Ene*Candidato.Ene) – (Candidato.x*Candidato.x)(Candidato.y*Candidato.y) - (Candidato.z*Candidato.z); if(sqrt(massa)>M0-del && sqrt(massa)<M0+del && (*dimPI0)<=Tmax_PI0){ loc=atomicAdd(dimPI0,1); Pi0[loc].x=Candidato.x; Pi0[loc].y=Candidato.y; Pi0[loc].z=Candidato.z; Pi0[loc].Ene=Candidato.Ene; Pi0[loc].g1=i; Pi0[loc].g2=j; } } } __device__ void createD0KPIPI0(QVECT *PIp, //array dei PIp QVECT *PIm, //array dei PIPI_0 *PI0, //array delle particelle PI0 unsigned int N, //dim PIp unsigned int M, //dim PIm unsigned int dimPI0, unsigned int Max_dim, //dimensione massima D0KPIPI0 float M0, //massa di riferimento float del, //delta D0KPI_PI0 *D0KPIPI0, //array delle particelle D0KPIPI0 unsigned int *dimD0KPIPI0, long Tmax_D0KPIPI0) //dimensione massima TOY D0KPIPI0 { //Dichiarazioni variabili locali sul device int i, //indice PIp j, //indice PIk, //indice PI0 block_thread_id, //indice del thread all'interno del blocco loc, //indice aggiornato per accedere alle D0KPI tid, //indice del thread all'interno della griglia tid2; float massa; //massa del quadrivettore candidato QVECT Candidato; //quadrivettore di appoggio Luigi Perillo 566/2699 Pagina 93 di 110 __syncthreads(); //Calcolo del thread ID block_thread_id=threadIdx.x+blockDim.x*threadIdx.y; tid=blockDim.x*blockDim.y*(blockIdx.x+gridDim.x*blockIdx.y)+ block_thread_id; tid2 = int(tid /dimPI0); //Algoritmo per il calcolo degli indici giusti in base al thread if (tid<Max_dim){ i=int(tid2 / M); //i-esima particella PIp j=tid2 -((M-1)*i) - i ; //j-esima particella PIm k=tid % dimPI0; //k-esima particella PI0 if( ((Pip[i].gen=='k')&&(PIm[j].gen!='k'))||((PIp[i].gen!='k')&&(PIm[j].gen=='k'))) { //Somma delle componenti della i-esima particella PIpiu genere k e della j-esima PImeno k Candidato.x= PIp[i].x + PIm[j].x + PI0[k].x; Candidato.y= PIp[i].y + PIm[j].y + PI0[k].y; Candidato.z= PIp[i].z + PIm[j].z + PI0[k].z; Candidato.Ene= PIp[i].Ene + PIm[j].Ene + PI0[k].Ene; //Calcolo della massa della particella candidata massa=(Candidato.Ene*Candidato.Ene)-(Candidato.x*Candidato.x)(Candidato.y*Candidato.y)-(Candidato.z*Candidato.z); //Controllo sulla massa if(sqrt(massa)>M0-del&&sqrt(massa)<M0+del &&(*dimD0KPIPI0)<=Tmax_D0KPIPI0) { loc=atomicAdd(dimD0KPIPI0,1); //incremento il numero di particelle D0KPI D0KPIPI0[loc].x=Candidato.x; D0KPIPI0[loc].y=Candidato.y; D0KPIPI0[loc].z=Candidato.z; D0KPIPI0[loc].Ene=Candidato.Ene; if(PIp[i].gen == 'k') { D0KPIPI0[loc].g1=i; //Particella genitrice PIpiu D0KPIPI0[loc].g2=-j; //Particella genitrice PImemo D0KPIPI0[loc].g3=k; //Particella genitrice PI0 } else Luigi Perillo 566/2699 Pagina 94 di 110 { D0KPIPI0[loc].g1=-i; //Particella genitrice PIpiu D0KPIPI0[loc].g2=j; //Particella genitrice PImemo D0KPIPI0[loc].g3=k; //Particella genitrice PI0 } } } } } __device__ void createD0K3PI( QVECT *PIp, //array dei PIp QVECT *PIm, //array dei PIunsigned int N, //dim PIp unsigned int M, //dim PIm unsigned int Max_dim, //dimensione massima D0K3PI float M0, //massa di riferimento float del, //delta D0KPI_PI0 *D0K3PI, //array delle particelle risultanti unsigned int *dimD0K3PI, long Tmax_D0K3PI)//dimensione massima TOY D0K3PI { int i, //indice PIp j, //indice PIk, //indice PI0 z, block_thread_id, //indice del thread all'interno del blocco loc, //indice aggiornato per accedere alle D0KPI tid; //indice del thread all'interno della griglia int dim_1, dim_2=N*M, dim_3; float massa; //massa del quadrivettore candidato QVECT Candidato; //quadrivettore di appoggio __syncthreads(); //Calcolo del thread ID block_thread_id=threadIdx.x+blockDim.x*threadIdx.y; tid=blockDim.x*blockDim.y*(blockIdx.x+gridDim.x*blockIdx.y)+ block_thread_id; if (tid<Max_dim){ if(N==M) Luigi Perillo 566/2699 Pagina 95 di 110 { dim_1=N; dim_3=M*M*N; i=tid/dim_3; j=(tid-(i*dim_3))/dim_2; k=(tid-((i*dim_3)+(j*dim_2)))/dim_1; z=(tid-((i*dim_3)+(j*dim_2)+(k*dim_1))); if((PIp[i].gen=='k')&&(PIm[j].gen!='k')&&(PIm[k].gen!='k')&&(PIp[z].gen!='k')) { if(j>k) { //Kp+PIm+PIm+PIp Candidato.x= PIp[i].x + PIm[j].x + PIm[k].x + PIp[z].x; Candidato.y= PIp[i].y + PIm[j].y + PIm[k].y + PIp[z].y; Candidato.z= PIp[i].z + PIm[j].z + PIm[k].z + PIp[z].z; Candidato.Ene= PIp[i].Ene + PIm[j].Ene + PIm[k].Ene + Pip[z].Ene; massa=(Candidato.Ene*Candidato.Ene)-(Candidato.x*Candidato.x)(Candidato.y*Candidato.y)-(Candidato.z*Candidato.z); //Controllo sulla massa if(sqrt(massa)>M0-del&&sqrt(massa)<M0+del && (*dimD0K3PI)<=Tmax_D0K3PI) { loc=atomicAdd(dimD0K3PI,1); //incremento il numero di particelle D0K3PI D0K3PI[loc].x=Candidato.x; D0K3PI[loc].y=Candidato.y; D0K3PI[loc].z=Candidato.z; D0K3PI[loc].Ene=Candidato.Ene; D0K3PI[loc].g1=i; D0K3PI[loc].g2=-j; D0K3PI[loc].g3=-k; D0K3PI[loc].g4=z; } } } if((PIm[i].gen=='k')&&(PIp[j].gen!='k')&&(PIp[k].gen!='k')&&(PIm[z].gen!='k')) { Luigi Perillo 566/2699 Pagina 96 di 110 if(j>k) { //Km+PIp+PIp+PIm Candidato.x= PIm[i].x + PIp[j].x + PIp[k].x + PIm[z].x; Candidato.y= PIm[i].y + PIp[j].y + PIp[k].y + PIm[z].y; Candidato.z= PIm[i].z + PIp[j].z + PIp[k].z + PIm[z].z; Candidato.Ene= PIm[i].Ene + PIp[j].Ene + PIp[k].Ene + PIm[z].Ene; massa=(Candidato.Ene*Candidato.Ene)-(Candidato.x*Candidato.x)(Candidato.y*Candidato.y)-(Candidato.z*Candidato.z); //Controllo sulla massa if(sqrt(massa)>M0-del && sqrt(massa)<M0+del&& (*dimD0K3PI)<=Tmax_D0K3PI){ loc=atomicAdd(dimD0K3PI,1); //incremento numero particelle D0Ks2PI D0K3PI[loc].x=Candidato.x; D0K3PI[loc].y=Candidato.y; D0K3PI[loc].z=Candidato.z; D0K3PI[loc].Ene=Candidato.Ene; D0K3PI[loc].g1=-i; D0K3PI[loc].g2=j; D0K3PI[loc].g3=k; D0K3PI[loc].g4=-z; } } } } }else{ if(N<M) { dim_1=N; dim_3=M*M*N; i=tid/dim_3; j=(tid-(i*dim_3))/dim_2; k=(tid-((i*dim_3)+(j*dim_2)))/dim_1; z=(tid-((i*dim_3)+(j*dim_2)+(k*dim_1))); if((i<=N)&&(z<=N)) { if((PIp[i].gen=='k')&&(PIm[j].gen!='k')&&(PIm[k].gen!='k')&&(PIp[z].gen!='k')) { if(j>k) { Luigi Perillo 566/2699 Pagina 97 di 110 //Kp+PIm+PIm+PIp Candidato.x= PIp[i].x + PIm[j].x + PIm[k].x + PIp[z].x; Candidato.y= PIp[i].y + PIm[j].y + PIm[k].y + PIp[z].y; Candidato.z= PIp[i].z + PIm[j].z + PIm[k].z + PIp[z].z; Candidato.Ene= PIp[i].Ene + PIm[j].Ene + PIm[k].Ene + Pip[z].Ene; massa=(Candidato.Ene*Candidato.Ene)-(Candidato.x*Candidato.x)(Candidato.y*Candidato.y)-(Candidato.z*Candidato.z); //Controllo sulla massa if(sqrt(massa)>M0-del && sqrt(massa)<M0+del && (*dimD0K3PI)<=Tmax_D0K3PI) { loc=atomicAdd(dimD0K3PI,1); //incremento il numero di particelle D0K3PI D0K3PI[loc].x=Candidato.x; D0K3PI[loc].y=Candidato.y; D0K3PI[loc].z=Candidato.z; D0K3PI[loc].Ene=Candidato.Ene; D0K3PI[loc].g1=i; D0K3PI[loc].g2=-j; D0K3PI[loc].g3=-k; D0K3PI[loc].g4=z; } } } }else{ if((j<=N)&&(k<=N)) { if((PIm[i].gen=='k')&&(PIp[j].gen!='k')&&(PIp[k].gen!='k')&&(PIm[z].gen!='k')) { if(j>k) { //Km+PIp+PIp+PIm Candidato.x= PIm[i].x + PIp[j].x + PIp[k].x + PIm[z].x; Candidato.y= PIm[i].y + PIp[j].y + PIp[k].y + PIm[z].y; Candidato.z= PIm[i].z + PIp[j].z + PIp[k].z + PIm[z].z; Candidato.Ene= PIm[i].Ene + PIp[j].Ene + PIp[k].Ene + Pim[z].Ene; massa=(Candidato.Ene*Candidato.Ene)-(Candidato.x*Candidato.x)Luigi Perillo 566/2699 Pagina 98 di 110 (Candidato.y*Candidato.y)-(Candidato.z*Candidato.z); //Controllo sulla massa if(sqrt(massa)>M0-del && sqrt(massa)<M0+del && (*dimD0K3PI)<=Tmax_D0K3PI) { loc=atomicAdd(dimD0K3PI,1); //incremento numero di particelle D0Ks2PI D0K3PI[loc].x=Candidato.x; D0K3PI[loc].y=Candidato.y; D0K3PI[loc].z=Candidato.z; D0K3PI[loc].Ene=Candidato.Ene; D0K3PI[loc].g1=-i; D0K3PI[loc].g2=j; D0K3PI[loc].g3=k; D0K3PI[loc].g4=-z; } } } } } }else { dim_1=M; dim_3=N*N*M; if((i<=M)&&(z<=M)) { if((PIp[i].gen=='k')&&(PIm[j].gen!= 'k')&&(PIm[k].gen!='k')&&(PIp[z].gen!='k')) { if(j>k) { //Kp+PIm+PIm+PIp Candidato.x= PIp[i].x + PIm[j].x + PIm[k].x + PIp[z].x; Candidato.y= PIp[i].y + PIm[j].y + PIm[k].y + PIp[z].y; Candidato.z= PIp[i].z + PIm[j].z + PIm[k].z + PIp[z].z; Candidato.Ene= PIp[i].Ene + PIm[j].Ene + PIm[k].Ene + Pip[z].Ene; massa=(Candidato.Ene*Candidato.Ene)-(Candidato.x*Candidato.x)(Candidato.y*Candidato.y)-(Candidato.z*Candidato.z); //Controllo sulla massa Luigi Perillo 566/2699 Pagina 99 di 110 if(sqrt(massa)>M0-del && sqrt(massa)<M0+del && (*dimD0K3PI)<=Tmax_D0K3PI) { loc=atomicAdd(dimD0K3PI,1); //incremento il numero di particelle D0K3PI D0K3PI[loc].x=Candidato.x; D0K3PI[loc].y=Candidato.y; D0K3PI[loc].z=Candidato.z; D0K3PI[loc].Ene=Candidato.Ene; D0K3PI[loc].g1=i; D0K3PI[loc].g2=-j; D0K3PI[loc].g3=-k; D0K3PI[loc].g4=z; } } } }else { if((j<=M)&&(k<=M)) { if((PIm[i].gen=='k')&&(PIp[j].gen!='k')&&(PIp[k].gen!='k')&&(PIm[z].gen!='k')) { if(j>k) { //Km+PIp+PIp+PIm Candidato.x= PIm[i].x + PIp[j].x + PIp[k].x + PIm[z].x; Candidato.y= PIm[i].y + PIp[j].y + PIp[k].y + PIm[z].y; Candidato.z= PIm[i].z + PIp[j].z + PIp[k].z + PIm[z].z; Candidato.Ene= PIm[i].Ene + PIp[j].Ene + PIp[k].Ene + Pim[z].Ene; massa=(Candidato.Ene*Candidato.Ene)-(Candidato.x*Candidato.x)(Candidato.y*Candidato.y)-(Candidato.z*Candidato.z); //Controllo sulla massa if(sqrt(massa)>M0-del && sqrt(massa)<M0+del && (*dimD0K3PI)<=Tmax_D0K3PI) { loc=atomicAdd(dimD0K3PI,1); //incremento numero di particelle D0Ks2PI D0K3PI[loc].x=Candidato.x; D0K3PI[loc].y=Candidato.y; D0K3PI[loc].z=Candidato.z; D0K3PI[loc].Ene=Candidato.Ene; D0K3PI[loc].g1=-i; Luigi Perillo 566/2699 Pagina 100 di 110 D0K3PI[loc].g2=j; D0K3PI[loc].g3=k; D0K3PI[loc].g4=-z; } } } } }}}} A.2.5. main.cu #include <stdio.h> #include <stdlib.h> #include <math.h> #include <cuda_runtime.h> #include "prototype.h" #include "cudaprototype.cuh" #include "function.c" #include "cudafunction.cu" int main(int argc, char **argv){ FILE *fd; FILE *fd2; char buf[100]; char *res; long evento=0; double quadriprodotto; double latogriglia; int lgint; double npi0=0, nk0s=0, nd0kpi=0, nd0kpipi0=0, nd0ks2pi=0, nd0k3pi=0, nds0d0pi0=0; unsigned int numpip=0, numpim=0, numgamma=0,s=1,i=0; QVECT *Gamma_host,*PIp_host,*PIm_host,*Gamma_dev,*PIp_dev,*PIm_dev; //Dati di output ... PI_0 *PI0_host, *PI0_dev; //Aos contenente le PI0, su host e device D0KPI_PI0 *D0KPIPI0_host, *D0KPIPI0_dev; //Aos contenente le D0KPIPI0, su host e device D0KPI_PI0 *D0K3PI_host, *D0K3PI_dev; //Aos contenente le D0K3PI, su host e device Luigi Perillo 566/2699 Pagina 101 di 110 unsigned int max_D0KPIPI0=0, //dimensione massima array D0KPIPI0 max_D0K3PI=0, //dimensione massima dell'array D0K3PI *dimPI0_host, *dimPI0_dev,//dimensione effettiva array PI0 *dimD0KPIPI0_host, *dimD0KPIPI0_dev, //dimensione effettiva array D0KPIPI0 *dimD0K3PI_host, *dimD0K3PI_dev; //dimensione effettiva array D0K3PI long Tmax_PI0=1000, Tmax_D0KPIPI0=1000, Tmax_D0K3PI=1000; /*timeline*/ cudaEvent_t start, stop; /*timeline*/ float time; remove("outlog.txt"); /*timeline*/ cudaEventCreate(&start); /*timeline*/ cudaEventCreate(&stop); /*timeline*/ cudaEventRecord(start, 0); /* apre il file */ fd=fopen("gpufile2.txt", "r"); if( fd==NULL ) { perror("Errore in apertura del file di input"); exit(1); } fd2=fopen("outlog.txt", "a"); if( fd2==NULL ) { perror("Errore in apertura del file di log"); exit(1); } res=fgets(buf, 100, fd); while((res!=NULL)) { //Allocazione memoria per vettore delle PI0 sul device cudaMalloc((void**)&PI0_dev,(Tmax_PI0)*sizeof(PI_0)); //Allocazione memoria per vettore delle D0KPIPI0 sul device cudaMalloc((void**)&D0KPIPI0_dev,(Tmax_D0KPIPI0)*sizeof(D0KPI_PI0)); Luigi Perillo 566/2699 Pagina 102 di 110 //Allocazione memoria per vettore delle D0K3PI sul device cudaMalloc((void**)&D0K3PI_dev,(Tmax_D0K3PI)*sizeof(D0KPI_PI0)); //Il device, a fine calcoli, passera il numero di particelle k0s(dimk0s_dev) all'host(dimk0s_host) mediante una cudaMemcpy() //Il device, a fine calcoli, passera il numero di particelle PI0(dimPI0_dev) all'host(dimPI0_host) mediante una cudaMemcpy() dimPI0_host=(unsigned int*)malloc(sizeof(unsigned int)); //Dimensione vettore PI0 host cudaMalloc((void**)&dimPI0_dev,sizeof(unsigned int) ); //Dimensione vettore PI0 device //Il device, a fine calcoli, passera il numero di particelle D0KPIPI0 all'host(dimD0KPIPI0_host) mediante una cudaMemcpy() dimD0KPIPI0_host=(unsigned int*)malloc(sizeof(unsigned int)); //Dimensione vettore D0KPIPI0 host cudaMalloc((void**)&dimD0KPIPI0_dev,sizeof(unsigned int) ); //Dimensione vettore D0KPIPI0 device //Il device, a fine calcoli, passera il numero di particelle D0K3PI(dimD0K3PI_dev) all'host(dimD0K3PI_host) mediante una cudaMemcpy() dimD0K3PI_host=(unsigned int*)malloc(sizeof(unsigned int)); //Dimensione vettore D0K3PI host cudaMalloc((void**)&dimD0K3PI_dev,sizeof(unsigned int) ); //Dimensione vettore D0K3PI device //Allocazione primo blocco di Gamma, PIp e PIm Gamma_host=(QVECT*)malloc(sizeof(QVECT)); PIp_host=(QVECT*)malloc(sizeof(QVECT)); PIm_host=(QVECT*)malloc(sizeof(QVECT)); while((strncmp(buf,"ENDEVENT",8)!=0)) { if(strncmp(buf,"gamma",5)==0) s=1; else if(strncmp(buf,"pi_plus",7)==0) s=2; else if(strncmp(buf,"pi_minus",8)==0) s=3; else if(strncmp(buf,"k_plus",6)==0) s=4; else if(strncmp(buf,"k_minus",7)==0) s=5; else{ Luigi Perillo 566/2699 Pagina 103 di 110 if(s==1){ Gamma_host=(QVECT*)realloc(Gamma_host,((numgamma)+1) *sizeof(QVECT)); add(buf,Gamma_host,numgamma,s); numgamma++; }else if(s==2){ Pip_host=(QVECT*)realloc(PIp_host,((numpip)+1) *sizeof(QVECT)); add(buf,PIp_host,numpip,s); numpip++; }else if(s==3){ Pim_host=(QVECT*)realloc(PIm_host,((numpim)+1) *sizeof(QVECT)); add(buf,PIm_host,numpim,s); numpim++; } else if(s==4){ Pip_host=(QVECT*)realloc(PIp_host,((numpip)+1) *sizeof(QVECT)); add(buf,PIp_host,numpip,s); numpip++; } else if(s==5){ Pim_host=(QVECT*)realloc(PIm_host,((numpim)+1) *sizeof(QVECT)); add(buf,PIm_host,numpim,s); numpim++; } } res=fgets(buf, 100, fd); } coefBin=(numgamma*(numgamma-1))/2; prod_cart=numpip*numpim; //Allocazione memoria per quadrivettori PImeno su device cudaMalloc((void**)&PIm_dev,numpim*sizeof(QVECT)); //Allocazione memoria per quadrivettori PIpiu su device cudaMalloc((void**)&PIp_dev,numpip*sizeof(QVECT)); //Allocazione memoria per quadrivettori Gamma su device cudaMalloc((void**)&Gamma_dev,numgamma*sizeof(QVECT) ); //Copio il vettore delle PImeno dall'host(PIm_host) al device(PIm_dev) Luigi Perillo 566/2699 Pagina 104 di 110 cudaMemcpy(PIm_dev,PIm_host,numpim*sizeof(QVECT),cudaMemcpyHostToDe vice); //Copio il vettore delle PIpiu dall'host(PIp_host) al device(PIp_dev) cudaMemcpy(PIp_dev,PIp_host,numpip*sizeof(QVECT),cudaMemcpyHostToDevi ce); //Copio il vettore delle Gamma dall'host(Gamma_host) al device(Gamma_dev) cudaMemcpy(Gamma_dev,Gamma_host,numgamma*sizeof(QVECT),cudaMemcp yHostToDevice); //inizializzo dimensione array delle PI0 cudaMemset(dimPI0_dev,0,sizeof(unsigned int)); //inizializzo dimensione array delle D0KPIPI0 cudaMemset(dimD0KPIPI0_dev,0,sizeof(unsigned int)); //inizializzo dimensione array delle D0K3PI cudaMemset(dimD0K3PI_dev,0,sizeof(unsigned int)); quadriprodotto=numpip*numpim*numpim*numpip; quadriprodotto=quadriprodotto/1024; latogriglia=sqrt(quadriprodotto); lgint=(int)(latogriglia); lgint++; if((numpip!=0)&&(numpim!=0)&&(numgamma!=0)) { //Invocazione kernel kernel<<<dim3(lgint,lgint,1),dim3(32,32,1)>>>( PIp_dev, PIm_dev, Gamma_dev, numpip, numpim, numgamma, prod_cart, coefBin, K0s_dev, D0KPI_dev, PI0_dev, D0KPIPI0_dev, dimk0s_dev, dimD0KPI_dev, dimPI0_dev, Luigi Perillo 566/2699 Pagina 105 di 110 dimD0KPIPI0_dev, max_D0KPIPI0, D0Ks2PI_dev, dimD0Ks2PI_dev, max_D0Ks2PI, D0K3PI_dev, dimD0K3PI_dev, max_D0K3PI, Ds0D0PI0_dev, dimDs0D0PI0_dev, Tmax_K0s, Tmax_D0KPI, Tmax_PI0, Tmax_D0KPIPI0, Tmax_D0Ks2PI, Tmax_D0K3PI, Tmax_Ds0D0PI0); } evento++; cudaThreadSynchronize(); //Barriera di sincronizzazione thread //Copio la dimensione dell'array delle PI0 dal device all'host cudaMemcpy(dimPI0_host,dimPI0_dev,sizeof(unsigned int), cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, PI0 PI0_host=(PI_0*)malloc(*dimPI0_host*sizeof(PI_0)); //Copio l'array dei risultati PI0 dal device all'host cudaMemcpy(PI0_host,PI0_dev,*dimPI0_host*sizeof(PI_0),cudaMemcpyDeviceTo Host); //Copio la dimensione dell'array delle D0KPIPI0 dal device all'host cudaMemcpy(dimD0KPIPI0_host,dimD0KPIPI0_dev,sizeof(unsigned int), cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, D0KPIPI0 D0KPIPI0_host=(D0KPI_PI0*)malloc(*dimD0KPIPI0_host*sizeof(D0KPI_PI0)); //Copio l'array dei risultati D0KPIPI0 dal device all'host cudaMemcpy(D0KPIPI0_host,D0KPIPI0_dev,*dimD0KPIPI0_host*sizeof(D0KPI_ PI0), cudaMemcpyDeviceToHost); //Copio la dimensione dell'array delle D0K3PI dal device all'host cudaMemcpy(dimD0K3PI_host,dimD0K3PI_dev,sizeof(unsigned int), cudaMemcpyDeviceToHost); Luigi Perillo 566/2699 Pagina 106 di 110 //Alloco memoria sull'host per l'array dei risultati, D0K3PI D0K3PI_host=(D0KPI_PI0*)malloc(*dimD0K3PI_host*sizeof(D0KPI_PI0)); //Copio l'array dei risultati D0K3PI dal device all'host cudaMemcpy(D0K3PI_host,D0K3PI_dev,*dimD0K3PI_host*sizeof(D0KPI_PI0),c udaMemcpyDeviceToHost); //scrittura su file fprintf(fd2,"PI0\n"); for(i=0; i<(*dimPI0_host) ;i++) fprintf(fd2,"%f %f %f %f %d %d\n",PI0_host[i].x, PI0_host[i].y, PI0_host[i].z, PI0_host[i].Ene, PI0_host[i].g1, PI0_host[i].g2); fprintf(fd2,"D0KPIPI0\n"); for(i=0; i<(*dimD0KPIPI0_host) ;i++) fprintf(fd2,"%f %f %f %f %d %d %d\n",D0KPIPI0_host[i].x, D0KPIPI0_host[i].y, D0KPIPI0_host[i].z, D0KPIPI0_host[i].Ene, D0KPIPI0_host[i].g1, D0KPIPI0_host[i].g2, D0KPIPI0_host[i].g3); fprintf(fd2,"D0K3PI\n"); for(i=0; i<(*dimD0K3PI_host) ;i++) fprintf(fd2,"%f %f %f %f %d %d %d %d\n",D0K3PI_host[i].x, D0K3PI_host[i].y, D0K3PI_host[i].z, D0K3PI_host[i].Ene, D0K3PI_host[i].g1, D0K3PI_host[i].g2, D0K3PI_host[i].g3, D0K3PI_host[i].g4); fprintf(fd2,"ENDEVENT\n"); npi0=npi0+(double)(*dimPI0_host); nk0s=nk0s+(double)(*dimk0s_host); nd0kpi=nd0kpi+(double)(*dimD0KPI_host); Luigi Perillo 566/2699 Pagina 107 di 110 nd0kpipi0=nd0kpipi0+(double)(*dimD0KPIPI0_host); nd0ks2pi=nd0ks2pi+(double)(*dimD0Ks2PI_host); nd0k3pi=nd0k3pi+(double)(*dimD0K3PI_host); nds0d0pi0=nds0d0pi0+(double)(*dimDs0D0PI0_host); //Rilascio memoria strutture e dimensioni sul device cudaFree(PIp_dev); cudaFree(PIm_dev); cudaFree(Gamma_dev); cudaFree(PI0_dev); cudaFree(D0KPIPI0_dev); cudaFree(D0K3PI_dev); cudaFree(dimPI0_dev); cudaFree(dimD0KPIPI0_dev); cudaFree(dimD0K3PI_dev); //Rilascio memoria strutture e dimensioni sull'host free(PIm_host); free(PIp_host); free(Gamma_host); free(PI0_host); free(D0KPIPI0_host); free(D0K3PI_host); free(dimPI0_host); free(dimD0KPIPI0_host); free(dimD0K3PI_host); numpip=0; numpim=0; numgamma=0; res=fgets(buf, 100, fd); } /* chiude il file */ fclose(fd); fclose(fd2); /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ cudaEventRecord(stop, 0); cudaEventSynchronize(stop); cudaEventElapsedTime(&time, start, stop); printf("\n[tempo totale kernel] Time elapsed: %f ms\n", time); Luigi Perillo 566/2699 Pagina 108 di 110 printf("EVENTI:%d\n",evento); printf("\npi0 trovate: %1.0f\n", npi0); printf("k0s trovate: %1.0f\n", nk0s); printf("d0kpi trovate: %1.0f\n", nd0kpi); printf("d0kpipi0 trovate: %1.0f\n", nd0kpipi0); printf("d0ks2pi trovate: %1.0f\n", nd0ks2pi); printf("d0k3pi trovate: %1.0f\n", nd0k3pi); printf("ds0d0pi0 trovate: %1.0f\n", nds0d0pi0); return 0; } Luigi Perillo 566/2699 Pagina 109 di 110 Bibliografia (1) “NVIDIA CUDA C Programming Guide Version 4.0”, NVIDIA. (2) “A Hierarchical NeuroBayes-based Algorithm for Full Reconstruction of B Mesons at B Factories”, M. Feindt, F. Keller, M. Kreps, T. Kuhr, S. Neubauer, D. Zander, A. Zupanc. (3) “CUDA C best practices GUIDE” , NVIDIA. (4) “CUDA by Example: An Introdution to General-Purpouse Programming”, J. Sanders, E. Kandrot. Luigi Perillo 566/2699 Pagina 110 di 110 GPU