You are on page 1of 8

Projekt

Programowanie równoległe i rozproszone

Zadanie BK20. Modelowanie transportu


ciepła metoda elementów skończonych

Część III. MPI

Wojciech Gałuszewski
(nr indeksu: 179956)
Cel projektu
Celem projektu jest wykorzystanie maszyn wielordzeniowych i wieloprocesorowych
oraz sieci maszyn, a także pewnych koprocesorów (np. na bazie kart graficznych) do
rozwiązywania złożonych zadań obliczeniowych.
Dzięki równoległemu, czyli jednoczesnemu, wykonywaniu obliczeń przez wiele
jednostek, można uzyskać znaczne przyśpieszenie w stosunku do obliczeń wykonywanych na
maszynie sekwencyjnej.

Zakres projektu (Część 3. MPI)


Zakresem drugiej części projektu jest zastosowanie i implementacja interfejsu
transmisji wiadomości (MPI), który jest standardem przesyłania komunikatów pomiędzy
procesami programów równoległych. Następnie przeprowadzenie jego testów
wydajnościowych i przedstawienie odpowiednich wniosków.

Implementacja
Podczas implementacji MPI głównym problemem z jakim trzeba się zmierzyć jest brak
współdzielonej pamięci. W moim przypadku aby „ciepło rozchodziło się” musi następować
ścisła komunikacja między poszczególnymi procesami. Dla ułatwienia postanowiłem dzielić
wierszami zadania dla poszczególnych procesów.

Przyjęty przeze mnie model komunikacji

Kod przedstawiający podział zadań, wraz z inicjalizacją MPI przedstawia poniższy listing:
MPI_Init(NULL, NULL);
MPI_Comm_size(MPI_COMM_WORLD, &p);
MPI_Comm_rank(MPI_COMM_WORLD, &id);

int Y_segment_min; // Pomocnicze zmienne ustawiające zakres


int Y_segment_max; // podzialu pracy na zadania.

Y_segment_min = (Y / p) * id;
Y_segment_max = (Y / p) + Y_segment_min;

if (id == 0)
{
Y_segment_min++; // Zakładam, że brzegi są zimne
printf("\nIlosc procesow: %i:\n", p);
}
printf("Robotnik: %i Przydzielony zakres OD %i DO %i \n", id, Y_segment_min,
Y_segment_max);

Aby poprawić czytelność programu algorytm zamiany tablic, dla czasów t, t+1
umieściłem w funkcji update().
Kod komunikacji między procesami przedstawia listing:

if (id == 0)
{
update(T1, T2, X, Y_segment_min, Y_segment_max + 1);
MPI_Send(&T2[(Y_segment_max - 1)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD);
MPI_Recv(&T2[(Y_segment_max)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);

update(T2, T1, X, Y_segment_min, Y_segment_max + 1);


MPI_Send(&T1[(Y_segment_max - 1)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD);
MPI_Recv(&T1[(Y_segment_max)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
}

if ((id < (p - 1)) && (id > 0))


{
update(T1, T2, X, Y_segment_min, Y_segment_max + 1);
MPI_Send(&T2[(Y_segment_max - 1)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD);
MPI_Recv(&T2[(Y_segment_max)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Recv(&T2[(Y_segment_min - 1)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
MPI_Send(&T2[(Y_segment_min)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD);

update(T2, T1, X, Y_segment_min, Y_segment_max + 1);


MPI_Send(&T1[(Y_segment_max - 1)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD);
MPI_Recv(&T1[(Y_segment_max)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Recv(&T1[(Y_segment_min - 1)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
MPI_Send(&T1[(Y_segment_min)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD);
}

if (id == (p - 1))
{
update(T1, T2, X, Y_segment_min, Y_segment_max);
MPI_Recv(&T2[(Y_segment_min - 1)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
MPI_Send(&T2[(Y_segment_min)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD);

update(T2, T1, X, Y_segment_min, Y_segment_max);


MPI_Recv(&T1[(Y_segment_min - 1)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
MPI_Send(&T1[(Y_segment_min)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD);
}

Generalnie można w nim wydzielić 3 rodzaje procesów. Procesy brzegowe („górny”,


”dolny”) oraz procesy środkowe. Zasada polega na tym aby proces po dokonaniu obliczeń
przesłał swój ostatni rząd procesowi sąsiedniemu, a ten po obliczeniach zwrócił ponownie
dane wykorzystywane do kolejnych obliczeń po zamianie tablic czasów t,t+1.
Działanie programu
Działanie programu sprawdziłem w wieloraki sposób. Najpierw uruchamiając
program na jednej maszynie, a następnie uruchamiając program na wirtualnym klastrze
3x1CPU oraz 3x2CPU.

Działanie programu na wirtualnym klastrze 3 maszyn

Dodatkowo sprawdziłem działanie hybrydy OpenMP + MPI dodając do funkcji


update „proste” zrównoleglenie z poprzedniej części projektu :
void update(float * T1, float * T2, int X, int Yp, int Yk)
{
#pragma omp parallel for
for (int j = Yp; j < Yk - 1; j++)
{
for (int i = 1; i < X - 1; i++)
{
float T_SR = T1[i + X * j]; //Element srodkowy
float T_L = T1[(i + 1) + X * j]; //Element Lewy
float T_P = T1[(i - 1) + X * j]; //Element Prawy
float T_G = T1[i + X * (j + 1)]; //Element Gorny
float T_D = T1[i + X * (j - 1)]; //Element Dolny
T2[i + X * j] = 0.125*(4 * T_SR + T_L + T_P + T_G + T_D);
}
}
}
Podsumowanie i wnioski
Pomimo swojej zawiłości implementacja MPI zaskoczyła mnie bardzo pozytywnie
swoimi wynikami. Przy dużych tablicach danych oraz małej ilości iteracji różnice czasowe
między MPI, a OpenMP były naprawdę bardzo znikome.
Zmusiło mnie to do przetestowania działania programu dla specyficznych danych.
Bardzo dużej ilości iteracji co przy moim założeniu komunikacji generuje bardzo dużą ilość
komunikatów. Dopiero wtedy udało mi się doprowadzić do sytuacji, gdzie dalsze zwiększanie
ilości procesów tylko spowalniało działanie programu.
60
OpenMP

50
MPI

Klaster 3x 2CPU
40

Klaster 3x 2CPU Hybryda


Czas [s]

(OpenMP + MPI)
30

20

10

0
1 3 5 7 9 11
Ilość procesów

Działanie programu w różnych wariantach dla bardzo dużych ilości iteracji (1000000) oraz małej tablicy (100x100)

Sprawdzenie działania MPI na „pół” rzeczywistym klastrze bazującym na rzeczywistej


komunikacji Ethernetowej (100Mbs) pokazuje jak kolosalna w skutkach jest niska
przepustowość i relatywnie duże opóźnienia sieci. W takim też wypadku bardzo dobrze
sprawdziło się rozwiązanie hybrydowe. W normalnych warunkach, gdzie każda maszyna
klastra może być wyposażona nawet w kilkadziesiąt rdzeni taki model przyniósłby znaczące
rezultaty.
Tak jak w poprzednich częściach dalszego przyśpieszenia, można oczekiwać jedynie
po zmianie samego algorytmu. Choć w początkowej fazie chciałem zastosować siatkowy
model komunikacji, zamiast podziału wierszowego. To jednak przy tak małej liczbie
rdzeni/wątków rezultat mógłby być odwrotny, gdyż dla tablicy 1000x1000 bok siatki dla 12
wątków wynosiłby ~288 elementów, co przy komunikacji 4 stronnej dałoby 1152 elementów
wspólnych, zamiast 1000.
Podsumowanie projektu
Choć zdaję sobie sprawę, że wachlarz możliwości w zakresie projektu jest jeszcze
olbrzymi. Uważam, że projekt spełnił postawiony przed nim cel jakim było zapoznanie się z
tematem zrównoleglania i rozpraszania aplikacji i programów.
Załącznik 1
Kompletny listing programu. BK20. Modelowanie transportu ciepła metoda
elementów skończonych. (wersja MPI)
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <mpi.h>

void update(float * T1, float * T2, int X, int Yp, int Yk)
{
for (int j = Yp; j < Yk - 1; j++)
{
for (int i = 1; i < X - 1; i++)
{
float T_SR = T1[i + X * j]; //Element srodkowy
float T_L = T1[(i + 1) + X * j]; //Element Lewy
float T_P = T1[(i - 1) + X * j]; //Element Prawy
float T_G = T1[i + X * (j + 1)]; //Element Gorny
float T_D = T1[i + X * (j - 1)]; //Element Dolny
T2[i + X * j] = 0.125*(4 * T_SR + T_L + T_P + T_G + T_D);
}
}
}

int main()
{
//********* WARTOSCI PODSTAWOWE *********
int X = 100; // Wielkosc tablicy elementow skonczonych X-Y
int Y = X; //Zakładam, ze tablica będzie kwadratowa
int ITER = 1000000; //Ilosc iteracji

float *T1; //Wskaznik na dane typu float


float *T2; //Wskaznik na dane typu float
T1 = (float*)malloc(sizeof(float)*X*Y); //Alokacja pamieci
T2 = (float*)malloc(sizeof(float)*X*Y); //Alokacja pamieci

//Zerowanie tablicy T1
for (int i = 0; i < X; i++) {
for (int j = 0; j < Y; j++) {
int index = i + j * X;
T1[index] = 0.0;
T2[index] = 0.0;
}
}

//Na potrzeby projektu zakladam, ze cieplo bedzie transferowane z jednego boku, a brzegi będą zimne
for (int j = 0; j < Y; j++) {
int index = j * X;
T1[index] = 99.0; //Ustawiam temperature na 99
T2[index] = 99.0; //Ustawiam temperature na 99
}

int iter = 0; //Ustawiam licznik iteracji na 0

long startclock; //Zmienna wykorzystywane do mierzenia czasu


startclock = (double)clock(); //Przypisanie czasu poczatkowego

int id;
int p;

MPI_Init(NULL, NULL);
MPI_Comm_size(MPI_COMM_WORLD, &p);
MPI_Comm_rank(MPI_COMM_WORLD, &id);

int Y_segment_min; // Pomocnicze zmienne ustawiające zakres


int Y_segment_max; // podzialu pracy na zadania.

Y_segment_min = (Y / p) * id;
Y_segment_max = (Y / p) + Y_segment_min;

if (id == 0)
{
Y_segment_min++;
printf("\nIlosc procesow: %i:\n", p);
}

printf("Robotnik: %i Przydzielony zakres OD %i DO %i \n", id, Y_segment_min, Y_segment_max);

while (iter < ITER) //Glowna petla wykonujaca zadana ilosc przebiegow
{
if (p == 1)
{
update(T1, T2, X, Y_segment_min, Y_segment_max);
update(T2, T1, X, Y_segment_min, Y_segment_max);
}

if (p >= 2)
{
if (id == 0)
{
update(T1, T2, X, Y_segment_min, Y_segment_max + 1);
MPI_Send(&T2[(Y_segment_max - 1)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD);
MPI_Recv(&T2[(Y_segment_max)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);

update(T2, T1, X, Y_segment_min, Y_segment_max + 1);


MPI_Send(&T1[(Y_segment_max - 1)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD);
MPI_Recv(&T1[(Y_segment_max)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
}

if ((id < (p-1))&&(id>0))


{
update(T1, T2, X, Y_segment_min, Y_segment_max + 1);
MPI_Send(&T2[(Y_segment_max - 1)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD);
MPI_Recv(&T2[(Y_segment_max)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Recv(&T2[(Y_segment_min - 1)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Send(&T2[(Y_segment_min)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD);

update(T2, T1, X, Y_segment_min, Y_segment_max + 1);


MPI_Send(&T1[(Y_segment_max - 1)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD);
MPI_Recv(&T1[(Y_segment_max)*X], X, MPI_FLOAT, id + 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Recv(&T1[(Y_segment_min - 1)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Send(&T1[(Y_segment_min)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD);
}

if (id == (p-1))
{
update(T1, T2, X, Y_segment_min, Y_segment_max);
MPI_Recv(&T2[(Y_segment_min - 1)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Send(&T2[(Y_segment_min)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD);

update(T2, T1, X, Y_segment_min, Y_segment_max);


MPI_Recv(&T1[(Y_segment_min - 1)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Send(&T1[(Y_segment_min)*X], X, MPI_FLOAT, id - 1, 0, MPI_COMM_WORLD);
}

MPI_Barrier(MPI_COMM_WORLD); // Zastosowanie tej bariery nieznacznie przyspiesza dzilanie


(tylko w przypadku klastra)
}

iter += 2; //Poniewaz wykorzystane sa zamiennie 2 tablice inkrementuje co 2


}

//Zebranie wyników ze wszystkich procesow


if ((id != 0) && (p > 1))
{
MPI_Send(&T1[(Y_segment_min)*X], (Y_segment_max-Y_segment_min)*X, MPI_FLOAT, 0, 0, MPI_COMM_WORLD);
printf("Wyslano z id: %i , Ofset wiersza: %i ilosc danych: %i \n", id, Y_segment_min, (Y_segment_max –
Y_segment_min)*X);
}

if ((id == 0)&&(p>1))
for (int i = 1; i < p; i++)
{
MPI_Recv(&T1[(Y_segment_max*i)*X], Y_segment_max*X, MPI_FLOAT, i, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
}

MPI_Finalize();

if (id == 0)
{
startclock = (double)clock() - startclock; //Obliczenie czasu trwania programu

printf("\nPrezentacja wyniku co: %i komorka tabeli\n\n", (X / 20));

//Pokazanie wynikow na ekranie, wyświetlane jest tylko 20x20 komorek (ograniczonych do typu int)
for (int j = 0; j < 20; j++) //Zmiana tylko wnętrza tablicy "Zimne brzegi"
{
for (int i = 0; i < 20; i++)
{
int index = i * (X / 20) + j * (X*(X / 20));
if ((int)T1[index] < 10) printf(" ");
printf(" %i", (int)T1[index]);
}
printf("\n");
}

printf("\nIlosc iteracji: %i Wielkosc tablicy X=Y: %i", ITER, X);


printf("\nCzas wykonania: %f \n", (float)startclock / CLOCKS_PER_SEC);
}

free(T1);
free(T2);

return 0;
}

You might also like