Skocz do: nawigacji, wyszukiwania

MICLAB:MPI


Środowisko i modele programowania

dr inż. Łukasz Szustak, Politechnika Częstochowska, IITiS
mgr inż. Kamil Halbiniak, Politechnika Częstochowska, IITiS



Standard MPI


MPI (Message Passing Interface) jest wieloplatformowym interfejsem wykorzystywanym do przesyłania komunikatów pomiędzy procesami programów równoległych. Zaprojektowany został on z myślą o funkcjonowaniu niezależnie od wykorzystywanej platformy sprzętowej. Interfejs ten wykorzystywany jest wykorzystywany zwłaszcza do tworzenia oprogramowania dedykowanego dla systemów z pamięcią rozproszoną. Transfer danych pomiędzy poszczególnymi procesami programu uruchomionymi na poszczególnych procesorach odbywa się za pośrednictwem sieci. Mimo, iż standard ten dedykowany jest dla systemów z pamięcią dzieloną programy MPI są również często uruchamiane w systemach z pamięcią wspólną.


MPI jest obecnie dominującym modelem wykorzystywanym w obliczeniach przeprowadzanych z wykorzysteniem klastów obliczeniowych oraz superkomputerów. Cechuje go niewątpliwie wysoka jakość, skalowalność oraz przenośność. Standard ten implementowany jest w postaci zestwu funkcji bibliotecznych, które mogą zostać wywołane z programów zaimplementowanych różnych językach programowania, przykładowo C, C++, Fortran czy Java. Programy MPI wykorzystują model programowania SPMD ( single-program multiple-data), w którym program wykonywany jest jednocześnie na kilku maszynach i procesy mogą przetwarzać w tym samym czasie różne fragmenty danych . Pierwsza wersja standardu MPI ukazała się w czerwcu 1994 roku.


Standard MPI zawiera około 500 funkcji. Jednakże, w pełni funkcjonujący program MPI zazwyczaj wykorzystuje jedynie 10-20 funkcji MPI. Wszystkie procesy progrmu uruchamiane są jednocześnie. Każdy program MPI musi wywołać dwie funkcje MPI_Init() oraz MPI_Finalize(). Pierwsza z nich MPI_Init() inicjalizuje uruchomienie środowiska MPI. Funkcję tą należy wywołać tylko raz, przed jakąkolwiek inną funkcją MPI. Po jej wywołaniu możliwa jest komunikacja pomiędzy uruchomionymi procesrami. Druga z nich MPI_Finalize() zamyka komunikację i kończy wykonywanie procesów w MPI. Funkcję tą należy wywołać jako ostatnią. Żadna inna funkcja MPI nie może zostać po niej wywołana.


Podstawową strukturą programu MPI jest komunikator definiujący grupę procesów, które mogą się wzajemnie komunikować. Podczas inicjalizacji środowiska MPI tworzony jest podstawowy komunikator o nazwie MPI_COMM_WORLD. Składa się on z wszystkich uruchomionych procesów. Standard MPI dostarcza także, szereg funkcji pozwalających na definiowanie własnych komunikatorów zawierających jedynie wybrane podgrupy procesów. Komunikacja między procesami oraz definiowanie zadań, które mają zostać wykonane przez poszczególne procesy wymaga uzyskania informacji o identyfikatorze procesu oraz liczbie wszystkich uruchomionych procesów. Każdy z procesów w komunikatorze posiada własny identyfikator, który pobrać można za pomocą funkcji MPI_Comm_rank. Uzyskanie informacji o liczbie procesów w komunikatorze realizowane jest za pomocą funkcji MPI_Comm_size. Przykład prostej aplikacji MPI przedstawiono na Listingu 1.


#include <mpi.h>
#include <stdio.h>
#include <unistd.h>
     
int main(int argc, char *argv[]) 
{
   int rank, size;
   MPI_Init(&argc, &argv);
 
   MPI_Comm_size(MPI_COMM_WORLD, &size);
   MPI_Comm_rank(MPI_COMM_WORLD, &rank);
 
   printf("Hello world: proces %d z %d\n", rank, size);
  
   MPI_Finalize();

   return 0;
}


Listing 1. Przykład aplikacji MPI


Przesyłanie danych pomiędzy procesami aplikacji MPI może odbywać się na różne sposoby. Do podstawowych metod komunikacji w standardzie MPI zalicza się:

  • komunikację punkt-punkt;
  • komunikację grupowa (kolektywna);
  • komunikację jednostronną.


Komunikacja punkt-punkt jest najprostszą formą komunukacji w standarzie MPI. Jest to dwustronna wymiana danych, polegająca na wykonaniu operacji Send-Recive. Dane przesyłane są pomiędzy dwoma procesami (jeden proces wysyła dane, drugi odbiera je). Komunikacja ta dzieli się na komunikację blokującą oraz nie blokującą. W przypadku komunikacji blokującej procesy, które przesyłają dane blokowane są do zakończenia komunikacji. Za wysyłanie danych odpowiedzialna jest za funkcja MPI_Send, natomiast odbiera danych przez proces docelowy realizowane jest za pomocą funkcji MPI_Recv. Komunikacja nieblokująca (asynchroniczna) pozwala na zwięskzenie wydajności przesyłania danych. Podstawowe założenie tego rodzaju komunikacji jest takie, że operacja wysyłająca zwraca sterowanie nie czekając, aż wiadomość zostanie odebrana. Odbiór danych może odbywać się analogicznie do wysyłania (w sposób asynchroniczny). Komunikacja nieblokująca realizowana jest za pomocą funkcji MPI_Isend (wysyłanie) oraz MPI_Irecv (odbiór). Wywołanie przedstawionych funkcji powoduje, iż od tego czasu obliczenia oraz transfer danych realizowane są w tym samym czasie. Do zakończenia komunikacji wymagane jest wywołanie funkcji blokującej MPI_Wait. Przed zakończeniem komunikacji pomiędzy procesami możliwe jest sprawdzenie jej statusu. Do tego celu służy funkcja MPI_Test, która zwraca informacje czy wykonywana operacja została zakończona czy nadal jest realizowana. Standard MPI nie narzuca ograniczeń związanych z kombinacją obu rodzai komunikacji punkt-punkt. Dane mogą zostać wysłane przez proces za pomocą funkcji nieblokujących i odebrane przed drugi proces za pomocą funkcji blokujących (i odwrotnie).

Komunikacja kolektywna (grupowa) jest to komunikacja przebiegająca w obrębie jednej grupy procesów, która identyfikowana jest przez komunikator. W przypadku tego typu komunikacji, wszystkie procesy w obrębie zadanego komunikatora wykonują tą samą funkcję komunikacyjną. Komunikacja kolektywna wymaga synchronizacji procesów. W standardzie MPI wyróżnia się następujące typy komunikacji grupowej:

  • synchronizacja procesów (realizowana za pomocą funkcji MPI_Barrier);
  • wysyłanie od jednego do wszystkich (realizowane za pomocą funkcji MPI_Broadcast). Dane umieszczone w tablicy przesyłane są od danego procesu do wszystkich procesów w obrębie komunikatora. Procesem wysyłającym może być dowolny proces w grupie. Funkcja ta wysyła dane jak i je odbiera (wszystkie procesy z wyjątkiem procesu wysyłającego);
  • rozpraszanie bufora komunikatu na wszystkie procesy w grupie (realizowane przez funkcje MPI_Scatter). Proces wysyłający dane dzieli wysyłaną tablicę na zadaną liczbę elementów i rozsyła te fragmenty do wszystkich procesów łącznie ze sobą samym. Dane umieszczone są w określonych tablicach. Każdy z procesów otrzymuję taką samą ilość danych;
  • składanie komunikatów do pojedynczej tablicy (realizowane za pomocą funkcji MPI_Gether). Każdy z procesów w obrębie komunikatora wysyła tablicę o zadanej długości. Proces odbierający zbiera wysyłane dane i umieszcza je w zadanej tablicy odbierającej. Dane umieszczane są w kolejności zgodnej z numerami procesów;
  • składanie komunikatów i rozsyłanie ich do grupy procesów (realizowane przez funkcję MPI_Allgather). Każdy z procesów w obrębie komunikatora wysyła zestaw danych o zadanej długości. Dane te są składane do jednej tablicy i wysyłane do wszystkich procesów w grupie;
  • redukcja wartości ze wszystkich procesów do pojedynczej wartości, Jest ona realizowana przez funkcję MPI_Reduce. Funkcja ta wykonuje zadaną operacje przez wszystkie procesy na elementach znajdujących się w tablicy. Operacją tą może być np.: operacja sumowania. Standard MPI posiada zdefiniowany zestaw operacji wykonywanych na elementach tablicy. Rezultat zapisywany jest w zdanej tablicy wyjściowej w określonym procesie. Metoda jest wywoływana przez wszystkie procesy w grupie z tymi samymi wartościami argumentów.


Komunikacja jednostronna wprowadzona została w drugiej wersji standardu MPI (MPI-2). Wymaga ona wprowadzenia odpowiednich funkcji synchronizujących. Wykonanie operacji komunikacji jednostronnych wymaga utworzenia tak zwanego okna dostępu w pamięci, które polega na wydzieleniu obszaru zaalokowanej pamięci procesu, do którego będą miały dostęp inne procesy. Dostęp do danych określonego procesu odbywa się w trybie RMA (Remote Memory Access). Utworzenie okna pamięci jest operacją zbiorową, wykonywaną przez wszystkie procesy w obrębie konkretnego komunikatora. Tworzenie okna pamięci realizowane jest za pomocą funkcji MPI_Win_create. Do usunięcia utworzonego okna słuzy funkcja MPI_Win_free. Utworzone okno zawiera zdefiniowany przez programistę rozmiar tablicy, przy czym rozmiar ten nie musi być całkowitym rozmiarem tablicy wykorzystywanej przez dany proces w trakcie obliczeń. Komunikacja jednostronna pozwala na wykonanie trzech operacji na zdefiniowanych oknach pamięci:

  • zapisu danych, który realizowany jest przez funkcję MPI_Put. Proces uzyskujący dostęp do pamięci docelowego procesu zapisuje dane o okeślonym rozmiarze do danej tablicy. Dane zostają umieszczone w pamięci pod adresem określonym przez okno pamięci oraz zdefiniowanym przesunięciem;
  • odczytu danych, który reazliwany jest przez funkcję MPI_Get. Dane odczytywane są z procesu docelowego spod adresu określonego przez początek okna oraz przesunięcia. Po wykonaniu operacji odczytu należy wykonać operację synchronizacji.
  • modyfikacji danych realziwanych przez funkcję MPI_Accumulate. Operacją wykonywaną na danych może być przykładowo operacja sumowania. Wynik wykonanej operacji zapisywany jest w pamięci procesu docelowego.
Komunikacja jednostronna dostarcza szereg mechanizmów synchornizacji operacji wykonywanch na danym oknie pamięci. Przykładem takiej funkcji jest MPI_Win_fence.


Każdy komunikat w MPI ma określony format. Dokładniej, pojedynczy komunikat MPI jest tablicą elementów pewnego typu zdefiniowanego przez programistę. Standard MPI posia zestaw predefiniowanych typów danych, które są odpowiednikami typów znanych z języków C oraz Fortran. Typy danych zdefiniowane w MPI przedstawione zostały w Tabeli 1.


Tabela 1. Typy danych w standardzie MPI
Typ danych MPI Typ danych w języku C
MPI_CHAR signed char
MPI_SHORT signed short int
MPI_INT signed int
MPI_LONG signed long int
MPI_UNSIGNED_CHAR unsigned char
MPI_UNSIGNED_SHORT unsigned short int
MPI_UNSIGNED unsigned int
MPI_UNSIGNED_LONG unsigned long int
MPI_FLOAT float
MPI_DOUBLE double
MPI_LONG_DOUBLE long double
MPI_BYTE 8 binarnych cyfr (8 bitów)
MPI_PACKED Brak odpowiednika w języku C.

Paczka danych przesyłana z użyciem bufora.
Dane spakowane lub rozpakowane z użyciem MPI_Pack() / MPI_Unpack().

MPI pozwala także na definiowanie własnych struktur danych bazujących na podstawowych typach przedstawionych powyżej. Tak stworzone struktury często są nazywane pochodnymi typami danych.


Przykład komunikacji pomiędzy procesami aplikacji przedstawiony został na Listingu 2. Zaprezentowana aplikacja odpowiedzialna jest za sumowanie elementów tablicy. Wykorzystuje ona dwa rodzaje komunikacji - komunikację punkt-punkt oraz komunikację zbiorową.

Listing 2. Przykład komunikacji pomiędzy procesami
#include <iostream>
#include <cstdlib>
#include <mpi.h>
using namespace std;

int main(int argc, char *argv[])
{
    int rank, size, len;  
   
    MPI_Init(&argc,&argv);
    
    MPI_Comm_rank(MPI_COMM_WORLD,&rank);
    MPI_Comm_size(MPI_COMM_WORLD,&size);
   
    int localSum = 0;
    int globalSum = 0;
    int* array;
    int n;
   
   
    if(rank == 0)
    {
        int totalSize = 40;
        n = totalSize / size;
        for(int i=1; i<size; ++i)
        {
            MPI_Send(&n, 1, MPI_INT, i, 0, MPI_COMM_WORLD);
        }
               
        array = new int[totalSize];
        for(int i=0; i<totalSize; ++i)
        {
            array[i] = i+1;
        }

        int offset = n;
        for(int i=1; i<size; ++i)
        {            
            MPI_Send(&array[offset], n, MPI_INT, i, 0, MPI_COMM_WORLD);
            offset += n;
        }
    }
    else
    {         
        MPI_Recv(&n, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
       
        array = new int[n];
        MPI_Recv(&array[0], n, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
    }
   
  
    for(int i=0; i<n; ++i)
    {
       localSum += array[i];
     }
    
   
    MPI_Reduce(&localSum, &globalSum, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);
   
    if(rank == 0)
    {
        printf("Suma elementów: %d\n", globalSum);
    }
    
    MPI_Finalize();    
    return 0;
}


Oprócz mechanizmów komunikacji standard MPI posiada szereg innych mechanizmów. Zalicza się do nich: mechanizm pozwalający na tworzenie topologi procesów, grup procesów, komunikatorów i operacji wykonywanych w ramach funkcji MPI_Reduce oraz dynamiczne zarządzanie procesami wprowadzone w drugiej wersji standardu. Nowe rozszerzenie usuwa model procesów znany ze standardu MPI-1 i umożliwia procesom MPI tworzenie nowych procesów poprzez tak zwaną operację rozmnażania (spawning). MPI-2 oferuje także możliwość wykonywania równoległych operacji wejścia-wyjścia. Pozwala to procesom na wykonywanie równoległych operacji plikowych zapisu oraz odczytu. Druga wersja standardu pozwala również na programowanie w wielu językach jednocześnie.


Najnowszą wersją standardu MPI jest MPI-3. Wprowadza ono kilka istotnych rozszerzeń. Zalicza się do nich nieblokujące operacja kolektywne oraz komunikację sąsiedzką. Trzecia wersja wprowadza także, rozszerzenie do komunikacji jednostronnej - wsparcie dla pamięci współdzielonej. Procesy mogą tworzyć okna pamięci, w których dane współdzielone będą przez wszystkie procesy uruchomione w obrębie pojedynczego węzła (podobnie jak np.: w standardzie OpenMP). MPI-3 wprowadza również klika mniejszych. Należą do nich między innymi możliwość tworzenia aplikacji z wykorzystaniem nowego standardu języka Fortran – Fortran 2008, rozszerzenie zestawu narzędzi MPI o możliwość podglądania zmiennych zewnętrznych i liczników oraz naprawa błędów funkcji MPI_Probe.



< Standardy programowania równoległego - Intel Cilk Plus

Mechanizm Affinity >