Stručný přehled vlastností a funkcí
MPI
(Základním podkladem pro tento přehled byla osmá
kapitola z knihy Iana Fostera Designing and Building Parallel
Programs)
Poznámka: Kromě příkladů není používána přesná
syntaxe funkcí - důraz je na srozumitelnost.
1. Charakteristika MPI
- Knihovna pro podporu
paralelních výpočtů v systémech s distribuovanou pamětí,
vazby (interface) má pro programovací jazyky C a
Fortran. Není zaručen determinismus chování programu
a překladač samozřejmě může provádět jen velmi
omezené kontroly správného využití MPI funkcí.
- Proti PVM se jedná o
programovací prostředek vyšší úrovně, vztah je asi
jako mezi jazykem symb. adres (odpovídá PVM) a vyšším
programovacím jazykem (odpovídá MPI), tedy:
- v PVM jde naprogramovat
"skoro cokoliv", ale dá to velkou práci
a program pak nejspíš nebude přenositelný,
- dominantní aplikací pro MPI jsou numerické výpočty
s regulárními daty (vektory či matice s číselnými prvky),
přičemž pro takové aplikace je programování relativně
pohodlné a program je dobře přenositelný do jiné
instalace MPI.
- MPI je primárně míněno (na rozdíl od PVM) pro
homogenní výpočetní prostředí (tj. cluster stanic
se společnou administrací, superpočítač jako N-Cube,
ap.), takže poskytuje prostředky i pro "synchronní"
algoritmy (tj. takové, kde se předpokládá přibližně
stejná rychlost běhu jednotlivých procesů) a odpovídající
statické rozdělení práce mezi procesy.
- MPI primárně využívá SPMD model paralelního výpočtu,
tj. vyrobí se (na rozdíl od PVM ) jen jeden "exe"
soubor programu a ten se zavede do zvoleného počtu (dále
N) procesorů. V každém procesoru běží jen
jeden proces. Všech N procesů tudíž běží
podle téhož programu, zjistí si své číselné ID a
podle toho odliší svou činnost. V zásadě je tudíž
realizovatelný i MPMD model výpočtu (třeba ve verzi farmer-workers,
přičemž v programu je přepínač podle ID, jednu větev
realizuje proces s číslem třeba 0 - farmer,
druhá větev (jiná čísla než 0) je pro procesy typu worker).
- Komunikace procesů je asynchronní message-passing s přímým
adresováním přes číselné ID procesu. Existuje
mechanismus skupin procesů (viz dále tzv. komunikátory)
a možnost broadcastu zprávy ve skupině procesů. Zprávy
není na rozdíl od PVM třeba pracně "pakovat".
MPI má svoje "primitivní datové typy" a z
nich lze skládat "strukturované typy", sloužící
ovšem jen pro účely komunikace (tj. zjednodušený
popis toho, co má přijít do zprávy - lze srovnat s náročností
"pakování" zprávy v PVM).
- Existuje sada funkcí pro tzv. "globální operace",
tj. operace nad daty, jejichž instance jsou "rozprostřeny"
ve všech procesech výpočtu. Realizace takových operací
v PVM se musí pracně rozepsat do primitivnějších
operací pvm_send() a pvm_recv().
2. Základní funkce MPI
Je jich jenom 6, přičemž už umožňují
napsat jednodušší aplikaci. První čtyři z nich je třeba
použít v každém MPI programu. Všechny funkce vrací celočíselný
kód úspěšnosti provedené operace, přičemž symbolická
hodnota MPI_SUCCESS znamená úspěch. Přehled základních
funkcí v C-syntaxi:
- int MPI_Init (int* argc,
char*** argv)
Inicializace MPI výpočtu, argc a argv jsou argumenty
hlavního programu.
- int MPI_Comm_size (MPI_Comm
comm, int* adr_size)
Zjištění počtu procesů, počet se dosadí do proměnné
odkazované parametrem adr_size. MPI_Comm je typ "komunikátor"
(viz dále část 5), pokud při volání dosadíme za
parametr comm hodnotu MPI_COMM_WORLD, zjišťujeme počet
všech vytvořených procesů aplikace N.
- int MPI_Comm_rank (MPI_Comm
comm, int* adr_rank)
Zjištění čísla procesu v rámci "komunikačního
světa" comm. Čísla jsou v rozmezí 0 až M-1, kde
M je "rozměr komunikačního světa"
reprezentovaného komunikátorem comm (M = N, pokud dosadíme
za comm hodnotu MPI_COMM_WORLD).
- int MPI_Finalize (void)
Ukončení výpočtu v MPI, provádí každý proces.
- int MPI_Send (void* adr_buf,
int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm
comm)
Odeslání zprávy s typem tag v komunikačním světě
comm procesu dest. Odesílá se zpráva z bufferu buf
obsahující count položek typu datatype.
Čili buffer pro zprávu je na rozdíl od PVM kdekoliv v
datech programu (zadá se adresa příslušného pole).
Za datatype se dosazují buď primitivní typy MPI, například
MPI_INT, MPI_DOUBLE, MPI_CHAR, nebo strukturované typy
vytvořené z primitivních (viz dále).
- int MPI_Recv (void* adr_buf,
int count, MPI_Datatype datatype, int source, int tag,
MPI_Comm comm, MPI_Status *adr_status)
Blokující příjem zprávy. Parametry
mají analogický význam jako u MPI_Send. Navíc je
parametr adr_status, odkazující kam se má uložit
status příjmu zprávy (výstupní parametr). Status
obsahuje položky: status.MPI_SOURCE - od koho zpráva přišla
a status.MPI_TAG - jakého typu zpráva přišla. Dosadí-li
se za source hodnota MPI_ANY_SOURCE a za tag MPI_ANY_TAG,
přijme se jakákoliv zpráva a z uvedených položek výstupního
parametru status se dá zjistit co to vlastně přišlo.
Další funkce už nepatří do kategorie "šesti základních",
ale uvedeme je zde kvůli úplnosti souboru základních operací
pro asynchronní message-passing v MPI:
- int MPI_Iprobe (int source,
int tag, MPI_Comm comm, int* flag, MPI_Status *adr_status)
Neblokující test příjmu zprávy.
Parametry mají analogický význam jako u MPI_Recv().
Pokud je hodnota parametru flag po volání funkce rovna
1, zpráva přišla.
- int MPI_Get_count (MPI_Status
*adr_status, MPI_Datatype datatype, int* count)
Použije se po volání MPI_Probe(),
zpráva už přišla, ale neví se od koho a jaká (tj.
Probe() bylo voláno s parametry MPI_ANY_SOURCE a/nebo
MPI_ANY_TAG). Protože z navráceného parametru status
nejde rovnou poznat délka zprávy, musí se ještě (nad
již vyplněným status) zavolat Get_count(), které vrátí
počet položek příslušného typu (který tudíž musí
být nějak implicitně znám). Teprve potom se
nadimenzuje (dynamicky přidělí podle hodnoty count) přijímací
buffer a zpráva se do něj "definitivně" přijme
voláním Recv() s již známými parametry.
3. Globální operace MPI
Globální operace jsou operace, do kterých
jsou zapojeny všechny procesy patřídí do téhož "komunikačního
světa" (tj. jedním parametrem funkcí pro globální
operace je příslušný komunikátor). Funkci globální operace
volají všechny zúčastněné procesy (což je pochopitelné,
protože typicky procesy běží podle téhož programu), přičemž
jejich činnost se v obecném případě liší podle čísla
procesu. Globální operace jsou "ušité" na
manipulace s regulárními datovými strukturami, konkrétně s
homogenními vektory a maticemi.
Globální operace v PVM nejsou (kromě
broadcastu) a musely by se pracně rozepsat do posloupnosti
operací send() a receive(). Globální operace MPI lze rozdělit
na tři skupiny - synchronizace, přesuny dat a redukční
operace.
3.1 Synchronizace
Každý komunikátor mj. realizuje bariéru, na
které se mohou všechny jeho procesy synchronizovat voláním
funkce
int MPI_Barrier (MPI_Comm
comm)
Volání této funkce je tedy blokující a výpočet každého
procesu pokračuje následujícím příkazem až poté, kdy se
na bariéře "sejdou" všechny procesy patřící do
"komunikačního světa" comm.
3.2 Přesuny dat
Jedná se o operace s charakterem broadcastu,
shromáždění či rozptýlení dat a tzv. redukční oprace.
- int MPI_Bcast (void* adr_buf,
int count, MPI_Datatype datatype, int root, MPI_Comm comm)
Operace broadcast. Má v zásadě stejné parametry jako
MPI_Send(). Proces s číslem root vysílá, ostatní zprávu
přijímají (čili root používá buffer jako vysílací,
ostatní jako přijímací). Proti send() chybí tag -
pro "globální" komunikační operaci nemá význam
- všichni aktéři komunikace prochází tímtéž bodem
programu a tudíž vědí, "o co při komunikaci jde".
- int MPI_Gather (void* adr_inbuf,
int incnt, MPI_Datatype intype, void* adr_outbuf, int
outcnt, MPI_Datatype outtype, int root, MPI_Comm comm)
Proces s číslem root shromažďuje
data od všech procesů včetně sebe.Čili všechny
procesy vysílají data (count položek typu intype) z
bufferu inbuf a proces root je zapisuje do bufferu outbuf
- samozřejme je zapisuje v pořadí podle čísel vysílajících
procesů (tj. nikoliv tak, jak zprávy došly). Parametr
outbuf (a též outcnt a outtype) využije jen proces
root. Za incnt a intype se normálně dosadí stejné
hodnoty jako za outcnt a outtype.
!!! Výstupní buffer zřejmě musí být M-krát větší
než vstupní buffer, kde M je rozměr (počet procesů)
komunikátoru comm.
- int MPI_Scatter (void* adr_inbuf,
int incnt, MPI_Datatype intype, void* adr_outbuf, int
outcnt, MPI_Datatype outtype, int root, MPI_Comm comm)
Inverzní operace k Gather(), tj. proces s číslem root
"rozptyluje" data z bufferu inbuf všem ostatním
procesům včetně sebe. Vstupní buffer musí obsahovat
M částí dat (obecně různých, jedna část je pole
count položek typu intype), přičemž i-tá část se
pošle do bufferu outbuf procesu s číslem i. Parametr
inbuf (a též incnt a intype) využije jen proces root.
Za incnt a intype se normálně dosadí stejné hodnoty
jako za outcnt a outtype.
!!! Vstupní buffer zřejmě musí být M-krát větší
než výstupní buffer, kde M je rozměr (počet procesů)
komunikátoru comm.
3.3 Redukční operace
Redukční operace má M operandů umístěných
ve vstupních bufferech komunikujících procesů. Výsledek
operace se zapisuje do výstupního bufferu buď jednoho určeného
procesu (root) nebo všech procesů.
- int MPI_Reduce (void* adr_inbuf,
void* adr_outbuf, int count, MPI_Datatype datatype, MPI_Op op, int
root, MPI_Comm comm)
Za parametr op se dosazuje typ realizované operace, kde
použitelné symbolické hodnoty typu jsou
MPI_MAX, MPI_MIN (maximum, minimum),
MPI_SUM, MPI_PROD (součet, součin),
MPI_LAND, MPI_LOR, MPI_LXOR (logické operace)
MPI_BAND, MPI_BOR, MPI_BXOR (logické operace se všemi
bity)
Operandy jsou ve vstupních bufferech procesů, výsledek
je ve výstupním bufferu procesu root.
- int MPI_Allreduce (void*
adr_inbuf, void* adr_outbuf, int count, MPI_Datatype datatype, MPI_Op
op, MPI_Comm comm)
Funguje jako předchozí operace, ale výsledek dostanou
do výstupního bufferu všechny zúčastněné procesy,
tudíž chybí parametr root, který pak nemá smysl.
4. Komunikátory
Komunikátor je interní objekt MPI (typ
MPI_comm, jedná se o typ tzv. "handle", čili jakéhosi
zobecněného ukazatele reprezentujícího komunikátor).
Komunikátor reprezentuje skupinu komunikujících procesů a
komunikační kontext této skupiny (viz třeba funkci MPI_Barrier()
z předchozí kapitoly).
Komunikátor slouží jako prostředek pro modulární členění
programu (tj. usnadňuje vytváření knihovních funkcí,
speciálně pro výpočet knihovní finkce se vyrobí "extra"
komunikátor jako duplikát existujícího komunikátoru a "dosadí
se" do volání knihovní funkce).
Dále komunikátor slouží pro paralelní členění
programu, či jinak řečeno pro realizaci modelu MPMD (funkční
paralelismus), kdy se procesy aplikace rozdělí na skupiny (reprezentované
různými komunikátory) a každá skupina "dělá něco jiného"
a její procesy "se vybavují jen mezi sebou". Čili
uvnitř skupiny procesů (komunikátor) funguje datový
paralelismus, kdežto mezi skupinami procesů funguje funkční
paralelismus.
Základní funkce nad komunikátory jsou (podrobnosti viz
literatura):
- MPI_Comm_rank()
vrací počet procesů komunikátoru, základní funkce
MPI - viz dříve v části 2
- MPI_Comm_dup()
vytváří duplikát komunikátoru
- MPI_Comm_split()
"štěpí" kpomunikátor na dva jiné, čili
rozděluje jednu skupinu procesů na dvě jiné
- MPI_Comm_free()
uvolňuje (likviduje) komunikátor (samozřejmě nikoliv
procesy, které komunikátor reprezentuje)
- MPI_Intercomm_create()
vytváří tzv. "interkomunikátor" jakožto
prostředek komunikace mezi dvěma skupinami procesů.
Poznámka: Pro jednoduché aplikace (v kategorii semestrálky z
PPR, nevytváří se funkčně odlišné skupiny procesů) lze na
komunikátory "zapomenout". Všechny procesy aplikace
jsou reprezentovány základním (implicitně vytvořeným)
komunikátorem se symbolickým označením MPI_COMM_WORLD. Pokud
tuto hodnotu systematicky dosazujeme za parametr comm v komunikačních
funkcích, probíhá komunikace vždy v "komunikačním světě"
všech procesů aplikace.
5. Datové typy pro komunikaci
Jak již bylo řečeno, MPI využívá v komunikačních
operacích buffer, který je "netypovaný" (tj.
ukazatel na buffer je typu void*). Buffer jde v rámci komunikační
operace "překrýt" přes libovolná data v programu.
Typ (a počet) datových prvků "přes které buffer leží"
se zadává jako jeden z parametrů komunikační operace - viz třeba
MPI_Send() dříve v části 2. K tomuto účelu zavádí MPI
"primitivní typy", které zhruba odpovídají
primitivním typům jazyka C (MPI_INT, MPI_DOUBLE, atd.). Aby
bylo možné jednou komunikační operací vyslat i složitěji
strukturovaná data, zavádí MPI tzv. "odvozené datové
typy", tj. typy, které lze "poskládat" z
primitivních či (již dříve) odvozených typů. Odvozené
typy jsou ve třech kategoriích (contiguous, vector, indexed).
Základní funkce využitelné pro práci s odvozenými typy (podrobnosti
viz literatura) jsou tyto:
- MPI_Type_contiguous()
Tzv. "typový konstruktor", definuje "spojité"
(tj. "bez mezer") pole prvků jiného (i
neprimitivního) typu.
- MPI_Type_vector()
Tzv. "typový konstruktor", definuje "pole
bloků" prvků jiného (i neprimitivního) typu,
mezi bloky může být "mezera".
- MPI_Type_indexed()
Tzv. "typový konstruktor", definuje "nepravidelné
pole bloků" prvků jiného (i neprimitivního) typu,
bloky mohou být různě dlouhé a mohou mezi nimi být různě
dlouhé mezery.
- MPI_Type_commit()
Voláním této funkce se typ (dynamicky vytvořený některou
z předcházejících operací) "přihlásí do MPI",
a pak se teprve může používat v komunikačních
operacích.
- MPI_Type_free()
Voláním této funkce se typ (vytvořený voláním některého
z výše uvedených konstruktorů) dynamicky zruší a už
se nesmí používat v komunikačních operacích.
Příklad použití odvozeného typu v programu:
float data [1024];
/*definuje data v programu */
MPI_Datatype floatype; /*definuje
handle nově vytvářeného typu */
MPI_Type_vector (10, 1, 32, MPI_FLOAT,
&floattype); /* dynamicky vytváří nový typ
floattype (odvozený od základního typu MPI_FLOAT), což je 10
bloků obsahujících jen 1 položku float, mezi bloky je "mezera"
32 položek float*/
...
MPI_Send (data, 1, floattype, dest,
tag, MPI_Comm_world); /*vlastní operace vysílání zprávy
*/
...
MPI_Type_free (&floattype);
/*dynamická likvidace typu */
Poznámka: Pro jednoduché aplikace (v kategorii semestrálky z
PPR) lze na odvozené datové typy "zapomenout" a používat
jen primitivní MPI typy ( komunikační operace navíc dovolují
přímo použít i pole položek primitivního typu).
St.R. 20.11.2001