Bis jetzt wurde der OpenCL-Kernel behandelt, also der Code, der auf der GPU (bzw. auf einem OpenCL-Device) ausgeführt wird. Aber davor muss noch ein Host-Programm (CPU-Code) geschrieben werden, um die GPU zu steuern und zu nutzen. Dieses Host-Programm findet und initialisiert die GPU(s), sendet die Daten und den Kernel-Code an die GPU, weist die GPU an, die Ausführung zu starten, weiß, wann die Ergebnisse bereit stehen, und liest sie aus der GPU aus.
OpenCL vereinfacht zudem die Ausführung von Code auf mehreren Geräten derselben Maschine. So kann zum Beispiel eine Implementierung die CPU nutzen, eine weitere die GPU(s) und wiederum eine andere weitere Beschleuniger, die ebenfalls mit an Bord sind. Deshalb sollte der erste Schritt im OpenCL-Programm darin bestehen, herauszufinden, welche OpenCL-Geräte in der aktuellen Maschine vorhanden sind, und eines davon auszuwählen (Listing 2).
Innerhalb einer Plattform wird ein Kontext generiert (typischerweise einer, für hochentwickelte Anwendungen können es auch mehrere sein). Den Kontext kann man sich als ein in sich geschlossenes Universum vorstellen, das alle anderen OpenCL-Ressourcen enthält. Alle folgenden Schritte finden innerhalb dieses generierten Kontextes statt.
Im nächsten Schritt (Listing 3) werden die in diesem Kontext vorhandenen OpenCL-Geräte abgefragt und erfasst, in diesem Fall GPUs. Auch muss das OpenCL-Programm aus einem oder mehreren Kernels compiliert und erstellt werden (Listing 4), um auf den ausgewählten Geräten zu laufen. Schließlich werden OpenCL-Puffer erzeugt, die die Daten vor und nach der Ausführung auf der GPU speichern. Für das Kernel-Beispiel werden drei Puffer erstellt, die mit den Vektoren a, b und c korrespondieren.
Jetzt, wo die GPU sowie der Code, der auf der GPU ausgeführt werden soll, initialisiert sind, können die Befehle an die GPU gesendet werden (Datenübermittlung, Kernel ausführen, Ergebnisse erfassen etc.). Aus Effizienzgründen ist es sinnvoll, dass das Host-Programm auf der CPU diese Befehlsaufrufe asynchron (non-blocking) ausführt. Das bedeutet, dass das Host-Programm einen Befehl (z.B. zur Datenübermittlung) an die GPU senden kann und nicht auf die Abarbeitung dieses Befehls (also die eigentliche Übermittlung der Daten) warten muss. Auf diese Weise kann der Host zu anderen Aufgaben übergehen und auch weitere Befehle an die GPU senden.
Ab die Post
OpenCL bietet ein komfortables Konstrukt, um Befehle an die GPU zu übermitteln: die Command Queue. Für eine einfache Benutzung gibt es Blocking- und Non-Blocking-Varianten für eine Reihe an Befehlen, die an die Command Queue übergeben werden können. Zudem gibt es Mechanismen, mit denen sich abfragen lässt, ob ein Befehl (oder ein Ereignis) abgearbeitet ist und auch Wege, damit das Host-Programm auf die Abarbeitung aller vorhergehenden OpenCL-Befehle wartet. Je nach den Eigenschaften der Command Queue können die übermittelten Befehle in Reihenfolge der Übermittlung (in-order) oder nicht (out-of-order) ausgeführt werden.
All diese Schritte im Host-Programm dienten bisher dazu, die Aktivitäten auf der GPU zu initialisieren und ein oder mehrere Programme auszuführen. Man hat eine Plattform gefunden, den Kontext erzeugt, ein OpenCL-Gerät sowie ein oder mehrere Kernels initialisiert sowie Datenpuffer und eine Command Queue generiert. In den meisten Anwendungen müssen diese Schritte nur einmal ausgeführt werden, bevor man dazu übergehen kann, den Code auf der GPU auszuführen.
Dazu werden zuerst die Daten der Vektoren a und b an die jeweiligen OpenCL-Puffer übermittel. Genauer gesagt werden die Befehle übertragen (oder queued), um die Datenübermittlung an die Command Queue zu starten. Im zweiten Schritt werden die Argumente aufgesetzt, die der OpenCL-Kernel nutzen soll. Im Beispiel werden die OpenCL-Puffer mit den Kernel-Parametern a, b und c verknüpft.
Im nächsten Schritt wird der Kernel ausgeführt. Eine wichtige Information, die an diesem Punkt fehlt, ist, an wie vielen Items der zusätzliche Kernel arbeiten soll. Genauer gesagt muss die gesamte Anzahl der Work Items spezifiziert werden. In diesem Beispiel wird die Vektorgröße angegeben mit sizeOfArray, was der Gesamtzahl der Work Items entspricht. Nach der Übermittlung des Kernels erfolgt der Befehlsaufruf, den Wert aus dem openCL-Buffer oc an den Vektor c zu übergeben. Werden die vorherigen Befehlsaufrufe an die Command Queue als non-blocking übermittelt, ist es nötig, einen Call zu setzen, um auf die Abarbeitung aller Aktionen der Command Queue zu warten. An diesem Punkt ist dann das korrekte Ergebnis in den Zielvektor geschrieben.
Viel Rechenleistung, wenig Watt
Ein breites Wissen um die Vorteile von OpenCL ist die Grundvoraussetzung für eine breite Adaption des GPGPU-Computings im Embedded-Markt. Viele Algorithmen korrelieren hervorragend mit der GPGPU-Architektur und zeigen im Vergleich zu traditionellen Multicore-CPU-Umsetzungen deutliche Leistungszuwächse. Die Verarbeitung von Berechnungen auf Grafikprozessoren liefert ansprechende GFLOPS pro Watt bei attraktiven GFLOPS pro Dollar und bringt neue Möglichkeiten für größen-, gewichts- und leistungssensitive Embedded-Applikationen.