RST Labor - Weblearn
Transcription
RST Labor - Weblearn
General Purpose Computation on Graphics Processing Unit RST Labor Ausarbeitung zum Vortrag vom 18.12.2007 Thema: General Purpose Computation on Graphics Processing Unit Mitarbeiter: Sebastian Behnen Matthias Looschen Christoph Menke Betreuer: Prof. Dr. Th. Risse Abgabedatum: 12.04.2008 Überarbeitete Version Seite: 1/64 General Purpose Computation on Graphics Processing Unit Inhaltsverzeichnis 1 Die GPU für grafische Zwecke...............................................................5 1.1 Schritt 1: Transformation...............................................................6 1.2 Schritt 2: Lighting.........................................................................6 1.3 Schritt 3: Viewpoint......................................................................6 1.4 Schritt 4: Clipping.........................................................................6 1.5 Schritt 5: Triangle Setup................................................................7 1.6 Schritt 6: Rasterization..................................................................7 1.7 Schritt 7: Culling..........................................................................7 1.8 Schritt 8: Texture & Shading...........................................................7 2 Prozessoren.......................................................................................8 2.1 Der Vertex-Prozessor.....................................................................8 2.2 Der Fragment-Prozessor................................................................9 2.3 Im nicht-grafischen Einsatz...........................................................11 3 Parallelität........................................................................................12 3.1 SIMD.........................................................................................12 3.2 MISD.........................................................................................13 3.3 MIMD........................................................................................13 4 Streams..........................................................................................14 5 Leistung..........................................................................................16 6 Kombination CPU und GPU auf einen Chip.............................................18 7 Der Kernel.......................................................................................19 8 Streams..........................................................................................21 9 Stream Programmierung....................................................................22 9.1 Kommunikation...........................................................................23 9.2 Berechnung................................................................................24 10 Frameworks...................................................................................25 10.1 CUDA......................................................................................25 10.1.1 Funktionsweise...................................................................25 10.1.2 Vorteile..............................................................................26 10.1.3 Einschränkungen.................................................................26 10.1.4 Beispiel: Bitonic Sort............................................................26 10.2 CTM........................................................................................27 10.2.1 Der Befehlsprozessor............................................................28 10.2.2 Paralleles Datenarray............................................................28 10.2.3 Speichercontroller................................................................28 10.3 Einschränkungen........................................................................28 11 Praktischer Teil................................................................................29 11.1 NVIDIAs C for Graphics (CG).......................................................29 11.2 OpenGL Utility Toolkit (GLUT)......................................................29 11.3 RapidMind................................................................................29 11.4 Beispiel Matrix-Reduktion...........................................................30 11.4.1 Umsetzung der Matrix-Reduktion(CPU)...................................31 11.4.2 Umsetzung der Matrix-Reduktion (GPU)..................................31 11.4.3 Ping-Pong...........................................................................32 Seite: 2/64 General Purpose Computation on Graphics Processing Unit 11.4.4 Messungen Matrix-Reduktion.................................................34 11.5 Beispiel Vektor Multiplikation.......................................................35 11.5.1 Umsetzung Multiplikation CPU...............................................35 11.5.2 Umsetzung Multiplikation GPU...............................................36 11.5.3 Messungen Multiplikation......................................................37 11.6 Beispiel Mandelbrot-Menge.........................................................39 11.6.1 Fast Floating Fractal Fun (FFFF)..............................................40 11.7 Umsetzung Mandelbrot CPU in FFFF.............................................41 11.7.1 Umsetzung Mandelbrot GPU in FFFF.......................................41 11.7.2 Darstellung der Mandelbrot-Menge mit FFFF............................44 11.7.3 Benchmark mit FFFF............................................................47 12 Fazit..............................................................................................49 13 Quellen..........................................................................................50 14 Anhang..........................................................................................53 14.1 Bitonic Hauptprogramm (bitonic.cu):............................................53 14.2 Bitonic Kernel (bitonic_kernel.cu)................................................54 14.3 Matrixreduktion.........................................................................55 14.4 Multiplikation mit RapidMind.......................................................63 Seite: 3/64 General Purpose Computation on Graphics Processing Unit Abbildungsverzeichnis Abbildung 1: Pipeline Stufen der Grafikpipeline...........................................5 Abbildung 2: transformierte Vertex-Daten [3].............................................6 Abbildung 3: Vertex-Rohdaten.................................................................6 Abbildung 4: Triangle Setup.....................................................................7 Abbildung 5: Rasterization.......................................................................7 Abbildung 6: Culling...............................................................................7 Abbildung 7: Vertex-Prozessor..................................................................8 Abbildung 8: Fragment Prozessor..............................................................9 Abbildung 9: nicht grafischer Einsatz.......................................................11 Abbildung 10: SIMD..............................................................................12 Abbildung 11: MISD..............................................................................13 Abbildung 12: MIMD.............................................................................13 Abbildung 13: Streams..........................................................................14 Abbildung 14: Leistung einer GeForce 8800 (Nutzung von CUDA) vs. 2.66 GHz Conroe ...............................................................................................16 Abbildung 15: GFLOPS Vergleich.............................................................17 Abbildung 16: AMD Fusion.....................................................................18 Abbildung 17: Beispiel Berechnung gesamtes Kontenkapital........................22 Abbildung 18: Grafikpipeline einer GPU....................................................23 Abbildung 19: Virtuelle Maschine CTM [26]...............................................27 Abbildung 20: Matrix-Reduktion..............................................................30 Abbildung 21: Messergebnisse Multiplikation.............................................38 Abbildung 22: Mandelbrot-Menge [32].....................................................39 Abbildung 23: Die Mandelbrot-Menge im Urzustand...................................44 Abbildung 24: Spalte zwischen Kopf und Körper........................................44 Abbildung 25: Zoom auf ein Seepferdchen................................................44 Abbildung 26: Tieferer Zoom auf ein Seepferdchen....................................44 Abbildung 27: Darstellung mit 40 Iterationen............................................45 Abbildung 28: Darstellung mit 50 Iterationen............................................45 Abbildung 29: Darstellung mit 70 Iterationen............................................45 Abbildung 30: Darstellung mit 256 Iterationen..........................................45 Abbildung 31: Berechnung mit der CPU....................................................46 Abbildung 32: Berechnung mit der GPU...................................................46 Abbildung 33: Tieferer Zoom; CPU-Berechnung.........................................46 Abbildung 34: Identisches Bild; GPU-Berechnung......................................46 Seite: 4/64 General Purpose Computation on Graphics Processing Unit 1 Die GPU für grafische Zwecke Die GPU kann als Pipeline verstanden werden, durch die die Daten „hindurchfließen“. In dieser Pipeline werden verschiedene komplexe grafische Berechnungen durchgeführt. Ziel ist es 3D-Rohdaten, die im VRAM bereit liegen, in ein zweidimensionales Bild zu konvertieren, um dieses auf dem Monitor anzeigen zu können. Im Folgenden wird in acht Schritten ein Weg durch diese Pipeline erläutert [1]. Dieser Weg ist nicht der einzig mögliche und gilt für Grafikberechnungen. Abbildung 1: Pipeline Stufen der Grafikpipeline Seite: 5/64 General Purpose Computation on Graphics Processing Unit 1.1 Schritt 1: Transformation Die Rohdaten liegen als Vertex-Daten vor. Ein Vertex ist ein Punkt im dreidimensionalen Raum der, mit weiteren Vertices verbunden, Polygone bildet. Während der Transformation werden alle Punkte im zukünftig anzuzeigenden 2D-Raum angeordnet, skaliert und relativ zueinander korrekt positioniert. An dieser Stelle kann ein Vertex-Shader(siehe Kapitel 7) genutzt werden um beispielsweise Objekte zu verzerren. Die Transformation überführt also 3DDaten in 2D-Daten. Abbildung 3: VertexRohdaten 1.2 Abbildung 2: transformierte VertexDaten [3] Schritt 2: Lighting Alle Objekte werden korrekt beleuchtet. Hierbei werden die Position der Lichtquellen und ggf. reflektierende Oberflächen berücksichtigt. Die Daten liegen immer noch in Vertices vor und sind somit nicht mehr als eine Menge an Punkten mit verschieden Farbabstufungen. 1.3 Schritt 3: Viewpoint Die Objekte werden dann in Bezug auf den Betrachtungswinkel ausgerichtet. Objekte, die näher am „Beobachter“ liegen, werden demnach größer dargestellt als jene, die weiter entfernt sind. 1.4 Schritt 4: Clipping Objekte, die nicht sichtbar wären, weil sie außerhalb des Darstellungsbereichs liegen (z.B. hinter dem Beobachter), werden entfernt. Dies erspart der GPU wertvolle Berechnungszeit. Seite: 6/64 General Purpose Computation on Graphics Processing Unit 1.5 Schritt 5: Triangle Setup An diesem Punkt werden alle Vertices zusammengesetzt, um Dreiecke (Triangle) oder Vielecke (Polygon) zu erzeugen. Abbildung 4: Triangle Setup 1.6 Schritt 6: Rasterization Während der Rastarisierung werden die Flächen, die durch die zusammengeführten Vertex-Daten beschrieben werden, mit Pixeln gefüllt. Ob ein Pixel gefüllt wird, hängt davon ab, ob der Mittelpunkt des Pixels innerhalb der beschriebenen Fläche liegt. [3][8] Abbildung 5: Rasterization 1.7 Schritt 7: Culling Das Culling setzt da an, wo das Clipping(siehe Kapitel 1.4) aufhört. In diesem Schritt werden alle Objekte entfernt, die nicht sichtbar sind, weil sie von anderen Objekten verdeckt werden. Dies kann die, vom Beobachter abgewandte Rückseite eines Objektes sein aber auch ein durch andere Objekte verdecktes Objekt. Abbildung 6: Culling 1.8 Schritt 8: Texture & Shading An diesem Punkt sind alle Pixel in der 2D-Fläche verblieben, die das spätere Bild ausmachen. Ein Pixel Shader kann an dieser Stelle dafür genutzt werden, um den einzelnen Pixeln Effekte (Spiegelungen, Transparenz, ...) zu geben. Die erstellten 2D-Flächen werden nun in den Frame-Buffer gelegt, um dann angezeigt zu werden. Seite: 7/64 General Purpose Computation on Graphics Processing Unit 2 Prozessoren Im Jahr 2000 wurden erste 3D-Grafikkarten mit zwei frei programmierbaren Prozessortypen veröffentlicht[35], die beide auf die Verarbeitung von Vektoren mit vier Komponenten spezialisiert sind. Dies sind zum Einen die VertexProzessoren und zum Anderen die Fragment-Prozessoren. 2.1 Der Vertex-Prozessor Der Vertex-Prozessor hat zwei Berechnungseinheiten. Dies sind die Skalar-Einheit und die Vektor-Einheit. Der VertexProzessor kann also pro Takt zwei Berechnungen durchführen. Dies sind eine MAD (multiply & add) auf einen Vektor mit drei Elementen (Vector3) und eine Funktion auf einen Skalar. Diese Funktion kann ein Kosinus, Sinus oder ähnliches sein(siehe Tabelle auf S.11). Der VertexProzessor erlaubt es, auf jede ankommende Komponente Berechnungen mit fp32 (32 Bit Abbildung 7: Vertex-Prozessor floating point) Genauigkeit durchzuführen. Möglich sind Zugriffe auf den Texture-Cache, welcher mit dem Fragment-Prozessor geteilt wird. Beim schreibenden Zugriff, Scatter genannt, hat der Vertex-Prozessor wahlfreien Zugriff, kann die Speicheradresse also selbst bestimmen. Liest der Vertex-Prozessor jedoch aus dem Texture-Buffer, Gather genannt, so hat er nur dann wahlfreien Zugriff, wenn die Grafikkarte bereits den Vertex-Shader 3.0 implementiert.[5][21] Seite: 8/64 General Purpose Computation on Graphics Processing Unit 2.2 Der Fragment-Prozessor Auf aktuellen Grafikkarten (z.B. GeForce 6er Reihe) sind pro Pixel-Pipeline zwei fp32 (32 Bit floating point) Shadereinheiten vorhanden. Der im Bild zu sehende Texture-Prozessor ist lediglich zum Laden der Texturen da und bietet dem „General Purpose“ - Zweck keinen Vorteil. Der BranchProzessor ist in der Lage, den Befehlszeiger zu ändern und implementiert so Schleifen und Verzweigungen. Die Shader Unit 1 ist nicht in der Lage, eine ADD-Operation auszuführen, bietet aber ein MUL. Das einfache ADD, sowie ein Abbildung 8: Fragment Prozessor weiteres MUL werden von Shader Unit 2 bereitgestellt. Die schon beim Vertex-Prozessor erwähnten Funktionen wie cos und sin werden zwischen beiden Shader Units aufgeteilt, da die Shader Unit 1 nicht alle Funktionen berechnen kann. Beide Shadereinheiten ergänzen sich also zu einer Allzweckeinheit. Seite: 9/64 General Purpose Computation on Graphics Processing Unit Name genutzte Unit Takte Operation RCP 1 1 1/x RSQ 1 2 1/sqrt(x) In diesem Schritt kann „kostenlos“ ein weiteres RCP ausgeführt werden, sodass sqrt(x) ebenfalls nur 2 Takte benötigt. LOG 2 1 log2(x) EXP 2 1 2x SINCOS 2 2 (3) Sin() und Cos() für eine bzw. zwei Komponenten des Vier-KomponentenVektors Die Division (DIV) wird nach Pixel Shader 2.0 noch nicht unterstützt und muss durch Kombination von RCP und MUL „per Hand“ hergestellt werden. 5/3 würde also in Form von 1/3 * 5 berechnet werden. [2][5][21] Es ist bekannt, dass NVIDIA für die in der Tabelle genannten „Spezialunktionen“ dedizierte Hardware verbaut. Ob und welche weiteren Spezialfunktionen ebenfalls „in Hardware gegossen“ sind, ist nicht bekannt. Seite: 10/64 General Purpose Computation on Graphics Processing Unit 2.3 Im nicht-grafischen Einsatz Wenn eine GPU für nicht-grafische Aufgaben, also im „General purpose“Sinn eingesetzt wird, kann sie als Aneinanderreihung zweier programmierbarer Blöcke (Abb. 12 Programmable MIMD und Programmable SIMD) gesehen werden, die seriell arbeiten. Dies sind der Vertex- und der Fragment-Prozessor. Beide nutzen die Texture-Unit, um auf Daten zugreifen zu können (random access).[5] Abbildung 9: nicht grafischer Einsatz Der Vertex-Prozessor gibt seine Daten, nachdem er mit ihnen gearbeitet hat, entweder direkt an den FragmentProzessor weiter, oder die Daten werden durch den Rasterizer geleitet, um sie in interpolierte Werte aufzufächern. Culling und andere Tests, die die Komplexität der Berechnungen für den Garfik-Einsatz mindern sollen, durchzuführen macht wenig Sinn, da im „General Purpose“-Einsatz Daten in die GPU gegeben werden, um Berechnungen durchzuführen und nicht um Bilder zu erstellen, auf denen Objekte durch andere verdeckt werden können. Nachdem der Fragment-Prozessor seine Berechnungen durchgeführt hat, können die Daten an eine, vom Vertex-Prozessor bestimmte Adresse im Speicher geschrieben werden (predicated write). Die dort abgelegten Daten können nun, wenn es sich nicht um fertige Ergebnisse handelt, wieder als Eingabestrom für den Fragment-Prozessor dienen. Seite: 11/64 General Purpose Computation on Graphics Processing Unit 3 Parallelität Im Folgenden werden einige Konzepte zur Parallelisierung aufgegriffen. Hierbei wird Augenmerk auf die Verfahren gelegt, die beim GPGPU Anwendung finden. Dies sind SIMD im Fragment-Prozessor, MIMD im Vertex-Prozessor und MISD im Rasterizer. Hauptsächlich wird der Fragment-Prozessor genutzt, weshalb hier auf SIMD besonders eingegangen wird. 3.1 SIMD „Single Instruction, Multiple Data“ kommt im Fragment-Prozessor zum Einsatz. Hier führen alle Fragment-Prozessoren gleichzeitig die gleiche Operation auf verschiedene ankommende Daten aus. Eine Applikation, bzw. ein Shader-Programm, kann diese Technik als Vorteil nutzen, wenn die ankommenden Daten voneinander unabhängig sind. Dies ist zum Beispiel der Fall, wenn Farbwerte von Pixeln verändert werden. Eine Menge von Pixeln dient als Eingabewert und ein Shader-Programm muss nicht mehr tun, als den Rot-, Blau, oder GrünWert zu inkrementieren oder zu dekrementieren. Abbildung 10: SIMD Dies hat zwei Vorteile: Zum Einen nimmt der Fragment-Prozessor die Daten in Blöcken an. Es werden also immer mehrere Daten zur Zeit geladen, statt ein Datum nach dem anderen zu laden. Zum Anderen führen die Fragment-Prozessoren die Operation auf allen Daten gleichzeitig aus. Auf aktuellen Grafikkarten finden sich bis zu 128 FragmentProzessoren. Die Nachteile treffen prinzipiell alle drei hier vorgestellten Konzepte. Nicht alle Algorithmen sind für die Vektorisierung geeignet. Auch ist es unter Umständen sehr schwer, die Eingangsdaten so zu erstellen, dass sie für den SIMDProzessor brauchbar sind. Ein Nachteil, der nur SIMD trifft, ist dass eben nur ein Befehl zur Zeit ausgeführt werden kann und nicht mehrere, wie bei anderen Konzepten.[10] Seite: 12/64 General Purpose Computation on Graphics Processing Unit 3.2 MISD „Multiple Instruction, Single Data“ ist eine weitere Art der parallelen Verarbeitung, bei der viele funktionale Einheiten verschiedene Befehle auf ein und den selben Datum ausführen. Ein Einsatz dieser Technik könnte man sich in der Fehlererkennung vorstellen. Eine Berechnung wird von unabhängigen Einheiten parallel auf gleichen Daten ausgeführt, um Fehler zu erkennen. Da MISD im Vergleich zu SIMD und MIMD schlecht skalierbar ist, wird es recht selten verwendet.[12] Abbildung 11: MISD 3.3 MIMD „Multiple Instruction, Multiple Data“ ist eine Art der parallelen Verarbeitung, bei der viele funktionale Einheiten verschiedene Operationen auf unterschiedlichen Daten ausführen. Dies ist möglich, da die Prozessoren vollständig asynchron und unabhängig voneinander arbeiten.[11] Abbildung 12: MIMD Seite: 13/64 General Purpose Computation on Graphics Processing Unit 4 Streams Die GPU ist im Gegensatz zu einer CPU als Stream- und nicht als serieller Prozessor konzipiert. Im Gegensatz zu seriellen Prozessoren kann ein Streamprozessor einzelne skalare Rechenoperationen an einem Datenstrom ausführen. Die Daten liegen also in Form von Streams vor, was im Grunde nichts anderes ist, als ein Array gleicher Datentypen. Auf diesen Stream wird eine Folge von Operationen ausgeführt. Innerhalb der GPU unterscheidet man vier verschiedene Streams, die im Folgenden erklärt werden.[9][22] Dies sind Vertex-Stream, Fragment-Stream, Texture-Stream und Frame-Buffer-Stream. Abbildung 13: Streams Der Vertex-Stream beinhaltet Vertices. Ein Vertex ist ein Eckpunkt eines Polygons oder eines Dreiecks und enthält neben der Position als 3D-Vektor meist noch weitere Daten wie Transparenz oder Farbe. Für den „General Purpose“-Zweck können in den Vertices beliebige Daten gespeichert werden, die so in den Vertex-Prozessor geleitet werden. Der Fragment-Stream versorgt den Fragment-Prozessor mit Daten und ist damit der einzige Stream, der nicht auf einen Puffer (lesend oder schreibend) zugreift. Neben dem FragmentStream bezieht der Fragment-Prozessor seine Daten aber hauptsächlich vom Texture-Stream. Seite: 14/64 General Purpose Computation on Graphics Processing Unit In diesen kann der Fragment-Prozessor schreiben (predicated write) und wahlfrei lesen. Dies ist ein Vorteil des Fragment-Prozessors, da er so seine eigene Ausgabe direkt wieder als Eingabe verwenden kann. Also ist der Fragment Prozessor prädestiniert dafür, Iteration der Form x . k q= f x k (parallel) durchzuführen(z.B. Newton oder Power-Method zur Berechnung von Matrizen Eigenwert oder Eigenvektor). Dieser Vorteil wird zwar ab VertexShader 3.0 (aktuell werden Grafikkarten mit Shader Version 4.0 ausgeliefert) relativiert, da der Vertex-Prozessor ebenfalls aus dem Texture-Stream lesen kann, der Fragment-Prozessor wird aber aufgrund der größeren Anzahl der verbauten Recheneinheiten für GPGPU-Aufgaben dennoch häufiger verwendet. Zudem kann der Fragment-Prozessor direkt in den Texture-Stream schreiben (Adresse durch Vertex-Prozessor vorher bestimmt), der Vertex-Prozessor hingegen muss seine Ausgabedaten erst durch den Rasterizer und den Fragment-Prozessor leiten, um in den Texture-Stream zu schreiben. Dies hat zur Folge, dass der Fragment-Prozessor für Berechnungen gesperrt ist, während er Daten des Vertex-Prozessors weiterleitet. Die CPU hat lesenden und schreibenden Zugriff auf alle Streams innerhalb der GPU mit Ausnahme des Fragment-Streams. Dieser wird ausschließlich innerhalb der GPU verwendet und ist für den Programmierer nicht sichtbar/beeinflussbar. Auch wenn die CPU direkt auf alle Streams zugreifen kann, ist es sinnvoll Daten direkt in den Texture-Buffer zu schreiben, damit diese Daten sofort als Eingabedaten für den Fragment-Prozessor zur Verfügung stehen. Seite: 15/64 General Purpose Computation on Graphics Processing Unit 5 Leistung Grafikkarten sind aufgrund der vielen parallelen Prozessoren sehr leistungsfähig. Den Vorteil der vielen Prozessoren kann eine Grafikkarte natürlich nur dann ausspielen, wenn ein Problem sich parallelisieren lässt. Zusätzlich dazu ist es vorteilhaft, wenn die Berechnungen im Verhältnis zu den Speicherzugriffen überwiegen. Offensichtlich sind gerade im Finanzbereich die Daten unabhängig voneinander und die Komplexität der Berechnungen hoch. Laut Beyond3D.com profitieren Berechnungen im Finanzbereich massiv von der schnellen Berechnung der Spezialfunktionen. Andy Keane, Leiter der GPUCoputig Sparte bei NVIDIA, sieht den größten Nutzen der GPU-Nutzung im Bereich der Risikoanalyse [36]. Der in Abbildung 14 zu sehende Vergleich stammt aus einem Marketingdiagramm, welches NVIDIA auf dem G80 Editors Day vorgestellt hat und bezieht sich auf die Leistung einer GeForce 8800 gegenüber eines Intel Conroe mit 2.66 GHz [4]. Abbildung 14: Leistung einer GeForce 8800 (Nutzung von CUDA) vs. 2.66 GHz Conroe Seite: 16/64 General Purpose Computation on Graphics Processing Unit Im Vergleich in Abbildung 15 zu sehen sind jeweils die Top-Modelle der GeForce Serien FX [15], 6 [16], 7 [17] und 8 [18], sowie die GeForce 6600 [16], die unser Testobjekt war. Als Vergleichswert dient der aktuell teuerste [20] Intel Prozessor „Core 2 Extreme QX6850“. NVIDIA hat bereits die GeForce 9 [19] angekündigt und prophezeit, dass die Karte ein „Terraflop-Beast“ sein wird. Abbildung 15: GFLOPS Vergleich Die angegebene Rechenleistung (GFLOPS [14]) der Grafikkarten über die Streamprozessoren bezieht sich auf die Nutzung beider MUL-Operationen, die bei Grafikshaderberechnungen nicht erreicht wird, da weitere Berechnungen ausgeführt werden müssen. Bei diesen Berechnungen ist die Rechenleistung der Stream-Prozessoren daher geringer. Alle hier angegebenen Werte sind theoretische Maximalwerte und stammen aus Herstellerangaben. Wie viel Leistung effektiv zur Verfügung steht, kann nur durch Benchmarks geklärt werden. Leider sind GPGPU-Benchmarks bisher nicht interessant genug, sodass es keine breite Auswahl und entsprechenden Funktionsumfang wie im CPUBenchmarking gibt. In Punkt 11.5.5 wird beispielhaft ein Benchmark-Ergebnis des Programms FFFF gezeigt. Bei Betrachtung des Leistungsvergleichs in Grafik 15 muss beachtet werden, dass es sich hier um theoretische Werte handelt, die generell (weder von Grafikkarte, noch von Prozessor) erreicht werden. Zudem sind Grafikkarten grade für einen hohen Datendurchsatz optimiert und bieten im Vergleich zu einer CPU radikal wenig Funktionalität. Der große Geschwindigkeitsunterschied darf also nicht zu der Frage führen, warum noch CPUs eingesetzt werden. Eine GPU kann eine CPU nicht ersetzen und ist lediglich in der Lage einen Bruchteil von CPU-Aufgaben zu erledigen. Seite: 17/64 General Purpose Computation on Graphics Processing Unit 6 Kombination CPU und GPU auf einen Chip Eine Kombination von CPU- und GPU-Kernen könnte, laut ATI-Chef Dave Orton, den Preis eines PCs von 400 Dollar auf etwa 200-300 Dollar reduzieren. Zudem könnte in Zukunft damit auch die Leistung gesteigert werden, da die GPU auch allgemeine Aufgaben der CPU übernehmen und aufgrund ihrer besonderen Struktur beschleunigen kann. AMD möchte dies zusammen mit seiner neuen Tochter ATI unter dem "Fusion" Konzept erreichen. Ein erster solcher Kombi-Prozessor mit einem x86-/x64-Kern und einer GPU, die beide den Arbeitsspeicher gemeinsam nutzen, Abbildung 16: AMD Fusion könnte bereits dieses Jahr erscheinen. Klar ist aber auch, dass die Leistungsfähigkeit des PC-Hauptspeichers vor allem das Performance-Potenzial der GPU limitieren wird: Während es die neuesten diskreten Grafikchips mit 384-Bit-Interface und GDDR3-RAM auf Datentransferraten von über 86 GByte/s bringen, ist von zwei DDR3Speicherkanälen (DDR3-1333/PC3-10600) gerade mal ein Viertel davon zu erwarten. Für Highend-Grafik will AMD deshalb auch weiterhin diskrete GPUs, d.h. wie gehabt eigenständige Grafikkarten, entwickeln. Auch PC-Chipsätze mit integrierter Grafik wird es weiterhin geben. So soll das Fusion Konzept nicht die herkömmliche Grafikkarte ersetzen, sondern die GPU und ihre Vorteile, unter anderem beim Energieverbrauch und bei parallelen Aufgaben, abseits des gewohnten Grafiksektors nutzen . Die CPU-GPU-Fusion zielt sicherlich zunächst eher auf die kleineren Geräte ab. Intel und AMD haben bereits beide im Verlaufe des Jahres ihre StromsparArchitektursparten (Alchemy/MIPS beziehungsweise XScale/ARM) abgestoßen und AMD baut auf den geringeren Stromverbrauch der GPU Prozessoren im mobilen Markt. So soll die Technik so sparsam gemacht werden, dass sie auch für Handys und PDAs einsetzbar ist.[27][40] Seite: 18/64 General Purpose Computation on Graphics Processing Unit 7 Der Kernel Viele Jahre lang waren die Kernel auf der Grafik-Hardware fest als Funktionseinheit implementiert und boten keine Möglichkeit, sich von außen programmieren zu lassen. Seit 2000 erlauben die GPUS jedoch den Entwicklern, individuelle Kernel für die Grafik Pipeline zu programmieren. Heutzutage bieten die GPUs hochperformante und parallel arbeitende Prozessoren, welche zwei Kernels zur Verfügung stellen: ● ● Ein Vertex Kernel, welcher das Programm auf jedem der Verticies ausführt, das die Pipeline passiert Ein Fragment Kernel, welcher es dem Benutzer erlaubt, ein Programm auf jedem Fragment auszuführen. Ein Kernel arbeitet auf kompletten Datenströmen. Er bekommt einen oder mehrere Eingangsströme und kann auch einen oder mehrere Ausgabeströme haben. Der typische Einsatz eines Kernels ist das Ausführen einer Funktion auf jedes Element eines Datenstroms. Ein Kerlen kann den Datenstrom auf verschiedene Arten manipulieren: ● Erweiterungen des Datenstroms Das bedeutet, dass für jedes Eingabedatum mehr als ein Ausgabedatum generiert wird ● Reduzierungen Das bedeutet, dass mehr als ein Eingabedatum zu einem einzigen Ausgabedatum zusammengefasst wird ● Filter Das bedeutet, dass eine Untermenge der Eingabeelemente in einen Ausgabestrom zusammengefasst werden Seite: 19/64 General Purpose Computation on Graphics Processing Unit Die Kernel Ausgaben hängen nur von den Eingaben ab und innerhalb der Kernel Ausführung darf ein Element niemals von einem anderem Element abhängen. Diese Einschränkungen haben zwei große Vorteile. ● Dass die Daten, die für die Ausführung des Kernels benutzt werden vollkommen bekannt sind, wenn der Kernel geschrieben (oder kompiliert) wird. Die resultiert daraus, dass die Daten unabhängig voneinander sind und sich so während der Ausführungszeit nicht aufgrund von fremden Daten ändern können. In einem Programm, in dem diese Voraussetzung nicht gegeben ist, kann sich ein Datum während der Ausführung zum Beispiel durch Multiplikation mit einem anderen beliebigen Datum unvorhergesehen ändern. ● Durch die Unabhängigkeit der einzelnen Daten in einem Kernel wird die Datenparallelität erst ermöglicht, da sich die Daten nicht gegenseitig manipulieren können und kein Datum auf die Berechnung eines anderen Datums warten muss. Auf das Prinzip der Datenparallelität wird in Kapitel 9 auch Anhand von Beispielen eingehender eingegangen.[23] Seite: 20/64 General Purpose Computation on Graphics Processing Unit 8 Streams Ein Stream ist eine Sammlung von Daten des selben Typs, auf welche der Kernel (siehe Kapitel 7) angewendet werden kann. In der GPGPU Programmierung werden ausschließlich die Vertex- und Fragment-Daten als Ein- und Ausgabe Stream benutzt. Erlaubten Operationen auf einem Stream sind: ● kopieren eines Streams ● aufteilen eines Streams in mehrere Substreams ● indizieren eines Streams mit Hilfe eines Index-Streams ● durchführen von Berechnungen auf einen Stream mit Hilfe von Kerneln Ein Stream ist am besten vergleichbar mit einem Array bei der gewöhnlichen Programmierung, der die genannten Anforderungen erfüllen muss. [23] Seite: 21/64 General Purpose Computation on Graphics Processing Unit 9 Stream Programmierung Graphic Processing Units ermöglichen z.B. Eingaben nicht nur als Streams sondern können auch zusätzlich aus Texturen lesen. Durch die recht starken Einschränkungen bezüglich der zulässigen Berechnungen (Kernel) und Datenstrukturen (Streams) ist es schwer, zur Compilezeit nicht bekannte und organisierbare Berechnungen mit Hilfe des Stream Processing Paradigmas auszudrücken. Berechnungen, die nicht an den Einschränkungen scheitern, bieten allerdings ein hohes Optimierungspotential. Als konkrektes Beispiel sei die Änderung eines Zinssatzes bei einer Bank genannt. In diesem Fall müssten auf alle Konten Berechnungen durchgeführt werden um den neuen Zinsbetrag, also den konkreten Geldbetrag der sich aus der Höhe des Kapitals und der Verzinsung ergibt, zu berechnen. Hier wäre das Stream Processing Paradigma besonders effektiv, da die Geldbeträge der Konten nicht voneinander abhängen und auf jedes Konto parallel die gleiche Berechnung durchgeführt werden könnte. Abbildung 17: Beispiel Berechnung gesamtes Kontenkapital Wäre nun ein anderes mathematisches Vorgehen vorgesehen, wie zum Beispiel die Bestimmung des gesamten Kontenkapitals der Bank, wäre das Stream Processing Paradigma nur noch eingeschränkt nützlich. Da hier jeweils der Betrag von Konten zusammenrechnet werden muss, hängen die Daten voneinander ab. Wie man in Abbildung 17 sehr gut sehen kann, müssen hier, auch wenn man Arbeiten parallel durchführen kann, mehrere Male auf Daten gewartet werden. Zum Beispiel muss, um die Berechnung von Ergebnis 5 durchzuführen, auf die Ergebnisse der Berechnungen von Ergebnis 1 und Ergebnis 2 gewartet werden. Dieses einfache Beispiel zeigt bereits, dass die Stärken des Stream Processing Paradigmas bei nicht vorhandener oder eingeschränkter Datenunabhängikeit nicht ausgespielt werden können. Seite: 22/64 General Purpose Computation on Graphics Processing Unit 9.1 Kommunikation Bei der Kommunikation wird grundsätzlich zwischen zwei Arten unterschieden: ● ● Die off-chip-Kommunikation bezeichnet die Kommunikation zwischen dem Prozessor und einem externen Partner(z.B. RAM-Speicher). Die on-chip-Kommunikation dagegen ist die Kommunikation innerhalb eines Prozessors. Ein großer Vorteil der Stream Programmierung ist, dass in der Regel viele Daten auf einen Kernel angewandt werden und die Kosten der Übertragung dieser Datenmenge im Vergleich zur Initialisierung der Speicherbewegung zwischen dem externem Speicher und der GPU geringer sind. Da die Zugriffszeit effektiv nur bei der Initialisierung der Speicherbewegung ins Gewicht fällt, ist die Bandbreite bei der Übertragung eines Streams am bedeutendsten. Bei Anwendungen, die nicht nicht nach dem Stream Processing Paradigma arbeiten, ist es häufig so, dass berechnete Daten nicht direkt im Anschluss weiter verwendet werden. Das bedeutet, dass diese Daten für spätere Berechnungen zumindest in den Cache geschrieben werden müssen. Bei der Stream Programmierung ist es hingegen so, dass man in der Regel den Ausgabestrom eines Kernels direkt als Eingabestrom für den nächsten Kernel nutzt. So ist es nicht nötig, Daten in den Cache zu schreiben und man kann die volle Bandbreite zwischen den einzelnen Kerneln nutzen. In Abbildung 18 sieht man sehr gut das Prinzip des Stream Processing Paradigmas, da hier die Ausgabeströme direkt wieder als Eingabeströme genutzt werden. Dies ist zwar, was man am Vorhandensein der Rasterization Einheit sehen kann, nicht GPGPU, das Prinzip ist aber das gleiche. Abbildung 18: Grafikpipeline einer GPU Ein weiterer Vorteil ist das Unterbrechen und Zwischenspeichern von Elementen. Dies kann unter der Voraussetzung geschehen, dass Cacheverfahren eingesetzt werden. Wenn dies der Fall ist, können Berechnungen angehalten werden, wenn noch nicht alle benötigten Daten vorhanden sind. Bis die benötigten Daten dann zur Verfügung stehen, können andere Elemente berechnet werden. Seite: 23/64 General Purpose Computation on Graphics Processing Unit 9.2 Berechnung Auch im Bereich der Berechnungen bietet die Stream Programmierung einige Vorteile. Diese ergeben sich durch die starken Einschränkungen, die auf die Form und die Operationen der Daten und Streams angewandt werden. Eine Einschränkung des Kernels ist, dass Berechnungen, welche auf ein Element des Streams durchgeführt werden, von anderen Elementberechnungen im Stream unabhängig sein müssen. Diese Beschränkung hat zur Folge, dass in der Theorie jedes einzelne Element eines Streams gleichzeitig bearbeitet werden könnte. In der Praxis wird die Anzahl der gleichzeitig ausführbaren Berechnungen durch die Anzahl der Recheneinheiten beschränkt. Somit ergibt sich eine lineare Beschleunigung mit einem Ansteigen der Anzahl der Recheneinheiten. Dadurch, dass Daten per Definition unabhängig sein müssen, ist es möglich mehrere Kernel parallel auszuführen. Da Ausgaben eines Kernels in der Regel wieder als Eingaben genutzt werden, kann der zweite Kernel sofort mit der Berechnung beginnen, sobald die ersten Daten vom erstem Kernel geliefert werden. Diese Taskparallelität (paralleles Ausführen der Kernel) ist am effizientesten, wenn jeder Kernel dedizierte Hardware zur Verfügung hat. Kernel bieten häufig auch die Möglichkeit, einzelne Befehle innerhalb des Kernels parallel auszuführen. Die Fähigkeit von Kerneln mehrere Eingabestreams zu nutzen, ist eine gute Möglichkeit, die Vorteile paralleler Befehle auszunutzen. Dies wird dadurch möglich, dass unterschiedliche Berechnungen gleichzeitig auf verschiedenen Streams durchgeführt werden können. [23] Seite: 24/64 General Purpose Computation on Graphics Processing Unit 10 Frameworks Im laufe der Arbeiten zum Praktischen Teil dieser Ausarbeitung wurden eine Vielzahl von Frameworks getestet und untersucht. Frameworks machen die Entwicklung von GPGPU-Programmen einfacher und übernehmen beispielsweise das Initialisieren der GPU (Ausschalten der Rasterizer, u.ä.) und das Verteilen der Last auf die Shadereinheiten. Im Folgenden sollen die Frameworks vorgestellt werden, die zur Zeit von den beiden großen Grafikkartenherstellern NVIDIA und ATI (AMD) entwickelt werden. Beide konnten von uns nicht getestet werden, da für das NVIDIA Framework CUDA keine Hardware zur Verfügung stand und von ATI noch keine Implementierung von „Close to metal“ in einer Hochsprache veröffentlicht wurde (davon abgesehen fehlte auch hier die Hardware). 10.1 CUDA Compute Unified Device Architecture (CUDA) ist ein von NVIDIA entwickelte API, welche auf der C Syntax basiert. CUDA unterstützt nur die GPUs der neuen GeForce 8 Serie. NVIDIA garantiert, dass Programme, welche für die GeForce 8 Serie entwickelt wurden, auch auf kommenden Grafikkarten ohne Modifikationen lauffähig bleiben. Das CUDA SDK wurde erstmalig am 15. Februar 2007 veröffentlicht. 10.1.1 Funktionsweise CUDA ist darauf ausgelegt, die GPU als Coprozessor zur Berechnung allgemeiner datenparalleler Probleme zu nutzen. Um mit CUDA die GPU zu nutzen, können entweder die mitgelieferten CUDA Libraries, welche zum Beispiel Funktionen für die schnelle Fourier Transformation[35] zur Verfügung stellen, genutzt oder selbst Programme für CUDA geschrieben werden. Diese Programme können sowohl normalen CPU/C++ Code sowie auch GPU/C Code enthalten. Übersetzt werden sie mit dem von NVIDIA entwickelten NVCC Compiler Driver[36] welcher den CPU- vom GPU Quelltext trennt. Der CPU Teil wird dann mit einem vorhandenem C++ Compiler übersetzt, während der GPU-Code an einen auf dem Open-Source Open64[37] basierenden Compiler weitergegeben wird, welcher einen PTX-Code(„Parallel Thread eXecution“) erzeugt. Dieser Code wird erst bei der Installation auf dem Zielrechner vom CUDATreiber in den angepassten Code für die jeweilige GPU übersetzt. Dadurch ist gewährleistet, dass einmal geschriebene Programme auch auf kommenden GPUs ohne Modifikationen lauffähig sind. Seite: 25/64 General Purpose Computation on Graphics Processing Unit 10.1.2 Vorteile Da schon bei der Entwicklung der Fokus auf die Nutzung der GPU als Coprozessor gelegt wurde, ist CUDA für general purposes weitaus besser geeignet als Grafik-APIs (DirectX, OpenGl). Mit der „Scattered writes“ Methode können mit CUDA zudem beliebige Adressen im Speicher beschrieben werden. Über ein „shared memory“ bietet CUDA auch einen schnellen geteilten Speicher über den max. 16 kB Daten zwischen Threads ausgetauscht werden können. CUDA bietet volle Unterstützung für Integer- und bitweise Operationen. 10.1.3 Einschränkungen Die GPUs von NVIDIA bieten nur eine 32-Bit Unterstützung und haben daher nur die Möglichkeit single precision Zahlen zu unterstützen. Bald sollen jedoch NVIDIA GPUs kommen, die 64-Bit, und damit double precision Zahlen, unterstützen. Eine gravierende Einschränkung sind die Abweichungen vom IEEE 754 Standard[42]. So werden keine denormalisierten Zahlen1 und auch nicht das NaN Symbol unterstützt. Zudem sind nur zwei Methoden zum Runden von Zahlen unterstützt (abschneiden und zur-nächsten-Runden), zum Beispiel binäres Runden ist nicht implementiert. Die Genauigkeit von Divisionen/Quadraten von Wurzeln ist auch deutlich geringer als in der single precision. 10.1.4 Beispiel: Bitonic Sort Im Anhang befindet sich ein Beispiel für eine Umsetzung des Sortierverfahrens Bitonic Sort[39] für GPGPU unter CUDA, welches grob aufzeigen soll wie man sich einen Programmaufbau unter CUDA vorstellen kann. [25][39][40][41] Dieser Quellcode wurde von uns, aufgrund fehlender Hardware (siehe 10.1) weder getestet noch ausgeführt. 1 Denormals oder subnormals wird der Zahlenbereich zwischen der kleinsten darstellbaren floating point Zahl und der 0 genannt. Seite: 26/64 General Purpose Computation on Graphics Processing Unit 10.2 CTM Close to Metal (CTM) ist eine von ATI entwickelte virtuelle Maschine, mit der man auf die tief liegende GPU-Hardware zugreifen kann. Die Virtuelle Maschine verdeckt alle für general purposes irrelevanten Komponenten und stellt ein paralleles Datenarray, welches über einen simplen Befehlsprozessor gefüttert wird, und einen einfachen Speichercontroller zur Verfügung stellt. Die Virtuelle Maschine besteht aus drei Hauptkomponenten: ● Befehlsprozessor ● paralleles Datenarray ● Speichercontroller Alle nicht-kritischen GPU-Unterstützungen, welche nicht für GPGPU Programmierung gebraucht werden, werden von der virtuellen Maschine verborgen und verwaltet. Dieses Prinzip macht es der virtuellen Maschine möglich, mehrere verschiedene Versionen mit einem vereinfachten Interface zu unterstützen. Abbildung 19 zeigt den Aufbau der Virtuellen Maschine. Die Abkürzung dpvm steht hier für „data parallel virtuell machine“, welches das von ATI entwickelte Konzept für die Radeon R580er Serie ist. CTM ist die Implementierung dieses Konzeptes. Aus der Abbildung geht hervor, wie die einzelnen Komponenten durch die VM zusammenarbeiten. Abbildung 19: Virtuelle Maschine CTM [26] Seite: 27/64 General Purpose Computation on Graphics Processing Unit 10.2.1 Der Befehlsprozessor Der Befehlsprozessor bekommt von der Applikation Befehle geschickt (wie z.B. das Setzen von Speicherzugriffen oder Programmstarts), indem diese die Befehle in den Befehlspuffer des Speichers schreibt und dann an die Virtuelle Maschine schickt. 10.2.2 Paralleles Datenarray Die Ausführung findet im parallelen Datenarray statt. Die Virtuelle Maschine spezifiziert ein binäres Interface, das den internen Instruktionssatz des Prozessors abbildet. Entwickler können so die GPU über Maschinencode oder durch binäre ausführbare Dateien, welche von einem Compiler erstellt wurden, programmieren. Sobald ein Programm kompiliert wurde, ist es immun gegenüber Treiberänderungen. 10.2.3 Speichercontroller Die Virtuelle Maschine stellt über den Speichercontroller der Anwendung direkt den Grafikspeicher zu Verfügung. Befehle, Konstanten, Eingaben und Ausgaben werden in den Speicher der GPU(Videospeicher) oder über PCI-Express (Verbindung zwischen GPU und CPU) in den Hauptspeicher geschrieben. Spezifiziert wird dies in der Anwendung. Zudem spezifiziert die Anwendung die Formate der Ein- und Ausgabedaten im Speichercontroller. Der Speicher kann außerdem gecasted werden: Es können also Werte, welche zum Beispiel als ein 4-Kanal-Array geschrieben werden, als 1-Kanal-Array ausgelesen werden. Hierfür muss jedoch 4 mal vom 4-Kanal-Array gelesen werden. Dies geschieht ohne das Verschieben oder Kopieren von Daten. 10.3 Einschränkungen Da von ATI weder High Level Programmiersprachen, noch sonstige Entwicklungstools vorliegen, werden die Vorteile von CTM wohl nur von Programmieren genutzt, die gewillt sind sich mit den neuen GPU-Assemblern und den Details der R580 Plattform zu beschäftigen. Für die R600 Plattform ist zudem bisher noch kein Konzept für GPGPU Programmierung erschienen. [23] [24][26][39][40] Seite: 28/64 General Purpose Computation on Graphics Processing Unit 11 Praktischer Teil In diesem Teil werden zunächst die in den Umsetzungen verwendeten Frameworks (NVIDIAs C for Graphics, das OpenGL Utility Toolkit und Rapidmind) beschrieben um die Grundlagen für die Programmierbeispiele zu schaffen. 11.1 NVIDIAs C for Graphics (CG) C for Graphics (CG) ist eine High-Level Programmiersprache von NVIDIA. NVIDIA wollte mit CG Entwicklern eine Möglichkeit bieten ihre Grafikprogramme in einer C ähnlichen Hochsprache entwickeln zu können. Neben dem leicht verständlichen Code der nun entwickelt werden kann, liefert CG einen Compiler mit, der den Code für die Hardware optimiert. Des weiteren lässt sich Code der in CG geschrieben wurde leichter auf andere Hardware übertragen. Um eine Funktion mit CG zu implementieren, wird ein so genannter Shader implementiert, welcher auf der GPU ausgeführt wird. CG wurde nicht für GPGPU entwickelt, lässt sich aber hierfür einsetzen.[30] 11.2 OpenGL Utility Toolkit (GLUT) Das OpenGL Utility Toolkit (GLUT) ist eine Programmierschnittstelle, die es ermöglicht plattformunabhängige Grafikprogramme zu erstellen. Es bietet unter anderem Bibliotheken für ANSI-C und Fortran. Glut setzt auf OpenGL und GLU auf. Ziel der Entwicklung von Glut war es, Fenster und deren Handler, sowie Tastaturein- und -ausgaben platformunabhängig mit OpenGL zu ermöglichen. Neben diesen Funktionen bietet GLUT Funktionen zum Zeichnen von einfachen geometrischen Objekten wie Rechtecken und Kreise. Glut ist kein spezielles Framework für GPGPU. Es lässt sich aber in Verbindung mit NVIDIAs CG hierfür verwenden. Hierfür wird ein CG Shader implementiert, der den Algorithmus für die gewünschte Rechenoperation ausführt. Dieser wird beim Zeichnen einer Texture von GLUT aufgerufen. Glut wird nicht mehr weiterentwickelt. Es gibt jedoch das kompatible FreeGlut, welches als Open Source Projekt weiterhin gepflegt wird.[31] 11.3 RapidMind RapidMind ist eine Framework welches den Quellcode für die GPU optimiert. Es besitzt einen speziellen Mechanismus, welcher Flaschenhälse findet und versucht sie zu beheben. Auch Engpässe bei der Datenübertragung werden erkannt und wenn möglich behoben. Wie dies geschieht, ist leider nicht näher in der Dokumentation des RapidMind Framworks beschrieben. Ein Loadbalancer übernimmt das Aufteilen und Synchronisieren der Daten auf der GPU.[29] Seite: 29/64 General Purpose Computation on Graphics Processing Unit 11.4 Beispiel Matrix-Reduktion Bei einer Matrix-Reduktion, wird die Matrix solange verkleinert, bis das größte Element der Matrix gefunden wurde. Die Matrix wird schrittweise reduziert. Es werden immer vier Elemente der Matrix miteinander verglichen, aus diesen das Größte ausgewählt und als Wert für die nächste Matrix verwendet. Abbildung 20: Matrix-Reduktion Da jede der gebildeten 2x2 Matrizen von keiner anderen Matrix abhängig ist, besteht Datenunabhängigkeit und das Verfahren lässt sich gut parallelisieren. Ein weiterer Vorteil für die GPU ist, dass der Ausgabewert eines Vergleiches den Eingabewert darstellt und so nicht erst über den Speicher die Daten geschrieben und gelesen werden müssen. Das volle Potenzial der GPU lässt sich mit diesem Versuch dennoch nicht ausnutzen, da hierfür nur Vergleiche und keine komplexen Berechnungen ausgeführt werden müssen. Seite: 30/64 General Purpose Computation on Graphics Processing Unit 11.4.1 Umsetzung der Matrix-Reduktion(CPU) Für die CPU wurde ein C-Programm implementiert. In diesem C-Programm wird zunächst ein Datenarray mit der Länge der Anzahl der Elemente der Matrix mit Zufallswerten gefüllt. Das erste Element wird in das cpuResult geschrieben und danach wird jedes Element mit dem cpuResult verglichen. Wenn ein Element größer ist, ist dieses das neue cpuResult. Damit enthält das cpuResult am Ende die größte Zahl. In dieser Implementation wird das gesamte Array sequenziell durchlaufen. Im „worst case“ wird das cpuResult also N mal ersetzt, wobei N die Länge des Feldes ist. float cpuResult = data[0]; for (int i=1; i<N; i++) { if (data[i] > cpuResult) { cpuResult = data[i]; } } 11.4.2 Umsetzung der Matrix-Reduktion (GPU) Für die GPU existiert ein C-Code als Beispiel im GPGPU Tutorial. [28] In diesem Beispiel wird GLUT und NVIDIAs CG eingesetzt. Der Algorithmus wird als CG Shader implementiert. Zunächst wird eine 2x2 Matrix auf der CPU mit Gleitkommazufallswerten befüllt. Diese werden als Luminanzwert(Luminanz: Lichstärke pro Fläche) in den Pixeln gespeichert. Das Speichern geschieht durch Erzeugen einer Texture auf der GPU. In diesem Fall ein Quadrat mit der Größe der Matrix. Über GLUT lässt sich definieren, welche Operation beim Zeichnen ausgeführt werden soll. Hier wird der in CG implementierte Shader angegeben. Ein Array ist also auf der GPU eine Textur, die im general purpose Einsatz statt Pixeln die Arrayelemte enthält. Der Shader wird innerhalb einer Schleife, durch Zeichnen einer Textur, aufgerufen. Bei jedem Aufruf reduziert der Shader die Matrix auf die lokalen Maxima, bis zuletzt nur noch ein Element übrig bleibt. Im Shader wird jeweils ein Quadrat aus 2x2 Pixeln ermittelt, dessen vier Luminanzwerte ausgelesen werden. Der höchste Luminanzwert wird zurückgegeben. Aus den Ausgabewerten der Shaderaufrufe folgt die neue, auf ¼ verkleinerte Eingabematrix für den nächsten Shaderaufruf. Seite: 31/64 General Purpose Computation on Graphics Processing Unit //Funktions deklaration in CG //float2 (nx2 Matrize) gespeichert in Form einer WPOS = world space //vertex position float maximum(float2 coords: WPOS, uniform samplerRECT texture) : COLOR { //Berechne oberen linken Punkt eines Teilquadrates (Teilmatrix) float2 topleft = ((coords-0.5)*2.0)+0.5; //Hole Luminanzwert der Pixel, textRect holt den Pixelwert einer //texture an angegebener Stelle float val1 = texRECT(texture, topleft); float val2 = texRECT(texture, topleft+float2(1,0)); float val3 = texRECT(texture, topleft+float2(1,1)); float val4 = texRECT(texture, topleft+float2(0,1)); //Rückgabe des ermittelten Maximum der vier Luminanzwerte //Durch return liegt das Ergebniss im Fragmentbuffer und kann // z.B. über glReaBuffer() ausgelesen werden. return max(val1,max(val2,max(val3,val4))); }; 11.4.3 Ping-Pong Das Ping-Pong-Verfahren ist ein Verfahren, um den Ausgabewert einer Operation direkt wieder als Eingabewert der nächsten Operation verwenden zu können. Hierfür enthält GLUT das FrameBufferObject (FBO). Das FBO repräsentiert den Framebuffer der GPU. Es existieren im Beispiel der MatrixReduktion zwei FBOs (FBO_old und FBO_new), es ist aber möglich weitere zu definieren. Bei der Matrix-Reduktion wird nun aus FBO_old FBO_new berechnet und die Rollen der beiden Objekte nach jeder Berechnung getauscht. Bevor dies geschehen kann, muss FBO_old existieren. Deshalb wird im ersten Durchgang eine Texture verwendet, so dass FBO_old berechnet werden kann. Ab dann findet der Wechsel nach jeder Berechnung statt. Vorteil bei diesem Verfahren ist, dass nach jeder Berechnung die Ausgabedaten direkt als Eingabedaten verwendet werden können. Es muss kein zeitaufwändiger Transfer des Framebuffers in eine Texture ausgeführt werden. Seite: 32/64 General Purpose Computation on Graphics Processing Unit for (int i=0; i<numPasses; i++) { // input texture is read-only texture of pingpong textures //readTex wird am Ende der Schliefe geändert, so wechselt sich immer //input und output texture cgGLSetTextureParameter(textureParam,pingpongTexID[readTex]); cgGLEnableTextureParameter(textureParam); // set render destination glDrawBuffer (attachmentpoints[writeTex]); // calculate output region width and height outputWidth = outputWidth / 2; // Durch zeichnen die Berechnung ausühren drawQuad(outputWidth,outputWidth); //Funktion zum tauschen der input und output Matrix swap(); } //Funktion zum tauschen der input und output Matrix void swap(void) { if (writeTex == 0) { writeTex = 1; readTex = 0; } else { writeTex = 0; readTex = 1; } } Seite: 33/64 General Purpose Computation on Graphics Processing Unit 11.4.4 Messungen Matrix-Reduktion Für die Messungen wurde folgende Konfiguration verwendet: GPU: GeForce 6600 300 MHZ Prozessortakt 225 MHZ Speichertakt 256 MB DDR-RAM CPU: AMD Sempron 2400+ 1.66 GHz Prozessortakt 166 MHz Speichertakt 256 MB DDR-RAM Die Messungen wurden unter Windows XP ServicePack 2 durchgeführt. Matrixgröße CPU Zeit in sec GPU Zeit in sec 512X512 0 (nicht messbar) 0,016 1024X1024 0,018 0,045 2048X2048 0,799 0,486 4096X4096 1,453 0,721 Es zeigte sich das bei einer größeren Matrix die GPU schneller ist als die CPU. Bei kleineren Matrizen war die Zeit auf der CPU sowie auch auf der GPU nicht messbar. Größere Matrizen ließ unsere Testhardware nicht zu. Ein Versuch der Erklärung bezieht sich auf die Ausgabe des FFFF Benchmarks (siehe Punkt 11.5.5), in der für unsere Grafikkarte folgende maximale Anzahl an floating point Anweisungen angegeben wurde: Maximum number of FP ALU instructions: 4096 Seite: 34/64 General Purpose Computation on Graphics Processing Unit 11.5 Beispiel Vektor Multiplikation In diesem Beispiel werden die Vektoren a und b multipliziert und in Vektor c geschrieben. Hier besteht zwar unter den einzelnen zu multiplizierenden Elementen der Vektoren auch Datenunabhängigkeit, aber der Vorteil, das ein Ausgabewert gleich neuer Eingabewert ist, kann hier nicht ausgenutzt werden. Auch sind in diesem Beispiel wenig Operation für die GPU auszuführen. Um dennoch der GPU einen kleinen Vorteil zu verschaffen, werden alle vier Werte(R, G, B, Alpha) des RapidMind Datentypen value4f benutzt. Durch die „4“ in value4f wird eine Matrix der Größe Nx4 erstellt: Dies ist die maximale Dimension. Mit value2f würde eine Nx2 Matrix erstellt werden. 11.5.1 Umsetzung Multiplikation CPU Für die CPU wurde ein C-Programm implementiert. Bei diesem werden in einer Schleife elementweise zwei zweidimensionale Arrays der Größe 10.000x4, gefüllt mit Zufallswerten, multipliziert. Es folgt ein Ausgabearray gleicher Länge. for ( int i = 0; i < length ; ++i ) { for( int j = 0; j < 4 ; ++j) { result[ i ][ j ] = inputcpu1[ i ][ j ] * inputcpu2[ i ][ j ]; } } Um eine aussagekräftige Messreihe zu bekommen, wird die Berechnung N-mal innerhalb einer Schleife ausgeführt. Seite: 35/64 General Purpose Computation on Graphics Processing Unit 11.5.2 Umsetzung Multiplikation GPU Umgesetzt wurde der Algorithmus mit C und dem RapidMind Framework. Zunächst werden die beiden Inputvektoren (ebenfalls mit 40.000 Werten) von der CPU mit Zufallsdaten befüllt. Diese werden dann im Kernel von RapidMind auf der Grafikkarte verarbeitet. Es entsteht der Ausgabevektor. //Kernel beginn Program prg = RM_BEGIN { //Erster Eingabevektor vom Typ Float //Value4f: Value Wert //4 = vier Werte(maximum), die von einem Kernel abgearbeitet werden können //f = float In<Value4f> a; //Zweiter Eingabevektor In<Value4f> b; //Ausgabevektor vom gleichen Typen Out<Value4f> c; //Multiplikation c[0] = a[0] * b[0]; c[1] = a[1] * b[1]; c[2] = a[2] * b[2]; c[3] = a[3] * b[3]; //Ende } RM_END; Der Aufruf erfolgt in einer Schleife die N-mal durchläuft mit: output = prg(input1, input2); Das Framework übernimmt selbständig die Lastverteilung, so dass sich der Entwickler nicht um die parallele Ausführung der Rechenoperation kümmern muss. Dennoch muss der Entwickler darauf achten, dass sein Code überhaupt parallelisierbar ist. Seite: 36/64 General Purpose Computation on Graphics Processing Unit 11.5.3 Messungen Multiplikation Für die Messungen der Multiplikation wurde der selbe Testrechner wie bei der Matrixreduktion verwendet. Die Schleife, in denen die Berechnung durchgeführt wird, wird N mal auf CPU und GPU durchgeführt. In jedem Schleifendurchlauf werden 40.000 Werte miteinander multipliziert, sodass im letzten Durchlauf unserer Messung 100.000 x 40.000 Berechnungen durchgeführt werden. Schleifendurchläufe CPU in sec GPU in sec 1 0 (nicht messbar) 0,562 100 0,047 0,593 1000 0,531 0,875 2000 1,062 1,203 2500 1,31 1,38 3000 1,656 1,625 4000 2,27 1,88 5000 2,625 2,156 10000 5,531 3,797 20000 10,75 7,203 30000 15,906 10,359 40000 21,438 13,984 50000 26,938 16,703 60000 32,937 19,828 70000 37,984 23,204 80000 41,843 26,438 90000 48,5 26,672 100000 54,328 32,875 Seite: 37/64 General Purpose Computation on Graphics Processing Unit Abbildung 21: Messergebnisse Multiplikation Es ist zu sehen, dass bei wenig Schleifendurchläufen die CPU schneller ist als die GPU. Dies hängt unter anderem mit der Initialisierung der GPU und dem Übertragen der Daten durch das Framework zusammen. Erst bei 2500 Schleifendurchläufen zeigt sich der Vorteil der GPU. Bei 100.000 Schleifendurchläufen zeigt sich, wie zu erwarten war, deutlich der Vorteil der GPU. Sie ist fast doppelt so schnell wie die CPU. Seite: 38/64 General Purpose Computation on Graphics Processing Unit 11.6 Beispiel Mandelbrot-Menge Die Mandelbrotmenge wurde 1979 vom polnischen Mathematiker Bernoit Mandelbrot zur Klassifizierung der Julia-Mengen eingeführt. Wie auch die JuliaMenge, ist die Mandelbrot-Menge ein Fraktal. Definiert ist die MandelbrotMenge als die Menge der komplexen Zahlen die durch das Bildungsgesetz mit der Anfangsbedingung gebildet werden und bei denen der Betrag der Folgenglieder nicht ins unendliche wächst. Stellt man das Mandelbrot in der komplexen Zahlenebene dar, so erhält man ein an das Apfelmännchen erinnerndes Fraktal. [32] Abbildung 22: Mandelbrot-Menge [32] Wie jedes Fraktal, weist auch die Mandelbrotmenge Selbstähnlichkeit auf. Wird ein Bereich des Mandelbrots vergrößert, so finden sich ähnliche geometrische Objekte wie das Ausgangsfraktal wieder. Seite: 39/64 General Purpose Computation on Graphics Processing Unit 11.6.1 Fast Floating Fractal Fun (FFFF) Fast Floating Fractal Fun [33] diente als Testobjekt für die Berechnung und grafische Darstellung der Mandelbrot-Menge. Hier besteht die Möglichkeit den Algorithmus verschieden berechnen zu lassen: • FPU computation, C code. Allgemeine „Single-Pixel“ Berechnung für MIPS4 ISA. • QuadFast SSE computation, 100% machine code. Genutzt wird die Befehlssatzerweiterung SSE. Dadurch wird ermöglicht, dass immer vier Pixel gleichzeitig in „Single Precision“ berechnet werden können. Der Algorithmus greift während der Berechnungen nicht auf den Speicher zu. • DualFast SSE2 computation, 100% machine code. Genutzt wird die Befehlssatzerweiterung SSE2. Hier werden immer zwei Pixel gleichzeitig in „Double Precision“ berechnet. Es ergibt sich, gegenüber der Verwendung von SSE ein Geschwindigkeitsverlust: Der Algorithmus ist nur noch halb so schnell. Allerdings wird eine höhere Präzision erreicht, was einen tieferen Zoom erlaubt. Der Algorithmus greift während der Berechnungen nicht auf den Speicher zu. • GPU Fragment Program computation Genutzt werden die Fragment-Prozessoren der Grafikkarte. Die Prozessoren berechnen jeweils vier Pixel in „Single Precision“, was durch die hohe Anzahl an Prozessoren zu einem Leistungszuwachs führt. Beispielsweise die 3DNow-Berechnung konnte aufgrund des Einsatzes von OS X 10.4 nicht getestet werden. Das Programm bietet neben dem Heraus- und Hineinzoomen des Fraktals und einem Benchmark auch die Möglichkeit, die Zahl der Iterationen zu variieren. Die maximale Anzahl der Iterationen beträgt laut „Hersteller“ 9999. Jedoch resultierte der Versuch die Iterationen heraufzusetzen in einem BUS-Error bei ca. 1200 Iterationen. Die Herkunft dieses Fehlers konnte nicht eindeutig bestimmt werden, da auch im Buchtracking System des Anbieters hierzu nichts zu finden war. Getestet wurde auf einem Apple iMac mit 1 GB Ram. 2 GHz Core2Duo Prozessor und einer NVIDIA GeForce 7800 Pro. Von FFFF wurde die Programmversion v3.2.3 eingesetzt. Seite: 40/64 General Purpose Computation on Graphics Processing Unit 11.7 Umsetzung Mandelbrot CPU in FFFF Auf der CPU wurde das Mandelbrot mit C++ realisiert. Die Iteration von n nach n+1 für einen Punkt c der komplexen Zahlenebene erfolgt mittels der komplexen Gleichung Diese lässt sich in und zerlegen. Im C++ Code innerhalb einer for-Schleife umgesetzt: for(i=0;i<maxi;i++){ zx2=zx*zx; zy2=zy*zy; //Abbruchbedingung if((zx2+zy2)>4)break; zy=2*zx*zy; zx=zx2-zy2; //Komplexer Teil zx+=cx; zy+=cy; } 11.7.1 Umsetzung Mandelbrot GPU in FFFF Umgesetzt wurde die Berechnung des Mandelbrots auf der GPU mit dem NVIDIA Vertex and Pixel Shader Macro Assembler (NVASM). Der Code, welcher auf der GPU ausgeführt wird, wird innerhalb eines C++ Programms über GLUTFunktion auf die Grafikkarte übertragen. Bei NVASM besitzt jedes Register vier Werte (x, y, z, w). Es ist möglich jeden Wert dieses Register einzeln anzusprechen z.B. MOV R1.x, R2.y. Des weiteren ist auch möglich mehrere Werte anzusprechen z.B. MOV R1.xw R2.yz. Der Code des Shaderprogramms unterteilt sich in drei Abschnitte, die im C++ Quellcode in Form von Strings vorliegen. Seite: 41/64 General Purpose Computation on Graphics Processing Unit Der erste Teil ist die Initialisierung der Grafikkarte. Hier wird in der ersten Zeile die Version des verwendeten Shaders für die Initialisierung der Grafikkarte angegeben. Mit dem Befehl „DP4“ wird ein Skalarprodukt eines 4D Vektors erzeugt. In diesem Fall dient der Befehl dazu, den Eingabevektor in den sichtbaren Bereich zu transformieren. Am Ende der Initialisierung werden mit Hilfe des Konstantenvektors von NVASM die Register, mit denen gerechnet wird, befüllt. Der Konstantenvektor c wurde an der Stelle c[0] zuvor im C++ Quellcode befüllt. "!!VP1.0\n" //o=Ausgabevektor; HPOS horizontale Position //c=Konstantenvektor von NVASM(c4 bis c7 enthält standardmässig die //Zeilenvektoren der Matrix für die Transformation in den Sichtbereich //des Betrachters) //v=Position des Vertex ; OPOS senkrechte Position "DP4 o[HPOS].x, c[4], v[OPOS];" "DP4 o[HPOS].y, c[5], v[OPOS];" "DP4 o[HPOS].z, c[6], v[OPOS];" "DP4 o[HPOS].w, c[7], v[OPOS];" // Werte für die Berechnung in R0 schieben, in c.x steht 1, in c.y steht //-1, daraus folgt R0 = v[o].xyz-y "MUL R0, v[0].xyzy, c[0].xxxy;" //aus R0 werden nur x und z übernommen, y und w werden auf 0 // gesetzt, in c.w steht 0 "MUL R4, R0.xzyw, c[0].xxww;" Seite: 42/64 General Purpose Computation on Graphics Processing Unit Der zweite Codeabschnitt enthält den Code für die Iterationen. Beim Zusammensetzen des NVASM-Codes wird dieser Abschnitt entsprechend der Anzahl der Iterationen in den Code eingefügt. Das heißt, dass der Codeabschnitt beispielsweise bei vier Iterationen vier mal in den Code eingefügt wird, statt Schleifen zu verwenden. Zunächst werden die Werte von R0 multipliziert und anschließend mit dem imaginären Anteil, der in R4 steht, addiert. Durch anschließende Subtraktion von den entsprechenden Werten aus R1 erhält man die Werte für die nächste Iteration. //R0.x=R0.x^2+r4-(R0.y^2+R4) //R0.y=R0.x*R0.y+r4-(R0.x*R0.y+R4) "MAD R1, R0.xyxx, R0.xyyw, R4;" "ADD R0.xyw, R1.xzww, -R1.ywwz;" Im letzten Teil des NVASM-Programms wird das Register R1 in den Outputvektor der Graffikkarte geschrieben und schließlich das Programm beendet. "MOV o[COL0].xyz, R1.xyzw;" "END" Seite: 43/64 General Purpose Computation on Graphics Processing Unit 11.7.2 Darstellung der Mandelbrot-Menge mit FFFF Die Abbildung 23 zeigt eine Mandelbrotmenge ohne Zoom. In diesem Beispiel wird die Grafik von der CPU erstellt. Zu sehen ist der Kopf (links) und der Körper (rechts) des Fraktals. Abbildung 23 zeigt einen Zoom auf die Spalte zwischen Kopf und Körper. Hier entstehen Spiralen, die, in Abbildung 24 auf der rechten Seite, an Seepferdchen erinnern. Durch Erhöhen der Iterationen wird ein Zoom auf den „Schwanz“ eines dieser Seepferdchen möglich. Dieser Zoom, wie in Abbildung 25 gezeigt, kann bei Verwendung von SSE2 bis zu einer 10^15-fachen Vergrößerung durchgeführt werden. Abbildung 23: Die MandelbrotMenge im Urzustand Abbildung 24: Spalte zwischen Kopf und Körper Abbildung 25: Zoom auf ein Seepferdchen Abbildung 26: Tieferer Zoom auf ein Seepferdchen Seite: 44/64 General Purpose Computation on Graphics Processing Unit Um den Effekt der Iterationen zu zeigen, wurde ein Bereich bei hoher Zoomstufe ausgewählt. Dieser wurde bei kontinuierlich gesteigerter Iterationsanzahl beobachtet. Um die Entwicklung der Darstellung zu zeigen, wurden Momentaufnahmen bei 40, 50, 70 und 256(siehe Abbildung 27-30) Iterationen gemacht. Abbildung 27: Darstellung mit 40 Iterationen Abbildung 29: Darstellung mit 70 Iterationen Abbildung 28: Darstellung mit 50 Iterationen Abbildung 30: Darstellung mit 256 Iterationen Während der Tests fiel auf, dass die Farbgebung der Mandelbrot-Menge variiert, wenn man die Grafik mit einem Fragment Programm berechnen lässt. Dies liegt indirekt am Alter der Programmversion (März 2006) und am Geldbeutel des Autoren. Die aktuelle Version von FFFF hat keine GPU Berechnungsroutine, die Branches nutzt. „Dynamic Branching“ ist eine Verbesserung, die im Shader Model 3 enthalten war und im April 2004 erstmals auf der GeForce 6 zum Einsatz kam. Daraus resultiert, dass alle Punkte mit maximalen Iterationen berechnet werden. Da die Farbe als Funktion des finalen komplexen Ergebnisses berechnet wird, kommt eine andere Farbgebung zustande [34]. Seite: 45/64 General Purpose Computation on Graphics Processing Unit Abbildung 31: Berechnung mit der CPU Abbildung 32: Berechnung mit der GPU Abbildung 33: Tieferer Zoom; CPU-Berechnung Abbildung 34: Identisches Bild; GPU-Berechnung Auffällig ist neben der Farbänderung auch der Vergleich der letzten beiden Aufnahmen, da im linken Bild (Abbildung 33) deutlich mehr erkennbar ist als im rechtem Bild (Abbildung 34). Dies wird nicht nur mit der „ungünstigen“ Farbwahl, sondern auch mit der geringeren Genauigkeit des GPU-Algorithmus (single prec.) gegenüber dem SSE2-CPU-Algorithmus (double prec.) zusammenhängen. Seite: 46/64 General Purpose Computation on Graphics Processing Unit 11.7.3 Benchmark mit FFFF Fast Floating Fractal Fun bietet die Möglichkeit, Benchmarks zu erstellen. In diesen Benchmarks wird die Mandelbrot-Menge auf die verschiedenen Weisen berechnet . Die Leistung wird in „Megaiters/sec“ angegeben, was „Millionen Iterationen pro Sekunde“ bedeutet. Für ein Benchmark mit möglichst eindrucksvollem Ergebnis haben wir einen AMD 64 mit 2.52 GHz Taktung und einer GeForce 8800 GTS aufgetrieben. Zur Grafikkarte: Die GeForce 8800 GTS verfügt über (für uns unwichtige, aber trotzdem genannte) 768 MB Ram mit einem Takt von 800 MHz. Die 96 Stream Prozessoren der Grafikkarte arbeiten mit einem Takt von 1200 MHz bei einem GPU-Kerntakt von 500 MHz. In der GeForce 8xxx-Reihe kommen bereits „unified“ Shader zum Einsatz. Das bedeutet, dass es keine hardwareseitige Trennung zwischen Fragment- und Vertex-Shadern mehr gibt. Die GPU entscheidet zur Laufzeit ob ein Shader als Fragment- oder Vertex-Shader eingesetzt wird. Alle Shadereinheiten sind also prinzipiell in der Lage, die gleichen Operationen auf Daten durchzuführen. Benchmark-Ausgabe: FFFF v3.2.3 BENCHMARK (Using 1 CPU, no render) size: 500*500 maxiters: 9999 rangex: -2.00 to 1.00 rangey: -1.50 to 1.50 [4f] SSE benchmark: 0.586 sec 718.046 MegaIters/sec [2d] SSE2 benchmark: 1.143 sec 367.925 MegaIters/sec [1d] FPU C benchmark: 2.504 sec 167.931 MegaIters/sec [4?] GPU VertexProgram benchmark (beta! maxiters=10) 0.315 sec 1587.937 MegaIters/sec [4?] GPU FragmentProgram benchmark (beta! maxiters=20) Maximum number of FP ALU instructions: 16384 Maximum number of FP native params: 1024 FP is hardware native (63 ALU instructions). 0.067 sec 14894.916 MegaIters/sec Seite: 47/64 General Purpose Computation on Graphics Processing Unit Bei 96 Shadern ergeben sich 155,156 Millionen Iterationen pro Sekunde und Shadereinheit. Der CPU-Kern berechnet in einer Sekunde 78,046 Millionen Iterationen (SSE). Damit kommt eine Shadereinheit auf ein gutes Fünftel (21,61%) der Leistungsfähigkeit (in Bezug auf die berechneten Iterationen) eines CPU-Kerns. Seite: 48/64 General Purpose Computation on Graphics Processing Unit 12 Fazit Es zeigte sich, dass die GPGPU-Programmierung ein hohes Umdenken erfordert. Dies lag hauptsächlich an der Parallelität. Des weiteren ist die Syntax zwar C oder Java ähnlich, aber dennoch lassen sich Shadersprachen für GPGPU nur schwer mit diesen Programmiersprachen vergleichen. Häufig ist ein hoher Aufwand nötig um die Frameworks zu installieren und in einer IDE nutzen zu können. Die Dokumentation ist meist mangelhaft. Informationen finden sich häufig nur in Artikeln verschiedener Internetseiten und Zeitschriften. Konkret wird auf die Programmierung selten eingegangen. Auch ist die Community der GPGPU Programmierer nicht groß. Hinzu gibt es das Problem der eingeschränkten Hardwareunterstützung. Dies liegt hauptsächlich daran, das es seit VGA (1987) keine standardisierte Architektur für Grafkikkarten gibt und die Frameworks immer nur bestimmte Serien von Grafikkarten unterstützen. Positiv zu sehen ist der enorme Geschwindigkeitszuwachs. Auch wird sich in Zukunft die GPGPU Gemeinde vergrößern und neue Frameworks wie CUDA sollen eine einfachere Handhabung als die Konkurrenten bieten. Seite: 49/64 General Purpose Computation on Graphics Processing Unit 13 Quellen Quellen [1] The Gamer‘s Graphics & Display Settings Guide http://www.tweakguides.com/Graphics_1.html [2] NV40-Technik im Detail http://www.3dcenter.de/artikel/nv40_pipeline [3] Introduction to the Hardware Graphics Pipeline http.download.NVIDIA.com/developer/presentations/2005/I3D/ I3D_05_IntroductionToGPU.pdf [4] NVIDIA CUDA Introduction http://www.beyond3d.com/content/articles/12/5 [5] GPU Gems 2 Kapitel 30 http.download.NVIDIA.com/developer/GPU_Gems_2/GPU_Gems2_ch30.pdf [6] GPGPU.org http://www.gpgpu.org [7] Pixel-Shader http://en.wikipedia.org/wiki/Pixel_shader [8] Pasterizer http://en.wikipedia.org/wiki/Rasterizer [9] Stream Processing http://en.wikipedia.org/wiki/Stream_processing [10] Single Instruction Multiple Data http://en.wikipedia.org/wiki/SIMD [11] Multiple Instruction Multiple Data http://en.wikipedia.org/wiki/MIMD [12] Multiple Instruction Single Data http://en.wikipedia.org/wiki/MISD [13] GPGPU http://en.wikipedia.org/wiki/GPGPU [14] Floating Point Operations Per Second http://en.wikipedia.org/wiki/FLOPS [15] NVIDIA GeForce FX (5) http://en.wikipedia.org/wiki/GeForce_FX_Series [16] GeForce 6 http://en.wikipedia.org/wiki/GeForce_6_Series [17] NVIDIA GeForce 7 http://en.wikipedia.org/wiki/GeForce_7_Series [18] NVIDIA GeForce 8 http://en.wikipedia.org/wiki/GeForce_8_Series [19] NVIDIA GeForce 9 http://en.wikipedia.org/wiki/GeForce_9_Series [20] E-Bug Onlinestore http://www.e-bug.de [21] Einführung in die Architektur moderner Grafikkarten Seite: 50/64 General Purpose Computation on Graphics Processing Unit http://www.ra.informatik.unimannheim.de/pages/student_work/seminar/ws0506/ Sven_Schenk/praesentation.pdf [22] GPGPU Basiskonzepte http://www.plm.eecs.unikassel.de/plm/fileadmin/pm/courses/gpgpu_seminar_ss2006/ unik_s_gpgpu_basiskonzepte_ss2006.pdf [23] GPGPU Stream Processing und High Level GPGPU Sprachen http://www.plm.eecs.unikassel.de/plm/fileadmin/pm/courses/gpgpu_seminar_ss2006/unik_s_gpgpu_st ream_processing_und_high_level_gpgpu_sprachen_ss2006.pdf [24] A Performance-Oriented Data Parallel Virtual Machine for GPUs http://ati.amd.com/developer/techreports/2006/I3D2006/Peercy-PerformanceOriented_Data_Parallel_Virtual_Machine_for_GPUs(SIG06_Sketch).pdf [25] CUDA http://en.wikipedia.org/wiki/CUDA [26] A Performance-Oriented Data parallel Virtual Machine for GPUs http://ati.amd.com/developer/siggraph06/dpvm_sketch_siggraph.pdf [27] AMD entwickelt integrierten CPU-GPU-Chip http://www.heise.de/newsticker/meldung/81183 [28] Matrix Reduktion Tutorial http://www.mathematik.uni-dortmund.de/~goeddeke/gpgpu/tutorial2.html [29] RapidMind Framework http://www.rapidmind.net/technology.php [30]NVIDIA C for Graphics http://developer.NVIDIA.com/page/cg_main.html [31]OpenGL Utility Toolkit http://de.wikipeda.org(wiki/OpenGL_Utility_Toolkit [32]Mandelbrot-Menge http://de.wikipedia.org/wiki/Mandelbrot-Menge [33]Fast Floating Fractal Fun - FFFF http://sourceforge.net/projects/FFFF/ [34] Daniele Paccaloni im FFFF-Forum http://sourceforge.net/forum/forum.php?forum_id=168353 [35] GPGPU History http://gpgpu.org/data/history.shtml [36]Supercomputer für den Schreibtisch http://www.handelsblatt.com/News/printpage.aspx? _p=204016&_t=ftprint&_b=1286446 [37] Open 64 € The Open Research Compiler http://www.open64.net [38] Bitonic Sort http://www.iti.fh-flensburg.de/lang/algorithmen/sortieren/bitonic/bitonic.htm [39] Spezialarchitekturen 1 (GPGPU: Architektur, Programmierung und Anwendungen) http://www.empanyc.net/content/projekte/shprc/spezialarchitekturen_gpgpu_ Seite: 51/64 General Purpose Computation on Graphics Processing Unit mario_kicherer.pdf [40] General Purposes Computation on Graphics Processing Unit http://wwwcg.in.tum.de/Teaching/SS2007/Seminar/Workouts/data/Stefan_Aue r.pdf [41] Brent Oster - CUDA http://www.cs.ucsb.edu/~gilbert/cs240aSpr2007/lectures/G80%20CUDA.ppt [42] IEEE 754 http://754r.ucbtest.org/standards/754.pdf Seite: 52/64 General Purpose Computation on Graphics Processing Unit 14 Anhang 14.1 Bitonic Hauptprogramm (bitonic.cu): #include <stdio.h> #include <stdlib.h> #include <cutil.h> #include "bitonic_kernel.cu" int main(int argc, char** argv) { CUT_DEVICE_INIT(); int values[NUM]; for(int i = 0; i < NUM; i++) { values[i] = rand(); } int * dvalues; CUDA_SAFE_CALL(cudaMalloc((void**)&dvalues, sizeof(int) * NUM)); CUDA_SAFE_CALL(cudaMemcpy(dvalues, values, sizeof(int) * NUM, cudaMemcpyHostToDevice)); bitonicSort<<<1, NUM, sizeof(int) * NUM>>>(dvalues); // check for any errors CUT_CHECK_ERROR("Kernel execution failed"); } CUDA_SAFE_CALL(cudaMemcpy(values, dvalues, sizeof(int) * NUM, cudaMemcpyDeviceToHost)); CUDA_SAFE_CALL(cudaFree(dvalues)); bool passed = true; for(int i = 1; i < NUM; i++) { if (values[i-1] > values[i]) { passed = false; } } printf( "Test %s\n", passed ? "PASSED" : "FAILED"); CUT_EXIT(argc, argv); Seite: 53/64 General Purpose Computation on Graphics Processing Unit 14.2 Bitonic Kernel (bitonic_kernel.cu) #ifndef _BITONIC_KERNEL_H_ #define _BITONIC_KERNEL_H_ #define NUM 256 __device__ inline void swap(int & a, int & b){ int tmp = a; a = b; b = tmp; } __global__ static void bitonicSort(int * values){ extern __shared__ int shared[]; const int tid = threadIdx.x; // Copy input to shared mem. shared[tid] = values[tid]; __syncthreads(); // Parallel bitonic sort. for (int k = 2; k <= NUM; k *= 2) { // Bitonic merge: for (int j = k / 2; j>0; j /= 2) { int ixj = tid ^ j; if (ixj > tid){ if ((tid & k) == 0){ if (shared[tid] > shared[ixj]){ swap(shared[tid], shared[ixj]); } } else{ if (shared[tid] < shared[ixj]){ swap(shared[tid], shared[ixj]); } } } __syncthreads(); } } // Write result. values[tid] = shared[tid]; } #endif // _BITONIC_KERNEL_H_ Seite: 54/64 General Purpose Computation on Graphics Processing Unit 14.3 Matrixreduktion #/* * License (based on zlib/libpng): * * Copyright (c) 2005-2007 Dominik Göddeke, University of Dortmund, Germany. * * This software is provided 'as-is', without any express or implied warranty. * In no event will the authors be held liable for any damages arising from * the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely. * */ /* * GPGPU Reduction Tutorial: Computes maximum of a vector on the GPU. * * Limitation: vector length must be 2^k by 2^k. * * This version: texture rectangles, on-the-fly texcoord computations * * More details: www.mathematik.uni-dortmund.de/~goeddeke/gpgpu * * Please drop me a note if you encounter any bugs, or if you have suggestions * on how to improve this tutorial: dominik.goeddeke@math.uni-dortmund.de * */ // includes #include <iostream> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> #include <time.h> #include <GL/glew.h> #include <GL/glut.h> #include <Cg/cgGL.h> using namespace std; // error codes static int ERROR_CG = -1; static int ERROR_GLEW = -2; static int ERROR_TEXTURE = -3; static int ERROR_BINDFBO = -4; static int ERROR_FBOTEXTURE = -5; static int ERROR_PARAMS = -6; // prototypes void cgErrorCallback(void); bool checkFramebufferStatus(void); void checkGLErrors(const char *label); void compareResults(void); Seite: 55/64 General Purpose Computation on Graphics Processing Unit void void void void void void void void void createTextures(void); drawQuad(int w, int h); initCG(void); initFBOandViewport(void); initGLEW(void); initGLUT(int argc, char** argv); performComputation(void); setupTexture(const GLuint texID); swap(void); // problem size (must be power of 2 by power of 2) int N = 4096*4096; int texSize = 4096; // texture identifiers GLuint pingpongTexID[2]; GLuint inputTexID; // ping pong management vars int writeTex = 0; int readTex = 1; GLenum attachmentpoints[] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT }; // Cg vars CGcontext cgContext; CGprofile fragmentProfile; CGprogram fragmentProgram; CGparameter textureParam; // FBO identifier GLuint fb; // handle to offscreen "window", only used to properly shut down the app GLuint glutWindowHandle; // shortcuts for texture internals GLenum texTarget = GL_TEXTURE_RECTANGLE_ARB; GLenum texInternalFormat = GL_FLOAT_R32_NV; GLenum texFormat = GL_LUMINANCE; // see course web page for details on this shader char* shader = \ "float maximum( " \ " float2 coords: WPOS,"\ " uniform samplerRECT texture) : COLOR { "\ "float2 topleft = ((coords-0.5)*2.0)+0.5;"\ "float val1 = texRECT(texture, topleft);"\ "float val2 = texRECT(texture, topleft+float2(1,0));"\ "float val3 = texRECT(texture, topleft+float2(1,1));"\ "float val4 = texRECT(texture, topleft+float2(0,1));"\ "return max(val1,max(val2,max(val3,val4)));}"; // actual data float* data; /** Seite: 56/64 General Purpose Computation on Graphics Processing Unit * main, just calls things in the appropriate order */ int main(int argc, char **argv) { printf("--------------------------------------------------------------\n"); printf("Parallel reduction example\n\n"); printf("see www.mathematik.uni-dortmund.de/~goeddeke/gpgpu for details\n"); printf("--------------------------------------------------------------\n"); // // create data vector // srand(time(NULL)); data = (float*)malloc(N*sizeof(float)); for (int i=0; i<N; i++) { data[i] = rand() / (((float)rand())+1.0) / 1000; } // // init glut and glew // initGLUT(argc, argv); initGLEW(); // // init offscreen framebuffer // initFBOandViewport(); // // create textures for data vector and pingpong // createTextures(); // // init shader runtime // initCG(); // // and start computation // double time1=0.0, tstart; tstart = clock(); // start performComputation(); time1 += clock() - tstart; // end time1 = time1/CLOCKS_PER_SEC; // rescale to seconds cout << " gpu = " << time1 << " sec."; // // compare results // compareResults(); // // and clean up // glDeleteFramebuffersEXT(1,&fb); free(data); glDeleteTextures(2,pingpongTexID); glDeleteTextures(1,&inputTexID); cgDestroyProgram(fragmentProgram); cgDestroyContext(cgContext); Seite: 57/64 General Purpose Computation on Graphics Processing Unit glutDestroyWindow (glutWindowHandle); return 0; } /** * Callback for Cg errors */ void cgErrorCallback(void) { CGerror lastError = cgGetError(); if(lastError) { printf(cgGetErrorString(lastError)); printf(cgGetLastListing(cgContext)); exit(ERROR_CG); } } /** * Sets up a floating point texture with NEAREST filtering. * (mipmaps etc. are unsupported for floating point textures) */ void setupTexture (const GLuint texID) { // make active and bind glBindTexture(texTarget,texID); // turn off filtering and wrap modes glTexParameteri(texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(texTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP); // define texture with floating point format glTexImage2D(texTarget,0,texInternalFormat,texSize,texSize,0,texFormat,GL_FLOAT,0); // check if that worked if (glGetError() != GL_NO_ERROR) { printf("glTexImage2D():\t\t\t [FAIL]\n"); exit (ERROR_TEXTURE); } else { printf("glTexImage2D():\t\t\t [PASS]\n"); } } /** * creates textures and populates the input texture */ void createTextures (void) { // pingpong needs two textures, alternatingly read-only and write-only, // input is just read-only glGenTextures (2, pingpongTexID); glGenTextures (1, &inputTexID); // set up textures setupTexture (pingpongTexID[readTex]); setupTexture (pingpongTexID[writeTex]); setupTexture (inputTexID); // attach pingpong textures to FBO glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, attachmentpoints[writeTex], pingpongTexID[writeTex], 0); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, attachmentpoints[readTex], pingpongTexID[readTex], 0); // check if that worked if (!checkFramebufferStatus()) { printf("glFramebufferTexture2DEXT():\t [FAIL]\n"); exit (ERROR_FBOTEXTURE); } else { texTarget, texTarget, Seite: 58/64 General Purpose Computation on Graphics Processing Unit } printf("glFramebufferTexture2DEXT():\t [PASS]\n"); } // transfer data vector to input texture glBindTexture(texTarget, inputTexID); glTexSubImage2D(texTarget,0,0,0,texSize,texSize,texFormat,GL_FLOAT,data); // check if something went completely wrong checkGLErrors ("createTextures()"); /** * Sets up GLUT, creates "window" (better put: valid GL context, since the window is never displayed) */ void initGLUT(int argc, char **argv) { glutInit ( &argc, argv ); glutWindowHandle = glutCreateWindow("MAXIMUM DEMO"); } /** * Sets up GLEW to initialise OpenGL extensions */ void initGLEW (void) { int err = glewInit(); // sanity check if (GLEW_OK != err) { printf((char*)glewGetErrorString(err)); exit(ERROR_GLEW); } } /** * Creates framebuffer object, binds it to reroute rendering operations * from the traditional framebuffer to the offscreen buffer */ void initFBOandViewport(void) { // create FBO (off-screen framebuffer) glGenFramebuffersEXT(1, &fb); // bind offscreen framebuffer (that is, skip the window-specific render target) glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb); // viewport for 1:1 pixel=texel mapping glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0.0, texSize, 0.0, texSize); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glViewport(0, 0, texSize, texSize); } /** * Sets up the Cg runtime and creates shader. */ void initCG(void) { // set up Cg cgSetErrorCallback(cgErrorCallback); cgContext = cgCreateContext(); fragmentProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT); cgGLSetOptimalOptions(fragmentProfile); // create fragment program fragmentProgram = cgCreateProgram (cgContext, CG_SOURCE, shader, fragmentProfile, "maximum", NULL); // load program cgGLLoadProgram (fragmentProgram); Seite: 59/64 General Purpose Computation on Graphics Processing Unit // and get parameter handle to input texture by name textureParam = cgGetNamedParameter (fragmentProgram, "texture"); } /** * Computes maximum on the CPU, compares results */ void compareResults () { // get GPU result (last render pass did pingpong "swap") float gpuResult; glReadBuffer(attachmentpoints[readTex]); glReadPixels(0, 0, 1, 1,texFormat,GL_FLOAT,&gpuResult); double time1=0.0, tstart; tstart = clock(); // start // calc on CPU float cpuResult = data[0]; for (int i=1; i<N; i++) if (data[i] > cpuResult) cpuResult = data[i]; time1 += clock() - tstart; // end time1 = time1/CLOCKS_PER_SEC; // rescale to seconds cout << " cpu = " << time1 << " sec."; // and print out results printf("--------------------------------------------------------------\n"); printf("GPU result:\t%f\n",gpuResult); printf("CPU result:\t%f\n",cpuResult); printf("--------------------------------------------------------------\n"); } /** * Performs the actual calculation. */ void performComputation(void) { //printf("--------------------------------------------------------------\n"); // enable fragment profile cgGLEnableProfile(fragmentProfile); // bind "maximum" program cgGLBindProgram(fragmentProgram); // // pass 1: read from inputTexture, first reduction step // idea: do not overwrite input during pingpong // // enable input texture cgGLSetTextureParameter(textureParam, inputTexID); cgGLEnableTextureParameter(textureParam); // set render destination glDrawBuffer (attachmentpoints[writeTex]); // calculate output region width and height int outputWidth = texSize / 2; // printf("Input size :\t%dx%d\n",texSize,texSize); // printf("Reduction step:\t%dx%d to %dx%d.\n",texSize, texSize, outputWidth, outputWidth); // perform computation and ping-pong output textures drawQuad(outputWidth,outputWidth); Seite: 60/64 General Purpose Computation on Graphics Processing Unit swap(); // // calculate number of remaining passes: // log_2 of current output width // int numPasses = (int)(log((double)outputWidth)/log(2.0)); // // perform remaining reduction steps // for (int i=0; i<numPasses; i++) { // input texture is read-only texture of pingpong textures cgGLSetTextureParameter(textureParam,pingpongTexID[readTex]); cgGLEnableTextureParameter(textureParam); // set render destination glDrawBuffer (attachmentpoints[writeTex]); // calculate output region width and height outputWidth = outputWidth / 2; //printf("Reduction step:\t%dx%d to %dx%d.\n",outputWidth*2, outputWidth*2, outputWidth, outputWidth); // perform computation and ping-pong output textures drawQuad(outputWidth,outputWidth); swap(); } // done, just do some checks if everything went smoothly. checkFramebufferStatus(); checkGLErrors("render()"); } /** * Renders w x h quad in top left corner of the viewport. */ void drawQuad(int w, int h) { glBegin(GL_QUADS); glVertex2f(0.0, 0.0); glVertex2f(w, 0.0); glVertex2f(w, h); glVertex2f(0.0, h); glEnd(); } /** * Checks for OpenGL errors. * Extremely useful debugging function: When developing, * make sure to call this after almost every GL call. */ void checkGLErrors (const char *label) { GLenum errCode; const GLubyte *errStr; if ((errCode = glGetError()) != GL_NO_ERROR) { errStr = gluErrorString(errCode); printf("OpenGL ERROR: "); printf((char*)errStr); printf("(Label: "); printf(label); printf(")\n."); } } Seite: 61/64 General Purpose Computation on Graphics Processing Unit /** * Checks framebuffer status. * Copied directly out of the spec, modified to deliver a return value. */ bool checkFramebufferStatus() { GLenum status; status = (GLenum) glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); switch(status) { case GL_FRAMEBUFFER_COMPLETE_EXT: return true; case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT: printf("Framebuffer incomplete, incomplete attachment\n"); return false; case GL_FRAMEBUFFER_UNSUPPORTED_EXT: printf("Unsupported framebuffer format\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT: printf("Framebuffer incomplete, missing attachment\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT: printf("Framebuffer incomplete, attached images must have same dimensions\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT: printf("Framebuffer incomplete, attached images must have same format\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT: printf("Framebuffer incomplete, missing draw buffer\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT: printf("Framebuffer incomplete, missing read buffer\n"); return false; } return false; } /** * swaps the role of the two y-textures (read-only and write-only) * Can be done smarter :-) */ void swap(void) { if (writeTex == 0) { writeTex = 1; readTex = 0; } else { writeTex = 0; readTex = 1; } } Seite: 62/64 General Purpose Computation on Graphics Processing Unit 14.4 Multiplikation mit RapidMind #include <rapidmind/platform.hpp> #include <time.h> using namespace rapidmind; int main() { //rapidmind initialisieren rapidmind::init(); double time1=0.0, tstart; //anzahl Berechnungen int durch = 100000; //valueXf X=anzahl int anzahl = 4; //Array länge int length =20000; //RapidMind für GPU optimieren, cell wäre zb auch möglich use_backend("glsl"); //input Vektoren für kernel Array< 1 , Value4f> input1( 10000 ); Array< 1 , Value4f> input2( 10000 ); //Inputarrays cpu float inputcpu1[10000][4]; float inputcpu2[10000][4]; //pointer auf Vektoren legen float * input_data1 = input1.write_data(); float * input_data2 = input2.write_data(); //Arrays mit Daten füllen for ( int i = 0 ; i < length ; ++i ) { for ( int j = 0 ; j< anzahl ; ++j ){ input_data1[i] = (i,i*j*5,j*6,j*7); input_data2[i] = (i * 2,j,j*4,j*i); inputcpu1[i][0] = i; inputcpu1[i][1] = i*j; inputcpu1[i][2] =j*6; inputcpu1[i][3] =j*7; inputcpu2[i][0] = i*2; inputcpu2[i][1] = j; inputcpu2[i][2] =j*4; inputcpu2[i][3] =j*i; } } //Berechnung CPU Anfang double time2=0.0, tstart2; tstart2 = clock(); float result[10000][4]; for ( int k = 0; k < durch; ++k ){ for ( int i = 0; i < length ; ++i ) { Seite: 63/64 General Purpose Computation on Graphics Processing Unit } for(int j =0; j<anzahl;j++){ result[ i ][j] = inputcpu1[ i ][j] * inputcpu2[ i ][j]; } } time2 += clock() - tstart2; time2 = time2/CLOCKS_PER_SEC; std::cout << " cputime = " << time2 << " sec.\n"; //Berechnung CPU Ende //Ouput Vektor vom kernel Array< 1 , Value4f > output; //Kernel Program prg = RM_BEGIN { In<Value2f> a; In<Value2f> b; Out<Value2f> c; c[0] = a[0] * b[0]; c[1] = a[1] * b[1]; c[2] = a[2] * b[2]; c[3] = a[3] * b[3]; } RM_END; //Beginn Berechnung GPU tstart = clock(); for ( int k = 0; k < durch ; ++k ){ output = prg(input1, input2); } //Ende Berechnung GPU //Zeit Ausgabe time1 += clock() - tstart; time1 = time1/CLOCKS_PER_SEC; std::cout << " gputime = " << time1 << " sec.\n"; } Seite: 64/64