Als Multicore-Prozessoren erstmals auf den Markt kamen, gab es kaum praktikable Methoden um Programme zu schreiben, die auf mehrere Cores verteilt oder möglicherweise auch über viele Bausteine hinweg ablaufen. Es gab keinen Standard für die plattformübergreifende Programmierung für die parallele Verarbeitung, der sich auch durchgesetzt hätte.

Im Jahr 2008 legte Apple der Khronos-Group einen vielversprechenden Vorschlag vor: Der erste Entwurf einer Open-CL-Spezifikation (Open Computing Language) war als plattformübergreifender paralleler Programmierstandard gedacht. Die Khronos-Group ist ein Industriekonsortium aus Apple, IBM, Intel, AMD, Nvidia, Altera und vielen anderen Mitgliedern und verantwortlich für die Definition aller Open-CL-Spezifikationen. Die aktuelle Version trägt die Nummer 1.2.

Auf einen Blick

Um spezielle mathematische Aufgaben schneller berechnen zu können, stellen DSPs eigene VLIW-Kommandos bereit, während GPU und FPGA vor allem dank enormer Parallelisierung aufs Tempo drücken. Klassischerweise erfordern alle diese Architekturen auch eigene Programmiertechniken. Mit Open-CL und dessen Aufteilung in Kernel und Host-Treiber lässt sich ein identisches Programm für jede Architektur nutzen.

Die Grundidee des Open-CL-Standards ist es, die Standard-Programmiersprache C um spezielle Anweisungen zu erweitern, die die Ausführung von parallelisierten Befehlen und Algorithmen auf Multicore-CPUs, GPUs oder FPGAs ermöglichen. Eine weitere Zielsetzung von Open-CL lautet, den Code mit minimaler Modifikation auf verschiedene Zielplattform portieren zu können. Dieser Beitrag betrachtet jedoch die Anwendung von Open-CL in FPGAs.

Hohe Abstraktionsebene

In der FPGA-Welt ermöglicht es Open-CL, parallel ausgeführte Algorithmen auf einer wesentlich höheren Abstraktionsebene zu entwickeln als mit den üblichen Hardware-Beschreibungssprachen wie VHDL oder Verilog. Bestehende High-Level-Synthese-Tools, die das Programmieren auf einer hohen Abstraktionsebene ermöglichen, leiden an einem Grundproblem: Sie versuchen, ein sequenzielles C-Programm in eine parallele HDL-Implementierung umzusetzen. Die Schwierigkeit besteht nicht so sehr in HDL, sondern im Erzeugen des Parallelismus auf Thread-Ebene.

Die Leistungsfähigkeit von FPGAs beruht von Natur aus auf der parallelen Ausführung von Aufgaben. Daher ist es besonders interessant mittels einer Hochsprache und eines entsprechenden Compilers sicherzustellen, dass der maximale Parallelismus gewährleistet ist. Der Open-CL-Standard löst diese Probleme, indem er Programmierern erlaubt, den Parallelismus explizit zu spezifizieren und zu steuern. Open-CL ist somit auf die optimale Nutzung der parallelen Strukturen der FPGAs ausgelegt.

Bild 1: Der Blick in ein Open-CL-Programm zeigt, dass Anwendungen aus einem parallelisierten Kernel und einem generischen Host-Teil bestehen.

Bild 1: Der Blick in ein Open-CL-Programm zeigt, dass Anwendungen aus einem parallelisierten Kernel und einem generischen Host-Teil bestehen.Altera

Die zwei Teile eines Open-CL-Programms

Open-CL-Anwendungen bestehen aus zwei Teilen. Das in Standard-C/C++ geschriebene Host-Programm läuft auf jeder Art von Mikroprozessor. Dieser Prozessor kann ein Embedded-Soft-Prozessor in einem FPGA, ein Hard-ARM-Prozessor oder ein externer x86-Prozessor sein (Bild 1).

Open-CL lagert rechenintensive Funktionen, die auf den Standard-CPUs sehr viel Zeit oder Verarbeitungsbandbreite benötigen, auf einen parallel verarbeitenden Baustein aus. Eine GPU oder ein FPGA beschleunigen diese Teile; die zu beschleunigende Funktion heißt Open-CL-Kernel. Diese Kernels sind in Standard-C geschrieben und mit Konstrukten versehen, die die Umsetzung auf parallele Prozesse beschreiben und die Speicherhierarchie festlegen.

Bild 2: Die Vektoraddition läuft dank Open-CL auf einem FPGA. Durch die massive Parallelität liegt das Resultat wesentlich schneller vor als in einer CPU.

Bild 2: Die Vektoraddition läuft dank Open-CL auf einem FPGA. Durch die massive Parallelität liegt das Resultat wesentlich schneller vor als in einer CPU.Altera

Vektoraddition

Das Beispiel in Bild 2 führt eine Vektoraddition der beiden Arrays A und B durch. Die Ergebnisse werden in ein Ausgangs-Array geschrieben. Parallele Threads arbeiten an jedem Vektorelement, so dass das Ergebnis wesentlich schneller bereitsteht. FPGAs zeigen hier große Vorteile, da sie diesen Vorgang sehr fein granulieren. Das Host-Programm greift auf die Standard-Open-CL-APIs zu, über die es Daten an das FPGA überträgt, den Kernel-Prozess auf dem FPGA steuert und die resultierenden Daten wieder zurückliest.

Im Gegensatz zu CPUs und GPUs, in denen parallele Threads auf verschiedenen Cores ablaufen können, verfolgen FPGAs eine andere Strategie. Kernel-Funktionen können in dedizierte und sehr tiefe Hardware-Pipelines umgewandelt werden, die von Grund auf multithreaded sind und zeitlich parallel ablaufen. Jede dieser Pipelines lässt sich vielfach replizieren, um noch mehr Parallelität zu bieten, als es mit einer einzigen Pipeline möglich wäre.

Der Open-CL-Compiler von Altera übersetzt einen Open-CL-Kernel in Hardwarefunktionen, indem er für jede Operation eine eigene Logikverschaltung erstellt. Diese Schaltkreise verbindet der Compiler entsprechend dem gewünschten Datenfluss im Kernel miteinander. Im Beispiel der Vektoraddition führt die Umsetzung in Hardware zu einer einfachen Feed-Forward-Pipeline: Die Daten aus den Arrays A und B implementiert der Compiler als Load-Units, die mittels Adressen aus dem externen Speicher die entsprechenden Daten lesen. Die gelesenen Werte speisen sie direkt in einen Addierer, welcher eine Fließkomma-Addition der beiden Werte durchführt. Das Ergebnis sendet er direkt an eine Store-Unit, welche die Summe zurück in den externen Speicher schreibt.

Bild 3: Jeder der drei Pipeline-Stufen Load, Sum und Store bearbeitet einen eigenen Thread. Bei jedem Takt schiebt sich der nächste Thread in die Pipeline und am anderen Ende steht ein neues Resultat bereit.

Bild 3: Jeder der drei Pipeline-Stufen Load, Sum und Store bearbeitet einen eigenen Thread. Bei jedem Takt schiebt sich der nächste Thread in die Pipeline und am anderen Ende steht ein neues Resultat bereit.Altera

Flotte Pipeline

Das wichtigste Konzept hinter dem Open-CL-zu-FPGA-Compiler ist der Pipeline-Parallelismus. Als Beispiel dient Bild 3. Der Einfachheit halber sei hier angenommen, dass der Compiler drei Pipeline-Stufen für den Kernel erstellt hat: Load, Sum, Store. Im ersten Taktzyklus wird Thread 0 in die zwei Load-Units getaktet, die dann die ersten Datenelemente der Arrays A und B aus dem Speicher abgerufen. Im zweiten Taktzyklus wird Thread 1 in die Load-Units getaktet, während Thread 0 sein Lesen vom Speicher beendet und die Ergebnisse in den Registern hinter den Ladeeinheiten gespeichert hat.

Im dritten Zyklus taktet Thread 2 in die Load-Units, Thread 1 holt sich seine Daten aus dem Speicher und Thread 0 schreibt die Summe der beiden Werte zurück in den Speicher. Es ist offensichtlich, dass im eingeschwungenen Zustand alle Teile der Pipeline ständig aktiv sind, wobei jede Stufe einen anderen Thread verarbeitet. Somit entsteht mit jedem Systemtakt ein Ergebnis.

Bild 4: In einem Open-CL-System arbeiten viele Kernel gemeinsam parallel. Sie können sich gegenseitig ihre Daten übermitteln und mit internem sowie externem Speicher arbeiten.

Bild 4: In einem Open-CL-System arbeiten viele Kernel gemeinsam parallel. Sie können sich gegenseitig ihre Daten übermitteln und mit internem sowie externem Speicher arbeiten.Altera

Das komplette System

Bild 4 zeigt eine High-Level-Darstellung eines kompletten Open-CL-Systems mit mehreren Kernel-Pipelines und Verschaltungen, welche diese Pipelines mit Datenschnittstellen außerhalb des Bausteins verbinden. Neben der Kernel-Pipeline erstellt Alteras Open-CL-Compiler auch Schnittstellen zu externem und internem Speicher. Jede Pipeline ist über einen globalen Speicherbus verbunden, der die gleichzeitigen Speicherzugriffe steuert und mit einer Gruppe von externen DDR-Speicher-DIMMs verbunden ist. In ähnlicher Weise laufen Open-CL-Zugriffe auf lokalen Speicher über eine spezielle Verbindung zu On-Chip-M9K-RAM. Diese Verschaltungen garantieren koordinierte Speicherzugriffe und damit eine effiziente Ausnutzung der maximalen Systemtaktrate.

Mit Open-CL können die Programmierer von Standard-CPUs recht einfach die Vorteile von FPGAs nutzen, ohne dass sie ihr gewohntes Entwicklungsumfeld verlassen müssen. Open-CL ist wie jeder andere µC-Compiler in den Entwicklungsablauf eingebettet: nach einer Konzeptphase, dem Programmieren des Algorithmus in einer Hochsprache wie C und dem Einsatz eines automatischen Compilers, wird der Maschinencode für das FPGA erzeugt. Damit ermöglicht Open-CL FPGA-Designs, ohne sich in die herkömmlichen HDL-Design-Methoden einarbeiten zu müssen (Bild 5).

Bild 5: Aus der Sicht des Programmierers versteckt Open-CL die komplexe Programmierung von FPGAs. Stattdessen bleibt der Entwickler bei C und bei seinen gewohnten Tool-Ketten.

Bild 5: Aus der Sicht des Programmierers versteckt Open-CL die komplexe Programmierung von FPGAs. Stattdessen bleibt der Entwickler bei C und bei seinen gewohnten Tool-Ketten.Altera

Lästige Teile automatisiert

Dieser Ansatz steht im Gegensatz zu herkömmlichen FPGA-Design-Methodologien bei denen der Entwickler Takt für Takt die Hardwareblöcke erstellen muss, um den Algorithmus zu implementieren. Der Open-CL-Compiler automatisiert komplexe Designschritte wie die Definition von Datenpfaden, Zustandsmaschinen zur Steuerung dieser Datenpfade, den Anschluss an Low-Level-IP-Cores über System-Level-Tools (etwa SOPC Builder, Platform Studio) und die Handhabung von Timing-Closure-Problemen.

Damit können sich Entwickler auf ihren Algorithmus konzentrieren statt mühsam die Details des Hardware-Designs auszuarbeiten. Mit diesem Entwicklungsablauf ist es auch möglich, auf neue FPGAs mit höherer Performance und Logikkapazität zu wechseln: Der Open-CL-Compiler verwendet das einmal geschriebene Programm und setzt es auf das neue FPGA um.