Paralleles Rechnen Konzepte und Anwendungen im Data Mining
Transcription
Paralleles Rechnen Konzepte und Anwendungen im Data Mining
Paralleles Rechnen Konzepte und Anwendungen im Data Mining Stefan Wissuwa 1. Dezember 2008 Thesis zur Erreichung des Grades Master of Science (M.Sc.) in Wirtschaftsinformatik Hochschule Wismar - Fakultät für Wirtschaftswissenschaften Eingereicht von: Stefan Wissuwa, Dipl. Wirt.-Inf. (FH) Erstbetreuer: Jürgen Cleve, Prof. Dr. rer. nat. Zweitbetreuer: Uwe Lämmel, Prof. Dr.-Ing. Inhaltsverzeichnis 1 Einleitung 1.1 Motivation . . . . . . 1.2 Anwendungsgebiete . 1.3 Inhalt der Arbeit . . . 1.4 Begriffsklärung . . . I . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen der parallelen Datenverarbeitung 5 2 Stufen der Parallelisierung 6 3 Parallele Architekturen 3.1 Flynnsche Klassifizierung . . . . . . . . . . 3.2 Speichermodelle . . . . . . . . . . . . . . 3.2.1 Rechner mit gemeinsamem Speicher 3.2.2 Rechner mit verteiltem Speicher . . 3.3 Prozesse / Threads . . . . . . . . . . . . . 3.4 Client-Server-Architekturen . . . . . . . . 3.5 Cluster-Computing . . . . . . . . . . . . . 3.6 Grid-Computing . . . . . . . . . . . . . . . 4 Parallelisierungsebenen 4.1 Parallelität auf Instruktionsebene 4.2 Parallelität auf Datenebene . . . 4.3 Parallelität in Schleifen . . . . . 4.4 Parallelität auf Funktionsebene . 1 1 2 3 4 . . . . . . . . . . . . 5 Parallele Programmiermodelle 5.1 Darstellung . . . . . . . . . . . . . . 5.2 Strukturierung . . . . . . . . . . . . . 5.3 Datenverteilung und Kommunikation . 5.3.1 Broadcast . . . . . . . . . . . 5.3.2 Scatter . . . . . . . . . . . . 5.3.3 Gather . . . . . . . . . . . . . 5.3.4 Reduktion . . . . . . . . . . . 5.4 Synchronisation . . . . . . . . . . . . 5.4.1 Kritische Abschnitte . . . . . 5.4.2 Barrieren . . . . . . . . . . . 5.4.3 Locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 8 9 10 10 11 11 . . . . 13 13 13 14 14 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 16 17 17 17 18 18 19 19 20 20 20 6 Einflussfaktoren Paralleler Programme 6.1 Parallele Skalierbarkeit . . . . . . . . . 6.1.1 Speedup . . . . . . . . . . . . . 6.1.2 Amdahl’sches Gesetz . . . . . . 6.1.3 Gustafson-Gesetz . . . . . . . . 6.1.4 Karp-Flatt-Metrik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 21 21 21 22 24 6.2 6.3 6.4 II Load Balancing und Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . Lokalität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Speichersynchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bibliotheken und Systeme 7 Unified Parallel C 7.1 Verfügbarkeit . . . . . . 7.2 Parallelisierungskonzept 7.3 Speichermodell . . . . . 7.4 Synchonisation . . . . . 7.5 Globale Operatoren . . . 25 25 26 28 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 29 29 30 32 32 8 Message Passing Interface 8.1 Verfügbarkeit . . . . . . . 8.2 Parallelisierungskonzept . 8.3 Speichermodell . . . . . . 8.4 Synchronisation . . . . . . 8.5 Globale Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 34 34 34 34 34 . . . . . 35 35 35 35 36 37 . . . . 38 38 38 38 38 . . . . . . . . . . 40 40 40 40 42 42 43 45 45 46 48 9 OpenMP 9.1 Verfügbarkeit . . . . . . 9.2 Parallelisierungskonzept 9.3 Speichermodell . . . . . 9.4 Synchronisation . . . . . 9.5 Globale Operatoren . . . 10 Andere 10.1 BOINC . . . . . . . . . 10.2 BLAS . . . . . . . . . . 10.3 PThreads . . . . . . . . 10.4 Parallel Virtual Machine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Vergleichende Betrachtung 11.1 Implementierung . . . . . . . . . . . . 11.1.1 Matrizenmultiplikation Seriell . 11.1.2 Matrizenmultiplikation UPC . . 11.1.3 Matrizenmultiplikation MPI . . 11.1.4 Matrizenmultiplikation OpenMP 11.2 Performance . . . . . . . . . . . . . . . 11.3 Bewertung . . . . . . . . . . . . . . . . 11.3.1 UPC . . . . . . . . . . . . . . . 11.3.2 MPI . . . . . . . . . . . . . . . 11.3.3 OpenMP . . . . . . . . . . . . III Parallelisierung im Data Mining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 12 Data Mining Grundlagen 12.1 Ablaufmodell . . . . . . . . . . . . 12.2 Klassifikation der Verfahren . . . . 12.3 Künstliche Neuronale Netze . . . . 12.3.1 Feed-Forward-Netze . . . . 12.3.2 Selbstorganisierende Karten . . . . . 50 50 51 53 53 54 13 Parallele Algorithmen 13.1 Serielle Selbstorganisierende Karte . . . . . . . . . . . . . . . . . . . . . . . . 13.2 Parallele Selbstorganisierende Karte . . . . . . . . . . . . . . . . . . . . . . . 13.3 Performance Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 56 57 59 14 Parallele Modelle 14.1 Hierarchische Kohonen-Karten . . . . . . . . 14.1.1 Konzept . . . . . . . . . . . . . . . . 14.1.2 Umsetzung . . . . . . . . . . . . . . 14.1.3 Testaufbau . . . . . . . . . . . . . . 14.1.4 Vergleich der Modellqualität . . . . . 14.1.5 Vergleich der Rechengeschwindigkeit 14.2 Feed-Forward-Netze . . . . . . . . . . . . . 14.2.1 Konzept . . . . . . . . . . . . . . . . 14.2.2 Umsetzung . . . . . . . . . . . . . . 14.2.3 Vergleich der Modellqualität . . . . . 14.2.4 Vergleich der Rechengeschwindigkeit 61 61 61 64 67 70 72 73 73 73 74 75 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IV Zusammenfassung und Ausblick 76 V 79 Ehrenwörtliche Erklärung VI Anhang 80 A Schnittstellen und Bibliotheken A.1 UPC Konsortium . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.2 Compilerübersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 81 82 B Messwerte B.1 Messwerte Matrizenmultiplikation B.2 Messwerte SOM . . . . . . . . . B.3 Messwerte Feed-Forward-Netz . . B.4 Inhalt der CD . . . . . . . . . . . 84 84 86 88 89 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Algorithmenverzeichnis 1 2 3 4 5 Matrizenmultiplikation Seriell . . . Matrizenmultiplikation MPI Global SOM Seriell . . . . . . . . . . . . . Parallele SOM mit OpenMP . . . . Hierarchische SOM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 43 57 59 68 Abbildungsverzeichnis 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Gemeinsames Speichermodell UMA . . . . . . . . . . . . . Gemeinsames Speichermodell NUMA . . . . . . . . . . . . Verbindungsnetzwerke . . . . . . . . . . . . . . . . . . . . Effiziente Broadcast-Operation . . . . . . . . . . . . . . . . Effiziente Akkumulations-Operation . . . . . . . . . . . . . Parallele Skalierbarkeit nach Amdahl . . . . . . . . . . . . . Parallele Skalierbarkeit nach Amdahl . . . . . . . . . . . . . Speedup Amdahl vs. Gustafson . . . . . . . . . . . . . . . . Row- vs. Column-first Ordering . . . . . . . . . . . . . . . Message-Passing . . . . . . . . . . . . . . . . . . . . . . . OpenMP Fork-Join-Modell . . . . . . . . . . . . . . . . . . Performance UPC . . . . . . . . . . . . . . . . . . . . . . . Performance OpenMP . . . . . . . . . . . . . . . . . . . . Performance MPI . . . . . . . . . . . . . . . . . . . . . . . Performance MPI vs. MPI Global . . . . . . . . . . . . . . Performance OpenMP vs. MPI Global . . . . . . . . . . . . Performance OpenMP vs. MPI Global . . . . . . . . . . . . CRISP-DM Phasenmodell . . . . . . . . . . . . . . . . . . Rechenzeit SOM OpenMP für kleine Karten . . . . . . . . . SOM OpenMP Rechenzeiten und Speedup nach Kartengröße U-Matrix einer Kohonen-Karte . . . . . . . . . . . . . . . . Aufteilung einer SOM . . . . . . . . . . . . . . . . . . . . Interpolation der Gewichte . . . . . . . . . . . . . . . . . . Clusterqualität vs. Lernrate / Kartengröße . . . . . . . . . . U-Matrix Seriell . . . . . . . . . . . . . . . . . . . . . . . . U-Matrix Hierarchisch . . . . . . . . . . . . . . . . . . . . Clusterqualität . . . . . . . . . . . . . . . . . . . . . . . . . Speedup SOM Parallel / Seriell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 8 9 18 19 22 23 24 26 33 36 44 45 45 46 47 47 51 60 60 62 63 63 69 71 71 71 72 Tabellenverzeichnis 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Linpack Top-5 Supercomputer Stand 06/2008 . . . . . . . . . . . . . . Übersicht UPC-Compiler . . . . . . . . . . . . . . . . . . . . . . . . . OpenMP-fähige Compiler . . . . . . . . . . . . . . . . . . . . . . . . Matrizenmultiplikation Seriell . . . . . . . . . . . . . . . . . . . . . . Matrizenmultiplikation UPC . . . . . . . . . . . . . . . . . . . . . . . Matrizenmultiplikation OpenMP . . . . . . . . . . . . . . . . . . . . . Matrizenmultiplikation MPI Master-Worker . . . . . . . . . . . . . . . Matrizenmultiplikation MPI Global . . . . . . . . . . . . . . . . . . . Clusterqualität (seriell) in Abhängigkeit von Lernrate und Kartengröße . Clusterqualität (parallel) in Abhängigkeit von Lernrate und Kartengröße Rechenzeit in Abhängigkeit von Kartengröße und Parallelisierungsgrad Berechnungszeiten Parallele SOM mit OpenMP . . . . . . . . . . . . . Erkennungsrate Teilklassifikatoren . . . . . . . . . . . . . . . . . . . . Rechenzeiten und Erkennungsrate n-Fach vs. binär nach Netzgröße . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 82 83 84 84 85 85 86 86 87 87 88 88 89 1 Einleitung 1.1 Motivation Parallelrechner, Mehrkernprozessoren, Grid-Computing und seit geraumer Zeit Cloud-Computing sind Schlagworte, die immer dann genannt werden, wenn es um enorme Rechenleistung von Computern geht. Doch um was handelt es sich dabei genau? Die Entwicklung schnellerer Prozessoren erfolgte bisher zu einem signifikanten Anteil durch immer stärkere Miniaturisierung der Schaltkreise, was eine höhere Zahl an Transistoren pro Chip und höhere Taktraten erlaubte. Die Anzahl der Transistoren verdoppelt sich etwa alle 18 Monate. Dieser Zusammenhang wurde 1965 empirisch durch Gordon Moore festgestellt und wird daher auch als Moore’sches Gesetz bezeichnet1 . Obwohl der dafür notwendige technologische Aufwand mit der Zeit immer größer wurde, wird diese Steigerungsrate auch in nächster Zukunft beibehalten werden können, bis die physikalische Grenze erreicht ist. Eine stetige Steigerung der Taktraten stellt sich bereits als sehr viel schwieriger heraus, da die damit verbundene Abwärme des Prozessors nur sehr schwer zu handhaben ist und bereits Werte erreicht hat, die - relativ zur Oberfläche - etwa der Heizleistung einer Herdplatte gleichkommen. Statt die Geschwindigkeit nur durch höhere Taktraten zu steigern, wurde die Komplexität der Prozessoren so weit erhöht, dass bereits mehrere Prozessorkerne auf einem Chip zusammengefasst werden und somit das, was man unter einem Prozessor-Chip versteht, selbst einen kleinen Parallelrechner bildet.2 Doch auch eine stetige Erhöhung der Anzahl der Prozessorkerne ist nicht unbedingt sinnvoll. Da Prozessoren heute um ein Vielfaches schneller Arbeiten als Daten vom Hauptspeicher zum Prozessor übertragen werden können, sind schnelle, teure und damit kleine Zwischenspeicher - Caches - notwendig. Da jeder Prozessorkern einen eigenen Cache besitzt, aber für alle Prozessorkerne ein konsistenter Speicherinhalt sichergestellt sein muss, sind Synchronisationsmechanismen notwendig, deren Komplexität mit der Zahl der Prozessoren steigt. Da eine Synchronisation zudem Zeit kosten, schmälert dies den Geschwindigkeitsgewinn durch zusätzliche Prozessorkerne. In bestimmten Fällen können zusätzliche Prozessoren sogar dazu führen, dass ein Programm langsamer wird3 . Diese vielfältigen Abhängigkeiten, die so auch in größerem Maßstab für Rechennetze gelten, führen dazu, dass Parallelisierung auf technischer wie auch auf softwaretechnischer Seite keine triviale Aufgabe ist. Parallelrechner waren bis vor wenigen Jahren hauptsächlich in der wissenschaftlichen Simulation im Einsatz. Sie erlaubten es erstmals, komplexe Phänomene, deren physische Analyse zu aufwändig, teuer oder gefährlich ist, am Computer zu simulieren. Rund um diese Simulationstechnik hat sich ein eigener Wissenschaftszweig etabliert, der unter dem Begriff Computational Science bekannt ist. Beginnend mit der Einführung der ersten Pentium Dual-Core Prozessoren für den ConsumerMarkt durch die Firma Intel Corp., sind heute kaum noch aktuelle PCs auf dem Markt zu finden, die nicht mindestens einen Doppelkern-Prozessor enthalten. Obwohl die jeweiligen Ziele, für die diese Technologien entwickelt werden, höchst unterschiedlich sind, gleichen sie sich jedoch in einem Punkt: die Steigerung der Leistungsfähigkeit von Computersystemen erfolgt nicht hauptsächlich durch höhere Taktrate, sondern durch Parallelisierung. Dies wird dazu führen, 1 Vgl.: [R AUBER 2007] S.100 Vgl.: [R AUBER 2008] S.6ff. 3 Vgl.: Linux-Magazin, Ausgabe 11/2008, MySQL 2 -1- dass verstärkt parallele Programmiertechniken in der Softwareentwicklung eingesetzt werden müssen, um die Leistung auch nutzen zu können. Doch auch durch Parallelisierung können nicht unbegrenzt nutzbar höhere Rechengeschwindigkeiten erzielt werden. Die Leistungsangaben für Supercomputer mögen einen in Erstaunen versetzen, jedoch ist dabei zu beachten, dass diese Leistung nur für sehr spezielle Programme auch wirklich genutzt werden kann - nämlich für Programme oder Algorithmen, die sich überhaupt parallelisieren lassen. Die 1,026 Peta-Flop/s des derzeit weltweit schnellsten Supercomputers4 werden nur durch Kombination von über hunderttausend Mehrkern-Prozessoren erreicht. Diese Leistung zur Lösung einer Aufgabe bündeln zu wollen bedeutet, die Aufgabe in über hunderttausend separate Teilaufgaben zu zerlegen, was weder immer möglich noch stets sinnvoll ist5 . Eine Bürosoftware wird auf einem Parallelrechner nicht unbedingt schneller ausgeführt werden, da sie meist nur einen einzigen der vorhandenen Prozessoren nutzen kann. Eine komplexe physikalische Simulation hingegen kann enorm von der Verteilung auf mehrere Prozessoren profitieren, sofern der Algorithmus diese Aufteilung zulässt. Data-Mining ist ein weiteres Anwendungsgebiet, für das Parallelisierung vorteilhaft sein kann, da sehr große Datenmengen zu verarbeiten sind und dafür komplexe Algorithmen eingesetzt werden. Die langen Rechenzeiten, die für das Erstellen eines Data-Mining-Modells notwendig sind, behindern eine interaktive und intuitive Arbeitsweise bei der Exploration von Datenmengen. Um Zeit zu sparen, können mehrere unterschiedliche Modelle nach dem Versuch-und-IrrtumPrinzip parallel berechnet werden. Dies erfordert jedoch große Planungssorgfalt und führt in der Regel zu vielen überflüssigen Berechnungen und damit zur Vergeudung von Rechenzeit. Ein Ansatz ist, die Gesamtrechenzeit eines Experiments zu verringern, in dem sowohl der DataMining-Prozess als auch einzelne Algorithmen parallel ausgeführt werden. Vor dem Hintergrund, dass selbst aktuelle, kostengünstige PCs häufig Mehrkern-Prozessoren enthalten, ist eine Parallelisierung umso interessanter. 1.2 Anwendungsgebiete Für parallele Rechnerarchitekturen lassen sich prinzipiell zwei Anwendungsgebiete unterscheiden:6 Das High-Availability-Computing dient der Zurverfügungstellung ausfallsicherer Dienste, beispielsweise für Datenbanken oder Web-Server, indem alle anfallenden Aufgaben nach Bedarf auf einzelne Knoten verteilt werden und dadurch auch der Ausfall einzelner Knoten kompensiert werden kann. Das High-Performance-Computing dient vor allem der Bündelung von Rechenleistung, um eine einzelne Aufgabe entweder schneller oder sehr viel genauer zu lösen7 . Dies ist auch das Anwendungsgebiet paralleler Algorithmen und Inhalt dieser Arbeit. Es existieren eine Reihe von wissenschaftlichen Anwendungen, die in großem Maßstab auf Parallelisierung setzen und die durch die Art, diese zu realisieren, in der Öffentlichkeit große 4 Siehe Tabelle 1 auf Seite 12. Siehe Kapitel 6 auf Seite 21. 6 Vgl.:[BAUKE 2006] S. 31ff. 7 Zur Unterscheidung siehe Kapitel 6. 5 -2- Popularität erlangt haben. Die wohl bekanntesten Vertreter gehören zur Gruppe der auf dem BOINC-Framework8 basierenden at-Home-Projekte, bei denen durch ans Internet angeschlossenen PCs ein Parallelrechner nach dem Master-Worker-Prinzip aufgebaut wird. Da hierbei auch sehr viele Privat- und Bürorechner zum Einsatz kommen, werden diese Verfahren auch als Desktop-Grid-Computing bezeichnet. Die bekanntesten Projekte sind Seti@Home9 und Folding@Home10 . Seti@Home dient der Analyse von Radiosignalen auf Muster, die von einer extraterrestrischen Intelligenz stammen könnten. Obwohl bisher erfolglos, ist das Projekt wohl unbestritten der populärste Vertreter seiner Art, vor allem unter Privatpersonen. Diese stellen Rechenzeit zur Verfügung, die sie selbst nicht nutzen, indem sie eine spezielle Software - in der ursprünglichen Version ein Windows-Bildschirmschoner - installieren, der Datenpakete von einem Server lädt und das Ergebnis der Berechnung zurückschickt. Folding@Home funktioniert auf die gleiche Weise, nur ist das Ziel die Bestimmung der Tertiärstruktur11 von Proteinen. Da die Funktion eines Proteins nicht nur von dessen chemischer Zusammensetzung - die durch die DNA codierte Aminsosäuresequenz - abhängt, sondern auch von dessen räumlicher Struktur. Die Ausbildung der dreidimensionalen Struktur aus einer langen Kette von Aminosäuren wird als Faltung bezeichnet, wobei es mehrere Varianten gibt. Nur die physiologisch korrekte Form kann die ihr zugedachte Funktion erfüllen, falsch gefaltete Proteine haben eine reduzierte, keine oder im schlimmsten Fall pathogene Funktion. Es wird vermutet, dass Krankheiten wie BSE oder die Kreutzfeldt-Jakob-Krankheit durch falsch gefaltete Proteine (Prionen) hervorgerufen werden. Neben der Erforschung, wie und unter welchen Bedingungen der Faltungsprozess genau funktioniert, ist die Bestimmung der Tertiärstruktur ein wichtiger Schritt, wenn es gilt, die Funktion eines neu entdeckten Gens des dadurch kodierten Proteins herauszufinden. Weitere, jedoch weniger populäre Projekte sind zum Beispiel Docking@Home12 zur Untersuchung der Molekülbindungen zwischen Liganden und Proteinen in der Biochemie sowie NQueens@Home13 zur Lösung des N-Damen-Problems auf Feldern größer als 26x26 Einheiten. 1.3 Inhalt der Arbeit Diese Arbeit befasst sich mit den Anwendung von Parallelisierungstechniken auf dem Gebiet des Data-Mining. Da das Thema dieser Arbeit viele Teilbereiche der Informatik berührt, die selbst wiederum sehr Umfangreich sind, gliedert sich diese Arbeit in drei Teile, die jedes für sich eine eigenständige thematische Einheit bilden. Der Erste Teil stellt eine hauptsächlich konzeptionelle Einführung in die Thematik des parallelen Rechnens dar. Es werden Begriffe und die theoretischen Grundlagen erläutert sowie grundlegende Modelle und Konzepte vorgestellt. Der Zweite Teil widmet sich konkreten Implementationen der im ersten Teil vorgestellten Konzepte. Der Schwerpunkt liegt auf Sprachen, Schnittstellen und Bibliotheken für die parallele 8 Siehe dazu Kapitel 10.1. http://setiathome.berkeley.edu/ 10 http://folding.stanford.edu/ 11 Vgl.: [K NIPPERS 2001] S. 37ff. 12 http://docking.cis.udel.edu/ 13 http://nqueens.ing.udec.cl/ 9 -3- Programmierung, die relativ verbreitet und frei verfügbar sind. Es werden ausgewählte Schnittstellen vorgestellt, deren Arbeitsweise anhand von Beispielen erläutert und eine erste vergleichende Bewertung vorgenommen. Der Dritte Teil behandelt das Thema Data Mining und ausgewählte Algorithmen. Es wird untersucht, inwiefern sich Verfahren parallelisieren lassen, welcher Aufwand hierfür notwendig ist und welche Resultate erzielt werden können. Insbesondere die Parallelisierung von DataMining-Modellen unter Verwendung vorhandener, rein sequentieller Data-Mining-Algorithmen wird untersucht. 1.4 Begriffsklärung Da aufgrund der Komplexität des Themas und der Vielzahl der angeschnittenen Themen zwangsläufig Überschneidungen von Begriffen auftreten, werden der Einfachheit halber folgende Begriffe verwendet: Als „Prozessor” wird in dieser Arbeit nicht der physikalische Chip, sondern die tatsächlich ausführende Recheneinheit bezeichnet. Demzufolge besitzt ein Computer mit einem MehrkernProzessor-Chip eine entsprechende Anzahl an Prozessoren. Diese Sichtweise ist aus dem Grund günstig, da es sich hier hauptsächlich um die Software-Sicht der parallelen Programmierung geht, und weniger um die Hardware. Zudem stellt sich ein Mehrkern-Prozessor aus Sicht eines Programms ebenfalls als eine Anzahl von Einzelprozessoren dar14 . In der Literatur wird zwischen Prozessen und Threads unterschieden, in dieser Arbeit wird nur der Begriff Prozesse verwendet. Der Grund dafür ist, dass sich Prozesse und Threads hauptsächlich durch die Art der Ressourcenteilung der Kindprozesse mit dem Elternprozess unterscheiden. Da die hier vorgestellten Programme und Bibliotheken unter Linux eingesetzt werden und Linux nur Prozesse unterstützt, ist die Nutzung von Threads zwangsläufig mit einer Emulation durch Prozesse verbunden. 14 Vgl.:[R AUBER 2007] S.101 -4- Teil I Grundlagen der parallelen Datenverarbeitung In diesem Kapitel werden die theoretischen Grundlagen verschiedener Ansätze zur Parallelisierung von Algorithmen vorgestellt. Dabei werden konkrete Methoden, Konzepte und Umsetzungsmöglichkeiten gezeigt. This chapter provides an introduction to the theory of parallel programming, concepts and techniques. It shows the most popular approaches, methods and concepts for parallelization of algorithms. -5- 2 Stufen der Parallelisierung Unter Parallelisierung wird die Zerlegung eines Problems in Teilprobleme verstanden, die gleichzeitig von mehreren Prozessoren verarbeitet werden können, so dass die Berechnung weniger Zeit benötigt, als würde sie auf einem einzelnen Prozessor erfolgen. Auf diese Weise lässt sich ein gegebenes Problem in kürzerer Zeit oder aber ein größeres Problem in der selben Zeit lösen. Es lassen sich vier Stufen der Parallelisierung unterschieden15 , wobei sich die ersten drei Stufen Wortbreite, Pipelining und Superskalare Prozessoren direkt auf die verwendete Technik im Prozessorkern beziehen. Die letzte Stufe, Parallelisierung auf Prozessebene, ist eher ein programmtechnisches Konstrukt, das nicht mehr direkt vom Prozessor abhängt16 , sondern vom Betriebssystem übernommen wird. Die Wortbreite ist der Anzahl der Bits, die in einer Operation vom Prozessor gleichzeitig verarbeitet werden können und entspricht somit der internen Busbreite und der Registergrösse. Die Wortbreite bestimmt maßgeblich, wie viele Bytes bei einem Speicherzugriff gleichzeitig übertragen werden können. Aktuelle Prozessoren haben eine Wortbreite von 32 oder 64 Bit. Eine Instruktion besteht aus mehreren atomaren Operationen, die in der Regel mindestens die folgenden Schritte umfasst: 1. fetch: Laden der nächsten Instruktion aus dem Speicher, 2. decode: Dekodieren der Instruktion, 3. execute: Bestimmung von Quell- und Zieladressen der Operanden und Ausführen der Instruktion, 4. write back: Zurückschreiben des Ergebnisses. Unter Pipelining versteht man die Ausführung solcher atomaren Operationen durch separate Hardwareeinheiten, wodurch eine zeitlich überlappende Ausführung mehrerer Instruktionen möglich wird. Die Verarbeitung der nachfolgenden Instruktion kann im günstigsten Fall bereits mit der fetch-Operation beginnen, sobald die vorhergehende Instruktion die decode-Operation durchläuft. Dies funktioniert jedoch nur dann, wenn keine Abhängigkeiten zwischen den Instruktionen bestehen17 . Superskalare Prozessoren18 erweitern das im Pipelining eingesetzte Konzept separater Hardwareeinheiten für atomare Operationen auf Instruktionsebene. Dazu verfügt der Prozessor über separate Funktionseinheiten für bestimmte Aufgaben, zum Beispiel für Ganzzahlarithmetik (ALU - arithmetic logical unit), Fließkommaarithmetik (FPU - floatin point unit) und Speicherzugriffe (MMU - memory management unit). Die Beschränkung der parallelen Ausführung hinsichtlich der Abhängigkeiten nachfolgender Instruktionen gelten auch hier. Moderne Prozessoren enthalten darüber hinaus auch spezielle erweiterte Befehlssätze wie MMX (Multi-Media-Extension) oder SSE (Streaming SIMD Extension)19 , die für die parallele Verar15 Vgl.: [R AUBER 2007] S.11 Eine Abhängigkeit besteht insofern, dass der verwendete Prozessor eine virtuelle Speicheradressierung unterstützt, was bereits mit der IA-32-Architektur eingeführt wurde. Dies schließt natürlich nicht aus, dass die Prozessortechnik Funktionen speziell für die Parallelisierung enthalten kann, wie z.B. Multi- und Hyperthreading. 17 Siehe auch Abschnitt 4.1 auf Seite 13. 18 Vgl.: [R AUBER 2007] S.14ff. 19 Intel: MMX ab Pentium, SSE ab Pentium III AMD: 3D-Now! ab K-6, SSE ab Athlon XP IBM/Motorola: VMX/AltiVec ab PowerPC 7400/G4 16 -6- beitung größerer Datenmengen nach dem SIMD-Prinzip vorgesehen sind. Diese werden in der Regel als Technologie zur Beschleunigung von Multimedia-Applikationen angepriesen, lassen sich aber ebenso gut für sinnvolle Rechenaufgaben nutzen. Die letzte Stufe der Parallelisierung auf Prozessebene ermögliche die parallele Ausführung mehrere Programme. Diese auch als Multitasking bezeichnete Konzept wird in erster Linie durch das Betriebssystem realisiert. Wenn man vom Parallelcomputing spricht, ist in der Regel eine solche Form der Parallelisierung von Algorithmen gemeint, wobei die Berechnung durch mehrere, parallel ausgeführte Prozesse erfolgt. Dies resultiert nur dann in einer kürzeren Berechnungszeit, wenn für jedes Programm auch ein eigener Prozessor zur Verfügung steht. 3 Parallele Architekturen 3.1 Flynnsche Klassifizierung Die Flynnsche Klassifizierung beschreibt vier idealisierte Klassen von Parallelrechnern basierend auf der Anzahl separater Programmspeicher, Datenspeicher und Recheneinheiten sowie deren Abhängigkeiten untereinander. Flynn unterscheidet dabei:20 SISD Single Instruction - Single Data beschreibt einen sequentiellen Rechner, der jeweils genau eine Anweisung auf genau einem Datenelement ausführt. Es existiert keine Parallelität. MISD Multiple Instruction - Single Data ist ein Konzept, bei dem ein Datenelement nacheinander durch verschiedene Anweisungen verarbeitet wird. Dieses Konzept ist eher theoretisch, jedoch kann Pipelining als eine einfache Form von MISD interpretiert werden. SIMD Single Instruction - Multiple Data entspricht der Verarbeitung mehrerer Datenelemente durch genau eine Anweisung. Dies ist bei Vektorrechnern der Fall, aber auch bei vielen Befehlssatzerweiterungen wie MMX oder SIMD. MIMD Multiple Instruction - Multiple Data ist die Verarbeitung unterschiedlicher Datenelemente durch jeweils unterschiedliche Anweisungen. Dies ist bei Superskalaren Prozessoren sowie Parallelrechnern der Fall, wobei verschiedene Funktionseinheiten oder Prozessoren parallel verschiedene Aufgaben ausführen. Diese Klassifizierung lässt sich auch auf die Parallelisierung von Algorithmen anwenden, wobei jeder der als „Multiple” aufgeführten Aspekte einen potenziellen Kandidaten für eine Parallelisierung darstellt. 3.2 Speichermodelle Aus logischer und physischer Sicht lassen sich Modelle mit gemeinsamem und verteiltem Speicher unterscheiden. Das physische Speichermodell hat großen Einfluss auf die Ausführungsgeschwindigkeit paralleler Programme, da es Latenz und Bandbreite bei Speicherzugriffen und Kommunikationsoperationen bestimmt. 20 Vgl.: [H OFFMANN 2008] S.11f., [R AUBER 2007] S.17ff., [R AUBER 2008] S.27f. -7- 3.2.1 Rechner mit gemeinsamem Speicher Bei gemeinsamem Speicher existiert ein für alle Prozessoren einheitlicher Adressraum. Gemeinsamer Speicher stellt hohe Anforderungen an Synchronisations- und Konsistenzmechanismen, um einen einheitlichen Speicherinhalt zu gewährleisten, was insbesondere zur Vermeidung von Race-Conditions bei konkurrierendem Schreib-Lese-Zugriffen notwendig ist. Bei gemeinsamem Speicher kann der Speicherzugriff auf unterschiedliche Weise realisiert werden, abhängig davon, wie die Speicherbänke physisch organisiert sind. Der Datenaustausch zwischen Prozessoren und Speicherbänken erfolgt über einen sogenannten Interconnect. Dieser kann z.B. als Bus ausgelegt sein, so dass abwechselnd immer nur ein Prozessor exklusiven Zugriff auf alle Speichermodule hat. Diese Architektur ist in der Regel bei Mehrkern-Prozessoren anzutreffen, die über einem gemeinsamen Adressbus mit dem Speicher-Controller kommunizieren. In diesem Fall ist die Zugriffsgeschwindigkeit stets gleich, daher spricht man von UMAArchitektur (Uniform Memory Access). Das Gegenstück hierzu bildet die NUMA-Architektur (Non-Uniform Memory Access), bei der jedem Prozessor ein eigenes Speichermodul zugeordnet ist, auf das demzufolge schneller zugegriffen werden kann als auf Module anderer Prozessoren.21 Die physische Verteilung des Speichers muss nicht zwingend mit dem logischen Speichermodell übereinstimmen. So kann z.B. ein gemeinsamer Speicher auch über einen Netzwerkverbund von Rechnern mit jeweils eigenem Speicher realisiert werden, wobei Adressumsetzung und Datenaustausch durch die jeweilige Programmierschnittstelle vorgenommen werden. Abbildung 1: Gemeinsames Speichermodell UMA Quelle: [BAUKE 2006] S. 11 Abbildung 2: Gemeinsames Speichermodell NUMA Quelle: [BAUKE 2006] S. 12. 21 Vgl.: [R AUBER 2007] S. 25ff. -8- 3.2.2 Rechner mit verteiltem Speicher Verteilter Speicher bedeutet, dass alle Prozessoren einen eigenen, privaten Adressraum besitzen. Für den Datenaustausch muss ein Verbindungsnetzwerk existieren, und er findet nur dann statt, wenn er explizit ausgelöst wird. Durch die entfallenden Synchronisationsmechanismen beim Speicherzugriff ermöglicht es diese Architektur, wesentlich mehr Rechner miteinander zu verbinden. Dies erfolgt allerdings auf Kosten der Übertragungsgeschwindigkeit von Daten zwischen den beteiligten Rechnern, was derartige Rechner für feingranulare Probleme wenig geeignet macht.22 Das Verbindungsnetzwerk hat erheblichen Einfluss auf die Geschwindigkeit. Die bestimmenden Faktoren sind Bandbreite und Latenz. Es kann davon ausgegangen werden, dass mit zunehmender physikalischer Entfernung zwischen den Rechnern die Latenz steigt und die Bandbreite sinkt.23 Weiterhin kann durch geschicktes Ausnutzen der Netzarchitektur die Geschwindigkeit von Datenverteilungs-Operationen wie Broadcast erhöht werden, wenn Nachbarschaftsbeziehungen ausgenutzt werden.24 Abbildung 3: Verbindungsnetzwerke Quelle: [BAUKE 2006] S.26 Durch die Verwendung von lokal zugeordneten Cache-Speichern25 entsteht das Problem der Cache-Kohärenz26 , da hier lokale Kopien gemeinsamer Variablen vorgehalten werden. Die Änderung einer Kopie muss nicht nur eine Aktualisierung des gemeinsamen Speichers, sondern 22 Vgl.: [R AUBER 2007] S.21ff. Vgl.: [R AUBER 2007] Kapitel 2.5 Verbindungsnetzwerke, S. 32ff. 24 Vgl.: [S ANTORO 2007] S. 32ff. 25 Vgl.: [R AUBER 2007] S.73ff. 26 Vgl.: [R AUBER 2007] S.31., S. 84ff. 23 -9- auch eine Aktualisierung aller anderen Kopien zur Folge haben. Die Steuerung des Caching erfolgt durch einen eigenen Cache-Controller und ist sowohl für den Prozessor als auch aus Programmsicht transparent.27 Die Probleme des Cachings lassen sich auf höhere Ebene übertragen, wenn es um die Entwicklung paralleler Programme und insbesondere von Bibliotheken für das verteilte Rechnen geht, da hier ebenfalls lokale und entfernte Daten synchronisiert werden müssen. 3.3 Prozesse / Threads Als Prozesse werden Programme bezeichnet, die zu einem gegebenen Zeitpunkt ausgeführt werden. Ein Prozess besitzt dabei eine Prozessumgebung, die durch eine Datenstruktur im Betriebssystem realisiert wird und zur Verwaltung des Zustandes sowie der Ressourcen eines Prozesses dient. Dazu gehören z.B. die Inhalte der Prozessorregister, verwendete Speicherbereiche, Umgebungsvariablen und geöffnete Dateien. Threads sind ebenfalls eine Form von Prozessen. Sie unterscheiden sich von ’richtigen’ Prozessen nur dadurch, dass sie keinen exklusiven Zugriff auf ihre Ressourcen haben, sondern diese mit anderen Prozessen, insbesondere mit dem ElternProzess, teilen. Prozesse werden unter Unix erzeugt, indem der laufende Prozess durch Aufruf eines fork()-Kommandos in seinem aktuellen Zustand dupliziert wird. Der so entstandene KindProzess kann nun einen anderen Programmzweig ausführen als der Elternprozess, andere Daten berechnen oder seine Prozessumgebung durch die eines anderen Programms ersetzen. Die Unterscheidung zwischen Prozessen und Threads dient hauptsächlich dazu, den physischen Ressourcenbedarf zu reduzieren, da beim Erzeugen eines Prozesses (theoretisch) dessen gesamte Prozessumgebung (inklusive Speicherbereiche) dupliziert werden muss, was beim Erzeugen von Threads nicht der Fall ist. Threads besitzen somit per Definition einen gemeinsamen Adressraum mit dem Elternprozess, währen Prozesse eigene Adressräume besitzen. Allerdings gibt es Methoden, wie z.B. Shared Memory, durch die auch Prozesse auf gemeinsame Speicherbereiche zugreifen können. Letztendlich bilden Prozesse die Grundlage für parallele Programme. Ein paralleler Algorithmus kann also bereits durch die Methoden, die für die Verwaltung von Prozessen sowie für die Interprozesskommunikation vom Betriebssystem bereitgestellt werden, realisiert werden. Allerdings ist die komplette Logik zur Steuerung der einzelnen Prozesse explizit zu implementieren, was zwar sehr viele Möglichkeiten zur Optimierung der Rechengeschwindigkeit, aber auch sehr viel Aufwand und hohe Fehleranfälligkeit bedeutet. Die Verwendung von speziell für diesen Zweck optimierten Bibliotheken ist häufig einfacher und auch effizienter. 3.4 Client-Server-Architekturen Client-Server-Architekturen verwenden als logische Struktur ein Stern-Modell, wobei der Server im Zentrum steht und direkte Verbindungen mit allen Clients unterhält. Parallelisierung durch Client-Server-Architekturen werden durch das Master-Worker-Konzept umgesetzt, bei dem der Server (Master) Rechenaufgaben oder Daten an die verbundenen Clients (Worker) sendet und diese das Resultat zurücksenden. Eine derartige Architektur lässt sich durch viele parallele Schnittstellen abbilden. Da die Abhängigkeiten zwischen den verbundenen Rechnern wenig 27 Die Existenz eines Caches macht sich durch Unterschiede in der Geschwindigkeit von Speicherzugriffen bemerkbar. Siehe dazu Kapitel 6.3. - 10 - komplex sind, ist diese Architektur relativ einfach zu implementieren und - sofern die physischen Ressourcen entsprechend dimensioniert sind - auch für sehr große Rechennetze höchst effektiv, wie z.B. das Seti@Home-Projekt zeigt. 3.5 Cluster-Computing Die Idee des Cluster-Computings besteht darin, durch Verwendung handelsüblicher Komponenten und freier Cluster-Software einen Parallelrechner mit verteiltem Speicher zu realisieren. Durch die günstigen Kosten werden derartige Cluster häufig von Universitäten und Forschungseinrichtungen eingesetzt. Diese auch als Beowulf-Cluster28 bezeichneten Computer gehen auf das Beowulf-Projekt der NASA von 1994 zurück, bei dem 16 Linux-PCs mittels PVM29 zu einem Parallelrechner verbunden wurden.30 Jeder Rechner in einem Cluster wird als Knoten bezeichnet und kann wiederum selbst eine eigene parallele Architektur aufweisen. Beowulf-Cluster lassen sich sehr leicht skalieren, da hierfür lediglich weitere Knoten an das Netzwerk angeschlossen und mit der Cluster-Software versehen werden müssen. Auf diese Weise lassen sich sogar ältere Rechner noch sinnvoll verwenden. Das Potential dieser Architektur zeigt sich darin, dass sich in Verbindung mit HochleistungsNetzwerken und moderner Rechentechnik Cluster bauen lassen, die zu den weltweit schnellsten Rechnern zählen. Die Tabelle 1 zeigt die weltweit fünf schnellsten Parallelrechner nach dem Linpack-Benchmark. Diese sind natürlich nicht mehr aus einzelnen PCs zusammengesetzt, sondern bestehen aus Racks mit einer entsprechenden Anzahl von Einschüben, verwenden jedoch handelsübliche Großserientechnik. Die Bedeutung freier Software für das Cluster-Computing zeigt sich daran, dass viele Hersteller auf eine Linux-Distribution zurückgreifen und diese nach Bedarf anpassen. Da der Quelltext der Programme frei verfügbar und veränderbar ist, können so zum Beispiel optimierte Kernel (z.B. von Cray und IBM) zum Einsatz kommen. 3.6 Grid-Computing Als Grid-Computing31 wird eine Form des Cluster-Computings bezeichnet, bei der eine wesentlich losere Koppelung der Knoten zum Einsatz kommt. Ein derartiges Computing-Grid ist häufig geographisch weit verteilt. Im deutschen Sprachraum wird häufig mit dem Begriff „Grid” ein regelmäßiges Gitter assoziiert und damit dem Grid-Computing seine Besonderheit durch Verwendung einer speziellen Topologie unterstellt. Tatsächlich weisen regelmäßige - und damit auch gitterförmige - Topologien manche Vorteile für Routingverfahren auf, in Wirklichkeit aber stammt die Bezeichnung „Grid” vom Englischen Begriff für „Stromnetz” (power grid) ab und weist eine beliebige Netzstruktur im Allgemeinen - und auf das Internet im Speziellen - hin. Die Analogie zum Stromnetz ist, dass Ressourcen im Grid transparent zur Verfügung gestellt und durch einfachen Anschluss an das Grid abgerufen werden können. 28 http://www.beowulf.org Siehe dazu Kapitel 10.4. 30 Vgl.: [BAUKE 2006] S.27f. 31 Vgl.:[BARTH 2006] 29 - 11 - # 1. 2. 3. 4. 5. Tabelle 1: Linpack Top-5 Supercomputer Stand 06/2008 System Standort Typ OS #Prozessoren IBM LANL Cluster Linux 122.400 BladeCenter (PowerXCell 8i, QS22/LS21 AMD Opteron Cluster Dual-Core) IBM LLNL MPP Suse Linux 212.992 eServer Blue Enterprise (PowerPC 440) Gene Solution Server 9 / CNK IBM ANL MPP Suse Linux 163.840 Blue Gene/P Enterprise (PowerPC 450) Solution Server 9 / CNK Sun TACC Cluster Linux 62.978 Microsystems (AMD x86_64 SunBlade Opteron Quad x6420 Core) Cray Inc. ORNL MPP CNL 30.978 Cray XT4 (AMD x86_64 Opteron Quad Core) GFlop/s 1.026.000 478.200 450.300 326.000 205.000 Erläuterung: LANL: Los Alamos National Laboratory, National Nuclear Security Administration LLNL: Lawrence Livermore National Laboratory, National Nuclear Security Administration ANL: Argonne National Laboratory TACC: Texas Advanced Computing Center/Univ. of Texas, ORNL: Oak Ridge National Laboratory CNK: Compute Node Kernel; von IBM angepasster Linux-Kernel CNL: Compute Node Linux von Cray Inc. Quelle: http://www.top500.org/lists/2008/06, Stand 01.11.2008 Ein wichtiges Alleinstellungsmerkmal des Grid-Computing - vor allem im wissenschaftlichen Bereich - gegenüber anderen Formen des Vernetzen Rechnens ist die Bereitstellung der Ressourcen auf volontärer Ebene. Dabei stellen die Teilnehmer freiwillig und kostenfrei nicht benötigte Rechenkapazität zur Verfügung, in sie ein Programm installieren, das mit niedriger Priorität ausgeführt wird und immer dann Berechnungen durchführt, wenn der Rechner nur gering ausgelastet ist. - 12 - 4 Parallelisierungsebenen Parallelisierung von Programmen kann auf verschiedenen konzeptionellen Ebenen erfolgen, für die es verschiedene Ansätze und Nebenbedingungen gibt. 4.1 Parallelität auf Instruktionsebene Die am dichtesten an der Hardware angesiedelte Ebene, in der eine Parallelisierung möglich ist, ist die Instruktionsparallelität. Unter bestimmten Voraussetzungen kann die Ausführung von Instruktionen eines an sich sequentiellen Programms von einer dafür ausgelegten CPU optimiert und parallel durchgeführt werden. Dabei legt der Scheduler die Reihenfolge der auszuführenden Instruktionen fest. Die zu parallelisierenden Instruktionen dabei lediglich auf folgende Abhängigkeiten zu prüfen:32 1. Fluss-Abhängigkeit 2. Anti-Abhängigkeit 3. Ausgabe-Abhängigkeit Die Prüfung der Abhängigkeiten beschränkt sich jeweils auf die Verwendung der CPU-Register durch die einzelnen Instruktionen. Besteht zwischen zwei Instruktionen keine dieser Abhängigkeiten, so können sie parallel ausgeführt werden. Eine Fluss-Abhängigkeit besteht immer dann, wenn eine Instruktion A einen Wert in ein Register lädt, das anschließend durch Instruktion B als Operand verwendet wird. In diesem Fall muss Instruktion A zwingend vor Instuktion B ausgeführt werden. Umgekehrt besteht eine AntiAbhängigkeit, wenn Instruktion B einen Wert in ein Register lädt, dass Instruktion A als Operand verwendet. Eine parallele oder umgekehrte Ausführungsreihenfolge führt in beiden Fällen dazu, dass falsche Operanden verwendet werden. Wenn beide Instruktionen das selbe Register zur Speicherung eines Ergebnisses verwenden, spricht man von Ausgabe-Abhängigkeit. Hier kann eine Parallelisierung dazu führen, dass eine nachfolgende Operation das falsche Ergebnis aus dem Register liest. Es tritt eine sogenannte Race-Condition ein, bei der das Ergebnis davon abhängt, welche Instruktion schneller ausgeführt wurde. 4.2 Parallelität auf Datenebene Datenparallelität33 liegt immer dann vor, wenn eine Operation auf mehrere, voneinander unabhängige Elemente oder Datenblöcke angewandt wird. Ein einfaches Beispiel hierfür ist eine Schleife, die ein Array mit Werten initialisiert: for(int i=0; i<size; i++){ array[i]=0; } 32 33 Vgl.: [R AUBER 2007] S.120f. Vgl.: [R AUBER 2007] S.122f - 13 - Es ist in diesem Fall völlig unerheblich, in welcher Reihenfolge auf die einzelnen ArrayElemente zugegriffen wird, das Resultat ist stets dasselbe. Daher ließe sich die Berechnung in 1...size Teilschritte zerlegen, die parallel ausgeführt werden können. Ein anderes Beispiel ist die Matrizenmultiplikation A=B*C. Hier können alle Elemente der Ergebnismatrix A unabhängig voneinander berechnet werden. Tatsächlich stellt die Implementierung von Berechnungen als eine Reihe von Matrixoperationen eine der leichtesten Parallelisierungsmöglichkeiten dar, da für diese Operationen bereits parallele Verfahren existieren.34 4.3 Parallelität in Schleifen Schleifen lassen sich parallelisieren, wenn zwischen einzelnen Iterationen keine Datenabhängigkeiten bestehen. Da dadurch die Reihenfolge der Iterationen unwichtig wird, kann jede Iteration unabhängig von den anderen ausgeführt werden.35 Dieser Ansatz wird beispielsweise durch OpenMP verfolgt. Unter bestimmten Umständen lassen sich auch Schleifen parallelisieren, die Datenabhängigkeiten enthalten. Voraussetzung hierfür ist, dass sich die einzelnen Iterationen in Gruppen aufteilen lassen, deren Resultate sich zu einem Gesamtergebnis zusammenfassen lassen. So kann z.B. die folgende Schleife: for(int i=0; i<100; i++){ summe+=array[i]; } in Teilschleifen aufgeteilt werden, deren Einzelresultate sich durch Addition zu einem Gesamtergebnis zusammenfassen lassen: for(int i=0; i<50; i++){ summe1+=array[i]; } for(int i=50; i<100; i++){ summe2+=array[i]; } summe=summe1+summe2; Ist eine derartige Aufteilung möglich, kann die ursprüngliche Schleife ebenfalls parallel ausgeführt werden. Derartige Konstrukte werden von verschiedenen parallelen Schnittstellen unterstützt, wobei die lokalen Ergebnisvariablen (hier summe) nach Beendigung aller Schleifendurchläufe unter Verwendung eines sogenannten Reduktionsoperators zusammengefasst werden. 4.4 Parallelität auf Funktionsebene Bei der Verwendung rein funktionaler Programmiersprachen wie z.B. Haskell lassen sich alle Funktionsläufe parallel ausführen, da durch die Konzeption der Sprache Seiteneffekte ausgeschlossen sind. Funktionen bestehen dabei nur aus elementaren Operationen oder aus anderen 34 35 Siehe Kapitel 10.2. Vgl.: [R AUBER 2007]S.123ff. - 14 - Funktionen, besitzen Eingabe- und Rückgabewerte, lösen aber keine Aktionen aus. Somit ist ausgeschlossen, dass eine Funktion die Eingabedaten einer anderen Funktion manipuliert. Dieses Konzept lässt sich auch auf nicht rein funktionale Programmiersprachen anwenden, sofern für den parallelisierenden Teil ausschließlich Funktionen verwendet werden, die nur lokale Variablen verwenden. Dies entspricht der Zerlegung eines Programms sind unabhängige Teilaufgaben, sogenannte Tasks. 36 36 Vgl.: [R AUBER 2007]S.127ff. - 15 - 5 Parallele Programmiermodelle 5.1 Darstellung Die Darstellung der Parallelität innerhalb eines Programms kann an unterschiedlichen Stellen erfolgen. Es lässt sich hier grob zwischen impliziter und expliziter Darstellung unterscheiden, wobei sich jeweils weitere Detaillierungsgrade ableiten lassen.37 Implizite Darstellung Die implizite Darstellung stellt sich aus Sicht eines Programmierers als einfachere Variante dar. Die Programmierung erfolgt gewohnt in sequentieller Weise, die Parallelisierung wird durch den Compiler oder durch die Eigenschaften der verwendeten Programmiersprache selbst realisiert. Die Parallelisierung durch einen Compiler erfordert, dass der Programmablauf und die Verbindungen zwischen Variablen analysiert und auf Parallelisierbarkeit hin untersucht werden. Dies ist ein sehr komplexer Vorgang und die erzielbaren Resultate sind in der Regel nicht sehr gut. Daher gibt es den Ansatz, im Quellcode Hinweise für den Compiler einzubetten, die Parallelisierbare Abschnitte anzeigen. Ein derartiger Ansatz - OpenMP - ist im Kapitel 9 beschrieben. Eine weitere Möglichkeit der impliziten Darstellung kann zum Beispiel durch die Verwendung rein funktionaler Programmiersprachen wie Haskell geschehen. Eine Parallelisierung kann Erfolgen, indem Funktionen, die als Argumente in anderen Funktionen auftreten, parallel ausgeführt werden, da in rein funktionalen Programmiersprachen Seiteneffekte ausgeschlossen sind. Explizite Darstellung Die Varianten der expliziten Darstellung sind zahlreicher als die der impliziten, da es mehr Einflussmöglichkeiten auf die konkrete Umsetzung der Parallelisierung gibt, während sich die implizite Darstellung letztendlich auf eine sehr formale Beschreibung der Parallelität beschränkt. Es lassen sich die folgenden vier Klassen unterscheiden: 1. Die Parallelität wird durch Konstrukte einer parallelen Programmiersprache (z.B. High Performance FORTRAN) oder durch Erweiterungen sequenzieller Programmiersprachen (z.B. OpenMP) explizit dargestellt. Die konkrete Umsetzung der Parallelität erfolgt jedoch durch den Compiler. 2. Die Zerlegung eines Algorithmus in parallele Abschnitte wird ebenfalls explizit vorgenommen, zum Beispiel durch Verwendung von mehreren Prozessen oder Threads. Die Aufteilung auf einzelne CPUs sowie die Kommunikation zwischen den Prozessen/Threads erfolgt durch den Compiler oder das Betriebssystem. 3. Die Zuordnung einzelner Prozesse/Threads zu den CPUs kann ebenfalls explizit vorgenommen werden. Dies ist jedoch nur in Ausnahmefällen notwendig, da die Verteilung von Prozessen auf CPUs durch das Betriebssystem vorgenommen werden sollte. 4. Die Synchronisation zwischen Prozessen sowie die Kommunikation und der Datenaustausch können ebenfalls explizit dargestellt werden. Als Vorteil wird angesehen, dass diese Methode die Verwendung eines Standard-Compilers erlaubt und eine sehr effiziente, auf den Anwendungsfall abgestimmte Implementierung zulässt, die dafür jedoch einen gewissen Aufwand 37 Vgl.: [R AUBER 2007]S.128ff. - 16 - erfordert. Als Beispiel sind Message-Passing-Konzepte wie PVM und MPI zu nennen, auf die im Abschnitt 10.4 bzw. im Kapitel 8 genauer eingegangen wird. 5.2 Strukturierung Die Aufgabenverteilung zwischen den Prozessen kann auf verschiedene Weise erfolgen.38 Häufig eingesetzte Modelle sind: Master-Worker: Hier wird die Arbeit durch ein oder mehrere Kindprozesse (Slaves) ausgeführt, die von einem Hauptprozess (Master) gestartet, kontrolliert und koordiniert werden. Pipelining: Beim Pipelining werden einzelne Prozesse so miteinander verbunden, dass die Ausgabe eines Prozesses direkt als Eingabe des nachfolgenden Prozesses dient, wobei alle Prozesse gleichzeitig aktiv sind. Client-Server: Dieses Modell entspricht in seiner Funktionsweise dem umgekehrten MasterWorker-Modell, mit dem zusätzlichen Unterschied, das es sich auf einen Netzwerkverbund bezieht und es mehrere Server geben kann. Die Rechenleistung wird durch den/die Server erbracht und von den Clients angefordert. 5.3 Datenverteilung und Kommunikation Die verschiedenen Arten, wie Daten zwischen Tasks ausgetauscht werden, können unter einer Menge von Operationen zusammengefasst werden. Diese werden so oder in ähnlicher Form durch die meisten parallelen Sprachen und Bibliotheken implementiert. Dabei lassen sich prinzipiell Einzel- und kollektive (globale) Operationen unterscheiden. Während erstere die asynchrone Kommunikation zwischen genau zwei Kommunikationspartnern beschreiben, werden letztere von alle beteiligten Einheiten gleichzeitig ausgeführt. 39 5.3.1 Broadcast Eine Broadcast-Operation verteilt Daten X, die einem Task P zugeordnet sind, auf alle anderen Tasks, so dass jeder Task nach der Operation exakt die gleichen Daten besitzt. P1 : X P2 : [] ... Pn : [] P1 : X P2 : X Broadcast =⇒ ... Pn : X Eine Broadcast-Operation ließe sich auch mit n nachrichtenbasierten Einzeloperationen erzielen. Das Zusammenfassen der Einzeloperationen in eine einzige Broadcast-Anweisung erlaubt es, dem zugrundeliegenden System eine effizientere Reihenfolge für das Verteilen der Daten zu organisieren, indem die zugrundeliegende Topologie das Verteilungsschema bestimmt. Kriterien sind die notwendige Anzahl von Schritten, bis die Daten komplett verteilt sind, und die dafür notwendige Anzahl von Nachrichten. 38 39 Vgl.: [R AUBER 2007] S.133f. Vgl.: [R AUBER 2007] S.142ff. - 17 - Eine als Flooding bezeichnete Methode besteht darin, dass ausgehend von initiierenden Knoten, jeder Knoten die Daten genau einmal an alle benachbarten Knoten schickt, sobald er sie empfangen hat. Die maximale nötige Anzahl von Schritten entspricht hier dem Durchmesser des Graphen.40 Die Anzahl der Nachrichten hängt von der Topologie ab. Ist die Topologie beispielsweise ein kompletter Graph, genügt genau ein Schritt, da jeder Empfänger direkt erreicht werden kann. Abbildung 4: Effiziente Broadcast-Operation Quelle: [BAUKE 2006] S.174 5.3.2 Scatter Eine Scatter-Operation verteilt Daten X, die einem Prozess P zugeordnet sind, in Blöcken x : x ∈ X gleichmäßig auf alle Prozesse, so dass jeder Prozess eine eindeutige Teilmenge der ursprünglichen Daten hält. In der Regel wird eine direkte Zuordnung Prozess-Nummer = BlockNummer vorgenommen, aber auch anderer Verteilungsmuster sind möglich. P1 : X P2 : [] ... Pn : [] P 1 : x1 P 2 : x2 Scatter =⇒ ... P n : xn 5.3.3 Gather Die Gather-Operation entspricht einer umgekehrten Scatter-Operation. Dabei werden die Blöcke x1 ...xn aller Prozesse im Datenbereich eines einzelnen Prozesses zusammengefasst. Sie wird als Gather-all-Operation bezeichnet, wenn das Zusammenfassen der Blöcke für jeden Prozess geschieht, die Prozesse also nach der Operation jeweils über alle Datenblöcke verfügen. P1 : x 1 P2 : x 2 ... Pn : x n 40 P1 : [x1 , ..., xn ] P 2 : x2 Gather =⇒ ... P n : xn Vgl.: [S ANTORO 2007] S.13f. - 18 - P 1 : x1 P 2 : x2 ... P n : xn P1 : [x1 , ..., xn ] P2 : [x1 , ..., xn ] Gather−All =⇒ ... Pn : [x1 , ..., xn ] 5.3.4 Reduktion Eine Reduktions- oder Akkumulations-Operation führt eine Funktion f() über den Daten aller Prozesse aus und stellt das Ergebnis in einem Prozess zur Verfügung. Typischerweise werden für f() arithmetische Funktionen (Summe, Produkt, Durchschnitt) oder Vergleichoperationen (größtes Element, kleinstes Element) implementiert. P 1 : x1 P 2 : x2 ... P n : xn P1 : f (x1 , ..., xn ) P 2 : x2 Akkumulation =⇒ ... P n : xn Der Sinn kollektiver Reduktionsfunktionen liegt ebenfalls in der Erzielung einer höheren Geschwindigkeit durch Verringerung der Anzahl notwendiger Kommunikationsschritte gegenüber der entsprechenden Anzahl von Einzeloperationen oder Synchronisierungsoperationen. Eine Akkumulationsfunktion muss sowohl kommutativ als auch assoziativ sein, da die Reihenfolge der Einzeloperationen nicht vorhergesagt werden kann. Abbildung 5: Effiziente Akkumulations-Operation Quelle: [BAUKE 2006] S.185 5.4 Synchronisation Eine parallele Ausführung bedeutet nicht, dass Programme auch synchron ausgeführt werden. Neben unterschiedlichen Prozessorgeschwindigkeiten und der jeweiligen Prozessorauslastung gibt es eine Reihe von Störfaktoren, die Einfluss auf die tatsächliche Ausführungsgeschwindigkeit haben. Daher kann man nicht davon ausgehen, dass sich eine gemeinsame Ressource - wie zum Beispiel ein Speicherinhalt - in einem stets definierten Zustand befindet. Wenn ein Prozess einen Abschnitt erreicht, der einen definierten Zustand voraussetzt, muss dieser zunächst hergestellt werden. Dies wird durch Synchronisationsmechanismen erreicht, die dafür sorgen, dass sich die zu synchronisierende Ressource zu einem bestimmten Zeitpunkt für jeden beteiligten - 19 - Prozess in einem definierten Zustand befindet. Beispielsweise stellt die Speichersynchronisation identische Speicherinhalte für alle Prozesse sicher, eine Ablaufsynchronisation sorgt dafür, dass alle parallelen Prozesse die exakt gleiche Stelle im Programmtext erreicht haben. 5.4.1 Kritische Abschnitte Bei Race-Conditions ist der Zustand einer Ressource davon abhängig, welche Prozesse in welcher Reihenfolge darauf zugreifen. Insbesondere nicht-atomare Anweisungen, die während der Ausführung unterbrochen werden können, sind dafür anfällig. Solche Programmabschnitte werden als „Kritische Abschnitte” bezeichnet. Für die Koordinierung der Prozesse sind eine Reihe von Verfahren bekannt.41 Parallele Sprachen und Bibliotheken stellen in der Regel Möglichkeiten zum Kennzeichnen kritischer Abschnitte bereit, so dass der Programmierer keine eigene Zugriffssteuerung implementieren muss. Probleme bereiten nur solche kritischen Abschnitte, die vom Programmierer nicht als kritisch erkannt werden, da die dadurch resultierenden Programmfehler sporadisch, an verschiedenen Programmstellen und schlecht reproduzierbar auftreten können, was den Anschein zufälliger Fehler erzeugt. Weiterhin kann es vorkommen, dass optimierende Compiler Code erzeugen, der nicht der Ausführungsreihenfolge der Anweisungen im Quelltext entspricht. 5.4.2 Barrieren Barrieren sind eine Methode der Ablaufsynchronisation. Sie stellen sicher, dass sich alle Prozesse zu einem bestimmten Zeitpunkt an der selben Stelle im Programmtext - der Barriere - befinden. Nachdem alle Prozesse die blockierende Barriere erreicht haben, werden sie synchron fortgesetzt. Dabei kann es vorkommen, dass einzelne Prozesse sehr lange auf andere warten müssen. Nicht-blockierende Barrieren dagegen erlauben es einem Prozess, mit der Ausführung von Programmcode fortzufahren, der nicht von anderen Prozessen abhängig ist, sofern diese mindestens eine bestimmte Stelle im Programmcode erreicht haben. Auf diese Weise kann ein Performancegewinn erzielt werden, da die Wartezeiten verkürzt werden können. 5.4.3 Locks Der konkurrierende Zugriff auf Ressourcen kann durch Locking-Mechanismen gesteuert werden. Locks stellen eine Möglichkeit dar, Race-Conditions zu vermeiden, da der Zugriff auf gemeinsame Ressourcen exklusiv erfolgt. Anders als kritische Abschnitte können Locks über verschiedene Programmteile hinweg aufrecht erhalten werden. So kann an einer Stelle ein Lock erzeugt und später an einer anderen Stelle wieder freigegeben werden. Die Verantwortung für die Freigabe eines Locks liegt allein beim Programmierer, daher ist die Verwendung potentiell Anfällig für Deadlock-Situationen. 41 Vgl. hierzu: [C ARVER 2006] S. 46ff. - 20 - 6 Einflussfaktoren Paralleler Programme 6.1 Parallele Skalierbarkeit Durch Parallelisierung soll in der Regel ein Geschwindigkeitszuwachs in der Form erzielt werden, dass pro Zeiteinheit mehr Rechenoperationen durchgeführt werden. Ähnlich der Frage nach den Grenzkosten oder dem Grenznutzen in der BWL und VWL stellt sich auch hier die Frage, welcher Aufwand - vor allem in Form zusätzlicher Rechner oder Prozessoren - für eine Parallelisierung notwendig ist und in welchem Verhältnis dieser zum erzielten Geschwindigkeitsgewinn steht. 6.1.1 Speedup Geht man davon aus, dass ein parallelisierbares Programm auf einem Prozessor die Rechenzeit T1 benötigt, so ist die Rechenzeit auf p Prozessoren im optimalen Fall T1 /p. Parallelisierbare Abschnitte machen in der Regel nur einen Teil des gesamten Programms aus, so dass ein bestimmter Anteil der Rechenzeit immer für einen sequentiellen Teil verbraucht wird, der konstant ist. Durch Erhöhung der Parallelität nimmt im parallelen Teil die Rechenzeit pro Prozessor ab, was beabsichtigt ist, wodurch der relative Anteil der sequentiellen Abschnitte größer wird. Das führt dazu, dass eine obere Grenze für den erzielbaren Geschwindigkeitszuwachs durch Hinzunahme eines weiteren Prozessor existiert. Dieser Effekt wird nach seinem Entdecker Gene Amdahl als Amdahl’sches Gesetz42 bezeichnet. Ist für Eingaben der Größe n die Rechenzeit auf einem Prozessor T1 (n) und die Rechenzeit auf p Prozessoren Tp (n), so ergibt sich der theoretisch erzielbare Geschwindigkeitszuwachs (Speedup) aus:43 Sp (n) = T1 (n) Tp (n) (1) Der höchste theoretisch erzielbare Speedup ist Sp (n) = p, was einem linear skalierenden Programm entspricht. In der Praxis kann jedoch ein Speedup Sp (n) > p auftreten, was als superlinearer Speedup bezeichnet wird. Dies kann dadurch eintreten, dass die Problemgröße bzw. Datenmenge pro Prozessor so klein wird, dass geschwindigkeitssteigernde Effekte durch Caching-Mechanismen eintreten 44 . Es ließe sich auch umgekehrt argumentieren, nämlich dass im Regelfall geschwindigkeitssenkende Effekte durch Verwendung langsamerer Komponenten außerhalb des schnellen Cache bei zu großen Datenmengen die Ursache sind. 6.1.2 Amdahl’sches Gesetz Amdahl nimmt einen Faktors f für den seriellen Anteil45 eines Programmlaufs an, so dass sich die parallele Rechenzeit aus der Summe des nicht parallelisierbaren Anteils und des paralleli42 Vgl.: [C HAPMAN 2008] S. 33f., [C HANDRA] S. 173f., [BAUKE 2006] S.10ff., [R AUBER 2008] S. 170ff., [R AUBER 2007] S.37f., [H OFFMANN 2008] S.13ff., [E M K ARNIADAKIS 2000] S.39f. 43 Vgl.: [R AUBER 2007] S.168 44 Vgl.: [R AUBER 2008] S.37, [R AUBER 2007] S.169 45 In der Literatur wird auch statt des seriellen Anteils f der parallele Anteil verwendet. Der Teiler ist dann entsprechend umzustellen. - 21 - sierbaren Anteils von T1 ergibt: 1−f T1 (n) p Nach obiger Formel ergibt sich der nach Amdahl erzielbare Speedup als eine von n unabhängige Größe, was als Amdahl’sches Gesetz bezeichnet wird:46 Tp (n) = f ∗ T1 (n) + Sp (n) = T1 (n) {f ∈ R|0 ≤ f ≤ 1}{p ∈ N|p > 0} f ∗ T1 (n) + 1−f T1 (n) p S(p) = 1 f+ 1−f p (2) Der in Abhängigkeit des Faktors f erzielbare Speedup ist in Abbildung 6 dargestellt47 . Im optimalen Fall einer vollständigen Parallelisierbarkeit mit f=1.0 skaliert ein Programm linear, während bereits für f=0.95 der Zuwachs sehr schnell und deutlich stagniert. In dieser Berechnung ist noch nicht berücksichtigt, dass mit höherer Parallelität der zusätzliche Aufwand für die Erzeugung von Threads/Prozessen sowie für die Synchronisation und den Datenaustausch ebenfalls ansteigt. Abbildung 6: Parallele Skalierbarkeit nach Amdahl Quelle: eigene Darstellung. 6.1.3 Gustafson-Gesetz Neben dem hier beschriebenen Amdahl’schen Gesetz existieren eine Reihe von Varianten, die weitere Faktoren berücksichtigen. Dazu gehören Beispielsweise der in Abhängigkeit von p stei46 47 Vgl.: [R AUBER 2007] S.170, [R AUBER 2008] S.38, [E M K ARNIADAKIS 2000] S.79f., Ein Rechner mit 16 Rechenkernen stellt dabei eher die untere Grenze heutiger Parallelrechner dar. Entsprechende Boards mit 4 Sockeln sowie Quad-Core-Prozessoren sind im normalen Handel erhältlich. Ein heutiger ’kleiner’ Supercomputer ist Beispielsweise der Cray CX1, der noch unter einem Schreibtisch Platz findet, und bis zu 16 Quad-Core CPUs enthält. Ein Cray XMT enthält bis zu 8096 CPUs. (http://www.cray.com) - 22 - gende Kommunikationsaufwand, wodurch bei großen p eine weitere Erhöhung zu einer Verringerung des Speedup führt, wie in Abbildung 7 dargestellt. Abbildung 7: Parallele Skalierbarkeit nach Amdahl Quelle: [BAUKE 2006] S.12 Eine weitere Variante besteht darin, den parallelisierbaren Anteil auch in Abhängigkeit von der Eingabegröße zu betrachten. Dabei wird eine konstante Ausführungszeit für den sequentiellen Teil angenommen, wodurch bei steigender Eingabegröße der sequentielle Anteil abnimmt. Das Amdahl’sche Gegenstück hierfür ist eine monoton sinkende Funktion f (p, n) > 0 anstelle des Faktors f. In der Betriebswirtschaft existieren prinzipiell zwei Möglichkeiten, um bei konstanten Verkaufszahlen den Gewinn und somit die Effizienz des Kapitaleinsatzes zu erhöhen: eine Erhöhung der Preise oder eine Senkung der Kosten. Um die Effizienz von Parallelisierungsmaßnahmen zu erhöhten, gibt es ebenfalls zwei grundsätzliche Möglichkeiten: Verringerung der Rechenzeit durch mehr Prozessoren oder Erhöhung der Eingabegröße. Die Verringerung der Rechenzeit durch steigende Prozessorzahlen ist durch die bisher beschriebenen Gesetzmäßigkeiten limitiert. Weiterhin stellt sich die Frage, ob eine signifikante Verringerung der Rechenzeit den dafür notwendigen zusätzlichen Aufwand überhaupt rechtfertigt, was in der Regel nur auf sehr zeitkritische oder aber sehr langwierige Berechnungen zutreffen dürfte. Ein alternatives Ziel stellt eine bei etwa gleichbleibender Rechenzeit erzielbare Erhöhung der Eingabegröße dar. Als Beispiel soll hier eine Anfrage an eine Internet-Suchmaschine dienen. Wenn die Antwortzeit in einem akzeptablen Bereich von z.B. wenigen Sekunden liegt, hat eine weitere Verringerung der Antwortzeit im Verhältnis zur damit verbundenen Erhöhung der Rechenkapazität nur wenig Sinn, wohl aber die Vergrößerung der Suchraumes, um die Trefferzahl zu erhöhen oder die Trefferqualität zu verbessern. Beim Gustafson-Gesetz48 wird die Eingabegröße berücksichtigt, indem von einer gegebenen parallelen Laufzeit Tp (n) für ein Problem ausgegangen und diese in Relation zur hypothetischen sequentiellen Laufzeit T1 (n) = f Tp (n) + p(1 − f )Tp (n) gesetzt wird. In Anlehnung an die bisherige Notation ergibt sich daraus der skalierte Speedup: Sp (n) = 48 f Tp (n) + p(1 − f )Tp (n) = f + p(1 − f ) = p + (1 − p)f Tp (n) Vgl.: [R AUBER 2007] S. 171f., [R AUBER 2008] S. 27f., [BAUKE 2006] S.16, [H OFFMANN 2008] S.16f. - 23 - (3) In Abbildung 8 ist der Unterschied zwischen beiden Ansätzen für f=0.05 beispielhaft dargestellt. Der fast lineare Speedup nach Gustafson beruht darauf, dass für ein gegebenes paralleles Problem die entsprechende rein sequentielle Rechenzeit durch die damit linear steigende Eingabegröße zunimmt, während Amdahls Ansatz ein gegebenes Problem auf einem einzelnen Rechner durch Parallelisierung schneller zu lösen versucht, was durch den stets geringer werdenden parallelen Anteil eine asymptotische Funktion zur Folge hat. Abbildung 8: Speedup Amdahl vs. Gustafson Quelle: eigene Darstellung. 6.1.4 Karp-Flatt-Metrik Nun lässt sich der Faktor f nur sehr schwer anhand theoretischer Überlegungen oder Quelltextanalysen bestimmen. Durch Messung lässt sich f jedoch für ein bestimmtes Problem und Programm empirisch feststellen. Ein solches Maß ist die Karp-Flatt-Metrik. Dabei wird für mehrere p der Wert von T(p) experimentell bestimmt. Anschließend kann durch Amdahl’s Gesetz der entprechende empirische Anteil f bestimmt werden:49 f= 1/Sp (n) − 1− 1 p 1 p Anhand des Verhaltens dieses Wertes zu p lässt sich aussagen, ob der Speedup eher vom sequentiellen Anteil oder vom durch die Parallelisierung induzierten Rechenaufwand (Overhead) bestimmt wird. Letzteres ist daran zu erkennen, dass f mit steigender Prozessorzahl zunimmt, wodurch auch der Speedup langsamer steigt, als durch Amdahl’s Gesetz beschrieben. Die Effizienz der Parallelisierung lässt sich aus dem Verhältnis zwischen Prozessor-Anzahl p und dem dadurch erzielten Speedup S(p) bestimmen: 49 Vgl.: [BAUKE 2006] S.16f. - 24 - E(p) = S(p) T (1) = pT (p) p 6.2 Load Balancing und Scheduling Die Aufteilung einer Rechenaufgabe in Teilaufgaben und deren Zuordnung zu Prozessen kann auf verschieden Weise erfolgen. Dies wird als Scheduling bezeichnet. Die Aufteilung erfolgt anhand einer Strategie mit dem Ziel, für eine Aufgabe eine möglichst effiziente Ressourcennutzung zu erzielen. Es lassen sich statische und dynamische Scheduling-Strategien unterscheiden. Die Verwendung einer bestimmten Strategie kann Auswirkungen auf die Gestaltung des Programms haben. Die verwendete Scheduling-Strategie hat Auswirkungen auf die Ausführungsgeschwindigkeit paralleler Programme. Insbesondere statische Strategien sind anfällig für Verzögerungen bei einzelnen Prozessen. Die gesamte Abarbeitungszeit ist dabei vom langsamsten Prozess abhängig. Dies führt dazu, dass eine sehr starke Parallelisierung oft nicht den erwarteten Effekt hat. Besonders wenn die Anzahl der Prozesse sich der Zahl verfügbarer Prozessoren annähert, steigt die Wahrscheinlichkeit, dass einzelne Prozesse sich Ressourcen mit anderen Prozessen teilen müssen, wodurch diese eine längere Zeit zur Beendigung ihrer Aufgabe benötigen. Dynamische Strategien oder das Master-Worker-Modell haben den Vorteil, dass Rechenaufgaben an die gerade verfügbaren Prozesse / Prozessoren verteilt werden. Verzögerungen einzelner Prozesse können leicht kompensiert werden, indem schnellere Prozesse zusätzliche Anteile übernehmen.50 6.3 Lokalität Unter Lokalität versteht man, dass die zu verarbeitenden Daten sich im direkten Zugriff des Verarbeiters befinden. Tun sie es nicht, müssen sie erst dorthin übertragen werden, was Zeitaufwändig ist. Diese Beschreibung ist sehr allgemein und kann sich auf unterschiedliche Bereiche bzw. Ebenen beziehen. Je höher diese Ebene angesiedelt ist, desto größer ist der Einfluss der Lokalität auf die Rechengeschwindigkeit, da der Aufwand zur Datenübertragung steigt und die Übertragungsgeschwindigkeit in der Regel sinkt. In einem Computercluster aus mehreren Knoten und einem Verbindungsnetzwerk sind Daten lokal, wenn sie im Speicher des Knoten verfügbar sind, der sie Verarbeitet. Auf der Ebene eines Parallelrechners mit mehreren Prozessoren sind Daten lokal, wenn sie sich im durch den Prozessor adressierbaren physikalischen Speicher befinden, auf Ebene eines Prozessors oder Prozessorkernes dann, wenn sie sich in dessen Cache oder gar in den Prozessorregistern befinden. Der Einfluss der Lokalität lässt sich bereits in seriellen Programmen feststellen. Die Geschwindigkeit, mit der auf Daten zugegriffen werden kann, hängt sehr stark davon ab, ob der Prozessor diese erst aus dem relativ langsamen Hauptspeicher lesen muss, oder ob die Daten bereits im deutlich schnelleren 2nd-Level oder gar 1st-Level Cache zur Verfügung stehen. Caching erfolgt in der Regel blockweise, so dass beim erstmaligen Zugriff auf eine Speicherzelle im RAM auch 50 Zum Vergleich der Strategien siehe Kapitel 11.2. - 25 - Abbildung 9: Row- vs. Column-first Ordering Quelle: Eigene Darstellung. benachbarte Bytes in den Cache übertragen werden. Im Zusammenspiel damit, wie ein Compiler Daten - insbesondere Arrays - im Hauptspeicher ablegt, hat dies Auswirkungen auf die Ausführungsgeschwindigkeit des Programms. Im C-Standard wird Row-First-Ordering verwendet.51 Das bedeutet, die Indizes [0][0] und [0][1] liegen im Speicher an hintereinander folgenden Adressen. Folglich müssen Matrizenoperationen, die Zeilenweise durchlaufen werden, schneller sein als solche, die Spaltenweise arbeiten. Wie groß der Unterschied tatsächlich ist, verdeutlicht die Abbildung 9. Es wurde die Zeit gemessen, um die Summe aller Matrizenelemente für quadratische Matrizen verschiedener Größen zu berechnen. 6.4 Speichersynchronisation Die Einhaltung einer identischen, eindeutigen Sicht aller Prozesse auf den gemeinsamen Speicher wird durch Synchonisierungsmechanismen realisiert. Es lassen sich verschieden strikte Modelle unterscheiden:52 • Das Modell mit den meisten Einschränkungen ist das sequentielle Konsistenzmodell. Es stellt sicher, dass Veränderungen gemeinsamer Variablen unmittelbar und in exakt der gleichen zeitlichen Reihenfolge, in der sie von allen Prozessen ausgeführt werden, für alle Prozesse sichtbar sind. Dies bedeutet im Prinzip eine sofortige Synchronisierung der betroffenen Speicherinhalte nach einem Schreibzugriff. • Abgeschwächte Konsistenzmodelle erlauben beschränkt inkohärente Speicherzustände für bestimmte Programmabschnitte was sowohl den Speicherinhalt als auch die Zugriffsreihenfolge betrifft. Da es nicht immer notwendig ist, stets eine kohärente Sicht auf den Speicher zu 51 52 Vgl. hierzu: [B RANDS 2005] Kapitel 6 - Mehrdimensionale Felder, S. 300ff. Für eine detaillierte Beschreibung siehe [R AUBER 2007] S.92ff. - 26 - gewährleisten, z.B. wenn nacheinander mehrere Schreibzugriffe erfolgen und nur der letzte Zugriff das endgültige Resultat darstellt, kann so die Ausführungsgeschwindigkeit von Programmen deutlich erhöht werden. Die Synchronisierung erfolgt in solchen Fällen explizit nach Beendigung des jeweiligen Programmabschnitts. Parallele Bibliotheken und Programmiersprachen unterstützen in der Regel verschiedene Konsistenzmodelle. Da diese direkte Auswirkungen auf die Funktionsweise und Ausführungsgeschwindigkeit eines parallelen Programms haben, ist dieses Thema auch aus Sicht eines Programmierers relevant. - 27 - Teil II Bibliotheken und Systeme Es existieren eine Reihe von Programmbibliotheken und Compilern, die das entwickeln Paralleler Programme unterstützen sollen. Viele davon sind GPL-Lizensiert und frei verfügbar. In diesem Teil wird auf konkrete Implementierungen und Schnittstellen für das entwickeln paralleler Algorithmen unter C/C++ eingegangen. Development of parallel programs is supported by various programming libraries, interfaces and compilers. Many of them are license under the GPL Public License, which makes them freely available. This chapter provides an introduction to interfaces and programming libraries for the C/C++ programming language. - 28 - 7 Unified Parallel C Unified Parallel C (UPC)53 erweitert ANSI C um Konstrukte, die einen Einsatz auf HochleistungsParallelrechnern ermöglichen54 . Dafür wurden der Sprache C folgende Erweiterungen hinzugefügt: • Ein explizites paralleles Ausführungsmodell • Ein gemeinsamer Adressraum • Synchronisationsmechanismen • Speicherkonsistenzmodelle • Eine angepasste Speicherverwaltung Die Spezifikation von UPC wurde von einem Konsortium aus Unternehmen, Bildungsträgern und Regierungsorganisationen der USA entwickelt. Die Zusammensetzung des Konsortiums ist der Übersicht im Anhang A.2 zu entnehmen. Um UPC nutzen zu können, sind spezielle Compiler erforderlich. Diese basieren häufig auf quelloffenen C/C++ Compilern, die um UPC-Konstrukte erweitert wurden. In Tabelle 2 auf Seite 82 sind einige aktuell verfügbare Compiler und die jeweils unterstützten Betriebssysteme und Rechnerarchitekturen aufgeführt. Der in dieser Arbeit verwendete Compiler ist der GCC UPC in der Version 4.0.3. 7.1 Verfügbarkeit Ein UPC-Compiler für Linux-Systeme basierend auf dem GCC ist im Internet verfügbar und kann kostenfrei heruntergeladen werden. Neben bereits übersetzten Paketen für verschiedene Prozessoren und Distributionen ist auch ein Quellpaket vorhanden.55 Nach dem Entpacken der Quellen ist ein separates Verzeichnis anzulegen, in dem die Übersetzung erfolgt, typischerweise “build” genannt. Die Konfiguration der Make-Umgebung erfolgt mittels des mitgelieferten configure-Skriptes. Um nicht in Konflikt mit existierenden GCCCompilern zu kommen, ist es empfehlenswert, den UPC in einem separaten Verzeichnis zu installieren, z.B. /usr/local/upc. Die Befehle zum Übersetzen der Quellen und anschließendem Installieren sind: $ t a r −x z f upc − 4 . 0 . 3 . 5 . s r c . t a r . gz && cd upc − 4 . 0 . 3 . 5 && m k d i r b u i l d && cd b u i l d $ . . / c o n f i g u r e −− p r e f i x = / u s r / l o c a l / $ make && make i n s t a l l 7.2 Parallelisierungskonzept Die Parallelisierung mittels UPC erfolgt durch Threads. Sie unterscheidet sich jedoch von herkömmlicher Thread-Programmierung dadurch, dass der Code für die Erzeugung der Threads durch den UPC-Compiler in das Programm integriert wird und nicht durch den Programmierer. 53 Vgl.: [C HAUVIN 2008], [R AUBER 2007] S.371ff., UPC Homepage: http://upc.gwu.edu, Stand 01.12.2008 55 http://www.intrepid.com/upc/downloads.html 54 - 29 - Die Anzahl zu verwendender Threads kann sowohl zur Übersetzungszeit als auch zur Laufzeit festgelegt werden. Um Berechnungen parallel durchzuführen, wird ein Schleifenkonstrukt upc_forall verwendet: upc_forall ( expr1; expr2; expr3; affinity) Die Syntax entspricht exakt der C-Syntax für for-Schleifen, mit dem Unterschied, dass ein vierter Parameter “affinity” existiert, der angibt, durch welchen Thread die aktuelle Iteration durchgeführt wird. Dies kann entweder durch die Angabe der Thread-Nummer oder einer Speicheradresse geschehen. In letzterem Fall wird die Iteration durch den Thread durchgeführt, in dessen Adressraum die Speicheradresse liegt. Dies ist insbesondere dann nützlich, wenn über ein verteiltes Array iteriert wird und die Lokalität verschiedener Array-Elemente ausgenutzt werden soll. 7.3 Speichermodell UPC implementiert ein Modell mit verteiltem Speicher, wobei dieser sowohl gemeinsam (shared) als auch einzeln (private) genutzt werden kann. Der private Speicher eines Threads liegt ausschließlich in seinem eigenen Adressraum und ist nur für ihn selbst sichtbar. Privater Speicher liegt immer dann vor, wenn Speicher in herkömmlicher C-Notation alloziert wird, so das es hier keinen Unterschied zur nicht-parallelen Programmierung gibt. Gemeinsam genutzter Speicher wird durch Verwenden des neuen Schlüsselwortes shared erzeugt. Auf eine als shared deklarierte Variable kann von allen Threads aus zugegriffen werden. Folgendes Listing zeigt einige Möglichkeiten, private und gemeinsame Variablen zu definieren: int local_i; shared int global_i; shared int * ptr1; // private Variable // gemeinsame Variable // privater Zeiger auf gemeinsame Variable shared int * shared ptr2; // gemeinsamer Zeiger auf gemeinsame Variable Eine interessante Variante bilden verteilte Arrays. Die Deklaration erfolgt folgendermaßen: shared [block_size] typ name[size]; Ein derart erzeugtes Array wird in Blöcken der Größe block_size komplett auf den gemeinsamen Speicher verteilt. Wird das Array anschließend durch eine upc_forall-Schleife verarbeitet, kann auf diese Weise bereits eine Zuordnung des verwendeten Speichers zu den einzelnen Threads vorgenommen werden, so dass Zugriffe auf die Array-Elemente lokal erfolgen können. Anderenfalls würden Zugriffe auf Elemente, die sich im Speicherbereich eines anderen Threads befinden, in entfernte Zugriffe umgewandelt werden müssen, was einen gewissen Kommunikationsaufwand erfordert und daher langsamer ist. Standardmäßig wird eine Blockgröße von 1 angenommen. Eine Blockgröße von 0 würde dazu führen, dass alle Elemente im Speicherbereich des ersten Threads abgelegt werden. Im Folgenden sind zwei Beispiele für die Verteilung von Elementen bei verschiedenen Blockgrößen dargestellt: - 30 - shared int a[10]; Thread 1 a[0] a[4] a[8] Thread 2 a[1] a[5] a[9] Thread 3 a[2] a[6] Thread 4 a[3] a[7] Thread 2 a[2] a[3] Thread 3 a[4] a[5] Thread 4 a[6] a[7] shared [2] int a[10]; Thread 1 a[0] a[1] a[8] a[9] Verteilte Arrays müssen global und dürfen nicht dynamisch sein. Daher ist das Anwendungsfeld dieser einfachen Notation von vornherein beschränkt. Weiterhin ist die Verwendung lokaler Variablen, z.B. in Funktionsrümpfen, für die Parallelisierung ausgeschlossen. Es kann zwar verteilter, dynamisch allozierter Speicher erzeugt werden, jedoch muss dieser dann durch entsprechende Speichermanipulationsroutinen explizit gefüllt werden. Diese Beschränkung lässt sich durch Verwendung dynamisch allozierten verteilten Speichers umgehen. UPC bietet dazu verschiedene malloc-ähnliche Funktionen. UPC stellt zwei Konsistenzmodelle bereit: strict und relaxed. Das strict-Modell stellt sicher, dass vor Speicherzugriffen eine Synchronisation stattfindet. Ebenso werden Optimierungen seitens des Compilers unterbunden, welche die Reihenfolge von Zugriffsoperationen auf unabhängige Speicherbereiche verändern. Das relaxed-Modell dagegen erlaubt den jederzeitigen Speicherzugriff. Synchonisierung und Locking-Mechanismen müssen dabei explizit im Programmtext implementiert werden. Das verwendete Konsistenzmodell kann entweder global durch das Einbinden der HeaderDateien upc_strict.h oder upc_relaxed.h erfolgen, durch #pragma-Direktiven für einzelne Programmabschnitte festgelegt, oder direkt bei der Variablendeklaration bestimmt werden. #include <upc_strict.h> #include <upc_relaxed.h> strict shared int i; strict relaxed int j; { #pragma upc strict // code-block - 31 - } #pragma upc relaxed { #pragma upc relaxed // code-block } 7.4 Synchonisation Neben den beschriebenen Konsistenzmodellen stellt UPC für die Synchronisation weitere Mechanismen bereit. Locks Locking-Mechanismen lassen sich auf Daten-Ebene verwenden. Dabei können gemeinsame Variablen durch den Aufruf von upc_lock() für den Zugriff durch andere Threads gesperrt und später durch upc_unlock() wieder freigegeben werden. Barrieren UPC stellt Barrier-Konstrukte bereit, mit denen sichergestellt werden kann, dass zu einem Zeitpunkt alle Threads eine bestimmte Stelle im Programmcode erreicht haben. Es werden blockierende und nicht-blockierende Barrieren unterschieden. Blockierende Barrieren werden über das upc_barrier-Kommando in den Programmcode eingefügt. Der Programmablauf wird an dieser Stelle unterbrochen, bis alle anderen Threads die selbe Stelle erreicht haben. 7.5 Globale Operatoren UPC unterstützt Datenverteilungs- und Sammeloperationen, Reduktionsoperationen sowie Einund Ausgabeoperationen. Diese werden als Funktionsaufruf an entsprechender Stelle im Programmtext eingefügt. Zur Datenverteilung sind Scatter-, Gather- sowie Permutationsverfahren implementiert. Als Reduktionsoperationen stehen arithmetische binäre Operatoren sowie Sortieroperationen zur Verfügung. Der koordinierte Zugriff auf Dateien und das Lesen und Schreiben aus bzw. in gemeinsame Speicherbereiche wird unterstützt. - 32 - 8 Message Passing Interface Message-Passing ist ein Konzept für Parallelrechner mit verteiltem Speicher. Die logische Sicht ist, dass Prozesse nur Zugriff auf ihren lokalen Speicher haben, aber miteinander durch das Versenden von Nachrichten kommunizieren können, unabhängig davon, auf welchem Prozessor oder Rechner sie gerade ausgeführt werden. Ein derartiges Modell wird beispielsweise durch MPI oder PVM dargestellt und lässt sich auf verschiedene physische Rechnerarchitekturen abbilden. Abbildung 10: Message-Passing Quelle: [BAUKE 2006] S.45 MPI ist eine Spezifikation für die Abbildung eines Modells nach dem Message-Passing-Prinzip. Es ist ein Standard, der Schnittstellen für die Sprachen C und Fortran definiert. Die Erweiterung MPI-2 stellt Schnittstellen auch für C++ bereit. Auf MPI-Standards wird hauptsächlich durch die Bezeichnungen MPI-1 und MPI-2 verwiesen. MPI-I entspricht dabei der Version MPI 1.2 von 1995. MPI-2 ist eine spätere abwärtskompatible Entwicklung, die MPI-1 um neue Funktionen erweitert. Dieser Standard wird nur von sehr wenigen MPI-Implementierungen vollständig unterstützt. MPI ist extrem umfangreich und es existieren von vielen Funktionen mehrere Varianten, die sich nur in Details voneinander unterscheiden. Daher kann MPI hier nur ansatzweise dargestellt werden. Für Details sei auf die Literatur verwiesen.56 56 Vgl. hierzu: [E M K ARNIADAKIS 2000]S.80ff., [R AUBER 2007] S.207ff, [BAUKE 2006] S.167ff., [G ROPP 2007] - 33 - 8.1 Verfügbarkeit Da MPI keine spezifische Implementation beinhaltet, existieren verschiedene MPI-Bibliotheken, die meist auf eine spezielle Rechnerarchitektur angepasst sind. Frei verfügbare Implementierungen sind z.B. Open-MPI, LAM-MPI und das vom Argonne National Laboratory stammende MPICH/MPICH2. Daneben existieren noch weitere proprietäre und teils optimierte Lösungen wie z.B. die Intel MPI Library. 8.2 Parallelisierungskonzept Eine MPI-Anwendung besteht stets aus einer konstanten Anzahl von Prozessen. Diese kann beim Programmstart variabel festgelegt werden, lässt sich jedoch zur Laufzeit nicht mehr verändern. Häufig geschieht das Aufrufen eines MPI-Programms durch ein separates Startprogramm, dass von der verwendeten Implementierung zur Verfügung gestellt wird und das entsprechend viele Instanzen des eigentlichen MPI-Programms aufruft. Da MPI auch in heterogenen Rechnerumgebungen ausgeführt werden kann, müssen alle Nachrichten auf MPI-Datentypen abgebildet werden, die von MPI bei Bedarf in die entsprechende Darstellung konvertiert werden. Um komplexere Datenstrukturen nicht Wert für Wert übertragen zu müssen, können eigene MPI-Datentypen und Strukturen definiert werden. 8.3 Speichermodell Einem MPI-Programm sind dabei lokal Daten zugeordnet, auf die frei zugegriffen werden kann. Durch das Verschicken von Nachrichten kann ein MPI-Programm mit anderen MPIProgrammen kommunizieren und so Daten austauschen. MPI definiert verschiedene Funktionen, die Verteil- und Sammeloperationen durchführen. Dabei ist es nicht möglich, dass ein Prozess unbemerkt die Daten eines anderen Prozesses verändern kann, da alle Kommunikationsoperationen stets, jedoch nicht zwingend synchron, durch alle beteiligten Prozesse aufgerufen werden müssen. 8.4 Synchronisation Barrieren und Kritische Abschnitte werden von MPI nicht unterstützt. Dies ist auch nicht notwendig, da jeder Prozess nur Zugriff auf lokalen Speicher hat. Eine Koordinierung des Programmablaufs erfolgt ausschließlich durch das Versenden von Nachrichten mit blockierenden Funktionen. MPI unterstützt bei vielen Kommunikationsfunktionen sowohl synchrone als auch asynchrone Varianten. 8.5 Globale Operatoren MPI unterstützt eine Vielzahl von Datenverteilungs- und Aggregierungsfunktionen. Scatter-, Gather und Reduktionsoperationen sind als globale blockierende Operationen implementiert, die von alle beteiligten Prozessen gleichzeitig ausgeführt werden müssen. - 34 - 9 OpenMP OpenMP57 ist eine Programmierschnittstelle (API) für Fortran und C/C++, um parallele Programme mit gemeinsamem Speicher zu realisieren. Es wurde mit der Maßgabe entworfen, vorhandenen, sequentiellen Code auf einfache Weise zu parallelisieren und dabei schrittweise vorgehen zu können. Ein Ziel von OpenMP ist, dass der vorhandene sequentielle Programmcode nicht oder nur minimal verändert und statt dessen durch Compilerdirektiven ergänzt wird. Ein OpenMP-Programm kann daher auch von einem nicht-OpenMP-fähigen Compiler übersetzt werden58 , da unbekannte Direktiven grundsätzlich ignoriert werden. Die Aufgabe des Programmierers ist es daher nicht, Algorithmen in eine parallele Version zu ändern, sondern parallelisierbare Abschnitte zu identifizieren, zu kennzeichnen und zu spezifizieren. Dazu stellt OpenMP eine Reihe von Direktiven, Bibliotheksfunktionen und Variablen bereit. 9.1 Verfügbarkeit Um OpenMP nutzen zu können, ist ein entsprechender Compiler erforderlich. In Tabelle 2 im Anhang A.2 ist eine Übersicht verschiedener Produkte aufgeführt, die OpenMP unterstützen. 9.2 Parallelisierungskonzept Grundsätzlich arbeitet OpenMP nach dem Thread-Konzept. Im Gegensatz zu den meisten anderen Konzepten, bei denen ein Programm von Anfang parallel ausgeführt wird, benutzt OpenMP das Fork-Join-Modell zur Parallelisierung, wie in Abbildung 11 dargestellt. Das bedeutet, dass zunächst nur ein einzelner Thread aktiv ist. Erst, wenn ein parallelisierbarer Programmabschnitt erreicht wird, erzeugt OpenMP neue Threads und beendet diese, sobald der Abschnitt wieder verlassen wird. Der notwendige Programmcode für die Erzeugung Synchronisierung der OpenMP-Threads wird durch den Compiler erzeugt. Wie dies konkret vonstatten geht und ob Threads oder Prozesse zum Einsatz kommen, hängt von dessen Implementierung ab und ist aus Sicht des Programmierers irrelevant. Vom Programmierer ist lediglich eine Kennzeichnung und Spezifikation der zu parallelisierenden Programmabschnitte notwendig. Hierfür stellt OpenMP eine Anzahl von Compilerdirektiven, Routinen und Variablen für die Sprachen C/C++ und Fortran bereit. 9.3 Speichermodell OpenMP implementiert ein Modell mit verteiltem Speicher, wobei dieser sowohl gemeinsam (shared) als auch einzeln (private) genutzt werden kann. Der private Speicher eines Threads liegt ausschließlich in seinem eigenen Adressraum und ist nur für ihn selbst sichtbar. Standardmäßig 57 Vgl. hierzu: [C HAPMAN 2008],[H OFFMANN 2008],[C HANDRA],[R AUBER 2008] S.127ff., [R AUBER 2007] S. 207ff., 354ff. 58 Mit Einschränkungen bei der Verwendung von OpenMP Bibliotheksfunktionen, die dann natürlich nicht Verfügbar sind, was beim Linken zu einem Fehler führt. Durch die Verwendung eines #ifdef _OPENMP-Blocks kann dies verhindert werden. - 35 - Abbildung 11: OpenMP Fork-Join-Modell Quelle: [C HAPMAN 2008] S. 24 sind alle Variablen als privat deklariert. Gemeinsame Variablen müssen in der #pragma omp parallel Direktive angegeben werden. 9.4 Synchronisation Für die Synchronisation stellt OpenMP je nach Anforderung verschiedene Mechanismen bereit. Kritische Abschnitte Abschnitte innerhalb eines Parallelen Anweisungsblocks, die Anfällig für eine Race-Condition sind, können durch Markierung mit #pragma omp critical gekennzeichnet werden. OpenMP stellt dadurch sicher, dass der Programmabschnitt nur von einem Thread zur selben Zeit ausgeführt wird. Betrifft dies nur eine kleine Anweisung, beispielsweise eine Operation auf eine gemeinsame Variable wie a=a+b, kann diese als Atomare Operation durch #pragma omp atomic gekennzeichnet werden, die für OpenMP mit geringerem Verwaltungsaufwand zur Synchronisation der beteiligten Prozesse verbunden ist. Durch die Atomare Anweisung wird sichergestellt, dass zwischen den einzelnen dafür notwendigen Prozessoranweisungen kein Prozesswechsel stattfindet. Locks Der konkurrierende Zugriff auf Ressourcen kann durch Locking-Mechanismen gesteuert werden. Im Gegensatz zur Verwendung von critical, das automatisch Locks erzeugt, ist es hier die Aufgabe des Programmierers, Locks zu erzeugen und wieder freizugeben. Dies ist notwendig, wenn auf die zu schützende Ressource nicht nur in einem einzelnen Programmabschnitt zugegriffen wird und somit nicht durch einen critical-Block geschützt werden kann. OpenMP stellt keine Funktionen zur Behandlung von Deadlock-Situationen bereit. Barrieren Barrieren lassen sich einfach durch Einfügen von #pragma omp barrier hinter einem Parallelen Abschnitt realisieren. Geordnete Ausführung In OpenMP kann die Ausführungsreihenfolge paralleler Abschnitte durch den Programmierer gesteuert werden. Dies kann z.B. Notwendig sein, wenn Ausgaben realisiert werden müssen. Ein mit #pragma omp ordered gekennzeichneter Bereich innerhalb eines Parallelen Abschnitts wird exakt so ausgeführt, als würde er seriell durchlaufen werden. - 36 - 9.5 Globale Operatoren OpenMP unterstützt keine expliziten Kommunikationsanweisungen. Globale Operatoren können daher nur indirekt durch entsprechende Spezifikation der parallelen Abschnitte genutzt werden. Hierfür wird das reduction - Konstrukt bereitgestellt: reduction ( operator : Variable [,Variable] ...) Wird beispielsweise eine Schleife, innerhalb der auf eine gemeinsame Variable schreibend zugegriffen wird, parallel ausgeführt, sind zeitaufwändige Synchronisierungsmaßnahmen erforderlich, um den Speicherinhalt für alle Threads konsistent zu halten. Dies muss jedoch nicht immer notwendig sein. Durch das reduction-Konstrukt wird die Synchronisierung erst nach Beendigung aller Iterationen unter Verwendung des angegebenen Operators durchgeführt, zum Beispiel für die Summenfunktion: int sum(int * v, int size){ result=0; #pragma omp parallel for reduction (+:result) for(i=0, i<size, i++){ result+=v[i]; } return result; } - 37 - 10 Andere 10.1 BOINC BOINC - Berkeley Open Infrastructure for Network Computing59 ist eine an der Universität Berkeley, Californien entwickelte Umgebung für das Grid-Computing. BOINC funktioniert nach dem Client-Server-Prinzip. Ein BOINC-Client wird auf einer Anzahl von Rechnern installiert und meldet sich bei einem Server an, von dem er Rechenaufgaben zugeteilt bekommt und die Resultate zurücksendet. Die BOINC-API ist sehr umfangreich und speziell für das Verteilte Rechnen mit sehr hoher Anzahl an Clients in heterogener Rechnerumgebung entwickelt. Basierend auf BOINC wurde weltweit Rechenleistung für verschiedene wissenschaftliche Projekte wie z.B. Seti@Home gebündelt. 10.2 BLAS BLAS ist eine Abkürzung für Basic Linear Algebra Subroutines und bezeichnet Bibliotheken, die lineare algebraischen Funktionen bereitstellen. Es existiert kein formaler Standard, dennoch hat sich ein konkreter Satz an Funktionen als de-facto Standard etabliert, der mit dem Begriff BLAS bezeichnet wird. Je nach Umfang werden diese in Levels 1-3 eingeordnet, wobei Level 1 Vektor-Operationen, Level 2 Vektor-Matrix-Operationen und Level 3 Matrix-Operationen unterstützt. BLAS ist keine Parallele Programmierbibliothek, jedoch existieren auch Implementierungen, die PVM, MPICH oder LAM nutzen. Eine Parallelisierung mittels BLAS gestaltet sich so, dass das zu lösende Problem in eine Reihe von Matrizenoperationen überführt wird, die dann durch BLAS-Funktionen ausgeführt werden. Für BLAS gibt es eine Reihe verschiedener, zum Teil hoch optimierter Implementierungen, die häufig von Hardware-Herstellern stammen und speziell an deren Produkte angepasst sind. Weiterhin sind z.B. auch parallel arbeitende Versionen für verschiedene MPI-Implementierungen verfügbar. Unix-Distributionen enthalten in der Regel mehrere BLAS-Implementierungen. Einige davon sind Beispielsweise „refblas3”, „scalapack” und „lapack3”. 10.3 PThreads PThreads ist eine Interface-Spezifikation zur Programmierung von Threads entsprechend dem POSIX.1-Standard für Unix-Basierte Systeme. Aktuelle Implementierungen sind LinuxThreads60 und die Native POSIX Threads Library (NPTL). 10.4 Parallel Virtual Machine PVM bildet einen virtuellen Parallelrechner mit nicht zwingend homogener Rechnerstruktur und lokal zugeordnetem Speicher ab. Die Kommunikation der beteiligten Knoten erfolgt durch 59 60 Homepage: http://boinc.berkeley.edu Wird ab glibc 2.4 nicht mehr unterstützt, stattdessen wird seit glibc 2.3.2 NPTL verwendet. - 38 - Message-Passing. PVM kann als Vorgänger von MPI betrachtet werden. PVM wird durch einen Dienst (pvmd) realisiert, der auf den beteiligten Rechnern gestartet wird und als Schnittstelle zu allen Rechnern fungiert. Die Parallelisierung erfolgt durch einen Elternprozess, der den Dienst dazu veranlasst, weitere Instanzen des Programms auf den anderen Rechnern zu starten und die Kommunikation zwischen den Instanzen zu steuern. PVMProgramme müssen daher auf allen Rechnern in ausführbarer Form vorliegen und der Nutzer muß Zugriffsrechte auf alle Rechner haben, da sowohl der Dienst als auch die Programme unter seiner Nutzerkennung laufen. - 39 - 11 Vergleichende Betrachtung Um den Programmieraufwand sowie die Rechengeschwindigkeit der vorgestellten Bibliotheken im Ansatz vergleichen zu können, wurde ein Algorithmus zur parallelen Matrizenmultiplikation in serieller Form implementiert und anschließend für die Verwendung mit den parallelen Bibliotheken angepasst. 11.1 Implementierung Im Folgenden wird die serielle Version des Algorithmus vorgestellt und auf die speziellen Merkmale der Versionen für jede der untersuchten parallelen Ansätze eingegangen. Der vollständige Quellcode ist, sofern nicht aufgeführt, auf der beiliegenden CD zu finden. Da es um den Vergleich der Techniken geht, wurde auf Optimierungen seitens des Compilers verzichtet. 11.1.1 Matrizenmultiplikation Seriell Die Serielle Version ist im Algorithmus 1 dargestellt. Das Programm besteht aus zwei Teilen, einer Funktion zur Matrizenmultiplikation und dem Hauptprogramm, dass diese aufruft und die Zeitmessung durchführt. Die Compilierung erfolgt mittels: $gcc -std=C99 standard.c -o standard 11.1.2 Matrizenmultiplikation UPC Für UPC sind nur wenige Änderungen erforderlich. Vor allem muss in der Funktion mult_matrix() die Hauptschleife durch das UPC-Pendant ersetzt werden. [...] upc_forall(int ay=0;ay<y1;ay++;ay){ [...] Weiterhin ist die Ergebnismatrix R als shared zu deklarieren. Hierbei wird eine Blockgröße entsprechend der Zeilengröße gewählt, so dass die Matrix zeilenweise auf die Threads verteilt wird: shared double * R=upc_all_alloc(dim,dim*sizeof(double)); Bei der Zeitmessung und der Ausgabe ist zu beachten, dass diese nur von einem einzigen Prozess durchgeführt wird, da es sonst zu mehrfachen Ausgaben kommt. Für die Compilierung muss ein UPC-Compiler eingesetzt werden, in diesem Fall der GCC-UPC 4.0.3.5. Die Compilierung erfolgt mittels: $upc-gcc -std=C99 -lupc -static upc.upc -o upc - 40 - Algorithm 1 Matrizenmultiplikation Seriell 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 # include # include # include # include # include # include < s t d l i b . h> < s t d i o . h> <math . h> < s t r i n g . h> < s y s / t i m e . h> < t i m e . h> i n t m a t r i x _ m u l t ( i n t x1 , i n t y1 , d o u b l e ∗ A, i n t x2 , i n t y2 , d o u b l e ∗ B , d o u b l e ∗ R ) { i f (A! =NULL && B! =NULL && R! =NULL) { i n t o f s _ a =0 , o f s _ b =0 , o f s _ r = 0 ; f o r ( i n t ay = 0 ; ay <y1 ; ay ++) { f o r ( i n t bx = 0 ; bx <x2 ; bx ++) { o f s _ r = ay ∗ x2 +bx ; R [ o f s _ r ] = 0 . 0 ; f o r ( i n t ax = 0 ; ax <x1 ; ax ++) { o f s _ a = ay ∗ x1 + ax ; o f s _ b = ax ∗ x2 +bx ; R [ o f s _ r ]+=A[ o f s _ a ] ∗B [ o f s _ b ] ; } } } } return 1; } i n t main ( i n t a r g c , char ∗ a r g v [ ] ) { i f ( a r g c < 1 ) r e t u r n −1; / / m a t r i x e r z e u g e n und i n i t i a l i s i e r e n i n t dim= a t o i ( a r g v [ 1 ] ) ; d o u b l e ∗ A= ( d o u b l e ∗ ) m a l l o c ( dim∗dim∗ s i z e o f ( d o u b l e ) ) ; d o u b l e ∗ B= ( d o u b l e ∗ ) m a l l o c ( dim∗dim∗ s i z e o f ( d o u b l e ) ) ; d o u b l e ∗ R= ( d o u b l e ∗ ) m a l l o c ( dim∗dim∗ s i z e o f ( d o u b l e ) ) ; f o r ( i n t i = 0 ; i <dim∗dim ; i ++) { A[ i ] = 1 ; B [ i ] = 1 ; R [ i ] = 0 ; } s t r u c t t i m e v a l time1 , time2 ; i n t ms1 , ms2 ; / / z e i t m e s s u n g und b e r e c h n u n g g e t t i m e o f d a y (& t i m e 1 , NULL) ; m a t r i x _ m u l t ( dim , dim , A, dim , dim , B , R ) ; g e t t i m e o f d a y (& t i m e 2 , NULL) ; ms1= t i m e 1 . t v _ s e c ∗1000000+ t i m e 1 . t v _ u s e c ; ms2= t i m e 2 . t v _ s e c ∗1000000+ t i m e 2 . t v _ u s e c ; i n t d e l t a =ms2−ms1 ; double d e l t a _ s e c =( double ) d e l t a / 1 0 0 0 0 0 0 ; p r i n t f ( "%i %f \ n " , dim , d e l t a _ s e c ) ; return 0; } - 41 - 11.1.3 Matrizenmultiplikation MPI Für MPI gibt es prinzipiell zwei Möglichkeiten der Implementierung, die jede ihre Vor- und Nachteile hat: Implementation mit Globalen Operationen oder ein Master-Worker-Modell. Die Verwendung globaler Operationen zur Verteilung der Daten erfordert weniger Aufwand als das Master-Worker-Modell, ist jedoch weniger flexibel in der optimalen Ausnutzung der Rechenkapazität. Im Gegensatz zu UPC oder OpenMP kann sich bei MPI die Parallelisierung nicht nur auf die Anpassung der Funktion matrix_mult beschränken. So muss beispielsweise beim Programmstart eine Initialisierung von MPI erfolgen. Daher wurde der Ansatz gewählt, die originale Funktion matrix_mult zu verwenden, deren Aufruf jedoch mit einem MPI-Konstrukt zu umgeben, so dass jeder Prozess nur Teile der Matrizen berechnen muss. MPI verteilt Daten stets in zusammenhängenden Blöcken. Für die Verteilung der Matrix A bedeutet dies, dass jeder Prozess mehrere aufeinanderfolgende Zeilen erhält. Da die Gesamtzahl der Zeilen nicht notwendigerweise ein Vielfaches der Prozessanzahl darstellt, ist eine ungleiche Verteilung notwendig. Die Anzahl und Größe der Pakete, die jeder Prozess empfängt, muss allen Teilnehmern beim Aufruf der Funktionen bekannt sein, d.h. der Empfänger muss wissen, wieviel Daten er bekommt, da er einen entsprechend dimensionierten Empfangspuffer bereitstellen muss. Globale Operationen Bei der Verwendung globaler Operationen erfolgt das Scheduling der Daten statisch. Dazu berechnet der Root-Prozess anhand der Prozessanzahl und der Datenmenge, wie die Aufteilung erfolgt. Der Algorithmus 2 stellt einen Auszug aus dem entsprechenden Testprogramm dar. Master-Worker-Modell Die Implementierung des Master-Worker-Modells erfolgt in der Form, dass Worker-Prozesse Nachrichten an den Master-Prozess schicken, um diesen zu veranlassen, entweder einen neuen Datenblock zu senden oder ein Ergebnis entgegenzunehmen. Das Master-Programm wird beendet, sobald alle Datenblöcke versendet und alle Ergebnisse empfangen wurden. Da der Algorithmus deutlich umfangreicher ist als bei Verwendung globaler Operatoren wird auf eine Darstellung verzichtet. 11.1.4 Matrizenmultiplikation OpenMP Der Aufwand für OpenMP ist ähnlich dessen für UPC. In der Funktion mult_matrix() werden Pragmas gesetzt, um dem Compiler mitzuteilen, dass die Indexvariablen ofs_a, ofs_b und ofs_r privat und die Matrizen A,B und R verteilt sind. Weiterhin wird eine statische Aufteilung der for-Schleife auf die einzelnen Threads festgelegt, wodurch jeder Thread die selbe Anzahl von Schleifen durchläuft. #pragma omp parallel for schedule(static) \ private(ofs_a,ofs_b,ofs_r) shared (A,B,R) for(int ay=0;ay<y1;ay++){ ... } - 42 - Algorithm 2 Matrizenmultiplikation MPI Global 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # i n c l u d e <mpi . h> // ... i n t main ( i n t a r g c , char ∗ a r g v [ ] ) { // ... i n t rank , nproc ; M P I _ I n i t (& a r g c ,& a r g v ) ; MPI_Comm_rank (MPI_COMM_WORLD,& r a n k ) ; MPI_Comm_size (MPI_COMM_WORLD,& n p r o c ) ; i n t s s i z e s [ nproc ] , s o f s [ nproc ] ; i f ( r a n k ==0) { f o r ( i n t i = 0 ; i < n p r o c ; i ++) { i n t rows =dim / n p r o c ; i f ( rows ==0) rows = 1 ; s s i z e s [ i ] = dim∗ rows ; s o f s [ i ] = dim∗ rows ∗ i ; i f ( i == n p r o c −1){ s s i z e s [ i ] + = ( dim%rows ) ∗dim ; } } } MPI_Bcast (&dim , 1 , MPI_INT , 0 ,MPI_COMM_WORLD) ; MPI_Bcast ( B , dim∗dim , MPI_DOUBLE , 0 ,MPI_COMM_WORLD) ; MPI_Bcast ( s s i z e s , n p r o c , MPI_INT , 0 ,MPI_COMM_WORLD) ; M P I _ S c a t t e r v (A, s s i z e s , s o f s , MPI_DOUBLE , A, s s i z e s [ r a n k ] , MPI_DOUBLE , 0 ,MPI_COMM_WORLD) ; m a t r i x _ m u l t ( dim , s s i z e s [ r a n k ] / dim , A, dim , dim , B , R ) ; MPI_Gatherv (A, s s i z e s [ r a n k ] , MPI_DOUBLE , A, s s i z e s , s o f s , MPI_DOUBLE , 0 ,MPI_COMM_WORLD return 0; } Das Hauptprogramm wird lediglich zu Beginn um Anweisungen zur Festlegung der Anzahl zu verwendender Prozesse ergänzt: int procs=atoi(argv[1]); omp_set_dynamic(0); omp_set_num_threads(procs); Die Compilierung erfolgt mittels des GCC-4.2: $gcc-4.2 -fopenmp -std=C99 openmp.c -o openmp 11.2 Performance Um den Einfluss des Betriebssystems beim Programmstart sowie eventuelle Initialisierungsphasen der Bibliotheken zu minimieren, wurde nur die Berechnungszeit für die eigentliche Multiplikation ermittelt. Die Berechnungszeit wurde in Abhängigkeit von Matrizengröße und Prozessanzahl gemessen. Jede Messung wurde drei mal Durchgeführt und das beste Resultat ausgewertet. Die Tests wurden auf einem Parallelrechner mit 8 Dual Core AMD Opteron(tm) Prozessoren mit 1 GHz Taktfrequenz und 32GB RAM durchgeführt. Als Betriebssystem diente Ubuntu 2.6. Außer für UPC wurde als Compiler der GCC 4.1.2 verwendet. Alle Messergebnisse sind in Tabellenform im Anhang B.2 aufgeführt. - 43 - Zunächst ist für UPC ein überraschendes Resultat festzustellen. Mit nur einem Prozessor ist die UPC-Version um etwa eine Potenz langsamer als die Serielle Variante. Dies deutet auf einen enormen Overhead hin, der durch UPC zur Speicherverwaltung erzeugt wird. Parität mit der Seriellen Version wird erst bei Verwendung von 8 Prozessoren erreicht. Abbildung 12: Performance UPC Quelle: Eigene Darstellung. Bei OpenMP ist festzustellen, dass das parallelisierte Programm auf nur einem Prozessor deutlich langsamer ausgeführt wird, als die serielle Variante. Dies stellt insofern keinen Nachteil dar, als das ein OpenMP-Programm auch ohne OpenMP-Unterstützung compiliert werden kann und dann rein seriell ausgeführt wird. Auf diese Weise können ohne programmiertechnischen Mehraufwand eine parallele und eine serielle Version des selben Programms erstellt werden. Mit steigender Prozessorzahl ist eine entsprechend gute Skalierung zu erkennen. Der Vergleich einer Master-Worker-Implementierung und einer Implementierung unter Verwendung globaler Operatoren unter MPI zeigt, dass bei wenigen Prozessoren die globalen Operatoren effizienter arbeiten und teilweise um den Faktor 2 schneller sind. Mit steigender Prozessorzahl ist jedoch das Worker-Slave-Modell leicht überlegen, da hier Differenzen in der Auslastung einzelner Prozessoren weniger starke Auswirkungen zeigen. Während bei globalen Operatoren die zu Verarbeitende Datenmenge pro Prozess fest vorgegeben ist und daher die Dauer der Gesamtoperation von dem langsamsten Prozess abhängt, erfolgt beim Master-Worker-Konzept eine lastabhängige Verteilung der Datenpakete. Ein Vergleich zwischen OpenMP und MPI zeigt, dass MPI schneller arbeitet. Hierbei wurde jedoch nur die jeweils einfachste Implementierung gewählt, so dass sich noch keine allgemeingültige Aussage über die Geschwindigkeiten der Implementierungen machen lassen. Sowohl OpenMP als auch MPI erlauben weitere Einstellungen, um die Parallelisierung effizienter zu gestalten. Abbildung 17 zeigt das Verhältnis der Rechenzeiten vom OpenMP und MPI. Für sehr kleine Problemgrößen und hoher Prozessoranzahl arbeitet MPI deutlich langsamer als OpenMP. Dieses Verhältnis kehrt sich bei steigender Problemgröße schnell um. - 44 - Abbildung 13: Performance OpenMP Quelle: Eigene Darstellung. Abbildung 14: Performance MPI (a) Master-Worker (b) Globale Operatoren Quelle: Eigene Darstellung. 11.3 Bewertung 11.3.1 UPC Der verwendete Compiler upc-gcc erzeugt Programme, die von Anfang an parallel laufen. Es ist nicht möglich, die parallele Ausführung gezielt auf bestimmte Programmabschnitte, z.B. einzelne Funktionen, zu beschränken. Daher ist das zu parallelisierende Programm sehr stark an die Anforderungen von UPC anzupassen. Die Parallelisierung eines existierenden Programms ist nur mit erheblichem Aufwand möglich. Nur sehr wenige Compiler (z.B. der Berkeley UPC) unterstützen die Verwendung von UPC-Code durch C++. Die Unterstützung der UPC-Spezifikation durch einzelne Compiler ist sehr unterschiedlich. So wurde beim Testen des UPC-GCC 4.0.3.5 mittels einfacher Beispielprogramme aus der UPC1.2 Dokumentation festgestellt, dass dieser anscheinend nicht Spezifikationsgerecht funktioniert, da sich diese nicht übersetzen lassen. So ist die schon Definition eines simplen statischen - 45 - Abbildung 15: Performance MPI vs. MPI Global Quelle: Eigene Darstellung. verteilten Arrays analog zu den Beispielen aus der Dokumentation durch beispielsweise shared [2] int[10] nicht möglich und führt zu einem Syntaxfehler. Mit shared [2] int[] lässt sich sogar ein Speicherzugriffsfehler während der Übersetzung provozieren. Die dynamische Erzeugung von verteiltem Speicher mittels upc_alloc-Varianten hingegen scheint zu funktionieren, wenn auch nicht Spezifikationsgerecht. So ist eine Festlegung der Blockgröße nicht möglich gewesen, so dass Speicherbereiche stets Byteweise über alle Threads verteilt wurden, was deutlich zu Lasten der Geschwindigkeit geht. Aufgrund der genannten Probleme wurde nach dem Release die neueste Version UPC-4.2.3.3 getestet, die jedoch das selbe Resultat lieferte. UPC ist ein Projekt, dass sich noch in der Entwicklung befindet. Das Programmierkonzept ist mit Ausnahme der Beschränkung auf reines C vom Funktionsumfang mit anderen Ansätzen zu vergleichen, lediglich die Umsetzung durch die getesteten Compiler scheint weder ausgereift zu sein, noch entspricht sie der in der Spezifikation dargelegten Funktionsweise. Daher wird UPC als nicht für den Produktiven Einsatz geeignet eingestuft. 11.3.2 MPI Ein MPI-Programm ist schwieriger zu implementieren als Beispielsweise mit OpenMP. Dafür bietet MPI mehr Freiheiten bezüglich der Strukturierung des Programms. Bei Verwendung globaler Operatoren ist der Aufwand vergleichbar mit dem für OpenMP. Eine flexible Aufgabenverteilung nach dem Master-Worker-Prinzip ist deutlich komplizierter zu implementieren, bietet dafür aber eine höhere Geschwindigkeit. In beiden Fällen ist MPI spürbar schneller als OpenMP. Ein weiterer Vorteil von MPI ist, dass sich komplexe rechnerübergreifende Kommunikationshierarchien implementieren lassen, wodurch es sich besonders für hochgradig parallele Anwendungen eignet. - 46 - Abbildung 16: Performance OpenMP vs. MPI Global Quelle: Eigene Darstellung. Abbildung 17: Performance OpenMP vs. MPI Global Quelle: Eigene Darstellung. - 47 - 11.3.3 OpenMP Eine Parallelisierung sowohl vorhandenen Codes als auch als Neuentwicklung stellt sich mit OpenMP außerordentlich einfach dar. Das Konzept der Schrittweisen Parallelisierung lässt sich gut umsetzen und es werden schnell erste Resultate erzielt. Dennoch muss der korrekten Spezifikation der parallelen Abschnitte und insbesondere der Unterscheidung zwischen privaten und nicht-privaten Variablen viel Aufmerksamkeit gewidmet werden. Im Gegensatz zu MPI kann ein OpenMP-Programm nur auf einem einzigen Rechner ausgeführt werden. Die maximale sinnvolle Parallelisierbarkeit ist dadurch entsprechend der Anzahl vorhandener Prozessoren eingeschränkt. Der Geschwindigkeitsgewinn durch Parallelisierung ist deutlich messbar, wenn auch nicht so groß wie bei MPI. - 48 - Teil III Parallelisierung im Data Mining Da sich diese Arbeit mit der Anwendung des verteilten Rechnens speziell im Data Mining befasst, gibt dieser Teil eine Einführung in das Data-Mining und die in dieser Arbeit verwendeten Verfahren. Es werden mögliche Umsetzungen für ein verteiltes Rechen im Data-Mining und exemplarisch die konkrete Implementierung zweier Ansätzes vorgestellt. Dabei wird gezeigt, dass eine Parallelisierung nicht in jedem Fall eine große Hürde darstellt, die komplizierte Programme erfordert, sondern dass Bereits mit einfachen Mitteln ein signifikanter Geschwindigkeitszuwachs erzielt werden kann. This chapter deals with the parallization of Data Mining algorithms. It provides a brief introduction into Data Mining and the methods to be parallelized. This chapter also shows that parallization does not necessarily mean complicated algorithms, but that the performance of Data Mining algorithms can be significantly improved using standard tools. - 49 - 12 Data Mining Grundlagen Data Mining ist Bestandteil einer Disziplin, die als Wissensextraktion aus Datenbanken - Knowledge Discovery in Databases (KDD) - bekannt ist. Es befasst sich mit der Gewinnung von neuem Wissen aus vorhandenen Daten. Dabei werden die Informationen, die in den Daten implizit enthalten sind, durch verschiedene Data-Mining-Verfahren extrahiert und in geeigneter Form dargestellt. Das auf diese Weise explizierte Wissen kann beispielsweise durch Modelle, Regeln oder Diagramme ausgedrückt und somit nutzbar gemacht werden.61 12.1 Ablaufmodell Als „Data Mining” bezeichnet man sowohl die Anwendung der Methoden als auch das Forschungsgebiet selbst. Da es ein relativ neues und stark experimentelles Forschungsgebiet ist, sind sowohl die Methoden als auch deren Anwendung Forschungsgegenstand. Besonders letzteres ist für den erfolgreichen Einsatz des Data Mining von Bedeutung, da hier stets auf die konkrete Zielstellung und die verfügbaren Daten eingegangen werden muss. Daher existiert kein für alle Fälle geeignetes Verfahren. Es lässt sich lediglich ein allgemeines Vorgehensmodell ableiten, das den Data-Mining-Vorgang als iteratives Phasenmodell beschreibt. Jedoch berücksichtigt dies weder konkrete Methoden noch lassen sich die einzelnen Phasen exakt gegeneinander abgrenzen, da sie sich in der Praxis teilweise überschneiden und stark aufeinander aufbauen. Es lassen sich folgende Phasen definieren: • Datenvorverarbeitung (Aufbereitung, Kodierung) • Analyse • Datennachbearbeitung (Auswertung, Visualisierung) • Interpretation Vor der eigentlichen Analyse der Daten ist eine Datenvorverarbeitung notwendig. Neben der inhaltlichen Bereinigung (Eliminieren von Fehlern, fehlenden Werten oder Ausreißern; Auswählen von Untermengen, Test- und Trainingsdaten) müssen die Daten in eine Form gebracht werden, die durch die gewählte Data-Mining-Methode verarbeitet werden kann, zum Beispiel durch Normalisierung, Skalierung, Intervallbildung oder Transformation. Die Datenerhebung als Teil der Datenvorverarbeitung hat dafür Sorge zu tragen, dass die einen Sachverhalt beschreibenden Informationen auch in der Datenmenge enthalten sind und andererseits irrelevante, aber signifikante Informationen die Datenmenge nicht verfälschen. Ebenso können ungenaue, fehlende oder falsche Werte das Ergebnis beeinträchtigen oder unbrauchbar machen. Die Datenvorverarbeitung hat entscheidenden Einfluss auf das Ergebnis der eingesetzten Methoden, da durch die Wahl ungeeigneter Vorverarbeitungsverfahren die Daten verfälscht werden können. Es ist notwendig, bei der Kodierung die Daten korrekt und reproduzierbar abzubilden und dabei deren implizite Eigenschaften und Beziehungen zu berücksichtigen. Die Datenanalyse bezieht sich auf die eigentliche Anwendung der Data-Mining-Methoden. Je nach Zielstellung finden Methoden aus den Bereichen Klassifikation, Clustering, Prognose oder Assoziation Anwendung. Um die für die zu analysierenden Daten optimale Methode aus dem 61 Dieses Kapitel wurde in ähnlicher Form vom Autor in [W ISSUWA 2003] und [C LEVE 2005] aufgeführt. - 50 - jeweiligen Bereich zu ermitteln, sind umfangreiche Experimente mit verschiedenen Methoden und Parametern notwendig. Die Validierung dient der Überprüfung des Data-Mining-Ergebnis auf tatsächliche Korrektheit. Dies geschieht meist durch Anwenden des erstellten Modells auf ausgewählte Testdaten. Datenvorverarbeitung (inkl. Datenerhebung) sowie die Validierung sind dem Data-Mining-Prozess vor- bzw. nachgelagert. Beide sind stark anwendungsbezogen und daher nur bedingt dem Data Mining selbst zuzuordnen. Jedoch haben sie großen Einfluss auf den Erfolg des Data Mining. Ein ähnliches Modell wurde durch das CRISP-DM Konsortium erstellt, das mit dem Ziel entwickelt wurde, ein generelles Vorgehensmodell für den praktischen Einsatz von Data Mining in Unternehmen zu bieten. Abbildung 18: CRISP-DM Phasenmodell Quelle: http://www.crisp-dm.org 12.2 Klassifikation der Verfahren In der Literatur ist eine Vielzahl von Data-Mining-Algorithmen beschrieben, von denen viele in der Praxis mit Erfolg eingesetzt werden.62 Die Anwendungsmöglichkeiten des Data Mining sind außerordentlich vielfältig. Es lassen sich drei grundlegende Formen der Anwendung unterscheiden: 1. Klassifikation, 2. Assoziation, 3. Clustering, 4. Vorhersage. Bei der Klassifikation werden überwachte Lernverfahren eingesetzt. Es wird anhand der Attribute von bereits klassifizierten Objekten ein Klassifikator erzeugt, der in der Lage ist, unbekannte Objekte ebenfalls korrekt zu klassifizieren. Als Klassifikatoren können zum Beispiel 62 Vgl.: [W ITTEN 2001],[W ITTEN 2005],[L ÄMMEL 2004],[L ÄMMEL 2003],[A LPAR 2000],[NAKHAEIZADEH 1998] - 51 - Entscheidungsbäume, k-Nearest-Neighbour oder Neuronale Netze zum Einsatz kommen. Typische Anwendungen sind z. B. Klassifikation von Kunden oder die Schrifterkennung. Die Assoziation unterscheidet sich von der Klassifikation dadurch, dass nicht nur die Klasse, sondern die Ausprägungen beliebiger Attribute und Attributkombinationen eines Objektes prognostiziert werden können. Als Methoden kommen hier der a-priori-Algorithmus oder wiederum Neuronale Netze in Betracht. Beispielanwendungen findet man in der Warenkorbanalyse oder beim Wiederherstellen eines verrauschten oder fehlerhaften Pixelmusters anhand eines vorher trainierten Beispiels. Das Clustering, ein unüberwachtes Verfahren, wird zur Bildung von Gruppen (Cluster) einander ähnlicher Objekte aus einer Grundmenge eingesetzt. Dabei kommen verschiedene multivariate Verfahren zum Einsatz. Es kann verwendet werden, um die Attribute herauszufinden, die wesentliche Merkmale einer Gruppe darstellen oder durch die sie sich von anderen Gruppen unterscheiden. Bekannte Clustering-Verfahren sind das K-Means-Verfahren und die von Teuvo Kohonen entwickelten Selbstorganisierenden Karten. Die mathematisch motivierten Support Vector Machines stellen ebenfalls einen erfolgversprechenden Clustering-Ansatz dar. Die Vorhersage ähnelt der Klassifikation. Sie dient der Bestimmung von Zielgrößen anhand gegebener Attributausprägungen. Im Gegensatz zur Klassifikation liefert die Vorhersage jedoch quantitative Werte. - 52 - 12.3 Künstliche Neuronale Netze Als Ansatz zur Parallelisierung im Data Mining sollen zwei Vertreter der Künstlichen Neuronalen Netze dienen: Feed-Forward-Netze und Selbstorganisierende Karten. Dieses Kapitel gibt eine grobe Einführung in die zugrundeliegenden Konzepte beider Verfahren. Für eine Detaillierte Darstellung sei auf die Literatur verwiesen.63 Künstliche neuronale Netze sind eine stark idealisierte Nachbildung der Funktionsweise von biologischen Neuronalen Netzen, wie beispielsweise Gehirne oder Nervensysteme. Die Verarbeitung von Informationen erfolgt nicht durch komplexe Algorithmen, sondern vielmehr durch sehr einfache Einheiten, die allerdings in großer Zahl vorhanden sind und untereinander Informationen austauschen. Analog zu ihren biologischen Vorbildern bestehen künstliche neuronale Netze aus Neuronen und einem Verbindungsnetzwerk. Die Informationsverarbeitung erfolgt mit Hilfe der Propagierungsfunktion, die für jedes Neuron den Aktivierungszustand anhand der von anderen Neuronen eingehenden Signale und der Verbindungsgewichte errechnet. Ein daraus abgeleitetes Ausgabesignal wird dann an andere Neuronen über das Verbindungsnetzwerk weitergeleitet. Ein künstliches neuronales Netz kann als gerichteter Graph mit gewichteten Kanten angesehen werden, durch den definiert wird, welche Neuronen miteinander kommunizieren. Die Gewichte dienen der Hemmung oder Verstärkung von Signalen. Zusammen mit dem Schwellwert (Bias) der Neuronen, der bestimmt, ab welchem Aktivierungsgrad ein Neuron aktiv wird und Signale aussendet, sind es die Gewichte, in denen das “Wissen” des Netzes gespeichert wird. Durch diese verteilte Repräsentation des Wissen sind neuronale Netze relativ unempfindlich gegenüber unvollständigen und verrauschten Eingabemustern. Die Anordnung der Neuronen erfolgt typischerweise in Schichten von Neuronen gleichen Typs. Die zu verarbeitenden Daten werden über Neuronen der Eingabeschicht an das Netz transferiert, indem diesen eine initiale Aktivierung entsprechend dem jeweiligen Trainingsmuster zugewiesen wird. Diese Aktivierungen werden entsprechend der Propagierungsfunktion durch das Netz verarbeitet. Nach vollständiger Propagierung kann die sogenannte „Netzantwort” als Aktivierungswerte der Neuronen der Ausgabeschicht abgelesen und interpretiert werden. Zwischen Eingabe- und Ausgabeschicht können sich weitere Schichten befinden, die als Versteckte Schichten bezeichnet werden. Der Einsatz Neuronaler Netze sind immer dann sinnvoll, wenn sich andere Lösungsansätze wie zum Beispiel algorithmische Lösungen oder eine regelbasierte Wissensdarstelllung als nicht geeignet herausgestellt haben. Natürlich funktionieren Künstliche Neuronale Netze ebenfalls nach einem Algorithmus, doch dient dieser nicht der Lösung eines Problems, sondern der Anpassung des Netzes an das Problem. 12.3.1 Feed-Forward-Netze Feed-Forward-Netze gehören zu den Klassifikatoren. Sie sind sind dadurch gekennzeichnet, dass die Propagierung nur in eine Richtung von der Eingabeschicht hin zur Ausgabeschicht erfolgt. Es existiert keine Rückkopplung, das Netz ist ein zyklenfreier Graph.64 63 64 Siehe dazu: [Z ELL 2000],[L ÄMMEL 2004] Vgl.: [Z ELL 2000] S.78 - 53 - Bei Feed-Forward-Netzen werden überwachte Lernverfahren eingesetzt. Dabei wird dem Netz ein Eingabemuster präsentiert, das nach der Propagierung ein Ausgabemuster erzeugt. Durch den Vergleich der Ausgabe mit der gewünschten Ausgabe (teaching input) kann für jedes Ausgabeneuron ein Fehlerwert entsprechend der Abweichung zur gewünschten Ausgabe errechnet werden. Basierend auf diesem Fehlersignal, den Verbindungsgewichten und der Aktivierung der Neuronen kann berechnet werden, wie die Verbindungsgewichte im Netz verändert werden müssen, damit der Fehler minimiert wird. Dieser Vorgang wird iterativ für alle Trainingsmuster durchlaufen bis der Fehler hinreichend klein ist. Ein trainiertes Feed-Forward-Netz ist in der Lage, neue Eingabemuster entsprechend ihrer Ähnlichkeit zu gelernten Mustern zu klassifizieren. 12.3.2 Selbstorganisierende Karten Unüberwachte Lernverfahren finden überall dort Anwendung, wo Datenmengen nach unbekannten Regeln zu klassifizieren sind, was beim Data Mining meist der Fall ist. Ein gutes Beispiel für die Anwendung unüberwachten Lernens sind die von Teuvo Kohonen entwickelten Selbstorganisierenden Karten, auch Feature Maps genannt.65 Eine SOM ist ein Neuronales Netz dessen Neuronen typischerweise als zweidimensionale Gitterstruktur angeordnet sind. Die Anzahl der Neuronen beeinflusst die Genauigkeit und die Fähigkeit zur Generalisierung der SOM. Jedes Neuron besitzt einen gewichteten Vektor W als Verbindung mit den Neuronen der Eingabeschicht, deren Anzahl n der Variablen im Eingaberaum entspricht: Wi = [wi,1 , ..., wi,n ] Das Trainieren der SOM beginnt mit der Initialisierung der Gewichtsvektoren durch Zufallszahlen im Intervall [-1,1]. Während des Trainings wird ein zufällig gewählter Eingabevektor I mit dem Gewichtsvektor W jedes Neurons der Kartenschicht verglichen. Das Neuron, dessen Gewichtsvektor der Eingabe am ähnlichsten ist, wird das Gewinnerneuron (BMU, Best Matching Unit). Als Abstandsmaß wird in der Regel die Euklidische Distanz verwendet. v u n uX d(I, W ) = t (Ii − Wi )2 i=0 Der Gewichtsvektor W des Gewinnerneurons wird anschließend so verändert, dass er dem Eingabevektor I ähnlicher wird. Das gleiche gilt für die Neuronen innerhalb eines Radius um das Gewinnerneuron. Der Grad der Beeinflussung wird durch den Lernfaktor η und die Distanzfunktion h (meist Gauss-Funktion) bestimmt, die normalerweise nach jedem Trainingsschritt reduziert werden: Wj (t + 1) = Wj (t) + η(t)hcj (t)[X(t) − Wj (t)] 65 Vgl.:[KOHONEN 2001], [Z ELL 2000] S179ff. - 54 - Eine SOM hat die Eigenschaft einer topologieerhaltende Transformation eines hochdimensionalen Eingaberaumes auf eine niedrigere Dimension. Ähnliche Eingabevektoren werden dabei auf benachbarte Neuronen projeziert. Da meist zweidimensionale SOMs zum Einsatz kommen, ist eine Visualisierung der Ergebnisse relativ einfach. - 55 - 13 Parallele Algorithmen Die augenscheinlichste Möglichkeit zur Parallelisierung im Data-Mining stellt der verwendete Algorithmus selbst dar. Abhängig davon, wie der Zugriff auf die Daten erfolgt, kann dies sogar sehr einfach geschehen, wie im Folgenden anhand künstlicher Neuronaler Netze skizziert wird. 13.1 Serielle Selbstorganisierende Karte Als Ausgangspunkt soll eine Kohonen-Karte dienen, die auf herkömmliche, serielle Weise implementiert ist. Ausgehend davon werden parallele Varianten implementiert, um verschiedene Ansätze miteinander vergleichen zu können. Das Netz wird in Form von Schichten von Neuronen abgebildet. Eine Schicht repräsentiert dabei eine Anzahl von Neuronen gleichen Typs. Schichten können miteinander vernetzt werden. Dies entspricht der vollständigen Vernetzung der Neuronen untereinander. Die Struktur einer Schicht kann wie folgt implementiert werden: typedef struct { char type; int size; int wsize; void * source; double * act; double * bias; double * error; double * weights; } t_layer; // // // // // // // // Neuronen-Typ Anzahl Neuronen Anzahl Gewichte pro Neuron Vorhergehende Schicht Aktivierungs-Vektor BiasVektor Fehler-Vektor Gewichts-Matrix Die Gewichtsmatrix W einer Schicht, wobei i die Anzahl der Quell-Neuronen und j die Anzahl der Ziel-Neuronen ist, stellt sich wie folgt dar: w0;0 w1;0 w0;1 ... Wij = ... ... w0;j ... ... wi;0 ... ... ... ... ... wij Neuronen als Arrays ihrer Eigenschaftswerte zu speichern, anstelle eine eigene NeuronenStruktur zu verwenden, hat den Vorteil, dass die Propagierungsfunktionen als Matrix-Operationen dargestellt werden können, die direkt auf den entsprechenden Schichten operieren. Das ist zum einen sehr schnell, zum anderen lassen sich Matrix-Operationen sehr leicht parallelisieren. Eine Propagierungsfunktion, die Aktivierungen einer Schicht mit den entsprechenden Gewichten auf die Aktivierung der nachfolgenden Schicht abbildet, hat somit die Form: Ai+1 = Ai ∗ Wi Analog dazu lässt sich auch die Rückwärtspropagierung des Fehlersignals z.B. beim Backpropagation-Lernverfahren als Matrizenoperationen darstellen: - 56 - Ei = Ei+1 ∗ Wi ∗ Oi Algorithmus 3 zeigt eine Implementierung für das Trainingsverfahren einer Kohonen-Karte. Zur Bestimmung des Gewinnerneurons wurde nicht der Euklidische Abstand, sondern das maximale Skalarprodukt verwendet, weshalb Eingabe- und Gewichtsvektoren in normalisierter Form vorliegen müssen. Algorithm 3 SOM Seriell 1 do{ 2 for(int i=0;i<nPatterns;i++){ 3 4 // propagation 5 for(int j=0;j<I->size;j++) { I->act[j]=pattern[i][j]; } 6 matrix_mult(O->wsize,O->size,O->weights, 1,I->size,I->act, O->act); 7 8 // find winner 9 id_max=0; 10 for(int j=0;j<nOutput;j++){if(O->act[j] > O->act[id_max]) id_max=j;} 11 12 maxx=id_max % sx; 13 maxy=id_max / sx; 14 15 // adjust weights 16 for(int j=0;j<nOutput;j++){ 17 dx=maxx - (j%sx); dy=maxy - (j/sx); 18 19 rate=sqrt( (double)(dx*dx + dy*dy)); 20 rate=factor * exp (- ((rate*rate) / (radius*radius)) ); 21 len=0.0; 22 for(int k=0;k<nInput;k++){ 23 O->weights[j*nInput+k]+=rate*(I->act[k]-O->weights[j*nInput+k]); 24 len+=O->weights[j*nInput+k]*O->weights[j*nInput+k]; 25 } 26 27 len=1.0/sqrt(len); 28 for(int k=0;k<nInput;k++){ 29 O->weights[j*nInput+k]*=len; 30 } 31 } 32 radius*=radius_factor; 33 } 34 }while(++cycle < cycles); 13.2 Parallele Selbstorganisierende Karte Die Berechnungszeit für einen Trainingszyklus einer selbstorganisierenden Karte hängt hauptsächlich von der Anzahl der Trainingsmuster sowie der Größe der Kartenschicht ab. Ein Trainingsschritt läuft dabei in 3 Stufen mit unterschiedlichem Rechenaufwand ab: 1. Propagierung des Eingabemusters: O(n2 ) - 57 - 2. Bestimmung des Gewinnerneurons: O(n) 3. Anpassung der Gewichte: O(n2 ) Die einzelnen Stufen sind nicht unabhängig voneinander, so dass eine Paralleliserung nicht durch Aufteilung der Trainingsmuster erfolgen kann. Stattdessen kann versucht werden, den Trainingsschritt zu parallelisieren. Die besten Kandidaten stellen die Stufen 1 und 3 dar. Im Folgenden wird die Parallelisierung unter Verwendung von OpenMP dargestellt. Der Bezug zum jeweils Abgebildeten Quelltext findet durch Angabe der Zeilennummer in Klammern statt. Ausgehend von Algorithmus 3 kann die Parallelisierung der einzelnen Stufen durch OpenMP mit wenig Aufwand durch das Hinzufügen von Compilerdirektiven erfolgen. Die parallelisierte Variante ist in Algorithmus 4 dargestellt. Da sich innerhalb der äußersten Schleife parallelisierbare (z.B. Schleifen) und nicht-parallelisierbare Abschnitte (z.B. Anpassung des Trainingsradius) abwechseln, ist es wenig sinnvoll, die Abschnitte einzeln zu parallelisieren, da so der zusätzliche Aufwand für die Erzeugung einzelner Prozesse den Geschwindigkeitsgewinn durch die parallele Berechnung schnell zunichte machen würde. Die Aufteilung in einzelne Prozesse sollte stets so weit außen wie möglich erfolgen (1). Die Initialisierung der Eingabeschicht muss für alle Prozesse zum selben Zeitpunkt stattfinden, da ansonsten die anschließende Propagierung unterschiedliche Resultate liefern würde. Dies wird durch den Prozess vorgenommen, der die Schleife zur Initialisierung als erstes Erreicht. Gleichzeitig wird implizit eine Barriere errichtet (7). Die Propagierung mittels Matrizenmultiplikation kann durch Verwendung des parallelen Multiplikationsalgorithmus wie in Abschnitt 11.1.4 dargestellt erfolgen (10). Dieser muss jedoch leicht modifiziert werden, da zum Zeitpunkt des Funktionsaufrufs das Programm bereits parallel ausgeführt wird. Somit ist dort die Zeile #pragma omp parallel for schedule(static) ... durch #pragma omp for schedule(static) ... zu ersetzen. Nach der Bestimmung des Gewinnerneurons durch genau einen Prozess (12) erfolgt die Anpassung der Gewichte. Dies kann parallel für jedes Neuron der Kartenschicht erfolgen. Die jeweiligen Abstände zum Gewinnerneuron, der Lernradius sowie die Länge des Gewichtsvektors für die Normalisierung müssen als privat deklariert werden (18). Abschließend wird nach jeder Iteration über die Trainingsmuster der Lernradius angepasst, was wiederum nur einmal pro Iteration geschehen darf und daher nur von einem Prozess durchgeführt wird (24). - 58 - Algorithm 4 Parallele SOM mit OpenMP 1 #pragma omp parallel 2 { 3 do{ 4 for(int i=0;i<nPatterns;i++) 5 { 6 // propagation 7 #pragma omp single 8 for(int j=0;j<I->size;j++){ I->act[j]=pattern[i][j]; } 9 10 matrix_mult(O->wsize,O->size,O->weights, 1,I->size,I->act, O->act); 11 12 #pragma omp single 13 { 14 // find winner 15 ... 16 } 17 18 #pragma omp for schedule(static) private(dx,dy,len,rate) 19 // adjust weights 20 for(int j=0;j<O->size;j++){ 21 ... 22 } 23 } 24 #pragma omp single 25 radius*=radius_factor; 26 } 27 } while(++cycle < cycles); 13.3 Performance Test In Abbildung 19 ist dargestellt, wie sich für verschiedene Problemgrößen die Steigerung der Parallelität auf die Rechenzeit auswirkt. Es ist sehr gut zu erkennen, dass eine zu starke, nicht an die Problemgröße angepasste Parallelisierung zu steigenden Rechenzeiten führt. Für größere Kohonen-Karten ist die Rechenzeit und der Speedup in Abhängigkeit von der Kartengröße in Abbildung 20 dargestellt. Es ist zu sehen, dass für p=12 der Speedup im Vergleich zu p=4 im Verhältnis nur gering ist. - 59 - Abbildung 19: Rechenzeit SOM OpenMP für kleine Karten Quelle: Eigene Darstellung. Abbildung 20: SOM OpenMP Rechenzeiten und Speedup nach Kartengröße Quelle: Eigene Darstellung. - 60 - 14 Parallele Modelle Verschiedene Gründe können gegen den Einsatz eines parallelen Algorithmus sprechen. Dies ist beispielsweise der Fall, wenn keine passende parallele Implementierung existiert und auch keine Anpassung des Quellcodes möglich ist, weil dieser zu komplex, nicht verfügbar oder der Einsatz ein einmaliges Projekt ist, dass den Aufwand nicht rechtfertigt. Wenn es nicht möglich ist, den Algorithmus zur Erzeugung eines Modells parallel auszuführen, kann stattdessen versucht werden, das Modell selbst zu Parallelisieren. Man kann ein Modell als Klassifikator betrachtet, der eine nichtsymmetrische Abbildung des Merkmalsraumes Rd auf Klassen realisiert: ( y f : Rd → C, x 7→ f (x) := ∅ dist(x, y) ≤ δ, y ∈ C sonst Anstelle eines einzelnen Klassifikators können auch mehrere Klassifikatoren verwendet werden, die jeweils eine Abbildung auf Teilmengen vornehmen. Die letztendliche Abbildung wird durch einen zusammengesetzten Klassifikator vorgenommen, der die entgültige Abbildung durch die Kombinationsfunktion σ()bestimmt: ( y fn : R → Cn , x 7→ fn (x) := ∅ d dist(x, y) ≤ δ, y ∈ C sonst fN : Rd → C, x 7→ fN (x) := σ(f1 (x), ..., fn (x)) Diese Vorgehensweise ist dann interessant, wenn dadurch die Komplexität der Berechnung jedes einzelnen Klassifikators gesenkt werden kann. 14.1 Hierarchische Kohonen-Karten Das Trainieren von Kohonen-Karten mit großer Eingabeschicht, großer Kohonen-Schicht und vielen Trainingsmustern ist sehr Rechen- und damit Zeitaufwändig. Eine Parallelisierung kann durch Zerlegung der Karte in kleinere Karten erfolgen, die unabhängig voneinander berechnet werden können, so dass ein paralleler Algorithmus nicht notwendig ist. Gleichzeitig sinkt der Rechenaufwand pro Karte, da weniger Neuronen und damit weniger Gewichtsvektoren vorhanden sind. 14.1.1 Konzept Diese Herangehensweise wird dadurch motiviert, dass während des Trainings einer KohonenKarte tatsächlich zu Beginn eine derartige grobe Aufteilung stattfindet, wie in Abbildung 21 zu sehen ist. Die Abbildung zeigt eine U-Matrix-Darstellung einer Karte in verschiedenen Stadien des Trainings. Neuronen, deren Gewichtsvektoren sich stark von denen benachbarter Neuronen unterscheiden, sind dunkel dargestellt, Cluster sind als helle Gebiete zu erkennen. Es ist deutlich zu sehen, dass sich zu Beginn mehrere sehr großflächige Bereiche herausgebildet haben. Bei diesem Ansatz sind jedoch folgende Faktoren zu berücksichtigen: - 61 - Abbildung 21: U-Matrix einer Kohonen-Karte (a) Initiale Karte (b) 2 Trainingszyklen (c) 5 Trainingszyklen (d) 10 Trainingszyklen Quelle: Eigene Darstellung. 1. Das Aufteilen der Trainingsmuster muss so erfolgen, dass die Teilkarten nur mit den Mustern trainiert werden, die für sie relevant sind. Es ist also eine Vorauswahl zu treffen. 2. Durch die Aufteilung der Karte könnten Nachbarschaftsbeziehungen zwischen Clustern gestört werden. Läge ein Gewinnerneuron am Rand einer Teilkarte, würde es bei entsprechend Großem Trainingsradius auch Neuronen benachbarter Karten beeinflussen müssen. Laut Punkt 1. stellt sich zunächst die Frage, wie die Datensätze auf die einzelnen Teil-Karten zu verteilen sind, da genau dies - die Gruppierung ähnlicher Datensätze - ja eigentlich durch die Karte selbst erfolgen soll. Hierfür bietet sich ein mehrstufiges Clustern an, bei dem zunächst eine grobe Aufteilung der Datensätze P durch eine sehr kleine Kohonen-Karte mit wenigen Neuronen vorgenommen wird. Anschließend werden für jeden Cluster bzw. jedes Neuron n eine neue Karte erzeugt und mit den diesem Cluster zugeordneten Trainingsmustern Pn ⊂ P, dist(P, Wn ) → min trainiert. Punkt 2. ist schwieriger zu Lösen. Ein Ansatz wäre, die Teilkarten erst dann zu erzeugen, wenn der Trainingsradius klein genug ist, dass die Distanzfunktion hcj (t) Werte nahe Null liefert und somit der zweite Summand der Lernfunktion ebenfalls gegen Null geht: Wj (t + 1) = Wj (t) + η(t)hcj (t)[X(t) − Wj (t)] Um beim späteren Zusammenfügen der Karten ein konsistentes Kartenbild zu erhalten, müssen die Teilkarten eine Gewichtung der Neuronen aufweisen, die die Nachbarschaftsbeziehung so abbildet, dass Neuronen an den Rändern benachbarter Karten ähnlich sind. Dies kann durch - 62 - Abbildung 22: Aufteilung einer SOM Quelle: Eigene Darstellung. Interpolation der Gewichtsvektoren erreicht werden. Wie später gezeigt wird, hat jedoch keinen Einfluss auf die Abbildungsqualität der Karte, kann jedoch das Identifizieren von Clustern erleichtern, die aus mehreren Neuronen bestehen, da so die Nachbarschaftsbeziehungen gewährleistet werden. Abbildung 23: Interpolation der Gewichte Quelle: Eigene Darstellung. Sei K die ursprüngliche Kohonen-Karte und K(x,y) die Teilkarte für das Neuron n ∈ K mit den Dimensionen sx und sy. Für jedes Neuron n̄ ∈ K(x,y) kann durch Interpolation ein initialer Gewichtsvektor ermittelt werden. Es ist dafür eine Abstandsfunktion ∆(n̄, n) für jedes Neurons n̄ entsprechend dessen Abstandes zu den Neuronen in K sowie eine Gewichtungsfunktion f (n̄, n) notwendig, um die Gewichtsvektoren entsprechen der Entfernung zwischen den Neuronen unterschiedlich stark zu berücksichtigen. Dabei ist zu Beachten, dass K und K̄ verschiedene - 63 - Koordinatensysteme verwenden. Die entsprechende Abstandsfunktion ∆(n̄, n) für den Euklidischen Abstand lautet: s ∆(n̄, n) = 1 xn̄ + xK + − xn 2 sx 2 + 1 yn̄ 1 + yK + 2 − yn 2 sy 2 Als Gewichtungsfunktion kann eine beliebige stetige, monoton fallende Funktion verwendet werden, zum Beispiel eine quadratisch fallende Funktion der Form f (n̄, n) = 1 ∆(n̄, n) oder die Gauss’sche Distanzfunktion, die häufig zur Bestimmung des Nachbarschaftsradius verwendet wird: f (n̄, n) = e−( ∆(n̄,n) 2 d ) Der interpolierte Gewichtsvektor ergibt sich somit aus: i<N 1X W̄n̄ = Wi ∗ f (n̄, ni ) N i=0 Eine andere Möglichkeit ist die von Kohonen vorgeschlagene lokale Interpolation für wachsende SOMs, die jeweils nur die direkt benachbarten Neuronen von K(x,y) in K berücksichtigt.66 14.1.2 Umsetzung Ist eine geeignete nicht-interaktive Umgebung zur Simulation Neuronaler Netze vorhanden, lässt sich dieses Konzept vollständig mit Hilfe der Verfügbaren Tools sowie der Shell-SkriptProgrammierung lösen. Hier wird das Konzept anhand des SNNS und der mitgelieferten Tools gezeigt. Hierfür sind folgende Komponenten notwendig: 1. Erzeugen der Kohonen-Karten 2. Trainieren der Kohonen-Karte 3. Bestimmung der Gewinnerneuronen für jedes Trainingsmuster 4. Aufteilung der Trainingsmuster Der SNNS arbeitet mit Dateien für die Ein- und Ausgabe sowie Konfiguration. Wenn im Folgenden von der Erzeugung von Mustern oder Netzen die Rede ist, so ist damit das Generieren einer entsprechenden Datei gemeint. Relevant sind hier die folgenden Dateiformate: • .pat : Pattern-Dateien enthalten Trainings- und Testmuster. • .net : Netz-Datei enthalten eine Beschreibung eines Neuronalen Netzes inklusive Topologie, Verbindungen, Aktivierungs- und Lernfunktionen. 66 Siehe [KOHONEN 2001] S. 172f. - 64 - • .res : Result-Dateien sind ähnlich wie Pattern-Dateien aufgebaut und können Muster für Input, Teaching-Output sowie Output enthalten. • .batchman : Skript-Dateien stellen die Eingabe für den SNNS-Batch-Prozessor dar, mit dem sich der SNNS per Kommandozeile bedienen lässt. Erzeugen von Kohonen-Karten Eine Kohonen-Karte lässt sich leicht mit dem Tool ff_bignet erzeugen. Der Aufruf zum Erzeugen einer Karte mit einer Eingabeschicht der Dimension [X1,Y1] sowie einer Kohonen-Schicht der Dimension [X2,Y2] lautet: $ff_bignet -p X1 Y1 Act_Identity Out_Identity input \ -p X2 Y2 Act_Identity Out_Identity hidden \ -l 1 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 DATEINAME.net Das Kommando wurde zur besseren Benutzbarkeit durch ein Shell-Skript create_kohonen.sh gekapselt. Die Syntax für den Aufruf lautet: $create_kohonen.sh #Inputs X-Groesse Y-Groesse Dateiname Als Aktivierungsfunktion der Kartenschicht wurde die Identität (Act_Identity) gewählt. Die Euklidische Distanz (Act_Euclid) als Aktivierungsfunktion ist sowohl ungeeignet als auch nutzlos. Dies hat folgende Gründe: 1. Der SNNS bestimmt während des Trainings das Gewinnerneuron der Kartenschicht anhand des maximalen Skalarproduktes aus Eingabevektor und Gewichtsvektor, die Aktivierungsfunktion wird dabei ignoriert. Dieses Verhalten entspricht der Verwendung von Act_Identity. Die Funktion Act_Euclid existiert lediglich zum Zweck der Visualisierung durch die Grafische Oberfläche XGUI. 2. Um die SOM in ein Feed-Forward-Netz umzuwandeln, wird eine Aktivierungsfunktion benötigt, die dem Neuron mit dem geringsten Euklidischen Abstand zum Eingabevektor die höchste Aktivierung zuweist. Act_Euclid würde hier stattdessen die geringste Aktivierung liefern und ist somit ungeeignet. 3. Es wurde festgestellt, dass der Bias-Wert während des Trainings extrem hohe Werte erreichen kann, obwohl eine Anpassung des Bias in der Theorie der Kohonen-Karten nicht vorgesehen ist. Der SNNS-Quellcode zeigt, dass der Bias-Wert als Zähler dafür genutzt wird, wie häufig ein Neuron Gewinnerneuron war. Daher müssen entweder alle Bias-Werte nach dem Training explizit auf 0,0 gesetzt werden, oder es wird eine Aktivierungsfunktion verwendet, die den Bias-Wert nicht benutzt, was bei Act_Identity der Fall ist. Trainieren der Kohonen-Karte Für das Trainieren einer Karte ist ein entsprechendes Batchman-Skript notwendig, dass die notwendigen Dateien lädt, die Trainingsfunktion konfiguriert und die Trainingszyklen durchführt (Auszug): loadNet(infile) loadPattern(patfile) setInitFunc("Randomize_Weights",-1.0,1.0) initNet() - 65 - setLearnFunc("Kohonen",0.8 ,radius, factor ,factor, width) setUpdateFunc("Kohonen_Order") setShuffle(TRUE) while radius>0.7 do trainNet() radius=radius*(factor^PAT) endwhile saveNet(outfile) Dieses Skript verwendet Variablen, deren Werte jedoch im Skript fest definiert sein müssen, weil Batchman keine Möglichkeit bietet, diese zur Laufzeit festzulegen und z.B. durch Kommandozeilen-Parameter zu übergeben. Da verschiedene Kohonen-Karten verwendet werden und daher verschiedene Werte für die Trainingsfunktion notwendig sind, ist es ratsam, das Skript dynamisch zu generieren. Dies wurde durch das Skript train_kohonen.sh realisiert. Die Syntax für den Aufruf lautet: $train_kohonen.sh Netz-Datei Ausgabe-Datei Pattern-Datei \ Kartenbreite Zyklen Faktor Bestimmung der Gewinnerneuronen Nachdem eine Karte trainiert wurde, müssen für jedes Trainingsmuster das Gewinnerneuron ermittelt werden, dass den Cluster repräsentiert, dem das Trainingsmuster zugeordnet wurde. Der SNNS ist in der Lage, eine Result-Datei zu generieren, die für jedes Trainingsmuster die Aktivierungswerte aller Ausgabeneuronen enthält. Das Problem ist, dass eine Kohonen-Karte im SNNS keine Ausgabeneuronen besitzt, sondern die Karten-Schicht aus Versteckten Neuronen (Hidden Layer) besteht. Wenn man jedoch alle Versteckten-Neuronen in Ausgabe-Neuronen umwandelt, erhält man ein einschichtiges Feed-Forward-Netz, womit sich dann problemlos die besagte Result-Datei erzeugen lässt. Hierfür ist es lediglich Notwendig, die Definition des Neuronen-Typs in der Netzwerk-Datei zu verändern, was sich mit einem einfachen Kommando bewerkstelligen lässt: $sed ’s/| h/| o/g’ < infile.net > outfile.net Anschließend kann wie schon beim Training der Kohonen-Karte ein Batchman-Skript erstellt werden, welches Netz- und Patterndatei lädt und die Erzeugung der Result-Datei veranlasst. Auch hier ist es ratsam, das Skript dynamisch zu erzeugen. loadNet(netfile) loadPattern(patfile) setUpdateFunc("Topological_Order") setShuffle(FALSE) saveResult(resfile, 1, PAT, TRUE, FALSE, "create") Das hier beschriebene Vorgehen wird durch das Shell-Skript apply_kohonen.sh realisiert. Die Syntax für den Aufruf lautet: $apply_kohonen.sh network.net pattern.net result.res Anschließend müssen aus den Ausgabemustern in der Result-Datei die entsprechenden Gewinnerneuronen bestimmt werden. Das fehlerfreie Parsen einer beliebigen Result-Datei erfordert - 66 - jedoch ein komplizierteres Skript, so dass hier auf das Tool cnv_res2sxml aus dem SXMLSNNS-Interface67 zurückgegriffen wird. Da dieses XML-Dateien erzeugt, müssen diese noch in CSV-Dateien umgewandelt werden. $cnv_res2sxml mod=number input=false teaching=false \ output=false file=pattern0.res -nostdin | mod_process -f \ xml2csv.sxml 2>/dev/null | grep -v winner > pattern.csv Die so erzeugte Datei pattern.csv enthält für jedes Muster eine Zeile mit der Nummer des Gewinnerneurons. Mittels $grep -n $a < pattern.csv | cut -f1 -d":" > subset0_$a.sub kann daraus eine Datei mit der Nummer aller Trainingsmuster erzeugt werden, die als Gewinnerneuron den Wert von $a haben. Diese Datei kann als Eingabe für das Tool pat_sel_simple verwendet werden, um aus der ursprünglichen Pattern-Datei eine Untermenge von Mustern zu bilden. Damit ist die Aufteilung der Trainingsmuster abgeschlossen. Erzeugen von Teilkarten Für jede Untermenge an Trainingsmustern kann nun erneut eine Kohonen-Karte erstellt und trainiert werden. Dies kann durch das oben genannte Skript create_kohonen.sh erfolgen oder aber es wird das Tool net_subdiv aus dem SXML-SNNS-Interface zurückgegriffen, welches für ein gewähltes Neuron eine interpolierte Karte erzeugt: $net_subdiv som.net #neuron X-Groesse Y-Groesse > subnet.net 14.1.3 Testaufbau Für eine Beurteilung des Verfahrens ist ein Vergleich der hierarchischen Kohonen-Karte gegenüber einer normalen Kohonen-Karte unter kontrollierten Bedingungen Voraussetzung. Folgende Parameter (SNNS-Pendant in Klammern) können dabei variiert werden: • die verwendete Kartengröße und initiale Gewichtung • Eigenschaften und Umfang der Trainingsdaten • der Lernfaktor η (Adption height h(0)) • die Distanzfunktion h (Adaption radius r(0)) • die Anzahl der Lernzyklen Die Qualität einer SOM bezüglich einer Menge an Eingabevektoren lässt sich anhand der Zuordungsgenauigkeit der Eingabevektoren zu Clustern bestimmen. Dazu kann der gemittelte Euklidische Abstand aller Eingabevektoren I zu den jeweiligen Gewichtsvektoren W der Gewinnerneuronen ermittelt werden: pPn ∆(I, W ) = i=0 (Ii − Wi )2 n Das Resultat allein stellt noch keine absolute Aussage über die Qualität dar, kann jedoch als Vergleichswert dienen. 67 Siehe dazu [W ISSUWA], [W ISSUWA 2003]; Die Bibliothek ist im Quellcode auf der beiliegenden CD enthalten. - 67 - Algorithm 5 Hierarchische SOM 1 2 3 4 5 6 7 8 9 10 11 12 INPUTS=10 SX=4 SY=1 SX2=30 SY2=25 CYCLES1=50000 CYCLES2=100000 SUBS=""; for (( a=0; a<SX*SY; a++ )); do SUBS=$SUBS"$a "; done ./create_kohonen.sh $INPUTS $SX $SY som_untrained.net ./train_kohonen.sh som_untrained.net som_trained.net pattern0.pat $SX $CYCLES1 13 ./apply_kohonen.sh som_trained.net pattern0.pat pattern0.res 14 15 /usr/local/sxml/bin/cnv_res2sxml mod=number input=false teaching=false output=false file=pattern0.res -nostdin | /usr/local/sxml/bin/ mod_process -f xml2csv.sxml 2>/dev/null | grep -v winner > pattern0.csv 16 17 for a in $SUBS ; do 18 grep -n $a < pattern0.csv | cut -f1 -d":" > subset0_$a.sub 19 if test -e pattern1_$a.pat; then rm pattern1_$a.pat; fi 20 /usr/local/SNNS/bin/pat_sel_simple subset0_$a.sub pattern0.pat pattern1_$a.pat 21 done 22 23 for a in $SUBS ; do 24 ./create_kohonen.sh 10 $SX2 $SY2 subnet0_$a.net 25 done 26 27 for a in $SUBS ; do 28 if test -s subset0_$a.sub ; then 29 ./train_kohonen.sh subnet0_$a.net subnet_trained_0_$a.net pattern1_$a. pat $SX2 $CYCLES2 & 30 else 31 cp subnet0_$a.net subnet_trained_0_$a.net 32 echo "no patterns for subnet $a." 33 fi 34 done 35 36 wait 37 38 Nets=""; 39 for a in $SUBS ; do 40 Nets=$Nets"subnet_trained_0_$a.net " 41 done 42 43 /usr/local/sxml/bin/net_merge $SX $Nets > som_trained_1.net - 68 - Kohonen-Karte Die verwendeten Karten variieren in der Größe der Kartenschicht, besitzen jedoch stets die selbe Anzahl von Eingabeneuronen. Für die hierarchische SOM wird die Kartenschicht in 3 mal 2 Teilkarten aufgeteilt. Die Initialisierung der Karten erfolgt mit einem konstanten Seed-Wert von 1. Trainingsdaten Die Trainingsmenge umfasst 1000 Vektoren mit 10 Elementen im Wertebereich [0.0-1.0]. In der Trainingsmenge wurden zunächst zufällig 20 Clusterzentren erzeugt. Diese wurden entsprechend häufig dupliziert und mit zufälligen Abweichungen im Bereich [±0.04] versehen. Lernfaktor Zunächst ist ein geeigneter Lernfaktor zu bestimmen. Um den Einfluss des Lernfaktors auf die Clusterqualität zu untersuchen, wurden Karten verschiedener Größe mit verschiedenen Lernfaktoren erzeugt68 . Wie die Abbildung 24 zeigt, ist die Wahl der Lernrate hauptsächlich für die hierarchische SOM relevant. Hier werden mit sehr kleinen Werten für η die besten Resultate erzielt. Daher wurde für alle Experimente eine konstante Lernrate von η = 0.01 festgelegt. Abbildung 24: Clusterqualität vs. Lernrate / Kartengröße Clusterqualität bei 10 Trainingszyklen mit je 1000 Eingabemustern. Die parallele SOM besteht aus 6 Teilkarten. Quelle: Eigene Darstellung. Distanzfunktion Als Distanzfunktion wird normalerweise die Gaussche-Distanzfunktion verwendet. Der SNNS jedoch Simuliert den Effekt dieser Funktion durch eine Reduzierung des Lernradius nach jeder Iteration69 . Als initialer Lernradius wird die Breite der Kohonen-Schicht 68 69 Die Messwerte sind in den Tabellen 9 und 10 im Anhang B.2 aufgeführt. Siehe [Z ELL 2000] S.72f. - 69 - verwendet. Der Faktor zur Anpassung des Lernradius nach der Präsentation jedes einzelnen Eingabevektors wird in Abhängigkeit von der Anzahl der Eingabemuster nI und der Trainingszyklen nT so berechnet, dass der Lernradius nach Ablauf aller Iterationen n = nI ∗ nT den Wert 1.0 erreicht: 1.0 = r ∗ multRn r multR = n 1) ln( r 1 = e n r Anzahl der Lernzyklen Es werden, sofern nicht anders angegeben, stets 50000 Lernschritte durchlaufen. 14.1.4 Vergleich der Modellqualität Durch eine visuelle Auswertung lassen sich erste Schlüsse auf die Eigenschaften einer KohonenKarte ziehen. Abbildung 25 und 26 zeigen jeweils eine U-Matrix-Darstellung von KohonenKarten identischer Dimension nach jeweils 50000 Lernschritten. Die Hierarchische Karte besteht aus vier Teilkarten. Die unterschiedliche Anzahl von erkennbaren Cluster-Bereichen pro Teilkarte ist darauf zurückzuführen, dass die Aufteilung der Trainingsdaten nicht gleichmäßig erfolgte. Es ist zu erkennen, dass beide Karten entsprechend der Clusteranzahl in den Trainingsmustern (mindestens) 20 Cluster-Bereiche enthalten. Während die normale Karte exakt die in den Daten enthaltenen 20 Cluster darstellt, ist die Anzahl bei der Hierarchischen Karte etwas höher. Es ist zu vermuten, dass einige Cluster-Bereiche den selben Cluster repräsentieren. Die U-Matrix der normalen Karte zeigt, dass einige Cluster-Bereiche sehr ähnlich sind, da die Clustergrenzen relativ schwach ausgeprägt sind. Die zugehörigen Trainingsmuster können bei der Aufteilung benachbarten Teilkarten der hierarchischen SOM zugewiesen werden, wo sie eigene Cluster bilden und so die Gesamtzahl von Cluster-Bereichen erhöhen. Dies ist jedoch eher ein optisches Problem, denn die Zugehörigkeit von Kartenneuronen zu Clustern wird letztendlich durch die Ähnlichkeit der Gewichtsvektoren und nicht durch die Position auf der Karte bestimmt. Da die Clusterqualität von der Größe der Karte abhängt, wurden Versuche mit unterschiedlichen Kartengrößen durchgeführt. Das Resultat ist in Abbildung 27 dargestellt. Wie zu erwarten liefern beide Karten relativ schlechte Ergebnisse, wenn die Anzahl der Neuronen nicht für die abzubildenden Cluster ausreicht. Die Hierarchische Karte ist sogar noch etwas schlechter, was dadurch bedingt ist, dass durch die Aufteilung der Karte die Neuronenzahl weiter abnimmt. Bei größeren Karten jedoch liefert die Hierarchische Version deutlich bessere Ergebnisse. - 70 - Abbildung 25: U-Matrix Seriell Quelle: Eigene Darstellung. Abbildung 26: U-Matrix Hierarchisch Quelle: Eigene Darstellung. Abbildung 27: Clusterqualität Clusterqualität Quelle: Eigene Darstellung. - 71 - 14.1.5 Vergleich der Rechengeschwindigkeit Die untersuchten Karten haben eine Größe von 30x30 Neuronen. Die Hierarchische Karte besteht aus 6 Teilkarten a 10x15 Neuronen. Für die Beurteilung der Rechengeschwindigkeit wurden drei Einsatzszenarien untersucht: 1. Serielle Karte auf einem Prozessor. 2. Hierarchische Karte auf einem Prozessor. 3. Hierarchische Karte auf mehreren Prozessoren. Die Zeitmessung umfasst stets den gesamten unter 14.1.2 dargestellten Prozess vom Erzeugen der untrainierten Karten bis zur Zusammensetzung der Teilkarten zu einer Gesamtkarte. Es wurden jeweils die real vergangene Zeit und die Prozessorzeit für verschiedene Kartengrößen gemessen. Das Resultat ist in Abbildung 28 dargestellt. Die hierarchische Karte ist, selbst wenn sie nicht parallel Berechnet wird, bereits ab einer sehr geringen Größe der Ausgabeschicht von ca. 100 Neuronen deutlich schneller als die serielle Version. Da der Aufwand zur Berechnung der Abstände und Gewichte einer Kohonen-Schicht mit n Neuronen O(n) = n2 ist, kann durch 2 2 Zerlegung der Karte in p Teilkarten der Aufwand auf O(n) = p ∗ np = np gesenkt werden. Der Speedup der parallelen gegenüber der nicht-parallelen Berechnung der hierarchischen SOM fällt relativ gering aus. Dies ist darauf zurückzuführen, dass neben der eigentlichen Berechnung viel Rechenzeit für die Erzeugung der Musterdateien und die Auswertung der Teilkarten beansprucht wird, da dies hauptsächlich durch vergleichsweise langsame Shell-Skripte realisiert wurde. Abbildung 28: Speedup SOM Parallel / Seriell Quelle: Eigene Darstellung. - 72 - 14.2 Feed-Forward-Netze 14.2.1 Konzept Analog zur hierarchischen SOM kann auch bei Feed-Forward-Netzen eine Parallelisierung durch Anpassung der Netztopologie erfolgen. Dies ist jedoch Abhängig von der Codierung und der Anzahl der Ausgabemuster. Für den Fall, dass ein n-fach-Klassifikator erzeugt werden soll, kann dies durch Verwendung von n binären Klassifikatoren erreicht werden, die jeweils separat berechnet werden können. Der Rechenaufwand reduziert sich jedoch entsprechend der Anzahl der Verbindungen maximal um den Faktor n. Eine Aufteilung der Trainingsmuster und damit eine Reduzierung der Trainingszyklen ist nicht möglich. Die Zahl der Rechenschritte p für einen Propagierungsschritt in einem mehrschichtigen Netz mit n voll vernetzten Schichten L ist: p= n−1 X Li ∗ Li+1 i=0 Durch Bildung separater Netze für jedes Ausgabeneuron in Ln reduziert sich die Anzahl der zu berechnenden Verbindungen in der letzten Schicht, gleichzeitig steigt die Anzahl der zu berechnenden Netze, so dass für p in diesem Fall gilt: p = Ln ∗ (Ln−2 + n−2 X Li ∗ Li+1 ) i=0 Für ein 10x10x20-Netz ist die Anzahl der zu Berechnenden Verbindungsgewichte 300, nach Reduktion auf ein 10x10x1-Netz nur noch 110. Da jedoch 20 solcher Netze trainiert werden müssen, steigt die Gesamtzahl auf 2200 Rechenschritte. Dieses Verfahren ist also nur dann sinnvoll, wenn die Berechnung wirklich parallel erfolgen kann. In diesem Fall ist eine Geschwindigkeitssteigerung um den Faktor Pn−1 i=0 Li ∗ Li+1 S= P Ln−2 + n−2 i=0 Li ∗ Li+1 zu erwarten. 14.2.2 Umsetzung Die Trainingsmenge umfasst 1000 Vektoren mit 10 Elementen im Wertebereich [0.0-1.0]. Die Vektoren sind jeweils einer von 20 Klassen zugeordnet, die erkannt werden sollen. Dazu wurde in der Trainingsmenge wurden zufällig 20 Referenzvektoren erzeugt, die entsprechend häufig dupliziert und mit zufälligen Abweichungen im Bereich ±0.2 versehen wurden. Die Codierung des Teaching-Output erfolgte so, dass für jede Klasse C das n-te Element aktiviert und die restlichen Elemente deaktiviert wurden: ( 1, 0 n = C tn = 0, 0 sonst - 73 - Für die binären Klassifikatoren wurde eine ähnliche Kodierung verwendet. Da jeder Klassifikator für die Unterscheidung zwischen C und ¬C trainiert wird, lautet die Kodierung: ( [1, 0; 0, 0] C t= [0, 0; 1, 0] ¬C 14.2.3 Vergleich der Modellqualität Zunächst ist die Topologie des zu verwendenden Netzes, insbesondere die Größe der versteckten Schicht zu ermitteln. Hierzu wurden Netze mit versteckten Schichten unterschiedlicher Größe jeweils so lange trainiert, bis der Fehlerwert klein genug war. Als Basis für den Fehlerwert dient der Summierte Quadratische Fehler (SSE70 ). Für eine Trainingsmenge P und die AusgabeNeuronen O ist dieser definiert als SSE = XX (tij − oj )2 i∈P j∈O Basierend darauf lassen sich der mittlere Fehler über alle Ausgabeneuronen SSE/O und der mittlere Fehler über alle Trainingsmuster MSE=SSE/P ableiten. Als genereller Indikator für den Trainingsfortschritt ist der MSE am besten geeignet, da er die Anzahl der Trainingsmuster berücksichtigt. Da das parallele Modell eine andere Topologie und eine andere Kodierung des Teaching-Output aufweist, sind geeignete Kriterien notwendig, um sowohl die Erkennungsqualität als auch die benötigte Rechenzeit vergleichbar zu machen. Da es das Ziel ist, eine möglichst hohe Erkennungsrate der Trainingsmuster zu erzielen, ist es sinnvoll, den jeweiligen Netzfehler als Indikator heranzuziehen und das Training zu beenden, sobald z.B. der MSE < 0.008 ist. Um diesen Wert zu erreichen, benötigt der n-fach-Klassifikator eine bestimmte Anzahl von Trainingszyklen c und eine bestimmte Rechenzeit t. Die Verwendung binärer Klassifikatoren ist sinnvoll, sobald t und im optimalen Fall auch c unterschritten werden. Die Bewertung der erstellten Netze wurde mittels des SNNS-Tools analyze durchgeführt. Als Analysefunktion wurde „WTA” gewählt, da die Klassenzugehörigkeit durch genau ein aktives Neuron repräsentiert wird. In Tabelle 14 sind die Erkennungsraten, Rechenzeiten und Trainingszyklen für einen n-FachKlassifikator und einen Binären Klassifikator für verschiedene große Zwischenschichten gegenübergestellt. Beim Binären Klassifikator wurde die Erkennungsgenauigkeit des jeweils schlechtesten Teilklassifikators zugrundegelegt. Als Vergleich sind die Erkennungsraten und Trainingszyklen aller Teilklassifikatoren für eine Zwischenschicht mit einem Neuron in Tabelle 13 aufgeführt. Der n-Fach-Klassifikator liefert erst ab einer Zwischenschicht mit 5 Neuronen akzeptable Werte, wogegen der Binäre Klassifikator bereits mit nur einem Zwischenneuron eine hohe Erkennungsrate erzielt. Eine Steigerung der Neuronenzahl bewirkt hier nur marginale Veränderungen. 70 Der SNNS und das Tool analyze geben für das selbe Netz und die selben Muster für den SSE leicht abweichende Werte an. - 74 - 14.2.4 Vergleich der Rechengeschwindigkeit Die Rechenzeiten sind Tabelle 14 zu entnehmen. Es wurden jeweils 3 Messungen durchgeführt und die kürzeste Rechenzeit gewertet. Die Rechenzeiten des n-Fach-Klassifikators und des binären Klassifikators lassen sich hinsichtlich zweier Kriterien miteinander verleichen: ähnliche Topologie und ähnliche Resultate. Beim Vergleich der Klassifikatoren mit ähnlicher Topologie bzw. gleicher Größe der Zwischenschicht reduziert sich der Speedup von ≈ 6, 8 bei 5 Zwischenneuronen auf ≈ 1, 25 ab 7 Zwischenneuronen. Werden die jeweils kleinsten Klassifikatoren verglichen, die vergleichbare Resultaten liefern, so kann ein deutlich höherer Geschwindigkeitszuwachs festgestellt werden. So erreicht der binäre Klassifikator bereits mit nur zwei Zwischenneuronen eine Erkennungsrate von 99,00 %. Der n-Fach-Klassifikator benötigt hierfür mindestens fünf Zwischenneuronen und eine deutlich höhere Berechnungszeit. Der Speedup beträgt in diesem Fall ≈ 7, 4. - 75 - Teil IV Zusammenfassung und Ausblick In dieser Arbeit wurden verschiedene Ansätze zur Parallelisierung von Algorithmen vorgestellt. Diese bezogen sich sowohl auf die Anpassung des Algorithmus selbst durch Verwendung paralleler Programmierkonzepte, als auch auf die Parallelisierung durch Aufteilung der Rechenaufgaben unter Verwendung serieller Programme. Es wurde gezeigt, dass Parallele Programmierung ein sehr komplexes Thema ist, das eine enge Verzahnung von Hardware, Software und Anwendungsgebiet auf verschiedenen Ebenen aufweist. Der aktuelle Stand der Technik wird durch MPI/MPI2 und OpenMP festgelegt. Beide Ansätze sind ausgereift, flexibel, relativ einfach zu handhaben und liefern sehr gute Resultate. MPI hat sich dabei als besonders performant erwiesen, während OpenMP sehr leicht auf bestehende Programme anzuwenden ist. UPC hingegen muss anhand der gemachten Erfahrungen als rein Experimentell angesehen werden. Die Entscheidung, ob MPI oder OpenMP das Mittel der Wahl sind, hängt im wesentlichen von den Faktoren Wiederverwendung und Skalierbarkeit ab. MPI erfordert einen höheren Programmieraufwand, ist dafür jedoch auch in sehr großen, netzwerkbasierten und heterogenen Rechnerumgebungen einsetzbar, während OpenMP-basierte Programme naturgemäß nur auf einem einzigen System ausführbar sind, sofern das Betriebssystem nicht die Migration von Prozessen über Rechenknoten hinweg unterstützt. Da MPI und OpenMP miteinander Kombiniert werden können und beispielsweise MPI für die Kommunikation zwischen Rechenknoten und OpenMP für die parallele Ausführung auf einem Knoten eingesetzt wird, lassen sich hochgradig parallele Anwendungen bei gleichzeitig überschaubarer Komplexität des Programmcodes implementieren. Anhand der Parallelisierung von Data-Mining-Modellen mittels Basistechnologien von LinuxSystemen wurde gezeigt, dass ein signifikanter Geschwindigkeitsgewinn erzielt werden kann, ohne den Algorithmus verändern zu müssen. Die vorgestellte Hierarchische Kohonen-Karte sowie der auf mehreren Feed-Forward-Netzen basierende n-Fach-binäre Klassifikator erzielen Resultate, die qualitativ mit der herkömmlichen Vorgehensweise der Erstellung eines Einzelmodells vergleichbar sind, jedoch deutlich schneller berechnet werden. Besonders die Hierarchische-Kohonen-Karte ist ein vielversprechender Ansatz, da auch bei nicht-paralleler Berechnung ein signifikanter Geschwindigkeitsgewinn erzielt wurde. Diese Arbeit zeigt, dass Parallelisierung von Data-Mining-Verfahren selbst in geringem Maß deutliche Geschwindigkeitsvorteile bringt. Die vorgestellten Ansätze bezogen sich dabei nur auf einen Teilaspekt des Data Mining - der Erstellung von Modellen. Der gesamte Data-MiningProzess besteht jedoch aus mehreren Stufen, die jede für sich auf Parallelisierbarkeit untersucht werden sollte. Insbesondere die zeitaufwändige Vorverarbeitung stellt ein geeignetes Ziel für weiterführende Arbeiten dar. - 76 - Literatur [UPC 2005] (2005). UPC Language Specification V1.2. The UPC Consortium. [A LPAR 2000] A LPAR , PAUL (2000). Data Mining im praktischen Einsatz. Vieweg, München. [BARTH 2006] BARTH , T HOMAS UND S CHÜLL , A NKE (2006). Grid Computing - Konzepte, Technologien, Anwendungen. Vieweg, Wiesbaden. [BAUKE 2006] BAUKE , H EIKE UND M ERTENS , S TEFAN (2006). Cluster Computing - Praktische Einführung in der Hochleistungsrechnen auf Linux-Clustern. Springer, Heidelberg. [B RANDS 2005] B RANDS , G ILBERT (2005). Das C++ Kompendium. Springer. [C ARVER 2006] C ARVER , R ICHARD H. UND TAI , K UO -C HUNG (2006). Modern Multithreading. Wiley. [C HANDRA ] C HANDRA , ROHIT UND DAGUM , L EONARDO UND KOHR DAVE UND M AYDAN D ROR UND M C D ONALD J EFF UND M ENON R AMESH . Parallel Programming in OpenMP. Morgan Kaufmann Publishers. [C HAPMAN 2008] C HAPMAN , BARBARA UND J OST, G ABRIELE UND VAN DER PAS RUUD (2008). Using OpenMP - Portable Shared Memory Parallel Programming. The MIT Press. [C HAUVIN 2008] C HAUVIN , S ÈBASTIEN (2008). UPC Manual v1.2. [C LEVE 2005] C LEVE , J ÜRGEN UND L ÄMMEL , U WE UND W ISSUWA S TEFAN (2005). Data Mining auf zeitabhängigen Daten – Kundenanalyse im Bankbereich. Hochschule Wismar, Wismar. [E M K ARNIADAKIS 2000] E M K ARNIADAKIS , G EORGE UND K IRBY, ROBERT M. II (2000). Parallel Scientific Computing in C++ and MPI. Cambridge University Press. [E NGELN -M ÜLLGES 2005] E NGELN -M ÜLLGES , G ISELA UND N IEDERDRENK , K LAUS UND W ODICKA R EINHARD (2005). Numerik-Algorithmen - Verfahren, Beispiele, Anwendungen. [UND G ORDON S. L INOFF 2000] G ORDON S. L INOFF , M ICHAEL J. A. B ERRY UND (2000). Mastering Data Mining. John Wiley and Sons, New York. [G ROPP 2007] G ROPP, W ILLIAM UND L USK , E WING MPI - Eine Einführung. Oldenbourg, München Wien. UND S KJELLUM A NTHONY (2007). [H AN 2001] H AN , J IAWEI UND K AMBER , M ICHELINE (2001). Data Mining - Concepts and Techniques. Morgan Kaufmann Publishers, San Francisco. [H OFFMANN 2008] H OFFMANN , S IMON UND L IENHART, R AINER (2008). OpenMP - Eine Einführung in die parallele Programmierung mit C/C++. Springer, Berlin Heidelberg. [K NIPPERS 2001] K NIPPERS , ROLF (2001). Molekulare Genetik. Georg Thieme Verlag, Stuttgart. [KOHONEN 2001] KOHONEN , T EUVO (2001). Self-Organizing Maps. Springer. [L ÄMMEL 2007] L ÄMMEL , U WE UND B EIFERT, A NATOLI UND W ISSUWA S TEFAN (2007). Business Rules - Die Wissensverarbeitung erreicht die Betriebswirtschaft. Wismarer Diskussionspapiere Heft 05/2007, Wismar. [L ÄMMEL 2004] L ÄMMEL , U WE UND CL EVE , J ÜRGEN (2004). Künstliche Intelligenz. Fachbuchverlag Leipzig, Leipzig. - 77 - [L ÄMMEL 2004] L ÄMMEL , U WE UND C LEVE , J ÜRGEN (2004). Lehr- und Übungsbuch Künstliche Intelligenz. Fachbuchverlag Leipzig, Leipzig. [L ÄMMEL 2003] L ÄMMEL , U WE (2003). Data Mining mittels künstlicher neuronaler Netze, WDP No. 7. Hochschule Wismar, Wismar. [M AHLMANN 2007] M AHLMANN , P ETER UND S CHINDELHAUER , C HRISTIAN (2007). Peerto-Peer Netzwerke - Algorithmen und Methoden. Springer, Berlin Heidelberg. [NAKHAEIZADEH 1998] NAKHAEIZADEH , G. (1998). Data Mining - Theoretische Aspekte und Anwendungen. Physica-Verlag, Heidelberg. [P ETERSEN 2004] P ETERSEN , W. P. puting. Oxford University Press. UND A RBENZ , P. (2004). Introduction to Parallel Com- [R AUBER 2007] R AUBER , T HOMAS UND RÜNGER , G UDULA (2007). Parallele Programmierung. Springer, Heidelberg. [R AUBER 2008] R AUBER , T HOMAS UND RÜNGER , G UDULA (2008). Multicore: Parallele Programmierung. Springer, Heidelberg. [S ANTORO 2007] S ANTORO , N ICOLA (2007). Design and Analysis of Distributed Algorithms. Wiley. [S CHILL 2007] S CHILL , A LEXANDER UND S PRINGER , T HOMAS (2007). Verteilte Systeme Grundlagen und Basistechnologien. Springer, Berlin Heidelberg. [W ISSUWA 2003] W ISSUWA , S TEFAN (2003). Data Mining und XML - Modularisierung und Automatisierung von Verarbeitungsschritten, WDP No. 12. Hochschule Wismar, Wismar. [W ISSUWA ] W ISSUWA , S TEFAN UND L ÄMMEL , U WE UND C LEVE J. [W ITTEN 2001] W ITTEN , I.H. UND F RANK , E. (2001). Data Mining. Hanser, München. [W ITTEN 2005] W ITTEN , I.H. UND F RANK , E. (2005). Data Mining: Practical Machine Learning Tools and Techniques. Morgen Kaufmann, San Francisco. [Z ELL 2000] Z ELL , A NDREAS (2000). Simulation Neuronaler Netze. Oldenbourg, München. [Z ELL ] Z ELL , A NDREAS ET. University of Tübingen. AL . SNNS User Manual, Version 4.2. University of Stuttgart, - 78 - Teil V Ehrenwörtliche Erklärung Ich erkläre hiermit ehrenwörtlich, dass ich die vorliegende Arbeit ohne unzulässige Hilfe Dritter und ohne Benutzung anderer als den angegebenen Hilfsmitteln angefertigt habe. Die aus anderen Quellen direkt oder indirekt übernommenen Daten und Konzepte sind unter Angabe der Quelle als solche gekennzeichnet. Die Arbeit wurde bisher weder im In- noch im Ausland in gleicher oder ähnlicher Form einer anderen Prüfungsbehörde vorgelegt. Wismar, 01.12.2008. - 79 - Teil VI Anhang - 80 - A Schnittstellen und Bibliotheken A.1 UPC Konsortium Mitglieder des UPC-Konsortiums nach[C HAUVIN 2008]: UPC-Konsortium: National Security Agency (NSA - http://www.nsa.gov) IDA Institute for Defense Analyses, Center for Computing Sciences (IDA-CCS - http:// www.super.org) The George Washington Universtity, High Performance Computing Laboratory (GWU HPCL http://upc.gwu.edu) Arctic Region Supercomputing Center (ARSC - http://www.arsc.edu); Hewlett-Packard (HP - http://h30097.ww3.hp.com/upc/) Cray Inc. (http://www.cray.com) Etnus LLC. (http://www.etnus.com) IBM (http://www.ibm.com) Intrepid Technologies (http://www.intrepid.com) Ernest Orlando Lawrence Berkley National Laboratory (LBNL - http://upc.lbl.gov) Lawrence Livermore National Laboratory (LLNL - http://www.llnl.gov) Michigan Technological University (MTU - http://upc.mtu.edu) Silicon Graphics, Inc. (SGI - http://www.sgi.com) Sun Microsystems (http://www.sun.com) University of California, Berkley (http://www.berkley.edu) US Department of Energy (DoE -http://www.energy.gov) Ohio State University (OSU - http://www.osu.edu) Argonne National Laboratory (ANL - http://www.anl.gov) Sandia National Laboratory (http://www.sandia.gov) University of North Carolina (UNC - http://www.unc.edu) - 81 - A.2 Compilerübersicht Produkt GCC UPC Berkley UPC Tabelle 2: Übersicht UPC-Compiler Architektur / System Anbieter OS: Linux Intrepid Technologc, Inc., Palo Alto, CA., CPU: x86_64 / ia64 / i686 / SGI www.intrepid.com/ IRIX / Source upc OS: Linux, FreeBSD, NetBSD, Tru64, AIX, IRIX, HPUX, Solaris, Microsoft Windows, Mac OS X, Cray Unicos, NEC SuperUX LBNL, UC Berkley upc.lbl.gov CPU: x86, Itanium, Opteron, Athlon, Alpha, PowerPC, MIPS, PA-RISC, SPARC, Cray T3E, Cray X1/X1E, Cray XD1, Cray XT3, SX-6, SGI Altix Compiler: GNU GCC, Intel C, Portland Group C, SunPro C, Compaq C, HP C, MIPSPro C, IBM VisualAge C, Cray C, NEC C, Pathscale C MuPC UPC-to-C HP UPC IBM XL UPC HP-UX ia64, HP-UX PA-RISC, Linux ia64, Linux Opteron, Tru64 UNIX OS: AIX 5.2/5.3, SUSE 10, Blue Gene/L CPU: Alpha - 82 - Michigan Technological University www.upc.mtu.edu Hewlett-Packard www.hp.com/go/upc Registrierung erforderlich IBM www.alphaworks. ibm.com/tech/ upccompiler Produkt gcc (4.2) XL C/C++, Fortran C/C++, Fortran C/C++, Fortran C/C++, Fortran Fortran C/C++, Fortran C/C++, Fortran C/C++, Fortran Visual Studio 2008 C++ Tabelle 3: OpenMP-fähige Compiler Architektur / System Anbieter OS: Linux, Solaris, AIX, GNU MacOSX, Windows OS: Windows, AIX, Linux IBM OS: Solaris, Linux Sun Microsystems OS: Windows, Linux, MacOSX Intel Portland Goup Compilers and Tools Absoft Pro FortranMP Lahey / Fujitsu Fortran 95 OS: Linux PathScale Hewlett-Packard Microsoft Quelle: http://openmp.org/wp/openmp-compilers/ - 83 - B Messwerte B.1 Messwerte Matrizenmultiplikation M\P 1 2 10 25 50 75 100 150 200 250 300 350 400 450 500 550 600 0.000066 0.000486 0.003486 0.004511 0.027885 0.098130 0.236753 0.479782 0.896232 1.336331 2.522894 4.457177 6.193651 9.428142 9.182826 4 6 8 10 12 Tabelle 4: Matrizenmultiplikation Seriell M\P 1 2 4 6 8 10 12 10 25 50 75 100 150 200 250 300 350 400 450 500 550 600 0.000345 0.005156 0.042337 0.151507 0.356807 1.275226 3.042646 5.853685 10.275381 16.344794 24.355682 34.658302 47.887960 62.819381 83.782705 0.000187 0.002717 0.021467 0.092006 0.173337 0.655847 1.528626 2.961748 5.169971 8.220303 12.276938 17.430334 24.080471 32.517365 42.148312 0.000127 0.001527 0.011224 0.039584 0.088143 0.327994 0.769294 1.498230 2.603281 4.159847 6.169760 8.777367 12.116765 16.459072 21.190994 0.000093 0.001069 0.007851 0.027183 0.059375 0.219487 0.521998 1.022699 1.798386 2.839749 4.309103 5.966676 8.128559 10.940555 14.289468 0.000090 0.000878 0.006020 0.037312 0.045220 0.166372 0.408651 0.760586 1.320743 2.154535 3.167183 4.456782 6.112915 8.379933 10.686609 0.000144 0.001291 0.013269 0.029618 0.051460 0.178803 0.332544 0.615677 1.094328 1.719209 2.499268 3.572334 4.877766 6.630831 8.798027 0.000283 0.003190 0.021087 0.044298 0.073834 0.163974 0.514728 0.547898 1.749707 1.521786 2.882435 3.032739 4.229874 6.716945 7.302215 Tabelle 5: Matrizenmultiplikation UPC - 84 - M\P 1 2 4 6 8 10 12 10 25 50 75 100 150 200 250 300 350 400 450 500 550 600 0.000061 0.000708 0.005523 0.018463 0.044097 0.148982 0.351516 0.689630 1.212470 2.266326 3.412175 5.918736 8.273327 11.395000 12.216489 0.000139 0.000495 0.002913 0.009502 0.022442 0.074534 0.177469 0.345555 0.613433 1.035687 1.739066 3.025944 4.225347 5.794530 6.473583 0.000215 0.000702 0.004236 0.009233 0.022083 0.072459 0.113075 0.189924 0.322143 0.564305 0.873463 1.547039 2.168703 2.930399 3.226290 0.000325 0.000847 0.004258 0.010174 0.021800 0.053846 0.086735 0.136494 0.252879 0.399579 0.622746 1.080466 1.509814 2.081293 2.224139 0.000405 0.000974 0.005080 0.011604 0.028045 0.054312 0.109039 0.161868 0.247536 0.354034 0.572927 0.821592 1.156219 1.586476 2.323643 0.000508 0.001115 0.005482 0.014458 0.027283 0.053527 0.113966 0.164122 0.183129 0.290627 0.452519 0.690350 0.950083 1.319551 1.386175 0.000667 0.001231 0.005587 0.015038 0.024235 0.068654 0.093838 0.139350 0.201306 0.329320 0.491077 0.806803 1.013034 1.534482 1.677829 Tabelle 6: Matrizenmultiplikation OpenMP M\P 10 25 50 75 100 150 200 250 300 350 400 450 500 550 600 1 2 4 6 8 10 12 0.000327 0.000981 0.002040 0.005550 0.029358 0.040484 0.094581 0.467562 0.399043 1.583401 1.595673 4.474244 5.819763 8.026178 8.624467 0.000992 0.001326 0.002146 0.005491 0.008078 0.035740 0.055123 0.078360 0.139833 0.400853 0.476732 1.205406 2.047167 2.890131 2.649446 0.001667 0.001469 0.002225 0.003860 0.006612 0.018740 0.080043 0.077515 0.136844 0.230183 0.446546 0.885877 1.274201 1.788950 1.377678 0.002076 0.001433 0.001652 0.004270 0.004113 0.015662 0.033456 0.107663 0.106269 0.223505 0.351717 0.715220 0.956625 1.183636 1.306128 0.001054 0.002717 0.003668 0.002541 0.003911 0.013740 0.017963 0.061808 0.135504 0.195669 0.314738 0.560056 0.705274 0.875405 1.062164 0.001063 0.001988 0.003607 0.004839 0.004692 0.013497 0.044365 0.037410 0.102665 0.180105 0.243988 0.422473 0.383832 0.828765 0.890537 Tabelle 7: Matrizenmultiplikation MPI Master-Worker - 85 - M\P 1 2 4 6 8 10 12 10 25 50 75 100 150 200 250 300 350 400 450 500 550 600 0.000066 0.000486 0.003486 0.004511 0.027885 0.098130 0.236753 0.479782 0.896232 1.336331 2.522894 4.457177 6.193651 9.428142 9.182826 0.000207 0.000542 0.002114 0.006303 0.014624 0.050182 0.116972 0.252491 0.409708 0.395171 1.268596 2.236218 3.754630 4.700820 4.207593 0.001851 0.001714 0.002658 0.005153 0.005050 0.026883 0.062404 0.119238 0.226100 0.416644 0.664815 1.263629 1.631193 2.522061 2.407333 0.001058 0.001587 0.017773 0.003338 0.006569 0.018313 0.043116 0.087102 0.152024 0.297131 0.439555 0.841829 1.215208 1.662818 1.684385 0.001132 0.002680 0.003899 0.004481 0.010957 0.015856 0.060138 0.063094 0.128903 0.244296 0.328091 0.640259 0.974273 1.254155 1.421796 0.001549 0.002619 0.003938 0.005441 0.007304 0.026477 0.059101 0.058353 0.118456 0.512465 0.385311 0.272954 0.757982 1.037532 1.042465 0.003259 0.003850 0.002773 0.006798 0.009408 0.024510 0.035822 0.081536 0.109268 0.149115 0.281513 0.549998 0.694487 0.979880 0.925685 Tabelle 8: Matrizenmultiplikation MPI Global B.2 Messwerte SOM Kartengröße \ η 0.01 0.02 0.05 0.1 0.2 6 12 24 35 54 70 96 117 150 176 216 247 294 0.979354 0.964106 0.971496 0.967688 0.967150 0.956431 0.968554 0.967589 0.962537 0.961058 0.965004 0.962998 0.957681 0.974925 0.970810 0.970836 0.972489 0.967812 0.964177 0.955066 0.961083 0.967079 0.954644 0.971950 0.961691 0.960076 0.969950 0.968835 0.968242 0.962414 0.968045 0.955921 0.952249 0.963069 0.958687 0.967487 0.955522 0.962436 0.957778 0.959706 0.969341 0.965021 0.965382 0.969016 0.962576 0.967951 0.960837 0.956596 0.961061 0.948122 0.955413 0.960455 0.966196 0.956361 0.954553 0.968662 0.962433 0.959484 0.960326 0.962029 0.961712 0.959043 0.948291 0.956562 0.954079 Tabelle 9: Clusterqualität (seriell) in Abhängigkeit von Lernrate und Kartengröße - 86 - Kartengröße \ η 0.01 0.02 0.05 0.1 0.2 6 12 24 35 54 70 96 117 150 176 216 247 294 0.997729 0.989731 0.982539 0.974015 0.962167 0.966213 0.963507 0.952056 0.946226 0.937983 0.939833 0.947016 0.934472 0.991785 0.989421 0.979781 0.984256 0.972806 0.962104 0.972702 0.974855 0.966315 0.953437 0.967333 0.947809 0.938684 1.002955 0.987820 0.986615 0.978814 0.982249 0.977105 0.982685 0.974192 0.982389 0.972333 0.966706 0.977643 0.971364 1.014007 0.989101 0.985965 0.986098 0.984485 0.982592 0.980234 0.980545 0.980699 0.980938 0.983018 0.981370 0.980312 1.002109 0.989269 0.984128 0.983383 0.982571 0.986211 0.981494 0.979453 0.985563 0.981915 0.985436 0.982376 0.982756 Tabelle 10: Clusterqualität (parallel) in Abhängigkeit von Lernrate und Kartengröße Kartengröße \ Zeit Real_P CPU_P Real_NP CPU_NP Real_S CPU_S 36 72 144 210 324 420 576 702 900 1056 1296 1482 1764 1980 2304 2550 2916 3192 2.09 2.37 2.73 3.07 3.60 4.17 4.91 5.58 6.55 7.62 8.89 9.93 11.64 13.07 14.84 16.43 18.15 19.14 1.08 1.57 2.37 3.01 3.75 4.86 6.22 7.26 9.00 10.78 13.28 15.18 18.04 20.55 23.74 26.26 29.70 31.66 2.89 3.33 4.02 4.76 5.76 6.19 8.25 9.68 11.44 13.13 16.17 18.36 21.28 23.90 30.18 35.67 36.48 40.64 1.24 1.57 2.39 3.07 3.96 4.20 6.24 7.28 9.10 10.72 13.46 15.30 18.25 20.59 24.16 26.68 30.27 32.69 1.68 3.01 4.90 7.30 11.19 14.51 19.63 22.57 29.28 34.44 40.03 47.05 58.95 77.69 93.07 122.93 168.47 182.00 1.48 2.60 4.68 7.02 10.83 14.21 19.22 22.13 28.57 33.70 39.52 46.27 56.03 76.41 88.40 121.10 166.39 178.16 Erläuterung der Spaltennamen: Real=Verstrichene reale Gesamtzeit; CPU=Verstrichene CPUZeit; P=Paralleles Modell auf mehreren Prozessoren; NP=Paralleles Modell auf einem Prozessor; S=Normales Modell auf einem Prozessor Tabelle 11: Rechenzeit in Abhängigkeit von Kartengröße und Parallelisierungsgrad - 87 - Größe \ # 12 24 35 54 70 96 117 150 176 1 2 .41 1.54 3.35 8.11 13.73 26.28 46.16 81.27 111.42 3 .65 1.22 2.24 4.83 7.97 14.77 22.23 37.49 60.10 4 5 6 .90 1.34 1.63 1.94 1.22 1.89 2.18 2.54 2.08 1.87 3.00 3.47 3.96 3.47 4.86 5.12 6.47 5.93 5.09 6.23 10.74 8.53 9.23 9.10 16.02 14.10 11.26 10.36 26.24 21.15 17.11 20.95 36.11 27.94 24.22 20.56 7 8 9 10 11 12 2.44 3.30 3.57 5.68 8.54 10.63 14.84 19.65 24.64 2.92 3.38 4.25 5.41 7.19 10.10 14.10 17.11 27.03 3.41 4.17 4.73 5.96 7.77 11.05 13.03 19.46 21.98 3.61 4.37 4.75 5.94 7.96 10.07 12.22 17.93 24.75 4.34 4.81 5.38 6.07 7.47 10.00 12.19 17.46 23.16 4.30 4.85 5.76 7.41 9.50 12.29 12.49 16.69 21.96 Tabelle 12: Berechnungszeiten Parallele SOM mit OpenMP B.3 Messwerte Feed-Forward-Netz Tabelle 13: Erkennungsrate Teilklassifikatoren Klassifikator Zyklen Korrekt (%) binär #0 binär #1 binär #2 binär #3 binär #4 binär #5 binär #6 binär #7 binär #8 binär #9 binär #10 binär #11 binär #12 binär #13 binär #14 binär #15 binär #16 binär #17 binär #18 binär #19 27 27 26 37 28 24 22 63 236 57 21 22 27 22 22 21 19 30 28 24 Min.: 99,80 99,90 100,00 99,70 100,00 99,90 99,80 99,30 98,50 100,00 100,00 99,80 100,00 100,00 100,00 100,00 100,00 100,00 98,70 99,70 98,50 - 88 - Tabelle 14: Rechenzeiten und Erkennungsrate n-Fach vs. binär nach Netzgröße #Hidden Zyklen Treal 1 1000 2 3 4 5 6 7 8 9 1000 1000 1000 1000 261 212 155 115 109 3,8250 4,6560 5,5290 6,3300 6,8410 2,2980 2,1230 1,9060 1,3940 1,5010 1,1890 1,4020 1,2260 1,3480 1,3340 10 11 12 13 14 15 76 89 76 66 68 n-Fach Tcpu 3,6700 4,5200 5,4300 6,1800 6,7200 2,1900 2,0400 1,7500 1,2800 1,4000 1,0700 1,2800 1,1100 1,2500 1,2200 Korrekt % Treal binär Tcpu 23,50 59,30 90,10 97,90 99,60 100,00 100,00 99,90 99,90 100,00 100,00 100,00 100,00 100,00 100,00 1,8310 0,9170 0,9110 0,9460 1,0140 1,0730 1,1750 1,0440 1,2420 1,1440 1,0640 1,1420 1,1620 1,0620 1,0530 4,1900 2,2600 2,5100 2,7000 2,8300 3,0200 2,9700 3,1000 3,9700 3,2700 3,3300 3,3400 3,6800 4,1900 3,9800 Korrekt % 98,50 99,00 99,00 99,40 98,90 99,10 99,40 98,90 98,80 98,80 99,00 98,90 99,00 98,90 98,30 B.4 Inhalt der CD Die in dieser Arbeit vorgestellten Programme und Algorithmen sind auf der beiliegenden CD enthalten, ebenso die verwendeten Skripte zum Erzeugen der Messdaten. Jedes Verzeichnis enthält eine README-Datei mit Informationen über den Inhalt des Verzeichnisses sowie dessen Verwendung. /sxml SXML Umgebung. /matrix Testprogramme zur Matrizenmultiplikation /cache Testprogramm Row-First vs. Column-First-Ordering /nn_parallel Parallele Selbstorganisierende Karte /snns_ff n-Fach-Klassifikator vs. Binäre-Klassifikatoren /snns_som Hierarchischen Kohonen-Karte /quellen Ergebnistabelle des Linpack-Benchmark (http://www.top500.org/lists/2008/06) - 89 -