630 likes | 744 Views
Algorithmische Skelette. Michael Bruland Michael Hüllbrock Münster, den 12.06.03. Gliederung. Motivation Grundlegende Technologien Algorithmische Skelette Alternativen zur Implementierung mittels Bibliothek Fazit. 1 Motivation (1).
E N D
Algorithmische Skelette Michael Bruland Michael Hüllbrock Münster, den 12.06.03
Gliederung • Motivation • Grundlegende Technologien • Algorithmische Skelette • Alternativen zur Implementierung mittels Bibliothek • Fazit
1 Motivation (1) • „Low-Level-Programmierung“ auf Parallelrechnern häufig erforderlich • Kommunikationsprobleme wie Deadlocks oder Starvation schnell möglich • Einsatz von Programmiersprachen speziell für Parallelrechner erfordert neue Einarbeitung häufig Scheu vor Nutzung neuer Programmiersprachen Lernkurveneffekte
1 Motivation (2) • Portierbarkeit von Programmen erwünscht • Einsatz von Bibliotheken zur Erweiterung bestehender Programmiersprachen • Programmiermuster für Parallelrechner
Gliederung • Motivation • Grundlegende Technologien • Algorithmische Skelette • Alternativen zur Implementierung mittels Bibliothek • Fazit
2 Grundlegende Technologien • Funktionen höherer Ordnung (Higher-Order-Functions) • Parametrisierte Datentypen • Partielle Applikationen • Verteilte Datenstrukturen
2.1 Funktionen höherer Ordnung • Gleichstellung von Funktionen und Werten in funktionalen Sprachen • Funktion mit Funktionen und/oder Ergebnissen als Argumenten • Neustrukturierung von Problemen aufgrund allgemeingültiger Berechnungsschemata möglich, durch Funktionsparameter an Kontext anpassbar • Bsp.: wendet eine Funktion auf alle Werte einer Kollektion an
2.2 Parametrisierte Datentypen • Schablonen von Berechnungsvorschriften • Typen erst durch Übergabe von Parametern in Klassendefinition festgelegt • Überprüfung zur Laufzeit auf Typsicherheit • Implementierung in C++ durch Templates
2.3 Partielle Applikationen • Funktionen, die mit weniger Argumenten angewendet werden können als eigentlich benötigt • Anwendung auf restliche Argumente führt zum selben Ergebnis wie Auflösen der Ursprungsfunktion • Ermittlung der letzten einstelligen Funktion und Rückgabe an weitere Funktionen • Currying als Identifikation mehrstelliger Funktionen mit einstelligen Funktionen höherer Ordnung • Ausgangsfunktion: • Mit Currying:
2.4 Verteilte Datenstrukturen (1) • Kollektionen wie Listen, Arrays oder Matrizen • Verteilung auf die partizipierenden Prozessoren • Aufteilung durch verschiedene Verfahren möglich • Blockpartitionierung • Zyklische Partitionierung • …
2.4 Verteilte Datenstrukturen (2) • Bsp.: Aufteilung einer Matrix auf 4 Prozessoren Prozessor1 Prozessor2 Prozessor3 Prozessor4
2.5 Eigenschaften von C++ für die Nutzung von Skeletten • Polymorphismus • Partielle Applikationen durch Currying ermöglicht • Parametrisierte Datentypen durch Templates template <class E> class DistributedArray{…}
Gliederung • Motivation • Grundlegende Technologien • Algorithmische Skelette • Alternativen zur Implementierung mittels Bibliothek • Fazit
3 Algorithmische Skelette (1) • Programmiermuster für Interaktion und Rechenoperationen zwischen Prozessen • Vorimplementierte, parametrisierte Komponenten • Globale Sichtweise bei Implementierung • Entweder Sprachkonstrukte oder Inhalte in Bibliotheken • Realisierung basiert auf MPI • Abstraktion von „Low-Level-Programmierung“ • Hardwareabhängige Implementierung Portierbarkeit der Programme
3 Algorithmische Skelette (2) • Aufbau der Bibliothek in C++ • Verteilte Datenstruktur ist Klasse • Nutzung der algorithmischen Skelette durch Methodenaufrufe der Klassen
3.1 Klassifikation algorithmischer Skelette • Datenparallele Skelette • Rechenskelette • Kommunikationsskelette • Taskparallele Skelette
3.2 Datenparallele Skelette (1) • Ermöglichen Ortstransparenz • Beherrschung von Datenparallelität • Aufteilung der Daten auf die Prozessoren • Steuerung der Prozessoren, wo welche Daten bearbeitet werden sollen • Bsp.: map oder fold
3.2 Datenparallele Skelette (2) • Bsp.: Geometrisch • Daten werden partitioniert und auf die Prozessoren verteilt • Kommunikation zwischen benachbarten Prozessoren möglich • Ergebnisse werden von einem Prozess geordnet • Anwendung: Vektorberechnung
3.2 Datenparallele Skelette (3) Nach Campbell 1996
3.2.1 Rechenskelette (1) • Arbeiten Elemente einer verteilten Datenstruktur parallel ab • Map: wendet eine Funktion auf Teile einer verteilten Datenstruktur an • Fold: kombiniert alle Elemente einer verteilten Datenstruktur sukzessive mit einer Verknüpfungsfunktion h
3.2.1 Rechenskelette (2) • Bsp.: Fold • Verknüpfungsfunktion h ist E plus(E,E) • A ist eine verteilte (4x4)-Matrix • A.fold(plus) bildet die Summe über alle Elemente
3.2.2 Kommunikationsskelette (1) • Tauschen Partitionen einer verteilten Datenstruktur aus • Realisierung basiert auf MPI • Kein Austausch individueller Nachrichten erlaubt → Probleme wie Deadlock, Starvation etc. werden verhindert
3.2.2 Kommunikationsskelette (2) • Bsp.: A.permutePartition(f) Partition (an Prozessor i) wird an Prozessor f(i) gesendet • Weiteres Kommunikationsskelett: rotate
3.3 Taskparallele Skelette (1) • Verarbeiten Strom von Eingabewerten in Menge von Ausgabewerten • Teilen den Prozessoren Tätigkeiten zu • Tätigkeit kann Funktion oder wiederum Skelett sein Verschachtelung von Skeletten möglich • Kann mit Funktion oder partieller Applikation aufgerufen werden
3.3 Taskparallele Skelette (2) • Bsp.: Farm • Anzahl der Prozessoren ist gleich Anzahl der Worker • Auswahl vom Farmer nicht-deterministisch Quelle: Kuchen 2002
3.3 Taskparallele Skelette (3) • Initial-Prozess template <class O> class Initial: public Process{ public: Initial(O* (*f)(Empty)) void start() }
3.3 Taskparallele Skelette (4) • Farm-Prozess template<class I, class O> class Farm: public Process{ public: Farm(Process& worker, int n) void start() }
3.3 Taskparallele Skelette (5) • Final-Prozess template <class I> class Final: public Process{ public: Final(void(*f)(I)) void start() }
3.3 Taskparallele Skelette (6) • Bsp.: Divide and Conquer • Probleme werden rekursiv in Subprobleme unterteilt • Lösung der Subprobleme erfolgt unabhängig von einander und parallel • Je nach Implementierung • unterschiedliche Anforderungen an Struktur • Unterstützung von konservativer und spekulativer Parallelität • Anwendung: Quicksort, Branch and Bound
3.3 Taskparallele Skelette (7) Nach Campbell 1996
3.3 Taskparallele Skelette (8) • Bsp.: Branch and Bound • Anwendung: Optimierungsprobleme • Vorgehensweise: • n Worker-Kopien durch Konstruktoraufruf • Verknüpfung mit internem Controller • Teillösungen werden vom Controller im Heap gesammelt, falls besser als bestehende • Suboptimale Lösungen werden verworfen
3.3 Taskparallele Skelette (9) • Bsp.: Branch and Bound template <class I> class BranchAndBound:public Process{ public: BranchAndBound(Process& worker, int n, bool (* lth)(I,I), bool (* isSolution)(I)) void start() }
3.4 Das 2-Ebenen-Modell (1) • Modell zur Kombination von task- und datenparallelen Skeletten • äußere Ebene: miteinander verzahnte taskparallele Skelette • innere Ebene: sequentielle Programme und datenparallele Skelette
3.4 Das 2-Ebenen-Modell (2) • Aufgabe: Ein Musikstück soll • von Hintergrundrauschen befreit werden, • Hall hinzugefügt werden, • in ein best. Dateiformat (z.B. wav) konvertiert werden • Lösung mit 2-Ebenen-Modell: • Äußere Ebene : Pipeline • Innere Ebene : sequentielle Bearbeitung oder datenparalleles Skelett
3.5 Zusätzliche Funktionen • Keine Skelette • Flexible Optimierung des Quellcodes • Lokale und globale Sichtweise möglich • Beispiele: • getLocalRows() gibt die Anzahl der lokal verfügbaren Zeilen zurück • getRows() gibt die Anzahl der Zeilen der gesamten verteilten Matrix zurück • isLocal (int i, int j) ist wahr, wenn das Element mit dem Index i,j lokal verfügbar ist • …
3.6 Laufzeitverhalten (1) • Skelette sind ein abstraktes Konstrukt • Wie hoch sind die Performanzeinbußen von Skeletten gegenüber einer „reinen“ MPI Implementierung? • Vergleich von 5 Beispielprogrammen auf einer Siemens hpcLine mit 4 bzw. 16 Prozessoren
3.6 Laufzeitverhalten (2) Quelle: Kuchen 2002
3.6 Laufzeitverhalten (3) Quelle: Kuchen 2002
3.6 Laufzeitverhalten (4) Fazit Laufzeitverhalten: • Skelette sind um den Faktor 1,2 bis 2,1 langsamer als „reines“ MPI • Grund: • Overhead bei der Parameterübergabe • Fehlende Optimierungsroutinen
3.7 Beispiele mit algorithmischen Skeletten • 3.7.1 Gauß‘sches Eliminationsverfahren • 3.7.2 Matrixmultiplikation
3.7.1 Gauß (1) Eliminationsverfahren nach Gauß • Lösungsmenge und Rang einer n x (n+1) Matrix • Hier: zusätzliche Voraussetzung a1,1 ≠ 0 • Idee: Reduzierung der Variabeln durch Addition/Subtraktion der einzelnen Zeilen mit der Pivotzeile
3.7.1 Gauß (2) #include „Skeleton.h“ inline double init(const int a, const int b){ return (a==b) ? 1.0 : 2.0;} inline double copyPivot(const DistributedMatrix<double>&A, int k, int i, int j, double Pij){ return A.isLocal(k,k) ? A.get(k,k) : 0;}
3.7.1 Gauß (3) inline void pivotOp(const DistributedMatrix<double>& Pivot, int rows,int firstrow, int k, double** A){ double Alk; for (int l=0; l<rows; l++){ Alk = A[l][k]; for (int j=k; j<=Problemsize; j++) if (firstrow+1 == k) A[l][j] = Pivot.getLocalGlobal(0,j); else A[l][j] -= Alk * Pivot.getLocalGlobal(0,j);}}
3.7.1 Gauß (4) void gauss(DistributedMartix<double>& A){ DistributedMatrix<double> Pivot(sk_numprocs,Problemsize+1,0.0,sk_numprocs,1); for (int K=0; k<Problemsize; k++){ Pivot.mapIndexInPlace(curry(copyPivot)(A)(k)); Pivot.broadcastPartition(k/A.getLocalRows(),0); A.mapPartitionInPlace(curry(pivotOp)(Pivot,A.getLocalRows(); A.getFirstRow(),k));}}
3.7.1 Gauß (5) int main(int argc, char **argv){ try{ InitSkeletons(argc, argv); DistributedMatrix<double> A(Problemsize,Problemsize+1,&init,sk_numprocs, 1); gauss(A); A.show(); TerminateSkeletons();} catch(Exception&){cout << “Exception” << endl <<flush;} }
3.7.1 Gauß (6) • Ausführung auf 3-Prozessor-Maschine • Pivot.mapIndexInPlace(curry(copyPivot)(A)(k)) kopiert die Pivotzeile (I) in eine p x (n+1), also eine 3x4 Matrix • broadcastPartition übermittelt diese Zeile weiter • Mit mapPartitionInPlace wird auf jeder Partition der Matrix parallel pivotOP ausgeführt
3.7.1 Gauß (7) • die Zeile II wird mit -(9/6 * I) addiert und die Zeile III mit –(3/6 * I) • Weitere Schritte analog
3.7.2 Matrixmultiplikation (1) Matrixmultiplikation • Idee: Multiplikation zweier verteilter Matrizen A und B durch Blockpartitionierung und Aufteilung auf n Prozessoren
3.7.2 Matrixmultiplikation (2) #include „Skeleton.h“ #include „math.h“ inline int negate(const int a) {return –a;} inline int add(const int a, const int b) {return a+b;} template <class C> C sprod (const DistributedMatrix<C>& A, const DistributedMatrix<C>& B, int i, int j, C Cij){ C sum=Cij; for(int k=0;k<A.getLocalRows();k++) sum+=A.getGlobalLocal(i,k))*B.getLocalGlobal(k,j); return sum;}
3.7.2 Matrixmultiplikation (3) template <class C> DistributedMatrix<C> matmult(DistributedMatrix<C> A,DistributedMatrix<C> B){ //assumption: A, B have same square shape A.rotateRows(& negate); B.rotateCols(& negate); DistributedMatrix<C> R(A.getRows(),A.getCols(),0, A.getBlocksInCol(),A.getBlocksInRow()); for(int i=0;i<A.getBlocksInRow();i++){ R.mapIndexInPlace(curry(sprod<C>)(A)(B)); A.rotateRows(-1); B.rotateCols(-1);} return R;}