Matrix Multiplikation auf der Grafikkarte mit .NET und Cudafy

Die Parallelisierung von Algorithmen und Programmteilen hat mir schon immer Spaß gemacht und ich habe mir sogar zu Hause einen Rechencluster gebaut, mit dem ich experimentieren konnte. In der aktuellen dotnetpro Ausgabe (07/2013) beschreibt Bernd Marquardt, bei dem ich auf der Parallel 2012 Konferenz einen .NET TPL Workshop mitmachen durfte, in einem Artikel die Parallelisierung von Algorithmen mit AMP unter C++, die dann auf der Grafikkarte ausgeführt werden. Leider muss man hier immer noch den Umweg über C++ gehen, aber glücklicherweise gibt es für .NET mit Cudafy ein Framework, mit dem man diesen Umweg nicht gehen muss. Cudafy unterstützt, neben dem namensgebenden CUDA von Nvidia, auch OpenCL, sodass man damit plattformübergreifende Parallelisierungen vornehmen kann. Ich habe in den letzten Tage ein wenig mit Cudafy herumgespielt und möchte hier einmal ein recht simples Beispiel der Matrixmultiplikation beschreiben.

Um Cudafy zu verwenden, benötigen wir zuerst ein GPGPU Objekt, das unsere zu verwendende Hardware repräsentiert. Cudafy unterstützt dabei sowohl CUDA von Nvidia als auch OpenCL der Khronos Group. Ich habe mich in meinem Beispiel für OpenCL entschieden, denn so konnte ich sowohl die CPU als auch die beiden Grafikkarten in meinem System (Intel und Nvidia) zur Berechnung auswählen. Folgender Codeschnippsel zeigt die Instanziierung des GPGPU Objektes, ein ganz rudimentäres Exception Handling (jaja, ich weiss ;-)) und die Instanziierung meiner Matrixmultiplikationsklasse.

 

Wichtigster Grundstein meiner Entwicklungsumgebung ist eine abstrakte Basisklasse, von denen meine implementierten Algorithmenklassen erben. Dieser Schritt ist nicht unbedingt notwendig, macht aber in meinem Fall Sinn, da ich beim „Herumspielen“ nicht nur die Matrixmultiplikation implementiert habe, sondern auch andere Algorithmen.

Im Konstruktor muss ein GPGPU Objekt übergeben werden, das die ausgewählte Client Hardware repräsentiert (die Instanziierung wurde bereits weiter oben beschrieben). Die Execute Methode enthält nun den Code, der benötigt wird, m einen Kernel mit Cudafy für die zu verwendende Client Hardware vorzubereiten (Aufruf von Cudafy(…) und LoadModule(..)). Außerdem wird noch eine Zeitmessung mit der allseits bekannten Stopwatch() durchgeführt. In diesem Block wird die innerhalb der Algorithmenklasse zu implementierenden Methode OnExecute() aufgerufen, sodass nach Ausführung die Dauer der Berechnung (inklusive der Übertragungszeit der Daten zur berechnenden Hardware) in der Konsole ausgegeben wird.

Die Klasse zur Matrixmultiplikation sieht dann so aus.

Wie bereits erwähnt, ist diese Klasse während meiner Tests entstanden, sodass ich in diesem Beispiel Testdaten verwenden, die ich bei Aufruf des Konstruktors generiere. Dem Konstruktor wird in diesem Beispiel neben dem GPGPU Objekt auch noch die Dimension der Matrix mit übergeben. Die wichtigsten Teile des Codes stecken aber in der Methode OnExecute() und Multiply(…). Die Methode OnExecute() ist dafür verantwortlich, den Speicher auf der Client Hardware anzulegen und die Arrays vom Host auf den Client zu übertragen. Dies wird hier durch die Methode CopyToDevice(…) vorgenommen. Das Ergebnisarray wird nicht auf die Client Hardware kopiert, sondern nur der Speicher reserviert, da es zu Beginn sowieso leer ist und deshalb keine Daten benötigt werden. Die Launch(…) Methode startet dann die Berechnung auf der Client Hardware. Den Aufruf werde ich hier nicht weiter erläutern, mehr zur Launch(…) Methode findet man aber in der Cudafy Dokumentation. Nach dem Aufruf wird das Ergebnis von der Hardware wieder auf den Host übertragen und der belegte Speicher auf dem Client wieder freigegeben. Das war’s auch schon.

Die Client Implementierung des Algorithmus steckt in der mit dem „Cudafy“ Attribut markierten Methode Multiply(…). Die Methodenparameter sind GThread (wird standardmäßig von Cudafy hinzugefügt und enthält Statusinformationen), die Dimension der Matrix und die Matrizen für die Matrixmultiplikation. In dieser Methode wird zuerst die x und y Position innerhalb der Matrix errechnet. Die Berechnung orientiert sich hier an den Blöcken, die über die Launch Methode mit übergeben werden. Genauere Informationen dazu findet ihr in den Grundlagen zum Thema OpenCL und CUDA. Im nächsten Schritt wird vorsichtshalber noch überprüft, dass x und y auch wirklich innerhalb der Matrix liegen und anschließend die Matrixmultiplikation durchgeführt, die dann im Ergebnisarray gespeichert wird.

Die Methode CpuCalculation() habe ich implementiert, um die Geschwindigkeit einer einzelnen CPU mit der Ausführung auf einem Client zu vergleichen. Auf meinem Rechner war eine Nvidia GT650M bei einer Dimension von 256 bereits 3x so schnell wie ein Kern meiner i7 CPU. Bei einer Dimension von 512 war sie schon um den Faktor 7,5 schneller.

Abschließend kann man sagen, dass sich die Nutzung einer Grafikkarte als Client bei einem hinreichend großen Problem lohnt und man damit eine große Beschleunigung erreichen kann. Dank Cudafy funktioniert das alles auch in .NET ohne den Umweg über C++. In meinem Beispiel bleibt außerdem noch viel Platz für Optimierungen, da ich das Beispiel möglichst simpel halten wollte – und das gilt sowohl für die Berechnung der Matrixmultiplikation auf der CPU (hier wäre bspw. eine Implementierung mit der TPL sinnvoll, die dann alle Kerne nutzt) als auch auf der GPU.

Onboard-Grafikchips verschwinden bis 2013

Wie man in letzter Zeit z.B. bei heise.de lesen konnte, wird laut einer Studie von Jon Peddie Research (JPR) bis 2013 der Marktanteil der ausgelieferten Onboard-Grafikchips auf unter 1 Prozent sinken. Der Grund dafür ist, dass die Hersteller in Zukunft dazu übergehen werden, einen Kern der CPU für die Grafikausgabe zu verwenden.

Im Prinzip deckt sich das mit den Erwartungen, die ich in Zukunft an die Grafikchips bzw. die CPUs habe. Während anfangs die CPUs immer neue Rekorde bei den Geschwindigkeiten aufgestellt haben, ist hier der Trend mittlerweile zur Parallelisierung immer größer geworden. Bei den GPUs war es eigentlich umgekehrt. Man hatte hier spezielle Kerne, die parallel arbeiten und geht mittlerweile dazu über auch spezielle Rechenaufgaben mit CUDA oder Stream zu übernehmen und mehr als nur Grafikoperationen zu ermöglichen. Somit nähern sich CPU und GPU immer weiter an. Im Prinzip erwarte ich, dass es in Zukunft wohl so sein wird, dass wir in allen Rechnern Mehrkernprozessoren haben, die auch gleichzeitig die Grafikbeschleunigung übernehmen. Im Prinzip wird dabei auch eine Art Load-Balancing möglich, da man z.B. nur bei 3D Anwendungen viele Kerne braucht, die man bei reiner 2D Darstellung für Rechenaufgaben zur Verfügung stellen kann. Insgesamt also ein höhere Flexibilität.
Dass die Industrie bereits in diese Richtung geht, sieht man an der derzeitigen Entwicklung bei Intel. Intel arbeitet derzeitig an einer neue Grafikkarte, der Larrabee, die auf einem Cluster von Pentium Prozessoren basiert. Ich bin jedenfalls mal gespannt wie es in Zukunft weiter gehen wird.