13. Mai 2019
Lesedauer 8 Min.
CUDA mit C#
Vollgas mit der GPU, Teil 3
Mit der Bibliothek Hybridizer C#-Code auf der GPU starten.

Nachdem in den ersten beiden Teilen dieser Artikelserie [1, 2] erläutert wurde, wie man mit C/C++ und Python auf der Grafikkarte (GPU) rechnen kann, soll dieses Mal die Programmiersprache C# ins Rennen gehen.In allen Beispielfällen dieses Artikels wird die CUDA-Bibliothek von Nvidia verwendet. Zudem wird die Bibliothek Hybridizer von Altimesh vorgestellt, die es erlaubt, den Code für die schnellen Rechenfunktionen der GPU direkt mit C# zu formulieren.Grundsätzlich ist es möglich, mit C# über den Interoperabilitätsmechanismus des .NET Frameworks auf die Grafikkarte im Rechner zuzugreifen. Die Optionen reichen von P/Invoke (DllImportAttribute) bis hin zu Wrapper-Klassen, die in C# instanziert und benutzt werden können. In beiden Fällen müssen allerdings größere Teile des Codes in C/C++ erstellt werden.Einfacher geht es hingegen mit der Bibliothek Hybridizer. Sie erlaubt es, C#-Code direkt auf der GPU auszuführen. Hybridizer wird in mehreren Versionen angeboten. Die hier eingesetzten Hybridizer Essentials [3] können kostenlos im akademischen und studentischen Bereich benutzt werden. Hobbyprogrammierer können ebenfalls eine kostenlose Version bekommen. Für professionelle Nutzer liegt der Preis aktuell bei 200 Euro pro Nutzer und Rechner.Der Rechner braucht zudem eine CUDA-taugliche Nvidia-Grafikkarte (mehr dazu in [1]), das CUDA-SDK [4] in einer Version größer oder gleich 8.0 sowie Microsoft Visual Studio ab Version 2012.Bei der kostenlosen Installation von Hybridizer wird einfach der Download installiert. Danach kann man Visual Studio starten und im Menüpunkt Hybridizer den Befehl License Settings aufrufen. Dort wählen Sie die gewünschte Version aus und erhalten eine E-Mail, welche die erforderliche Subscription-ID enthält. Diese ID müssen Sie dann im geladenen Dialog eintragen. Die kostenlose Lizenz gilt drei Monate und kann dann erneuert werden.In den folgenden Beispielprogrammen werden das CUDA-SDK 9.1 und Microsoft Visual Studio 2015 benutzt.
Der erste Einstieg
Im ersten Beispiel soll etwas Integer-Mathematik ausgeführt werden. Nach der Installation von Hybridizer stehen in Visual Studio mehrere Projektvorlagen im Ordner Visual C# | Altimesh zur Auswahl. Wichtig ist es, die korrekte Vorlage für die installierte CUDA-Version zu benutzen, damit das Programm korrekt ausgeführt werden kann, weil sonst die passenden CUDA-Bibliotheken nicht gefunden werden.Mithilfe der Vorlagen erstellen Sie ein Startprogramm – siehe Listing 1. In der Main-Methode des Listings werden zunächst zwei Integer-Arrays deklariert und mit Daten gefüllt. In diesem einfachen Beispiel enthalten die Arrays nur fünf Datenelemente.Listing 1: Ein erstes Beispiel
<span class="hljs-keyword">using</span> Hybridizer.Runtime.CUDAImports; <br/><span class="hljs-keyword">using</span> System; <br/><span class="hljs-keyword">using</span> System.Threading.Tasks; <br/><br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">HybridizerSample1</span> <br/>{ <br/> <span class="hljs-keyword">class</span> <span class="hljs-title">Program</span> <br/> { <br/> [EntryPoint] <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Run</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> N, <span class="hljs-keyword">int</span>[] a, <span class="hljs-keyword">int</span>[] b</span>) </span><br/><span class="hljs-function"> </span>{ <br/> <span class="hljs-comment">// Parallel-Syntax wie mit C# (TPL) </span><br/><span class="hljs-comment"> Parallel.For(0, N, i =&gt; </span><br/><span class="hljs-comment"> { </span><br/><span class="hljs-comment"> a[i] += 3 * b[i]; </span><br/><span class="hljs-comment"> a[i] -= b[i] / 2 + 10; </span><br/><span class="hljs-comment"> }); </span><br/><span class="hljs-comment"> } </span><br/><br/><span class="hljs-comment"> static void Main(string[] args) { </span><br/><span class="hljs-comment"> const int N = 5; </span><br/><span class="hljs-comment"> int[] a = { 1, 2, 3, 4, 5 }; </span><br/><span class="hljs-comment"> int[] b = { 10, 20, 30, 40, 50 }; </span><br/><span class="hljs-comment"> // Name der Grafikkarte ermitteln </span><br/><span class="hljs-comment"> cudaDeviceProp prop; </span><br/><span class="hljs-comment"> cuda.GetDeviceProperties(out prop, 0); </span><br/><span class="hljs-comment"> Console.WriteLine(prop.name); </span><br/><br/><span class="hljs-comment"> HybRunner runner = HybRunner.Cuda(); </span><br/><br/><span class="hljs-comment"> // Erzeuge einen Wrapper, um die GPU-Methoden </span><br/><span class="hljs-comment"> // aufzurufen (anstelle der normalen </span><br/><span class="hljs-comment"> // C#-Methoden) </span><br/><span class="hljs-comment"> dynamic wrapped = runner.Wrap(new Program()); </span><br/><br/><span class="hljs-comment"> // Kernel der auf GPU ausführen </span><br/><span class="hljs-comment"> wrapped.Run(N, a, b); </span><br/><br/><span class="hljs-comment"> // Ergebnisse ausgeben </span><br/><span class="hljs-comment"> for(int i = 0; i &lt; N; i++) </span><br/><span class="hljs-comment"> { </span><br/><span class="hljs-comment"> Console.WriteLine("{0}: {1}", i, a[i]); </span><br/><span class="hljs-comment"> } </span><br/><span class="hljs-comment"> } </span><br/><span class="hljs-comment"> } </span><br/><span class="hljs-comment">} </span>
Um tatsächlich eine Performance-Steigerung beim Rechnen auf einer GPU zu erzielen, müssen allerdings wesentlich mehr Daten verarbeitet werden. Je mehr Daten, desto besser! Gleichzeitig gilt es zu berücksichtigen, dass die Übertragung der Daten in den Speicher der GPU etwas Zeit kostet und dadurch die Performance verschlechtert wird. Für ein einführendes Beispiel taugen die kleinen Arrays aber sehr gut.Im darauffolgenden Codeteil soll hier beispielhaft der Name der Grafikkarte mithilfe von cuda.GetDeviceProperties abgefragt werden. Die Variable cuda wird mit der Bibliothek Hybridizer.Runtime.CUDAImports zur Verfügung gestellt, die im Projekt als Referenz und als Namensraum eingefügt ist. Die Variable prop ist vom Typ cudaDeviceProp und liefert vielfältige Informationen über das installierte CUDA-System und die dazugehörige Grafikkarte.Nun wird die Instanz runner vom Typ HybRunner angelegt, die im Prinzip eine CUDA-Laufzeitumgebung zur Verfügung stellt. Über diese Variable wird dann auch eine Hüllklasse (Wrapper) erzeugt, welche die Logik abbildet, die auf der Grafikkarte laufen soll.Jetzt kann der Kernel auf der GPU mit wrapped.Run(...) gestartet werden. In den runden Klammern werden die erforderlichen Parameter angegeben. Hier sind das die beiden Arrays a und b sowie die Anzahl der Elemente N in den Arrays.Die Run-Methode ist blockiert, bis die Berechnungen auf der GPU komplett ausgeführt wurden. Es handelt sich also um einen synchronen Aufruf. Danach können die Ergebnisse der Berechnung im Konsolenfenster ausgegeben werden.Der Code, der auf der GPU ausgeführt werden soll, befindet sich also in der Run-Methode, die als statische Methode ohne Rückgabewert mit den entsprechenden Parametern implementiert wird. Wichtig ist in diesem Fall das Attribut [EntryPoint], das der Hybridizer-Bibliothek sagt, welche Methode als CUDA-Kernel auf der GPU ausgeführt werden soll. Diese Methode kann außedem weitere Funktionen aufrufen, die ebenfalls zum Kernel gehören und auf der GPU ausgeführt werden.Der weitere Code in der Run-Methode sieht eigentlich so aus, wie eine Parallel.For-Schleife, die mit der normalen Task Parallel Library (TPL) aus dem .NET Framework programmiert wurde. Die Syntax ist identisch, allerdings wird diese Parallel-Schleife dann in eine CUDA-Parallel-Variante von Hybridizer umgesetzt. Darum muss im Programm auch der Namensraum System.Threading.Tasks bereitgestellt werden.In der Parallel-Schleife der Run-Methode werden nun einfach einige Integer-Berechnungen durchgeführt. Das Programm kann ganz normal in Visual Studio gestartet und ausgeführt werden.Hier noch ein kleiner Tipp: Wenn Sie am CUDA-Code nach dem Attribut [EntryPoint] etwas ändern, und auch beim ersten Übersetzen, müssen Sie mit Visual Studio das gesamte Projekt übersetzen, ansonsten wird die erforderliche CUDA-DLL nicht neu erstellt.Selbstverständlich können Sie im GPU-Code der Run-Methode auch mathematische Funktionen aus dem .NET-Namensraum Math benutzen. Hierzu zählen trigonometrische Funktionen wie Sin, Cos, Tan, et cetera, außerdem Pow, Exp, Log und viele andere mehr. Das Beispiel in Listing 2 zeigt eine Berechnung mit trigonometrischen Funktionen. Vielleicht erinnern Sie sich noch an Ihre Schulzeit: Damals haben wir folgende Beziehung gelernt:
Listing 2: Etwas mehr Mathematik
using Hybridizer.Runtime.CUDAImports; <br/>using System; <br/>using System.Threading.Tasks; <br/><br/>namespace HybridizerSample2 <br/>{ <br/> <span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">Program</span></span> <br/> { <br/> [EntryPoint] <br/> <span class="hljs-keyword">public</span> static void Run(<br/> <span class="hljs-built_in">int</span> N, <span class="hljs-keyword">double</span>[] a, <span class="hljs-keyword">double</span>[] b) { <br/> Parallel.For(<span class="hljs-number">0</span>, N, i =&gt; <br/> { <br/> // <span class="hljs-built_in">Sin</span>(x*x) + <span class="hljs-built_in">Cos</span>(x*x) = <span class="hljs-number">1</span> <br/> b[i] = Math.Pow(Math.<span class="hljs-built_in">Sin</span>(a[i]), <span class="hljs-number">2</span>) + <br/> Math.Pow(Math.<span class="hljs-built_in">Cos</span>(a[i]), <span class="hljs-number">2</span>); <br/> }); <br/> } <br/><br/> static void Main(string[] args) { <br/> const <span class="hljs-built_in">int</span> N = <span class="hljs-number">1000</span>; <br/> <span class="hljs-keyword">double</span>[] a = new <span class="hljs-keyword">double</span>[N]; <br/> <span class="hljs-keyword">double</span>[] b = new <span class="hljs-keyword">double</span>[N]; <br/><br/> for (<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; N; i++) { <br/> a[i] = (<span class="hljs-keyword">double</span>)i * <span class="hljs-number">2.0</span> * Math.PI / <span class="hljs-number">1000.0</span>; <br/> } <br/><br/> // CUDA-Kernel starten <br/> HybRunner runner = HybRunner.Cuda(); <br/> dynamic wrapped = runner.Wrap(new <span class="hljs-function"><span class="hljs-keyword">Program</span><span class="hljs-params">()</span></span>); <br/> wrapped.Run(N, a, b); <br/><br/> // Ausgabe einiger Ergebnisse <br/> Console.WriteLine(<span class="hljs-string">"{0} {1} {2} {3} {4}"</span>, <br/> b[<span class="hljs-number">0</span>], b[<span class="hljs-number">2</span>], b[<span class="hljs-number">50</span>], b[<span class="hljs-number">100</span>], b[N - <span class="hljs-number">1</span>]); <br/> } <br/> } <br/>}
sin<sup>2</sup>(<span class="hljs-name">x</span>) + cos<sup>2</sup>(<span class="hljs-name">x</span>) = <span class="hljs-number">1</span>
Hier kann x ein beliebiger Winkel zwischen 0 und 360 Grad – oder besser zwischen 0 und 2*π – sein. Das Quadrat des Sinus dieses Winkels plus dem Quadrat des Cosinus des Winkels ergibt immer 1, egal welcher Winkel benutzt wird. Somit kann man das Rechenergebnis des folgenden Beispielprogramms sehr leicht überprüfen.Der Aufbau des Programms in Listings 2 ist analog zum ersten Beispiel. In der Kernel-Funktion Run wird im Array b die oben gezeigte Funktion mit den Eingabedaten aus Array a durchgerechnet. Somit sollte in jedem Array-Element von b die Zahl 1 stehen.In der Main-Methode werden zunächst die Ausgabedaten im Array a gespeichert. Diese Werte liegen im Bereich von 0 bis etwa 6,283 (= 2*π) in 1 000 Schritten. Nach dem Aufruf des CUDA-Kernels mit den Parametern a, b und N werden einige Testergebnisse ausgegeben.
Und wieder: Die Matrixmultiplikation
Nun soll die altbekannte Matrixmultiplikation mit Hybridizer implementiert werden, wie das bereits in den beiden ersten Folgen dieser Artikelserie gemacht wurde. Die Multiplikation zweier Arrays (Matrizen) ist in vielen wissenschaftlichen und technischen Bereichen sehr wichtig und leicht zu implementieren.Zunächst wird in Listing 3 eine Standard-Implementierung des Algorithmus in C# vorgestellt. Die hier angewendete Variante arbeitet mit linearen Arrays. Das heißt, ein quadratisches Array mit der Größe von 5 x 5 Elementen wird auf ein eindimensionales Feld mit 25 Elementen abgebildet. Dabei ist dann etwas Indexrechnung erforderlich, die Datenübergabe ist dagegen sehr einfach.Listing 3: Matrixmultiplikation mit C#
<span class="hljs-keyword">using</span> <span class="hljs-type">System</span>; <br/><span class="hljs-keyword">using</span> <span class="hljs-type">System</span>.<span class="hljs-type">Diagnostics</span>; <br/><br/>namespace <span class="hljs-type">ConsoleApplication1</span> <br/>{ <br/> class <span class="hljs-type">Program</span> <br/> { <br/> <span class="hljs-keyword">static</span> <span class="hljs-built_in">void</span> matMult(<span class="hljs-built_in">int</span> N, <span class="hljs-built_in">float</span>[] a, <br/> <span class="hljs-built_in">float</span>[] b, <span class="hljs-built_in">float</span>[] c) <br/> { <br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; N; i++) <br/> { <br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> j = <span class="hljs-number">0</span>; j &lt; N; j++) <br/> { <br/> <span class="hljs-built_in">int</span> ind = i * N + j; <br/> <span class="hljs-built_in">float</span> cc = <span class="hljs-number">0</span>.<span class="hljs-number">0</span>f; <br/><br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> k = <span class="hljs-number">0</span>; k &lt; N; k++) <br/> { <br/> // <span class="hljs-type">Index</span>-<span class="hljs-type">Berechnung</span> für lineare <span class="hljs-type">Arrays</span> <br/> <span class="hljs-built_in">int</span> ind1 = i * N + k; <br/> <span class="hljs-built_in">int</span> ind2 = k * N + j; <br/> // <span class="hljs-type">Entspricht</span>: c[i,j] += a[i,k] * b[k,j] <br/> cc += a[ind1] * b[ind2]; <br/> } <br/><br/> c[ind] = cc; <br/> } <br/> } <br/> } <br/><br/> <span class="hljs-keyword">static</span> <span class="hljs-built_in">void</span> <span class="hljs-type">Main</span>(<span class="hljs-built_in">string</span>[] args) <br/> { <br/> <span class="hljs-type">Stopwatch</span> sw = new <span class="hljs-type">Stopwatch</span>(); <br/> <span class="hljs-keyword">const</span> <span class="hljs-built_in">int</span> N = <span class="hljs-number">1024</span>; <br/> <span class="hljs-built_in">float</span>[] a = new <span class="hljs-built_in">float</span>[N * N]; <br/> <span class="hljs-built_in">float</span>[] b = new <span class="hljs-built_in">float</span>[N * N]; <br/> <span class="hljs-built_in">float</span>[] c = new <span class="hljs-built_in">float</span>[N * N]; <br/><br/> // <span class="hljs-type">Initialisierung</span> <br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; N; i++) <br/> { <br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> j = <span class="hljs-number">0</span>; j &lt; N; j++) <br/> { <br/> <span class="hljs-built_in">int</span> ind = i * N + j; <br/> a[ind] = (<span class="hljs-built_in">float</span>)ind / (<span class="hljs-built_in">float</span>)(N * N); <br/> b[ind] = (<span class="hljs-built_in">float</span>)ind / (<span class="hljs-built_in">float</span>)(N * N); <br/> c[ind] = <span class="hljs-number">0</span>.<span class="hljs-number">0</span>f; <br/> } <br/> } <br/><br/> sw.<span class="hljs-type">Start</span>(); <br/> matMult(N, a, b, c); <br/> sw.<span class="hljs-type">Stop</span>(); <br/><br/> // <span class="hljs-type">Ausgabe</span> einiger <span class="hljs-type">Testergebnisse</span> <br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">3</span>; i++) <br/> { <br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> j = <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">3</span>; j++) <br/> { <br/> <span class="hljs-built_in">int</span> ind = i * N + j; <br/> <span class="hljs-type">Console</span>.<span class="hljs-type">WriteLine</span>(c[ind]); <br/> } <br/> } <br/><br/> <span class="hljs-type">Console</span>.<span class="hljs-type">WriteLine</span>(<br/> <span class="hljs-string">"Zeit: {0} msek"</span>, sw.<span class="hljs-type">ElapsedMilliseconds</span>); <br/> } <br/> } <br/>}
Der Code erklärt sich im Prinzip von selbst. In der Methode matMult befindet sich die bekannte Dreifachschleife für die Multiplikation der beiden Arrays. Es müssen drei Indizes berechnet werden, um auf die linearen Arrays a, b und c an den korrekten Stellen zuzugreifen. In der Main-Methode wird die Größe der Arrays definiert, die dann deklariert und initialisiert werden.Die Zeit für die Ausführung der Matrixmultiplikation wird mit der .NET-Klasse Stopwatch gemessen und nach einigen Testwerten ausgegeben. Die entsprechende CUDA-Variante finden Sie in Listing 4.
Listing 4: Matrixmultiplikation mit Hybridizer
<span class="hljs-keyword">using</span> <span class="hljs-type">Hybridizer</span>.<span class="hljs-type">Runtime</span>.<span class="hljs-type">CUDAImports</span>; <br/><span class="hljs-keyword">using</span> <span class="hljs-type">System</span>; <br/><span class="hljs-keyword">using</span> <span class="hljs-type">System</span>.<span class="hljs-type">Threading</span>.<span class="hljs-type">Tasks</span>; <br/><span class="hljs-keyword">using</span> <span class="hljs-type">System</span>.<span class="hljs-type">Diagnostics</span>; <br/><br/>namespace <span class="hljs-type">HybridizerSample3</span> <br/>{ <br/> class <span class="hljs-type">Program</span> <br/> { <br/> [<span class="hljs-type">EntryPoint</span>] <br/> public <span class="hljs-keyword">static</span> <span class="hljs-built_in">void</span> <span class="hljs-type">Run</span>(<br/> <span class="hljs-built_in">int</span> N, <span class="hljs-built_in">float</span>[] a, <span class="hljs-built_in">float</span>[] b, <span class="hljs-built_in">float</span>[] c) <br/> { <br/> // <span class="hljs-type">Zwei</span> <span class="hljs-type">Schleifen</span> zusammengefasst <br/> <span class="hljs-type">Parallel2D</span>.<span class="hljs-type">For</span>(<span class="hljs-number">0</span>, N, <span class="hljs-number">0</span>, N, (i, j) =&gt; <br/> { <br/> <span class="hljs-built_in">int</span> ind = i * N + j; <br/> <span class="hljs-built_in">float</span> cc = <span class="hljs-number">0</span>.<span class="hljs-number">0</span>f; <br/><br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> k = <span class="hljs-number">0</span>; k &lt; N; k++) <br/> { <br/> // <span class="hljs-type">Index</span>-<span class="hljs-type">Berechnung</span> für lineare <span class="hljs-type">Arrays</span> <br/> <span class="hljs-built_in">int</span> ind1 = i * N + k; <br/> <span class="hljs-built_in">int</span> ind2 = k * N + j; <br/> // c[i,j] += a[i,k] * b[k,j] <br/> cc += a[ind1] * b[ind2]; <br/> } <br/><br/> c[ind] = cc; <br/> }); <br/> } <br/><br/> <span class="hljs-keyword">static</span> <span class="hljs-built_in">void</span> <span class="hljs-type">Main</span>(<span class="hljs-built_in">string</span>[] args) <br/> { <br/> <span class="hljs-type">Stopwatch</span> sw = new <span class="hljs-type">Stopwatch</span>(); <br/> <span class="hljs-keyword">const</span> <span class="hljs-built_in">int</span> N = <span class="hljs-number">1024</span>; <br/> <span class="hljs-built_in">float</span>[] a = new <span class="hljs-built_in">float</span>[N * N]; <br/> <span class="hljs-built_in">float</span>[] b = new <span class="hljs-built_in">float</span>[N * N]; <br/> <span class="hljs-built_in">float</span>[] c = new <span class="hljs-built_in">float</span>[N * N]; <br/><br/> // <span class="hljs-type">Initialisierung</span> <br/> <span class="hljs-keyword">for</span>(<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; N; i++) { <br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> j = <span class="hljs-number">0</span>; j &lt; N; j++) <br/> { <br/> <span class="hljs-built_in">int</span> ind = i * N + j; <br/> a[ind] = (<span class="hljs-built_in">float</span>)ind / (<span class="hljs-built_in">float</span>)(N * N); <br/> b[ind] = (<span class="hljs-built_in">float</span>)ind / (<span class="hljs-built_in">float</span>)(N * N); <br/> c[ind] = <span class="hljs-number">0</span>.<span class="hljs-number">0</span>f; <br/> } <br/> } <br/><br/> // <span class="hljs-type">CUDA</span>-<span class="hljs-type">Kernel</span> starten <br/> // <span class="hljs-type">Bei</span> großen <span class="hljs-type">Arrays</span> muss die <span class="hljs-type">Thread</span>- und <span class="hljs-type">Block</span>- <br/> // <span class="hljs-type">Verteilung</span> <span class="hljs-keyword">in</span> <span class="hljs-type">CUDA</span> richtig gesetzt werden <br/> // <span class="hljs-type">Hier</span>: <span class="hljs-number">32</span>*<span class="hljs-number">32</span> <span class="hljs-type">Threads</span>/<span class="hljs-type">Block</span>, <span class="hljs-number">16</span>*<span class="hljs-number">16</span> <span class="hljs-type">Bl</span>öcke/<span class="hljs-type">Grid</span> <br/> <span class="hljs-type">HybRunner</span> runner = <br/> <span class="hljs-type">HybRunner</span>.<span class="hljs-type">Cuda</span>().<span class="hljs-type">SetDistrib</span>(<br/> <span class="hljs-number">32</span>, <span class="hljs-number">32</span>, <span class="hljs-number">16</span>, <span class="hljs-number">16</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>); <br/> dynamic wrapped = runner.<span class="hljs-type">Wrap</span>(new <span class="hljs-type">Program</span>()); <br/> // <span class="hljs-type">Der</span> erste <span class="hljs-type">Aufruf</span> dauert länger! <br/> // wrapped.<span class="hljs-type">Run</span>(N, a, b, c); <br/> sw.<span class="hljs-type">Start</span>(); <br/> wrapped.<span class="hljs-type">Run</span>(N, a, b, c); <br/> sw.<span class="hljs-type">Stop</span>(); <br/><br/> // <span class="hljs-type">Ausgabe</span> einiger <span class="hljs-type">Testergebnisse</span> <br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">3</span>; i++) <br/> { <br/> <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> j = <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">3</span>; j++) <br/> { <br/> <span class="hljs-built_in">int</span> ind = i * N + j; <br/> <span class="hljs-type">Console</span>.<span class="hljs-type">WriteLine</span>(c[ind]); <br/> } <br/> } <br/><br/> <span class="hljs-type">Console</span>.<span class="hljs-type">WriteLine</span>(<br/> <span class="hljs-string">"Zeit: {0} msek"</span>, sw.<span class="hljs-type">ElapsedMilliseconds</span>); <br/> } <br/> } <br/>}
Auch in dieser Variante werden eindimensionale Daten-Arrays verwendet. In der Kernel-Methode, die auf der GPU ausgeführt werden soll, wird hier jedoch eine Parallelschleife benutzt, die zwei Schleifenindizes verwaltet:
Parallel2D.<span class="hljs-keyword">For</span>(<span class="hljs-number">0</span>, <span class="hljs-keyword">N</span>, <span class="hljs-number">0</span>, <span class="hljs-keyword">N</span>, (i, j) =>
{
//...
}
Es handelt sich also um eine „zweidimensionale“ Schleife, welche die beiden äußeren Indizes der drei Standard-Multiplikationsschleifen verarbeitet. Natürlich müssen für diese Parallel2D-Schleife zwei Startwerte und zwei Endwerte angegeben werden. Man erhält dafür die beiden Laufvariablen als Lambdaparameter i und j, die dann in der weiteren Berechnung genutzt werden können.Im Inneren der Parallel2D-Schleife bleibt nun nur noch die k-Schleife mit der Summierung übrig. Es werden also zu einem bestimmten Zeitpunkt der Berechnung immer viele Hundert Matrixelemente im Array c gleichzeitig in den GPU-Threads berechnet. Wie viele Threads tatsächlich benutzt werden, hängt von der Leistungsfähigkeit der Grafikkarte ab.In der inneren k-Schleife werden nun die für den Zugriff auf die Arrays a und b benötigten Indizes ermittelt und die Summierung durchgeführt. Das Ergebnis aus der for-Schleife wird im Array c an der richtigen Stelle abgelegt.Der erste Teil der Main-Methode (Deklarieren und Initialisieren) ist identisch mit der schon erläuterten C#-Version des Programms.Beim Aufbau der CUDA-Laufzeitumgebung gibt es aber etwas Besonderes. Da mit dem Beispielprogramm auch sehr große Arrays verarbeitet werden können, muss man sich um die Verteilung der Threads pro Block und der Blöcke pro Grid kümmern. Werden die Standardeinstellungen benutzt, wird das Programm eventuell nicht korrekt arbeiten. Wie die CUDA-Bibliothek mit Threads, Blöcken und Grids arbeitet, wurde in [1] erläutert. Mit der Hybridizer-Bibliothek wird mit der Methode SetDistrib(...) die Verteilung von Threads und Blöcken gesteuert:
HybRunner runner = HybRunner.Cuda().SetDistrib(
<span class="hljs-number">32</span>, <span class="hljs-number">32</span>, <span class="hljs-number">16</span>, <span class="hljs-number">16</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>);
Hier werden 32 x 32 Threads (= 1 024 Threads) pro Block benutzt. Jedes CUDA-Grid kann 16 x 16 Blöcke bei der Berechnung nutzen.Danach wird der Wrapper erstellt, und dann kann der Kernel mit der Matrixgröße und den drei Arrays aufgerufen werden. Schließlich werden am Ende des Programms einige Testergebnisse und die Laufzeit für die Multiplikationsroutine ausgegeben. Außerdem ist im Code noch eine auskommentierte Programmzeile vorhanden, die beachtet werden sollte:
// Der erste Aufruf dauert länger!
// wrapped.<span class="hljs-keyword">Run</span>(<span class="hljs-keyword">N</span>, a, b, c);
sw.Start();
wrapped.<span class="hljs-keyword">Run</span>(<span class="hljs-keyword">N</span>, a, b, c);
sw.Stop();
Wie schon in [1] erwähnt wurde, ist der erste Aufruf eines CUDA-Kernels etwas langsamer, da der Code noch für die Ausführung auf der GPU vorbereitet werden muss. Dies ist beim zweiten Aufruf nicht mehr notwendig. Darum wird die Zeitmessung des Beispiels aus Listings 4 zweimal durchgeführt: Einmal mit nur einem Run-Aufruf und dann mit zwei aufeinanderfolgenden Run-Aufrufen. Hier wird jedoch nur die Ausführungszeit des zweiten Kernel-Aufrufs gemessen. Die Messergebnisse, die ich auf einen Standard-Notebook mit der Grafikkarte Nvidia GeForce MX 150 erhalten habe, werden in Bild 1 dargestellt.

Zeitenfür die Matrixmultiplikation (MM)(Bild 1)
Autor
Die Ergebnisse des Tests sind eindeutig. Mit der Hybridizer-Bibliothek wird das Programm knapp 30-mal so schnell ausgeführt. Außerdem wurde im Beispiel nur die einfachste Variante der Matrixmultiplikation implementiert. Mit etwas optimiertem Code können die Ergebnisse wahrscheinlich noch verbessert werden. Mögliche Erweiterungen wurden bereits in [1] vorgestellt.
Zusammenfassung
Nun kann man auch direkt aus C# auf Nvidia-Grafikkarten zugreifen, um schnelle Berechnungen auf der GPU auszuführen. Diese Erweiterung ist nicht uninteressant, aber die Bibliothek ist nicht Teil des .NET Frameworks, sondern muss von einem Drittanbieter zugekauft werden. Für alle C#-Programmierer, die sich jedoch zum Beispiel aus Zeitgründen nicht in die Programmiersprache C für den direkten Zugriff auf CUDA einarbeiten wollen, steht mit Hybridizer von Altimesh eine Alternative zur Verfügung.Fussnoten
- Bernd Marquardt, Vollgas mit der GPU, Teil 1, Rechnen mit CUDA, dotnetpro 4/2019, Seite 68 ff., http://www.dotnetpro.de/A1904CUDA
- Bernd Marquardt, Vollgas mit der GPU, Teil 2, CUDA mit Python, dotnetpro 5/2019, Seite 76 ff., http://www.dotnetpro.de/A1905CUDA
- Hybridizer Essentials, http://www.dotnetpro.de/SL1906CUDA1
- CUDA-Download, http://www.dotnetpro.de/SL1906CUDA2