Vorlesung Rechnerarchitektur 2 Seite 71 MPI-Einführung Parallel Programming: Voraussetzungen im Source-Code für ein MPI Programm: mpi.h includen Die Kommandozeilenparameter des Programms müssen an MPI_Init übergeben werden (die Parameter werden automatisch von dem Run-Script mpirun zugewiesen). MPI_Init muß vor allen anderen MPI-Funktionen aufgerufen werden: MPI_Init(&argc,&argv); Nach der letzten MPI-Funktion muß der Befehl MPI_Finalize aufgerufen werden, um der MPI-Umgebung das Programmende mitzuteilen. Nach dem Befehl MPI_Finalize darf keine andere MPI-Funktion mehr aufgerufen werden: MPI_Finalize(); Jeder Prozess kann mittels MPI-Funktionen feststellen, wieviele Prozesse es insgesamt gibt (MPI_Comm_size) und seine eindeutige Nummer ermitteln (MPI_Comm_rank): MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &size); Der Standard-Kommunikator ist MPI_COMM_WORLD. Es ist jederzeit möglich, neue Kommunikatoren zu erstellen und verschiedene Topologien auf diese Kommunikatoren zu mappen. // A very short MPI test routine to show the involved hosts #include <mpi.h> #include <stdio.h> #include <unistd.h> int main(int argc, char **argv) { int size,rank; char hostname[50]; MPI_Init(&argc,&argv); MPI_Comm_rank(MPI_COMM_WORLD, &rank); //Who am I? MPI_Comm_size(MPI_COMM_WORLD, &size); //How many processes? gethostname (hostname, 50); printf ("Hello World! I'm number %2d of %2d running on host %s\n", rank, size, hostname); MPI_Finalize(); return 0;
Vorlesung Rechnerarchitektur 2 Seite 72 Kompilieren/Starten eines MPI-Programmes (OpenMPI) Einlesen des Source-Scriptes für MPI in bash startup script einfügen: node00 # echo "source /opt0/administratio/common/ompi_path" >> ~/.bashrc Beim nächsten einloggen erscheint: Setting up OpenMPI-enviroment (v1.2.4) for Linux (x86_64) Kompilieren des Programmes (anstatt von gcc): node00 # mpicc -Wall prog1.c -o prog1 Kompilieren eines C++ Programmes (anstatt von g++): node00 # mpic++ -Wall test.cpp -o test oder node00 # mpicxx -Wall test.cpp -o test Starten des Programmes: node00 # mpirun -np 4 --machinefile machines prog1 Beachte: Angabe der Anzahl Prozesse mit dem Parameter -np <Anzahl> Das machinefile gibt die Knoten an auf denen die jobs laufen sollen Ohne Angabe eines machinefiles werden jobs nur auf dem lokalen Knoten gestartet MPI basiert auf ssh, einloggen ohne Angabe des Passworts ist mit ssh-keygen: node00 # ssh-keygen node00 # cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys Machinefiles enthalten Knotennamen und optional die Anzahl der (maximalen) Prozesse, die auf diesem Knoten gestartet werden soll. Beispiel machinefile: node00 slots=2 node01 slots=2 node02 slots=2 node03 slots=2 Mit diesem Machinefile: karibik # mpirun -np 8 -machinefile machinefile prog1 Hello World! I m number 0 of 4 running on host node00 Hello World! I m number 2 of 4 running on host node01 Hello World! I m number 1 of 4 running on host node00 Hello World! I m number 3 of 4 running on host node01 Beachte: So kann man z.b. SMP-Rechner besonders gut ausnutzen, indem man in dem machinefile den entsprechenden Node-Name mehrmals einträgt. Der eigentliche mpirun Befehl kann auch auf einem unbeteiligten Rechner erfolgen
Vorlesung Rechnerarchitektur 2 Seite 73 Weitere OpenMPI Details & Kommandos OpenMPI ist Komponenten-orientiert Komponente für einen Zweck kann zur Laufzeitausgewählt werden mpirun Parameter Komponenten können zum Beispiel gewählt werden für folgende Kategorien: Bit-Transfer-Layer Message-Transfer-Layer processor und memory affinity (NUMA support!) Kollektive Operationen (Broadcast, Barrier,...) u.a. Das ompi_info Kommando zeigt Details der OpenMPI Installation an, z.b. alle installierten und selektierbaren Komponenten und ihren jeweiligen Typ. Beispiele: mpirun -mca btl tcp,self... Das Programm verwendet für das Bit-Transfer-Layer TCP/IP bzw. gemeinsamen Speicher (self) für Kommunikation mit sich selbst. Auf Karibik-Knoten korrespondiert dies mit der Benutzung von Gigabit-Ethernet. mpirun -mca mtl mx,self... Das Programm verwendet Myrinext Express (kurz: MX) zur Kommunikation und damit das dedizierte schnelle Myrinet Netzwerk von Karibik. MX ist in OpenMPI im Message-Transfer-Layer eingebunden (MX übernimmt "mehr" Arbeit von MPI als etwa TCP). MX ist das default-netzwerk auf Karibik.
Vorlesung Rechnerarchitektur 2 Seite 74 Kommunikationsverfahren (communication modes) Prinzipiell wird zwischen folgenden Verfahren unterschieden: Synchronous Send/Receive: Bevor von A an B gesendet wird, muß B einen entsprechenden synchronous Receive gestartet haben. Erst dann wird die Nachricht versendet. Nach dem Aufruf der Send-/Receive-Funktion ist die Kommunikation garantiert abgeschossen und der Buffer kann wieder verwendet werden. Blocking Send/Receive: Ein blocking Send wird sofort ausgeführt, ohne auf den zugehörigen Receive zu warten. Beim Rücksprung wurde die Nachricht garantiert versandt, der Buffer kann wieder verwendet werden. Ein blokking Send sagt nichts über den Empfang der Nachricht aus. Non-blocking Send/Receive: Ein non-blocking Send wird ebenfalls sofort ausgeführt, allerdings kann der Rücksprung sofort nach der Benachrichtigung des Systems über den Versand der Nachricht erfolgen. Daher ist der Buffer unter Umständen noch mit der Nachricht belegt. Man darf den Buffer erst wieder verwenden, wenn man über entsprechende Funktionen überprüft hat, daß die Nachricht versandt worden ist. Buffered-Send (MPI_BSend, MPI_Ibsend): Beim buffered-send muss der User explizit Speicherplatz im System reservieren (MPI_Buffer_attach, MPI_Buffer_detach) Standard-Send (MPI_Send, MPI_Isend): Entspricht synchronous- oder buffered-send, je nach Implementierung Ready-Send (MPI_RSend, MPI_Irsend): Hier muss der Anwender dafür sorgen, daß der entsprende Receive schon gestartet ist. So kann man Overhead durch Warten vermeiden und sofort mit dem Versenden beginnen. Sollte ein Ready-Send ohne Receive gestartet werden, ist das Verhalten undefiniert. Überprüfung des Status von non-blocking Send/Receive: Mittels der Funktionen MPI_Wait und MPI_Test kann man den Status einer non-blocking Kommunikation überprüfen. Jede non-blocking Kommunikation muss/sollte mit diesem Completion-Check abgeschlossen werden, um eventuell belegte Systemresourcen wieder freizugeben (implementationsabhängig).
Vorlesung Rechnerarchitektur 2 Seite 75 Verschiedene Buffer-Typen in MPI A M Process P User buffer only B Process Q A M A M Process P S B Process Q Usage of a system buffer S T B Process P Process Q Usage of a user-level buffer T User buffer only: Einfach für synchronous-send, beim asynchronous-send ohne Zwischenspeicher nicht möglich. Usage of a system buffer S: Alternative für asynchronous-send, hier wird die Nachricht zuerst in einen Zwischenspeicher kopiert. Probleme: Zusätzlicher Overhead, Nachricht zu groß für Zwischenspeicherung (Maximal verfügbarer Systemspeicher nicht zugänglich für den User). Usage of a user-level buffer T: In diesem Falle sorgt der User für einen entsprechend großen Zwischenspeicher. Da jetzt der User weiß wieviel Speicher verfügbar ist, kann er die Nachricht gegebenenfalls in kleinere Stücke aufteilen. Ganz anderer Ansatz: Verwendung von One-sided-Commmunication: Remote-Direct-Memory-Access (RDMA). Zu vergleichen mit DMA innerhalb eines Systems, nur auf einen entfernten Rechner. Der entfernte Rechner muss vor der Kommunikation ein Memory-Window freigeben, in dem bestimmte Rechner lesen/schreiben dürfen. Andere Kommunikationspartner können dann mit Put/Get-Operationen auf diesen Speicherbereich zugreifen.
Vorlesung Rechnerarchitektur 2 Seite 76 Beispiel für eine Kommunikation mit MPI //// A very short MPI test routine to test message passing #include <mpi.h> #include <stdio.h> int main(int argc, char **argv) { int i, size, rank, signal; MPI_Status status; double starttime, endtime; //Initialisation of MPI MPI_Init (&argc, &argv); MPI_Comm_rank (MPI_COMM_WORLD, &rank); MPI_Comm_size (MPI_COMM_WORLD, &size); //Master: send to every proc and wait for answer if (rank == 0) { signal = 666; starttime=mpi_wtime(); for (i=1; i<size; i++) { MPI_Send(&signal, 1, MPI_INT, i, 0, MPI_COMM_WORLD); printf ("%d: Sent to %d\n", rank, i); MPI_Recv (&signal, 1, MPI_INT, i, 0, MPI_COMM_WORLD, &status); printf ("%d: Received from %d\n", rank, i); endtime=mpi_wtime(); printf ("%d: Time passed: %f\n", rank, endtime-starttime); //Clients: wait for message from master and send back else { MPI_Recv (&signal, 1, MPI_INT, MPI_ANY_SOURCE, 0, MPI_COMM_WORLD, &status); printf ("%d: Received from %d\n", rank, status.mpi_source); MPI_Send(&signal, 1, MPI_INT, status.mpi_source, 0, MPI_COMM_WORLD); printf ("%d: Sent to %d\n", rank, status.mpi_source); //Deinitialisation of MPI MPI_Finalize(); return 0;