Mnożenie macierzy
-- Sebastian Pawlak, 2002.
Projekt ilustruje problematykę równoległego mnożenia macierzy.
Program operuje na pamięci przydzielanej dynamicznie.
Jeden z procesów (Master) wczytuje obie macierze do pamięci,
a następnie rozsyła odpowiednie fragmenty do pozostałych procesów.
Wyniki obliczeń są przesyłane do jednego
procesu, który zapisuje całościowy wynik do pliku.
Format plików z macierzami
Macierze będą przechowywane w dwóch osobnych plikach.
Ponieważ mnożenie będzie wykonywane dla bardzo
dużych macierzy toteż najrozsądniejszym formatem
plików będzie:
int liczbaWierszy; int liczbaKolumn; double macierz [liczbaWierszy * liczbaKolumn]; przy czym: sizeof(int) = 4, sizeof(double) = 8
Przykładowy program zapisujący macierz do pliku
Kod źródłowy pliku "tworzmacierz.c":
/*
* Program tworzy plik z macierza o zadanych
* wymiarach.
* Program pozwala tworzyc bardzo duze pliki.
*/
#include <stdio.h>
#include <fcntl.h>
#define PERMISSION 0666
#define PRZYROST 0.45
#define ROZMIAR_BUF 1000000
void err(char *komunikat)
{
printf("%s\n", komunikat);
exit(1);
}
int main(int argc, char **argv)
{
int plik;
int liczbaWierszy, liczbaKolumn;
int i, j, k;
double *buf;
if(argc != 4)
err("./tworzmacierz m n plikWynikowy\n"\
"gdzie: m - liczba wierszy macierzy,\n"\
" n - liczba kolumn macierzy.");
if((plik = creat(argv[3], PERMISSION)) == -1)
err("nie moge utworzyc pliku");
if(!(buf = (double *)malloc(ROZMIAR_BUF *
sizeof(double))))
err("nie moge przydzielic pamieci");
j = (liczbaWierszy = atoi(argv[1])) *
(liczbaKolumn = atoi(argv[2]));
write(plik, &liczbaWierszy, sizeof(int));
write(plik, &liczbaKolumn, sizeof(int));
k = 0;
while(k < j) {
for(i = 0; k + ROZMIAR_BUF > j ? i < j - k :
i < ROZMIAR_BUF; i++)
buf[i] = (double)(i + k) * PRZYROST;
write(plik, buf, i * sizeof(double));
k += i;
}
free(buf);
close(plik);
return 0;
}
Przykładowy program wczytujący macierz z pliku do pamięci przydzielanej dynamicznie
Kod źródłowy pliku "wczytajmacierz.c":
/*
* Program wczytuje macierz z pliku do
* pamieci.
*/
#include <stdio.h>
#include <fcntl.h>
void err(char *komunikat)
{
printf("%s\n", komunikat);
exit(1);
}
int main(int argc, char **argv)
{
int plik;
int liczbaWierszy, liczbaKolumn;
int i, j;
double *buf;
if(argc != 2)
err("./wczytajmacierz plikWejsciowy");
if((plik = open(argv[1], O_RDONLY, 0)) == -1)
err("nie moge otworzyc pliku z macierza");
read(plik, &liczbaWierszy, sizeof(int));
read(plik, &liczbaKolumn, sizeof(int));
j = liczbaWierszy * liczbaKolumn;
if(!(buf = (double *)malloc(j * sizeof(double))))
err("nie moge przydzielic pamieci");
read(plik, buf, j * sizeof(double));
close(plik);
/* wypisz macierz na ekran */
for(i = 0; i < j; i++) {
printf("%6g", buf[i]);
!((i + 1) % liczbaKolumn) ?
printf("\n") : printf(" ");
}
free(buf);
return 0;
}
Nie stanowi problemu wczytanie macierzy do tablicy dwuwymiarowej
(tablicy tablic) lub
tablicy wskaźników.
Jednak C nie gwarantuje spójności przydzielonego obszaru pamięci
dla poszczególnych wierszy, dlatego wczytywanie należy wykonać wiersz
po wierszu.
Metoda rozwiązania - opis metody sekwencyjnej
Program realizujący przedstawiony algorytm Matrix-Multiply
przedstawia się następująco:
Kod źródłowy pliku "sekwencyjna.c":
/*
* Sekwencyjne mnozenie macierzy.
*/
#include <stdio.h>
#include <fcntl.h>
void err(char *komunikat)
{
printf("%s\n", komunikat);
exit(1);
}
void wypiszMacierz(double *buf, int liczbaWierszy,
int liczbaKolumn)
{
int i, j = liczbaWierszy * liczbaKolumn;
for(i = 0; i < j; i++) {
printf("%6g", buf[i]);
!((i + 1) % liczbaKolumn) ?
printf("\n") : printf(" ");
}
}
int main(int argc, char **argv)
{
int plik;
int liczbaWierszyA, liczbaKolumnA,
liczbaWierszyB, liczbaKolumnB;
int i, j, k;
double *bufA, *bufB, *bufC;
if(argc != 3)
err("./sekwencyjna macierzA macierzB");
/* wczytywanie macierzy A */
if((plik = open(argv[1], O_RDONLY, 0)) == -1)
err("nie moge otworzyc pliku z macierza A");
read(plik, &liczbaWierszyA, sizeof(int));
read(plik, &liczbaKolumnA, sizeof(int));
j = liczbaWierszyA * liczbaKolumnA;
if(!(bufA = (double *)malloc(j * sizeof(double))))
err("nie moge przydzielic pamieci na bufA");
read(plik, bufA, j * sizeof(double));
close(plik);
printf("Macierz A\n");
wypiszMacierz(bufA, liczbaWierszyA,
liczbaKolumnA);
/* wczytywanie macierzy B */
if((plik = open(argv[2], O_RDONLY, 0)) == -1)
err("nie moge otworzyc pliku z macierza B");
read(plik, &liczbaWierszyB, sizeof(int));
read(plik, &liczbaKolumnB, sizeof(int));
j = liczbaWierszyB * liczbaKolumnB;
if(!(bufB = (double *)malloc(j * sizeof(double))))
err("nie moge przydzielic pamieci na bufB");
read(plik, bufB, j * sizeof(double));
close(plik);
printf("\nMacierz B\n");
wypiszMacierz(bufB, liczbaWierszyB,
liczbaKolumnB);
/* przydzielenie pamieci na macierz wynikowa C */
if(!(bufC = (double *)malloc(liczbaWierszyA *
liczbaKolumnB * sizeof(double))))
err("nie moge przydzielic pamieci na bufC");
/* mnozenie macierzy */
for(i = 0; i < liczbaWierszyA; i++)
for(j = 0; j < liczbaKolumnB; j++) {
bufC[j + i * liczbaKolumnB] = 0;
for(k = 0; k < liczbaKolumnA; k++)
bufC[j + i * liczbaKolumnB] +=
bufA[k + i * liczbaKolumnA] *
bufB[j + k * liczbaKolumnB];
}
printf("\nMacierz C\n");
wypiszMacierz(bufC, liczbaWierszyA,
liczbaKolumnB);
free(bufC);
free(bufB);
free(bufA);
return 0;
}
Przedstawiony program można nieco zoptymalizować.
Poniżej przedstawiłem najistotniejsze
fragmenty poprawionej wersji programu.
Kod źródłowy pliku "sekwencyjna2.c":
/*
* Sekwencyjne mnozenie macierzy.
* Wersja zoptymalizowana.
*/
...
int i_liczbaKolumnB = 0, i_liczbaKolumnA = 0,
k_liczbaKolumnB = 0;
...
/* mnozenie macierzy - zoptymalizowane */
for(i = 0; i < liczbaWierszyA; i++) {
for(j = 0; j < liczbaKolumnB; j++) {
bufC[i_liczbaKolumnB] = 0;
k_liczbaKolumnB = j;
for(k = 0; k < liczbaKolumnA; k++) {
bufC[i_liczbaKolumnB] +=
bufA[k + i_liczbaKolumnA] *
bufB[k_liczbaKolumnB];
k_liczbaKolumnB += liczbaKolumnB;
}
i_liczbaKolumnB++;
}
i_liczbaKolumnA += liczbaKolumnA;
}
...
Wersja zoptymalizowana jest szybsza o średnio 23%,
co ilustruje poniższa tablica.
rozmiar | czas | czas | przyśpie- macierzy | normalny | optymalny | szenie ------------+----------+-----------+---------- 500 x 500 | 25,48 | 20,59 | 23,74% ------------+----------+-----------+---------- 1000 x 1000 | 228,77 | 186,92 | 22,3% ------------+----------+-----------+---------- Porównanie czasów działania przedstawionych programów sekwencyjnych
Metoda rozwiązania - opis metody równoległej
Master wczytuje obie macierze do pamięci przydzielanej dynamicznie.
Następnie druga macierz rozsyłana jest w całości do pozostałych procesów.
Macierz pierwsza dzielona jest wierszami, a następnie rozsyłana.
W poszczególnych procesach następuje wymnażanie fragmentów macierzy.
Wyniki obliczeń przesyłane są do Mastera, który scala je
oraz zapisuje wynikową macierz do pliku.
Optymalna wersja programu rozsyła w całości macierz mniejszą - dzięki
temu zaoszczędzamy pamięć oraz czas potrzebny na przesłanie macierzy.
Wtedy większą macierz dzieli się kolumnami (jeśli w całości przesłaliśmy
macierz pierwszą) albo wierszami (jeśli w całości przesłaliśmy
macierz drugą).
Kod źródłowy pliku "rownolegla.c":
/*
* Zrownoleglone mnozenie macierzy.
* Master wczytuje 2 macierze do swojej pamieci.
* Nastepnie rozsyla jedna macierz do Slave`ow.
* Macierz druga dzielona jest na fragmenty i
* rozsylana do Slave`ow.
* Wyniki obliczen wracaja do Mastera.
* Pamiec przydzielana jest dynamicznie - wektor.
*
* Sebastian Pawlak, s1222.
*/
#include <stdio.h>
#include <fcntl.h>
#include "mpi.h"
#define MAX_LICZBA_PROCESOW 16
#define PERMISSION 0666
void err(char *komunikat)
{
printf("%s\n", komunikat);
MPI_Abort(MPI_COMM_WORLD, -1);
}
int main(int argc, char **argv)
{
int plik;
int liczbaWierszyA, liczbaKolumnA,
liczbaWierszyB, liczbaKolumnB;
int i, j, k, n;
double *bufA, *bufB, *bufC;
int i_liczbaKolumnB = 0, i_liczbaKolumnA = 0,
k_liczbaKolumnB = 0;
int rank, size;
double czasStart;
struct przedzial {
int nMin, nMax;
} przedzialyTab[MAX_LICZBA_PROCESOW] = { 0 };
int tag = 55;
MPI_Status status;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if(argc != 4)
err("./rownolegla macierzA macierzB wynik");
if(rank == 0) {
/* wczytywanie macierzy A */
if((plik = open(argv[1], O_RDONLY, 0)) == -1)
err("nie moge otworzyc pliku A");
read(plik, &liczbaWierszyA, sizeof(int));
read(plik, &liczbaKolumnA, sizeof(int));
j = liczbaWierszyA * liczbaKolumnA;
if(!(bufA = (double *)malloc(
j * sizeof(double))))
err("nie moge przydzielic pamieci na A");
read(plik, bufA, j * sizeof(double));
close(plik);
/* wczytywanie macierzy B */
if((plik = open(argv[2], O_RDONLY, 0)) == -1)
err("nie moge otworzyc pliku z B");
read(plik, &liczbaWierszyB, sizeof(int));
read(plik, &liczbaKolumnB, sizeof(int));
k = liczbaWierszyB * liczbaKolumnB;
if(!(bufB = (double *)malloc(
k * sizeof(double))))
err("nie moge przydzielic pamieci na B");
read(plik, bufB, k * sizeof(double));
close(plik);
/* przydzielenie pamieci na macierz C */
if(!(bufC = (double *)malloc(liczbaWierszyA *
liczbaKolumnB * sizeof(double))))
err("nie moge przydzielic pamieci na C");
/* Master przydziela fragmenty macierzy */
przedzialyTab[0].nMin = 0;
przedzialyTab[0].nMax =
dzielenie(liczbaWierszyA, size) - 1;
for(i = 1; i < size; i++) {
przedzialyTab[i].nMin =
przedzialyTab[i - 1].nMax + 1;
przedzialyTab[i].nMax =
dzielenie(liczbaWierszyA -
przedzialyTab[i - 1].nMax - 1, size -
i) + przedzialyTab[i - 1].nMax;
}
}
czasStart = MPI_Wtime(); /* pomiar czasu */
/* rozsylanie informacji o przedzialach do
* poszczegolnych procesow
*/
MPI_Scatter(przedzialyTab,
sizeof(struct przedzial), MPI_BYTE,
przedzialyTab,
sizeof(struct przedzial), MPI_BYTE,
0, MPI_COMM_WORLD);
MPI_Bcast(&liczbaWierszyB, 1, MPI_INT, 0,
MPI_COMM_WORLD);
MPI_Bcast(&liczbaKolumnB, 1, MPI_INT, 0,
MPI_COMM_WORLD);
/* rozsylanie macierzy B do procesow */
if(rank != 0 &&
!(bufB = (double *)malloc(liczbaWierszyB *
liczbaKolumnB * sizeof(double))))
err("nie moge przydzielic pamieci na B");
MPI_Bcast(bufB, liczbaWierszyB * liczbaKolumnB,
MPI_DOUBLE, 0, MPI_COMM_WORLD);
/* rozsylanie macierzy A we fragmentach */
MPI_Bcast(&liczbaKolumnA, 1, MPI_INT, 0,
MPI_COMM_WORLD);
if(rank == 0)
for(i = 1; i < size; i++)
MPI_Send(bufA + przedzialyTab[i].nMin *
liczbaWierszyA,
(przedzialyTab[i].nMax -
przedzialyTab[i].nMin + 1) *
liczbaKolumnA, MPI_DOUBLE, i,
tag, MPI_COMM_WORLD);
else {
i = (przedzialyTab[0].nMax -
przedzialyTab[0].nMin + 1) *
liczbaKolumnA;
if(!(bufA = (double *)malloc(i *
sizeof(double))))
err("nie moge przydzielic pamieci na A");
MPI_Recv(bufA, i, MPI_DOUBLE, 0, tag,
MPI_COMM_WORLD, &status);
}
if(rank != 0 &&
!(bufC = (double *)malloc(
(przedzialyTab[0].nMax -
przedzialyTab[0].nMin + 1) *
liczbaKolumnB * sizeof(double))))
err("nie moge przydzielic pamieci na C");
/* mnozenie macierzy */
n = przedzialyTab[0].nMax -
przedzialyTab[0].nMin + 1;
for(i = 0; i < n; i++) {
for(j = 0; j < liczbaKolumnB; j++) {
bufC[i_liczbaKolumnB] = 0;
k_liczbaKolumnB = j;
for(k = 0; k < liczbaKolumnA; k++) {
bufC[i_liczbaKolumnB] +=
bufA[k + i_liczbaKolumnA] *
bufB[k_liczbaKolumnB];
k_liczbaKolumnB += liczbaKolumnB;
}
i_liczbaKolumnB++;
}
i_liczbaKolumnA += liczbaKolumnA;
}
/* Master zbiera wyniki */
if(rank == 0)
for(i = 1; i < size; i++)
MPI_Recv(bufC + przedzialyTab[i].nMin *
liczbaKolumnB,
(przedzialyTab[i].nMax -
przedzialyTab[i].nMin + 1)
* liczbaKolumnB, MPI_DOUBLE, i,
tag, MPI_COMM_WORLD, &status);
else
MPI_Send(bufC, (przedzialyTab[0].nMax -
przedzialyTab[0].nMin + 1) *
liczbaKolumnB, MPI_DOUBLE, 0, tag,
MPI_COMM_WORLD);
if(rank == 0) {
printf("liczba procesow %d\n", size);
printf("czas wykonywania programu %g "\
"sekund\n", MPI_Wtime() - czasStart);
if((plik = creat(argv[3], PERMISSION)) == -1)
err("nie moge utworzyc pliku");
write(plik, &liczbaWierszyA, sizeof(int));
write(plik, &liczbaKolumnB, sizeof(int));
write(plik, bufC, liczbaWierszyA *
liczbaKolumnB * sizeof(double));
close(plik);
}
free(bufC);
free(bufB);
free(bufA);
MPI_Finalize();
return 0;
}
/*
* Funkcja zwraca, zaokraglony do liczby calkowitej,
* wynik dzielenia dwoch liczb. Gdy liczba
* po przecinku jest >= 0.5 to wynik zaokraglany
* jest w gore.
*/
int dzielenie(int dzielna, int dzielnik)
{
return (double)dzielna / dzielnik -
(double)(dzielna / dzielnik) >= 0.5
? dzielna / dzielnik + 1
: dzielna / dzielnik;
}
Problemy występujące w implementacji
W założeniach do programu przyjąłem, że proces Master wczytuje,
do swojej pamięci, macierze w całości,
a następnie rozsyła je (lub ich fragmenty) do pozostałych procesów.
Takie postępowanie znacznie ogranicza wielkości macierzy, które będą mnożone.
Należało zredukować ilość danych przesyłanych pomiędzy procesorami.
W tym celu należy wybrać mniejszą z macierzy i właśnie tę wysyłać
w całości - dzielić na fragmenty należy macierz większą.
Testy i rezultaty
Poniższa tablica obrazuje wyniki testów przeprowadzonych na ,,superkomputerze'' SR2201.
Obie mnożone macierze miały wymiary 1500 x 1500.
liczba procesów | czas [sekundy]
----------------+---------------
1 | 2473,3
2 | 1234,75
3 | 825,357
4 | 618,305
5 | 495,842
6 | 413,310
7 | 361,914
8 | 316,493
9 | 282,699
10 | 253,599
11 | 232,138
12 | 211,132

Speedup - przyśpieszenie; S - przyśpieszenie, n - liczba procesów; linia ciągła - przyśpieszenie idealne; linia przerywana - przyśpieszenie rzeczywiste
Sposób uruchamiania programów na partycji j12:
echo "/usr/local/mpi/bin/mpirun -np 12 -p j12 `pwd`/rownolegla
`pwd`/A `pwd`/B `pwd`/C" | qsub -N 12 -q j12





