Wstęp do MPI-2 Jacek Dziedzic FTiMS, Politechnika Gdańska Gdańsk, 2006-2018 Algorytmy rozproszone 2017/2018 v1.14
Pracujemy w systemie równoległym o architekturze MIMD, z pamięcią rozproszoną... N E T W O R K wiadomość #718 od: węzeł #1 do: węzeł #3 długość: 8192 B dane:...... serwer plików pamięć masowa Pamięć rozproszona każdy węzeł pracuje 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.
Pracujemy w systemie równoległym o architekturze MIMD, z pamięcią rozproszoną plik1... N E T W O R K user@node2:~> ls plik1 plik2 plik3 user@node2:~> user@node3:~> ls plik1 plik2 plik3 user@node3:~> serwer plików pamięć masowa plik2 plik3 Dostęp do pamięci masowej zapewnia na ogół serwer plików, w sposób przezroczysty dla użytkownika.
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ć do 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. W 2017: praktycznie wszystkie.
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 chcielibyś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. Łatwe 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 jeśli pliki mają po 4 GB?). Zapis do wielu plików jest niewydajny (seek, fragmentacja). storage
Bez równoległego we/wy metoda 2 Można zebrać dane na jednym węźle 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ęźle 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.
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). Dobry podział pracy (wszystkie węzły zajęte). Efektywne (MPI i SO mogą optymalizować zapisy i odczyty). Podobnie dla odczytu. pamięć masowa
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 Trzy metody: przesuń wskaźnik pliku i czytaj, czytaj-od-miejsca (read_at), widoki plików. node #(N-1)
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ą (ważne na laborce).
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óźniejszych operacji na pliku. Na ogół nie będziemy przekazywali żadnych dodatkowych informacji przekazujemy wówczas MPI::INFO_NULL.
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ę (na tym wykładzie 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.
Równoległe we/wy uwagi dot. odczytu Wskaźniki pozycji pliku są niezależne na wszystkich węzłach. Oznacza to, że odczyt na jednym węźle nie ma wpływu na stan pliku (w szczególności na wskaźnik pozycji pliku) na pozostałych węzłach: plik A B Po tym jak węzeł A dokonał odczytu jego wskaźnik pozycji pliku przesuwa się, ale wskaźnik pozycji na B pozostaje niezmieniony. plik A B
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...) Odczyt w stylu C++ std::read(... ile bajtów...) Zapis w stylu C++ std::write(... ile bajtów...) ale Równoległe we/wy uwagi dot. odczytu 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).
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 wskaźnik 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 wskaźników pozycji pliku (shared file pointers) nie będziemy się nimi zajmować.
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 wskaźnika 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 bajtów (wyjątkowo nie elementów) o którą przesuwamy wskaźnik 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 wskaźnik po pierwszych 10 bajtach (0..9) mój_plik.seek(10, MPI_SEEK_SET); // cofnij się o 6 bajtó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 <mpi.h> #include <iostream> using namespace std; int main(int argc, char** argv) { MPI::Init(argc,argv); węzeł #0 węzeł #1 węzeł #2 k*porcja...... węzeł węzeł #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*sizeof(double),mpi_seek_set); plik.read(bufor,porcja,mpi::double); plik.close(); } MPI::Finalize();
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 wskaźnika 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ć. 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. widok na węźle #0 plik widok na węźle #2 Każdy węzeł widzi tylko część pliku. Widoki można zmieniać zdejmować i nakładać nowe. widok na węźle #1 widok na węźle #3
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ą. widok na węźle #0 widok na węźle #1 plik widok na węźle #2 widok na węźle #3 W szeregowym we/wy każdy węzeł musiałby wczytać cały plik, po czym wyrzucić ¾ danych (albo wykonać mnóstwo operacji przemieszczenia). Gdy korzystamy z widoków MPI-2, implementacja wie, że plik trzeba odczytać tylko raz, po czym automagicznie 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 takich będzie dużo. niedostępne na żadnym z węzłów plik nagłówek widok na węźle #0 widok na węźle #1 widok na węźle #2 widok na węźle #3 Każdy z węzłów widzi tylko to, co jest dla niego istotne, nie wie co dzieje się w pozostałych częściach pliku, nie musi jawnie przeskakiwać dziur widzi należną mu część jak mniejszy, ciągły plik. Bardzo wygodne, wydajne. Jedyna trudna część definiowanie (nakładanie) widoku.
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 dzięki temu może mieć 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. nagłówek plik - etype, np. MPI::DOUBLE - filetype
Widoki uwagi Po nałożeniu widoku wskaźnik 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 wskaźnik 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ęźle. 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).
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. 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); Widoki przykład widok w proc 0 widok w proc 1 widok w proc 2
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 przy MPI_MODE_SEQUENTIAL.
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 }
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ądź 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++.
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).
MPI-2: współoperatywność 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 w operacjach we/wy 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 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 rozpoczyna zapis A koniec zapisu na A A wysyła wiadomość do B, że dane są gotowe A B odbiera wiadomość i zaczyna odczyt B czas bufory systemowe... ale w tym momencie danych nie ma jeszcze fizycznie na dysku dysk
2. 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. Równoległe we/wy synchronizacja i spójność 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.