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