Sviluppo di un codice di ricostruzione di particelle su architettura
Transcript
Sviluppo di un codice di ricostruzione di particelle su architettura
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 di ricostruzione di particelle su architettura multicore GPU: Valutazione delle performance di ricostruzione su dati reali Relatori Candidato Prof. Guido Russo Dr. Guglielmo De Nardo Dr. Silvio Pardi Angelo Tebano matr. 566/2750 Anno Accademico 2011-2012 Alla mia famiglia, ai miei amici, a coloro che hanno sempre creduto in me e che mi hanno sostenuto rendendo possibile questo momento della mia vita. Angelo Tebano 566/2750 Pagina 2 di 105 Indice generale Introduzione...............................................................................................................8 1. L'esperimento SuperB..........................................................................................10 1.1. L'acceleratore SuperB....................................................................................11 2. Le GPU e il modello di programmazione GPGPU..............................................14 2.1. Architettura di una GPU................................................................................18 2.2. Il modello GPGPU........................................................................................20 2.3. nVIDIA Tesla S2050.....................................................................................22 3. L'ambiente di sviluppo CUDA............................................................................24 3.1. CUDA-C.......................................................................................................26 3.1.1. Compilazione di un programma CUDA-C..............................................28 3.1.2. Gestione della memoria in CUDA-C......................................................29 3.1.3. Qualificatori ed operazioni atomiche......................................................32 3.1.4. Gestione degli Eventi e degli errori.........................................................34 4. SuperB e il problema della ricostruzione degli eventi..........................................36 4.1. Strutture dati utilizzate..................................................................................38 4.2. Moduli di calcolo preesistenti........................................................................40 4.3. Modulo per la ricostruzione di Mesoni D0 (K0s Π+ Π- e K0s Π- Π+)..........41 4.3.1. Strategia sequenziale...............................................................................43 4.3.2. Strategia parallela....................................................................................45 4.4. Modulo per la ricostruzione di Mesoni D*0 (D0 Π0)....................................48 4.4.1. Strategia sequenziale...............................................................................49 4.4.2. Strategia parallela....................................................................................51 4.5. Una variante “multi-evento” degli algoritmi di ricostruzione dei decadimenti del mesone B per l'analisi di input reali................................................................53 5. Testing e valutazione degli algoritmi di ricostruzione dei decadimenti del mesone B.............................................................................................................................. 57 5.1. Obiettivi della valutazione e tipologie di testing...........................................58 5.1.1. Test delle principali CUDA memory functions.......................................59 5.1.2. Test dell'allocazione della memoria per le strutture in uso......................62 5.1.3. Test del trasferimento della memoria per le strutture in uso....................63 5.1.4. Performance test sul modulo per la ricostruzione di Mesoni D0 (K0s Π+ Π- e K0s Π- Π+)...............................................................................................64 Angelo Tebano 566/2750 Pagina 3 di 105 5.1.5. Performance test sul modulo per la ricostruzione di Mesoni D*0 (D0 Π0) .......................................................................................................................... 66 5.1.6. Performance test di computazione totale.................................................68 5.1.7. Capacità di ricostruzione della variante “multi-evento” dell'algoritmo generale su input reali.......................................................................................70 Conclusioni.............................................................................................................. 73 Appendice................................................................................................................75 A.1. Main function – versione sequenziale...........................................................75 A.2. Modulo D0 (K0s Π+ Π- e K0s Π- Π+) – versione sequenziale....................79 A.3. Modulo D*0 (D0 Π0) – versione sequenziale...............................................80 A.4. Main function – versione parallela...............................................................82 A.5. Kernel function – versione parallela.............................................................88 A.6. Modulo D0 (K0s Π+ Π- e K0s Π- Π+) – versione parallela........................91 A.7. Modulo D*0 (D0 Π0) – versione parallela...................................................92 A.8. Main function – versione “multi-evento”.....................................................95 Bibliografia............................................................................................................104 Ringraziamenti......................................................................................................105 Angelo Tebano 566/2750 Pagina 4 di 105 Indice delle illustrazioni Illustrazione 1: L’acceleratore SuperB.....................................................................10 Illustrazione 2: Strategia di recupero della luce di sincrotrone................................12 Illustrazione 3: Calcolatori del GRID system del CERN di Ginevra.......................13 Illustrazione 4: Mappa dei moderni sistemi core-based...........................................15 Illustrazione 5: Confronto tra CPU e GPU nel calcolo floating point......................16 Illustrazione 6: Grafico della cooperazione tra moduli CPU e GPU........................17 Illustrazione 7: Schema di massima di un'architettura GPU thread-based...............18 Illustrazione 8: Schema delle memorie di una GPU................................................19 Illustrazione 9: nVIDIA Tesla S2050.......................................................................21 Illustrazione 10: Schema del ciclo di vita di un programma CUDA........................25 Illustrazione 11: Composizione di una grigia e di un blocco nel caso bidimensionale.........................................................................................................27 Illustrazione 12: I quattro stadi della ricostruzione completa del mesone B............38 Illustrazione 13: Il processo di selezione delle particelle candidate mediante test di massa.......................................................................................................................42 Illustrazione 14: Calcolo degli indici della tripla in base al thread ID.....................46 Illustrazione 15: Passaggio dalla seconda alla terza fase della ricostruzione...........48 Illustrazione 16: Fisionomia di un evento nel file di input.......................................54 Illustrazione 17: Fisionomia di un evento nel file di output.....................................55 Illustrazione 18: Il server rack Dell PowerEdge R510.............................................57 Illustrazione 19: Grafico delle performance del modulo D0 (K0s Π+ Π- e K0s ΠΠ+)..........................................................................................................................65 Illustrazione 20: Grafico delle performance del modulo D*0 (D0 Π0)....................67 Illustrazione 21: Grafico delle performance della funzione kernel..........................69 Angelo Tebano 566/2750 Pagina 5 di 105 Illustrazione 22: Distribuzione di massa tipica per il modulo K0s...........................71 Illustrazione 23: Distribuzione di massa della variante “multi-evento” per il modulo K0s..........................................................................................................................72 Angelo Tebano 566/2750 Pagina 6 di 105 Indice delle tabelle Tabella 1: Posizione fisica, modalità di accesso e scope per ciascuna delle memorie. ................................................................................................................................. 31 Tabella 2: Le quattro principali fasi nel processo di ricostruzione del mesone B.....36 Tabella 3: Performance test della funzione cudaMalloc(...).....................................59 Tabella 4: Performance test della funzione cudaMemcpy(...)..................................60 Tabella 5: Performance test dell'allocazione delle strutture in uso...........................62 Tabella 6: Performance test del trasferimento delle strutture in uso.........................63 Tabella 7: Performance test sul modulo D0 (K0s Π+ Π- e K0s Π- Π+)...................64 Tabella 8: Performance test sul modulo D*0 (D0 Π0).............................................66 Tabella 9: Performance test della funzione kernel...................................................68 Angelo Tebano 566/2750 Pagina 7 di 105 Introduzione L'obiettivo primario del presente lavoro di tesi è stato la valutazione delle architetture GPU, del paradigma computazionale GPGPU e dell'ambiente di programmazione nVIDIA CUDA, all'interno del progetto SuperB. Il lavoro è da inquadrare in un più ampio e longevo contesto di sperimentazione, a cui il datacenter S.C.o.P.E. dell'Università degli Studi di Napoli “Federico II”, sta dedicando particolare attenzione; presso la sede del data-center sono state svolte le attività di tirocinio che il presente lavoro di tesi mira ad illustrare. Si è cercato di capire quali sono i vantaggi derivanti dall'utilizzo delle GPU come co-processori matematici nel calcolo ad alte prestazioni ed in che misura il paradigma GPGPU, applicato al problema della ricostruzione dei decadimenti del mesone B, tramite CUDA, può contribuire al miglioramento delle performance di algoritmi che, ad oggi, vengono tradizionalmente progettati in logica sequenziale ed eseguiti quindi su normali CPU. Oltre all'implementazione di due nuovi moduli per la ricostruzione del decadimento di particolari particelle, è stato, per la prima volta, affrontato il problema della computazione ciclica di un numero molto consistente di eventi (i principali output dell'esperimento SuperB, nonchè input per la globale ricostruzione dei decadimenti del mesone B). La tesi è articolata nei seguenti capitoli: – Capitolo 1: viene fatta una panoramica del progetto SuperB, illustrando l'entità dell'esperimento, le caratteristiche dell'acceleratore e le esigenze di calcolo; – Capitolo 2: viene descritta l'architettura di una GPU in generale ed analizzato il modello di programmazione GPGPU, con particolare attenzione all'hardware utilizzato per la sperimentazione; Angelo Tebano 566/2750 Pagina 8 di 105 – Capitolo 3: viene proposta una panoramica sull'ambiente di sviluppo nVIDIA CUDA, con riferimenti alla natura e all'utilizzo di alcune importanti funzioni utilizzate; – Capitolo 4: viene analizzata la relazione tra gli scopi dell'esperimento SuperB e l'applicazione di tecniche di programmazione basate sul framework CUDA-C, vengono inoltre illustrati gli algoritmi per la ricostruzione dei decadimenti sviluppati; – Capitolo 5: vengono testati e valutati gli algoritmi illustrati nel Capitolo 4, con particolare attenzione al confronto tra le performance delle versioni sequenziali e quelle delle versioni parallele; – Appendice: viene riportato in versione integrale il codice sorgente sviluppato. Angelo Tebano 566/2750 Pagina 9 di 105 1. L'esperimento SuperB Il progetto SuperB ha come obiettivo la costruzione di un nuovo acceleratore di particelle nell'area dell'Università degli Studi di Roma di Tor Vergata. Una volta che le infrastrutture necessarie saranno disponibili, sarà possibile dare il via al vero e proprio esperimento, che interessa lo studio dei fenomeni naturali, ed attraverso il quale, grazie ai principi della fisica quantistica, sarà possibile comprendere meglio l'evoluzione dell'universo primordiale. Il progetto è a livello internazionale, attualmente vede impegnati oltre 250 tra fisici e ingegneri di diverse nazionalità, ed è stato ampiamente promosso dall'INFN, con un finanziamento diretto da parte del MIUR. La realizzazione del programma consta di diverse fasi: – la progettazione e la realizzazione dell'acceleratore; – la realizzazione dell'apparato sperimentale; – l'acquisizione e l'elaborazione dei dati prodotti. Illustrazione 1: L’acceleratore SuperB. Angelo Tebano 566/2750 Pagina 10 di 105 Tramite la collisione di elettroni e positroni all'interno dell'acceleratore, verrà prodotto un grande numero di particelle pesanti, contenenti quark di diverso sapore. L'acceleratore produrrà 100 volte più collisioni rispetto al limite attuale, ed è quindi facile immaginare quanto sia grande la mole di dati prodotta. La terza delle fasi succitate si sofferma appunto sul trattamento di tali dati, i quali opportunamente elaborati serviranno per la creazione di un nuovo modello della realtà (attualmente è in vigore il Modello Standard) e completeranno altre misure, frutto di esperimenti che verranno fatti al CERN con gli acceleratori LHC ed ILC (attualmente in fase di realizzazione). 1.1. L'acceleratore SuperB Questioni fondamentali della fisica moderna come l'origine e l'evoluzione dell'universo a partire dal Big Bang, l'origine della massa e la natura della materia oscura, potrebbero divenire in parte più chiare proprio grazie all'analisi delle collisioni. Particelle fondamentali del Modello Standard come i quark di sapore “bottom” e “charm”, sono contenute all'interno di particelle più complesse come i Leptoni Tau e i Mesoni B e D. La produzione di queste particelle complesse rappresenta la ragion d'essere dell'acceleratore SuperB. Gli acceleratori di particelle sono detti collisori, proprio perchè fanno scontrare particelle elementari di diversa natura al fine di studiarne le proprietà. Lo schema di funzionamento di SuperB è simile a quello di diversi altri collisori già in uso ed il processo si articola nelle seguenti fasi: – l'iniettore lineare LINAC, primo elemento del sistema, genera ed inietta, con una frequenza di 50Hz, pacchetti di elettroni e positroni in un piccolo anello chiamato Accumulatore. L'Accumulatore ottimizza i pacchetti di particelle in termini di spazio e di energia, infatti, ad ogni giro dell'anello, le particelle perdono parte della loro energia originaria (disperdendo la cosiddetta luce di Angelo Tebano 566/2750 Pagina 11 di 105 sincrotrone) e quest'ultima viene restituita dalla cavità a radiofrequenza alle particelle sotto forma di campo elettromagnetico variabile – 2000 pacchetti da 50 miliardi di elettroni ognuno vengono iniettati in uno degli anelli principali del collisore, altrettanti positroni vengono iniettati nell'altro anello in direzione contraria agli elettroni. I due anelli si incontrano in un solo ed unico punto detto collider hall – il rivelatore, posizionato sul collider hall, acquisisce ed analizza gli effetti delle interazioni. Anche qui ad ogni giro negli anelli principali, la cavità a radiofrequenza restituisce alle particelle l'energia che esse disperdono naturalmente, sottoponendole ad un campo elettromagnetico variabile Illustrazione 2: Strategia di recupero della luce di sincrotrone. Perchè sia più chiaro qual è la dimensione del problema dal punto di vista dell'analisi (e del trattamento in generale) dei dati prodotti da SuperB, si può pensare al suo rivelatore come ad una “macchina fotografica” in grado di acquisire molto rapidamente una “istantanea” a tre dimensioni di quello che succede nel punto di collisione. Angelo Tebano 566/2750 Pagina 12 di 105 Ebbene, a pieno regime si stima che verranno prodotte circa 6 milioni di queste istantanee al minuto, per un flusso di dati calcolato di circa 80 miliardi di bit al secondo. Perchè così tanti dati, quindi così tanti eventi? Proprio perchè SuperB è alla ricerca di interazioni molto rare ed è solo lavorando su un insieme così vasto di situazioni da analizzare che sarà possibile ottenere i risultati sperati. Si stima che in un anno la mole di dati da analizzare impiegherà, come risorse in termini di storage, circa 50 PB (circa 500 PB per l'intero progetto). È chiaro quindi che verranno coinvolte decine di migliaia di unità di calcolo distribuite su tutto il territorio nazionale, che saranno in contatto tra loro tramite una vasta rete digitale tenuta sotto controllo utilizzando le moderne tecnologie GRID, già largamente impiegate al CERN di Ginevra nell'ambito del calcolo ad alte prestazioni. Ecco perchè si è alla ricerca di nuove soluzioni per gestire al meglio e magari aumentare la potenza di calcolo del sistema nel suo complesso. Illustrazione 3: Calcolatori del GRID system del CERN di Ginevra. Angelo Tebano 566/2750 Pagina 13 di 105 2. Le GPU e il modello di programmazione GPGPU L'onerosa attività di ricostruzione degli eventi da cui scaturiscono le esigenze di calcolo di cui si è appena discusso e le attività di simulazione/data-skimming, introducono la necessità di ricercare una alternativa al tradizionale calcolo CPUcore. A completamento dell'esperimento infatti, dopo la fase di acquisizione dei dati, occorrerà un lavoro di calcolo di circa 100.000 volte superiore a quello degli attuali CPU-core. La direzione intrapresa negli ultimi anni è quella di affiancare a tali unità di calcolo classiche, le GPU (Graphics Processing Unit) come coprocessori. Queste ultime in tempi recenti si sono notevolmente evolute dal punto di vista della potenza di calcolo e meglio si prestano all'impiego del parallelismo massiccio. Nelle CPU single-core si è tentato, negli anni, di incrementare sempre di più la frequenza di clock del processore, al fine di aumentare l'attività produttiva dei calcolatori. Tale tendenza ha raggiunto un suo limite naturale nei primi anni 2000, dal momento che hanno cominciato a presentarsi delle serie problematiche relative al consumo di energia ed all'eccessivo surriscaldamento dei chip. Con l'avvento delle CPU multi-core, invece si cerca attualmente di mantenere la velocità di programmi sequenziali, eseguendoli su core multipli; un esempio di CPU multi-core è dato dal moderno processore Intel Core i7 equipaggiato con 4 cores. Processori appartenenti invece alla famiglia dei many-core, come le moderne schede video nVIDIA, dalla serie GeForce 8 in poi, si concentrano sull'esecuzione di un certo numero di operazioni per unità di tempo (nell'ambito stavolta di algoritmi paralleli) su un grande numero di cores. Si pensi che le moderne schede video nVIDIA, a partire da quelle più commerciali, sono equipaggiate con un numero minimo di cores nell'ordine delle centinaia. Angelo Tebano 566/2750 Pagina 14 di 105 Illustrazione 4: Mappa dei moderni sistemi core-based. Le GPU sono inoltre significativamente più veloci e precise nel calcolo floating point ed in grado di trasferire dati ad una velocità di molto superiore rispetto alle CPU. Basti pensare che una comune GeForce serie 8 è in grado di trasferire dati, da se stessa verso la DRAM, ad una velocità di 85 GB/s. Questo perchè ci sono delle differenze di tipo progettuale tra le due tipologie di calcolatori: durante il concepimento di una GPU si è sempre tenuto conto della natura delle operazioni, che sono in genere sempre semplici ma ripetitive; le CPU sono fornite invece di una logica di controllo complessa poiché la natura delle operazioni da svolgere è varia e sono tipicamente dotate di memorie cache molto ampie per ridurre la latenza di accesso alle istruzioni e ai dati da parte dei programmi. Angelo Tebano 566/2750 Pagina 15 di 105 Illustrazione 5: Confronto tra CPU e GPU nel calcolo floating point. Ulteriori dettagli tecnici sulle GPU e sulla loro filosofia progettuale saranno ampiamente trattati nel prossimo paragrafo, ma per il momento si potrebbe essere stati portati a pensare che le GPU superino le CPU in termini di prestazioni su ogni fronte. In realtà non è così, i programmatori che si sono dedicati allo sviluppo di applicativi che utilizzano le schede video per il calcolo numerico spostano tipicamente sulle di esse solo le parti di calcolo particolarmente semplici ma pesanti, ovvero quelle che meglio si prestano ad essere eseguite in parallelo da molte unità di calcolo elementari. Le GPU infatti non saranno mai in grado di soddisfare altrettanto bene esigenze per cui le CPU sono state progettate. Angelo Tebano 566/2750 Pagina 16 di 105 È sfruttando l'interazione tra i due tipi di calcolatori che si ottengono i migliori risultati, eseguendo le parti sequenziali sulla CPU e quelle adatte al massively parallel computing sulla GPU. Dal 2007 nVIDIA ha messo a punto, proprio per soddisfare questo tipo di esigenze di sviluppo, il modello di programmazione CUDA (Compute Unified Device Architecture), che supporta la cooperazione tra CPU e GPU nell'esecuzione dei programmi. Illustrazione 6: Grafico della cooperazione tra moduli CPU e GPU. Angelo Tebano 566/2750 Pagina 17 di 105 2.1. Architettura di una GPU E' necessario, prima di addentrarsi nel dettaglio del modello di programmazione messo a punto da NVIDIA, dare qualche informazione tecnica relativa alle GPU in generale ed al dispositivo sul quale l'entourage del data-center S.C.o.P.E. ha testato e continuerà a testare tutto il codice prodotto per l'esperimento SuperB. Innanzitutto, una qualsiasi GPU evoluta è costituita da array di multiprocessori composti da streaming processors (SP). Per quanto riguarda la memoria poi, la GPU ne dispone di tre tipi, ognuna con funzioni diverse: la global memory, la shared memory e la local memory. Illustrazione 7: Schema di massima di un'architettura GPU thread-based. Angelo Tebano 566/2750 Pagina 18 di 105 La global memory è una GDDR (Graphics Double Data Rate) ad alta capacità (tipicamente nell'ordine dei GB) ma con latenza di accesso più alta rispetto alle altre due, essa è condivisa da tutti i threads di esecuzione ed è utilizzata prevalentemente per il 3D rendering. La shared memory è a disposizione di ogni streaming multiprocessor (SM) quindi SP appartenenti allo stesso SM condividono la stessa shared memory. Infine ogni SP dispone di una memoria privata detta local memory, con capienza molto limitata (viene usata prevalentemente come stack per le istruzioni) ma con latenza di accesso molto bassa. Illustrazione 8: Schema delle memorie di una GPU. Angelo Tebano 566/2750 Pagina 19 di 105 2.2. Il modello GPGPU Il GPGPU (General Purpose computing on Graphics Processor Unit) è un nuovo campo di ricerca informatico che ha come scopo principale, partendo da questi nuovi tipi di hardware, quello di eseguire applicazioni diverse da quelle tradizionalmente grafiche proprio sulle GPU, il che significa riuscire a trarre il maggior vantaggio possibile dal concetto di parallelismo (che è l'anima del GPU computing) nell'eseguire task di natura diversa, come per esempio calcoli matematici particolarmente impegnativi per una CPU. Se si pensa al fatto che, a parità di costo dell'hardware, una GPU può offrire prestazioni nell'ordine di 1 TeraFLOPS quando una CPU arriverà al massimo a 100 GigaFLOPS (senza contare la differenza di consumo energetico che c'è tra le due tipologie di calcolatori) risulta immediato capire che far girare codice general purpose sulle GPU conviene molto di più in termini di tempo. Questa considerazione non ha molto riscontro su applicativi banali, ma tornando ai numeri che abbiamo snocciolato parlando dei dati prodotti dall'esperimento SuperB, è chiaro che mettersi alla ricerca di un sistema per “fare economia” sul tempo di esecuzione del software che analizza tali dati ha molto senso. Il tipo di parallelismo su cui si basano le GPU è una evoluzione concettuale del parallelismo a grana fine di tipo SIMD (Single Instruction Multiple Data); dal momento che l'unità fondamentale eseguita dalla GPU è il thread, si passa al modello SIMT (Single Instruction Multiple Thread). Negli ultimi anni le principali aziende produttrici di chip grafici si sono specializzate nella realizzazione di componenti hardware dedicati al calcolo scientifico, tutte ovviamente basate sul modello SIMT. nVIDIA, con la serie Tesla, ha trovato una soluzione ad hoc con rack che montano più schede video pensate specificatamente per applicazioni GPGPU tramite CUDA. Negli ultimi mesi il data center S.C.o.P.E. ha messo in Angelo Tebano 566/2750 Pagina 20 di 105 funzione due server, entrambi equipaggiati con una nVIDIA Tesla S2050, che verranno utilizzati anch’essi per il progetto SuperB. Il seguente lavoro di tesi è stato svolto proprio su queste due macchine. Illustrazione 9: nVIDIA Tesla S2050. Angelo Tebano 566/2750 Pagina 21 di 105 2.3. nVIDIA Tesla S2050 nVIDIA Tesla S2050 è un sistema di calcolo complesso ma altamente performante, è un dispositivo a corpo unico che si può montare su rack ed è costituito da quattro processori grafici di tipo nVIDIA Fermi. Al sistema si possono connettere uno o due sistemi host tramite interfaccia PCI Express, queste connessioni sono separate, nel senso che ogni switch dell'interfaccia connette all'host due delle quattro GPU. Se si vuole connettere un unico host al dispositivo, ma sfruttando la configurazione completa di quattro GPU, bisogna collegare due cavi alla succitata interfaccia e quindi si rende necessario che l'host abbia due slot PCI Express avviabili. Il corpo esterno della Tesla è conforme alle specifiche standard EIA 310E dei rack per quanto riguarda le dimensioni ed utilizza cavi PCI Express di 50 cm con raggio di curvatura di 3.87 cm per la connessione agli host. Nel dettaglio, le specifiche tecniche di una nVIDIA Fermi sono: – Numero di core: 448; – Processor core clock: 1.15 Ghz; – Memory clock: 1.546 Ghz; – Interfaccia di memoria: 384 bit. Le specifiche tecniche dell’intero sistema Tesla S2050 sono: – Quattro GPU Fermi; – 12.0 GB di memoria GDDR5, 3.0 GB per ognuna delle 4 GPU. – Consumo di corrente: 900W. Il sistema garantisce un picco prestazionale di più di 515 Gigaflops in doppia precisione per ogni GPU, quindi più di 2 Teraflops per l'intera unità. In singola precisione il picco di prestazioni supera il Teraflop per ogni GPU. Angelo Tebano 566/2750 Pagina 22 di 105 Il sistema dispone inoltre di due cache configurabili, una L1 cache per ogni Streaming Multiprocessor ed una L2 cache per ogni processor core del blocco. Il trasferimento dei dati avviene in modalità asincrona attraverso il PCIe bus, cioè i dati possono viaggiare attraverso il bus tranquillamente mentre i cores stanno processando altri dati. Applicazioni che richiedono questo tipo di funzionalità, come i software per l'analisi dell'attività sismica, traggono vantaggio da questa strategia poiché godono della possibilità di massimizzare l'efficienza computazionale trasferendo dati verso la memoria locale prima ancora che tali dati siano necessari. L'ambiente di programmazione CUDA, con diverse APIs ed il completo supporto a diversi linguaggi di programmazione è naturalmente l'anima dell'intero sistema. È possibile scegliere tra C, C++, OpenGL, DirectCompute e Fortran per la realizzazione degli applicativi. Angelo Tebano 566/2750 Pagina 23 di 105 3. L'ambiente di sviluppo CUDA Prima di essere un ambiente di sviluppo, CUDA è una architettura hardware per l'elaborazione in parallelo. CUDA è l'acronimo di Compute Unified Device Architecture, si tratta di uno strumento creato da nVIDIA per lo sviluppo e l'esecuzione di applicazioni general purpose su dispositivi GPU. La maggior parte degli attuali dispositivi nVIDIA supporta CUDA, quindi al programmatore non resta che fare la scelta del linguaggio da utilizzare per la stesura del codice. I linguaggi disponibili in ambiente CUDA sono estensioni di alcuni linguaggi tradizionali, per il seguente lavoro di tesi si è utilizzato CUDA-C, una estensione del linguaggio C. CUDA è anche una API (Application Programming Interface), ovvero una raccolta di strumenti a disposizione del programmatore che si basano sul concetto thread. I threads sono organizzati in gruppi che interagiscono tra loro tramite la condivisione della memoria, inoltre il flusso delle operazioni può essere regolato tramite le barriere di sincronizzazione. Queste astrazioni su cui si basa l'ambiente garantiscono la scalabilità dei problemi, che è automatica, ovvero non è importante per il programmatore conoscere a priori il numero di cores fisici del dispositivo, dal momento che ogni blocco di threads può essere schedulato su ogni tipo di core ed in ogni ordine. Questo implica che un programma CUDA può essere eseguito con un numero qualsiasi di core, è solo il sistema a runtime che ha bisogno di conoscere il numero di processori fisici che ha a disposizione per la computazione. Per quanto riguarda ancora il discorso della cooperazione tra CPU e GPU, un programma in CUDA è niente altro che un listato di istruzioni, alcune adatte ad essere eseguite dalla CPU, altre dalla GPU ed altre ancora che regolano la comunicazione tra quelli che da ora in poi chiameremo spesso host (CPU) e device (GPU). Queste operazioni sono molto importanti e riguardano soprattutto la memoria, gestire opportunamente ed in maniera efficiente questo ultimo aspetto è Angelo Tebano 566/2750 Pagina 24 di 105 fondamentale ai fini delle prestazioni. Lo schema di massima del ciclo di vita di un programma CUDA è riassunto nella seguente illustrazione: Illustrazione 10: Schema del ciclo di vita di un programma CUDA. Angelo Tebano 566/2750 Pagina 25 di 105 3.1. CUDA-C Durante l'esecuzione di un programma CUDA-C i thread invocati eseguono in parallelo una funzione chiamata kernel, la cui dichiarazione avviene mediante l'apposizione, prima del nome della funzione stessa, della parola chiave __global__. Dal kernel è possibile invocare altre funzioni di qualsiasi tipo dette device (dichiarate appunto usando la parola chiave __device__). Un kernel viene lanciato specificando, tramite la sintassi <<<numero_di_blocchi, numero_thread_per_blocco>>>, quanti thread dovranno eseguirlo; i thread saranno poi in grado di svolgere un lavoro piuttosto che un altro all'interno del kernel, auto-identificandosi, ovvero calcolando il proprio ID unico, tramite la variabile di sistema threadIdx, che è un vettore a tre dimensioni di tipo dim3, un tipo specifico di CUDA-C. La variabile threadIdx ha tre campi accessibili tramite i suffissi .x, .y e .z, che consentono di ricavare le “coordinate” del thread all'interno del blocco. Sono necessarie tre dimensioni perchè in CUDA un blocco di thread può essere anche tridimensionale, fino a raggiungere il numero massimo di 1024 thread per tutto il blocco (ciò vuol dire che sono accettate configurazioni come [32,16,2] ma non ad esempio [32,16,3]). I blocchi sono invece parte di una griglia, che può essere monodimensionale o bidimensionale, con dimensione massima di 4294836225 blocchi sulle due dimensioni (la configurazione massima è appunto [65535,65536]). Un esempio di invocazione kernel può essere il seguente: ... MioKernel<<<dim3(10,10,1), dim3(10,10,10)>>> (a,b,c); ... Angelo Tebano 566/2750 Pagina 26 di 105 dove la kernel function viene eseguita in parallelo da 100.000 thread, ovvero da una griglia di 100 blocchi, ogni blocco della quale è costituito da 1000 thread (a,b,c sono parametri della funzione). I blocchi della griglia possono a loro volta ricavare il proprio ID tramite la variabile blockIdx e la loro dimensione tramite la variabile blockDim. Sapendo ora che un thread o un blocco possono ricavare il loro ID unico al fine di potersi auto-identificare per svolgere o meno alcune operazioni [si pensi al caso in cui un blocco di istruzioni nel kernel sia condizionale, ovvero abbia in testa una istruzione del tipo if(threadID==1254)], si presenta l'esigenza di poter sincronizzare l'attività, se magari si desidera che da un certo punto in poi del listato i thread partano ad eseguire le istruzioni esattamente in contemporanea. Questo è possibile grazie alla procedura __syncthreads(), che quando viene invocata, funge da barriera di sincronizzazione, ovvero l'esecuzione delle istruzioni da parte di tutti i thread riparte solo quando tutti i thread hanno raggiunto questa chiamata. Illustrazione 11: Composizione di una grigia e di un blocco nel caso bidimensionale. Angelo Tebano 566/2750 Pagina 27 di 105 3.1.1. Compilazione di un programma CUDA-C Il linguaggio C non è l'unico linguaggio di programmazione in cui poter scrivere applicazioni CUDA, come non lo sono neanche i linguaggi C++, OpenGL, DirectCompute e Fortran, cui si è fatto riferimento nel paragrafo 2.3. È possibile scrivere un programma CUDA sotto forma di istruzioni dell'architettura chiamate PTX (PTX è esso stesso un linguaggio assemblativo). Nel caso in cui il programma sia scritto in C, è necessario che il compilatore trasformi il tutto in un codice binario, che altro non è che la traduzione delle succitate istruzioni di architettura PTX. Il compilatore a questo punto ha dunque bisogno di una direttiva, la Compute Capability del device: una coppia di numeri che indica il tipo di architettura dei core della periferica ed un progressivo di revisione che indica i miglioramenti introdotti per quel tipo di architettura. Un esempio di compilazione, utilizzando naturalmente il compilatore nvcc, è: nvcc -arch sm_20 Programma.cu -o Eseguibile dove Programma.cu è il nostro sorgente (l'estensione .cu è obbligatoria), Eseguibile è il nostro programma in output e -arch sm_20 è un esempio della succitata direttiva per la Compute Capability del device. Angelo Tebano 566/2750 Pagina 28 di 105 3.1.2. Gestione della memoria in CUDA-C Un thread in ambiente CUDA, durante l'esecuzione di un kernel, ha accesso a diversi tipi di memoria. Innanzitutto tutti i thread che eseguono il kernel hanno accesso alla global memory; i thread appartenenti ad uno stesso blocco hanno accesso alla shared memory del blocco; ogni thread ha a disposizione una local memory privata. Inoltre esistono altri due tipi di memoria accessibili in sola lettura da tutti i thread in esecuzione: la constant memory e la texture memory. Di seguito verranno analizzati i primi tre tipi di memoria citati. Su un multiprocessore vengono eseguiti, per ogni ciclo di clock, 32 thread alla volta (tale gruppo di thread rappresenta un'unità chiamata warp). L'unità cui si fa riferimento per gli accessi alla global memory è l'half-warp, ovvero un gruppo di 16 thread. La global memory è accessibile in maniera coalescente (ciò significa che è in grado di accorpare più scritture e più letture in un unico ciclo di clock) a seconda della Compute Capability del device. Per chip grafici con Compute Capability 1.0 o 1.1, ad esempio, gli accessi alla global memory di un half-warp sono coalescenti se leggono un'area contigua di memoria di 64,128 o 256 byte; l'indirizzo iniziale di una regione deve essere multiplo della grandezza della regione e gli accessi devono essere perfettamente allineati. É facile intuire che in taluni casi è complicato sottostare a questa serie di regole di accesso, quindi il modo più semplice per ridurre gli accessi non coalescenti è sfruttare la shared memory, ricopiando i dati nella memoria globale prima dell'uscita dal kernel. La shared memory ha una larghezza di banda maggiore ed una latenza di accesso minore rispetto alla global memory, essendo una memoria on-chip. Essa è divisa in moduli di uguale dimensione chiamati banchi a cui si può accedere simultaneamente, cioè se viene effettuata una richiesta di lettura o scrittura, con N indirizzi che ricadono in N banchi diversi, la shared memory soddisfa le richieste Angelo Tebano 566/2750 Pagina 29 di 105 simultaneamente. Per richieste con indirizzi che ricadono nello stesso banco, la shared memory cerca comunque di applicare la simultaneità; qualora si dovesse creare un bank-conflict, la shared memory effettua una rapidissima analisi per individuare le aree di memoria conflict-free e serializza le risposte organizzandole in base a questa suddivisione. La local memory consente accessi completamente coalescenti, ma risiedendo sulla memoria del device, ha stessa latenza di accesso e la stessa larghezza di banda della global memory. Parole consecutive da 32 bit sono accessibili a thread con ID consecutivo, per esempio nella lettura di un array, se tutti i thread di un warp richiedono in lettura uno stesso indice dell'array, vengono serviti simultaneamente. Le principali funzioni messe a disposizione da CUDA per la gestione della memoria sono le seguenti: – cudaMalloc(...), funzione per l'allocazione della memoria sul device, alla quale va specificato il puntatore all'area di memoria da allocare e la dimensione in bytes dell'area; – cudaMemcpy(...), per il trasferimento dati tra host e device, alla quale va specificato il puntatore all'area di memoria da copiare, la dimensione in bytes dell'area, il puntatore all'area di memoria in cui copiare ed il verso del trasferimento, che può essere HostToDevice, DeviceToHost e DeviceToDevice; – cudaFree(...), che dealloca la memoria allocata in precedenza servendosi del solo puntatore all'area di memoria da liberare; – cudaMemset(...), che inizializza un'area di memoria (indirizzata da un puntatore che la funzione prende in input) ad un dato valore da specificare; – cudaMallocPitch(...), una funzione per l'allocazione di array a 2 e a 3 dimensioni che permette automaticamente di aumentare la coalescenza delle transazioni; per raggiungere l'allineamento di memoria che riduca il più Angelo Tebano 566/2750 Pagina 30 di 105 possibile il numero di transazioni, viene calcolato il pitch, un valore che esprime quale è la lunghezza massima in byte che dovrebbe avere una linea della matrice per rispettare le regole degli accessi coalescenti; in base al pitch una matrice viene allocata, sulla memoria del device, linea per linea ogni pitch byte. Tabella 1: Posizione fisica, modalità di accesso e scope per ciascuna delle memorie. Angelo Tebano 566/2750 Pagina 31 di 105 3.1.3. Qualificatori ed operazioni atomiche Le diverse tipologie di funzioni all'interno di un programma CUDA-C vanno identificate anteponendo alla loro dichiarazione alcune parole chiave dette qualificatori. Come anticipato in precedenza (paragrafo 3.1), esistono tre qualificatori per le funzioni: – __global__ , identifica la funzione kernel; – __device__ , identifica una funzione della GPU, tipicamente viene invocata in parallelo all'interno di un kernel; – __host__ , identifica una funzione dell'host, viene eseguita solo dalla CPU come una normale funzione in linguaggio C. Come per le funzioni, anche le variabili vanno identificate a mezzo di qualificatori. I qualificatori più importanti per le variabili sono quelli che permettono di definire la destinazione di allocazione ed uso della variabile stessa: – __device__ , determina l'allocazione della variabile sulla global memory; dopo la dichiarazione di una variabile di questo tipo viene tipicamente effettuata una cudaMalloc(...) e per tutto il tempo di vita dell'applicazione la variabile risulterà accessibile da tutti i thread in esecuzione; – __shared__ , determina l'allocazione della variabile sulla shared memory; questo tipo di variabili sono allocate a tempo di compilazione o runtime a seconda dei casi e sono accessibili a tutti i thread appartenenti ad uno stesso blocco; la variabile viene distrutta quando termina la funzione kernel. Per quanto riguarda il tipo nativo delle variabili, abbiamo a disposizione alcuni tipi standard del C per gli scalari: char, short, int, long, float e double (ad eccezione di float e double, i primi quattro sono disponibili anche nelle varianti unsigned). Per i vettori invece i tipi a disposizione sono: char[...], short[...], int[...], long[...] (anche nella variante unsigned) e float[]. Questa seconda famiglia di variabili però, è Angelo Tebano 566/2750 Pagina 32 di 105 fortemente customizzata in base alla versione del CUDA Toolkit in uso, sempre per ragioni di ottimizzazione nella gestione delle aree di memoria e di riduzione dei tempi di accesso. Sempre relativamente agli scalari è da segnalare che il tipo double è disponibile solo dalla versione 3 in poi del Toolkit. Può presentarsi la necessità di fare in modo che una variabile venga aggiornata in maniera mutuamente esclusiva dai thread, per evitare conflitti e preservare la coerenza del dato durante l'esecuzione del programma. La già discussa funzione di sincronizzazione __synchthreads() (paragrafo 3.1) non soddisfa appieno questa esigenza. Si pensi alla necessità di effettuare l'incremento di una variabile di tipo intero allocata sulla global memory: è possibile che in maniera concorrente siano moltissimi i thread a cercare di incrementare tale variabile, creando inevitabilmente un collo di bottiglia o, facendo in modo che il valore finale della variabile in qualche modo non sia quello atteso. A tale proposito CUDA consente di effettuare operazioni atomiche, queste operazioni vengono effettuate in maniera seriale appunto per evitare problemi di sincronizzazione. Nel succitato caso specifico, ad esempio, la variabile verrebbe incrementata utilizzando la funzione int atomicAdd(int* address, int val), in grado atomicamente di sommare alla variabile di indirizzo address la quantità val. Oltre ad atomicAdd(...) esistono molte altre funzioni per operazioni atomiche, capaci di effettuare aggiornamenti di tipo associativo, algebrico, logico, comparativo, ecc.. Angelo Tebano 566/2750 Pagina 33 di 105 3.1.4. Gestione degli Eventi e degli errori CUDA-C mette a disposizione del programmatore degli strumenti per analizzare i progressi di una elaborazione sul device. Il caso più classico è quello in cui il programmatore voglia inserire delle righe di codice specifiche per la misurazione del tempo di esecuzione del kernel, di una singola funzione o di qualsiasi altra sezione di codice a sua discrezione. Ciò è possibile tramite la registrazione degli eventi, variabili particolari che possono essere aggiornate in determinati punti del programma tramite alcune funzioni CUDA. Un evento è sempre una variabile di tipo cudaEvent_t, funzioni di particolare interesse relative agli eventi sono: cudaEventCreate(...), cudaEventRecord(...), cudaEventSynchronize(...) e cudaEventElapsedTime(...). Il seguente esempio chiarisce meglio l'utilizzo di queste funzioni, il listato riportato si occupa di registrare tramite degli eventi il tempo di esecuzione del blocco di codice {…} : cudaEvent_t start, stop; cudaEventCreate(&start); cudaEventCreate(&stop); cudaEventRecord(start, 0); {...} cudaEventRecord(stop, 0); cudaEventSynchronize(stop); float time; cudaEventElapsedTime(&time, start, stop); Dopo la chiamata alla funzione cudaEventElapsedTime(...), nella variabile time, sarà memorizzato il tempo in millisecondi trascorso tra la registrazione di start e quella di stop, quindi il tempo di esecuzione del blocco {…}. Angelo Tebano 566/2750 Pagina 34 di 105 L'esecuzione di una qualsiasi funzione __device__ in CUDA-C termina con la restituzione, da parte della funzione al chiamante, di un valore di tipo cudaError_t. Questo valore è un codice di errore che informa il chiamante sugli eventuali errori di esecuzione della funzione, esso viene in ogni caso ritornato, poiché anche la circostanza di mancato errore è codificata. Nel codice host è possibile acquisire tale codice di errore mediante la funzione cudaError_t cudaGetLastError(...), quindi stamparlo, dopo averlo opportunamente tradotto in stringa, come segue: printf(“%s\n”, cudaGetErrorString(cudaGetLastError())); La funzione cudaGetErrorString(...) restituisce sotto forma di stringa una descrizione dell'errore. Angelo Tebano 566/2750 Pagina 35 di 105 4. SuperB e il problema della ricostruzione degli eventi In fisica, con il nome mesone si indica una famiglia di particelle subatomiche instabili costituite rispettivamente da un quark bottom e da un antiquark che può avere quattro diversi sapori: up (B-), down (B0), strange (B0s) oppure charm (B-c). Un particolare mesone è il mesone B, composto invece da un antiquark bottom e da un quark di sapore up (B+), down (B0), strange (B0s) oppure charm (B+c). Ricostruire il mesone B significa, dopo che è avvenuta la collisione di elettroni e positroni all'interno dell'acceleratore, partire dalle osservazioni e cercare di ricomporre gerarchicamente un mesone seguendo uno dei migliaia di canali diversi di decadimento ottenendo la massima efficienza. L'approccio è gerarchico poiché il processo di ricostruzione è scomposto in quattro fasi principali, ognuna caratterizzata dalla presenza di determinate particelle coinvolte: Tabella 2: Le quattro principali fasi nel processo di ricostruzione del mesone B. La ragione di questa scomposizione in fasi è molto semplice: siccome i canali di decadimento del mesone sono migliaia, tramite questo approccio è possibile effettuare dei tagli nella fase di calcolo in cui ci si trova, al fine di ridurre, nella fase successiva, il range di dati da elaborare. In altri termini, dopo aver sottoposto l'input iniziale al calcolo per la ricostruzione delle particelle della prima fase, è possibile Angelo Tebano 566/2750 Pagina 36 di 105 escluderne un numero molto importante, andandone a controllare la massa. Questo significa che, di tutte le possibili “ricomposizioni” nella prima fase, solo alcune corrisponderanno a possibilità reali di decadimento. Queste particelle selezionate fungono a loro volta da punto di partenza per l'avvio della seconda fase di elaborazione. Seguendo lo stesso ragionamento si procede con le fasi successive. Gli eventuali mesoni B, ricostruiti nell'ultima fase, corrispondono necessariamente ad un cammino di decadimento per il quale sono state soddisfatte, attraverso le varie fasi, le condizioni di massa da parte delle particelle coinvolte. I tagli effettuati durante le prime tre fasi avranno portato all'esclusione di un numero non trascurabile di cammini possibili, evitando quindi di appesantire l'elaborazione con calcoli non necessari. Controllare la massa delle particelle coinvolte durante le fasi, significa verificare che, una volta trovata una particella candidata, la sua massa sia compresa in un range specifico che varia in base alla natura della particella stessa. Ad esempio, note due quantità che chiameremo M0 (massa di riferimento) e Δ (delta), esaminiamo il caso in cui, nella prima fase, si vada a valutare una particella Π0 candidata ad essere input per la fase successiva: – nel caso di Π0 M0 vale 0,1349 e Δ vale 0,020; – si calcola il quadrato della massa (Mc)2 della candidata Π0 tramite la seguente formula: [ (Mc)2 = (Energia)2 - (CoordinataX)2 - (CoordinataY)2 - (CoordinataZ)2 ]; – se Mc = √(Mc)2 è compresa tra M0-Δ ed M0+Δ la particella viene conservata, altrimenti scartata. Il seguente lavoro di tesi esula dalla scrittura dell'intero algoritmo di ricostruzione del mesone B, soffermandosi invece su due parti intermedie: il calcolo di una particolare particella D0, chiamata D0|K0s2Π (seconda fase) e quello di una particolare particella D*0, chiamata D*0|D0Π0 (terza fase). Angelo Tebano 566/2750 Pagina 37 di 105 Illustrazione 12: I quattro stadi della ricostruzione completa del mesone B. 4.1. Strutture dati utilizzate Gli algoritmi sviluppati prendono in input degli insiemi di particelle, ognuna caratterizzata da: – un valore di Energia – tre coordinate spaziali Le particelle che fungono da input per la prima fase del processo di ricostruzione sono: – i fotoni ɣ – i pioni Π+ – i pioni Π- Angelo Tebano 566/2750 Pagina 38 di 105 I kaoni K+ e K- sono dei particolari mesoni caratterizzati dal numero quantico della stranezza (sapore strange), essi condividono con i pioni e con i fotoni la struttura delle componenti. Per i tipi di particelle appena citate (ɣ , Π+ , Π- , K+ , K-) viene quindi utilizzata la medesima struttura dati, che appare così: typedef struct { float Ene; //Energia float x; //Componente spaziale x float y; //Componente spaziale y float z; //Componente spaziale z char gen; //Genere }QVECT; Vista la forma della struttura, QVECT sta per “quadrivettore”; il campo “gen” è un carattere settato a 'k' o ad 'a' a seconda che si tratti di un kaone o di un pione/fotone. Un insieme di particelle è rappresentato tramite un array di strutture QVECT. Tutte le particelle complesse (ricavate dalla composizione di quelle iniziali) rispettano la struttura vista sopra con, in aggiunta, due, tre o quattro campi genitore, a seconda del numero di particelle generatrici. Un campo genitore (g1, ..., g4) è un indice dell'insieme di appartenenza (ovvero dell'array di quadrivettori) di una particella generatrice. Angelo Tebano 566/2750 Pagina 39 di 105 4.2. Moduli di calcolo preesistenti Come emerso finora, il seguente lavoro di tesi è da inquadrare in un contesto applicativo molto più ampio. Il data-center S.C.o.P.E., partner del progetto SuperB, sta da tempo, tramite il lavoro di ricercatori e di altri tesisti, contribuendo allo sviluppo del meta-modulo di calcolo del decadimento del mesone B, sempre orientandosi verso l'applicazione del parallelismo GPGPU. Partendo dal lavoro svolto in precedenza dagli altri tesisti, si è potuto disporre, in questa fase del progetto, dei seguenti moduli di calcolo già messi a punto e testati: – modulo per la ricostruzione dei mesoni neutri Π0 (prima fase); – modulo per la ricostruzione dei kaoni neutri di tipo breve K0s (prima fase); – modulo per la ricostruzione dei mesoni D0 (K+ Π- e K- Π+) (seconda fase); – modulo per la ricostruzione dei mesoni D0 (K+ Π- Π0 e K- Π+ Π0) (seconda fase); – modulo per la ricostruzione dei mesoni D0 (K+ Π- Π- Π+ e K- Π+ Π+ Π-) (seconda fase). Nei paragrafi che seguono vengono proposti due algoritmi che rappresentano la parte più importante del seguente lavoro di tesi: – modulo per la ricostruzione dei mesoni D0 (K0s Π+ Π- e K0s Π- Π+) (seconda fase); – modulo per la ricostruzione dei mesoni D*0 (D0 Π0) (terza fase). Angelo Tebano 566/2750 Pagina 40 di 105 4.3. Modulo per la ricostruzione di Mesoni D0 (K0s Π+ Π- e K0s Π- Π+) Per valutare l'architettura nVIDIA cui si è fatto riferimento nel paragrafo 2.3, è stato scritto un algoritmo per la ricostruzione del decadimento di un particolare mesone D0, quello formato da particelle di tipo K0s Π+ Π- e K0s Π- Π+. Dapprima è stata messa a punto e testata una versione sequenziale dell'algoritmo, poi si è valutato quali parti di codice potevano essere parallelizzate, secondo i principi cardine del parallelismo SIMD. L'algoritmo prende, sia nel caso sequenziale che in quello parallelo, in ingresso due insiemi di particelle iniziali (i pioni Π+ e Π-) ed un insieme di particelle derivate (i kaoni neutri di tipo breve K0s). Il secondo insieme è costituito dall'output del modulo preesistente cui si è fatto riferimento nel paragrafo 4.2. Tutti gli insiemi di particelle in input sono rappresentati come array di strutture nella forma discussa in precedenza. L'output, ovvero le particelle ricostruite, vengono memorizzate in un ulteriore array, tenendo traccia, in ogni struttura, delle particelle generatrici (sotto forma di indici appartenenti agli insiemi in input); tale array funge a sua volta da input ai moduli della fase successiva. I moduli ricevono in input le particelle K0s, Π+, Π- e le combinano a triple senza ripetizioni, ovvero le componenti spaziali e l'energia di kaoni e pioni vengono sommate per creare un candidato che verrà accettato o meno come particella di output a seconda di quale sia l'esito del test di massa. Il test consiste nel confrontare la massa della particella candidata con la massa di riferimento cui si è accennato in precedenza (l'intervallo [M0-Δ , M0+Δ]), che viene presa anch'essa in input poiché varia a seconda del tipo di particella da ricostruire. Angelo Tebano 566/2750 Pagina 41 di 105 Illustrazione 13: Il processo di selezione delle particelle candidate mediante test di massa. Và da sé che, valutando tutte le possibili combinazioni senza ripetizione tra i tre array in input, vengano calcolati sia i candidati dell'insieme K0s Π+ Π- che quelli dell'insieme K0s Π- Π+, essendo l'operazione tra gli elementi, una normale somma aritmetica delle componenti, per cui l'ordine degli array Π- e Π+ non è rilevante. Angelo Tebano 566/2750 Pagina 42 di 105 4.3.1. Strategia sequenziale L'algoritmo sequenziale prende in ingresso i due array di quadrivettori Π+ (che chiameremo PIp), Π- (che chiameremo PIm) e l'array di quadrivettori K0s (che chiameremo K0s) con le relative dimensioni. Restituisce in output l'array dei risultati D0Ks2PI che inizialmente è vuoto, poiché l'allocazione della memoria per le particelle risultanti viene effettuata in maniera dinamica. L'algoritmo scorre gli array in input con un sistema di tre cicli for innestati: – il primo ciclo scorre l'array delle Π+ servendosi dell'indice i; – il secondo ciclo scorre l'array delle Π- servendosi dell'indice j; – il terzo ciclo, quello più interno e deputato al calcolo vero e proprio, scorre l'array delle K0s servendosi dell'indice k. Nel ciclo più interno si giunge quindi alla situazione di calcolo e di esame della particella candidata. In testa al programma è stata dichiarata una struttura di tipo QVECT di appoggio chiamata Candidato che a questo punto viene valorizzata come segue: Candidato.x= PIp[i].x + PIm[j].x + K0s[k].x; Candidato.y= PIp[i].y + PIm[j].y + K0s[k].y; Candidato.z= PIp[i].z + PIm[j].z + K0s[k].z; Candidato.Ene= PIp[i].Ene + PIm[j].Ene + K0s[k].Ene; Questa valorizzazione è però soggetta a due condizioni espresse tramite degli if: – la particella PIp e quella PIm in esame non devono essere di genere “k”; – la particella PIp e quella PIm in esame devono essere diverse da quelle che hanno generato al livello precedente la particella K0s in esame. Angelo Tebano 566/2750 Pagina 43 di 105 Viene a questo punto calcolata la massa della particella Candidato e, se questa massa è compresa nel range in input, viene allocato, tramite la funzione realloc() del C, lo spazio di memoria necessario per la memorizzazione definitiva della particella ricostruita nell'array D0Ks2PI. Dopo la memorizzazione della particella viene aggiornata la dimensione dell'array di output come segue: if(massa>M0-del && massa<M0+del){ //allocazione della memoria su D0Ks2PI D0Ks2PI_host=(D0KPI_PI0*)realloc(D0Ks2PI_host, ((*dimD0Ks2PI)+1)*sizeof(D0KPI_PI0)); //memorizzazione dei campi quadrivettore D0Ks2PI_host[*dimD0Ks2PI].x=Candidato.x; D0Ks2PI_host[*dimD0Ks2PI].y=Candidato.y; D0Ks2PI_host[*dimD0Ks2PI].z=Candidato.z; D0Ks2PI_host[*dimD0Ks2PI].Ene=Candidato.Ene; //memorizzazione della genesi della particella D0Ks2PI_host[*dimD0Ks2PI].g1=k; D0Ks2PI_host[*dimD0Ks2PI].g2=i; D0Ks2PI_host[*dimD0Ks2PI].g3=j; //incremento della dimensione dell'array *dimD0Ks2PI=(*dimD0Ks2PI)+1; } Altra operazione importante è la memorizzazione degli indici delle particelle generatrici, informazione che sarà utile ad altri moduli come lo è stata qui, per l'esclusione di certe combinazioni non valide. Angelo Tebano 566/2750 Pagina 44 di 105 4.3.2. Strategia parallela É fuori da ogni dubbio che, in una situazione come quella in esame, le operazioni che vanno sicuramente parallelizzate sono quelle di somma aritmetica delle componenti delle particelle. Il calcolo effettuato è sempre lo stesso (componente per componente) ma viene ripetuto per un numero molto grande di oggetti (le triple appartenenti all'insieme {K0s} x {Π+} x {Π-}). Questo è l'ambiente naturale per l'applicazione del parallelismo SIMD (SIMT nel caso di CUDA). La struttura del programma parallelo è influenzata dalla suddivisione del lavoro tra host (CPU) e device (GPU) ed assume questa forma: – calcolo dei thread necessari in base alle dimensioni dell'input; – allocazione delle strutture dati necessarie sia sull'host che sul device; – caricamento delle strutture allocate; – trasferimento delle strutture inizializzate dall'host al device; – invocazione della funzione kernel sul device; – trasferimento dell'output dal device all'host; – stampa/archiviazione dei risultati. L'algoritmo parallelo per la ricostruzione del decadimento del mesone D0 (K 0s Π+ Π- e K0s Π- Π+) è rappresentato dalla funzione __device__ void createD0Ks2PI(...). Tale funzione viene invocata all'interno del kernel, quindi quando è noto il numero di thread necessari ad eseguirla e quando le strutture iniziali sono già state trasferite verso il device. Inoltre l'invocazione di questa funzione avviene successivamente a quella della funzione __device__ void createK0s(...) che produce l'array K0s, anch'esso preso in ingresso dal modulo in esame. Tra le due invocazioni viene effettuata una sincronizzazione tramite la funzione __syncthreads() ad evitare che al momento dell'invocazione di createD0Ks2PI(...) vi siano dei thread ancora impegnati nell'esecuzione di createK0s(...). Angelo Tebano 566/2750 Pagina 45 di 105 Essendo i thread non lanciati in ordine, è stato necessario un algoritmo per il calcolo degli indici su cui operare. Nello specifico, ogni thread è in grado, a partire dal prorpio ID, di ricavare una tripla univoca di indici i,j,k che corrisponde (sempre in termini di posizione all'interno degli array di input) alla tripla di particelle da sommare. Il calcolo viene effettuato come segue: block_thread_id=threadIdx.x+blockDim.x*threadIdx.y; tid=blockDim.x*blockDim.y*(blockIdx.x+gridDim.x*blockIdx. y)+block_thread_id; tid2=tid/dimK0s; i=tid2/dimPIM; //indice PIp j=tid2-((dimPIM-1)*i)-i ; //indice PIm k=tid%dimK0s; //indice K0s L'illustrazione che segue testimonia la correttezza di questa indicizzazione (numero PIp = 4; numero PIm = 2; numero K0s = 3). Illustrazione 14: Calcolo degli indici della tripla in base al thread ID. Angelo Tebano 566/2750 Pagina 46 di 105 Anche in questa versione parallela ritroviamo una struttura di tipo QVECT di appoggio chiamata Candidato che a questo punto viene valorizzata come segue: Candidato.x= PIp[i].x + PIm[j].x + K0s[k].x; Candidato.y= PIp[i].y + PIm[j].y + K0s[k].y; Candidato.z= PIp[i].z + PIm[j].z + K0s[k].z; Candidato.Ene= PIp[i].Ene + PIm[j].Ene + K0s[k].Ene; Questa valorizzazione è sempre soggetta alle due condizioni di genere e di genesi della versione sequenziale. A questo punto l'algoritmo procede con il calcolo ed il controllo della massa della particella e, qualora la candidata sia valida la memorizza. In questa versione parallela si dispone di un array di output già allocato, poiché è stato necessario predisporsi alla gestione dell'output già a partire dall'host. Tale array ha una dimensione che la nostra funzione conosce e che, dopo la memorizzazione della particella, deve incrementare, come avviene nella versione sequenziale. In ambiente parallelo però, le stesse istruzioni a volte non possono essere eseguite da più thread contemporaneamente. È il caso appunto di questa variabile di dimensione dell'array, che è un dato condiviso da tutti i thread ed è facilmente immaginabile cosa potrebbe accadere se più thread cercassero di incrementare questa stessa variabile contemporaneamente. In questa circostanza è stata quindi utilizzata una delle funzioni citate nel paragrafo 3.1.3, la funzione atomicAdd() di CUDA, che consente l'incremento della dimensione dell'array di output in maniera mutuamente esclusiva da parte dei thread, senza quindi pericolo di conflitti o colli di bottiglia. Angelo Tebano 566/2750 Pagina 47 di 105 4.4. Modulo per la ricostruzione di Mesoni D*0 (D0 Π0) La filosofia di progettazione del seguente modulo di ricostruzione è stata analoga a quella appena vista, ma con qualche differenza relativa alla situazione raggiunta a livello di quadro generale. Essendo questo modulo D*0 il primo di una nuova fase dell'elaborazione (la terza), ci si è trovati a dover raccogliere gli output provenienti da quattro diversi moduli D0 in una sorta di collector ideale, come si può osservare dalla seguente illustrazione: Illustrazione 15: Passaggio dalla seconda alla terza fase della ricostruzione Si è voluto però evitare, per ragioni legate al tempo di allocazione delle strutture, di effettuare la copia dei quattro array di output della seconda fase in un nuovo array D0 (collector) da utilizzare come input, assieme a Π0, per il seguente modulo, anche se sarebbe stata la scelta più agevole per quanto riguarda l'indicizzazione dei thread nella versione parallela. Angelo Tebano 566/2750 Pagina 48 di 105 4.4.1. Strategia sequenziale L'algoritmo sequenziale prende in ingresso i quattro array di quadrivettori D0 (che chiameremo D0KPI, D0KPIPI0, D0Ks2PI e D0K3PI) e l'array di quadrivettori Π0 (che chiameremo PI0) con le relative dimensioni. Restituisce in output l'array dei risultati Ds0D0PI0 che inizialmente è vuoto, poiché l'allocazione della memoria per le particelle risultanti viene effettuata in maniera dinamica. L'algoritmo scorre gli array in input con un sistema di quattro cicli for indipendenti: – il primo ciclo scorre l'array delle D0KPI combinando tutti i suoi elementi con quelli dell'array PI0 (tramite un ulteriore ciclo innestato); – il secondo ciclo scorre l'array delle D0KPIPI0 combinando tutti i suoi elementi con quelli dell'array PI0 (tramite un ulteriore ciclo innestato); – il terzo ciclo scorre l'array delle D0Ks2PI combinando tutti i suoi elementi con quelli dell'array PI0 (tramite un ulteriore ciclo innestato); – il quarto ciclo scorre l'array delle D0K3PI combinando tutti i suoi elementi con quelli dell'array PI0 (tramite un ulteriore ciclo innestato). Nei cicli interni si giunge quindi alla situazione di calcolo e di esame della particella candidata. In testa al programma è stata dichiarata una struttura di tipo QVECT di appoggio chiamata Candidato che viene valorizzata come visto in precedenza. La valorizzazione è però soggetta ad una condizione, espressa tramite un if, nel caso del secondo ciclo for: – la particella PI0 in esame deve essere diversa da quella che ha generato al livello precedente la particella D0KPIPI0 in esame. Viene a questo punto calcolata la massa della particella Candidato e, se questa massa è compresa nel range in input, viene allocato, tramite la funzione realloc() del C, lo spazio di memoria necessario per la memorizzazione definitiva della Angelo Tebano 566/2750 Pagina 49 di 105 particella ricostruita nell'array Ds0D0PI0. Dopo la memorizzazione della particella viene aggiornata la dimensione dell'array di output. Altra operazione importante è la memorizzazione degli indici delle particelle generatrici, informazione che sarà utile ad altri moduli come lo è stata qui, per l'esclusione di certe combinazioni non valide. In questo frangente, i campi genitore della struttura dati in cui viene memorizzata la particella ricostruita, assumono un significato leggermente diverso rispetto allo schema visto in precedenza. Nello specifico ritroviamo tre campi genitore (g1, g2 e g3) che vengono valorizzati secondo le seguenti regole: – il campo g1 assume come valore un intero compreso nell'insieme {0, 1, 2, 3} a seconda di quale sia l'array D0 del livello precedente che ha generato la particella; più precisamente assume valore: ◦ 0 se si tratta di D0KPI; ◦ 1 se si tratta di D0KPIPI0; ◦ 2 se si tratta di D0Ks2PI; ◦ 3 se si tratta di D0K3PI; – il campo g2 assume come valore l'indice della particella D0 all'interno dell'array g1; – il campo g3 assume come valore l'indice della particella all'interno dell'array PI0. Angelo Tebano 566/2750 Pagina 50 di 105 4.4.2. Strategia parallela Anche in questo caso, le operazioni che vanno sicuramente parallelizzate sono quelle di somma aritmetica delle componenti delle particelle. Il calcolo effettuato è sempre lo stesso (componente per componente) ma viene ripetuto per un numero molto grande di oggetti (le coppie appartenenti all'insieme {D0} x {Π0}). L'algoritmo parallelo per la ricostruzione del decadimento del mesone D*0 (D0 Π 0) è rappresentato dalla funzione __device__ void createDs0D0PI0(...). Prima della chiamata a tale funzione (sempre a cura della funzione kernel, quindi in ambiente GPU), viene effettuata una sincronizzazione tramite la funzione __syncthreads() ad evitare che, al momento dell'invocazione di createDs0D0PI0(...), vi siano dei thread ancora impegnati nelle precedenti elaborazioni. Essendo i thread non lanciati in ordine, è stato necessario un algoritmo per il calcolo degli indici su cui operare. Nello specifico, ogni thread è in grado, a partire dal prorpio ID, di ricavare una coppia univoca di indici che corrisponde (sempre in termini di posizione all'interno degli array di input) alla coppia di particelle da sommare. In base all'ID ogni thread sa esattamente “quale particella di quale array D0” combinare con la propria particella PI0 “di competenza”. Il semplice codice sorgente dell'algoritmo di indicizzazione appena citato è disponibile nell'Appendice del seguente lavoro di tesi. Anche in questa versione parallela ritroviamo una struttura di tipo QVECT di appoggio chiamata Candidato che a questo punto viene valorizzata sempre rispettando la condizione di genesi vista nella versione sequenziale. Angelo Tebano 566/2750 Pagina 51 di 105 A questo punto l'algoritmo procede con il calcolo ed il controllo della massa della particella e, qualora la candidata sia valida la memorizza. In questa versione parallela si dispone di un array di output già allocato, poiché è stato necessario predisporsi alla gestione dell'output già a partire dall'host. Tale array ha una dimensione che la nostra funzione conosce e che, dopo la memorizzazione della particella, deve incrementare, come avviene nella versione sequenziale. Questa operazione viene effettuata ancora una volta facendo uso della funzione atomicAdd() di CUDA, che consente l'incremento della dimensione dell'array di output in maniera mutuamente esclusiva da parte dei thred, al fine di evitare le situazioni di conflitto cui si è fatto riferimento nei paragrafi 3.1.3 e 4.3.2. Angelo Tebano 566/2750 Pagina 52 di 105 4.5. Una variante “multi-evento” degli algoritmi di ricostruzione dei decadimenti del mesone B per l'analisi di input reali Come si è potuto osservare, tutti gli algoritmi sviluppati finora vengono invocati dalla funzione kernel sotto forma di funzioni distinte, seguendo la logica delle fasi. La funzione kernel stessa, viene lanciata dall'host dopo l'allocazione delle strutture necessarie ed il conseguente trasferimento dei dati. Un ulteriore passo in avanti nello sviluppo del meta-modulo di calcolo consiste nel dare una forma specifica a questi dati e nel cominciare a considerarli e manipolarli per quello che rappresentano in un modello più realistico di quello usato finora. In tutto il codice preesistente gli array delle particelle iniziali sono sempre stati inizializzati con dei valori pseudo-casuali. In questa fase invece si è iniziato a pensare ai tre array iniziali come a delle componenti di quello che da ora in poi verrà chiamato un evento. Un evento rappresenta una istantanea di partenza per la prima delle quattro fasi di computazione di cui si è discusso finora, un set di particelle Π+, Π-, K+, K- e ɣ predefinite. Per ogni evento esiste una funzione kernel che si occupa di computarlo, è necessario quindi introdurre una nuova logica di acquisizione dei dati nella parte host. Tutto questo si avvicina molto di più a quello che sarà il reale modus operandi del metamodulo di calcolo. Vista la natura dei dati da manipolare l'evoluzione del codice sorgente verso l'acquisizione automatica di un numero anche molto grande di eventi, è stata concettualmente immediata. La parte host deve essere in grado di acquisire un file con all'interno un certo numero N di eventi e, per ogni evento, occuparsi di lanciare un processo device di computazione registrando i risultati. Angelo Tebano 566/2750 Pagina 53 di 105 Un evento, nel file di input appena citato, assume la forma seguente: Illustrazione 16: Fisionomia di un evento nel file di input Per ogni tipo sono elencate, come riga del file, le particelle. Una riga contiene quattro campi separati da uno spazio che corrispondono rispettivamente alle tre componenti spaziali della particella ed alla sua energia. La parola chiave ENDEVENT marca la fine dell'evento. In un file si susseguono N di questi eventi. Angelo Tebano 566/2750 Pagina 54 di 105 Tramite un apposito algoritmo, la funzione host interpreta ad uno ad uno gli eventi di cui è costituito il file in input, sulla base dell'evento valorizza gli array iniziali e lancia la kernel function che computa l'evento. Gli array di output del kernel vengono riversati in un secondo file e l'acquisizione riparte. Il file di output ha una forma molto simile a quello di input, come si evince dalla seguente illustrazione: Illustrazione 17: Fisionomia di un evento nel file di output Angelo Tebano 566/2750 Pagina 55 di 105 L'acquisizione viene gestita automaticamente da un algoritmo sviluppato per riconoscere le “parole chiave” all'interno del file e valorizzare quindi gli array. È importante sottolineare come a questo punto la funzione host sia stata riconcepita per reagire in maniera opportuna anche alla forma che l'evento di volta in volta assume. In altri termini, all'interno del ciclo di acquisizione di un evento, la funzione host invoca quella device con delle direttive che variano a seconda delle dimensioni dell'evento: in base alla dimensione di ciascuno dei tre array iniziali, essa calcola il numero massimo di thread necessari per l'elaborazione. Noto il numero di thread necessari viene effettuato un ulteriore calcolo per determinare le dimensioni della griglia di computazione e di ciascun blocco; la chiamata alla funzione kernel è quindi parametrica, e di volta in volta, la funzione host invoca il kernel richiedendo solo le risorse necessarie. Angelo Tebano 566/2750 Pagina 56 di 105 5. Testing e valutazione degli algoritmi di ricostruzione dei decadimenti del mesone B Gli algoritmi di cui si è discusso finora sono stati testati su hardware specifici, a seconda della loro natura implementativa. Le versioni sequenziali degli algoritmi sono state lanciate su un sistema in dotazione al data center S.C.o.P.E., con le seguenti caratteristiche tecniche: – server rack Dell PowerEdge R510; – processore 8-core Intel Xeon serie E5506 @ 2.13 Ghz con 4096 KB di cache; – 32 GB di memoria DDR3 fino a 1666 Mhz; – 8 unità dischi rigidi SATA (7200 rpm) da 3.5”, con capacità di 500 GB ognuno. Illustrazione 18: Il server rack Dell PowerEdge R510. La versione parallela degli algoritmi è stata testata sul sistema nVIDIA Tesla S2050, di cui si è ampiamente discusso in precedenza. Angelo Tebano 566/2750 Pagina 57 di 105 5.1. Obiettivi della valutazione e tipologie di testing Gli algoritmi implementati, a livello concettuale, non rappresentano alcuna rivoluzione nell'ambito fisico della ricostruzione delle particelle. Algoritmi molto simili sono già impiegati in esperimenti esistenti come BaBar ed LHC, ed integrano il framework ROOT, un ambiente di calcolo sviluppato in C++ e progettato per l'analisi dei dati nel campo della fisica particellare. Tali sorgenti sono però scritti in logica sequenziale; lo scopo primario del testing di applicativi GPGPU è, quindi, valutare se l'affiancamento delle GPU quali coprocessori matematici può portare o meno dei vantaggi in termini di performance di computazione. È questa la ragione per cui vengono confrontate due versioni degli stessi algoritmi, una sequenziale ed una parallela; dagli esiti del confronto si potrà stabilire se e quanto convenga attuare un porting dei sorgenti sequenziali esistenti in paralleli, sfruttando le GPU nVIDIA e CUDA nell'ambito del progetto SuperB. I test sono stati effettuati facendo uso delle funzioni per la gestione degli Eventi di CUDA, cui si è fatto riferimento nel paragrafo 3.1.4 e più precisamente delle funzioni: – cudaEventCreate(...); – cudaEventRecord(...); – cudaEventSynchronize(...); – cudaEventElapsedTime(...). Angelo Tebano 566/2750 Pagina 58 di 105 5.1.1. Test delle principali CUDA memory functions Si sono volute testare in questa fase le due funzioni principali per la gestione della memoria in CUDA. cudaMalloc(void **devPtr, size_t size) è in genere la prima funzione che si incontra in un codice CUDA-C e consente di allocare uno spazio di memoria di size byte, ritornando in devPtr un puntatore alla memoria allocata. Allocando varie quantità di elementi di tipo float e misurando il tempo di allocazione è stato possibile ricavare la seguente tabella: Tabella 3: Performance test della funzione cudaMalloc(...) Angelo Tebano 566/2750 Pagina 59 di 105 Salta subito all'occhio come l'allocare 1 o 10000000 di numeri float sia pressochè identico in termini di tempo impiegato. Ciò indica che, per quanto riguarda l'overhead, è sempre consigliabile ricorrere a poche, ma quanto più pesanti è possibile, chiamate di questo tipo, piuttosto che abusare di cudaMalloc(...) ma per allocare volta per volta quantità minime di memoria sul device. cudaMemcpy(void *dst, const void *src, size_t count, enum cudaMemcpyKind kind) è la funzione usata per trasferire dati dall'host al device e viceversa. Essa copia count bytes dalla memoria puntata da src in quella puntata da dst secondo la “direzione” kind. Il test svolto riguarda il trasferimento di diverse quantità di byte e misurando il tempo impiegato è stato possibile ricavare la seguente tabella: Tabella 4: Performance test della funzione cudaMemcpy(...) Angelo Tebano 566/2750 Pagina 60 di 105 Anche in questo caso è possibile notare, oltre ad una particolare stabilità dell'overhead fino ai 100000 byte, come sia sostanzialmente equivalente in termini di tempo allocare 8 od 8000 byte, segno che trasferire pochi byte alla volta non è conveniente. Questo è dovuto essenzialmente al fatto che la funzione in esame trasferisce memoria da e verso il device in blocchi di lunghezza prefissata. Angelo Tebano 566/2750 Pagina 61 di 105 5.1.2. Test dell'allocazione della memoria per le strutture in uso La tabella seguente mostra i tempi impiegati sia dall'algoritmo seriale che da quello parallelo per l'allocazione delle strutture necessarie per la rappresentazione delle particelle. È stata fatta variare proporzionalmente la dimensione dei tre array iniziali e misurata in termini di byte la quantità di memoria allocata. Tabella 5: Performance test dell'allocazione delle strutture in uso. É evidente che il codice seriale impiega meno tempo del parallelo per allocare le strutture dati necessarie a contenere i dati in input. Ciò è dovuto al fatto che il codice parallelo deve allocare memoria sia sull'host che sul device. Inoltre vale la stessa considerazione fatta per l'allocazione di semplici numeri float: per input compresi tra 5 e 10000 si può notare che il tempo di allocazione del codice parallelo resta pressapoco invariato. Anche questo è dovuto al fatto che la funzione cudaMalloc(...) alloca memoria in blocchi di lunghezza prestabilita. Angelo Tebano 566/2750 Pagina 62 di 105 5.1.3. Test del trasferimento della memoria per le strutture in uso La tabella seguente mostra i tempi impiegati dall'algoritmo parallelo per la copia, in aree di memoria precedentemente allocate tramite cudaMalloc(...), delle strutture contenenti le particelle di input. È stata fatta variare proporzionalmente la dimensione dei tre array iniziali e misurato il tempo di trasferimento. Tabella 6: Performance test del trasferimento delle strutture in uso. L'overhead di trasferimento è quindi un tempo che si somma a quello dell'elaborazione e non è un parametro trascurabile nella valutazione degli algoritmi, basti pensare infatti che è un aspetto che non riguarda per nulla la variante seriale. Inoltre, nel caso in esame, non è un aspetto che si può stimare con precisione nel complesso (quindi sommando l'overhead di trasferimento da host a device a quello da device ad host), poiché non è possibile, ad esempio, ricavare una tabella dell'overhead di trasferimento delle particelle ricostruite, dal momento che dipendono esclusivamente dalla natura dell'input. Angelo Tebano 566/2750 Pagina 63 di 105 5.1.4. Performance test sul modulo per la ricostruzione di Mesoni D0 (K0s Π+ Π- e K0s Π- Π+) Passiamo ora, avendo discusso del ruolo fondamentale del tempo di overhead nella valutazione degli algoritmi, ad esaminare tempi di elaborazione degli stessi. È bene precisare che le rilevazioni fatte sono, questa volta, da intendersi al netto degli overhead di trasferimento. Viene quindi considerato il tempo di elaborazione e di allocazione delle strutture di output per entrambi i moduli, sia nella versione sequenziale che in quella parallela. Siccome le strutture in input sono state tutte precedentemente allocate e trasferite, in altre parole, per i moduli che stiamo considerando, è stata effettuata la misurazione dei soli tempi di esecuzione, registrando quindi il tempo trascorso tra l'istante precedente e quello successivo alla chiamata alla relativa funzione. In questo tempo è naturalmente compreso il tempo di memorizzazione dei dati in output nelle strutture già allocate nel caso del codice parallelo ed allocate dinamicamente nel caso del codice sequenziale. La seguente tabella evidenzia le differenze tra i due algoritmi al variare della dimensione dell'input: Performance test di D0 (K0s Π+ Π- e K0s Π- Π+) Numero Pioni+ Numero Pioni- Numero Fotoni Sequenziale Parallelo 10 10 20 0,026 ms 0,006 ms 50 50 100 0,416 ms 0,018 ms 100 100 200 1,692 ms 0,231 ms 200 200 400 9,122 ms 3,626 ms Tabella 7: Performance test sul modulo D0 (K0s Π+ Π- e K0s Π- Π+). Angelo Tebano 566/2750 Pagina 64 di 105 É innanzitutto importante notare come in questo caso di test, la versione parallela dell'algoritmo non risulti mai sconfitta, in termini di tempo di esecuzione, da quella sequenziale, circostanza ancora più evidente nel seguente grafico delle funzioni di tempo: Tempo (ms) 10 9 8 7 6 Sequenziale Parallelo 5 4 3 2 1 0 10 10 20 50 50 100 100 100 200 200 200 400 Dimensione input Illustrazione 19: Grafico delle performance del modulo D0 (K0s Π+ Π- e K0s Π- Π+). Questa situazione la dice lunga sul peso dell'overhead di allocazione e trasferimento della memoria da e verso il device che, come vedremo nel caso globale, risulterà essere, per qualche caso, un punto di vantaggio per l'algoritmo sequenziale nei confronti di quello parallelo. Angelo Tebano 566/2750 Pagina 65 di 105 5.1.5. Performance test sul modulo per la ricostruzione di Mesoni D*0 (D0 Π0) Test analogo a quello appena visto per la funzione di ricostruzione delle particelle D0 (K0s Π+ Π- e K0s Π- Π+), è stato effettuato per il modulo D*0 (D0 Π0). Anche in questo caso le rilevazioni fatte sono da intendersi al netto degli overhead di allocazione della memoria per l'input iniziale e di trasferimento delle strutture. Viene quindi considerato il tempo di elaborazione e di allocazione delle strutture di output sia nella versione sequenziale che in quella parallela. In questo tempo è naturalmente compreso il tempo di memorizzazione dei dati in output nelle strutture già allocate nel caso del codice parallelo ed allocate dinamicamente nel caso del codice sequenziale. É importante tenere presente che il modulo in esame è fortemente condizionato dalle elaborazioni della seconda fase. Infatti, in questo caso, i tempi di esecuzione dipendono molto anche da quante sono le particelle ricostruite precedentemente dagli altri moduli. La seguente tabella evidenzia le differenze tra i due algoritmi in un caso generico in cui le strutture in input sono state inizializzate con valori pseudo-casuali, al variare della dimensione dell'input: Performance test di D*0 (D0 Π0) Numero Pioni+ Numero Pioni- Numero Fotoni Sequenziale Parallelo 10 10 20 0,014 ms 0,002 ms 50 50 100 0,213 ms 0,006 ms 100 100 200 0,812 ms 0,076 ms 200 200 400 3,804 ms 1,192 ms Tabella 8: Performance test sul modulo D*0 (D0 Π0). Angelo Tebano 566/2750 Pagina 66 di 105 Anche in questo caso l'algoritmo parallelo risulta sempre migliore di quello sequenziale, circostanza ancora più evidente nel seguente grafico delle funzioni di tempo: Tempo (ms) 4 3,5 3 Sequenziale Parallelo 2,5 2 1,5 1 0,5 0 10 10 20 50 50 100 100 100 200 200 200 400 Dimensione input Illustrazione 20: Grafico delle performance del modulo D*0 (D0 Π0). Vale anche qui la stessa osservazione fatta osservando il grafico delle funzioni di tempo del modulo precedentemente esaminato: è bene tenere presente che l'overhead che qui abbiamo trascurato per necessità, ma che verrà considerato nel caso globale, può condizionare pesantemente (specie in caso di dimensione minima dell'input) il tempo di elaborazione dell'algoritmo parallelo, anche se dai rilievi appena esaminati sembrerebbe che l'algoritmo sequenziale non “vinca” mai su di esso in termini di performance. Angelo Tebano 566/2750 Pagina 67 di 105 5.1.6. Performance test di computazione totale A questo punto vediamo l'ultimo caso di test, che ci permetterà di valutare quale tra le due strategie implementative conviene adoperare per affrontare il tipo di problematica in esame. È stato valutato il tempo di esecuzione totale dell'intero meta-modulo composto da tutte le funzioni prodotte finora, ovvero è stato registrato il tempo di esecuzione dell'algoritmo completo a partire dall'allocazione delle strutture in input fino ad arrivare al free della memoria per tutte le strutture utilizzate. Mentre nei casi visti in precedenza è stato considerato il solo tempo di calcolo dei singoli moduli, è quindi naturale che in questo test rientrino anche i tempi di overhead di allocazione e trasferimento di cui si è ampiamente discusso finora. È questo il caso di test di maggiore interesse dal punto di vista della valutazione. La seguente tabella evidenzia le differenze tra i due algoritmi in un caso generico in cui le strutture in input sono state inizializzate con valori pseudo-casuali, al variare della dimensione dell'input: Performance test kernel function Numero Pioni+ Numero Pioni- Numero Fotoni Sequenziale Parallelo 10 10 20 0,244 ms 0,963 ms 50 50 100 46,438 ms 4,831 ms 100 100 200 775,794 ms 49,489 ms 500 500 1000 296610,531 ms 24153,752 ms 1000 1000 2000 2759512,689 ms 218623,760 ms Tabella 9: Performance test della funzione kernel. Angelo Tebano 566/2750 Pagina 68 di 105 Il seguente grafico delle funzioni di tempo riflette i valori appena visti in forma tabellare per il caso in esame: Tempo (ms) 3000000 2500000 Sequenziale Parallelo 2000000 1500000 1000000 500000 0 10 10 20 50 50 100 100 100 200 500 500 1000 1000 1000 2000 Dimensione input Illustrazione 21: Grafico delle performance della funzione kernel. Ciò che abbiamo annunciato in precedenza, sulla base delle considerazioni fatte relativamente al tempo di overhead delle funzioni CUDA, trova pienamente riscontro nei dati appena snocciolati. Nel caso di input minimale (10 Pioni+, 10 Pioni-, 20 Fotoni) il tempo di esecuzione dell'algoritmo globale in parallelo supera di quasi quattro volte quello della versione sequenziale. Quintuplicando la dimensione dell'input (50 Pioni+, 50 Pioni-, 100 Fotoni) le parti si invertono con una crescita molto consistente ai danni della versione sequenziale (più di dieci volte il tempo di esecuzione di quella parallela). Dando in input all'algoritmo 1000 Pioni+, 1000 Pioni- e 2000 Fotoni, l'algoritmo sequenziale supera di quasi tredici volte il tempo di computazione dell'algoritmo parallelo. É la prova che la dimensione dell'input è fondamentale per consentire all'algoritmo parallelo di superare l'iniziale gap che ha nei confronti del sequenziale per via degli overhead. Angelo Tebano 566/2750 Pagina 69 di 105 5.1.7. Capacità di ricostruzione della variante “multi-evento” dell'algoritmo generale su input reali A questo punto è d'obbligo una considerazione sulle capacità di ricostruzione dell'algoritmo complessivo su input reali. La variante “multi-evento” presentata è una versione iniziale ed incompleta di quello che sarà, nel prosieguo della sperimentazione, l'algoritmo finale in grado di ricostruire il mesone B a partire da un numero molto consistente di eventi. Inoltre è stata testata su un input ancora minimale (100.000 eventi) in confronto alle esigenze di calcolo di quella che sarà la reale produzione di istantanee (e quindi di eventi) di SuperB. Nonostante questo, si è voluto verificare l'attendibilità dell'output, al fine di avere almeno un'idea delle capacità di ricostruzione dell'algoritmo. Si è pensato di esaminare uno dei moduli dell'algoritmo globale sviluppato finora, andandone a controllare a campione le particelle ricostruite, mediante la traccia delle particelle generatrici. In sostanza, preso a caso un evento, ne sono state analizzate diverse particelle di output dello stesso modulo ed è stato controllato tramite l'indice dei genitori che realmente i risultati delle somme fossero corretti. Il test è stato ripetuto diverse volte e la verifica è sempre andata a buon fine. Un altro test, di maggiore rilevanza, consiste nell'analisi della distribuzione delle masse delle particelle di output. Sappiamo, avendo osservato i grafici di distribuzione di massa elaborati tramite il framework ROOT nell'ambito dell'esperimento BaBar, che per un numero molto grande di eventi (nell'ordine del milione) le masse di particelle dello stesso tipo (quindi generate dallo stesso modulo) hanno una distribuzione ben nota, anche se gli eventi sono in numero elevato e tutti diversi tra loro. Quella di distribuzione è tipicamente una funzione normale (o gaussiana). Angelo Tebano 566/2750 Pagina 70 di 105 A titolo di esempio riportiamo un grafico della distribuzione delle masse per il modulo K0s, generato nell'ambito dell'esperimento BaBar, nel quale viene confermata l'osservazione fatta poc'anzi riguardo il tipo di distribuzione: Illustrazione 22: Distribuzione di massa tipica per il modulo K0s. Sebbene la precisione che caratterizza il grafico appena riportato sia indotta, soprattutto, da un input di partenza già filtrato, si può notare come la maggior parte delle particelle di output siano concentrate attorno al centro di massa (0,497614 GeV) e come la funzione tenda ad “appiattirsi” ai lati. Come anticipato in precedenza una tale “simmetria” è osservabile in due circostanze: un input già filtrato (per cui sia stata fatta quindi un'operazione preliminare di selezione delle particelle generatrici in base alla massa) o un input costituito da un numero di eventi rilevante, anche nell'ordine del milione (in riferimento al paragrafo 1.1 ricordiamo che SuperB produrrà fino a 6 milioni di eventi al minuto). Angelo Tebano 566/2750 Pagina 71 di 105 Non è però un errore aspettarsi che la versione testata, per quanto sia ancora ad uno stadio embrionale, rispetti, almeno in parte, le caratteristiche di distribuzione descritte nel caso generale. Lo stesso modulo K0s è stato allora preso dalla nostra variante “multi-evento” come termine di confronto; l'illustrazione che segue mostra il grafico della distribuzione delle masse: Illustrazione 23: Distribuzione di massa della variante “multi-evento” per il modulo K0s. Il grafico manca dell'elemento di simmetria per le succitate ragioni, quindi non possiamo assolutamente parlare di una funzione gaussiana vera e propria, ma allo stesso tempo mette in evidenza la situazione di addensamento delle particelle di output attorno al centro di massa. Questo indizio suggerisce la “bontà” della distribuzione e ci induce quindi a credere che la capacità di ricostruzione dell'algoritmo sia significativa. Le osservazioni fatte vanno intese quindi come un buon trampolino di lancio per il prosieguo della sperimentazione e per il completamento dell'implementazione della variante “multi-evento” per la ricostruzione dei decadimenti del mesone B a partire da input reali. Angelo Tebano 566/2750 Pagina 72 di 105 Conclusioni Dopo avere introdotto le caratteristiche del progetto SuperB ed averne descritto i principali obiettivi, abbiamo visto in quale misura è necessario che i sistemi di calcolo deputati all'analisi dei risultati prodotti dall'esperimento, assistano il sistema fisico di generazione dei dati. Da qui abbiamo discusso dell'esigenza di sperimentazione per la messa a punto di nuove tecniche di calcolo, più performanti rispetto a quelle tradizionali, indotta dalla necessità di gestire quantità molto grandi di dati, di varia natura, in tempi ragionevoli. Si è quindi introdotto il modello nVIDIA CUDA, un nuovo ambiente di programmazione che si sta rivelando un vero e proprio trampolino di lancio per numerose applicazioni nel campo della ricerca scientifica. Sono stati progettati in CUDA-C dei moduli di calcolo parallelo in grado di ricostruire il decadimento di alcune particelle oggetto delle osservazioni dell'esperimento SuperB; tali moduli di calcolo sono stati messi a confronto con le versioni sequenziali degli stessi algoritmi al fine di valutarne le performance globali, i tempi di overhead e le capacità di ricostruzione. I risultati degli speed test hanno dimostrato che il calcolo su GPU (o più precisamente l'interazione tra CPU e GPU nel calcolo ad alte prestazioni) può migliorare, in modo considerevole, le prestazioni degli algoritmi che richiedono quantità di tempo importanti a causa dell'enorme mole di dati che devono processare. Se invece ci troviamo a dover lavorare con applicativi che utilizzano pochi elementi in input, gli algoritmi “standard” per CPU risultano ancora i più performanti. Il nuovo paradigma implementativo esaminato ha quindi, come vantaggio, la possibilità di essere sfruttato in circostanze in cui è necessario impiegare al meglio il massively parallel, sempre a patto che siano chiare al programmatore quali sono le principali opportunità di parallelizzazione massiva di un algoritmo e quali invece sono le operazioni che è sempre meglio affidare alla tradizionale computazione sequenziale. Applicativi per il calcolo ad alte prestazioni (come software per la ricostruzione delle particelle in fisica, software per il modelling acustico, simulatori di varia Angelo Tebano 566/2750 Pagina 73 di 105 natura) richiedono esattamente questo tipo di capacità computazionali. È possibile dunque concludere che il co-processing CPU-GPU si è rivelato uno strumento molto potente di calcolo ed a questo va aggiunto che, generalmente ad oggi, le GPU hanno un migliore rapporto Gflops/consumo e Gflops/costo rispetto alle tradizionali CPU; questo sistema ibrido, quindi, si rivela interessante e rivoluzionario anche sotto il profilo economico. Angelo Tebano 566/2750 Pagina 74 di 105 Appendice A.1. Main function – versione sequenziale int main(int argc, char **argv){ //Dichiarazione variabili globali su CPU float massa0=pow(4.5,2), //massa di riferimento delta=6; //delta //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 unsigned int num_PIm=PIM, //numero di particelle PImeno num_PIp=PIP, //numero di particelle PIpiu num_Gamma=GAMMA, //numero di particelle Gamma prod_cart, //dimensione massima dell'array delle K0s e D0KPI coefBin, //dimensione massima dell'array dei PI0 max_Ds0D0PI0=1000, //dimensione massima dell'array Ds0D0PI0 *dimK0s_host, //dimensione effettiva array K0s *dimD0KPI_host, //dimensione effettiva array D0KPI *dimPI0_host, //dimensione effettiva array PI0 *dimD0KPIPI0_host, //dimensione effettiva array D0KPIPI0 *dimD0Ks2PI_host, //dimensione effettiva array D0Ks2PI *dimDs0D0PI0_host, //dimensione effettiva array Ds0D0PI0 *dimD0K3PI_host; //dimensione effettiva array D0K3PI /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ cudaEvent_t start, stop, start2, stop2; float time, time2; cudaEventCreate(&start); cudaEventCreate(&stop); cudaEventCreate(&start2); cudaEventCreate(&stop2); cudaEventRecord(start, 0); cudaEventRecord(start2, 0); prod_cart=num_PIp*num_PIm; coefBin=(num_Gamma*(num_Gamma-1))/2; //Allocazione vettori considerando dimensioni massime PIm_host=(QVECT*)malloc(num_PIm*sizeof(QVECT)); PIp_host=(QVECT*)malloc(num_PIp*sizeof(QVECT)); Gamma_host=(QVECT*)malloc(num_Gamma*sizeof(QVECT)); K0s_host=(PI_0*)malloc(0); D0KPI_host=(PI_0*)malloc(0); PI0_host=(PI_0*)malloc(0); D0KPIPI0_host=(D0KPI_PI0*)malloc(0); D0Ks2PI_host=(D0KPI_PI0*)malloc(0); D0K3PI_host=(D0KPI_PI0*)malloc(0); Ds0D0PI0_host=(D0*)malloc(0); //Allocazione dimensioni effettive Angelo Tebano 566/2750 Pagina 75 di 105 dimK0s_host=(unsigned int*)malloc(sizeof(unsigned int)); *dimK0s_host=0; dimD0KPI_host=(unsigned int*)malloc(sizeof(unsigned int)); *dimD0KPI_host=0; dimPI0_host=(unsigned int*)malloc(sizeof(unsigned int)); *dimPI0_host=0; dimD0KPIPI0_host=(unsigned int*)malloc(sizeof(unsigned int)); *dimD0KPIPI0_host=0; dimD0Ks2PI_host=(unsigned int*)malloc(sizeof(unsigned int)); *dimD0Ks2PI_host=0; dimD0K3PI_host=(unsigned int*)malloc(sizeof(unsigned int)); *dimD0K3PI_host=0; dimDs0D0PI0_host=(unsigned int*)malloc(sizeof(unsigned int)); *dimDs0D0PI0_host=0; //inizializzazione dei quadrivettori PI+, PI- e Gamma init_PIp(PIp_host, num_PIp); init_PIm(PIm_host, num_PIm); init_Gamma(Gamma_host, num_Gamma); ScreateK0s( /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ PIp_host, PIm_host, num_PIp, num_PIm, massa0, delta, dimK0s_host); cudaEventRecord(stop2, 0); cudaEventSynchronize(stop2); cudaEventElapsedTime(&time2, start2, stop2); printf("[ScreateK0s] Time elapsed: %f ms\n", time2); cudaEventRecord(start2, 0); ScreateD0KPI( /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ PIp_host, PIm_host, num_PIp, num_PIm, massa0, delta, dimD0KPI_host); //cudaEventRecord(stop2, 0); //cudaEventSynchronize(stop2); //cudaEventElapsedTime(&time2, start2, stop2); //printf("[ScreateD0KPI] Time elapsed: %f ms\n", time2); //cudaEventRecord(start2, 0); ScreatePI0( Gamma_host, num_Gamma, massa0, delta, dimPI0_host); /*timeline*/ /*timeline*/ cudaEventRecord(stop2, 0); cudaEventSynchronize(stop2); Angelo Tebano 566/2750 Pagina 76 di 105 /*timeline*/ /*timeline*/ /*timeline*/ cudaEventElapsedTime(&time2, start2, stop2); printf("[ScreatePI0] Time elapsed: %f ms\n", time2); cudaEventRecord(start2, 0); ScreateD0KPIPI0( /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ cudaEventRecord(stop2, 0); cudaEventSynchronize(stop2); cudaEventElapsedTime(&time2, start2, stop2); printf("[ScreateD0KPIPI0] Time elapsed: %f ms\n", time2); cudaEventRecord(start2, 0); ScreateD0Ks2PI( /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ PIp_host, PIm_host, K0s_host, num_PIp, num_PIm, *dimK0s_host, massa0, delta, dimD0Ks2PI_host); cudaEventRecord(stop2, 0); cudaEventSynchronize(stop2); cudaEventElapsedTime(&time2, start2, stop2); printf("[ScreateD0KS2PI] Time elapsed: %f ms\n", time2); cudaEventRecord(start2, 0); ScreateD0K3PI( /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ PIp_host, PIm_host, PI0_host, num_PIp, num_PIm, *dimPI0_host, massa0, delta, dimD0KPIPI0_host); PIp_host, PIm_host, num_PIp, num_PIm, massa0, delta, dimD0K3PI_host); cudaEventRecord(stop2, 0); cudaEventSynchronize(stop2); cudaEventElapsedTime(&time2, start2, stop2); printf("[ScreateD0K3PI] Time elapsed: %f ms\n", time2); cudaEventRecord(start2, 0); ScreateDs0D0PI0( Angelo Tebano 566/2750 D0KPI_host, dimD0KPI_host, D0KPIPI0_host, dimD0KPIPI0_host, D0Ks2PI_host, dimD0Ks2PI_host, Pagina 77 di 105 D0K3PI_host, dimD0K3PI_host, PI0_host, dimPI0_host, massa0, delta, dimDs0D0PI0_host); /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ cudaEventRecord(stop2, 0); cudaEventSynchronize(stop2); cudaEventElapsedTime(&time2, start2, stop2); printf("[ScreateDS0D0PI0] Time elapsed: %f ms\n", time2); cudaEventRecord(start2, 0); stampa_risultati(PI0_host, dimPI0_host, K0s_host, dimK0s_host, D0KPI_host, dimD0KPI_host, D0KPIPI0_host, dimD0KPIPI0_host, PIp_host, PIm_host, D0Ks2PI_host, dimD0Ks2PI_host, D0K3PI_host, dimD0K3PI_host, Ds0D0PI0_host, dimDs0D0PI0_host); //Rilascio memoria strutture e dimensioni sull'host free(PIm_host); free(PIp_host); free(Gamma_host); free(K0s_host); free(D0KPI_host); free(PI0_host); free(D0KPIPI0_host); free(D0Ks2PI_host); free(D0K3PI_host); free(Ds0D0PI0_host); free(dimK0s_host); free(dimD0KPI_host); free(dimPI0_host); free(dimD0KPIPI0_host); free(dimD0Ks2PI_host); free(dimD0K3PI_host); free(dimDs0D0PI0_host); /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ cudaEventRecord(stop, 0); cudaEventSynchronize(stop); cudaEventElapsedTime(&time, start, stop); printf("[tempo totale netto main sequenziale] Time elapsed: %f ms\n\n", time); } Angelo Tebano 566/2750 Pagina 78 di 105 A.2. Modulo D0 (K0s Π+ Π- e K0s Π- Π+) – versione sequenziale void ScreateD0Ks2PI( QVECT *PIp, //array dei PIp QVECT *PIm, //array dei PIPI_0 *K0s, //array delle particelle K0s unsigned int N, //dim PIp unsigned int M, //dim PIm unsigned int dimK0s, //dimensione effettiva K0s float M0, //massa di riferimento float del, //delta unsigned int *dimD0Ks2PI)//dimensione effettiva dell'array D0Ks2PI { int i, //indice PIp j, //indice PIm k; //indice K0s 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<dimK0s;k++) { if((PIp[i].gen == 'a') && (PIm[j].gen == 'a')) if((K0s[k].g1!=i)&&(K0s[k].g2!=j)) { //Somma delle componenti della i-esima particella PIpiu genere k e della j-esima PImeno k Candidato.x= PIp[i].x + PIm[j].x + K0s[k].x; Candidato.y= PIp[i].y + PIm[j].y + K0s[k].y; Candidato.z= PIp[i].z + PIm[j].z + K0s[k].z; Candidato.Ene= PIp[i].Ene + PIm[j].Ene + K0s[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(massa>M0-del && massa<M0+del){ D0Ks2PI_host=(D0KPI_PI0*)realloc(D0Ks2PI_host, ((*dimD0Ks2PI+1)*sizeof(D0KPI_PI0)); D0Ks2PI_host[*dimD0Ks2PI].x=Candidato.x; D0Ks2PI_host[*dimD0Ks2PI].y=Candidato.y; D0Ks2PI_host[*dimD0Ks2PI].z=Candidato.z; D0Ks2PI_host[*dimD0Ks2PI].Ene=Candidato.Ene; D0Ks2PI_host[*dimD0Ks2PI].g1=k; //Particella genitrice K0s D0Ks2PI_host[*dimD0Ks2PI].g2=i; //Particella genitrice PIpiu D0Ks2PI_host[*dimD0Ks2PI].g3=j; //Particella genitrice PImeno *dimD0Ks2PI=(*dimD0Ks2PI)+1; } } } } Angelo Tebano 566/2750 Pagina 79 di 105 A.3. Modulo D*0 (D0 Π0) – versione sequenziale void ScreateDs0D0PI0( PI_0 *D0KPI, unsigned int *dimD0KPI, D0KPI_PI0 *D0KPIPI0, unsigned int *dimD0KPIPI0, D0KPI_PI0 *D0Ks2PI, unsigned int *dimD0Ks2PI, D0KPI_PI0 *D0K3PI, unsigned int *dimD0K3PI, PI_0 *PI0, unsigned int *dimPI0, float M0, float del, unsigned int *dimDs0D0PI0) //array delle D0KPI //dimensione effettiva array delle D0KPI //array delle D0KPIPI0 //dimensione effettiva array delle D0KPIPI0 //array D0Ks2PI //dimensione effettiva array delle D0Ks2PI //array delle D0K3PI //dimensione effettiva array delle D0K3PI //array delle PI0 //dimensione effettiva array delle PI0 //massa di riferimento //delta //dimensione effettiva array delle DS0D0PI0 { int i, j; float massa; QVECT Candidato; //massa del quadrivettore candidato //quadrivettore di appoggio for(i=0;i<*dimD0KPI;i++) for(j=0;j<*dimPI0;j++) { //Somma delle componenti della i-esima particella D0KPI e della j-esima PI0 Candidato.x= D0KPI[i].x + PI0[j].x; Candidato.y= D0KPI[i].y + PI0[j].y; Candidato.z= D0KPI[i].z + PI0[j].z; Candidato.Ene= D0KPI[i].Ene + PI0[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(massa>M0-del && massa<M0+del){ Ds0D0PI0_host=(D0*)realloc(Ds0D0PI0_host, ((*dimDs0D0PI0)+1)*sizeof(D0)); Ds0D0PI0_host[*dimDs0D0PI0].x=Candidato.x; Ds0D0PI0_host[*dimDs0D0PI0].y=Candidato.y; Ds0D0PI0_host[*dimDs0D0PI0].z=Candidato.z; Ds0D0PI0_host[*dimDs0D0PI0].Ene=Candidato.Ene; Ds0D0PI0_host[*dimDs0D0PI0].g1=0; //Etichetta array componente di D0 (0=D0KPI) Ds0D0PI0_host[*dimDs0D0PI0].g2=i; //Elemento della componente di D0 Ds0D0PI0_host[*dimDs0D0PI0].g3=j; //Particella genitrice PI0 *dimDs0D0PI0=(*dimDs0D0PI0)+1; //incremento l'indice dell'array dei dimDs0D0PI0 } } for(i=0;i<*dimD0KPIPI0;i++) for(j=0;j<*dimPI0;j++) { //Somma delle componenti della k-esima particella D0KPIPI0 e della j-esima PI0 Candidato.x= D0KPIPI0[i].x + PI0[j].x; Angelo Tebano 566/2750 Pagina 80 di 105 Candidato.y= D0KPIPI0[i].y + PI0[j].y; Candidato.z= D0KPIPI0[i].z + PI0[j].z; Candidato.Ene= D0KPIPI0[i].Ene + PI0[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(massa>M0-del && massa<M0+del){ Ds0D0PI0_host=(D0*)realloc(Ds0D0PI0_host, ((*dimDs0D0PI0)+1)*sizeof(D0)); Ds0D0PI0_host[*dimDs0D0PI0].x=Candidato.x; Ds0D0PI0_host[*dimDs0D0PI0].y=Candidato.y; Ds0D0PI0_host[*dimDs0D0PI0].z=Candidato.z; Ds0D0PI0_host[*dimDs0D0PI0].Ene=Candidato.Ene; Ds0D0PI0_host[*dimDs0D0PI0].g1=1; //Etichetta array componente di D0 (1=D0KPIPI0) Ds0D0PI0_host[*dimDs0D0PI0].g2=i; //Elemento della componente di D0 Ds0D0PI0_host[*dimDs0D0PI0].g3=j; //Particella genitrice PI0 *dimDs0D0PI0=(*dimDs0D0PI0)+1; //incremento l'indice dell'array dei dimDs0D0PI0 } } for(i=0;i<*dimD0Ks2PI;i++) for(j=0;j<*dimPI0;j++) { //Somma delle componenti della k-esima particella D0Ks2PI e della j-esima PI0 Candidato.x= D0Ks2PI[i].x + PI0[j].x; Candidato.y= D0Ks2PI[i].y + PI0[j].y; Candidato.z= D0Ks2PI[i].z + PI0[j].z; Candidato.Ene= D0Ks2PI[i].Ene + PI0[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(massa>M0-del && massa<M0+del){ Ds0D0PI0_host=(D0*)realloc(Ds0D0PI0_host, ((*dimDs0D0PI0)+1)*sizeof(D0)); Ds0D0PI0_host[*dimDs0D0PI0].x=Candidato.x; Ds0D0PI0_host[*dimDs0D0PI0].y=Candidato.y; Ds0D0PI0_host[*dimDs0D0PI0].z=Candidato.z; Ds0D0PI0_host[*dimDs0D0PI0].Ene=Candidato.Ene; Ds0D0PI0_host[*dimDs0D0PI0].g1=2; //Etichetta array componente di D0 (2=D0Ks2PI) Ds0D0PI0_host[*dimDs0D0PI0].g2=i; //Elemento della componente di D0 Ds0D0PI0_host[*dimDs0D0PI0].g3=j; //Particella genitrice PI0 *dimDs0D0PI0=(*dimDs0D0PI0)+1; //incremento l'indice dell'array dei dimDs0D0PI0 } } for(i=0;i<*dimD0K3PI;i++) for(j=0;j<*dimPI0;j++) { //Somma delle componenti della k-esima particella D0K3PI e della j-esima PI0 Candidato.x= D0K3PI[i].x + PI0[j].x; Angelo Tebano 566/2750 Pagina 81 di 105 Candidato.y= D0K3PI[i].y + PI0[j].y; Candidato.z= D0K3PI[i].z + PI0[j].z; Candidato.Ene= D0K3PI[i].Ene + PI0[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(massa>M0-del && massa<M0+del){ Ds0D0PI0_host=(D0*)realloc(Ds0D0PI0_host, ((*dimDs0D0PI0)+1)*sizeof(D0)); Ds0D0PI0_host[*dimDs0D0PI0].x=Candidato.x; Ds0D0PI0_host[*dimDs0D0PI0].y=Candidato.y; Ds0D0PI0_host[*dimDs0D0PI0].z=Candidato.z; Ds0D0PI0_host[*dimDs0D0PI0].Ene=Candidato.Ene; Ds0D0PI0_host[*dimDs0D0PI0].g1=3; //Etichetta array componente di D0 (3=D0K3PI) Ds0D0PI0_host[*dimDs0D0PI0].g2=i; //Elemento della componente di D0 Ds0D0PI0_host[*dimDs0D0PI0].g3=j; //Particella genitrice PI0 *dimDs0D0PI0=(*dimDs0D0PI0)+1; //incremento l'indice dell'array dei dimDs0D0PI0 } } } A.4. Main function – versione parallela int main(int argc, char **argv){ //Dichiarazione variabili globali su CPU float massa0=pow(4.5,2), //massa di riferimento delta=6; //delta //Dati di input QVECT *Gamma_host, *Gamma_dev;//Aos contenente le particelle Gamma, su host e device QVECT *PIm_host, *PIm_dev; //AoS contenente le PImeno, su host e device QVECT *PIp_host, *PIp_dev; //Aos contenente le PIpiu, su host e device //Dati di output PI_0 *K0s_host, *K0s_dev; PI_0 *D0KPI_host, *D0KPI_dev; PI_0 *PI0_host, *PI0_dev; //Aos contenente le K0s, su host e device //Aos contenente le D0KPI, su host e device //Aos contenente le PI0, su host e device D0KPI_PI0 *D0KPIPI0_host, *D0KPIPI0_dev; D0KPI_PI0 *D0Ks2PI_host, *D0Ks2PI_dev; D0KPI_PI0 *D0K3PI_host, *D0K3PI_dev; D0 *Ds0D0PI0_host, *Ds0D0PI0_dev; unsigned int //Aos contenente le D0KPIPI0, su host e device //Aos contenente le D0Ks2PI, su host e device //Aos contenente le D0K3PI, su host e device //Aos contenente le Ds0D0PI0, su host e device num_PIm=PIM, //numero di particelle PImeno num_PIp=PIP, //numero di particelle PIpiu num_Gamma=GAMMA, //numero di particelle Gamma prod_cart, //dimensione massima dell'array delle K0s e D0KPI coefBin, //dimensione massima dell'array dei PI0 Angelo Tebano 566/2750 Pagina 82 di 105 max_D0KPIPI0=0, //dimensione massima dell'array D0KPIPI0 max_D0Ks2PI=0, //dimensione massima dell'array D0Ks2PI max_D0K3PI=0, //dimensione massima dell'array D0K3PI *dimk0s_host, *dimk0s_dev, //dimensione effettiva array K0s *dimD0KPI_host, *dimD0KPI_dev, //dimensione effettiva array D0KPI *dimPI0_host, *dimPI0_dev, //dimensione effettiva array PI0 *dimD0KPIPI0_host, *dimD0KPIPI0_dev, //dimensione effettiva array D0KPIPI0 *dimD0Ks2PI_host, *dimD0Ks2PI_dev, //dimensione effettiva array D0Ks2PI *dimDs0D0PI0_host, *dimDs0D0PI0_dev, //dimensione effettiva array Ds0D0PI0 *dimD0K3PI_host, *dimD0K3PI_dev; //dimensione effettiva array D0K3PI //dimensioni toy per gli array dei risultati long Tmax_K0s=1000, Tmax_D0KPI=1000, Tmax_PI0=1000, Tmax_D0KPIPI0=1000, Tmax_D0Ks2PI=1000, Tmax_D0K3PI=1000, Tmax_Ds0D0PI0=1000; /*timeline*/ /*timeline*/ cudaEvent_t start, stop; float time; //Calcolo del prodotto cartesiano(dimensione massima K0s e D0KPI) prod_cart=num_PIp*num_PIm; //Calcolo del coefficiente binomiale (dimensione massima PI0) coefBin=(num_Gamma*(num_Gamma-1))/2; //Allocazione memoria per quadrivettori PImeno su host e device PIm_host=(QVECT*)malloc(num_PIm*sizeof(QVECT)); cudaMalloc((void**)&PIm_dev,num_PIm*sizeof(QVECT)); //Allocazione memoria per quadrivettori PIpiu su host e device PIp_host=(QVECT*)malloc(num_PIp*sizeof(QVECT)); cudaMalloc((void**)&PIp_dev,num_PIp*sizeof(QVECT)); //Allocazione memoria per quadrivettori Gamma su host e device Gamma_host=(QVECT*)malloc(num_Gamma*sizeof(QVECT)); cudaMalloc( (void**)&Gamma_dev,num_Gamma*sizeof(QVECT) ); //Allocazione memoria per vettore delle K0s sul device cudaMalloc((void**)&K0s_dev,(Tmax_K0s)*sizeof(PI_0)); //Allocazione memoria per vettore delle D0KPI sul device cudaMalloc((void**)&D0KPI_dev,(Tmax_D0KPI)*sizeof(PI_0)); //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)); //Allocazione memoria per vettore delle D0Ks2PI sul device cudaMalloc((void**)&D0Ks2PI_dev,(Tmax_D0Ks2PI)*sizeof(D0KPI_PI0)); Angelo Tebano 566/2750 Pagina 83 di 105 //Allocazione memoria per vettore delle D0K3PI sul device cudaMalloc((void**)&D0K3PI_dev,(Tmax_D0K3PI)*sizeof(D0KPI_PI0)); //Allocazione memoria per vettore delle Ds0D0PI0 sul device cudaMalloc((void**)&Ds0D0PI0_dev,(Tmax_Ds0D0PI0)*sizeof(D0)); //Il device, a fine calcoli, passera il numero di particelle k0s(dimk0s_dev) //all'host(dimk0s_host) mediante una cudaMemcpy() dimk0s_host=(unsigned int*)malloc(sizeof(unsigned int)); //Dimensione vettore k0s host cudaMalloc((void**)&dimk0s_dev,sizeof(unsigned int)); //Dimensione vettore k0s device //Il device, a fine calcoli, passera il numero di particelle D0KPI(dimD0KPI_dev) //all'host(dimD0KPI_host) mediante una cudaMemcpy() dimD0KPI_host=(unsigned int*)malloc(sizeof(unsigned int)); //Dimensione vettore D0KPI host cudaMalloc((void**)&dimD0KPI_dev,sizeof(unsigned int)); //Dimensione vettore D0KPI device //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(dimD0KPIPI0_dev) //all'host(dimD0KPIPI0_host) mediante una cudaMemcpy() dimD0KPIPI0_host=(unsigned int*)malloc(sizeof(unsigned int)); cudaMalloc((void**)&dimD0KPIPI0_dev,sizeof(unsigned int) ); //Il device, a fine calcoli, passera il numero di particelle D0Ks2PI(dimD0Ks2PI_dev) //all'host(dimD0Ks2PI_host) mediante una cudaMemcpy() dimD0Ks2PI_host=(unsigned int*)malloc(sizeof(unsigned int)); cudaMalloc((void**)&dimD0Ks2PI_dev,sizeof(unsigned int) ); //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)); cudaMalloc((void**)&dimD0K3PI_dev,sizeof(unsigned int) ); //Il device, a fine calcoli, passera il numero di particelle Ds0D0PI0(dimDs0D0PI0_dev) //all'host(dimDs0D0PI0_host) mediante una cudaMemcpy() dimDs0D0PI0_host=(unsigned int*)malloc(sizeof(unsigned int)); cudaMalloc((void**)&dimDs0D0PI0_dev,sizeof(unsigned int) ); //inizializzazione dei quadrivettori PI+, PI- e Gamma init_PIp(PIp_host, num_PIp); init_PIm(PIm_host, num_PIm); init_Gamma(Gamma_host, num_Gamma); //Copio il vettore delle PImeno dall'host(PIm_host) al device(PIm_dev) cudaMemcpy(PIm_dev,PIm_host,num_PIm*sizeof(QVECT),cudaMemcpyHostToDevice); //Copio il vettore delle PIpiu dall'host(PIp_host) al device(PIp_dev) cudaMemcpy(PIp_dev,PIp_host,num_PIp*sizeof(QVECT),cudaMemcpyHostToDevice); //Copio il vettore delle Gamma dall'host al device cudaMemcpy(Gamma_dev,Gamma_host,num_Gamma*sizeof(QVECT),cudaMemcpyHostToDevice); Angelo Tebano 566/2750 Pagina 84 di 105 //inizializzo dimensione array delle K0s cudaMemset(dimk0s_dev,0,sizeof(unsigned int)); //inizializzo dimensione array delle D0KPI cudaMemset(dimD0KPI_dev,0,sizeof(unsigned int)); //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 D0Ks2PI cudaMemset(dimD0Ks2PI_dev,0,sizeof(unsigned int)); //inizializzo dimensione array delle D0K3PI cudaMemset(dimD0K3PI_dev,0,sizeof(unsigned int)); //inizializzo dimensione array delle Ds0D0PI0 cudaMemset(dimDs0D0PI0_dev,0,sizeof(unsigned int)); /*timeline*/ /*timeline*/ /*timeline*/ cudaEventCreate(&start); cudaEventCreate(&stop); cudaEventRecord(start, 0); //Invocazione kernel kernel<<<dim3(XGRID,YGRID,1),dim3(32,32,1)>>>(PIp_dev, PIm_dev, Gamma_dev, num_PIp, num_PIm, num_Gamma, prod_cart, coefBin, massa0, delta, K0s_dev, D0KPI_dev, PI0_dev, D0KPIPI0_dev, dimk0s_dev, dimD0KPI_dev, dimPI0_dev, 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, Angelo Tebano 566/2750 Pagina 85 di 105 Tmax_D0KPIPI0, Tmax_D0Ks2PI, Tmax_D0K3PI, Tmax_Ds0D0PI0); cudaThreadSynchronize(); //Barriera di sincronizzazione thread /*timeline*/ /*timeline*/ /*timeline*/ /*timeline*/ cudaEventRecord(stop, 0); cudaEventSynchronize(stop); cudaEventElapsedTime(&time, start, stop); printf("\n[tempo totale lordo kernel] Time elapsed: %f ms\n\n\n", time); //Copio la dimensione dell'array delle K0s dal device all'host cudaMemcpy(dimk0s_host,dimk0s_dev,sizeof(unsigned int),cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, K0s K0s_host=(PI_0*)malloc(*dimk0s_host*sizeof(PI_0)); //Copio l'array dei risultati K0s dal device all'host cudaMemcpy(K0s_host,K0s_dev,*dimk0s_host*sizeof(PI_0),cudaMemcpyDeviceToHost); //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),cudaMemcpyDeviceToHost); //Copio la dimensione dell'array delle D0KPI dal device all'host cudaMemcpy(dimD0KPI_host,dimD0KPI_dev,sizeof(unsigned int),cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, D0KPI (compreso D0KPI_bad) D0KPI_host=(PI_0*)malloc(*dimD0KPI_host*sizeof(PI_0)); //Copio l'array dei D0KPI dal device all'host cudaMemcpy(D0KPI_host,D0KPI_dev,*dimD0KPI_host*sizeof(PI_0),cudaMemcpyDeviceToHost); //Copio la dimensione dell'array delle D0KPIPI0 dal device all'host cudaMemcpy(dimD0KPIPI0_host,dimD0KPIPI0_dev,sizeof(unsignedint),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'cudaMemcpy(D0KPIPI0_host,D0KPIPI0_dev,*dimD0KPIPI0_host*sizeof(D0KPI_PI0),cudaMemcpyDe viceToHost); //Copio la dimensione dell'array delle D0Ks2PI dal device all'host cudaMemcpy(dimD0Ks2PI_host,dimD0Ks2PI_dev,sizeof(unsignedint),cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, D0Ks2PI D0Ks2PI_host=(D0KPI_PI0*)malloc(*dimD0Ks2PI_host*sizeof(D0KPI_PI0)); //Copio l'array dei risultati D0Ks2PI dal device all'host cudaMemcpy(D0Ks2PI_host,D0Ks2PI_dev,*dimD0Ks2PI_host*sizeof(D0KPI_PI0),cudaMemcpyDeviceTo Host); //Copio la dimensione dell'array delle D0K3PI dal device all'host cudaMemcpy(dimD0K3PI_host,dimD0K3PI_dev,sizeof(unsigned int),cudaMemcpyDeviceToHost); //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),cudaMemcpyDeviceToHo st); Angelo Tebano 566/2750 Pagina 86 di 105 //Copio la dimensione dell'array delle Ds0D0PI0 dal device all'host cudaMemcpy(dimDs0D0PI0_host,dimDs0D0PI0_dev,sizeof(unsigned int),cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, D0K3PI Ds0D0PI0_host=(D0*)malloc(*dimDs0D0PI0_host*sizeof(D0)); //Copio l'array dei risultati Ds0D0PI0 dal device all'host cudaMemcpy(Ds0D0PI0_host,Ds0D0PI0_dev,*dimDs0D0PI0_host*sizeof(D0),cudaMemcpyDeviceToHost) stampa_risultati(PI0_host, dimPI0_host, K0s_host, dimk0s_host, D0KPI_host, dimD0KPI_host, D0KPIPI0_host, dimD0KPIPI0_host, PIp_host, PIm_host, D0Ks2PI_host, dimD0Ks2PI_host, D0K3PI_host, dimD0K3PI_host, Ds0D0PI0_host, dimDs0D0PI0_host); //Rilascio memoria strutture e dimensioni sul device cudaFree(PIp_dev); cudaFree(PIm_dev); cudaFree (Gamma_dev); cudaFree(K0s_dev); cudaFree(D0KPI_dev); cudaFree(PI0_dev); cudaFree(D0KPIPI0_dev); cudaFree(D0Ks2PI_dev); cudaFree(D0K3PI_dev); cudaFree(Ds0D0PI0_dev); cudaFree(dimk0s_dev); cudaFree(dimD0KPI_dev); cudaFree(dimPI0_dev); cudaFree(dimD0KPIPI0_dev); cudaFree(dimD0Ks2PI_dev); cudaFree(dimD0K3PI_dev); cudaFree(dimDs0D0PI0_dev); //Rilascio memoria strutture e dimensioni sull'host free(PIm_host); free(PIp_host); free(Gamma_host); free(K0s_host); free(D0KPI_host); free(PI0_host); free(D0KPIPI0_host); free(D0Ks2PI_host); Angelo Tebano 566/2750 Pagina 87 di 105 free(D0K3PI_host); free(Ds0D0PI0_host); free(dimk0s_host); free(dimD0KPI_host); free(dimPI0_host); free(dimD0KPIPI0_host); free(dimD0Ks2PI_host); free(dimD0K3PI_host); free(dimDs0D0PI0_host); return 0; } A.5. Kernel function – versione parallela __global__ void kernel( Angelo Tebano 566/2750 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 array K0s e D0KPI unsigned int Max_dim2,//dimensione massima array PI0 float M0, //massa di riferimento float del, //delta PI_0 *K0s, //array delle particelle k0s risultanti PI_0 *D0KPI, //array delle particelle D0KPI risultanti PI_0 *PI0, //array delle particelle PI0 risultanti D0KPI_PI0 *D0KPIPI0, //array delle particelle D0KPIPI0 risultanti unsigned int *dimk0s, //dimensione effettiva array K0s unsigned int *dimD0KPI, //dimensione effettiva array D0KPI unsigned int *dimPI0, //dimensione effettiva array PI0 unsigned int *dimD0KPIPI0, //dimensione effettiva array D0KPIPI0 unsigned int max_D0KPIPI0, //dimensione massima array D0KPIPI0 D0KPI_PI0 *D0Ks2PI, //array delle particelle D0Ks2PI risultanti unsigned int *dimD0Ks2PI, //dimensione effettiva array D0Ks2PI unsigned int max_D0Ks2PI, //dimensione massima array D0Ks2PI D0KPI_PI0 *D0K3PI, //array delle particelle D0K3PI unsigned int *dimD0K3PI, //dimensione effettiva array D0K3PI unsigned int max_D0K3PI, //dimensione massima array D0K3PI D0 *Ds0D0PI0, //array delle particelle DS0DOPI0 unsigned int *dimDs0D0PI0, //dimensione effettiva array 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 DS0D0PI0 Pagina 88 di 105 { /*timeline*/ //Calcolo del thread ID /*timeline*/ int block_thread_id=threadIdx.x+blockDim.x*threadIdx.y; /*timeline*/ int tid = blockDim.x*blockDim.y*(blockIdx.x + gridDim.x* blockIdx.y) +block_thread_id; /*timeline*/ __syncthreads(); /*timeline*/ clock_t start = clock(); /*timeline*/ clock_t globe = clock(); //Invoco funzione per il calcolo delle k0s createK0s(PIp,PIm,N,M,Max_dim,M0,del,K0s,dimk0s,Tmax_K0s); /*timeline*/ __syncthreads(); /*timeline*/ if(tid==0) /*timeline*/ { /*timeline*/ printf("[createK0s] CLOCKS_PER_SEC); /*timeline*/ start = clock(); /*timeline*/ } Time elapsed: %f ms\n", ((double)clock() - start) / //Invoco funzione per il calcolo delle D0KPI createD0KPI(PIp,PIm,N,M,Max_dim,M0,del,D0KPI,dimD0KPI,Tmax_D0KPI); /*timeline*/ __syncthreads(); /*timeline*/ if(tid==0) /*timeline*/ { /*timeline*/ printf("[createD0KPI] CLOCKS_PER_SEC); /*timeline*/ start = clock(); /*timeline*/ } Time elapsed: %f ms\n", ((double)clock() - start) / //Invoco la funzione per il calcolo delle PI0 createPI0(Gamma,L,Max_dim2,M0,del,PI0,dimPI0,Tmax_PI0); /*timeline*/ __syncthreads(); /*timeline*/ if(tid==0) /*timeline*/ { /*timeline*/ printf("[createPI0] CLOCKS_PER_SEC); /*timeline*/ start = clock(); /*timeline*/ } Time elapsed: %f ms\n", ((double)clock() - start) / max_D0KPIPI0=Max_dim*(*dimPI0); //Invoco la funzione per il calcolo delle D0KPIPI0 createD0KPIPI0(PIp,PIm,PI0,N,M,*dimPI0,max_D0KPIPI0,M0,del,D0KPIPI0,dimD0KPIPI0,Tmax_D0KPI PI0); /*timeline*/ __syncthreads(); /*timeline*/ if(tid==0) /*timeline*/ { /*timeline*/ printf("[createD0KPIPI0] Time elapsed: %f ms\n", ((double)clock() - start) / CLOCKS_PER_SEC); /*timeline*/ start = clock(); /*timeline*/ } Angelo Tebano 566/2750 Pagina 89 di 105 max_D0Ks2PI=N*M*(*dimk0s)*2; //Invoco la funzione per il calcolo delle D0Ks2PI createD0Ks2PI(PIp,PIm,K0s,N,M,*dimk0s,max_D0Ks2PI,M0,del,D0Ks2PI,dimD0Ks2PI,Tmax_D0Ks2PI); /*timeline*/ __syncthreads(); /*timeline*/ if(tid==0) /*timeline*/ { /*timeline*/ printf("[createD0Ks2PI] CLOCKS_PER_SEC); /*timeline*/ start = clock(); /*timeline*/ } Time elapsed: %f ms\n", ((double)clock() - start) / max_D0K3PI=N*(M*M*N); //Invoco la funzione per il calcolo delle D0K3PI createD0K3PI(PIp,PIm,N,M,max_D0K3PI,M0,del,D0K3PI,dimD0K3PI,Tmax_D0K3PI); /*timeline*/ __syncthreads(); /*timeline*/ if(tid==0) /*timeline*/ { /*timeline*/ printf("[createD0K3PI] CLOCKS_PER_SEC); /*timeline*/ start = clock(); /*timeline*/ } Time elapsed: %f ms\n", ((double)clock() - start) / int tot_el=(*dimD0KPI)+(*dimD0KPIPI0)+(*dimD0Ks2PI)+(*dimD0K3PI); int cartesio=tot_el*(*dimPI0); //Invoco la funzione per il calcolo delle Ds0D0PI0 createDs0D0PI0(D0KPI,dimD0KPI,D0KPIPI0,dimD0KPIPI0,D0Ks2PI,dimD0Ks2PI,D0K3PI,dimD0K3PI,t ot_el,cartesio,PI0,dimPI0,M0,del, Ds0D0PI0,dimDs0D0PI0,Tmax_Ds0D0PI0); /*timeline*/ __syncthreads(); /*timeline*/ if(tid==0) /*timeline*/ { /*timeline*/ printf("[createDs0D0PI0] Time elapsed: %f ms\n", ((double)clock() - start) / CLOCKS_PER_SEC); /*timeline*/ printf("[tempo totale netto kernel] Time elapsed: %f ms\n", ((double)clock() - globe) / CLOCKS_PER_SEC); /*timeline*/ } } Angelo Tebano 566/2750 Pagina 90 di 105 A.6. Modulo D0 (K0s Π+ Π- e K0s Π- Π+) – versione parallela __device__ void createD0Ks2PI( QVECT *PIp, //array dei PIp QVECT *PIm, //array dei PIPI_0 *K0s, //array delle particelle K0s unsigned int N, //dim PIp unsigned int M, //dim PIm unsigned int dimK0s, //dimensione effettiva K0s unsigned int Max_dim, //dimensione massima D0KPIPI0 float M0, //massa di riferimento float del, //delta D0KPI_PI0 *D0Ks2PI, //array delle particelle D0KPIPI0 risultanti unsigned int *dimD0Ks2PI, long Tmax_D0Ks2PI){ //dimensione massima TOY dell'array 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; QVECT Candidato; //massa del quadrivettore 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; tid2 = int(tid /dimK0s); //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 % dimK0s; //k-esima particella K0s if((PIp[i].gen == 'a') && (PIm[j].gen == 'a')){ //if gen gen if((K0s[k].g1!=i)&&(K0s[k].g2!=j)) //if g1 g2 { //Somma delle componenti della i-esima particella PIpiu genere k e della j-esima PImeno k Candidato.x= PIp[i].x + PIm[j].x + K0s[k].x; Candidato.y= PIp[i].y + PIm[j].y + K0s[k].y; Candidato.z= PIp[i].z + PIm[j].z + K0s[k].z; Candidato.Ene= PIp[i].Ene + PIm[j].Ene + K0s[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); Angelo Tebano 566/2750 Pagina 91 di 105 //Controllo sulla massa if(massa>M0-del && massa<M0+del && (*dimD0Ks2PI)<=Tmax_D0Ks2PI){ loc=atomicAdd(dimD0Ks2PI,1); //incremento il numero di particelle D0Ks2PI D0Ks2PI[loc].x=Candidato.x; D0Ks2PI[loc].y=Candidato.y; D0Ks2PI[loc].z=Candidato.z; D0Ks2PI[loc].Ene=Candidato.Ene; D0Ks2PI[loc].g1=k; //Particella genitrice K0s D0Ks2PI[loc].g2=i; //Particella genitrice PIpiu D0Ks2PI[loc].g3=j; //Particella genitrice PImeno } } } } } A.7. Modulo D*0 (D0 Π0) – versione parallela __device__ void createDs0D0PI0(PI_0 *D0KPI, unsigned int *dimD0KPI, D0KPI_PI0 *D0KPIPI0, unsigned int *dimD0KPIPI0, D0KPI_PI0 *D0Ks2PI, unsigned int *dimD0Ks2PI, D0KPI_PI0 *D0K3PI, unsigned int *dimD0K3PI, unsigned int tot_el, //array delle D0KPI //dimensione effettiva array delle D0KPI //array delle D0KPIPI0 //dimensione effettiva array delle D0KPIPI0 //array D0Ks2PI //dimensione effettiva array delle D0Ks2PI //array delle D0K3PI //dimensione effettiva array delle D0K3PI //somma delle dimensioni degli array D0KPI, D0KPIPI0, D0Ks2PI, D0K3PI unsigned int cartesio, //numero thread necessari per combinare D0 con PI0 PI_0 *PI0, //array delle PI0 unsigned int *dimPI0, //dimensione effettiva array delle PI0 float M0, //massa di riferimento float del, //delta D0 *Ds0D0PI0, //array delle DS0D0PI0 unsigned int *dimDs0D0PI0, //dimensione effettiva array delle DS0D0PI0 long Tmax_Ds0D0PI0 //dimensione massima array delle DS0D0PI0 ) { int i,j,k,loc; QVECT Candidato; float massa; //Calcolo del thread ID int block_thread_id=threadIdx.x+blockDim.x*threadIdx.y; int tid = blockDim.x*blockDim.y*(blockIdx.x + gridDim.x* blockIdx.y) + block_thread_id; if(tid<cartesio) { i=tid/(*dimPI0); j=tid%(*dimPI0); Angelo Tebano 566/2750 Pagina 92 di 105 if(i<(*dimD0KPI)) //Somma D0KPI e PI0 { //Somma delle componenti della i-esima particella D0KPI e della j-esima PI0 Candidato.x= D0KPI[i].x + PI0[j].x; Candidato.y= D0KPI[i].y + PI0[j].y; Candidato.z= D0KPI[i].z + PI0[j].z; Candidato.Ene= D0KPI[i].Ene + PI0[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(massa>M0-del && massa<M0+del && (*dimDs0D0PI0)<=Tmax_Ds0D0PI0){ loc=atomicAdd(dimDs0D0PI0,1); //incremento l'indice dell'array dei dimDs0D0PI0 Ds0D0PI0[loc].x=Candidato.x; Ds0D0PI0[loc].y=Candidato.y; Ds0D0PI0[loc].z=Candidato.z; Ds0D0PI0[loc].Ene=Candidato.Ene; Ds0D0PI0[loc].g1=0; //Etichetta array componente di D0 (0=D0KPI) Ds0D0PI0[loc].g2=i; //Elemento della componente di D0 Ds0D0PI0[loc].g3=j; //Particella genitrice PI0 } } else if(i<(*dimD0KPI)+(*dimD0KPIPI0)) //Somma D0KPIPI0 e PI0 { k=i-(*dimD0KPI); //Somma delle componenti della k-esima particella D0KPIPI0 e della j-esima PI0 Candidato.x= D0KPIPI0[k].x + PI0[j].x; Candidato.y= D0KPIPI0[k].y + PI0[j].y; Candidato.z= D0KPIPI0[k].z + PI0[j].z; Candidato.Ene= D0KPIPI0[k].Ene + PI0[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(massa>M0-del && massa<M0+del && (*dimDs0D0PI0)<=Tmax_Ds0D0PI0){ loc=atomicAdd(dimDs0D0PI0,1); //incremento l'indice dell'array dei dimDs0D0PI0 Ds0D0PI0[loc].x=Candidato.x; Ds0D0PI0[loc].y=Candidato.y; Ds0D0PI0[loc].z=Candidato.z; Ds0D0PI0[loc].Ene=Candidato.Ene; Ds0D0PI0[loc].g1=1; //Etichetta array componente di D0 (1=D0KPIPI0) Ds0D0PI0[loc].g2=k; //Elemento della componente di D0 Ds0D0PI0[loc].g3=j; //Particella genitrice PI0 } } else if(i<(*dimD0KPI)+(*dimD0KPIPI0)+(*dimD0Ks2PI)) //Somma D0Ks2PI e PI0 { k=i-(*dimD0KPI)-(*dimD0KPIPI0); //Somma delle componenti della k-esima particella D0Ks2PI e della j-esima PI0 Candidato.x= D0Ks2PI[k].x + PI0[j].x; Angelo Tebano 566/2750 Pagina 93 di 105 Candidato.y= D0Ks2PI[k].y + PI0[j].y; Candidato.z= D0Ks2PI[k].z + PI0[j].z; Candidato.Ene= D0Ks2PI[k].Ene + PI0[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(massa>M0-del && massa<M0+del && (*dimDs0D0PI0)<=Tmax_Ds0D0PI0){ loc=atomicAdd(dimDs0D0PI0,1); //incremento l'indice dell'array dei dimDs0D0PI0 Ds0D0PI0[loc].x=Candidato.x; Ds0D0PI0[loc].y=Candidato.y; Ds0D0PI0[loc].z=Candidato.z; Ds0D0PI0[loc].Ene=Candidato.Ene; Ds0D0PI0[loc].g1=2; //Etichetta array componente di D0 (2=D0Ks2PI) Ds0D0PI0[loc].g2=k; //Elemento della componente di D0 Ds0D0PI0[loc].g3=j; //Particella genitrice PI0 } } else { //Somma D0K3PI e PI0 k=i-(*dimD0KPI)-(*dimD0KPIPI0)-(*dimD0Ks2PI); //Somma delle componenti della k-esima particella D0K3PI e della j-esima PI0 Candidato.x= D0K3PI[k].x + PI0[j].x; Candidato.y= D0K3PI[k].y + PI0[j].y; Candidato.z= D0K3PI[k].z + PI0[j].z; Candidato.Ene= D0K3PI[k].Ene + PI0[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(massa>M0-del && massa<M0+del && (*dimDs0D0PI0)<=Tmax_Ds0D0PI0){ loc=atomicAdd(dimDs0D0PI0,1); //incremento l'indice dell'array dei dimDs0D0PI0 Ds0D0PI0[loc].x=Candidato.x; Ds0D0PI0[loc].y=Candidato.y; Ds0D0PI0[loc].z=Candidato.z; Ds0D0PI0[loc].Ene=Candidato.Ene; Ds0D0PI0[loc].g1=3; //Etichetta array componente di D0 (3=D0K3PI) Ds0D0PI0[loc].g2=k; //Elemento della componente di D0 Ds0D0PI0[loc].g3=j; //Particella genitrice PI0 } } } } Angelo Tebano 566/2750 Pagina 94 di 105 A.8. Main function – versione “multi-evento” 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; float massa0=pow(4.5,2), delta=6; //massa di riferimento //delta //Dati di output PI_0 *K0s_host, *K0s_dev; PI_0 *D0KPI_host, *D0KPI_dev; PI_0 *PI0_host, *PI0_dev; //Aos contenente le K0s, su host e device //Aos contenente le D0KPI, su host e device //Aos contenente le PI0, su host e device D0KPI_PI0 *D0KPIPI0_host, *D0KPIPI0_dev; D0KPI_PI0 *D0Ks2PI_host, *D0Ks2PI_dev; D0KPI_PI0 *D0K3PI_host, *D0K3PI_dev; D0 *Ds0D0PI0_host, *Ds0D0PI0_dev; unsigned int //Aos contenente le D0KPIPI0, su host e device //Aos contenente le D0Ks2PI, su host e device //Aos contenente le D0K3PI, su host e device //Aos contenente le Ds0D0PI0, su host e device prod_cart, //dimensione massima dell'array delle K0s e D0KPI coefBin, //dimensione massima dell'array dei PI0 max_D0KPIPI0=0, //dimensione massima dell'array D0KPIPI0 max_D0Ks2PI=0, //dimensione massima dell'array D0Ks2PI max_D0K3PI=0, //dimensione massima dell'array D0K3PI *dimk0s_host, *dimk0s_dev, //dimensione effettiva array K0s *dimD0KPI_host, *dimD0KPI_dev, //dimensione effettiva array D0KPI *dimPI0_host, *dimPI0_dev, //dimensione effettiva array PI0 *dimD0KPIPI0_host, *dimD0KPIPI0_dev, *dimD0Ks2PI_host, *dimD0Ks2PI_dev, *dimDs0D0PI0_host, *dimDs0D0PI0_dev, *dimD0K3PI_host, *dimD0K3PI_dev; long Tmax_K0s=1000, Tmax_D0KPI=1000, Tmax_PI0=1000, Tmax_D0KPIPI0=1000, Tmax_D0Ks2PI=1000, Tmax_D0K3PI=1000, Tmax_Ds0D0PI0=1000; Angelo Tebano 566/2750 Pagina 95 di 105 /*timeline*/ /*timeline*/ cudaEvent_t start, stop; float time; remove("outlog2.txt"); /*timeline*/ /*timeline*/ /*timeline*/ cudaEventCreate(&start); cudaEventCreate(&stop); cudaEventRecord(start, 0); //Apertura del file in input fd=fopen("gpufile2.txt", "r"); if( fd==NULL ) { perror("Errore in apertura del file di input"); exit(1); } fd2=fopen("outlog2.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 K0s sul device cudaMalloc((void**)&K0s_dev,(Tmax_K0s)*sizeof(PI_0)); //Allocazione memoria per vettore delle D0KPI sul device cudaMalloc((void**)&D0KPI_dev,(Tmax_D0KPI)*sizeof(PI_0)); //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)); //Allocazione memoria per vettore delle D0Ks2PI sul device cudaMalloc((void**)&D0Ks2PI_dev,(Tmax_D0Ks2PI)*sizeof(D0KPI_PI0)); //Allocazione memoria per vettore delle D0K3PI sul device cudaMalloc((void**)&D0K3PI_dev,(Tmax_D0K3PI)*sizeof(D0KPI_PI0)); //Allocazione memoria per vettore delle Ds0D0PI0 sul device cudaMalloc((void**)&Ds0D0PI0_dev,(Tmax_Ds0D0PI0)*sizeof(D0)); //Il device, a fine calcoli, passera il numero di particelle k0s(dimk0s_dev) // all'host(dimk0s_host) mediante una cudaMemcpy() dimk0s_host=(unsigned int*)malloc(sizeof(unsigned int)); //Dimensione vettore k0s host cudaMalloc((void**)&dimk0s_dev,sizeof(unsigned int)); //Dimensione vettore k0s device //Il device, a fine calcoli, passera il numero di particelle D0KPI(dimD0KPI_dev) //all'host(dimD0KPI_host) mediante una cudaMemcpy() dimD0KPI_host=(unsigned int*)malloc(sizeof(unsigned int)); //Dimensione vettore D0KPI host cudaMalloc((void**)&dimD0KPI_dev,sizeof(unsigned int)); //Dimensione vettore D0KPI device Angelo Tebano 566/2750 Pagina 96 di 105 //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(dimD0KPIPI0_dev) //all'host(dimD0KPIPI0_host) mediante una cudaMemcpy() dimD0KPIPI0_host=(unsigned int*)malloc(sizeof(unsigned int)); cudaMalloc((void**)&dimD0KPIPI0_dev,sizeof(unsigned int) ); //Il device, a fine calcoli, passera il numero di particelle D0Ks2PI(dimD0Ks2PI_dev) //all'host(dimD0Ks2PI_host) mediante una cudaMemcpy() dimD0Ks2PI_host=(unsigned int*)malloc(sizeof(unsigned int)); //Dimensione vettore D0Ks2PI host cudaMalloc((void**)&dimD0Ks2PI_dev,sizeof(unsigned int) ); //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) ); //Il device, a fine calcoli, passera il numero di particelle Ds0D0PI0(dimDs0D0PI0_dev) //all'host(dimDs0D0PI0_host) mediante una cudaMemcpy() dimDs0D0PI0_host=(unsigned int*)malloc(sizeof(unsigned int)); //Dimensione vettore Ds0D0PI0 host cudaMalloc((void**)&dimDs0D0PI0_dev,sizeof(unsigned int) ); //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 { 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++; } Angelo Tebano 566/2750 Pagina 97 di 105 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 host e device //PIm_host=(QVECT*)malloc(numpim*sizeof(QVECT)); cudaMalloc((void**)&PIm_dev,numpim*sizeof(QVECT)); //Allocazione memoria per quadrivettori PIpiu su host e device //PIp_host=(QVECT*)malloc(numpip*sizeof(QVECT)); cudaMalloc((void**)&PIp_dev,numpip*sizeof(QVECT)); //Allocazione memoria per quadrivettori Gamma su host e device //Gamma_host=(QVECT*)malloc(numgamma*sizeof(QVECT)); cudaMalloc( (void**)&Gamma_dev,numgamma*sizeof(QVECT) ); //Copio il vettore delle PImeno dall'host(PIm_host) al device(PIm_dev) cudaMemcpy(PIm_dev,PIm_host,numpim*sizeof(QVECT),cudaMemcpyHostToDevice); //Copio il vettore delle PIpiu dall'host(PIp_host) al device(PIp_dev) cudaMemcpy(PIp_dev,PIp_host,numpip*sizeof(QVECT),cudaMemcpyHostToDevice); //Copio il vettore delle Gamma dall'host(Gamma_host) al device(Gamma_dev) cudaMemcpy(Gamma_dev,Gamma_host,numgamma*sizeof(QVECT),cudaMemcpyHostToDevice); //inizializzo dimensione array delle K0s cudaMemset(dimk0s_dev,0,sizeof(unsigned int)); //inizializzo dimensione array delle D0KPI cudaMemset(dimD0KPI_dev,0,sizeof(unsigned int)); //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 D0Ks2PI cudaMemset(dimD0Ks2PI_dev,0,sizeof(unsigned int)); //inizializzo dimensione array delle D0K3PI cudaMemset(dimD0K3PI_dev,0,sizeof(unsigned int)); //inizializzo dimensione array delle Ds0D0PI0 cudaMemset(dimDs0D0PI0_dev,0,sizeof(unsigned int)); /*timeline*/ // cudaEventCreate(&start); /*timeline*/ // cudaEventCreate(&stop); /*timeline*/ //cudaEventRecord(start, 0); quadriprodotto=numpip*numpim*numpim*numpip; quadriprodotto=quadriprodotto/1024; latogriglia=sqrt(quadriprodotto); Angelo Tebano 566/2750 Pagina 98 di 105 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, massa0, delta, K0s_dev, D0KPI_dev, PI0_dev, D0KPIPI0_dev, dimk0s_dev, dimD0KPI_dev, dimPI0_dev, 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 /*timeline*/ //cudaEventRecord(stop, 0); /*timeline*/ //cudaEventSynchronize(stop); /*timeline*/ //cudaEventElapsedTime(&time, start, stop); /*timeline*/ //printf("\n[tempo totale lordo kernel] Time elapsed: %f ms\n\n\n", time); //Copio la dimensione dell'array delle K0s dal device all'host cudaMemcpy(dimk0s_host,dimk0s_dev,sizeof(unsigned int),cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, K0s K0s_host=(PI_0*)malloc(*dimk0s_host*sizeof(PI_0)); //Copio l'array dei risultati K0s dal device all'host cudaMemcpy(K0s_host,K0s_dev,*dimk0s_host*sizeof(PI_0),cudaMemcpyDeviceToHost); Angelo Tebano 566/2750 Pagina 99 di 105 //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),cudaMemcpyDeviceToHost); //Copio la dimensione dell'array delle D0KPI dal device all'host cudaMemcpy(dimD0KPI_host,dimD0KPI_dev,sizeof(unsigned int),cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, D0KPI (compreso D0KPI_bad) D0KPI_host=(PI_0*)malloc(*dimD0KPI_host*sizeof(PI_0)); //Copio l'array dei D0KPI dal device all'host cudaMemcpy(D0KPI_host,D0KPI_dev,*dimD0KPI_host*sizeof(PI_0),cudaMemcpyDeviceToHost); //Copio la dimensione dell'array delle D0KPIPI0 dal device all'host cudaMemcpy(dimD0KPIPI0_host,dimD0KPIPI0_dev,sizeof(unsignedint),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),cudaMemcpyDevic eToHost); //Copio la dimensione dell'array delle D0Ks2PI dal device all'host cudaMemcpy(dimD0Ks2PI_host,dimD0Ks2PI_dev,sizeof(unsignedint),cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, D0Ks2PI D0Ks2PI_host=(D0KPI_PI0*)malloc(*dimD0Ks2PI_host*sizeof(D0KPI_PI0)); //Copio l'array dei risultati D0Ks2PI dal device all'host cudaMemcpy(D0Ks2PI_host,D0Ks2PI_dev,*dimD0Ks2PI_host*sizeof(D0KPI_PI0),cudaMemcpyDeviceTo Host); //Copio la dimensione dell'array delle D0K3PI dal device all'host cudaMemcpy(dimD0K3PI_host,dimD0K3PI_dev,sizeof(unsigned int),cudaMemcpyDeviceToHost); //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),cudaMemcpyDeviceToHo st); //Copio la dimensione dell'array delle Ds0D0PI0 dal device all'host cudaMemcpy(dimDs0D0PI0_host,dimDs0D0PI0_dev,sizeof(unsignedint),cudaMemcpyDeviceToHost); //Alloco memoria sull'host per l'array dei risultati, D0K3PI Ds0D0PI0_host=(D0*)malloc(*dimDs0D0PI0_host*sizeof(D0)); //Copio l'array dei risultati Ds0D0PI0 dal device all'host cudaMemcpy(Ds0D0PI0_host,Ds0D0PI0_dev,*dimDs0D0PI0_host*sizeof(D0),cudaMemcpyDeviceToHost) 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,"K0s\n"); for(i=0; i<(*dimk0s_host) ;i++) fprintf(fd2,"%f %f %f %f %d %d\n",K0s_host[i].x,K0s_host[i].y,K0s_host[i].z,K0s_host[i].Ene,K0s_host[i].g1,K0s_host[i].g2); Angelo Tebano 566/2750 Pagina 100 di 105 fprintf(fd2,"D0KPI\n"); for(i=0; i<(*dimD0KPI_host) ;i++) fprintf(fd2,"%f %f %f %f %d %d\n",D0KPI_host[i].x,D0KPI_host[i].y,D0KPI_host[i].z,D0KPI_host[i].Ene,D0KPI_host[i].g1,D0KPI_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,"D0Ks2PI\n"); for(i=0; i<(*dimD0Ks2PI_host) ;i++) fprintf(fd2,"%f %f %f %f %d %d %d\n",D0Ks2PI_host[i].x,D0Ks2PI_host[i].y,D0Ks2PI_host[i].z,D0Ks2PI_host[i].Ene,D0Ks2PI_host[i].g1, D0Ks2PI_host[i].g2,D0Ks2PI_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,D0K3 PI_host[i].g2,D0K3PI_host[i].g3,D0K3PI_host[i].g4); fprintf(fd2,"Ds0D0PI0\n"); for(i=0; i<(*dimDs0D0PI0_host) ;i++) fprintf(fd2,"%f %f %f %f %d %d %d\n",Ds0D0PI0_host[i].x,Ds0D0PI0_host[i].y,Ds0D0PI0_host[i].z,Ds0D0PI0_host[i].Ene,Ds0D0PI0_host[ i].g1,Ds0D0PI0_host[i].g2,Ds0D0PI0_host[i].g3); fprintf(fd2,"ENDEVENT\n"); npi0=npi0+(double)(*dimPI0_host); nk0s=nk0s+(double)(*dimk0s_host); nd0kpi=nd0kpi+(double)(*dimD0KPI_host); nd0kpipi0=nd0kpipi0+(double)(*dimD0KPIPI0_host); nd0ks2pi=nd0ks2pi+(double)(*dimD0Ks2PI_host); nd0k3pi=nd0k3pi+(double)(*dimD0K3PI_host); nds0d0pi0=nds0d0pi0+(double)(*dimDs0D0PI0_host); stampa_risultati(PI0_host, dimPI0_host, K0s_host, dimk0s_host, D0KPI_host, dimD0KPI_host, D0KPIPI0_host, dimD0KPIPI0_host, PIp_host, PIm_host, D0Ks2PI_host, dimD0Ks2PI_host, D0K3PI_host, dimD0K3PI_host, Ds0D0PI0_host, dimDs0D0PI0_host); Angelo Tebano 566/2750 Pagina 101 di 105 //Rilascio memoria strutture e dimensioni sul device cudaFree(PIp_dev); cudaFree(PIm_dev); cudaFree(Gamma_dev); cudaFree(K0s_dev); cudaFree(D0KPI_dev); cudaFree(PI0_dev); cudaFree(D0KPIPI0_dev); cudaFree(D0Ks2PI_dev); cudaFree(D0K3PI_dev); cudaFree(Ds0D0PI0_dev); cudaFree(dimk0s_dev); cudaFree(dimD0KPI_dev); cudaFree(dimPI0_dev); cudaFree(dimD0KPIPI0_dev); cudaFree(dimD0Ks2PI_dev); cudaFree(dimD0K3PI_dev); cudaFree(dimDs0D0PI0_dev); //Rilascio memoria strutture e dimensioni sull'host free(PIm_host); free(PIp_host); free(Gamma_host); free(K0s_host); free(D0KPI_host); free(PI0_host); free(D0KPIPI0_host); free(D0Ks2PI_host); free(D0K3PI_host); free(Ds0D0PI0_host); free(dimk0s_host); free(dimD0KPI_host); free(dimPI0_host); free(dimD0KPIPI0_host); free(dimD0Ks2PI_host); free(dimD0K3PI_host); free(dimDs0D0PI0_host); numpip=0; numpim=0; numgamma=0; res=fgets(buf, 100, fd); } /* Chiusura dei file di input ed output */ fclose(fd); fclose(fd2); /*timeline*/ /*timeline*/ /*timeline*/ cudaEventRecord(stop, 0); cudaEventSynchronize(stop); cudaEventElapsedTime(&time, start, stop); Angelo Tebano 566/2750 Pagina 102 di 105 /*timeline*/ printf("\n[tempo totale lordo kernel] Time elapsed: %f ms\n", time); 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; } Angelo Tebano 566/2750 Pagina 103 di 105 Bibliografia (1) B Decays – Revised 2nd Edition; Sheldon Stone, World Scientific (2) CUDA by Example - An Introduction to General-Purpose GPU Programming; Jason Sanders, Edward Kandrot (3) NVIDIA CUDA C Programming Guide Version 4.0; NVIDIA (4) Programming Massively Parallel Processors A Hands-on approach, David B. Kirk, Wen-mei W. Hwu Angelo Tebano 566/2750 Pagina 104 di 105 Ringraziamenti Un affettuoso ringraziamento va a coloro che mi hanno permesso di svolgere questa esperienza di tirocinio: al Prof. Guido Russo, al Dr. Silvio Pardi ed al Dr. Guglielmo De Nardo, la cui assistenza è stata indispensabile quanto puntuale. Un saluto, con la speranza che le nostre strade si incontrino presto di nuovo, va a coloro che con me hanno condiviso questa esperienza: Andrea e Luigi. Un abbraccio (che ha il sapore del “ce l'ho fatta anch'io!”) va a tutti gli altri compagni d'avventura che sono stati, negli anni, parte integrante della mia vita universitaria. Angelo Tebano 566/2750 Pagina 105 di 105