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