Fakultät Informatik Institut für Systemarchitektur, Professur für Datenbanken Diplomarbeit FRAMEWORK FÜR DIE SPEZIFIKATION UND AUSFÜHRUNG PARALLELER CLUSTERING-ALGORITHMEN Alexander Krause Matr.-Nr.: 3411206 Betreut durch: Prof. Dr.-Ing. Wolfang Lehner und: Dr.-Ing. Dirk Habich Eingereicht am 28. Februar 2015 2 ERKLÄRUNG Ich erkläre, dass ich die vorliegende Arbeit selbständig, unter Angabe aller Zitate und nur unter Verwendung der angegebenen Literatur und Hilfsmittel angefertigt habe. Dresden, 28. Februar 2015 3 4 ABSTRACT Aufgrund der immer größeren Datenmengen, die durch immer größere Sensornetzwerke, Protokollierungsmaßnahmen oder andere Quellen aufgezeichnet werden, steigt die Menge von zu bearbeitenden Daten immer weiter an. Um diese Informationen zu verarbeiten, wird der Prozess des Data-Mining angewendet, wovon ein wichtiger Teil aus der Clusteranalyse besteht. Dadurch sollen diejenigen Elemente einer Datenmenge gefunden werden, deren Merkmale einander ähnlich oder sogar identisch sind. Um bei der Analyse selbst so variabel wie möglich zu sein, werden Ausführungsumgebungen benötigt, um die Clustering-Algorithmen verwenden zu können. Für jede Implementierung gelten dabei in der Regel Abhängigkeiten, welche sie an eine Umgebung binden. Diese Arbeit untersucht eine Möglichkeit, um basierend auf dem MapReduce Modell eine plattformunabhängige Spezifikation von Clustering-Algorithmen zu ermöglichen und die Parallelität der zu Grunde liegenden Hardware auszunutzen. Dabei wird ein mögliches Sprachkonzept aufgezeigt und darauf eingegangen, wie dessen Umsetzung in plattformunabhängigen Programmcode funktionieren kann. Die Funktionalität des Ansatzes wird anhand einer MapReduce und OpenCL Implementierung bestätigt. 5 6 INHALTSVERZEICHNIS 1 2 Einführung 1.1 Zielsetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.2 Aufbau der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen 13 2.1 Vorbetrachtungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.1.1 Parallele Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.1.2 MapReduce und PACT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.1.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.2 PIPE-Konzept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.3 cPIPE-Ansatz für Clustering-Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . 19 2.3.1 Modulare Beschreibung von Algorithmen . . . . . . . . . . . . . . . . . . . . 19 2.3.2 cPIPE Elemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.3.3 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 2.4 3 9 PIPE Compiler für effiziente Ausführung 27 3.1 Vorbetrachtungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 3.1.1 28 Source-to-Source Compiling . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Inhaltsverzeichnis 3.1.2 Ausführungsumgebungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.1.3 MapReduce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.1.4 OpenCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 3.2 PIPE Compiler Ansatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.3 Optimierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 3.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 4 Implementierung 39 5 Evaluation 43 5.1 PIPE Konzept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 5.2 Source-to-Source Compiler und Ausführungsumgebungen . . . . . . . . . . . . . . 45 6 8 Zusammenfassung und Ausblick 49 1 EINFÜHRUNG Im derzeitigen „Petabyte Zeitalter“, wie es in [Ott09] beschrieben wird, sind große Datenmengen allgegenwärtig. Diese auch als „Big Data“ bezeichnete Datenmenge zu verarbeiten und hinsichtlich nützlicher Informationen zu analysieren, ist oft nicht nur ein ökonomisches, sondern auch ein technisches Problem. „Data“ bedeutet im lateinischen „Gegebenes“. Big Data verweist also auf eine enorme Menge von gegebenen Dingen, in diesem Fall Wissen. Um eine so große Menge an unentdeckten Wissen zu verarbeiten, werden effiziente Analysealgorithmen gebraucht. Diese stammen unter anderem aus dem Bereich des Data Mining, dessen Aufgabe es ist, unbekanntes Wissen aus einer Menge von Quellen zu extrahieren. Um dabei kosteneffizient und gleichzeitig schnell zu sein, müssen auch die verwendeten Algorithmen mit aktuellen Entwicklungen Schritt halten. Eine Kategorie des Data Minings wird als Data Clustering, also Clusteranalyse, bezeichnet. Ihr Fokus liegt darauf, Häufungen von ähnlichen Elementen zu finden und diese gegen andere Ballungsräume von ähnlichen Elementen abzugrenzen. Das Ergebnis einer solchen Analyse ist in Abbildung 1.1 zu sehen. In realen Anwendungsfällen werden weit mehr als nur sechs Cluster betrachtet bzw. gefunden, sodass die Effizienz eines Algorithmus einen kritischen Aspekt von Analysen darstellt. Auf diese Weise können, aus der enormen Fülle von teilweise unstrukturiertem Wissen, bisher unbekannte Informationen gewonnen werden. 2 1.5 1 0.5 0 0 0.5 1 1.5 2 Abbildung 1.1: Beispielcluster unterschiedlicher Form und Größe 9 Kapitel 1 Einführung Neben den Datenquellen entwickelt sich auch die Hardware der zu Grunde liegenden Systeme stetig weiter. Wo in früheren Workstations oder Servern lediglich Prozessoren mit einem Kern verbaut waren, finden in aktuellen Systemen Multi- oder Manycore Prozessoren Anwendung. Die Unterscheidung beider Varianten bezieht sich dabei lediglich auf die tatsächlich verbaute Anzahl von Kernen auf einem Chip. Mit einer steigenden Anzahl von Prozessorkernen steigt gleichermaßen auch die Anzahl von Berechnungsaufgaben, die zur selben Zeit von einem Prozessor ausgeführt werden können. Dieser Effekt wird auch als parallele Berechnung bezeichnet und bedeutet, dass ein oder mehrere Programme zur gleichen Zeit auf mehreren Prozessoren oder deren Kernen ausgeführt werden kann. Für einzelne Programme, wie etwa Analysealgorithmen, bedeutet dies eine Umstrukturierung der internen Programmstruktur. Um diesem Mehraufwand entgegen zu wirken, müssen die verwendeten Algorithmen effizient im parallelen Umfeld funktionieren. Bei optimaler Ausnutzung der zur Verfügung stehenden Hardware können dabei, je nach Szenario, Geschwindigkeitsvorteile entstehen, die sogar bis an die Anzahl der zur Berechnung verwendeten Prozessorkerne heranreicht. Um Algorithmen ausführen zu können, wird eine Ausführungsumgebung benötigt. Diese weist bestimmte Eigenschaften auf, die an ihre zu Grunde liegende Hardware gebunden sind. Je nach System werden mehrere Szenarien, wie beispielsweise „Scale Up“ oder „Scale Out“, beschrieben. Ersteres meint den Einsatz eines größeren, leistungsfähigeren Chips. Scale Out bedeutet hingegen die Verwendung von mehreren Subsystemen zur Erstellung eines Gesamtsystems. Ein Analysealgorithmus, der in solchen Szenarien eingesetzt werden soll, muss also gut skalierbar und effizient im parallelen Umfeld arbeiten. Dabei muss berücksichtigt werden, dass eine Ausführungsumgebung aufgrund der jeweiligen Hardwareeigenschaften oftmals an ein ausführendes System gebunden ist. Wird also ein Algorithmus für eine bestimmte Ausführungsumgebung programmiert, so kann dieselbe Implementierung in der Regel nicht ohne Anpassungen auf ein anderes System portiert werden. Grundlegend wäre also eine Möglichkeit wünschenswert, mit der ein Wechsel des verwendeten Systems und der damit verbundenen Ausführungsumgebung so einfach wie möglich gestaltet wird. Dadurch könnte gleichermaßen der Aufwand zur Beschreibung von skalierbaren, effizienten Analysealgorithmen im parallelen Umfeld reduziert und die Effektivität der durchgeführten Analysen gesteigert werden. 1.1 ZIELSETZUNG Ziel dieser Arbeit ist es, einen Ansatz zur Formulierung von parallel ablaufenden Algorithmen zu finden. Dabei soll eine uniforme Beschreibung solcher Algorithmen möglich sein, damit sie auf einer Vielzahl von unterschiedlichen Plattformen ausführbar sind. Da die konkreten Implementierungen eines Algorithmus für spezifische Ausführugnsumgebungen voneinander abweichen, muss eine Abbildungsschicht zwischen der Spezifikation von Analysealgorithmen und den angestrebten Ausführungsumgebungen geschaffen werden. Dieses Schema wird in Abbildung 1.2 verdeutlicht. Am Beispiel der Klasse von Clustering-Algorithmen soll gezeigt werden, wie diese in parallelisierbare Segmente unterteilt und formuliert werden können. Die Abbildung der plattformunabhängigen Beschreibung auf eine plattformabhängige Implementierung bedeutet lediglich, dass eine Programmiersprache in eine andere umgewandelt werden muss. Es wird demnach ein Prozess benötigt, der eine Programmiersprache in eine andere umwandeln kann. Für solche Zwecke wurde bereits in der Vergangenheit ein Source-to-Source Compiler eingesetzt. Dieser soll prototypisch implementiert werden, sodass er die für die Spezifikation der Clustering- 10 1.2 Aufbau der Arbeit Algorithmen entworfene Sprache verarbeiten und in eine der, möglicherweise mehreren, Zielsprachen übersetzen kann. Der gesamte Übersetzungs- und Ausführungsprozess soll am Beispiel der beiden Ausführungsumgebungen MapReduce sowie OpenCL verdeutlicht werden, da diese eine Vielzahl von homo- und heterogenen Systemen verwenden können. Als Testplattformen stehen zwei Systeme zur Verfügung, die jeweils mit den 64 Bit Versionen der Betriebssysteme Ubuntu 12.04 und Windows 7 betrieben werden. Plattformunabhängige Beschreibung Abbildungsschicht Plattform 1 Plattform ... Plattform N Abbildung 1.2: Schematischer Aufbau des Spezifikations- und Ausführungsvorgangs 1.2 AUFBAU DER ARBEIT Diese Arbeit betrachtet die Entwicklung einer plattformunabhängigen Spezifikationssprache für parallele Algorithmen am Beispiel des Themas Data Clustering. In Kapitel 2 wird das Thema Data Clustering beleuchtet. Neben einer kurzen Einführung in den Bereich der parallelen Programmierung werden das klassische MapReduce Modell sowie dessen Verallgemeinerung, das PACT Modell, erläutert. Es folgt eine Beschreibung des abstrakten PIPE Modells, welches an seiner Spezialisierungsform den cPIPEs für den Bereich Data Clustering im Detail erläutert wird und den plattformunabhängigen Beschreibungsteil aus Abbildung 1.2 darstellt. In Kapitel 3 wird eine Übersicht zum Thema Source-to-Source Kompilierung, also der Abbildungsschicht aus Abbildung 1.2, gegeben. Dabei wird außerdem auf die beiden zu Testzwecken ausgewählten Ausführungsumgebungen MapReduce und OpenCL eingegangen. Weiterhin wird die Funktionsweise des für diese Arbeit notwendigen Source-to-Source Compilers erläutert sowie Optimierungsmöglichkeiten des Programmcodes aufgezeigt. Das Kapitel 4 beschreibt konkrete Implementierungsdetails. Kapitel 5 zeigt mit Hilfe von Experimenten die Durchführbarkeit des erarbeiteten Konzepts. Das Kapitel 6 fasst diese Arbeit zusammen und gibt einen Überblick über mögliche zukünftige Entwicklungen. 11 Kapitel 1 Einführung 12 2 PIPE-BASIERTE SPEZIFIKATION PARALLELER CLUSTERING-ALGORITHMEN Mit der steigenden Parallelität auf Hardwareebene entstehen für Programmierer neue Probleme in Bezug auf die Spezifikation von Algorithmen. Im Folgenden werden die Grundlagen paralleler Programmierung mit Hilfe von früheren und aktuellen Methoden erläutert. Es folgt die Darstellung eines neuen, abstrakten und plattformunabhängigen Konzeptes zur Beschreibung paralleler Algorithmen. Dessen Funktionsweise wird an der Thematik des Data Clustering im Detail erläutert. Abschließend wird dieses Kapitel übersichtsgebend zusammengefasst. 2.1 VORBETRACHTUNGEN Parallelität auf Hardwareebene kann durch die zwei Formen „Scale Up“ sowie „Scale Out“ beschrieben werden. Ersteres meint den Einsatz eines größeren, leistungsfähigeren Chips. Scale Out bedeutet hingegen die Verwendung von mehreren Subsystemen zur Erstellung eines Gesamtsystems. Der folgende Abschnitt erläutert die Grundlagen von paralleler Programmierung und die damit verbundenen Probleme. Zusätzlich wird eine Übersicht über aktuelle Methoden zur Ausführung von parallelen Algorithmen gegeben und diese im Anschluss zusammengefasst. 2.1.1 Parallele Programmierung In den frühen Formen der Programmierung galt es, einen Algorithmus auf einer einzigen Recheneinheit auszuführen. Lange Zeit ging bei der Entwicklung von Prozessoren der Trend in die Richtung, dass einzelne Kerne immer höher getaktet werden. Als diese Entwicklung an physikalische Grenzen zu stoßen drohte, wurde das Hardwaredesign von „Scale Up“ auf „Scale Out“ 13 Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen umkonzipiert. Das bedeutet, dass nicht mehr nur ein einzelner, extrem leistungsfähiger Chip verwendet wird, sondern eine Vielzahl von Systemen für die Erledigung anstehender Aufgaben genutzt werden. Dieses Prinzip lässt sich auf einen Prozessor genauso wie auf Rechenzentren anwenden. Im lokalen Kontext bedeutet es, anstatt einem Einzelkernprozessor mit hoher Taktrate einen Mehrkernprozessor zu verwenden, dessen Kerne mit geringerer Frequenz arbeiten. Neben dem ökonomischen Aspekt ermöglicht diese Veränderung auch, dass mehrere Aufgaben gleichzeitig abgearbeitet werden können. Davon können auch einzelne Algorithmen profitieren. Mit einer angepassten Architektur können separate Abschnitte oder das gesamte Programm in nebenläufige Aufgaben partitioniert werden. Um das zu realisieren, wurden zunächst Programmiermodelle eingeführt, welche das Beschreiben von parallelen Abschnitten ermöglichen. Ein kritischer Aspekt ist dabei allerdings, dass sequentiell programmierte Algorithmen im parallelen Kontext ihre Speicherbereiche teilen müssen. Dabei ist zu berücksichtigen, dass zu jeder Zeit von anderen Programmteilen ein Zugriff auf den selben Speicherbereich oder eine Manipulation dessen erfolgen kann. Grundsätzlich muss immer beachtet werden, dass bei gleichzeitigem Zugriff auf den selben Speicherbereich Probleme auftreten können. Für die folgenden Beispiele wird ein System angenommen, dass mit zwei Prozessorkernen ausgestattet ist und deswegen mit zwei Threads zur gleichen Zeit arbeiten kann. Ein Thread stellt dabei einen Prozessteil dar, welcher Programmcode losgelöst von anderen Programmteilen ausführen kann. Weiterhin sollen keine sonstigen Modifikationen des Speichers durch andere Programme möglich sein. Versuchen diese zwei Threads den selben Speicherbereich zu lesen, besteht kein Problem. Problematisch wird es, wenn ein Thread den Bereich beschreibt, während der andere ihn liest oder den selben Bereich beschreiben möchte. Ähnlich dem Transaktionsprinzip in Datenbankmanagementsystem (DBMS) kommt es zu Datenabhängigkeiten, weswegen ein schreibender Zugriff immer vor lesenden Zugriffen geschützt werden muss. Andernfalls kann es bei nicht-atomaren Operationen dazu kommen, dass einer der Threads einen inkonsistenten Speicherzustand einliest, woraufhin dessen Berechnungen mit fehlerhaften Daten durchgeführt würden. Beim Schreib-Schreib-Konflikt geht dies soweit, dass die berechneten Daten von Thread1 in den Speicher geschrieben würden, wobei sie unter Umständen vollständig von Thread2 überschrieben werden, ohne dass diese je hätten verarbeitet werden können. Die standardisierte Lösung für diese Probleme stellt das Sperren von kritischen Abschnitten dar, in denen nichtatomare Operationen ausgeführt werden und ein gemeinsamer Speicherbereich genutzt wird. Zwei Modelle zur Ermöglichung paralleler Programmierung werden durch Open Multi-Processing (OpenMP) sowie Message-Passing Interface (MPI) realisiert. Ersteres ermöglicht die Parallelisierung von Programmteilen eines Prozesses. Dabei werden vor allem Schleifen mittels Compilerdirektiven gekennzeichnet, wobei die jeweiligen Iterationsschritte von unabhängigen Threads ausgeführt werden. OpenMP wurde für die Verbesserung der Laufzeit von Programmen auf Basis von Non-Uniform Memory Access (NUMA) Systemen erdacht [Dag98]. Für solche Systeme waren die unterschiedlichen Zugriffszeiten innerhalb der Speicherhierarchie namensgebend. Ein Zugriff auf den Cache des Prozessors ist am schnellsten; Daten aus dem Hauptspeicher abzurufen benötigt bereits deutlich mehr Zeit und die längste Wartezeit entsteht bei Speicherzugriffen auf Archivspeicher wie Hard Disk Drives (HDDs) oder Solid State Disks (SSDs). Anders funktioniert dies bei MPI. Dessen Intention ist es, gleich mehrere Prozesse die nebenläufige Abarbeitung zu ermöglichen. Diese werden allerdings durch ein gemeinsames Interface, über das Nachrichten untereinander ausgetauscht werden können, synchronisiert [Pac98]. 14 2.1 Vorbetrachtungen 2.1.2 MapReduce und PACT Die nächste Evolutionsstufe paralleler Programmierung stellte die Entwicklung von MapReduce durch Google dar. Dieses System wurde für das „Scale Out“ Szenario konzipiert und nahm eine einheitliche Datengrundlage an, wobei die Ein- und Ausgabedaten aus Schlüssel-Wert-Paaren bestehen. Es ermöglicht dem Programmierer das Einbringen von nutzerseitig spezifizierten Programmcode in ein Framework, welches sämtliche Aufgaben von Datenpartitionierung, Speichermanagement und Arbeitsverteilung selbst übernimmt. Die einzig zwingende Konvention war, dass es zwei Funktionen zweiter Ordnung gibt, namentlich Map und Reduce, die immer in exakt dieser Reihenfolge ausgeführt werden. Beide arbeiten auf dem fest definierten Eingabe Datenschema und ohne Abweichung von der Ausführungsreihenfolge. Abbildung Map 2.1 verdeutlicht dieses Konzept. Die EinZwischenergebnis Ausgabe gabedaten werden von mehreren MapMap pern bearbeitet und nach bestimmten InReduce formationen durchsucht. Diese werden im Schlüssel-Wert-Paar Schema als ZwiMap Reduce schenergebnisse abgelegt und anschließend nach den Schlüsseln gruppiert. Abschließend findet eine Reduktion aller Map Werte in eine gemeinsame Ergebnisliste mittels einem oder mehrerer Reducer statt. Abbildung 2.1: MapReduce Schema Ebenfalls auf die Verarbeitung großer Datenmengen ausgelegt ist das von MapReduce abgeleitete Parallelization Contract (PACT) Programmiermodell aus [Ale11]. PACT steht für Parellelization Contracts, also Vereinbarungen für die parallele Abarbeitung von Operatoren zur Datentransformation. Das System arbeitet auf einer Datengrundlage von Schlüssel-Wert-Paaren, genau wie das MapReduce Konzept, mit dem es verglichen wird und das es generalisiert [Ale11]. Eine Innovation gegenüber dem klassischen MapReduce Verfahren stellen die Programmiermöglichkeiten mittels PACTs dar. Die zuvor fest definierte Abfolge von Map- und Reduce-Schritten bedeutet immer eine gewisse Limitierung in der Anwendbarkeit dieses Modells, um mehrere Arten von Algorithmen auszu- Eingabe B Eingabe A Unabhängige Subsets (a) Cross PACT Eingabe B Eingabe B Eingabe A Unabhängige Subsets (b) Match PACT Eingabe A Unabhängige Subsets (c) CoGroup PACT Abbildung 2.2: PACT Operatoren, entnommen aus [Ale11] 15 Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen drücken, wie in [Kal13] gezeigt wird. Um diese durch das MapReduce Framework gegebenen Grenzen zu überwinden, führte [Ale11] unter Beibehaltung des Datenschemas zusätzlich zu Map und Reduce drei weitere Operatoren ein. Diese sind namentlich „Cross“, „Match“ und „CoGroup“ und werden in Abbildung 2.2 illustriert. Jeder dieser Operatoren wird als Parallelization Contract (PACT) bezeichnet. Der Cross PACT berechnet ein Kreuzprodukt aus allen Eingabedaten. Durch den Match PACT werden jene Datensätze miteinander kombiniert, deren Schlüssel übereinstimmen. Dabei können zwei verschiedene Eingabedatenpaare auch von zwei verschiedenen Match-Instanzen bearbeitet werden. Der CoGroup PACT generiert Datensubsets aus allen Schlüssel-Wert-Paaren mit dem selben Schlüssel. Anders als bei Match ist dabei garantiert, dass diese so entstandenen Subsets von der selben Instanz der Nutzerfunktion bearbeitet werden. Dieses Modell hat den grundlegenden Vorteil, dass eine größere Anzahl von Algorithmen einfach als Orchestrierung von PACTs erstellt werden kann. Das Schema des PACT Programmiermodells wird in Abbildung 2.3 verdeutlicht. Ein PACT besteht immer aus genau einer Funktion zweiter Ordnung, die als Eingabe eine Funktion erster Ordnung erwartet. Diese beinhaltet problemspezifischen Code. Für jeden PACT gilt, dass ihre Eingabedaten nicht mit denen anderer PACTs übereinstimmen müssen. Optional kann auch ein Ausgabe-PACT spezifiziert werden, der Bedingungen für die Art der Ausgabedaten festlegt. Schlüssel Wert Eingabe PACT ... Nutzer Code Ausgabe PACT ... Ausgabe Eingabe Unabhängige Datengrundlagen Abbildung 2.3: PACT Schema, übersetzt aus [Ale11] 2.1.3 Zusammenfassung Unter Berücksichtigung der historischen Entwicklung kann festgestellt werden, dass die parallele Programmierung ein relevantes Thema im Bereich der Informatik darstellt. Beginnend mit Compilerparadigmen im Low-Level Bereich entstanden mit der Zeit diverse Frameworks, um dem Anwender eine Vielzahl an Werkzeugen bereit zu stellen, mit denen die Spezifikation von parallelisierbaren Algorithmen immer einfacher wird. Zwischen den jeweiligen Evolutionsschritten wurde versucht, die Nachteile des Vorgängers zu beseitigen. Sobald ein Programmierer mittels OpenMP oder MPI eine Implementierung vornimmt, ist diese an eine bestimmte Plattform gebunden. MapReduce behebt dieses Problem durch ein einheitliches Datenmodell gepaart mit zwei Funktionen zweiter Ordnung, welche die Verwendung von Nutzercode erlauben. Dieses Programmiermodell wurde allerdings ursprünglich für den Bereich Information Retrieval erdacht, um beispielsweise Wörter in Dokumenten zu zählen, was die konzeptionellen Schwächen in Bezug auf die fehlende Vielseitigkeit für die Spezifikation anderer Algorithmen erklärt. Das für den Bereich Datentransformation erdachte PACT revolutioniert den MapReduce Ansatz in 16 2.2 PIPE-Konzept sofern, dass dem Benutzer weitere Elemente zur Verfügung gestellt werden, mit denen die Daten bearbeitet werden können. Obwohl das Datenschema im Schlüssel-Wert-Paar Stil beibehalten wird, kann aufgrund der freien Kombinationsmöglichkeiten aller Elemente bereits ein Fortschritt in Richtung Vielseitigkeit erreicht werden. Nachteilig wirkt sich dabei allerdings wieder die feste Bindung an ein einzelnes Datenschema aus. Als nächsten Evolutionsschritt bietet sich demnach ein Konzept an, welches eine vom zu Grunde liegenden Datenmodell unabhängige Spezifikation von Algorithmen ermöglicht und zusätzlich unabhängig von der Zielplatform einsetzbar ist. 2.2 PIPE-KONZEPT Die Effektivität und Ausführbarkeit von Algorithmen ist oft unmittelbar mit der angestrebten Ausführungsumgebung und der verwendeten Hardware verknüpft. Abhängig von der verwendeten Programmiersprache können etwaige Implementierungen nicht zwischen einzelnen Systemen portiert werden, sondern müssen erst aufwändig aufgearbeitet und angepasst werden, um den verschiedensten Anforderungen diverser homo- und heterogener Systeme gerecht werden zu können. Es wird also eine Möglichkeit gesucht, Algorithmen auf generische Art und Weise plattformunabhängig zu spezifizieren. Basierend auf dieser möglichst einfach zu haltenden Beschreibung soll eine große Anzahl unterschiedlicher Plattformen verwendet werden können. Eine zwingend notwendige Eigenschaft einer solchen Beschreibung muss allerdings sein, dass sie sich den hohen Grad an Parallelität der unterliegenden Hardwarestruktur zu Nutze machen kann. Aus den Vorüberlegungen ergeben sich drei Herausforderungen, die es zu lösen gilt. Ein Framework für die Spezifikation und Ausführung von parallelen Algorithmen braucht also zunächst eine Möglichkeit, Clustering-Algorithmen zu beschreiben, ohne auf hardwarebezogene oder systemspezifische Eigenschaften eingehen zu müssen. Wünschenswert ist in diesem Zusammenhang auch eine grundlegend einfache Syntax. Je einfacher die zur Formulierung der Algorithmen verwendeten Konstrukte sind, desto geringer ist im Endeffekt auch der Aufwand, diese in plattformabhängigen Code zu übersetzen. Dies kann am Beispiel von bereits vorhandenem Programmcode verdeutlicht werden. Wenn eine problemspezifische Lösung bereits vorhanden ist und diese allerdings nur in einer sequentiellen Umgebung lauffähig ist, gibt es zwei Möglichkeiten. Entweder der Code kann in einer Form annotiert werden, sodass er im parallelen Umfeld lauffähig wird oder er muss gänzlich neu verfasst werden. Letzteres bedeutet immer einen großen Mehraufwand, verglichen mit einer einfachen Übersetzung. Der zweite Schwerpunkt liegt bei den Ausführungsumgebungen. Diese bestimmen die Form, in der die Algorithmen ausgedrückt werden, in der Regel maßgeblich. Es wird also ein Programm benötigt, welches die Übersetzung zwischen der allgemeinen Spezifikation der Algorithmen und der spezifischen Implementierung für die Ausführungsumgebung realisieren kann. Als letzte Aufgabe gilt es die Problematik der Optimierung zu lösen. Je allgemeingültiger die Formulierung von Code ist, desto schlechter ist seine Leistung. Die Optimierung von Algorithmen ist für die Ökonomie bzw. Laufzeit von Programmen unentbehrlich. Anders als bei den Vorgängern MapReduce und PACT wird eine Spezifikationsmethode aus Applikationssicht benötigt, um ein Höchstmaß an Flexibilität zu erreichen. Für die Spezifikation von Algorithmen werden nach wie vor zwei Dinge benötigt. Diese sind ein Datenschema und eine endliche Menge von Platform Independent Parallel PattErn (PIPE) Elementen, welche auf jenem Schema arbeiten. Diese Elemente bestehen, wie in Abbildung 2.4 illustriert, aus einer Menge von Ein- und Ausgabeparametern. Die Struktur und Anzahl jener Parameter ist dabei lediglich 17 Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen PIPE-Element Beschreibung Nutzercode Beschreibung Nutzercode Eingabe 1 Eingabe n Ausgabe 1 Ausgabe n Nutzercode Abbildung 2.4: PIPE Beschreibungsschema vom Datenschema der Applikation determiniert. Weiterhin dienen die PIPE-Elemente zur Ausführung von nutzerseitig beschriebenen Funktionen zur letztendlichen Datenverarbeitung. Je nach Domäne können sowohl das Datenschema, als auch die Form der PIPE-Elemente variieren. Dieses Modell weicht vom MapReduce Ansatz insofern ab, dass nicht nur der Funktionsrumpf spezifiziert wird, sondern auch die Position innerhalb der Verarbeitungskette. Es muss lediglich darauf geachtet werden, dass auch nur miteinander solche PIPEs hintereinander ausgeführt werden können, deren jeweilige Aus- bzw. Eingabeparameter miteinander kompatibel sind. Unter Berücksichtigung dieser Limitation ist eine freie Orchestrierung beliebig vieler PIPE Elemente problemlos möglich, siehe Abbildung 2.5. Die Verwendung einer Domain-specific Language (DSL) wurde zunächst ausgeschlossen. Ein Hauptgrund dafür war, dass eine komplette Sprache hätte modelliert werden müssen, welche alle Probleme einer entsprechenden Domäne abdeckt und darüber hinaus eine umfassende, aber einfache Algorithmenbeschreibung ermöglicht. Begründet durch die begrenzte Zeit der Arbeit schien dies also unmöglich. Diese Erkenntnis führte letztendlich zu dem Schluss, dass die Verwendung einer bereits bestehenden Programmiersprache die sinnvollste Variante wäre. Um die Handhabung von Variablen, deren Initialisierung und Freigabe, weitestgehend zu vereinfachen, fiel die Wahl auf die Klasse der funktionalen Programmiersprachen. Anders als bei imperativen Eingabe 1 Ausgabe 1 Nutzer Funktion Eingabe n Eingabe 1 Nutzer Funktion Ausgabe n PIPE 1 Ausgabe 1 Eingabe n Eingabe 1 Ausgabe 1 Nutzer Funktion Ausgabe n Eingabe n Ausgabe n PIPE n Abbildung 2.5: PIPE Verbindungsschema 18 2.3 cPIPE-Ansatz für Clustering-Algorithmen Sprachen bestehen funktionale Programme lediglich aus der Definition von Funktionen. Es finden keine expliziten Speichermanipulationen statt, weswegen diese Sprachen auch ohne Nebeneffekte auskommen. Um all diese Anforderungen erfüllen zu können, wurde für die Spezifikation der Algorithmen die „PIPE Based Algorithm SpEcification (PIPEBASE)“ eingeführt. Sie fußt auf der Verwendung von PIPEs und soll durch eine möglichst einfach gehaltene Syntax auch für Programmierlaien problemlos anwendbar sein. Zur Beschreibung der Funktionen erster Ordnung, also der Nutzerfunktionen, wird auf die Syntax der Programmiersprache Haskell zurückgegriffen. Haskell ist eine nach dem US-amerikanischen Logiker und Mathematiker Haskell Brooks Curry benannte, funktionale Programmiersprache. Die Sprache selbst basiert auf dem Lambda-Kalkül, einer formalen Sprache für die Definition von Funktionen mittels gebundener Parameter. Neben ihrem Bekanntheitsgrad führte auch die Einfachheit der Syntax zur Wahl dieser Sprache. Obgleich die Definition von Funktionen in Haskell relativ einfach ist, so besteht das Hauptproblem in der Überführung von Haskell-Code in die Syntax der Sprachen C bzw. C++. Als erster Ansatz wurde Haskell in seiner Rohform verwendet. Begründet durch das Ziel, mehrere Ausführungsumgebungen unterstützen zu können, ist keine Verwendung von Klassen zur Vererbung möglich. Dadurch unterscheidet sich PIPEBASE von anderen Systemen wie dem von Apache entwickelten Hadoop [Dit12], welches für dessen Map- und Reduce-Funktionen vordefinierte Konstrukte mittels Vererbung bereit stellt. Daher ist es notwendig, Kenntnis von der vorliegenden Schnittstelle innerhalb der bereitgestellten Ausführungsumgebungen zu haben, wie beispielsweise Anzahl und Typ von Eingabeparametern. Diese codeseitigen Informationen reichen allerdings nicht aus, um vollständigen MapReduce- bzw. OpenCL-lauffähigen Quellcode zu generieren. Es fehlen die Hinweise, welche PIPE dieser Operator repräsentiert. Um eine möglichst einfache Entwicklung zu ermöglichen, wurde die Verwendung von Annotationen innerhalb des Codes eingeführt. Dabei wird dem Compiler, ähnlich wie bei JavaDoc [Ora04], mit Hilfe von als Kommentaren gekennzeichneten Hinweisen der Rest der notwendigen Informationen mitgeteilt. Diese sind unter anderem die Kennzeichnung der zu verwendenden PIPE. Weiterhin wird dem Compiler mitgeteilt, welcher Codebereich gerade eingelesen wird. 2.3 CPIPE-ANSATZ FÜR CLUSTERING-ALGORITHMEN Die Clustering Platform Independent Parallel PattErns (cPIPEs) stellen eine domänenspezifische Implementierung des PIPE-Modells dar. Am Beispiel des Komplexes Data Clustering soll gezeigt werden, wie die Spezifikation von parallelen Clustering-Algorithmen basierend auf PIPEBASE funktioniert. Im folgenden wird beschrieben, wie sich die Wahl eines spezifischen Datenschemas auf das Layout der benötigten cPIPE-Elemente auswirkt. 2.3.1 Modulare Beschreibung von Algorithmen Neben vielen anderen Themen arbeitet der Lehrstuhl für Datenbanken an der Technischen Universität Dresden an einem eigenen DBMS. Dieses Projekt bietet die Möglichkeit für Innovationen und damit verbundene Testläufe, welche in diesem Umfang in proprietären System nicht möglich wären. Eine dieser Ideen stellt die Modulare Clusteranalyse dar, welche in [Hah14] veröffentlicht 19 Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen wurde. Der Kerngedanke besteht darin, die Analysewerkzeuge in die namensgebende, modularisierte Form zu bringen. Ebenfalls zu Grunde liegt eine veränderte Datenhaltung, denn das erdachte System arbeitet grundlegend auf Matrizen. Damit verbunden ist eine Klassifizierung der Operatoren. Diese werden in die drei Kategorien „cross“, „row“ und „column“ eingeteilt, also entsprechend der Art und Weise, wie sie Daten aus der Matrix entnehmen und weiterverarbeiten. Neben den modularisierten Operatoren können Clustering-Algorithmen laut [Hah14] in mehrere Phasen unterteilt werden. Diese können in Tabelle 2.1 nachgelesen werden. Da sämtliche Clustering-Algorithmen jede dieser Phasen durchlaufen, könnten bei gleichbleibenden Ein- und Ausgabedatenströmen nicht nur einzelne Operatoren, sondern ganze Phasen ausgetauscht werden. Dies dient dem Zweck, eine vereinheitlichte Sichtweise auf Clustering-Algorithmen durch ein kompaktes Set von Instruktionen zu generieren. Weiterhin wird das Ziel verfolgt, die Schwierigkeiten parallelisierter Ausführung dem Programmierer zu erleichtern. Durch die Datenrepräsentation in Form von Matrizen werden verwendete Algorithmen als Matrixoperatoren geschrieben. Dies vereinfacht das Verständnis und Programmieren von Clustering-Algorithmen immens und macht es weiterhin möglich, verschiedene Algorithmen direkt miteinander zu vergleichen [Hah14]. Tabelle 2.1: Phasen von Clustering-Algorithmen aus [Hah14] (übersetzt) Phase Evaluation Eingabe Punkte, Referenzen Verarbeitung Distanzmaß Selektion Punkt-Distanz-ReferenzTripel Punkt-Distanz-ReferenzTripel - Filter Assoziation Optimierung 2.3.2 Ausgabe Punkt-Distanz-ReferenzTripel Punkt-Distanz-ReferenzTripel Entfernungen (Punkt-Referenz-Tupel) - Assoziationsfunktion - cPIPE Elemente In Anlehnung an die aus [Hah14] definierten Operatoren und deren Datengrundlage können mit Hilfe des PIPE-Konzepts drei cPIPEs definiert werden. Diese benötigen fest definiert zwei Eingabemengen und liefern genau einen Ausgabewert, der wiederum eine Menge repräsentiert. Der erste Operator ist dem PACT „Cross“ aus [Ale11] ähnlich und wird in der folgenden Abbildung 2.6a Referenzen 1 2 Referenzen 3 4 1 2 Referenzen 3 4 1 3 4 A A 1 A 2 A 3 A 4 A A 1 A 2 A 3 A 4 A A 1 A 2 A 3 A 4 B B 1 B 2 B 3 B 4 B B 1 B 2 B 3 B 4 B B 1 B 2 B 3 B 4 C C 1 C 2 C 3 C 4 C C 1 C 2 C 3 C 4 C C 1 C 2 C 3 C 4 D D 1 D 2 D 3 D 4 D D 1 D 2 D 3 D 4 D D 1 D 2 D 3 D 4 Daten (a) cross cPIPE Schema Daten (b) row cPIPE Schema Daten (c) col cPIPE Schema Abbildung 2.6: cross, row und col cPIPE Schemata 20 2 2.3 cPIPE-Ansatz für Clustering-Algorithmen dargestellt. Seine Eingabeparameter bestehen aus einer Datenmenge und einer Referenzmenge. Cross stellt das aus anderen Anwendungen bekannte Kreuzprodukt dar und realisiert dieses im parallelen Umfeld. Bei Initialisierung dieser PIPE wird sicher gestellt, dass jedes Element aus der Datenmenge mit jedem einzelnen Element aus der Referenzmenge kombiniert wird. Er stellt also grundlegend den aus dem SQL Bereich bekannten Join dar. Das Kreuzprodukt bildet die Grundlage für einige von Clusteranalysen. Beispiele sind hierfür der in Abschnitt 2.1 beschriebene k-Means Algorithmus oder der DBSCAN Algorithmus, bei dem die Eingabedatenmenge mit sich selbst verglichen wird, um Ähnlichkeiten zu finden. Die Ausgabeparameter von cross bestehen also aus einer einzelnen Menge, welche aus sämtlichen Kombinationen aller Elemente der Daten- und Referenzmenge besteht. Das zweite cPIPE-Konstrukt stellt „row“ dar. Dieser Operator arbeitet, wie der Name impliziert, zeilenbasiert. Seine Eingabeparameter bestehen aus einer einzelnen Menge. Diese beinhaltet sämtliche Tupel einer Matrix, wie sie beispielsweise vom cross Operator generiert wird. Abbildung 2.6b stellt die jeweiligen Eingabemengen hellgrau bzw. dunkelgrau hinterlegt dar. Dieser Operator realisiert, ausgehend von einem Matrixschema als Datengrundlage, eine Partitionierung der Elemente nach der Datenmenge. Der dritte, neu eingeführte Operator ist „col“ und wird durch Abbildung 2.6c dargestellt. Der col Operator stellt das vertikale Pendant zum row Operator dar. Die Eingabeparameter sind ebenfalls auf eine Menge begrenzt, in der Abbildung hell- und dunkelgrau hervorgehoben. Analog zu row erwirkt die Verwendung des col Operators eine Partitionierung der Datengrundlage nach der Referenzmenge. Für alle vorgestellten Operatoren gilt, dass diese sich an der Datengrundlage aus [Hah14] orientieren, also grundlegend zur Verarbeitung von Daten im Stile einer Matrix erdacht sind. Von technischer Seite betrachtet bedeutet dies, dass sie mit Tripeln arbeiten. Diese sind von der Form ZeilenID-SpaltenID-Wert. Jeder der Operatoren verwendet außerdem eine zuvor in PIPEBASE spezifizierte Nutzerfunktion. Grundlegend kann also festgestellt werden, dass jede cPIPE als Proxy zwischen der Ausführungsumgebung und der Nutzerfunktion arbeitet. Die Daten werden auf eine Art und Weise aufbereitet, wie die Nutzerfunktion sie zur Berechnung benötigt. Anschließend wird deren Rechenergebnis wieder, sofern notwendig, in die vom Framework benötigte Form umgewandelt, sodass diese dann weiter verarbeitet werden können und gegebenenfalls weiteren cPIPEs als Eingabedaten dienen können. 2.3.3 Beispiele Im Bereich des Data Clustering unterscheiden sich die bearbeiteten Datenmengen innerhalb eines Algorithmus. Oft wird mit zwei Mengen, einer Daten- und einer Referenzmenge gestartet, aus denen Zwischenergebnisse generiert werden. Basierend auf dieser Ergebnismenge muss gegebenenfalls auch auf die zugehörigen Daten- und Referenzpunkte zurückgegriffen werden, um deren Koordinaten zu manipulieren. Dies ist beispielsweise beim k-Means Algorithmus der Fall. Um k-Means in PIPEBASE zu formulieren, werden lediglich drei cPIPEs benötigt; jeweils eine Instanz von cross, row und col. Der folgende Vorgang kann durch Abbildung 2.7 nachvollzogen werden. Als erstes müsste die euklidische Distanz von allen Datenpunkten zu allen Referenzpunkten mit Hilfe des cross Operators berechnet werden. Jedes andere Distanzmaß würde ebenfalls funktionieren. Anschließend wird durch eine Minimumfunktion von row die minimale Entfernung eines Datenpuntkes zu einem Referenzpunkt ermittelt, wobei dessen Position in der Matrix mit „1“ ersetzt wird und der Rest der Zeile zu nullen ist. Abschließend würde durch einen col Operator jeder Punkt aus der Datenmenge ausgewählt werden, dessen Minimum beim gleichen Referenz- 21 Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen Re f 1 Data1 3.699 Data2 2.327 Data3 1.056 Data4 2.113 Data5 4.180 Data6 2.694 Re f 2 3.422 2.725 4.614 0.376 1.986 4.310 Re f 3 2.591 1.245 2.571 4.085 4.543 2.953 Re f 4 0.545 3.821 1.224 4.343 1.405 2.902 Data1 Data2 Data3 Data4 Data5 Data6 Re f 1 0 0 1 0 0 1 Re f 2 0 0 0 1 0 0 Re f 3 0 1 0 0 0 0 Re f 4 1 0 0 0 1 0 (a) Berechnete Distanzen des Kreuzproduktoperators (b) Substituierte Minima des Zeilenoperators Re f 10 Data1 0 Data2 0 Data3 0 Data4 0 Data5 0 Data6 0 Re f 20 0 0 0 0 0 0 Re f 30 0 0 0 0 0 0 Re f 40 0 0 0 0 0 0 (c) 0-initialisierte Matrix mit neuen Referenzpunkten Abbildung 2.7: Schrittweise Veränderung der Matrizen durch eine k-Means Variante punkt liegt. Diese Punkte sind durch die mit einer 1 gekennzeichneten Spalten zu wählen. Die Nutzerfunktion des col Operators mittelt alle Koordinaten und generiert so die neue Referenzmenge. Dieser Ablauf müsste solange wiederholt werden, bis die ausgegebene Referenzmenge keinen Unterschied mehr zur eingegebenen Referenzmenge aufweist. Die Spezifikation einer row cPIPE wird in Code Beispiel 2.1 in der ursprünglichen Haskellfassung dargestellt. Zu sehen sind drei relevante Codeabschnitte, um eine cPIPE zu beschreiben. Mit Hilfe der zuerst auftretenden Variablendeklaration kann der in Abschnitt 3.1 näher beschriebene Source-toSource Compiler einfacher die Datentypen von verwendeten Variablen erkennen. Ein mit „Start“ beginnender Kommentar bedeutet, dass der nachfolgende Code die eigentliche Funktionalität beschreibt, wobei der „FunctionCall“-Kommentar dem Compiler mitteilt, dass der folgende Block die Aufrufmodalitäten des Operators beinhaltet. Jede Codeeinheit wird mit einer kommentierten Raute beendet. Diese Methode vereinfacht das Einlesen der jeweiligen Codebereiche für den Compiler extrem. Es müssen keine aufwändigen Methoden geschaffen werden, um die Segmente voneinander unterscheiden zu können. Wie Code Beispiel 2.1 ebenfalls zu entnehmen ist, fehlen die für Haskell typischen Tabulatoren nach Zeilenumbrüchen. Das Vorhandensein oder Weglassen selbiger ist aufgrund der Komplexität der untersuchten Algorithmen unerheblich und wurde aus diesem Grund nicht als obligatorisch eingestuft. Weiterhin ist der Umfang der unterstütz- //VariableDefinition inList1 :: [[Double]] inList2 :: [[Double]] //# //StartMinimum calcMin :: [Double] -> Double calcMin inList = minimum inList //# //FunctionCallMinimum //rows //OperatorArgs:inList1,inList2 //InputType:Value //Fusion:Euklid [ calcMin inList2!!i | i <- [ 0 .. (length inList1) ] ] //# Code Beispiel 2.1: Haskellbasierter Ansatz: Beispielcode eines zeilenweise arbeitenden Minimum Algorithmus 22 2.3 cPIPE-Ansatz für Clustering-Algorithmen ten Funktionen seitens Haskell zunächst eingeschränkt und aus Gründen der Effizienz nur auf ein Subset eingegrenzt, welches für die Realisierung der zu testenden Algorithmen relevant ist. Eine weitere strukturelle Regel stellt die Reihenfolge der Spezifikation dar. Die Haskelldatei ist so konzipiert, dass die darin enthaltenen Informationen für den Source-to-Source Compiler ausreichend sind, um einen Analysevorgang starten zu können. Eine solche Datei repräsentiert also immer genau einen Clustering-Algorithmus. Da dieser aus mehreren, verketteten cPIPEs bestehen kann, muss auch eine Reihenfolge selbiger angegeben werden. Diese wird trivialer Weise direkt aus der Reihenfolge extrahiert, in der die „//Start“ Blöcke eingelesen werden. Bei oberflächlicher Betrachtung von Code Beispiel 2.1 könnte der Eindruck entstehen, dass anstatt der Programmkonstrukte eher Pragmas zur Anwendung kommen. Diese stellen compilerspezifische Anweisungen dar, welche nur dann ausgeführt werden, wenn der aktive Compiler jene auch kennt. Bei unbekannten Pragmas werden diese ohne die Ausgabe von Fehlern oder Warnungen übergangen. Auf diese Weise wird beispielsweise in OpenMP die parallelisierte Verarbeitung von sonst sequentiellem Code reguliert. Die Annotationen im Haskellprogramm sind allerdings als Hinweise zu deuten, welche das Einlesen des Codes vereinfachen sollen und dürfen in keinem Fall ignoriert werden, da sonst Inkonsistenzen auftreten. Eine semantische Ähnlichkeit mit Pragmas besteht nicht. Durch die Einführung der Annotationen im Code sollen die dem Reifegrad der Sprache entsprechenden Mängel auf einfache Weise ausgeglichen werden können. Der zuvor beschriebene k-Means Algorithmus wird in Code Beispiel 2.2 komplett in der weiterentwickelten Form von PIPEBASE dargestellt. Mit der reinen, auf Haskell basierten Fassung hat diese Version gemein, dass die Reihenfolge der Operatoren durch deren Auftreten innerhalb der Spezifikationsdatei bestimmt ist. Die Beschreibung der Nutzerfunktionen findet weiterhin durch die an Haskell angelehnte Syntax statt. Die zweite Entwicklungsstufe von PIPEBASE arbeitet die Modularität der cPIPEs besser heraus und zeigt, wie einzelne Elemente miteinander verknüpft werden können. Dass die neuere PIPEBASE Version abwärtskompatibel ist, wird durch Abbildung 2.8 gezeigt. Auf der linken Seite ist zu sehen, wie die Variablendeklaration der neuen Version (oben) in den haskellbasierten, ursprünglichen Ansatz überführt werden kann. Die Funktionsdefinition auf der rechten Seite wird analog behandelt. Jedes Element der neuen PIPEBASE-Formulierung ist entsprechend seinem Vorgängerpendant eingefärbt. Einzig der „Fusion“ Kommentar, durch dessen Anwesenheit Hinweise zur Optimierung gegeben werden, ist bisher nocht nicht abbildbar. A = cross( "points", D, R, functionEuklid B = row( "values", A, functionMinimum ); D = Vector< Vector< Double > >; R = Vector< Vector< Double > >; //VariableDefinition inList1 :: [[Double]] inList2 :: [[Double]] //# ); //FunctionCallMinimum //rows //OperatorArgs:inList1,inList2 //InputType:Value //Fusion:Euklid [ calcMin inList2!!i | i <- [ 0 .. (length inList1) ] ] //# Abbildung 2.8: Transformation von PIPEBASE in den reinen Haskell Ansatz 23 Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen -- Haskell Funktionsdefinition functionEuklid = "euklid :: [Double] -> [Double] -> Double euklid p1 p2 = (sqrt . sum) squaresOfDifferences where squaresOfDifferences = zipWith squareDist p1 p2 where squareDist x1 x2 = dist * dist where dist = x1 - x2"; functionMinimum = "calcMin :: [Double] -> Double calcMin inList = minimum inList"; functionKMeans = "kmeans :: [Double] -> Double kmeans inList1 = [sum inList1] / [length inList1]"; -- Daten- und Referenzmengendefinition D = Vector< Vector< Double > >; -- Datenliste mit 2-dimensionalen Punkten R = Vector< Vector< Double > >; -- Referenzlistemit 2-dimensionalen Punkten -- cPIPE Definition for ( 50 ) { -- 50 Iterationen -- Berechnet Distanzen A = cross( "points", D, R, functionEuklid ); -- Ersetzt Minima mit 1, rest der Zeile mit 0 B = row( "values", A, functionMinimum, "1" ); } -- 1-markierte Punkte werden functionKMeans übergeben R = col( "points", B, functionKMeans ); Code Beispiel 2.2: Alternative Spezifizierung, leichter lesbar 2.4 ZUSAMMENFASSUNG Als maßgeblicher Trend für zukünftige Entwicklungen im Hard- und Softwarebereich ist die parallele Programmierung im Fokus aller Bereiche. Aufgrund der vielen verschiedenen Systemkombinationen und Komponenten ist die Spezifikation von parallelen Algorithmen nicht immer trivial. Um die Nachteile der plattformnahen Entwicklung zu überdecken, hat Google mit MapReduce ein System entwickelt, welches genau dieses Problem beseitigen konnte. Durch dessen Erweiterung mit dem PACT Programmiermodell wurde ein weiterer Freiheitsgrad gewonnen. Indem die PACTs nicht mehr an ein starres Gerüst gebunden sind, sondern frei verbunden werden können. Als universelles Mittel zur plattformunabhängigen Spezifikation von parallelen Algorithmen bietet das abstrakte PIPE Modell ein System, mit dem es keine Limitierung mehr auf eine einzelne Domäne gibt. Die durch das System zur modularen Clusteranalyse von [Hah14] inspirierten PIPEs sind ähnlich zu [Ale11]. Die Definitionen der spezialisierten cPIPEs und PACTs funktionieren analog. Ebenfalls bieten beide Modelle eine Möglichkeit, vom MapReduce Standard abzuweichen und eine dynamische Abarbeitung von komplexeren Aufgaben zu ermöglichen. Genau wie bei PACT ist die Reihenfolge der cPIPEs unerheblich, sodass eine konkrete Algorithmenimplementierung als Orchestrierung der zu verwendenden PIPE-Elemente bzw. deren 24 2.4 Zusammenfassung Ausprägungen, im Falle des Data Clusterings den cPIPEs, erstellt wird. Die beiden Ansätze unterscheiden sich allerdings in zwei wichtigen Punkten. PACT wird durch ein eigenes System realisiert und arbeitet auf Schlüssel-Wert Paaren. PIPE ist hingegen frei vom Datenschema, obgleich jede Ausprägung sich auf ein Datenschema festlegen muss. Im Falle von cPIPE wäre die Basis für die interne Datenhaltung ein Schema bestehend aus Tripeln mit der Form ZeilenID-SpaltenID-Wert. Weiterhin ist PACT an die eigene Ausführungsumgebung gebunden, wohingegegen PIPE durch seine Abstraktion als absolut plattformunabhängig betrachtet werden kann. 25 Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen 26 3 PIPE COMPILER FÜR EFFIZIENTE AUSFÜHRUNG Das PIPE Konzept aus Kapitel 2 stellt einen abstrakten Ansatz zur Beschreibung von parallelen Algorithmen dar. Die cPIPE Ausprägung für den Bereich Data Clustering ist ebenfalls noch plattformunabhängig und muss durch einen Übersetzer in plattformabhängigen Programmcode übersetzt werden. Dafür bietet sich die Verwendung eines Source-to-Source Compilers an, wie in Abbilung 3.1 dargestellt ist. Ziel ist es, die domänenspezifischen, aber plattformunabhängigen cPIPEs in für eine Ausführungsumgebung spezifischen Code auf effektive Weise umzuwandeln. Zu diesem Zweck werden die Umgebungen MapReduce und OpenCL im Detail betrachtet. Eingabe 1 Beschreibung Ausgabe 1 Eingabe n func Ausgabe n Plattformunabhängig Source-to-Source Compiler MapReduce OpenCL Plattformabhängig ... Abbildung 3.1: PIPEBASE Schema 27 Kapitel 3 PIPE Compiler für effiziente Ausführung 3.1 VORBETRACHTUNGEN Im Folgenden Abschnitt werden theoretische Grundlagen zum Thema Source-to-Source Compiling sowie zu den angestrebten Ausführungsumgebungen MapReduce und OpenCL gegeben. Neben historischen Daten werden auch ähnlich geartete Arbeiten vorgestellt. 3.1.1 Source-to-Source Compiling Der Gedanke, Quellcode einer Sprache in den einer anderen umzuwandeln, ist keineswegs neu. Bereits 1977 verfasste Loveman einen Artikel, welcher die Möglichkeiten der Programmoptimierung mittels Source-to-Source Transformation thematisiert [Lov77]. Programme seien schon immer an Zeit- oder Kapazitätsgrenzen gebunden, was entweder durch deren Ausführung auf Minicomputern, Echtzeitanforderungen oder ökonomische Betrachtungsweisen aufgrund mehrfacher Ausführung des selbigen begründet ist [Lov77]. Die Optimierung selbst sei schlußendlich nur das Ergebnis sukzessiv ausgeführter Transformationen, um einen Quellcode in anderen Quellcode zu überführen. Dafür nimmt er an, dass von einem Programm eine baumstrukturierte Repräsentation erstellbar ist und dass Souce-to-Source Transformationen patterngesteuerte Überführungen von Baumdarstellungen des Programmcodes sind. Zu seinen Erkenntnissen zählt, dass nicht jede anwendbare Transformation einen Gewinn für die Leistungsfähigkeit des Programms bringen muss und dass in der Regel die Anwendung einer Transformation ihre logischen Nachfolger automatisch impliziert [Lov77]. Loveman stellte fest, dass bei der Programmierung die Formulierung eines problemlösenden Algorithmus im Vergleich zu dessen Effizienz höher priorisiert ist. Das aus dem Quellcode kompilierte Programm neige deswegen dazu, weniger effizient zu sein. Zu seinen Erkenntnissen zählt, dass Performanz großes Gewicht hat. Um das volle Potenzial von Programmen auszuschöpfen, sei eine transparente Nutzung von Source-to-Source Transformationen notwendig [Lov77]. Code Beispiel 3.1 zeigt den Nutzen von Source-to-Source Transformation mittels des eingegebenen Codes und dem fertig optimierten Produkt. Es handelt sich um einen Algorithmus zur Multiplikation diagonaler Matrizen. Aufgrund a priori vorhandener Informationen wie dieser können mehrere Transformationen durchgeführt werden, welche den stark vereinfachten Code auf der rechten Seite generieren. Einen aktuelleres Beispiel stellt die Arbeit von Vitaly Schreibmann dar. Er verwendet eine DSL, um mit einem modellgesteuerten Softwareentwicklungsprozess eine Representational State Transfer (REST) konforme Programmierschnittstelle generieren zu lassen. Anders als eine Generalpurpose Language (GPL) ist eine DSL darauf ausgelegt, nur die Probleme der eigenen Domäne loop for i = 1 to 10 loop for j = l to 20 c[i,j] = 0, loop for k = 1 to 10 c[i,j] = a[i,k] * b[k,j] + c[i,j] repeat repeat repeat loop for i = 1 to 10 loop for j = 1 to 20 c[i,j] = a[i,j] * b[i,j] repeat repeat Code Beispiel 3.1: Anfang und Ende der Source-to-Source Transformation eines Matrix-Multiplikationsalgorithmus, entnommen aus [Lov77] 28 3.1 Vorbetrachtungen abzubilden. Dies dient dem Zweck, domänenspezifischen Experten die Programmierung solcher Schnittstellen ohne externes Wissen zu ermöglichen und die Komplexität der Programmierung auf ein Minimum zu reduzieren. Weiterhin soll dadurch die Qualität des Codes gesteigert, der gesamte Entwicklungsprozess verkürzt und REST Konformität garantiert werden [Sch14]. Ähnlich zu dieser Arbeit verwendet [Das11] einen skeletonbasierten Ansatz. SkePU steht für die zwei Begriffe „Skeleton“ und „PU“. Der Begriff des Skeletons wird von [Das11] als vordefinierte Fuhnktion definiert, die vom Programmierer zur Umsetzung seines Algorithmus zu verwenden sind. Sie stellen also zuvor verfasste Funktionsgerüste dar, welche lediglich mit den relevanten Code für die aktuelle Problemstellung gefüllt werden müssen. „PU“ steht für Processing Unit [Das14]. Der Name soll implizieren, dass die vordefinierten Skeletons mit einer Vielzahl von möglichen Backends, also bereits für Zielplatformen optimierte Implementierungen, ausgelegt sind und so ein breites Anwendungsspektrum bieten. [Das11] beschreibt die aktuelle Verfügbarkeit von Skeletons für „map“, „reduce“, „mapreduce“, „map-with-overlap“, „maparray“ sowie „scan“. Ausserdem sei das Framework so allgemein gehalten, dass es mehere Architekturen unterstützt und auch für sequentiellen Code für Central Processing Units (CPUs) sowie parallelisierte OpenMP-Implementierungen bei der Verwendung von Mehrkern-CPUs ausgelegt ist [Das11]. Das Compiler-Framework Cetus ist der Arbeit von Loveman ähnlich. Es stellt eine Softwaresuite bereit, mit der ANSI C Programme Source-to-Source transformiert werden können. Seinen Ursprung hat Cetus in einem Kursprojekt einiger Compilerbau-Studenten. Deren Aufgabe war es, einen Source-to-Source Compiler für C zu entwickeln. Geschrieben wurde das Projekt in Java, beinhaltet keinen proprietären Code und basiert ausschließlich auf frei verfügbaren Werkzeugen. Anders als diese Arbeit setzt Cetus auf die Verwendung eines vorhandenen Präprozessors, um dessen Ausgabe für die Erstellung seiner internen Repräsentation des Codes zu verwenden [Lee04]. 3.1.2 Ausführungsumgebungen Verschiedene Systeme, seien es Hard- oder Software, bedeuten unterschiedliche Bedingungen. Hardwareseitig betrachtet reicht die Vielfältigkeit von Plattformen beginnend bei Standardsystemen mit nur einem (Mehrkern-)Prozessor und gipfelt in kompletten, auch dezentral verwalteten, Rechenzentren, in denen Hochleistungsserver mit einer Vielzahl von Recheneinheiten stehen. Für den Programmierer ist es in der heutigen Zeit unerheblich, ob es sich um die klassische CPU handelt, oder um einen Rechencluster bestehend aus Grafikkarten bzw. Graphics Processing Units (GPUs). Für vielerlei solcher Systeme, seien sie homo- oder heterogen, wurden Frameworks entwickelt, um effiziente, transparante und skalierbare Anwendungen zu entwickeln, welche die volle Leistungsfähigkeit ihres Zielsystems ausnutzen können. Zu diesem Zweck existieren unterschiedlichste Ausführungsumgebungen. Sie beschreiben den Kontext, in dem eine Applikation funktioniert; eine Schnittstelle zur Kommunikation mit der zu Grunde liegenden Hardware. Um aktuellen Applikationen ein Höchstmaß an Skalierbarkeit und Effizienz zu verleihen, setzen Programmierer immer mehr auf eine verteilte Berechnung der internen Datenströme. Wie in Abschnitt 2.1.2 angedeutet, stellt MapReduce einen bekannten Vertreteter einer solchen verteilten Ausführungsumgebung dar. Weitere Alternativen sind unter Anderen OpenMP, OpenCL und CUDA, über die in [Yan11] sowie [Khr14] eine grundlegende Übersicht gegeben werden. Abhän- 29 Kapitel 3 PIPE Compiler für effiziente Ausführung gig von der verwendeten Programmiersprache kann der selbe Code in diversen Umgebungen verwendet werden. Je nach dem, wie unterschiedlich die gewählten Ansätze sind, müssen Adaptionen nur in geringfügigem Maße durchgeführt werden. Um effektive Tests durchführen zu können, orientiert sich diese Arbeit an den zwei repräsentativen Umgebungen MapReduce sowie OpenCL. Sowohl MapReduce als auch OpenCL weisen die grundlegende Ähnlichkeit auf, dass eine spezifizierte Funktion auf unterschiedlichen Daten arbeitet, was auch als Single Instruction, Multiple Data (SIMD) bezeichnet wird [Gro13]. Sie unterscheiden sich allerdings hinsichtlich der Art der Partitionierung der Daten. Bei einem MapReduce-Job werden diese nicht zwangsweise in eine Anzahl zerlegt, die der Menge an vorhandenen Mappern entspricht. Bei einer Vektormultiplikation mittels OpenCL ist dies allerdings notwendig. Weiterhin haben beide die Eigenschaft gemein, dass durch sie ein Framework bereit gestellt wird, welches von Anwendern programmierten, sequentiellen Code parallelisiert ausführt, der auch auf Just-in-Time (JIT)-kompilierte Weise in das System eingebracht werden kann. Eine Synthese dieser beiden Systeme stellt HadoopCL dar. Dieses Framework ermöglicht es, ein MapReduce System innerhalb heterogener Rechencluster mit Hilfe von OpenCL zu verbinden und so die Stärken beider Technologien zu vereinen. Programmcode von Nutzern soll auf diese Weise auf allen Zielplattformen ausführbar sein [Gro13]. 3.1.3 MapReduce MapReduce stellt ein von einem Master gesteuertes System dar, welches zur effizienten Prozessierung großer Datenmengen von der Firma Google entwickelt wurde. Zu Grunde liegt die Idee, aus einer Vielzahl von potentiell fehleranfälligen Rechnern einen Cluster zu bauen, der fehlertolerant Benutzerprogramm (1) fork (1) fork (1) fork Master (2) Map Zuordnung (2) Reduce Zuordnung split 0 Arbeiter Arbeiter split 1 Ausgabedatei 0 (6) Schreiben split 2 (3) Einlesen Arbeiter (4) Lokales Schreiben (5) Externes Lesen split 3 Arbeiter Ausgabedatei 1 Reduce Phase Ausgabedateien Arbeiter split 4 Daten Partitionierung Map-Phase Temporäre Dateien (Mapper-lokal) Abbildung 3.2: MapReduce Prozess, entnommen aus [Dea08] 30 3.1 Vorbetrachtungen arbeiten kann. Stellt ein Benutzer eine Anfrage an den MapReduce-Cluster, werden zunächst die notwendigen Daten in einer vom Nutzer zu spezifizierende Größe partitioniert. Anschließend werden Master-, Mapper- und Reducer-Instanzen auf den verschiedenen Rechnern innerhalb des Clusters gestartet. Darauf folgt der namensgebende Map-Reduce-Zyklus. Jeder Mapper liest die ihm vom Master zugewiesenen Daten ein und erstellt Schlüssel-Wert-Paare. Diese werden in lokalen, temporären Dateien zwischengespeichert, bis sie von einem Reducer weiterverarbeitet werden. Dieser kombiniert die jeweiligen Datenpakete in einer Ausgabedatei. Kommen mehrere Reducer zum Einsatz, müssen diese Dateien abschließend kombiniert werden. Der gesamte Vorgang kann mit Hilfe von Abbildung 3.2 nachempfunden werden. Der Master steuert die Zuteilung der Daten sowie die Aufgabenverteilung, welcher Porzess Mapper- oder Reduceraufgaben übernimmt. Der Master überwacht permanent die Lebendigkeit der aktiven Jobs. Sollte einer jener Prozesse abstürzen, so wird dessen gesamte Aufgabe an einen neuen Mapper weiter gegeben und neu gestartet. Die bisher in den temporären Dateien abgelegten Rechenergebnisse gehen so verloren. Es ist allerdings effektiver, die vollständige Arbeit von neuem zu starten, als aufwändige Datensicherungsmaßnahmen wie beispielsweise Replikationen zu verwenden. Eine gute Übersicht zum kompletten MapReduce-Prozess bietet [Dea08]. 3.1.4 OpenCL OpenCL steht für Open Computing Language und stellt den ersten, offenen und kostenlosen Standard für parallele Programmierung von Mehrkernprozessoren dar [Khr14]. Bisher wurden 4 Versionen veröffentlich, von denen OpenCL 2.0 die aktuellste ist. Dem System liegt die Idee zu Grunde, dass eine klassische Schleife durch eine N-dimensionale Berechnungsdomäne ausgetauscht werden kann. Abbildung 3.3 illustriert einen solchen Ablauf und Code Beispiel 3.2 zeigt C++ Programm func( p1, p2 ) func( p1, p3) func( p1, p.. ) C++ Programm OpenCL Kontext Buffer Strukturen Kernel Initialisierung func( p1, pN ) Asynchroner Kernel Aufruf Synchronisation Ergebnisverarbeitung Abbildung 3.3: Illustrierter Ablauf eines OpenCL gestützten Programmes 31 Kapitel 3 PIPE Compiler für effiziente Ausführung diese Idee in C Code. Bei der klassischen Variante, auf der linken Seite, wird die Schleife N mal auf serielle Weise ausgeführt. Der OpenCL-Kernel rechterhand wird hingegen N mal parallel berechnet. Dadurch kann die Ausführungszeit deutlich vermindert werden, je stärker die Anzahl verfügbarer Prozessoren gegen N konvergiert. Ein OpenCL-basiertes Programm muss dafür zunächst immer einen OpenCL-Kontext initialisieren. Dieser wird auf die Zielplattform, also den zu verwendenden Prozessortyp, ausgelegt. Ist ein solcher Kontext einmal erstellt, können sämtliche verfügbaren OpenCL-Kernels zu jeder Zeit ausgeführt werden. Grundlegend kann eine Vielzahl von Prozessortypen verwendet werden. Gängige Beispiele hierfür sind unter anderen CPUs, GPUs oder Field Programmable Gate Arrays (FPGAs). Das System wurde so konzipiert, dass derselbe Code auf allen genannten Prozessoren problemlos ausgeführt werden kann, was wiederum durch den zuvor initialisierten OpenCL-Kontext realisiert wird. Programmiert wird in einem Subset des ISO C99 Standards [Tra13], wobei mittlerweile auch Erweiterungen hinsichtlich C++ Features wie Vererbung oder Templates vorhanden sind [Ros11]. Für grundlegende Informationen und weitergehende Lektüre wird auf [Tra13] und [Sto10] verwiesen. void vectorMult( const float* a, const float* b, float* c, const unsigned int count) { for(int i=0; i<count; i++) c[i] = a[i] *b[i]; } kernel void vectorMult( global const float* a, global const float* b, global float* c) { int id = get_global_id(0); c[id] = a[id] * b[id]; } Code Beispiel 3.2: Traditionelle Schleife vs. OpenCL Ansatz, entnommen aus [Tra13] 3.2 PIPE COMPILER ANSATZ Eine fertige Algorithmenbeschreibung in PIPEBASE enthält eine Menge von PIPE Elementen in einer domänenspezifischen Ausprägung. Im Falle des Data Clusterings werden die cPIPEs betrachtet. Die Aufgabe des Source-to-Source Compilers besteht darin, die von einem Entwickler verfasste Haskelldatei in PIPEBASE-Syntax zu lesen und zu verarbeiten. Dabei gilt es, mehrere Herausforderungen zu bewältigen. Zunächst müssen die Funktionsrümpfe erstellt werden, die den cPIPEs selbst entsprechen. Diese können entweder direkt übersetzt werden oder als vorgefertigte Implementierung bereits vorhanden sein. Im Rahmen von MapReduce entsprächen diese Rümpfe den namensgebenden Funktionen. Für OpenCL repräsentieren die cPIPEs die Kernels. Der nächste Schritt beinhaltet die Transformation der Funktionen erster Ordnung in die jeweilige Syntax der angestrebten Ausführungsumgebung. Dabei sollen, entsprechend der Definition von PIPE, aus der selben Spezifikation sowohl die Inhalte der Map und Reduce Funktionen, als auch die Funktionen innerhalb der OpenCL Kernels generierbar sein. Dafür muss der Source-to-Source Compiler die Syntax der in Haskell spezifizierten Nutzerfunktionen sowie die Syntax der Zielsprachen kennen und über Regeln verfügen, um Haskell in die jeweilige Zielsprache übersetzen zu können. Am Ende dieses Prozesses soll die zu verwendende Ausführungsumgebung gestartet werden, um den Analysevorgang zu beginnen. Das ganze System wurde so konzipiert, dass dem Compiler zwei obligatorische Kommandozei- 32 3.2 PIPE Compiler Ansatz lenparameter zu übergeben sind, wobei auch ein optionales hinzugefügt werden kann. Notwendig ist der Pfad zur Datei mit dem Algorithmus sowie die Spezifikation, ob das MapReduce oder OpenCL Framework verwendet werden soll. Das optionale Argument lautet „filter“. Es weist den Compiler an, Annotationen der Form „//Fusion:Funktionsname“ zu beachten, wie sie in Code Beispiel 2.1 zu sehen ist. Zunächst muss allerdings erst eine interne Darstellung der spezifizierten Algorithmen angelegt werden. Ähnlich dem Baukasten für Data Clustering aus [Hah14] soll auch der Compiler modular aufgebaut sein. Dadurch wird gewährleistet, dass neben MapReduce und OpenCL durch zukünftige Entwicklung auch andere Ausführungsumgebungen ohne erheblichen Aufwand unterstützt werden können. Wie in Abschnitt 2.2 angedeutet, wird für jede Umgebung auch eine spezifische Implementierung der PIPEs vorhanden sein. Um eine neue Umgebung zu unterstützen, muss dem Compiler also lediglich ein neues Ausgabemodul hinzugefügt werden. Dieses kennt die spezifische Syntax der neuen Ausführungsumgebung und kann aus der internen Darstellung den Quellcode für die neue Plattform erzeugen. Nach dem Einlesen der Codeblöcke werden diese mit jeweils eigenen Methoden verarbeitet und deren Informationen extrahiert. Der Compiler arbeitet hierbei mit der LL(1) Methode. Das bedeutet, dass der Programmcode von links nach rechts gelesen wird und anschließend eine Linksableitung stattfindet. Fundierte Informationen zum Thema LL- bzw. LR-Parsing können [Par11] entnommen werden. Bei der Generierung der internen Darstellung wird für jeden Operator ein Objekt angelegt. Dieses beschreibt seine enthaltenen Symbole und ggf. Unterfunktionen, falls diese sich aus der Implementierung ergeben. Dafür wurden Ableitungsregeln erstellt, welche der Haskellsyntax nachempfunden sind. In Orientierung an Abbildung 3.6 besteht eine feste Reihenfolge, welche die einzelnen Schritte steuert. Um von Position (1) zu (2) zu gelangen, wird zunächst die vollständige Datei gelesen und zeitglich markiert, an welcher Stelle die jeweiligen Codeblöcke zu finden sind. Bei der Verarbeitung eines Funktionsblocks wird auf folgende Weise vorgegangen: Befindet sich in dessen erster Zeile beispielsweise eine Zeichenkette mit n Parametern gefolgt von einem doppelten Doppelpunkt gefunden, so muss diese der Funktionsdefinition entsprechen und kann daraufhin in ihre Bestandteile zerlegt werden. Anschließend erfolgt eine zeilenweise Verarbeitung des Codes, gestützt von einer Art Mustererkennung. Bei der Transition von Zustand (2) zu (3) findet die tatsächliche Abbildung vom plattformunabhängigen Algorithmus zur spezifischen Implementierung statt. Nachdem alle Blöcke auf diese Weise verarbeitet wurden, beginnt die Übersetzung in die Zielsprache. Dies geschieht ebenfalls anhand einfacher Regeln und wird durch die, so allgemeingültig wie möglich gehaltene, interne Darstellung vereinfacht. Ein solcher Ableitungsvorgang soll anhand des Codes zur Berechnung der euklidischen Distanz aus Code Beispiel 2.2 durch Abbildung 3.4 schrittweise verdeutlicht werden. Zu Beginn der Transformation wird der gesamte Körper des Algorithmus analysiert, um die von einer Ersetzung oder Erweiterung betroffenen Bezeichner zu entdecken. Im Beispiel betrifft das „squareOfDifferences“ sowie „squareDist“. Ersteres ist ein Platzhalter, um eine bessere Lesbar- (1) (2) (3) (4) (5) (6) squareOfDifferences - Funktion: zipWith Argumente: squareDist, p1, p2 static double squareDist( double p1, double p2 ) {...} sqrt( sum ) sqrt( haskell_sum( squareOfDifferences ) ) sqrt( haskell_sum( zipWith ) ) ) sqrt( haskell_sum( haskell_zipWith( squareDist, p1, p2 ) ) ) Abbildung 3.4: Schrittweise Generierung des Funktionsaufrufs für die Berechnung der euklidischen Distanz 33 Kapitel 3 PIPE Compiler für effiziente Ausführung keit der Zeile zu garantieren. Zweiteres stellt ein Alias für eine eingebettete Funktion dar. Die Symbole werden schrittweise in der Reihenfolge ihrer Erkennung verarbeitet und zwischengespeichert bzw. direkt in die Ausgabedatei geschrieben, siehe (1) und (2). Die Schritte (3) bis (6) zeigen die schrittweise Ersetzung der Funktionsnamen, basierend auf der Erkennung ihrer Parameter. Begründet durch die Haskellsyntax wurde eine linksorienterte Ersetzungsstrategie gewählt. Ausgehend von der am weitesten links stehenden Funktion werden deren Parameter erschöpfend durch die resultierenden Funktionsaufrufe ersetzt, bis schließlich der komplette Operatorkern entstanden ist. Damit dieser Prozess ohne externe Einwirkung funktionieren kann, muss jedes Ausgabemodul des Source-to-Source Compilers die genaue Struktur der Datengrundlage einer Ausführungsumgebung kennen und wissen, wie die erwarteten Parameter der verwendeten cPIPEs definiert sind. Während des Transformationsvorgangs konnten die Vorteile der zu Beginn von Kapitel 3 beschriebenen skeletonbasierten Programmierung nutzbar gemacht werden. Gerüste von OpenCL Kernels bzw. von MapReduce verwendeter Code müssen lediglich befüllt werden und können so in ihrer Struktur auch leichter angepasst werden, falls dies nötig werden sollte. Sobald die Transformation abgeschlossen ist, wird die angestrebte Ausführungsumgebung kompiliert und somit zu Punkt (4) aus Abbildung 3.6 übergegangen. Nachdem die interne Darstellung vervollständigt wurde, wird diese als Basis für die Codegeneration genutzt. Grundlegend gilt, dass der Compiler abhängig von der Ausführungsumgebung eine Ausgabedatei erstellt. Im Fall von MapReduce entsteht eine Headerdatei, welche eine Klasse enthält. Diese Klasse wird mit sämtlichen übersetzten cPIPEs und deren Unterfunktionen gefüllt. Für OpenCL wird eine Kerneldatei erstellt. Diese enthält normalen C Code. Abbildung 3.5 zeigt vereinfacht, auf welche Weise der Minimumoperator aus Code Beispiel 2.1 in C++ Code für das MapReduce Framework überführt wird. //StartMinimum calcMin :: [Double] -> Double calcMin inList = minimum inList //# static double calcMin( vector< double > inList ) { return haskell_minimum( inList ); } static double haskell_minimum( vector< double > inVec ) { double minVal = std::numeric_limits<double>::max(); if ( !inVec.isEmpty() ) { minVal = inVec.first(); for ( int i = 1; i < inVec.size(); ++i ) { minVal = min( minVal, inVec.at( i ) ); } } return minVal; } //FunctionCallMinimum //rows //OperatorArgs:inList1,inList2 //InputType:Value //Fusion:Euklid [ calcMin inList2!!i | i <- [ 0 .. (length inList1) ] ] //# static List< vector< double > > Minimum( vector< Vector< double > > inList1, vector< Vector< double > > inList2 ) { List< vector< double > > out; for ( int i = 0; i < inList1.size(); ++i ) { vector< double > pair; double val = HaskellTranslated::calcMin( inList2.at( i ) ); pair.push_back( inList1.at( i ).first() ); pair.push_back( inList2.at( i ).indexOf( val ) ); pair.push_back( val ); out.append( pair ); } return out; } Abbildung 3.5: Parametrierung eines MapReduce Skeletons mit dem Code aus Beispiel 2.1 34 3.2 PIPE Compiler Ansatz Auffällig ist, dass eine Zusatzfunktion „haskell_minimum“ entstanden ist, welche nicht spezifiziert war. Während der Erkennung von eingebetteten Funktionen untersucht der Compiler die jeweiligen Funktionsnamen auf Übereinstimmung mit Standardfunktionen, welche von vielen Programmiersprachen bereit gestellt werden. Für das von dieser Arbeit benötigte Subset solcher Standards wurden Funktionstemplates vorgefertigt, welche bei Auftreten einer bekannten und unterstützten Standardfunktion automatisch eingefügt werden. Nachdem alle Standardfunktionen gefunden wurden und sämtliche Funktionsnamen bekannt sind, werden je nach Ausführungsumgebung Funktionsprototypen in die Ausgabedatei geschrieben. Bei der verwendeten OpenCL Version ist dies beispielsweise notwendig. Deren Kernels sind im C99 Standard verfasst und kennen deswegen keine Klassen, weshalb es auf die Reihenfolge der Spezifikation von Funktionen ankommt. Um dem MapReduce Framework eine Überprüfungsmöglichkeit für das Vorhandensein von Operatoren zu geben, wird weiterhin eine Liste mit den Namen der übersetzten Operatoren implementiert. Nachdem alle Skeletons parametriert wurden, wird die fertige Ausgabedatei beim entsprechenden Framework abgelegt, sodass dieses kompiliert und gestartet werden kann. Zur Erstellung des Maschinencodes aus dem C/C++ Code der Ausführungsumgebungen wird die weit verbreitete Gnu Compiler Collection (GCC) verwendet. Abhängig vom Betriebssystem wurde diese auf Linux direkt eingesetzt, wobei für Microsoft Windows die Entscheidung auf die Entwicklungsumgebung Minimalist Gnu for Windows (MinGW) fiel, welche die GCC für das Betriebssystem bereit stellt. Abbildung 3.6 illustriert den vollständigen, vom Framework angestrebten Prozess. Die in PIPEBASE verfasste Datei aus Schritt (1) beinhaltet die Spezifikation des zu übersetzenden Algorithmus. Dieser wird hinsichtlicher aller enthaltenen Funktionen und Symbole analysiert. Anschließend wird im Schritt (2) eine interne Darstellung generiert, welche eine Beschreibung aller gefundenen Informationen sowie deren Beziehungen zueinander abbildet. Aus dieser Datenmenge können im Folgenden die Funktionen zweiter Ordnung, also die cPIPE Rümpfe im Zielcode erstellt werden. Sind diese bereits als Implementierung vorhanden, müssen sie lediglich parametriert werden. Im zweiten Fall ist wird der plattformspezifische MapReduce bzw. OpenCL Kernel Code erzeugt und während Schritt (4) in die bereits übersetzten cPIPE Rümpfe eingefügt, woraufhin das komplette Analyseprogramm JIT-kompiliert wird. Abschließend startet der Source-to-Source Compiler das fertige Programm für die Analyse einer bereits vorhandenen Datenmenge und ei- Clustering Algorithmus (1) Wird eingelesen Source-to-Source Compiler (2) (3) (4) Kompiliert für Ausführungsumgebung 1 Ausführungsumgebung 2 MapReduce Programm OpenCL Programm Funktion1{…}, Funkion2{…}, Variable1, Variable2, ... Interne Darstellung Plattformabhängiger C/C++ Code Abbildung 3.6: Schematischer Ablauf des Source-to-Source Compilers 35 Kapitel 3 PIPE Compiler für effiziente Ausführung ner Referenzmenge. Dieser Ansatz bietet die Möglichkeit, ein universelles Framework bereit zu stellen. Mit dessen Hilfe kann auf hochdynamische Art und Weise die Erstellung von Programmcode für eine Vielzahl von unterschiedlichen Ausführungsumgebungen generiert werden. Durch die Verwendung von PIPEs muss lediglich eine domänenspezifische Implementierung der selbigen vorhanden sein, um diese aus Sicht des Source-to-Source Compilers zu unterstützen. 3.3 OPTIMIERUNGEN Für jeden handgeschriebenen Code gilt in der Regel, dass dieser nicht der Laufzeit entspricht, die unter optimalen Bedingungen erreicht werden könnte. Diese Arbeit untersucht die Möglichkeit, einfache Optimierungsregeln für den gegebenen Nutzercode anzuwenden. Dabei gibt es eine Einschränkung auf die Verkettung der cPIPEs. An dieser Stelle sei allerdings erwähnt, dass auch die Nutzerfunktionen selbst optimiert werden könnten. Grundlegend wäre eine Optimierung der Algorithmen anhand deren Datenflussgraphen denkbar. Es ist ebenfalls möglich, die nach der Übersetzung entstandenen Algorithmen zu restrukturieren, um eine verbesserte Laufzeit zu erreichen. Es werden an dieser Stelle allerdings lediglich logische Optimierungen betrachtet. Als Beispiel dient hierfür der „//Fusion“ Kommentar, wie er in Code Beispiel 2.1 zu sehen ist. Anders als die restlichen Kommentare stellt dieser den einzigen dar, welcher nicht notwendig für die Funktionalität des spezifizierten Algorithmus ist. Auf diese Weise gekennzeichnete Operatoren können vom Compiler in einem neuen fusioniert werden. Ziel ist es dabei, den Aufwand innerhalb der Datenanalyse so gering wie möglich zu halten. Das beste Beispiel stellt cross dar. Beim Kreuzprodukt von genügend großen Datenmengen entstehen Zwischenergebnisse von extremer Größe, die gegebenenfalls nicht mehr im Hauptspeicher gehalten werden könnten. Dementsprechend würde der Source-to-Source Compiler versuchen, einen cross Operator immer mit einem folgenden row bzw. col Operator zu verschmelzen, um so die Speicherung des enorm großen Zwischenergebnisses von cross zu umgehen. Wie die Orchestrierung der cPIPEs dadurch beein- Daten Euklid Referenzen Distanzen Distanzen Minimum Minima cross Minima Mittelwert row col Daten Euklid + Minimum Referenzen Distanzen Minima Mittelwert Referenzen row col Abbildung 3.7: Reduzierung von cPIPEs durch logische Optimierung 36 Referenzen 3.4 Zusammenfassung flusst wird, ist in Abbildung 3.7 schematisiert dargestellt. Weiterhin können direkt aufeinander folgende row bzw. col cPIPEs zusammengefasst werden. Durch die Verschmelzung von cPIPEs kann die Laufzeit vor allem im MapReduce Framework drastisch gesenkt werden, da die Anzahl auszuführender MapReduce Jobs reduziert wird und so weniger Overhead bezüglich der Reinitialisierung aller beteiligten Komponenten notwendig ist. 3.4 ZUSAMMENFASSUNG Der Source-to-Source Compiler stellt das essentielle Bindeglied zwischen der plattformunabhängigen Algorithmenbeschreibung und der plattformabhängigen Implementierung dar. Durch das Überführen einer Eingabesprache in eine interne Repräsentation und einen modularisierten Aufbau ist es möglich, eine Vielzahl von Ausführungsumgebungen anzusteuern, ohne die eigentliche Spezifikation des Algorithmus zu verändern. Mittels MapReduce und OpenCL kann gezeigt werden, dass die cPIPE Ausprägungen von PIPE tatsächlich unabhängig von der eigentlichen Zielplattform sind. Durch den Einsatz einer MapReduce Implementierung ist es ausserdem möglich zu zeigen, dass PIPE lediglich eine abstraktere Variante dessen ist und durch die Spezialisierung als cPIPE äquivalent ausgeführt werden kann. Mit Hilfe von OpenCL ist es weiterhin machbar, den selben Code sowohl auf einer CPU als auch einer GPU auszuführen. 37 Kapitel 3 PIPE Compiler für effiziente Ausführung 38 4 IMPLEMENTIERUNG Der praktische Teil dieser Arbeit besteht daraus, mehrere Ausführungsumgebungen für Tests und zu Validierung der entwickelten Konzepte zur Verfügung zu stellen. Gleichzeitig ist der Source-to-Source Compiler zu implementieren. Um die cPIPE Ausprägung zu erproben, kommen die in Abschnitt 3.1, 3.1.3 und 3.1.4 beschriebenen Ausführungsumgebungen MapReduce und OpenCL zum Einsatz. Die Applikationsbeschreibung der cPIPEs und deren benötigte Datengrundlage weichen von der des standardisierten MapReduce Konzeptes ab. Vorhandene MapReduce Frameworks wie Hadoop konnten unter anderem aus diesem Grund nicht genutzt werden. Die Partitionierung der Daten könnte aufgrund der Zeilen- und Spaltenbasierten Verarbeitungsweise der cPIPEs nicht ohne erheblichen Aufwand realisiert werden. Aus diesem Grund war die Implementierung eines eigenen MapReduce Frameworks notwendig. Abbildung 4.1 zeigt den schematischen Aufbau und die Ablaufstruktur der eigens für diese Arbeit entworfenen Ausführungsumgebung. Da ein Clustering-Algorithmus aus mehreren cPIPEs besteht, wurde deren Abarbeitung im MapReduce Framework als eine Art von Jobs angesehen. Diese werden vom JobHandler nach Ablauf des Transformationsvorgangs ausgeführt, wobei für jede cPIPE ein neuer MapReduce Zyklus gestartet wird (1). Während der Ausführung einer cPIPE steuert der MapReduce Coordinator die Funktionen des Datenstreamers, der Mapper, und des Reducers (2). Die Verwendung einer eigenen Instanz für die Bereitstellung der Daten in Form eines Streamers gewährleistet eine kontinuierliche Bereitstellung der Daten. Während Mapper und Reducer ihre Datenpakete verarbeiten, kann der Streamer aus der Datengrundlage neue Arbeitspakete erstellen und diese in einer synchronisierten Liste für die Mapper bereitstellen. Genauso wie die Mapper und Reducer, arbeitet der Streamer in einem eigenen Thread, also in einer eigenen Aktivitätsumgebung, die von anderen Threads unabhängig ist. In Mehrkernprozessorsystemen können je Prozessor mehrere Threads gleichzeitig verarbeitet werden, dies wird auch Multithreading genannt und bildet eine Grundlage für die parallele Programmierung. Bei der Verteilung der Pakete muss auch auf die in Abschnitt 2.1.1 erklärte Speicherproblematik geachtet werden. Wenn mehrere Mapper gleichzeitig ein Paket entnehmen wollen, könnte das aktuelle Element aus der Liste des Datenstreamers entweder von mehreren Mappern gleichzeitig verarbeitet werden oder ein Mapper könnte auf einen ungeschützten Speicherbereich zugreifen. Um dies zu verhindern, werden die Zugriffe auf die Datenpakete innerhalb des Streamers mittels Semaphoren geschützt. Begründet durch eine ungleiche Datenverteilung, kann die Verarbeitungsgeschwindigkeit der Mapper untereinander variieren. Würde immer gewartet, bis alle Mapper ihr aktu- 39 Kapitel 4 Implementierung JobHandler (1) (4) MapReduce Framework Coordinator (2) (3) Mapper func( ... ) DataStreamer Reducer Mapper func( … ) Abbildung 4.1: Schematische Darstellung des verwendeten MapReduce Frameworks elles Datenpaket verarbeitet haben, käme es zu einer immensen Verschwendung von Rechenzeit, wie in Abbildung 4.2 illustriert wird. Dieser Nachteil wird durch die Datenbereitstellung durch den Streamer beseitigt. Wann Thread1 immer ein Mapper sein aktuelles Datenpaket verarbeitet hat, kann ohne Verzögerung ein neues vom DatenstreThread2 Totzeit amer abgerufen werden, wodurch die Gesamtbearbeitungszeit aus zeitlicher Sicht optimiert werden konnte. ThreadX Im Anschluss an die Verarbeitung und Zusammenführung der Daten seitens Mapper und Reducer werden Sync die Daten im Coordinator gesammelt (3) und abschließend an den JobHandler weitergereicht (4). Die so propagierten Informationen werden als Basis für die Einga- Abbildung 4.2: Totzeiten bei synchronisierter Verarbeitung bedaten des nächsten Jobs verwendet. Als Implementie- 40 rungssprache wurde hierfür das Framework Qt in der Version 5.3 verwendet [Dig14]. Die Wahl fiel auf diese bekannte Open Source Lösung, da sie Plattformunabhängigkeit garantiert, was für diese Arbeit von fundamentaler Bedeutung ist. Weiterhin ermöglichen einige bereitgestellte Klassen eine triviale Handhabung von Threads und deren Kommunikation untereinander auf Basis von Signalen und sogenannten Slots. Dies sind besondere Funktionen, welche zum einen zur Synchronisation verwendet werden können und so fest definierte Blockaden innerhalb kritischer Abschnitte darstellen oder aber auf asynchrone Weise miteinander verbunden werden können, um eine nebenläufige Abarbeitung bestimmter Aufgaben zu realisieren. Ein weiterer Bonus ist die Ermöglichung von Reflexion innerhalb eines C++ Programms, worauf auch Qt basiert. Dafür verwendet Qt einen Meta-Object Compiler (moc), welcher während des Kompilierungsvorgangs eine Liste der vorhandenen Funktionen und deren Parameter erstellt und diese mit in das Program einkompiliert. Solche Informationen sind für jede Klasse verfügbar, welche von der Standardklasse QObject erbt. Dieser Mechanismus wird vom Source-to-Source Compiler in Synergie mit dem selbst verfassten MapReduce-Framework ausgenutzt, um die zur Übersetzungszeit, dem MapReduce Framework, unbekannten cPIPEs ausführen zu können. Neben der notwendigen Plattformunabhängigkeit war auch dies ein Grund, um den Source-toSource Compiler ebenfalls mit dem auf C++ basierten Qt zu implementieren. Neben der Bereitstellung von Klassen zur Handhabung von Threads besteht auch die Möglichkeit zur plattformunabhängigen Bearbeitung von Dateien. Da das komplette System zu einem ausgereifteren Entwicklungszeitpunkt nach Möglichkeit eine Vielzahl von Plattformen unterstützen soll, ist eine derart portable Lösung wünschenswert. Um eine Zeit-effektive Implementierung des Compilers zu ermöglichen, wurde eine grundlegend sequentielle Abarbeitung aller Arbeitsschritte angestrebt. Anders als zu Beginn konzipiert, kann der Compiler noch nicht aus einer absolut dynamischen Menge von Ableitungsregeln schöpfen, sondern ist an ein relativ beschränktes, den getesteten Algorithmen angepasstes, Set zur Erkennung der Syntax gebunden. Die interne Darstellung der Funktionsdaten wurde mit Hilfe eines dedizierten Objekts je cPIPE realisiert. Jeder Bearbeitungsschritt, der zur Erkennung und Verarbeitung der eingegebenen PIPEBASE Spezifikation dient, wurde aus Gründen der Erweiterbarkeit in separate Funktionen ausgegliedert, um eine möglichst einfache Erweiterbarkeit der Funktionalitäten zu ermöglichen. Im Anschluss an die Transformation von PIPEBASE in den plattformabhängigen Programmcode startet der Compiler die jeweilige Ausführungsumgebung. Dafür wurde ein kommandozeilenbasierte Aufruf erdacht, welcher den Programmen die übersetzte Datei sowie die Namen der auszuführenden cPIPEs und deren Eingabetypen in der festgelegten Reihenfolge übergibt. Als Alternative wäre auch die Verwendung einer weiteren Datei möglich, welche all diese Informationen beinhaltet. Um die notwendigen Festplattenzugriffe so gering wie möglich zu halten, eignete sich die kommandozeilenbasierte Variante. Die Entwicklung der OpenCL-Umgebung wurde ausschließlich durch Standard-C++ realisiert. Als Ausgabe des Compilers erhält die OpenCL-Umgebung eine Datei, welche die im eingeschränkten C99 Standard verfassten Kernels enthält. Jeder Kernel repräsentiert eine cPIPE. Die hier notwendige Erkennung der Kernels zur Laufzeit wird durch OpenCL allein durch den Namen der cPIPE gewährleistet, was der Reflexion in Java bzw. mit der Funktionalität von Qt’s moc vergleichbar ist. Anders als im MapReduce Framework wurde Steuerung der Operatoren und Bereitstellung der Daten nicht in dedizierte Threads ausgelagert. Als prototypische Implementierung der OpenCL-Umgebung erwies sich eine Parallelisierung zwischen den Operatoren als unnötig. In der nebenstehenden Abbildung 4.3 wird illustriert, wie die interne Struktur nach der Transformation eines in PIPEBASE spezifizierten Algorithmus funktioniert. Die Kerneldatei 41 Kapitel 4 Implementierung wird als ein OpenCL Programm kompiliert und kann dadurch die Aufrufe der jeweiligen Kernels ausführen. Die OpenCL-Umgebung folgt dabei mit Ausnahme der Kernels einem strikt sequenAusführungsumgebung tiellem Ablauf. Jeder Kernelaufruf fungiert dabei als Synchronisationspunkt, nach dem die verarOpenCL beiteten Daten vom verwendeten Prozessor abgerufen, gegebenenfalls nachbearbeitet und als Eingabedaten für den nächsten Arbeitsschritt verKernel wendet werden. Dieser Vorgang ist analog zum Schema aus Abbildung 3.3, wobei die gewählte C++ Programm Ausprägung allerdings auf jede Parallelisierung abseits der Kernels verzichtet. Bei der Entwicklung aller drei Komponenten, also dem MapReduceFramework, der OpenCL-Umgebung sowie des Source-to-Source Compilers wurden auch Funktionen des C++11 Standards verwendet, sodass zur Abbildung 4.3: Ablauf innerhalb der OpenCLKompilierung des Projekts aktuelle Versionen von Umgebung Qt bzw. MinGW oder GCC notwendig sind. Das Konzept von PIPEBASE wurde bei der Implementierung noch nicht berücksichtigt. Während der Entwicklung des Source-to-Source Compilers wurde die Spezifikationssprache und deren Umfang angepasst und erreichte erst gegen Ende der Arbeit den Stand der prozeduralen Spezifikation. Eine Anpassung des Compilers an die neue Syntax würde eine Umstellung der zuvor genannten Ableitungsregeln zur Folge haben, wodurch das gesamte Programm in großem Umfang hätte überarbeitet werden müssen. Wie in Abschnitt 2.3.3 und durch Abbildung 2.8 gezeigt wurde, kann PIPEBASE in den reinen Haskellansatz überführt werden. Aus diesem Grund ist die Überarbeitung als aktuell unnötig eingestuft worden. 42 5 EVALUATION Dieses Kapitel bewertet die Ansätze von PIPE und das Source-to-Source Compiler Konzept zur Überführung von plattformunabhängigen Code zur plattformspezifischen Implementierung. Es wird auf die Vollständigkeit der Spezifikationsmöglichkeiten mittels PIPEBASE eingegangen und die Eignung von Haskell als Definitionssprache für die Funktionen erster Ordnung diskutiert. Weiterhin wird gezeigt, auf welche Weise sich die Anzahl von cPIPE-Elementen auf die Kompilierungs- bzw. Ausführungszeiten von Algorithmen innerhalb der gewählten Ausführungsumgebungen auswirken können. 5.1 PIPE KONZEPT Die cPIPE Ausprägung von PIPE wurde genutzt, um den k-Means Algorithmus zu beschreiben. Dieser kann in Code Beispiel 2.2 nachgeschlagen werden. K-Means stellt grundlegend den Repräsentanten der Klasse der partitionierenden Clustering-Algorithmen dar. Sämtliche anderen Algorithmen, die zur selben Klasse gehören, können als Variation von k-Means betrachtet werden und hätten deswegen eine ähnliche Spezifikation. Da k-Means mittels PIPEBASE spezifizierbar ist, kann angenommen werden, dass eine Spezifikation anderer partitionierender ClusteringAlgorithmen möglich ist. Ein weiterer bekannter Clustering-Algorithmus ist Density-Based Spatial Clustering of Applications with Noise (DBSCAN), der in [Est96] vorgestellt wurde. Diese Arbeit betrachtet eine Version von DBSCAN, deren Definition aus [Hah14] entnommen ist. Der Algorithmus sucht ausgehend von einem als Cluster markierten Punkt innerhalb einer e-Umgebung nach Punkten. Wenn in dieser e-Umgebung eine Mindestanzahl von Punkten enthalten ist, gilt der Bereich als „dicht“ und wird als Cluster betrachtet. Anschließend werden alle zu einem Cluster gehörenden Punkte hinsichtlich ihrer e-Umgebung untersucht und, falls die Dichtebedingung erfüllt wird, deren Inhalt mit dem Elterncluster verschmolzen. Code Beispiel 5.1 zeigt eine mögliche PIPEBASE Implementierung von DBSCAN. Zur Berechnung des Algorithmus werden lediglich vier cPIPEs benötigt: Eine Instanz von cross, zwei row cPIPEs sowie eine col cPIPE. Diese berechnen zunächst die euklidische Distanz der Punkte und testen auf die Einhaltung der e-Umgebungsbedingung. An- 43 Kapitel 5 Evaluation -- Haskell Funktionsdefinition functionEuklid = "euklid :: [Double] -> [Double] -> Double euklid p1 p2 = (sqrt . sum) squaresOfDifferences where squaresOfDifferences = zipWith squareDist p1 p2 where squareDist x1 x2 = dist * dist where dist = x1 - x2"; functionEpsilon = "calcEps :: [Double] -> Double calcEps inList = if theDistance < __epsilon__ then theDistance else -1 where theDistance = minimum inList"; functionReplace = "let out = [] calcRep :: [Double] -> Double calcRep inList = [ if inList!!j < 0 then out:inList!!j else out : minimum inList | j <- [ 0 .. (length inList) ] ]" -- Daten- und Referenzmengendefinition D = Vector< Vector< Double > >; -- Datenliste mit 2-dimensionalen Punkten -- cPIPE Definition -- Berechnet Distanzen A = cross( "points", D, D, functionEuklid ); -- Ersetzt Minima kleiner epsilon mit der ZeilenID, rest der Zeile mit -1 B = row( "values", A, functionMinimum, "rowID" ); for ( 50 ) { -- 50 Iterationen C = row( "values", B, funtionReplace ); B = col( "values", C, functionReplace ); } Code Beispiel 5.1: In PIPEBASE beschriebene Version von DBSCAN schließend werden in einer Schleife abwechseln zeilen- und spaltenweise die Cluster gesucht, falls vorhanden, deren ID mit dem jeweiligen Minimum der Zeile bzw. Spalte ersetzt. Da dieser Algorithmus stellvertretend für die Klasse der dichtebasierten Clustering-Algorithmen fungiert, wird auch in diesem Fall angenommen, dass diese Klasse mittels PIPEBASE beschrieben werden kann. Die Spezifikation der Nutzerfunktionen mittels Haskell stellt einen kritischen Teil von PIPEBASE und den zugehörigen cPIPEs dar. Die Spezifikation eines Clustering-Algorithmus in einer funktionalen Programmiersprache ist grundlegend einwandfrei, allerdings ist die Ausdrucksweise ein Problem. Um eine möglichst einfache Spezifikation von Nutzerfunktionen zu ermöglichen, muss die Erstellung einer DSL erwogen werden. Sowohl die Beschreibung von Teilen aus k-Means 44 5.2 Source-to-Source Compiler und Ausführungsumgebungen als auch der Ersetzungsfunktion in DBSCAN sind eher kompliziert und unübersichtlich. Mit einer DSL könnten Direktiven geschaffen werden, welche nur die Probleme des Data Clusterings addressieren und dadurch eine effizientere und einfachere Spezifikation ermöglichen. 5.2 SOURCE-TO-SOURCE COMPILER UND AUSFÜHRUNGSUMGEBUNGEN Bei der Übersetzung von einer Eingabesprache in eine Zielsprache ist es immer relevant, wie viel Zeit dieser Prozess in Anspruch nimmt. Das beste Beispiel stellt hierfür der Query-Optimierer in einem DBMS dar. Es muss beachtet werden, dass der Prozess die Query zu optimieren nicht länger dauern sollte, als sie unoptimiert auszuführen. Andernfalls ist der Kosten-Nutzen Faktor zu hinterfragen. Für den Source-to-Source Compiler dieser Arbeit bedeutet dies, ob die Übersetzung der cPIPEs in einem guten Verhältnis zur letztendlichen Ausführungszeit eines Algorithmus steht. Abbildung 5.1 zeigt das Verhältnis Kompilierungszeit und der Menge zu übersetzender cPIPEs. Zu sehen sind drei Kurven. Wird ausschließlich die Übersetzungszeit von PIPEBASE zu MapReduce bzw. OpenCL Code betrachtet, können die Kurven beider Übersetzungszeiten als nahezu deckungsgleich bezeichnet werden. Wird die, in der aktuellen Implementierung als notwendig erachtete, Verarbeitung der Eingabedatenmenge in die Betrachtung mit einbezogen, zeigt die Übersetzung zu MapReduce Code einen deutlichen Geschwindigkeitsvorteil. Aufgrund der Annahme, dass die Anzahl und Dimensionen der Daten- und Referenzmengen zur Kompilierungszeit nicht bekannt sind, müssen die Eingabedaten vor der OpenCL-Übersetzung verarbeitet werden, da dessen Kernels abhängig von den Nutzerfunktionen auch Speicherallokation mit festen Werten benötigen. Dieser Umweg ist nötig, da innerhalb eines OpenCL Kernels keine dynamische Speicherallokation erlaubt ist. Kompilierungszeit in Abhängigkeit von der Anzahl zu übersetzender cPIPEs Kompilierungszeit in Millisekunden 250 200 150 MapReduce Code OpenCL Kernel OpenCL Kernel mit CSV 100 50 0 0 20 40 60 80 100 120 140 160 180 200 Anzahl übersetzter cPIPEs Abbildung 5.1: Kompilierungszeiten in Abhängigkeit der zu übersetzenden cPIPEs 45 Kapitel 5 Evaluation Ein anderes Szenario stellt die Menge der auszuführenden cPIPE-Elemente dar. Es kann davon ausgegangen werden, dass mit steigender Komplexität eines Clustering-Algorithmus auch die Menge der zu verkettenden cPIPEs steigt. Abbildung 5.2 zeigt das Verhalten der Ausführungsumgebungen MapReduce und OpenCL bei der Verkettung mehrerer cross cPIPEs zur Berechnung der euklidischen Distanz. Wie zu erwarten, zeigen beide Ausführungsumgebungen einen linearen Anstieg der Ausführungszeiten. Es besteht also ein direkt proportionaler Zusammenhang zwischen der Anzahl auszuführender cPIPEs und der Dauer der notwendigen Berechnungen. Es muss allerdings festgestellt werden, dass das für diese Arbeit implementierte MapReduce Framework im Vergleich zu OpenCL eine deutlich schlechtere Zeiteffizienz aufweist. Dieser Effekt kann allerdings dadurch erklärt werden, dass MapReduce grundlegend für ein „Scale Out“ Szenario konzipiert wurde. Im Rahmen dieser Arbeit konnten allerdings lediglich Tests im „Scale Up“ Bereich durchgeführt werden. Anders als OpenCL skaliert das MapReduce Framework auf nur einer CPU nicht zufriedenstellend. Laufzeit in Millisekunden für 15.000 Datenpunkte Laufzeit in Abhängigkeit von der Anzahl ausgeführter cPIPEs 4000 3500 3000 2500 MapReduce OpenCL 2000 1500 1000 500 0 1 2 3 4 5 6 7 8 9 10 Anzahl verketteter cPIPEs Abbildung 5.2: Ausführungszeiten bei unterschiedlicher cPIPE Anzahl Dieser Trend wird auch in Abbildung 5.3 widergespiegelt. Der obere Graph der Abbildung zeigt die Ausführungszeit des aus Code Beispiel 2.2 übersetzten k-Means Algorithmus in den beiden Ausführungsumgebungen MapReduce sowie OpenCL. Als Referenz dient eine alternative k-Means Implementierung, die im .NET Framework 4.0 als parallelisierte Variante für einen einzelnen Rechner verfasst wurde. Wie Abbildung 5.2 bereits vermuten ließ, zeigt MapReduce mit steigender Anzahl zu verarbeitender Punkte ein immer schlechteres Laufzeitverhalten. Die übersetzten Kernels von OpenCL berechnen die Cluster der selben Datenmenge allerdings schneller, als die .NET 4.0 Variante für Windows. Die Meßdaten wurden für eine Eingabedatenmenge zwischen 15.000 und 120.000 Datenpunkten erhoben, bei denen jeweils 15 Cluster zur Bestimmung festgelegt waren. Die untere Grafik aus Abbildung 5.3 zeigt den Geschwindigkeitsbonus, auch genannt Speedup, der jeweiligen Ausführungsumgebungen untereinander. Es kann abgelesen werden, dass OpenCL relativ konstant den selben Geschwindigkeitsvorteil gegenüber .NET 4.0 zeigt. MapReduce wird verglichen mit der .NET 4.0 Implementierung immer langsamer. In Bezug auf Abbildung 5.1 kann festgestellt werden, dass die Übersetzungszeit nur einen geringen Teil der tatsächlichen Ausführungszeit einnimmt. Aus diesem Grund kann die Transformation vom plattformunabhängigen PIPEBASE in plattformspezifischen Code als sinnvoll erachtet werden. 46 Laufzeit in Sekunden 5.2 Source-to-Source Compiler und Ausführungsumgebungen k-Means im Qt MapReduce Framework vs. .NET 4.0 Parallelisierung 90 70 50 30 10 .Net 4.0 MapReduce OpenCL 20 40 60 80 100 120 100 120 Anzahl berechneter Punkte (x * 1000) Speedup Faktor Speedup der Technologien untereinander 10 8 6 4 2 .NET 4.0 vs MapReduce OpenCL vs .NET 4.0 20 40 60 80 Anzahl berechneter Punkte (x * 1000) Abbildung 5.3: Laufzeit von k-Means ins .NET, MapReduce und OpenCL 47 Kapitel 5 Evaluation 48 6 ZUSAMMENFASSUNG UND AUSBLICK Ziel dieser Arbeit war es, einen Ansatz zur plattformunabhängigen Spezifikation von parallelen Clustering-Algorithmen zu erarbeiten. Basierend auf dieser Spezifikation sollen mehrere Plattformen sowohl für homogene, als auch für heterogene Systeme angesprochen werden können. Kapitel 2 zeigt den PIPE Ansatz, der genau dieses Problem löst. Gemeinsam mit dem Source-toSource Compiler aus Kapitel 3 konnte ein Framework erarbeitet werden, dessen Funktionsweise am Beispiel des Themenbereichs Data Clustering gezeigt werden konnte. Abbildung 6.1 zeigt, dass auch Ähnlichkeiten zum klassischen DBMS Ansatz vorhanden sind. PIPEs und deren spezialisierte cPIPEs zeigen ein analoges Verhalten zu logischen und physischen Datenbankoperatoren. Logische Datenbankoperatoren repräsentieren Funktionen mit zwei Dateneingängen sowie einem Datenausgang. Für jeden logischen Operator existiert mindestens eine, oftmals aber auch mehrere Implementierungen. Aus dieser Menge wird vom Query Optimierer diejenige ausgewählt, welche die beste Leistung verspricht. Die Implementierungen eines Systems sind dabei allerdings platformabhängig. Kommen PIPEs zur Anwendung bedeutet dies jedoch, von der Plattformabhängigkeit losgelöst zu sein. Die Spezifikation mittels PIPEBASE funktioniert hierbei analog zu SQL. Bei der Definition des vom System abzuarbeitenden Codes wird lediglich definiert, welche Gerüste zu verwenden sind. Das System selbst greift dabei auf die den Bedingungen bestangepasstesten Implementierungen der cPIPEs bzw. logischen Datenbankoperatoren zurück. Anders als bei SQL bietet Haskell keine Möglichkeit, als Data Definition Language (DDL) zu fungieren. In PIPEBASE spezifizierter Code beschreibt durch die Anordnung der Operatoren implizit die Abarbeitungsreihenfolge, bei SQL ist einer strikten Syntax Folge zu leisten. Beide so erstellte Pläne werden im weiteren Verlauf transformiert und optimiert, sodass am Ende ein lauffähiges C/C++ Programm steht, welches das tatsächliche Clustering der Daten vornimmt. Gemeinsam haben beide, dass die Art und Weise des Clusterings beschrieben wird. Es besteht ebenfalls die Möglichkeit, beide Codes zu optimieren. Seitens PIPEBASE bestehende Möglichkeiten zur Optimierung wurden rudimentär getestet und können noch weiterführend untersucht werden. 49 benutzt Logische Datenbankoperatoren Haskell benutzt SQL beschriebener Algorithmus als Ablaufplan erstellter Query-Execution-Plan wird ersetzt durch Query Optimierer wird ersetzt durch Source to Source Compiler erstellt Physische Datenbankoperatoren benutzt cPIPE benutzt transformiert zu C/C++ Programm Ausführung Map/Reduce OpenCL Framework benutzt C/C++ Programm benutzt Abbildung 6.1: Vergleich des PIPE Modells mit dem klassischen DBMS Anstaz Kapitel 6 Zusammenfassung und Ausblick 50 PIPE Für jede der drei zu Beginn von Abschnitt 2.2 beschriebenen Herausforderungen konnte eine funktionsfähige Lösung gefunden werden. Die Beschreibung von Algorithmen mittels PIPEs, unter Verwendung von PIPEBASE, der Source-to-Source Compiler als Übersetzungsglied sowie die ansatzweise Optimierung, können alle zuvor angsprochenen Probleme lösen. Für die Weiterentwicklung des PIPE Konzeptes erscheint die Erstellung einer DSL als sinnvoll. Dadurch könnte eine effektivere Formulierung und Übersetzung der Nutzerfunktionen ermöglicht werden, als es mit der Spezifikation in Haskell möglich ist. Für die Verbesserung des Sourceto-Source Compilers gibt es mehrere Möglichkeiten. Die Übersetzung der Eingabesprache basiert in der aktuellen Form auf festen Regeln. Um in zukünftigen Versionen eine dynamsiche Unterstützung von mehreren Ausführungsumgebungen zu ermöglichen, wäre die Einführung von generischen Regeln über ein zusätzliches Modul denkbar. Dadurch könnte für jede neue Ausführungsumgebung ein neuer Regelsatz hinzugefügt werden, der die interne Darstellung des Compilers in die Zielsprache überführt. Diese Regeln könnten dadurch auch nutzerseitig spezifizierbar sein, sodass ein Anwender den Source-to-Source Compiler nach Belieben erweitern kann. Dadurch würde ein weiterer Freiheitsgrad gewonnen werden, der den plattformunabhängigen, dynamischen Charakter des PIPE Konzeptes unterstützt. 51 Kapitel 6 Zusammenfassung und Ausblick 52 ABKÜRZUNGSVERZEICHNIS PIPEBASE PIPE Based Algorithm SpEcification DBSCAN Density-Based Spatial Clustering of Applications with Noise OpenMP Open Multi-Processing MinGW Minimalist Gnu for Windows NUMA Non-Uniform Memory Access DBMS Datenbankmanagementsystem SIMD Single Instruction, Multiple Data FPGA Field Programmable Gate Array PACT Parallelization Contract REST Representational State Transfer cPIPE Clustering Platform Independent Parallel PattErn PIPE Platform Independent Parallel PattErn MPI Message-Passing Interface CPU Central Processing Unit DDL Data Definition Language DSL Domain-specific Language GCC Gnu Compiler Collection GPL General-purpose Language GPU Graphics Processing Unit HDD Hard Disk Drive SSD Solid State Disk 53 Kapitel 6 Zusammenfassung und Ausblick JIT Just-in-Time moc Meta-Object Compiler 54 ABBILDUNGSVERZEICHNIS 1.1 Beispielcluster unterschiedlicher Form und Größe . . . . . . . . . . . . . . . . . . . 9 1.2 Schematischer Aufbau des Spezifikations- und Ausführungsvorgangs . . . . . . . 11 2.1 MapReduce Schema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.2 PACT Operatoren, entnommen aus [Ale11] . . . . . . . . . . . . . . . . . . . . . . . 15 2.3 PACT Schema, übersetzt aus [Ale11] . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.4 PIPE Beschreibungsschema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.5 PIPE Verbindungsschema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.6 cross, row und col cPIPE Schemata . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.7 Schrittweise Veränderung der Matrizen durch eine k-Means Variante . . . . . . . . 22 2.8 Transformation von PIPEBASE in den reinen Haskell Ansatz . . . . . . . . . . . . . 23 3.1 PIPEBASE Schema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 3.2 MapReduce Prozess, entnommen aus [Dea08] . . . . . . . . . . . . . . . . . . . . . 30 3.3 Illustrierter Ablauf eines OpenCL gestützten Programmes . . . . . . . . . . . . . . 31 3.4 Schrittweise Generierung des Funktionsaufrufs für die Berechnung der euklidischen Distanz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.5 Parametrierung eines MapReduce Skeletons mit dem Code aus Beispiel 2.1 . . . . 34 3.6 Schematischer Ablauf des Source-to-Source Compilers . . . . . . . . . . . . . . . . 35 55 Abbildungsverzeichnis 56 3.7 Reduzierung von cPIPEs durch logische Optimierung . . . . . . . . . . . . . . . . . 36 4.1 Schematische Darstellung des verwendeten MapReduce Frameworks . . . . . . . . 40 4.2 Totzeiten bei synchronisierter Verarbeitung . . . . . . . . . . . . . . . . . . . . . . . 40 4.3 Ablauf innerhalb der OpenCL-Umgebung . . . . . . . . . . . . . . . . . . . . . . . . 42 5.1 Kompilierungszeiten in Abhängigkeit der zu übersetzenden cPIPEs . . . . . . . . . 45 5.2 Ausführungszeiten bei unterschiedlicher cPIPE Anzahl . . . . . . . . . . . . . . . . 46 5.3 Laufzeit von k-Means ins .NET, MapReduce und OpenCL . . . . . . . . . . . . . . 47 6.1 Vergleich des PIPE Modells mit dem klassischen DBMS Anstaz . . . . . . . . . . . 50 Literaturverzeichnis LITERATURVERZEICHNIS [Ale11] Alexandrov, A.; Ewen, S.; Heimel, M.; Hueske, F.; Kao, O.; Markl, V.; Nijkamp, E.; Warneke, D.: MapReduce and PACT - Comparing Data Parallel Programming Models, in Datenbanksysteme für Business, Technologie und Web (BTW), 14. Fachtagung des GI-Fachbereichs Datenbanken und Informationssysteme (DBIS), 2.-4.3.2011 in Kaiserslautern, Germany, 2011, S. 25–44. [Dag98] Dagum, L.; Menon, R.: OpenMP: an industry standard API for shared-memory programming, Computational Science Engineering, IEEE, Bd. 5, Nr. 1, Jan 1998, S. 46–55. [Das11] Dastgeer, U.; Enmyren, J.; Kessler, C. W.: Auto-tuning SkePU: A Multi-backend Skeleton Programming Framework for multi-GPU Systems, in Proceedings of the 4th International Workshop on Multicore Software Engineering, IWMSE ’11, ACM, New York, NY, USA, 2011, S. 25–32. [Das14] Dastgeer, U.; Kessler, C.: SkePU Frequently Asked Questions, http://www.ida.liu.se/labs/pelab/skepu/faq.html [Online, Zugriff 02.01.2015]. 2014, [Dea08] Dean, J.; Ghemawat, S.: MapReduce: Simplified Data Processing on Large Clusters, Commun. ACM, Bd. 51, Nr. 1, Jan. 2008, S. 107–113. [Dig14] Digia Plc: Qt Project, 2014, http://qt-project.org/ [Online, Zugriff 17.01.2015]. [Dit12] Dittrich, J.; Quiané-Ruiz, J.-A.: Efficient Big Data Processing in Hadoop MapReduce, Proc. VLDB Endow., Bd. 5, Nr. 12, Aug. 2012, S. 2014–2015. [Est96] Ester, M.; Kriegel, H.-P.; Sander, J.; Xu, X.: A density-based algorithm for discovering clusters in large spatial databases with noise., in Kdd, Bd. 96, 1996, S. 226–231. [Gro13] Grossman, M.; Breternitz, M.; Sarkar, V.: HadoopCL: MapReduce on Distributed Heterogeneous Platforms Through Seamless Integration of Hadoop and OpenCL, in Proceedings of the 2013 IEEE 27th International Symposium on Parallel and Distributed Processing Workshops and PhD Forum, IPDPSW ’13, 2013, S. 1918–1927. [Hah14] Hahmann, M.; Habich, D.; Lehner, W.: Modular Data Clustering - Algorithm Design beyond MapReduce, Technische Universität Dresden, 2014. 57 Literaturverzeichnis [Kal13] Kalavri, V.; Vlassov, V.: MapReduce: Limitations, Optimizations and Open Issues, in Trust, Security and Privacy in Computing and Communications (TrustCom), 2013 12th IEEE International Conference on, July 2013, S. 1031–1038. [Khr14] Khronos Group: OpenCL, 2014, https://www.khronos.org/opencl/ [Online, Zugriff 27.12.2014]. [Lee04] Lee, S.-I.; Johnson, T.; Eigenmann, R.: Cetus – An Extensible Compiler Infrastructure for Source-to-Source Transformation, in Rauchwerger, L. (Hrsg.): Languages and Compilers for Parallel Computing, Bd. 2958 von Lecture Notes in Computer Science, Springer Berlin Heidelberg, 2004, S. 539–553. [Lov77] Loveman, D. B.: Program Improvement by Source-to-Source Transformation, J. ACM, Bd. 24, Nr. 1, Jan. 1977, S. 121–145. [Ora04] Oracle: Javadoc Tool, 2004, http://www.oracle.com/technetwork/articles/java/indexjsp-135444.html [Online, Zugriff 06.01.2014]. [Ott09] Ottchen, C.: The Future of Information Modelling and the End of Theory: Less is Limited, More is Different, Architectural Design, Bd. 79, Nr. 2, 2009, S. 22–27. [Pac98] Pacheco, P. S.: A User’s Guide to MPI, University of San Francisco, San Francisco, CA 94117, 1998. [Par11] Parr, T.; Fisher, K.: LL(*): The Foundation of the ANTLR Parser Generator, SIGPLAN Not., Bd. 46, Nr. 6, Juni 2011, S. 425–436. [Ros11] Rosenberg, O.; Gaster, B. R.; Zheng, B.; Lipo, I.: OpenCL Static C++ Kernel Language Extension, 2011, http://amd-dev.wpengine.netdnacdn.com/wordpress/media/2012/10/CPP_kernel_language.pdf [Online, Zugriff 01.01.2015]. [Sch14] Schreibmann, V.: Design and Implementation of a Model-Driven Approach for Restful APIs, IEEE Germany Student Conference, 2014. [Sto10] Stone, J. E.; Gohara, D.; Shi, G.: OpenCL: A Parallel Programming Standard for Heterogeneous Computing Systems, Computing in Science & Engineering, Bd. 12, Mai 2010, S. 66–72. [Tra13] Travet, N.; Khronos Group: OpenCL Introduction, 2013, https://www.khronos.org/assets/uploads/developers/library/overview/opencl_overview.pdf [Online, Zugriff 31.12.2014]. [Yan11] Yang, C.-T.; Huang, C.-L.; Lin, C.-F.: Hybrid CUDA, OpenMP, and {MPI} parallel programming on multicore {GPU} clusters, Computer Physics Communications, Bd. 182, Nr. 1, 2011, S. 266 – 269, Computer Physics Communications Special Edition for Conference on Computational Physics Kaohsiung, Taiwan, Dec 15-19, 2009. 58