Sei sulla pagina 1di 16

DEI - Department of Information Engineering

Università degli Studi di Padova

Facoltà di Ingegneria
Corso di Laurea MAGISTRALE in Ingegneria Informatica

Corso di Calcolo Parallelo

Progetto:
Implementazione parallela della
FFT su IBM RS/6000 SP con
librerie MPI.

Studenti: Professore:
Giuseppe Cassano 602102 Prof. Gianfranco Bilardi
Mirco Furlan 601050
Nicolo Paganin 607267 Assistente:
Ing. Michele Scquizzato

Anno Accademico 2009/2010 01.04.2010


.
Indice 2

Indice
1 Introduzione 3
1.1 Fast Fourier Transform . . . . . . . . . . . . . . . . . . . . . . 3

2 Descrizione dell’algortimo 3

3 Implementazione Parallela 5
3.1 Descrizione dell’implementazione . . . . . . . . . . . . . . . . 5
3.2 Funzionamento e scelte implementative . . . . . . . . . . . . . 6

4 Analisi delle prestazioni 8


4.1 Prestazioni con Shared Memory . . . . . . . . . . . . . . . . . 10
4.1.1 Analisi dei dati . . . . . . . . . . . . . . . . . . . . . . 12
4.2 Prestazioni senza Shared Memory . . . . . . . . . . . . . . . . 13
4.2.1 Analisi dei dati . . . . . . . . . . . . . . . . . . . . . . 15

5 Conclusioni 15
3

1 Introduzione
1.1 Fast Fourier Transform
Sia x = (x0 , x1 , ..., xn−1 ), x ∈ Cn un vettore di numeri complessi. Si definisce
DF T (x), trasformata discreta di Fourier del vettore x, il vettore X =
(X0 , X2 , ..., Xn−1 ), X ∈ Cn tale che
N −1
2πi
X
Xk = xn e− n
kj
k = 0, ..., N − 1
j=0

Con FFT, trasformata di Fourier veloce (F F T , dall’inglese Fast Fourier


Transform), si intendono una famiglia di algoritmi in grado di calcolare la
trasformata discreta di Fourier in tempo O(nlogn) invece dell’usuale O(n2 ),
nel caso in cui si applicasse l’algoritmo in base alla definizione matematica
di DF T .
L’algoritmo F F T più diffuso per il calcolo della DF T è l’algoritmo di Cooley-
Tukey, di cui noi abbiamo realizzato una sua implementazione.

2 Descrizione dell’algortimo
L’algoritmo di Cooley Tukey con n = p· q e p, q > 1 prevede di:

1. Creare una matrice C ∈ pCq dal vettore x disponendo le componenti


in row-major;
2. ∀j 0 ≤ j ≤ q − 1 trasformare la colonna C j della matrice. Si hanno q
DF Tp ;
3. per 0 ≤ i ≤ p−1, 0 ≤ j ≤ q −1 moltiplicare Cij per ωnij (twiddle factor,
ωn = radice principale dell’unità complessa);
4. ∀i 0 ≤ i ≤ p − 1 trasformare la riga C i della matrice. Si hanno p DF Tq ;
5. estrarre il vettore trasformato X in column-major.

Nell’implementazione corrente dell’algoritmo di Cooley Tuckey sono stati


posti p = 2 e q = N/2.
4

In letteratura questa specifica configurazione viene chiamata radix-2 decimation-


in-time (DIT) e rende l’algoritmo ascendente. In questa specifica configura-
zione la DFT di dimensione n viene divisa in 2 DFT di dimensione n/2 cal-
colate poi ricorsivamente. Radix-2 DIT prima calcola la DFT dei componenti
pari del vettore di input x2m m ∈ {0, 1, ..., N/2 − 1} e quella delle compo-
nenti dispari x2m+1 m ∈ {0, 1, ..., N/2 − 1} e combina questi due risultati per
ottenere la DFT dell’intera sequenza di lunghezza n tramite operazioni but-
terfly tra gli elementi. Questa idea può poi essere applicata ricorsivamente.
Riportiamo in figura 1 il circuito che esemplifica quanto appena detto per la
DF T8 .

Figura 1: Circuito per il calcolo parallelo della DFT di ordine 8

Dalla figura si può facilmente notare che partendo da destra e spondandoci


verso sinistra vengono calcolate ricorsivamente le DFT del vettore fino ad
arrivare agli input che, per il corretto calcolo dei valori di output, devono
essere disposti in bit-reversal . I collegamenti diagonali presenti nella figura 1
sono chiamati operazioni Buttefly. L’operazione Butterfly è semplicemente la
5

DFT di ordine 2, che, dati in ingresso due input x0 e x1 , restituisce in uscita


la DF T2 y0 e y1 . In particolare y0 e y1 sono cosı̀ definiti:
(
y0 = x0 + x1 ω 2πik/n
y1 = x0 − x1 ω 2πik/n

2πi/n
dove ωn è la n − esima radice dell’unità complessa e k un intero che
dipende dalla trasformata eseguita (k è scritto nell’immagine per la trasfor-
mata di ordine 8). In letteratura questo fattore moltiplicativo viene chiamato
twiddle factor.

3 Implementazione Parallela
3.1 Descrizione dell’implementazione
L’implementazione dell’algoritmo è stata progettata nel seguente modo: con-
siderando una sequenza di ingresso di lunghezza N e con a disposizione P pro-
cessori, abbiamo pensato di dividere la sequenza in P sottovettori di lunghez-
za N/P .
Ogni processore i − esimo avrà a dispozione la sequenza i − esima. Ora,
dato che ogni processore ha a dispozione N/P valori, le prime log2 (N/P )
dimensioni possono essere eseguite in locale dato che ogni processore ha a
disposizione tutti i dati neccessari per le operazioni butterfly. Queste dimen-
sioni nel seguito del progetto verranno chiamate “interne”. Per le restanti
log2 (N )−log2 (N/P ) dimensioni (chiamate dimensioni “esterne”), invece, de-
vono essere instaurate delle comunicazioni fra processori in modo da poter
scambiarsi reciprocamente il valore neccessario alle operazioni butterfly. In
particolare il processore Pi dovrà comunicare con il processore Clog2 (N/P ) Pi ,
Clog2 (N/P )+1 Pi ,...,Clog2 (N )−1 Pi 1 . In generale nella j − esima fase si avrà lo
scambio dei valori fra i processori Pj ←→ Cj Pj per log2 (N/P ) ≤ j ≤
log2 (N ) − 1.
1
Notazione vista a lezione
3.2 Funzionamento e scelte implementative 6

3.2 Funzionamento e scelte implementative


Viene di seguito illustrato come è stato progettata l’implementazione dell’al-
goritmo di Cooley Tukey.
Si ipotizza che:

ˆ N = 2n n∈N N = |x|;
ˆ P = 2p p∈N P = # processori;
ˆ P < N => N/P ≥ 2. Questo vincolo garantisce che ci siano sempre
almeno due valori disponibili ad ogni processore, e che quindi non ci
siano processori che non eseguono nulla.

Innanzitutto, è stato creato un generatore di numeri complessi in modo da


poter testare l’effettivo funzionamento del codice. All’interno della cartella
“Util ” è contenuto il file “complexGenerator.c” in cui è stato implementato
tale generatore di complessi. Sono state previste tre modalità di creazione
dei complessi:

1. generazione 2n numeri complessi random nell’intervallo [−10000, 10000];


2. generazione di 2n numeri complessi tutti uguali ad 1 + i0;
3. generazione di 2n numeri complessi con il primo uguale ad 1 + i0 ed i
restanti 2n − 1 uguali a 0.

La scelta delle tre modalità di generazione di complessi è dovuta al fatto


che la prima modalità viene usata per simulare la trasformata di Fourier
di un vettore arbitrario di complessi, mentre le restanti due modalità sono
modalità di test che, sfruttando particolari proprietà della trasformata di
Fourier, ci hanno permesso di verificare l’effettivo funzionamento del codice,
in particolare:

ˆ x = (1 + i0, 1 + i0, ..., 1 + i0) con N = |X| allora DF T (x) = X =


(N + i0, 0 + i0, ..., 0 + i0)
ˆ x = (1 + i0, 0 + i0, ..., 0 + i0) con N = |X| allora DF T (x) = X =
(1 + i0, 1 + i0, ..., 1 + i0)

Per chiamare da terminale il generatore di complessi:

Util/ComplexGenerator n m
3.2 Funzionamento e scelte implementative 7

Per la creazione di 2n complessi in modalità m con m = 1, 2, 3.


L’esecuzione del programma di generazione di complessi genera due file di
output contenenti entrambi lo stesso output. Viene creato innanazitutto il file
“complexX ” (contenente i numeri complessi uno per riga) e successivamente
il file “complexX.bin”, X = 2n . Il primo file è un file di lettura che permette
di controllare facilmente i complessi creati. Il secondo è un file binario, cre-
ato per essere passato all’algoritmo di F F T in modo che la lettura
dei valori da file sia più efficiente.

L’implementazione parallela dell’algoritmo di Cooley Tukey è stata proget-


tata in questo modo:

1. Il processore P0 legge il file di complessi in input e lo dispone in “bit-


reversal ”;
2. Il processore P0 distribuisce ai P processori gli N/P valori di loro re-
sponsabilità, tramite una chiamata alla primitiva bloccante “MPI Scat-
ter”;
3. Ogni processore Pi calcola localmente le log2 (N/P ) dimensioni interne;
4. Ogni processore Pi calcola le log2 (N ) − log2 (N/P ) dimensioni esterne;
per la comunicazione tra i processori è stata utilizzata la primitiva bloc-
cante “MPI Sendrecv” dato che ogni processore deve ricevere/spedire
dati allo stesso collega;
5. Il processore P0 raccoglie dai P processori gli N/P valori trasformati,
tramite una chimata alla primitiva bloccante “MPI Gather”;
6. Il procesore P0 scrive l’output nel file “FFTOutX”, X = 2n ;

Per il calcolo delle dimensioni interne (nel punto 3) è stato utilizzato l’al-
gortirmo iterativo “in-place” di calcolo della F F T 2 tramite lo pseudocodice
riportato in Algorithm 1.
Come specificato nelle slide presentate a lezione, si sono utizzate le due
modalità di esecuzione. Per la modalità diretta (per debugging) bisogna
inserire il comando:

./FFT complexX.bin -procs p -labelio yes


2
Presente nel libro Leiserson, Cormen, Rivest, Stein “Introduction toi Algorithms” 3rd
Edition MIT press Pag.917
8

Algorithm 1 Iterative-FFT(x)
BIT-REVERSE(x,x̃)
n = x.lenght
for s = 1 to log(n) do
m = ss
ωm = e2πi/m
for k = 0 to n − 1 by m do
ω=1
for j = 0 to m/2 − 1 do
t = ωx̃[k + j + m/2]
u = x̃[k + j]
x̃[k + j] = u + t
x̃[k + j + m/2] = u − t
ω = ωωn
end for
end for
end for
return x̃

Per l’utilizzo del IBM LoadLeveler (per determinare i tempi di esecuzione)


invece, come spiegato in classe, è stato creato il file “.job” e la sottomissione
di tale file avviene con il comando: llsubmit FFT.job

4 Analisi delle prestazioni


Si è deciso di testare l’algortimo implementato abilitando/disabilitando la
memoria condivisa. In particolare tutti i test sono prima stati fatti mettendo
nel file “.job” la direttiva

#@ environment = MP_SHARED_MEMORY=yes; MP_LABELIO=yes

e successivamente

#@ environment = MP_SHARED_MEMORY=no; MP_LABELIO=yes

I test sono stati eseguiti in questo modo: siamo partiti da un numero di


complessi in ingresso N = 215 e siamo arrivati a fornire in input sequenze
9

di N = 222 , pari al massimo gestibile dalla memoria dei processori. Per ogni
file di complessi in input sono stati fatti test con 1, 2, 4, 8, 16 processori. Per
ogni coppia “dimensione file di ingresso - numero processori” sono state fatte
tre misurazioni e alla fine si è preso la media dei tre tempi. Per la stima dei
tempi si sono usate le librerie “HPM Toolkit”. Si sono calcolati i tempi per
rispettivamente:
ˆ BitReversal: Tempo impiegato dal processore P0 per l’ordinamento
in bit-reversal dell’input;
ˆ Scattering: Tempo di comunicazione impiegato per trasferire a tutti i
processori il sottovettore (di lunghezza N/P ) del vettore di input;
ˆ InnerFFT: Tempo impiegato dal processore Pi per il calcolo delle di-
mensioni interne nel suo sottovettore. Sia Ti il tempo impiegato dall’i−
esimo processore per il calcolo delle dimensioni interne, TinnerF F T =
max{Ti , 0 ≤ i ≤ P };
ˆ ExternFFT: Tempo impiegato dal processore Pi per il calcolo delle
Operazioni Butterfly;
ˆ Sendrcv: Tempo di comunicazione impiegato dai processori i e j per
scambiarsi i complessi di cui hanno bisogno;
ˆ Gathering: Tempo di comunicazione impiegato dal processore P0 per
la raccolta dei sottovettori di ogni processore.

Il tempo totale delle comunicazioni è dato dalla somma dei tempi di Scatter-
ing, SendRcv e Gathering. Si precisa oltretutto che tutte le prove sono state
fatte impostando la comunicazione attraverso lo switch ad alte prestazioni e
quindi impostando nel file “.job” #@ network.mpi = switch,shared,US.
Nel caso, invece, venisse impostata la rete ethernet i tempi erano esponenzial-
mente più alti e in certi casi le comunicazioni si bloccavano, di conseguenza
non siamo riusciti a fare test in questa modalità. Tutti i test sono stati
fatti impostando nel file “.job” la direttiva #@ blocking = unlimited in
modo da allocare per ciascun nodo il massimo numero di processori possibili
e ridurre quindi al minimo il tempo di comunicazione dovuto allo scambio
di messaggi tra processori in nodi differenti. Si sono oltrettutto fatte delle
prove aggiungendo: in compilazione le ottimizzazioni standard (-O3) fornite
dal compilatore gnu gcc, nel file “.job” il buffering intermedio usando la di-
rettiva MP EAGER LIMIT con diverse capacità, senza però ottenere nessun
4.1 Prestazioni con Shared Memory 10

risultato in termini di prestazioni. Vengono di seguito riportati i dati prin-


cipali ottenuti con le due modalità e i grafici dello Speed-up e tempo delle
comunicazioni.

4.1 Prestazioni con Shared Memory


HH
HH #P
1 2 4 8 16
N H
HH
H
Ttot 0.3378 0.1956 0.1137 0.0686 0.0437
%Comm - 2.12 5.55 10.99 17.91
215
SpeedUp 1 1.73 3 4.95 7.73
Ttot 0.7215 0.4128 0.2370 0.1464 0.0883
%Comm - 1.78 4.66 13.27 16.56
216
SpeedUp 1 1.75 3 4.95 8.16
Ttot 1.5409 0.8750 0.4952 0.2927 0.1799
%Comm - 1.60 3.44 8.40 12.88
217
SpeedUp 1 1.76 3.11 5.26 8.57
Ttot 3.2725 1.8440 1.0342 0.6108 0.3810
%Comm - 1.78 4.66 13.27 16.56
218
SpeedUp 1 1.78 3.17 5.36 8.59
Ttot 6.9950 3.9175 1.1911 1.2845 0.8535
%Comm - 1.57 2.59 6.30 17.59
219
SpeedUp 1 1.79 3.20 5.45 8.20
Ttot 14.8583 7.3308 4.6509 2.7252 1.6813
%Comm - 1.56 2.76 5.73 8.87
220
SpeedUp 1 1.78 3.20 5.45 8.84
Ttot 31.3161 17.4790 9.8216 5.7383 3.5689
%Comm - 1.45 2.63 5.39 8.20
221
SpeedUp 1 1.79 3.20 5.46 8.78
Ttot 65.6728 36.5813 20.4707 12.1093 7.2375
%Comm - 1.40 2.40 5.35 8.12
222
SpeedUp 1 1.80 3.21 5.45 9.04
4.1 Prestazioni con Shared Memory 11

Figura 2: Grafico dello Speed-up con Shared Memory abilitata

Figura 3: Grafico del tempo delle comunicazioni con Shared Memory abilitata
4.1 Prestazioni con Shared Memory 12

4.1.1 Analisi dei dati

Analizzando le tabelle e i grafici si possono fare alcune considerazioni inter-


essanti. La prima considerazione che si può fare è che l’algoritmo scala bene
all’aumentare del numero di processori utilizzati. Dai grafici precedenti, in
particolare dalla (fig.2) è possibile vedere che lo speed-up (circa) raddoppia
al raddoppiare del numero di processori.
Il tempo per le comunicazioni aumenta con il numero di processori utilizzati
e nel caso peggiore (n = 15, #P = 16) è al massimo del 18%, valore molto
ragionevole considerando il fatto che tale tempo è ottenuto nel caso in cui
tutti i 16 processori sono abilitati.
Si è visto un miglioramento notevole delle performance anche nella lettura da
file binario: all’inizio l’algoritmo era stato svilupparo in modo da leggere da
file “.txt”, veniva effettuato quindi un semplice parsing dei numeri complessi.
Abbiamo in seguito migliorato questo punto salvando i dati in file binario.
Ciò ci ha permesso sia di ridurre notevolmente il tempo per la lettura del file
di input sia di semplificare molto il nostro sorgente, dato che non c’era più
nessun bisogno di fare il parsing del file, ma con l’uso del file binario si è in
grado di inizializzare le variabili con il contenuto stesso del file, tutto tramite
la sola direttiva fread.
Per risparmiare tempo e spazio per la computazione, oltretutto, si è pensato
di calcolare i twiddle factor “al volo”. Si era partiti all’inizio con l’idea di
pre-calcolare i twiddle factor e di mantenerli in un vettore. Dato che per
una DFT di ordine N sono neccessari N/2 twiddle factor si era pensato di
pre-calcolare il vettore ed utilizzare il twiddle factor nel momento oppurtuno.
Con alcune cosiderazioni, sia per il costo dello spazio (O(N )) ma sopratutto
per il fatto che il calcolo “al volo” del twiddle factor avrebbe impiegato solo
spazio costante, siamo riusciti a calcolare analiticamente il preciso twiddle
factor che il processore Pi nel calcolo della dimensione j avrebbe dovuto us-
are.
Infine, abbiamo provato a fornire in input file di dimensioni N = 223 , 224 , in-
serendo nel file job: le direttive #@data_limit=1.0GB, #@stack_limit=0.5GB
modificando i valori di memoria oppurtunamente e nel nel makefile le opzioni
-bmaxdata:2000000000 -bmaxstack:2000000000, dato che senza tali opzioni
il programma andava in crash al momento della lettura per problemi di
4.2 Prestazioni senza Shared Memory 13

memoria. Con queste opzioni siamo riusciti a fare dei test parziali con N = 223
(in alcune occasioni si finiva lo stesso in crash) confermando il fatto che anche
con tali dimensioni di input lo speed-up cresceva di quasi il doppio.

4.2 Prestazioni senza Shared Memory


HH
HH #P
1 2 4 8 16
N H
HH
H
Ttot 0.3428 0.2034 0.1215 0.0736 0.0470
%Comm - 4.67 10360 16.95 23.45
215
SpeedUp 1 1.68 2.82 4.66 7.28
Ttot 0.7340 0.4282 0.2538 0.1434 0.0968
%Comm - 3.67 9.93 14.10 22.69
216
SpeedUp 1 1.71 2.89 4.91 7.58
Ttot 1.5591 0.9042 0.5495 0.3080 0.1450
%Comm - 3.52 11.83 12.51 15.86
217
SpeedUp 1 1.72 2.84 5.06 8
Ttot 3.3198 1.9041 1.1093 0.6419 0.3915
%Comm - 3.34 8.26 10.98 14.19
218
SpeedUp 1 1.74 2.99 5.17 8.48
Ttot 7.0756 4.0428 2.3430 1.3484 0.8364
%Comm - 3.62 7.98 10.27 13.15
219
SpeedUp 1 1.75 3.02 5.25 8.46
Ttot 15.0709 8.5210 4.9473 2.8646 1.7618
%Comm - 2.93 7.60 9.92 11.73
220
SpeedUp 1 1.77 3.05 5.26 8.55
Ttot 31.7622 17.9602 10.5828 5.9828 3.7294
%Comm - 3.03 6.23 9.18 11.31
221
SpeedUp 1 1.77 3 5.31 8.52
Ttot 66.6330 37.6782 21.6932 12.4454 7.7165
%Comm - 3.11 6.85 9.05 12.02
222
SpeedUp 1 1.77 3.07 5.35 8.64
4.2 Prestazioni senza Shared Memory 14

Figura 4: Grafico dello Speed-up con Shared Memory disabilitata

Figura 5: Grafico del tempo delle comunicazioni con Shared Memory


disabilitata
15

4.2.1 Analisi dei dati

Anche in questo caso, come nel caso precedente, ciò che si può concludere è
che l’algoritmo scala molto bene. Dal confronto dei grafici relativi a “shared
memory abilitata” e “shared memori disabilitata” è possibile vedere come il
tempo di comunicazione sia leggermente superiore nel caso di “shared memo-
ry disabilitata”. Questo si traduce in uno speed-up minore nel caso di “shared
memory disabilitata”. Infatti, il tempo di comunicazione maggiore va ad in-
cidere sul tempo totale di esecuzione dell’algoritmo che, al raddoppiare dei
processori, diminuirà meno rispetto al caso con “shared memoey abilitata”.

5 Conclusioni
In conclusione possiamo dire che il progetto è stato molto interessante e i dati
che abbiamo ottenuto sono apprezzabili. In effetti, non avendo mai fatto es-
perimenti di questo tipo, non ci aspettavamo dei risultati cosı̀ incorraggianti.
Il fatto che i tempi di comunicazione siano maggiori nel caso della memo-
ria condivisa abilitata è abbastanza ragionevole, ed è dovuto al fatto che i
processori in questa modalità sono obbligati a scambiarsi i valori tramite lo
switch. Nel caso di memoria condivisa abilitata, invece, la lettura da memoria
è sicuramente più veloce e i dati scambiati tramite switch sono in quantità
minore.
In media, il 90% del tempo viene speso totalmente per il calcolo. I risultati,
in termini di prestazione, sono molto evidenti dato che (per il caso N = 222
e P = 16) uno speed-up pari a 9 ci permette il calcolo della DF T222 in 9 sec.
invece che 1 minuto “perdendo” solo 0.6 sec. per le comunicazioni.

Potrebbero piacerti anche