Università degli Studi di Napoli Federico II

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