Software-Entwicklung Grafikprozessor zweckentfremdet

Moderne Grafikprozessoren sind frei programmierbare, massiv parallele Rechenmaschinen. Das weckt Begehrlichkeiten, denn diese Leistung wäre auch für andere Aufgaben als Grafik gut verwendbar. Die Programmiersprache OpenCL ebnet den Weg zur Nutzung der Grafik-Engine als universelles Rechenwerk.

Was die Nutzung des Grafikprozessors für allgemeine Rechenaufgaben so attraktiv macht, ist ihre extrem hohe Gleitkomma-Leistung. Damit Embedded-Systeme von diesem Leistungszuwachs profitieren können, müssen allerdings drei Anforderungen erfüllt sein: eine geringe Leistungsaufnahme, parallele Algorithmen sowie im Idealfall offene Standards. Die erste Anforderung ergibt sich daraus, dass bei vielen Embedded-Applikationen die Grenzwerte für die Thermal Design Power (TDP) deutlich geringer gesteckt sind, als dies mit aktuellen Desktop-Grafikkarten realisierbar wäre. Gründe hierfür sind oft umgebungsbedingte Beschränkungen hinsichtlich Größe, Gewicht und Leistungsaufnahme vieler Embedded-Applikationen.

Für diesen Bedarf bietet AMD die Radeon E6760 an, eine Embedded-GPU mit 480 Stream-Prozessoren bei 16,5 GFLOPS pro Watt bei einer TDP von nur 35 Watt.

Um diese Rechenleistung einfach zu nutzen, gibt es mit OpenCL, der „Open Computing Language“, einen offenen Standard, der die parallele Programmierung von GPUs und anderen Prozessoren wie CPUs, FPGAs, DSPs usw. ermöglicht. Er wurde von einem Industrie-Konsortium geschaffen, zu dem viele Chip-Hersteller, Software-Firmen und Forschungseinrichtungen zählen. Mit zunehmender Reife hat sich OpenCL zur idealen Programmierschnittstelle für Code entwickelt, der über unterschiedliche Hardware und Betriebssysteme portierbar ist.

In gewisser Weise kann man OpenCL als eine Form der Virtualisierung betrachten. Denn OpenCL-Programme sind - ähnlich wie auch Java-Code - plattformneutral. Erst zur Laufzeit werden die Programme für die Ziel-Hardware compiliert. Auch ein Pre-Compiling durch den OpenCL-Compiler ist möglich, um den Ablauf zu beschleunigen. Die OpenCL-Sprache stützt sich auf die Spezifikation von ISO C99, mit einigen Einschränkungen und Erweiterungen. Erweiterungen betreffen Typdefinitionen und Befehle für Vektorverarbeitung, besseren Zugriff auf Bilddaten und die Bezeichner für Adressbereiche. Einschränkungen bestehen insofern, als OpenCL keine Funktionszeiger, Bitfelder und keine Rekursion kennt.

OpenCL gliedert ein System hierarchisch, von der „Plattform“ bis zum Rechenwerk („Processing Unit“). Bild 1 zeigt diese Hierarchie und die für die einzelnen Elemente verwendeten Bezeichnungen. Ein „Host“ ist in diesem Zusammenhang ein beliebiger Computer mit CPU und einem Standard-Betriebssystem. OpenCL-Geräte („Devices“) können Grafik- oder digitale Signalprozessoren ebenso sein wie eine Multicore-CPU. Jedes OpenCL-Gerät besteht aus einem oder mehreren „Compute Units“, worunter die Rechenkerne oder Cores zu verstehen sind.

Die Compute Units sind nochmals unterteilt in Rechenwerke („Processing Elements“), die Befehle ausführen. SIMD-Befehle (Single Instruction, Multiple Data) werden üblicherweise auf einer CPU ausgeführt, während SPMD-Befehle (Single Program, Multiple Data) einen Vektorprozessor wie eine GPU oder eine Grafikeinheit in einer CPU benötigen.

Korrespondierend mit den Ebenen und Unterebenen der OpenCL-Architektur gibt es ein Plattform-API und ein Laufzeit-API. Mittels des Plattform-APIs wird das System auf die Existenz vorhandener OpenCL-Devices abgefragt. Auf dieser Ebene richtet der Entwickler auch die Arbeitsumgebungen („Context“) für das oder die OpenCL-Gerät(e) ein, vergibt Arbeitsaufträge an sie und organisiert den Datentransfer von und zu diesen Geräten. Das Laufzeit-API enthält die Programmierschnittstellen für alles, was sich innerhalb eines Kontexts abspielt, wie z.B. die Verwaltung von Command Queues, Speicher- und Kernel-Objekten. In der OpenCL-Nomenklatur sind Kernels die eigentlichen OpenCL-Programme, die auf einem oder mehreren Geräten zum Ablauf gebracht werden können.

Alles auf einmal statt Stück für Stück

Grafikprozessoren sind besonders gut für die parallele Datenverarbeitung geeignet, insbesondere wenn es um ähnliche Berechnungen für eine große Menge von Daten geht (auch datenparallele Berechnungen genannt). Beispiel: Eine einfache elementweise Addition von zwei Vektoren a und b, deren Ergebnis in den Vektor c geschrieben wird. Anstatt jeweils ein Paar von Elementen zu addieren, wie es CPU-Code handhabt, kann man OpenCL dazu verwenden, viele Additionen parallel auf der GPU durchzuführen.

Listing 1 zeigt einen typischen Code-Schnipsel, um die Addition auf einer Single-Core-CPU durchzuführen, sowie das OpenCL-Pendant, welches dieselbe Addition auf der GPU durchführt.

Der Befehl für jedes Element i wird Work Item genannt. Konzeptionell werden alle Work Items in einem Programm parallel ausgeführt. Man kann nicht sagen, ob das Work Item i = x vor, gleichzeitig oder nach dem Work Item i = y ausgeführt wird. Allerdings weiß man, dass auf GPUs hunderte und sogar tausende von Work Items gleichzeitig in der Ausführung sein können.

OpenCL bietet allerdings einen Weg, Sätze von Work Items in Arbeitsgruppen zusammenzufassen. In der Regel können Work Items sich nicht untereinander synchronisieren oder Daten austauschen, nur Work Items innerhalb der gleichen Arbeitsgruppe können das. Das ermöglicht es, OpenCL-Programme zu schreiben, die deutlich intelligenter sind als dieses Beispiel. Solche Beispiele würden allerdings den Umfang dieses Artikels sprengen.