AR3


Wstęp do MPI-2
Jacek Dziedzic
FTiMS, Politechnika Gdańska
Gdańsk, 2006.
Algorytmy równoległe 2006/2007
Pracujemy w systemie równoległym
o architekturze MIMD, z pamięcią rozproszoną
wiadomość #718
pamięć
od: węzeł #1 serwer plików
masowa
do: węzeł #3
N
długość: 8192 B
E
dane: ....................
T
.............................
W
O
R
Pamięć rozproszona  ka\dy węzeł pracuje
K
we własnej przestrzeni adresowej, nie mo\e
...
bezpośrednio "zajrzeć" do pamięci pozosta-
łych węzłów.
Komunikacja między węzłami odbywa się za
pomocą przesyłania wiadomości po wydajnej
sieci.
1
Pracujemy w systemie równoległym
o architekturze MIMD, z pamięcią rozproszoną
plik1
pamięć
serwer plików
masowa
N
plik2
user@node2:~> ls
E
plik3
T
plik1 plik2 plik3
W
user@node2:~>
O
R
K
Dostęp do pamięci masowej
...
zapewnia na ogół serwer
plików, w sposób przezro-
user@node3:~> ls
czysty dla u\ytkownika.
plik1 plik2 plik3
user@node3:~>
MPI-2
W 1995 przeprowadzono ankietę wśród u\ytkowników MPI,
pytając co chcieliby widzieć w kolejnej wersji standardu.
Najczęściej padały prośby o dodanie:
- dynamicznego zarzÄ…dzania procesami (startowanie nowych
procesów w trakcie działania programu, etc.),
- wsparcia dla C++ (ju\ z niego korzystamy),
- komunikacji jednostronnej (wszystkie informacje potrzebne do
przesłania wiadomości dostarcza jedna strona, drugiej nie
zawraca się głowy). Operacje typu Put i Get, pozwalające pisać i
czytać ze zdalnego węzła bez jego bezpośredniego udziału.
- równoległego we/wy.
W 1997 pojawiła się specyfikacja MPI-2, w której zawarto wszystkie
powy\sze udogodnienia i jeszcze trochÄ™ (np. mo\liwe jest ju\
wysyłanie wiadomości w Fortranie i jej odbiór w C++).
Nie wszystkie implementacje MPI dysponujÄ… rozszerzeniami
zawartymi w MPI-2, ale większość tak.
2
Po co nam równoległe we/wy?
Najczęściej mamy do czynienia z sytuacją, w której ka\dy węzeł pracuje
na porcji danych (dekompozycja danych). W pamięci masowej chcieli-
byśmy jednak mieć wszystkie dane w (uporządkowanej) całości.
MAMY: CHCEMY:
pamięć masowa
Bez równoległego we/wy  metoda 1
Ka\dy węzeł mo\e zapisywać dane do oddzielnego pliku.
Aatwe do zrobienia, ale ma szereg wad:
- Podczas obróbki danych (post-processing)
niewygodnie jest pracować z wieloma plikami
zamiast z jednym.
- Co zrobić, jeśli liczba węzłów zmieni się
pomiędzy kolejnymi wywołaniami programu?
Przykładowo program pracował na 4 węzłach,
wyprodukował 4 pliki wyjściowe, a jutro chcemy
by pracował na 5 węzłach.
- Potrzeba dodatkowych u\ytków, które sklejałyby
pliki do jednego po tym, jak program zakończy
siÄ™ strata czasu i przestrzeni dyskowej (co
storage
jeśli pliki mają po 4 GB?).
- Zapis do wielu plików jest niewydajny (seek,
fragmentacja).
3
Bez równoległego we/wy  metoda 2
Mo\na zebrać dane na jednym węzle korzystając
z przesyłania wiadomości i zapisać je z tego węzła.
Gather(...)
pamięć masowa
Bez równoległego we/wy  metoda 2
Wady tego rozwiÄ…zania:
- Nierówny podział pracy  węzeł, na którym piszemy do
pliku jest zajęty wykonując we/wy, podczas gdy
pozostałe węzły nudzą się.
- Na węzle odpowiedzialnym za we/wy potrzebujemy
mnóstwo dodatkowej pamięci, \eby pomieścić na raz
dane ze wszystkich węzłów (jeśli korzystamy z Gather).
- Mo\na przesyłać po kawałku i pisać po kawałku, ale
wtedy pozostałe węzły są dłu\ej zajęte (czekają z
przesłaniem reszty a\ master zapisze poprzednią
porcjÄ™).
- Obcią\enie sieci  teraz wszystkie dane muszą przejść
przez węzeł master, a być mo\e bezpośrednie połą-
czenia węzeł-serwer plików są krótsze.
4
MPI-2 oferuje równoległe we/wy 
wszystkie węzły piszą jednocześnie do jednego pliku
MPI::File::Write(...)
Wygodne (dostajemy jeden plik).
pamięć masowa
Dobry podział pracy (wszystkie węzły zajęte).
Efektywne (MPI i SO mogą optymalizować
zapisy i odczyty).
Podobnie dla odczytu.
Równoległe we/wy  jak?
Skoncentrujmy uwagę na najbardziej pospolitej czynności 
wczytaniu danych z jednego pliku na wiele węzłów tak, \e
ka\dy z nich dostaje fragment danych.
plik
...
node #0 node #1 node #2 node #(N-1)
Trzy metody:
- przesuń wskaznik pliku i czytaj,
- czytaj-od-miejsca (read_at),
- widoki plików.
5
Równoległe we/wy  otwarcie pliku
Zaczynamy od równoległego otwarcia pliku:
static MPI::File MPI::File::Open(const MPI::Intracomm& comm,
const char *filename, int amode, const MPI::Info& info);
To jest operacja zbiorowa  wszystkie węzły wewnątrz komunikatora
muszą ją wywołać. (Jeśli chcemy równolegle otworzyć plik na jednym
procesorze, mo\emy skorzystać z komunikatora MPI::COMM_SELF).
comm  komunikator,
filename  nazwa otwieranego pliku. Mo\e się tak zdarzyć, \e ró\ne
węzły widzą ten sam plik pod ró\nymi nazwami, np. jeden węzeł mo\e widzieć
plik pod nazwÄ… /local/home/janek/plik1 a inny jako /mnt/lab/home/
janek/plik1. Jeśli tak jest, ka\dy węzeł przekazuje swoją nazwę.
info  dodatkowe informacje, które moglibyśmy chcieć przekazać
implementacji MPI. Jeśli nie chcemy, przekazujemy MPI::INFO_NULL).
amode  tryb dostępu (o tym za chwilę).
zwraca: uchwyt do pliku.
domyślnie błędy nie kończą się katastrofą.
Równoległe we/wy  otwarcie pliku
Tryb dostępu jest bitową kombinacją poni\szych flag:
MPI_MODE_RDONLY  tylko do odczytu,
MPI_MODE_WRONLY  tylko do zapisu,
MPI_MODE_RDWR  do odczytu i zapisu,
MPI_MODE_CREATE  utwórz plik, jeśli nie istnieje,
MPI_MODE_EXCL  sygnalizuj błąd, jeśli nie istnieje,
MPI_MODE_DELETE_ON_CLOSE  usuń plik po zamknięciu (dla plików tymczasowych),
MPI_MODE_UNIQUE_OPEN  obietnica, \e plik nie będzie jednocześnie otwarty gdzieś indziej (pozwala SO na lepszą optymalizację),
MPI_MODE_SEQUENTIAL  tylko dostęp sekwencyjny (taśmy, etc.),
MPI_MODE_APPEND  przewiń na koniec pliku po otwarciu.
Zadana kombinacja musi mieć sens, np. MPI_MODE_RDONLY | MPI_MODE_WRONLY
nie ma sensu.
W FORTRANie flagi te dodaje siÄ™ (bo nie ma operatora bitowego OR).
Wszystkie węzły muszą przekazać ten sam tryb dostępu.
Parametr info  dodatkowe wskazówki, które chcemy przekazać MPI, np.
atrybuty pliku, jeśli jest tworzony, sugerowany rozmiar bufora, wskazówki co do
tego, czy plik będzie głównie czytany czy pisany, etc.. Informacje te pozwalają
implementacji na lepszą optymalizację pózniejszych operacji na pliku. Na ogół
nie będziemy przekazywali \adnych dodatkowych informacji  przekazujemy
wówczas MPI::INFO_NULL.
6
Równoległe we/wy  zamykanie pliku
Gdy skończymy pracę z plikiem, trzeba go zamknąć:
void MPI::File::Close();
Zatem wołamy metodę Close() na rzecz obiektu klasy MPI::File,
który reprezentuje nasz plik i który otrzymaliśmy jako wynik działania
funkcji MPI::File::Open().
To te\ jest operacja zbiorowa  wszystkie węzły komunikatora muszą
jednocześnie zamykać plik.
Musimy zamknąć wszystkie pliki zanim wywołamy MPI::Finalize().
W momencie zamknięcia pliku musimy zagwarantować, \e wszystkie
nieblokujące operacje we/wy zakończyły się (my mówimy tylko o
blokujÄ…cych operacjach we/wy).
Równoległe we/wy  czytamy z pliku
Odczyt wygląda podobnie do odbierania wiadomości:
void MPI::File::Read(void* buf, int count,
const MPI::Datatype& datatype, MPI::Status& status);
NIE jest operacją grupową (!)  nie wszystkie węzły komunikatora
muszą czytać. Jeśli chcemy czytać z pliku tylko na niektórych
węzłach, wywołujemy MPI::File::Read() tylko na tych węzłach.
Istnieje wersja grupowa: MPI::File::Read_all(), która słu\y do
odczytu na wszystkich węzłach komunikatora jednocześnie.
Jeśli czytamy na wszystkich węzłach, korzystniej jest zastosować
wersję grupową z uwagi na mo\liwość optymalizacji odczytu przez
MPI i SO.
buf  adres bufora do którego czytamy dane.
count  liczba elementów (nie bajtów!) do wczytania.
datatype  typ odczytywanych danych (MPI::DOUBLE, MPI::INT, etc.)
status  struktura statusu, która zawiera szczegóły odczytu, tak
samo jak w przypadku Recv(), np. liczbÄ™ faktycznie odczytanych
elementów.
7
Równoległe we/wy  uwagi dot. odczytu
Wskazniki pozycji pliku są niezale\ne na wszystkich węzłach.
Oznacza to, \e odczyt na jednym węzle nie ma wpływu na stan pliku
(w szczególności na wskaznik pozycji pliku) na pozostałych węzłach:
plik
A B
Po tym jak węzeł A dokonał odczytu jego wskaznik pozycji pliku
przesuwa siÄ™, ale wskaznik pozycji na B pozostaje niezmieniony.
plik
A B
Równoległe we/wy  uwagi dot. odczytu
W równoległym we/wy w MPI-2 dane w pliku są określonego typu 
nie liczymy danych w bajtach, tylko w elementach:
Odczyt w stylu C (POSIX)read(... ile bajtów ...)
Zapis w stylu C (POSIX)write(... ile bajtów ...)
Zapis w stylu C++std::read(... ile bajtów ...)
Zapis w stylu C++std::write(... ile bajtów ...)
ale
MPI-2 MPI::File::Read(... ile elementów, jakiego typu...)
MPI-2 MPI::File::Write(... ile elementów, jakiego typu...)
operujemy na elementach, nie bajtach.
zwalnia nas to z konieczności pamiętania czy np. sizeof(int)==2,
4 czy 8 na danej platformie.
jeśli w pliku są dane nie tylko jednego typu  tworzymy typ danych
u\ytkownika (np. rekord).
8
Równoległe we/wy  uwagi dot. odczytu
Istnieje przeciÄ…\ona wersja operacji Read() pozbawiona ostatniego
argumentu (status)  korzystamy z niej jeśli status nas nie
interesuje.
Liczbę odczytanych elementów mo\emy wydobyć ze statusu za
pomocÄ… metody Get_count().
Kontrolą błędów musimy się zająć sami  domyślnie operacje
równoległego we/wy nie zgłaszają wyjątków ani nie kończą
programu.
Równoległe we/wy  zapis do pliku
Analogicznie do odczytu (i podobnie do wysłania wiadomości):
void MPI::File::Write(const void* buf, int count,
const MPI::Datatype& datatype, MPI::Status& status);
Podobnie jak poprzednio: NIE jest operacjÄ… zbiorowÄ… (!)  nie
wszystkie węzły komunikatora muszą pisać.
Jeśli jednak piszemy na wszystkich węzłach, warto skorzystać ze
zbiorowej operacji MPI::File::Write_all()  pozwoli ona MPI
optymalizować zapis.
Argumenty  jak dla MPI::File::Read(), tylko bufor jest const.
Jak przy odczycie  ka\dy węzeł ma swój, niezale\ny wskaznik
zapisu.
Sygnalizacja problemu: co jeśli dwóch jednocześnie pisze w to samo
miejsce?
Omówione operacje odczytu i zapisu nie mają zastosowania do plików otwartych w
trybie MPI_MODE_SEQUENTIAL, te trzeba czytać i pisać z u\yciem współdzielonych
wskazników pozycji pliku (shared file pointers)  nie będziemy się nimi zajmować.
9
Równoległe we/wy  jak przemieszczać się w pliku?
Chcemy pisać/czytać w ró\nych miejscach pliku na ró\nych węzłach, musimy
więc mieć mo\liwość przesuwania wskaznika pozycji pliku (przemieszczania się
w pliku, seek):
void MPI::File::Seek(MPI::Offset offset, int whence);
Składnia podobnia do POSIX lseek() u\ywanej do przemieszczania w pliku
w szeregowym we/wy.
offset  liczba elementów (uwaga, nie bajtów!) o którą przesuwamy wskaznik
względem whence,
whence (ang. skąd) stała symboliczna określająca względem jakiego miejsca
mierzymy przesunięcie. Do dyspozycji mamy
- MPI_SEEK_SET od poczÄ…tku pliku,
- MPI_SEEK_END od końca pliku,
- MPI_SEEK_CUR od bie\Ä…cej pozycji.
Przykładowo:
// ustaw wskaznik po pierwszych 10 elementach (0..9)
mój_plik.Seek(10, MPI_SEEK_SET);
// cofnij się o 6 elementów
mój_plik.Seek(-6, MPI_SEEK_CUR);
// przesuń się na koniec pliku
mój_plik.Seek(0, MPI_SEEK_END);
Równoległe we/wy  przykład
input.dat
#include
k*porcja
#include
using namespace std;
int main(int argc, char** argv) { ... ...
węzeł węzeł węzeł węzeł węzeł
MPI::Init(argc,argv); #0 #1 #2 #k #(N-1)
int moj_numer = MPI::COMM_WORLD.Get_rank();
const int porcja=200;
MPI::File plik;
double bufor[porcja];
plik = MPI::File::Open(MPI::COMM_WORLD,"input.dat",
MPI_MODE_RDONLY,MPI::INFO_NULL);
plik.Seek(moj_numer*porcja,MPI_SEEK_SET);
plik.Read(bufor,porcja,MPI::DOUBLE);
plik.Close();
}
10
Równoległe we/wy  odczyt bez jawnego przemieszczania
Druga metoda odczytu (lub zapisu)  czytanie (lub zapis) od razu
od konkretnego miejsca, bez jawnego przesuwania wskaznika
pliku:
void MPI::File::Read_at(MPI::Offset offset, void* buf, int count,
const MPI::Datatype& datatype, MPI::Status& status);
Dodatkowy parametr (offset) mówi z którego miejsca pliku
czytać/pisać.
Przesunięcie to mierzymy w elementach, nie w bajtach!
Parametr offset jest typu MPI::Offset  chocia\ zachowuje siÄ™ jak
liczba całkowita (na ogół jest synonimem int), to nie ma gwarancji \e
pozostanie tak w przyszłości. W związku z powy\szym przesunięcia
w pliku nale\y deklarować jako MPI::Offset, nie int.
Znowu istniejÄ… wersje zbiorowe:
MPI::File::Read_at_all(), MPI::File::Write_at_all().
Metoda trzecia: widoki (file views)
Najbardziej skomplikowana metoda, ale dająca największe
mo\liwości.
Koncepcja widoków zwalnia programistę od pamiętania, \e ka\dy
węzeł czyta z innej części pliku.
Nało\enie widoku na plik działa jak zasłonięcie niektórych
fragmentów pliku na ka\dym z węzłów.
plik
Ka\dy węzeł widzi tylko
część pliku.
Widoki mo\na zmieniać 
widok na węzle #0 widok na węzle #2 zdejmować i nakładać
nowe.
widok na węzle #1 widok na węzle #3
11
Metoda trzecia: widoki (file views)
Co więcej, fragmenty pliku widziane na ka\dym z węzłów nie
muszą być ciągłe!
Otwiera to systemowi operacyjnemu i implementacji MPI pole do
wielu optymalizacji, zwłaszcza jeśli dane się przeplatają.
plik
W szeregowym we/wy ka\dy
węzeł musiałby wczytać cały plik,
po czym wyrzucić ¾ danych (albo
widok na węzle #0 widok na węzle #2
wykonać mnóstwo operacji prze-
mieszczenia).
Gdy korzystamy z widoków MPI-2,
widok na węzle #1 widok na węzle #3
implementacja wie, \e plik trzeba
odczytać tylko raz, po czym auto-
magicznie rozparcelowuje dane
pomiędzy węzły.
Metoda trzecia: widoki (file views)
Mo\liwe jest równie\ określenie, w których miejscach pliku znajdują
się "dziury", jeśli mamy do czynienia z sytuacją, w której chcemy
ukryć pewne dane przed wszystkimi węzłami.
Implementacja będzie się starała zoptymalizować we/wy jeśli dziur
niedostępne na
takich będzie du\o.
\adnym z węzłów
plik
nagłówek
Ka\dy z węzłów widzi tylko to,
co jest dla niego istotne, nie wie
co dzieje się w pozostałych
widok na węzle #0 widok na węzle #2
częściach pliku, nie musi jawnie
przeskakiwać dziur  widzi na-
le\ną mu część jak mniejszy,
widok na węzle #1 widok na węzle #3
ciągły plik.
Bardzo wygodne, wydajne.
Jedyna trudna część  definio-
wanie (nakładanie) widoku.
12
Równoległe we/wy  nakładanie widoku
Widok nakładamy korzystając z metody
void MPI::File::Set_view(MPI::Offset disp, const MPI::Datatype& etype,
const MPI::Datatype& filetype,
const char *datarep, const MPI::Info& info)
Plik musi być uprzednio otwarty.
disp  przesunięcie w pliku, od którego zaczyna się widok dla tego
procesu. Przesunięcie to jest (wyjątkowo) mierzone w bajtach, nie w
elementach. Pozwala to na przeskoczenie nagłówka pliku, który ma
dowolną długość.
etype  podstawowa jednostka danych w pliku,
filetype  opisuje, które części pliku są widoczne,
datarep  łańcuch określający wewnętrzny format pliku  jeden z
{"native", "internal", "external32"}.
info  dodatkowe informacje, które chcemy przekazać implementacji
MPI, podobnie jak przy MPI::File::Open(). Na ogół będziemy
przekazywać MPI::INFO_NULL, nie przejmując się tym.
Równoległe we/wy  etype i filetype
etype  typ danych reprezentujÄ…cy najmniejszÄ… jednostkÄ™
informacji w pliku. Np. dla pliku zawierającego liczby całkowite,
etype==MPI::INT. Jeśli w pliku mamy bardziej skomplikowane
struktury, korzystamy z typów danych definiowanych przez
u\ytkownika (rekordów).
filetype  opisuje w jaki sposób w pliku rozło\one są interesujące
dane i niepotrzebne "dziury"  najczęściej jest to typ definiowany
przez u\ytkownika.
Jeśli w pliku nie ma dziur, filetype==etype.
plik
nagłówek
- etype, np. MPI::DOUBLE
- filetype
13
Widoki  uwagi
Po nało\eniu widoku wskaznik pozycji w pliku dla ka\dego procesu ustawia się na
poczÄ…tek widoku.
Po nało\eniu widoku wszystkie odczyty, zapisy i pozycjonowania działają względem
nało\onego widoku, np. mój_plik.Seek(0, MPI_SEEK_SET) przesuwa wskaznik na
poczÄ…tek widoku, nie pliku.
MPI::File::Set_view() jest operacją zbiorową  wszyscy w obrębie komunikatora
nakładają widok jednocześnie. Argument datarep i rozmiar typu etype muszą być
takie same na ka\dym węzle.
Przed nało\eniem widoku nale\y mieć pewność, \e wszystkie operacje na pliku
zakończyły się (dotyczy operacji nieblokujących).
Argument datarep opisuje jak wyglÄ…da reprezentacja danych w pliku. Najprostszy
wariant to taki, w którym dane w pliku są dokładną kopią danych w pamięci. Tryb ten
wybieramy podając "native". Ma to tę zaletę, \e działa szybko i zapisuje/czyta dane
bez \adnych strat, bo nie ma konwersji. Wada jest taka, \e nie da się tego stosować w
środowiskach heterogenicznych  na innej architekturze reprezentacja danych mo\e
być zupełnie inna.
O pozostałych mo\liwościach ("internal", "external32") powiemy sobie przy
temacie współoperatywności (jak zapewnić przenośność pliku między ró\nymi
architekturami).
Widoki  przykład
Załó\my, \e mamy do odczytania plik o następującym formacie:
- nagłówek: 16 bajtów, zawartość którego (dla uproszczenia) chcemy zignorować,
- 2000 liczb double przeznaczonych dla procesu 0,
- 2000 liczb double przeznaczonych dla procesu 1,
- 2000 liczb double przeznaczonych dla procesu 2.
widok w proc 0 widok w proc 1 widok w proc 1
&
& &
0 15 15+2000*d 15+4000*d
przesunięcia (disp) (w bajtach). d=sizeof(double)
// otwórz plik
const int count = 2000;
MPI::File plik;
plik = MPI::File::Open(MPI::COMM_WORLD, "test.dat", MPI_MODE_RDONLY, MPI::INFO_NULL);
// oblicz poczÄ…tek widoku zale\nie od numeru procesora
d=sizeof(double);
MPI::Offset disp=15+mój_numer*count*d; // wszystko w bajtach
// wybierz typ danych w pliku. Rezygnujemy z "dziur", wobec
// czego filetype == etype
MPI::Datatype etype = MPI::DOUBLE;
MPI::Datatype filetype = MPI::DOUBLE;
// ustaw widok
plik.Set_view(disp, etype, filetype, "native", MPI::INFO_NULL);
// czytaj swojÄ… porcjÄ™
double bufor[count];
plik.Read(bufor, count, etype);
14
MPI-2: mniej istotne operacje na plikach
Sprawdzenie rozmiaru pliku:
MPI::Offset MPI::File::Get_size();
Operacja punktowa.
Rozmiar zwracany jest w bajtach (sic!).
Pamiętamy, \e rozmiar jest typu MPI::Offset.
Zaalokowanie dodatkowej przestrzeni w pliku:
void MPI::File::Preallocate(MPI::Offset size);
size nowy rozmiar pliku.
Słu\y do powiększania pliku  podanie size mniejszego od bie\ącego rozmiaru pliku
nie skraca go.
Wymuszenie powiększenia pliku (potencjalnie) zapobiega fragmentacji, która mo\e
nastąpić gdy plik powiększamy po kawałku.
Operacja grupowa  wszyscy muszą podać to samo size.
Skutkuje powiększeniem pliku do zadanego rozmiaru. Dane, o które powiększył się
plik sÄ… niezdefiniowane.
Nie ma sensu przyMPI_MODE_SEQUENTIAL.
MPI-2: współoperatywność
Zagadnienie współoperatywności (interoperability) operacji we/wy  jak będą
wyglądały operacje we/wy w systemach heterogenicznych?
Dwa aspekty:
1. Fizyczny układ danych w pliku  ró\ne architektury zapisują dane na ró\ne sposoby. Np. liczba
0x304AB7F2 na procesorach Intel x86 będzie zapisana jako cztery bajty: 0xF2 0xB7 0x4A
0x30 (tzw. konwencja little-endian), a na procesorach Motorola 68000 jako cztery bajty: 0x30
0x4A 0xB7 0xF2(tzw. konwencja big-endian).
MPI, poniewa\ gwarantuje współoperatywność, musi dać do dyspozycji mo\liwość
poprawnego odczytania na Motoroli pliku zapisanego kiedyś na Intelu (gorzej, w środowisku
heterogenicznym ten sam plik mo\e być jednocześnie pisany w jednym fragmencie przez
MotorolÄ™, a w innym przez Intela x86).
2. Ewentualne konwersje  na jednej architekturze MPI::DOUBLE mo\e mieć osiem a na
innej sześć bajtów. Co więcej jednocześnie z tego samego pliku mogą korzystać oba
procesory. Nale\y zapewnić ew. przycinanie wartości przy przesyłaniu w jedną
stronę i ew. poszerzanie jej przy przesyłaniu w drugą.
Z drugiej strony niektóre programy są pisane z myślą tylko o środowiskach
homogenicznych i nie obchodzą je zagadnienia współoperatywności  dobrze, gdyby
dało się z tego zrezygnować (i mo\na).
15
MPI-2: współoperatywność
W operacjach we/wy drugi aspekt współoperatywności mamy zagwarantowany
automatycznie  MPI samo dokonuje potrzebnych konwersji zarówno przy
operacjach na plikach jak i przy przesyłaniu wiadomości.
Pierwszy aspekt współoperatywności osiągamy za pomocą widoków.
void MPI::File::Set_view(& , const char *datarep, & );
datarep  łańcuch określający wewnętrzny format pliku  jeden z {"native", "internal",
"external32"}.
Tryb "native" oznacza rezygnację ze współoperatywności. Wszystkie dane w pliku są
dokładną kopią danych w pamięci. Pliki tak zapisane nie działają w systemach
heterogenicznych. Nie dają się czytać (bez jakiegoś ręcznego przekonwertowania) na
maszynach, gdzie obowiązują inne konwencje. Zaletą jest prostota i szybkość (brak
jakichkolwiek narzutów związanych z konwersją).
Tryb "internal" wymusza zapis w pewnym (nieokreślonym dokładnie) formacie wspólnym
dla wszystkich maszyn wykonujÄ…cych program. WÅ‚Ä…czone sÄ… konwersje, zatem osiÄ…gamy
aspekt drugi współoperatywności. Nie ma jednak gwarancji, \e plik da się czytać w innych
środowiskach (nie osiągamy aspektu pierwszego).
Tryb "external32" wymusza zapis w dobrze określonym formacie, o którym mamy
gwarancję \e będzie taki sam we wszystkich środowiskach. Włączone są konwersje.
Osiągamy oba aspekty współoperatywności kosztem największych narzutów czasowych.
Równoległe we/wy  kontrola błędów
W MPI dla Fortranu i C funkcje we/wy zwracają wartość (w Fortranie  dodatkowy
parametr, w C wartość zwracana), którą mo\na sprawdzić \eby upewnić się, czy
operacja we/wy zakończyła się sukcesem:
int error;
error=MPI_File_Open(...);
if(error) // nie udalo sie
error=MPI_File_Seek(...);
if(error) // nie udalo sie
error=MPI_File_Read(...);
if(error) // nie udalo sie
// ...
W C++ korzystamy z wyjątków, \eby móc rozdzielić gałęzie kodu obsługujące
sytuację w której wszystko idzie pomyślnie od sytuacji wyjątkowej:
try {
MPI::File plik = MPI::File::Open(...);
plik.Seek(...);
plik.Read(...);
// ...
}
catch(MPI::Exception &e) {
// nie udalo sie
}
16
Równoległe we/wy  kontrola błędów
Aby wykorzystać mechanizm wyjątków w MPI-2 musimy zrobić dwie rzeczy.
1) Upewnić się, \e ich obsługa jest włączona  często jest tak, \e biblioteka MPI musi
być skompilowana z odpowiednimi opcjami, \eby włączyć obsługę wyjątków. Na ogół
domyślnie obsługa ta jest wyłączona (\eby niekorzystający z nich mogli uniknąć
niepotrzebnych narzutów). Dla przykładu na dzień dzisiejszy na olimpie korzystamy z
wersji bez wkompilowanej obsługi wyjątków:
[jaca@olimp ~]$ ompi_info | grep exceptions
C++ exceptions: no
2) Wymusić, aby wystąpienie błędów we/wy kończyło się zgłoszeniem wyjątku.
W MPI mamy do dyspozycji trzy sposoby reakcji na błędy:
MPI::ERRORS_ARE_FATAL  natychmiastowe zakończenie programu w razie błędu,
domyślnie stosowane dla błędów w przesyłaniu wiadomości,
MPI::ERRORS_RETURN  w razie błędu funkcja zwraca kod błędu (przydatne w C,
Fortranie)  domyślnie stosowane dla błędów we/wy,
MPI::ERRORS_THROW_EXCEPTIONS  w razie błędu zgłaszany jest wyjątek  ten
sposób reakcji na błędy interesuje nas w C++.
Musimy zatem wymusić trzeci typ reakcji dla operacji we/wy.
Równoległe we/wy  kontrola błędów
Do zmiany reakcji na błędy operacji we/wy korzystamy z metody
void MPI::File::Set_errhandler(const MPI::Errhandler& errhandler) const,
której jako argument przekazujemy jedną z wartości MPI::ERRORS_ARE_FATAL,
MPI::ERRORS_RETURN, MPI::ERRORS_THROW_EXCEPTIONS (bądz funkcję obsługi błędów
stworzoną przez u\ytkownika), a wywołujemy ją na rzecz pliku, dla którego chcemy
zmienić sposób reakcji na błędy.
Jeśli obsługa wyjątków nie została włączona przy kompilacji biblioteki MPI,
zachowanie programu po napotkaniu błędu będzie zale\ało od konkretnej
implementacji MPI (np. wersja na olimpie wypisuje komunikat o błędzie i próbuje
kontynuować pracę).
Przy korzystaniu z wyjątków musimy mieć pewność, \e zarówno bibliotekę MPI, jak i
program z niej korzystajÄ…cy skompilowano tym samym kompilatorem oraz \e kod
napisany w innych językach (C, Fortran) będzie poprawnie propagował wyjątki do
momentu wyłapania ich w kodzie w C++.
17
Równoległe we/wy  synchronizacja i spójność
Dwa podstawowe problemy, typowe dla środowisk w których mamy
do czynienia ze współbie\nością.
1.Procesor A wykonuje zapis do pliku F na pozycji k, po czym
procesor B odczytuje z pliku F na pozycji k. Czy procesor B
odczyta ju\ nowozapisane dane? Problem w tym, \e
zakończenie się operacji zapisu na procesorze A nie oznacza,
\e dane faktycznie sÄ… w pliku (na skutek buforowania).
A wysyła B odbiera
koniec
wiadomość wiadomość
zapisu
A rozpo- do B, \e dane i zaczyna
na A
czyna zapis sÄ… gotowe odczyt
|
| | |
czas
A A B
... ale w tym
momencie
danych nie
ma jeszcze
fizycznie na
dysk
dysku
bufory
systemowe
Równoległe we/wy  synchronizacja i spójność
1. Procesor A wykonuje zapis do pliku F na pozycji k, w tym samym
czasie procesor B wykonuje zapis do tego samego pliku na tej samej
pozycji. Co znajdzie siÄ™ w pliku?
Sposób traktowania powy\szych przypadków  semantyka
spójności.
Np. w standardzie POSIX (http://en.wikipedia.org/wiki/POSIX) zakłada się
tzw. semantykę silnej spójności  oznacza to  ad 1: gdy zakończy
siÄ™ zapis, mamy gwarancjÄ™ \e nowe dane widziane sÄ… we wszystkich
procesach (co niekoniecznie oznacza, \e sÄ… ju\ fizycznie na dysku,
tylko \e system dba o spójność buforowanych danych), ad 2:
w sytuacji dwóch jednoczesnych zapisów mamy gwarancję, \e dane
będą pochodziły od jednego lub drugiego procesu (nie wiadomo
którego), ale wiadomo, \e nie będą zbitką danych z obydwu.
18
Równoległe we/wy  synchronizacja i spójność
MPI zakłada słabszą spójność ni\ POSIX (nie daje takich samych
gwarancji). Pozwala to na lepszÄ… optymalizacjÄ™.
Mo\liwe jest w MPI osiągnięcie spójności prawie tak silnej jak w POSIX
wymuszając dostęp do pliku w trybie atomowym.
Atomowość operacji (atomicity) = niepodzielność = gwarancja, \e
w trakcie wykonywania operacji we/wy inna operacja we/wy
(np. zainicjowana przez inny węzeł) nie przeszkodzi tej wykonywanej.
Dostęp do plików w trybie atomowym będzie "bezpieczniejszy" (w sensie
silniejszych gwarancji), ale mniej wydajny.
Włączyć/wyłączyć atomowość dla danego pliku mo\emy korzystając z:
void MPI::File::Set_atomicity(bool flag);
flag  \ądana atomowość (true  włączona, false  wyłączona)
Ustawienie atomowości to operacja zbiorowa  wszystkie procesy w
grupie operującej na tym pliku muszą za\ądać tego samego.
Odnosi się tylko do przyszłych operacji na pliku  dla operacji, które ju\
się rozpoczęły wcią\ obowiązuje poprzednia atomowość.
Szczegóły synchronizacji we/wy i spójności
Niech będzie dany plik F, otwierany przez dwie grupy procesów.
Niech FHa = {fha1, fha2, & , fhan,} będzie zbiorem uchwytów do tego pliku (w C++:
obiektów reprezentujących ten plik) widzianych w pierwszej grupie otwierającej plik.
Niech FHb = {fhb1, fhb2, & , fhan,} będzie zbiorem uchwytów do tego pliku (w C++:
obiektów reprezentujących ten plik) widzianych w drugiej grupie otwierającej plik.
Niech A1 i A2 będą dwiema operacjami dostępu do danych w pliku, a D1 i D2 obszarami
w pliku na których działają operacje A1 i A2.
Będziemy mówić, \e operacje A1 i A2 nakładają się, jeśli D1)"D2 `" ".
Będziemy mówić, \e operacje A1 i A2 są niezgodne, jeśli nakładają się i przynajmniej
jedna realizuje zapis.
Niech Sfh będzie sekwencją operacji na uchwycie fh pliku (w C++: obiekcie
reprezentujÄ…cym plik), otoczonych operacjami synchronizacji MPI::File::Sync()
(o której powiemy więcej pózniej).
Będziemy mówić, \e sekwencja operacji Sfh jest sekwencją zapisu, jeśli co najmniej
jedna z operacji w Sfh jest zapisem lub zmienia stan pliku (np. MPI::File::Preallocate).
Mówimy, \e osiągamy spójność sekwencyjną, jeśli zbiór operacji zachowuje się tak,
jak gdyby były one wykonane szeregowo, a ka\da z nich była atomowa.
19
Szczegóły synchronizacji we/wy i spójności
Będziemy mówić, \e dwie sekwencje operacji S1 i S2 są niewspółbie\ne, jeśli mamy
gwarancję, \e jedna sekwencja operacji całkowicie poprzedza drugą (w czasie).
Mamy do rozpatrzenia trzy przypadki
(od najprostszych):
1. Jeden procesor operuje na jednym pliku za pomocÄ… jednego uchwytu (obiektu
plikowego) fha1"FHa.
W trybie atomowym wszystkie operacje będą sekwencyjnie spójne. W trybie
nieatomowym operacje będą sekwencyjnie spójne, jeśli będą niewspółbie\ne lub
zgodne (lub jedno i drugie). TÅ‚umaczenie: Gdy operujemy na jednym pliku na jednym
procesorze, nie ma problemów jeśli: albo włączymy atomowość, albo nie będziemy
jednocześnie pisać do jednego miejsca w pliku i wykonywać innej operacji na tym
obszarze pliku. Zatem jeśli dwa wątki na tym samym procesorze chcą pisać w to
samo miejsce pliku, albo jeden pisze, a drugi czyta (z tego samego miejsca), to
musimy działać w trybie atomowym.
Szczegóły synchronizacji we/wy i spójności
2. Dwa procesory z tej samej grupy operujÄ… na jednym pliku (otwartym w tej samej
zbiorowej operacji MPI::File::Open), ka\dy za pomocÄ… swojego uchwytu (obiektu
plikowego), tj. pierwszy wykonuje operacjÄ™ A1 na fha1"FHa, a drugi A2 na fha2"FHa.
Je\eli dla wszystkich operacji A1 \adna z operacji A2 nie jest z niÄ… niezgodna, to
spójność sekwencyjna jest zagwarantowana. W razie niezgodności nie ma takiej
gwarancji (inaczej ni\ w POSIX)! Gwarancję taką mo\na wymusić wybierając tryb
atomowy. TÅ‚umaczenie: Gdy dwa procesory otworzÄ… ten sam plik w jednej operacji
otwarcia, po czym jeden zapisze dane w pewne miejsce pliku, a drugi będzie chciał
czytać z tego miejsca (bądz pisać w to miejsce)  to \eby nie było kłopotów
potrzebne jest włączenie trybu atomowego. Jeśli nie włączymy atomowości, dane
zapisane na jednym procesorze mogą nie być od razu widoczne na innych
procesorach  synchronizację mo\emy wymusić wywołując MPI::File::Sync()
(poniewa\ kończy to sekwencję operacji). Podobnie dla zapisów w to samo miejsce
pliku  jeśli nie włączymy atomowości, gdy dwa procesy będą pisały w to samo
miejsce pliku (nawet nie w tym samym czasie!) nie mamy gwarancji co do tego, co
znajdzie siÄ™ w pliku. WÅ‚Ä…czajÄ…c tryb atomowy mamy gwarancjÄ™ jak-w-POSIX  do
pliku trafią dane z jednego z procesorów, choć nie wiadomo którego.
20
Szczegóły synchronizacji we/wy i spójności
3. Dwa procesory z ró\nych grup operują na jednym pliku, ka\dy za pomocą swojego
uchwytu (obiektu plikowego), tj. pierwszy wykonuje operacjÄ™ A1 na fha1"FHa, a drugi
A2 na fhb2"FHb.
Aby zagwarantować spójność sekwencyjną, nale\y skorzystać zMPI::File::Sync()
oraz zapewnić niewspółbie\ność sekwencji, w których występują operacje zapisu.
Tłumaczenie: Nawet jeśli operacje zapisu są niewspółbie\ne, musimy korzystać z
MPI::File::Sync() jeśli chcemy, by w pliku znalazły się dane "niewymieszane".
Nie mo\emy sobie pozwolić na współbie\ne sekwencje zapisu, nawet do ró\nych
części pliku.
Dlaczego muszą być niewspółbie\ne, przecie\ piszemy do ró\nych części pliku?!
Ilustracja problemu: Mamy dwa rozłączne, ale współbie\ne
A B C D E F G H
zapisy. Na skutek tego, \e operacje na dysku wykonywane
na blokach (klastrach), dane mogą się wymieszać jeśli
zapis odbywa się do tych samych bloków. "12" trafia do
1 2 4 5 6
bloku 1., "456" jest na granicy bloków 1. i 2. Oczekiwany
wynik mamy tu. Na skutek wyścigów (zapis jednego bloku
1 2 C 4 5 6 G H
wchodzi między odczyt a zapis drugiego bloku) mo\emy
jednak dostać zarówno
1 2 C D 5 6 G H
to, jak i
A B C 4 5 6 G H
to.
Szczegóły synchronizacji we/wy i spójności
Widać, \e jest to sprawa dość skomplikowana, zwłaszcza jeśli otwieramy plik
w więcej ni\ jednej grupie procesów. Najłatwiej będzie otwierać plik jednocześnie
tylko w jednej grupie.
Wybierając tryb atomowy ułatwiamy sobie \ycie, ale być mo\e kosztem wydajności.
W najtrudniejszym przypadku (3) musimy ręcznie wymuszać synchronizację pliku.
Osiągamy ją wołając:
void MPI::File::Sync();
Wywołanie to skutkuje faktycznym zapisaniem wszystkich danych z buforów do pliku
oraz tym, \e kolejne odczyty z tego pliku na pewno "widzÄ…" nowozapisane dane.
Operacja zbiorowa.
Trzeba zagwarantować uprzednie zakończenie nieblokujących operacji we/wy
na pliku.
Otwarcie i zamknięcie pliku równie\ powodują jego synchronizację.
21
Komunikacja jednostronna
Wprowadzona w MPI-2.
Pozwala na przesyłanie danych między procesorami bez (jawnego) udziału jednego
z nich.
Mechanizm, z którego korzysta: Remote Memory Access (RMA). Tak nazwano sam
proces ("dostęp do pamięci innego węzła"), nie jest to \aden mechanizm sprzętowy
(jak DMA).
nadawca odbiorca origin target
Send() Put()
Recv()
odbiorca nadawca
Send() Get()
Recv()
Komunikacja jednostronna
W odró\nieniu od komunikacji dwustronnej, która wymaga kooperacji dwóch
procesorów, mamy tu do czynienia z sytuacją, w której tylko jedna strona wywołuje
funkcje odpowiedzialne za komunikacjÄ™.
Zatem jeden z węzłów (origin) pełni aktywną rolę, drugi bierze biernie udział w
komunikacji (target). Węzeł aktywny mo\e albo pisać (put) do pamięci węzła
pasywnego, albo czytać z jego pamięci (get).
nadawca odbiorca origin target
Send() Put()
Recv()
odbiorca nadawca
Send() Get()
Recv()
22
Komunikacja jednostronna
W komunikacji dwustronnej przesłanie wiadomości wymagało synchronizacji między
dwoma węzłami (musieli "spotkać się" na przesłanie wiadomości). Przy przesłaniu
wiadomości mieliśmy zatem do czynienia z komunikacją i synchronizacją.
Komunikacja jednostronna rozdziela te dwa procesy. Trzy procedury: Get() (zdalny
odczyt), Put() (zdalny zapis) i Update() (zdalne uaktualnienie) zapewniajÄ…
komunikacjÄ™, inne funkcje zapewniajÄ… synchronizacjÄ™.
Na programiście spoczywa obowiązek takiej synchronizacji węzłów, \eby nie
dopuścić do problemów związanych z wyścigami (jednoczesne pisanie w to samo
miejsce, jednoczesne czytanie z i pisanie w to samo miejsce, etc.).
Na niektórych komputerach procedury komunikacji jednostronnej mogą być
efektywniejsze ni\ przesyłanie wiadomości (np. systemy z pamięcią współdzieloną).
Nazewnictwo: węzeł, który wywołuje funkcję komunikacji jednostronnej  origin
(węzeł aktywny). Drugi węzeł (target)  węzeł pasywny. W operacji Put() nadawcą
jest węzeł aktywny (source=origin) a odbiorcą pasywny (destination=target).
W operacji Get() nadawcą jest węzeł pasywny (source=target), a odbiorcą aktywny
(destination=origin).
Nie ma gotowego nazewnictwa po polsku, trochÄ™ improwizujemy.
Komunikacja jednostronna  okno
Zanim skorzystamy z procedur komunikacyjnych, musimy na węzle pasywnym
utworzyć okno  wyspecyfikować region pamięci, który będzie brał udział w
operacjach RMA.
Aby to uczynić, korzystamy z funkcji Create():
static MPI::Win MPI::Win::Create(const void* base, MPI::Aint size,
int disp_unit, const MPI::Info& info, const MPI::Intracomm& comm);
Metoda statyczna klasy MPI::Win, reprezentujÄ…cej okno.
Podobnie jak przy otwarciu pliku  metoda zwraca obiekt swojej własnej klasy
(MPI::Win), który reprezentuje utworzone okno.
base  adres pamięci, od którego zaczyna się okno komunikacyjne.
size  rozmiar okna, w bajtach.
(Typ MPI::Aint działa jak int, ale ma większy zakres aby zapewnić poprawne działanie przy size>2GiB).
disp_unit  rozmiar (w bajtach) jednostki informacji w oknie.
(Pozwala na pózniejsze posługiwanie się przesunięciami liczonymi w elementach zamiast w bajtach,
np. podając disp_unit==8 mo\emy operować przesunięciami mierzonymi w double'ach).
info  dodatkowe informacje, które chcemy przekazać implementacji MPI.
comm  komunikator, w obrębie którego pracujemy.
23
Komunikacja jednostronna  okno
Create() jest operacją zbiorową  wszystkie węzły w obrębie komunikatora muszą
jednocześnie zdefiniować okno.
Oczywiście ka\dy z węzłów mo\e umieścić okno w innym miejscu i nadać mu inny
rozmiar. Jeśli nie chcemy udostępniać \adnej pamięci zdalnym procesorom,
podajemy size==0.
W systemach heterogenicznych mo\e się zdarzyć, \e ró\ne węzły podają ró\ną
wartość disp_unit  jest to dopuszczalne i pozwala nadal mierzyć przesunięcia
w elementach, mimo \e rozmiar elementu jest ró\ny na ró\nych węzłach.
Równie\ argument info mo\e być ró\ny na ró\nych węzłach. Przekazujemy w nim
dodatkowe wskazówki dla implementacji, np. mo\emy obiecać \e \aden inny
program nie korzysta z okna w trakcie działania programu, co pozwala uprościć
i przyspieszyć komunikację RMA.
Jeśli nie zamierzamy przeprowadzać ju\ więcej operacji RMA na danym oknie,
powinniśmy je zwolnić wywołując na jego rzecz metodę Free().
void MPI::Win::Free();
Zwolnienie okna jest operacjÄ… zbiorowÄ… i synchronizujÄ…cÄ… (zawiera barierÄ™).
Nale\y zapewnić zakończenie wszystkich operacji RMA na oknie przed jego
zwolnieniem.
Komunikacja jednostronna  Put()
Zapis zdalny realizujemy za pomocÄ… operacji Put():
void MPI::Win::Put(const void* origin_addr, int origin_count,
const MPI::Datatype& origin_datatype,
int target_rank, MPI::Aint target_disp, int target_count,
const MPI::Datatype& target_datatype) const;
Wykonuje przesłanie origin_count danych typu origin_datatype spod adresu
origin_addr w węzle aktywnym pod adres base+target_disp*disp_unit w węzle
pasywnym target_rank.
Efekt jest taki sam, jak nadania na węzle aktywnym wiadomości:
Send(origin_addr,origin_count,origin_datatype,target_rank,jakis_tag,comm)
i odbioru na węzle pasywnym:
Recv(base+target_disp*disp_unit,target_count,target_datatype,
source,jakis_tag,comm).
Rozró\nienie typów danych nadawanych i odbieranych pozwala na konwersję
podczas przesyłania.
Typ danych target_datatype, chocia\ specyfikowany na węzle aktywnym, jest
interpretowany jak na węzle pasywnym. Ma to znaczenie w systemach
heterogenicznych.
24
Uwagi dot. komunikacji jednostronnej  Put()
Bufor odbiorczy musi mieścić się w oknie.
Operacja nieblokująca  inicjuje tylko przesyłanie wiadomości i natychmiast kończy
się. Bufora nadawczego (na węzle aktywnym) nie mo\emy zmieniać dopóki nie
wykonamy operacji synchronizacji.
Podobnie na węzle pasywnym  gwarancję, \e dane dotarły (i legalny dostęp do
bufora odbiorczego) mamy dopiero po wykonaniu operacji synchronizacji.
Niedozwolony jest niezgodny dostęp współbie\ny (co najmniej dwie współbie\ne
operacje, z których co najmniej jedna jest zapisem) do jakiegokolwiek adresu w
oknie. Dotyczy to zarówno operacji RMA jak i operacji lokalnych.
Tłumaczenie: \aden adres w oknie nie mo\e brać jednocześnie udziału w więcej ni\
jednej operacji Put(), jednoczesnej operacji Put() i Get() ani operacji Put() i
jednoczesnym dostępie lokalnym.
Wyjątek od powy\szej reguły: współbie\ne operacje Accumulate() (o nich pózniej).
Zaostrzenie powy\szej reguły: niedozwolony jest jednoczesny dostęp do okna za
pomocą Put() i zapisu lokalnego, nawet gdy odnoszą się do ró\nych adresów
wewnÄ…trz okna. Pozwala to na wydajniejszÄ… implementacjÄ™.
Ka\dy węzeł mo\e legalnie wykonywać operację Put() na samym sobie.
Komunikacja jednostronna  Get()
Odczyt zdalny realizujemy za pomocÄ… operacji Get():
void MPI::Win::Get(void* origin_addr, int origin_count,
const MPI::Datatype& origin_datatype,
int target_rank, MPI::Aint target_disp, int target_count,
const MPI::Datatype& target_datatype) const;
Działa jak Put(), tylko w drugą stronę, czyli dane kopiowane są z okna na węzle
target_rank do bufora origin_addr.
Efekt jest taki sam, jak odebrania na węzle aktywnym wiadomości:
Recv(origin_addr,origin_count,origin_datatype,target_rank,source,jakis_tag,comm)
i nadania na węzle pasywnym:
Send(base+target_disp*disp_unit,target_count,target_datatype,jakis_tag,comm).
25
Komunikacja jednostronna  synchronizacja
Powiedzieliśmy jak przesłać dane (komunikacja), ale skąd wiadomo, \e dane ju\ są
na miejscu?
Do synchronizacji mamy oddzielne funkcje. NajprostszÄ… z nich realizuje metoda
void MPI::Win::Fence(int assert) const;
Wykonanie na oknie Fence() skutkuje zsynchronizowaniem RMA na danym oknie.
Znaczy to, \e sterowanie opuści funkcję Fence() dopiero gdy wszystkie transfery
RMA na danym oknie będą zakończone.
Jest to operacja zbiorowa  wszystkie procesory w grupie zwiÄ…zanej z danym oknem
biorą w niej udział, równie\ węzeł pasywny.
Zatem w tym modelu węzeł pasywny, mimo \e nie bierze (jawnie) udziału w
komunikacji, musi brać udział w synchronizacji.
To jaki zysk i gdzie jednostronność, skoro i tak trzeba zawracać głowę drugiej stronie
przy synchronizacji? Mo\na wymieniać dane, gdy węzeł pasywny jest zajęty i
dopiero potem się synchronizować. Mo\na wykonać wiele operacji RMA a syn-
chronizować się tylko raz. To tylko najprostsza forma synchronizacji, są te\ inne.
argument assert  dodatkowe informacje dla MPI, dla nas nieistotny. Wartość 0 jest
zawsze legalna.
Krótko mówiąc: Fence() gwarantuje, \e operacje na oknie zakończyły się.
Komunikacja jednostronna  synchronizacja
Komunikacja przy u\yciu RMA i synchronizacja z Fence()wygląda na ogół tak:
Create();
Fence();
if(i_am_origin) {
Put();
Put();
Get();
// ... kolejne operacje RMA na oknie w pamieci wezla target
}
if(i_am_target) {
// wykonuj obliczenia, nie zwazaj na operacje RMA
}
Fence(); // tu operacje RMA koncza sie
To najprostszy typ synchronizacji  synchronizujÄ… siÄ™ wszyscy.
Bardziej zaawansowane sposoby  synchronizowanie parami  metody Start(),
Complete(), Post() i Wait()  nie będziemy ich omawiać.
Mo\liwy jest te\ scenariusz, w którym dwa węzły komunikują się przez okno na
trzecim węzle  ten trzeci nie musi wtedy brać udziału w synchronizacji.
26
Komunikacja jednostronna  Accumulate()
Oprócz czytania z okna i pisania do okna, mo\emy wykonywać uaktualnianie danych
w oknie.
Accumulate() pozwala zdalnie wykonać operację na danych wewnątrz okna.
Przydaje się np. przy zliczaniu sumy  wiele procesów dodaje jednocześnie swoje
wkłady do danych zawartych w oknie jednego z procesów.
void MPI::Win::Accumulate(const void* origin_addr, int origin_count,
const MPI::Datatype& origin_datatype,
int target_rank, MPI::Aint target_disp, int target_count,
const MPI::Datatype& target_datatype, const MPI::Op& op) const;
Składniowo ró\ni się od Put() tylko dodatkowym argumentem op, który definiuje
operację, którą będzie wykonywana na elementach okna i elementach przesyłanych
 operacje te poznaliśmy ju\ przy redukcji.
Działa tak samo jak Put(), z tą ró\nicą \e nie nadpisuje danych w oknie, a wykonuje
na nich i danych z bufora operacjÄ™ op.
Inaczej ni\ w redukcji, nie jest dozwolone korzystanie z operacji zdefiniowanych
przez u\ytkownika (tylko predefiniowane operacje).
Inaczej ni\ dla Put(), dozwolone są współbie\ne operacje Accumulate() na jednym
oknie (kilka procesów mo\e jednocześnie uaktualniać dane).
Komunikacja jednostronna  przykład
Spróbujmy przesłać bufor 1000 liczb całkowitych przy u\yciu komunikacji jedno-
stronnej.
Zakładamy, \e są dwa procesory, na #0 wypełniamy bufor, zapisujemy go zdalnie
do #1, tam drukujemy \eby sprawdzić czy się udało.
Ogólny schemat:
Init();
int bufor[1000];
moj_numer=Get_rank();
if(wezel #0) rozmiar_okna = 0; // nie udostepniamy nic
if(wezel #1) rozmiar_okna = 1000; // udostepniamy wszystko
okno = Create(bufor,rozmiar_okna,sizeof(int),...);
if(wezel #0) wypelnij_czyms_bufor();
okno.Fence(); // przygotuj sie do RMA
if(wezel #0) okno.Put(bufor,1000,MPI::INT,1,0,1000,MPI::INT);
if(wezel #1) // rob cokolwiek
okno.Fence(); // zakoncz RMA
if(wezel #1) // wypisz bufor, zeby zobaczyc czy dziala
okno.Free();
Finalize();
27
O czym nie mówiliśmy (i nie powiemy)
Krótki przegląd mo\liwości oferowanych przez MPI, MPI-2,
o których nie mówiliśmy:
Komunikacja nieblokująca (nonblocking communications)  wspomnieliśmy \e istnie-
je, ale nie omawialiśmy, podobnie nieblokujące operacje plikowe (nonblocking I/O),
Operacje plikowe korzystające ze wspólnego wskaznika pozycji w pliku (shared file
pointer I/O).
Zaawansowane operacje zbiorowe.
Typy definiowane przez u\ytkownika,
Zaawansowane zagadnienia zwiÄ…zane z grupami i komunikatorami, definiowanie
topologii,
Dynamiczne zarzÄ…dzanie procesami,
WÄ…tki w MPI.
Drobiazg na koniec
Problem przesyłania obiektów przez MPI.
Przy przesyłaniu wiadomości, zapisie do pliku  specyfikujemy jakiego typu są dane. Mamy do
dyspozycji typy wbudowane i typy definiowane przez u\ytkownika, ale co zrobić jeśli chemy
przesłać np. std::string albo std::vector albo obiekt klasy, którą sami
stworzyliśmy?
Problem w tym, \e przesłanie samych danych zawartych bezpośrednio wewnątrz obiektu to
nie to samo, co skopiowanie obiektu.
Przykładowo:
class macierz {
public:
// operacje na macierzy
private:
double* dane;
};
Przesłanie zawartości obiektu klasy macierz nie załatwia sprawy  kopiuje tylko wskaznik
dane, a nie to, na co on wskazuje. Po stronie odbiorcy nie będzie danych.
Ten sam problem "płytkiej kopii" mamy lokalnie, jeśli nie zadbamy o konstruktor kopiujący:
macierz oryginal;
macierz kopia = oryginal; // kopiuje wskaznik 'dane', nie '*dane'
// zmiany w kopii zmieniaja tez oryginal, poniewaz pamiec wskazywana przez
// 'dane' jest wspólna
28
Drobiazg na koniec
Generalnie kopiowanie "brutalne" obiektów klas, które nie składają się z samych
danych (takie to tzw. POD  plain old data*) albo które same przydzielają pamięć
albo jest nielegalne, albo nie ma sensu, albo jedno i drugie.
class klopot {
public:
klopot() : cos(0);
private:
int cos;
};
klopot oops;
Próba odtworzenia obiektu oops przez "brutalne" skopiowanie sizeof(klopot)
bajtów spod adresu &oops w inne miejsce (na inny obiekt klasy klopot) jest
nielegalna (zachowanie niezdefiniowane)  od momentu dodania konstruktora klasa
klopot przestala byc POD.
W konsekwencji nie mo\na obiektu tej klasy przeslać za pomocą wiadomości, która
kopiowałaby bajty składające się na obiekt oops.
* definicja niezwykle uproszczona. Dokładny opis tego, kiedy typ danych jest POD,
a kiedy nie: http://www.parashift.com/c++-faq-lite/intrinsic-types.html#faq-26.7
Drobiazg na koniec
Podobnie z std::string czy std::vector  to nie sÄ… typy POD, a do tego same
przydzielają sobie pamięć ze sterty:
string tekst = "Co sie stalo?";
cout << "Dlugosc lancucha: " << tekst.length()
<< "\nSizeof(tekst): " << sizeof(tekst) << endl;
Dlugosc lancucha: 13
Sizeof(tekst): 4
Dlatego nie mo\emy łańcuchów i wektorów w prosty sposób przesyłać za pomocą
wiadomości.
Jak sobie radzić?
Jedna metoda  nie u\ywać ich, zastępując tablicami. Ale tablice są złe.
Druga metoda  na czas przesyłania, zamienić na tablice.
Np. std::string daje metodę data(), która zwraca wskaznik do wewnętrznej
reprezentacji łańcucha (const char*)  mo\emy wyjąć tekst.length() bajtów
stamtąd i przesłać. Gorzej po stronie odbiorcy, bo dostajemy tam tablicę znaków 
ale jeśli wcześniej prześlemy długość tekstu oddzielną wiadomością, to mo\emy z
tablicy z powrotem zrobić łańcuch (niestety pisać do *data() nie mo\na). Trzeba
utworzyć nowy łańcuch po stronie odbiorcy, do którego  przez kopiowanie 
wstawimy odebrane znaki. TrochÄ™ roboty.
29
Drobiazg na koniec
Dla std::vector jest odrobinÄ™ Å‚atwiej  mamy gwarancjÄ™, \e kolejne elementy
wektora są umieszczone kolejno w pamięci oraz \e &(wek[0]) jest adresem
pierwszego elementu. Podobnie jak dla łańcucha mo\emy wtedy wyjąć
wek.size()*sizeof(wek[0]) bajtów spod adresu &(wek[0]) i przesłać wiadomo-
ścią. Po stronie odbiorcy będzie więcej roboty  trzeba będzie zrekonstruować
wektor na podstawie odebranych danych.
Najlepiej zamknąć te nieciekawe operacje wewnątrz jakichś funkcji, \eby nie
powtarzać brzydkiego kodu w programie. Przykładowo mo\naby stworzyć funkcje
void sendstring(string& s, int target);
string recvstring(int source);
Jeśli chodzi o klasy stworzone przez nas, to postępujemy jak przy serializacji do pliku
czy do strumienia. Dodajemy klasie dwie operacje: pierwszą, która zamienia klasę na
ciąg bajtów zawierających wszystko, co potrzebne do jej pózniejszego odtworzenia;
druga operacja pozwala na odtworzenie klasy na podstawie ciągu bajtów. Ciągi
bajtów przesyłamy za pomocą wiadomości.
Koniec!
Zaliczenie 31.01, dokładny termin będzie zale\ał od dostępności sal.
Materiały na WWW  ju\ są.
Do zdobycia na zaliczeniu (120-2*N) punktów, gdzie N jest liczbą
wykładów, na których była lista.
Zatem do zdobycia jest 120 pkt, zalicza 60.1 pkt.
Pytania o zró\nicowanym stopniu trudności  od ogólnych, sprawdza-
jących generalne zrozumienie tematu, do bardziej szczegółowych.
TrochÄ™ opisowych, trochÄ™ testowych.
W odró\nieniu od poprzedniego roku, programu na sucho pisać nie
będziemy  będzie mo\na się wykazać na laboratorium w przyszłym
semestrze.
Termin poprawki  do negocjacji.
Nie warto robić ściąg  nie będzie czasu ściągać.
30


Wyszukiwarka

Podobne podstrony:
ar3
AR3

więcej podobnych podstron