5094

Mathematica und Cuda

Über das integrierte CUDALink Paket liefert Mathematica Unterstützung für die Grafikprozessorprogrammierung für grafikprozessorbeschleunigte lineare Algebra, diskrete Fourier-Transformationen und Bildverarbeitungsalgorithmen. Bei Bedarf können Sie CUDALink Module auch ganz einfach selbst schreiben.

Das CUDALink Paket von Mathematica liefert folgende Funktionen ganz ohne zusätzliche Kosten:

  • Zugriff auf die automatischen Interface-Builder, Import/Export-Funktionen und Visualisierungsfunktionen von Mathematica
  • Zugriff auf die betreuten Mathematica Datenbanken
  • Unterstützung für Arithmetik mit einfacher und doppelter Genauigkeit
  • Möglichkeit, benutzerdefinierte CUDA Programme in Mathematica zu laden
  • Skalierbarkeit für mehrere Geräte
  • Integration mit existierenden Mathematica Technologien wie Remote Sessions

Mehr Infos zu High Performance Computing mit Wolfram Mathematica.

Weitere Ressourcen:

CUDA Bausteine

Einleitung

Die "Common Unified Device Architecture" (CUDA) wurde im Jahre 2007 von NVIDIA entwickelt. Auch wenn es die Programmierung der GPU schon seit Jahren gibt, haben Hardwarerestriktionen das Schreiben von Software für die Grafikkarte nicht leicht gemacht. Eine Aufgabe von CUDALink von Mathematica ist es, das Programmieren und die Portierung der eigenen Software zu vereinfachen. Wenn man Mathematica benutzt, muss man sich keine Gedanken über die vielen Hürden machen. Somit bleibt das Hauptaugenmerk auf dem Schreiben der CUDA Kernel. Das wird durch durch Nutzen aller NVIDIA Architektur Stacks bewerkstelligt.

Architektur

Hier der schematische Aufbau der CUDA Architektur.

Grid und Blocks

CUDA-Programmierung basiert auf dem Daten-Parallelisierungs-Modell. Berechnungen werden auf viele hundert - wenn nicht gar tausend Threads geteilt. Wenn Folgendes Ihre Berechnung wäre:

OutputData = Table[fun[InputData[[i, j]]], {i, 10000}, {j, 10000}]

, dann wäre dies hier Ihr CUDA-Äquivalent:

CUDALaunch[fun, InputData, OutputData, {10000, 10000}]

wobei "fun" Ihre selbst definierte Funktion ist. Das Beispiel startet 1000 x 1000 Threads und übergibt jedem deren Indizes. Das Ergebnis wird InputData zugewiesen und in OutputData gespeichert.

Der Grund, weshalb CUDA Tausende von Threads starten können, liegt an der Hardwarearchitektur. Im folgenden Abschnitt erörtern wir nun, wie Threads geteilt werden.

CUDA teilt programmatische Probleme in ein-, zwei- oder dreidimensionale Grids auf. Folgendes Bild zeigt eine typische zweidimensionale CUDA-Thread-Konfiguration.

Jedes Grid beinhaltet mehrere Blöcke, und jeder Block besteht aus Threads. Ausgehend von dem obigen Bild, kann man sich die Grids, Blocks und Threads wie folgt vorstellen.

Ob man nun eine ein-, zwei- oder dreidimensionale Thread-Konfiguration verwendet, ist abhängig von dem Problem. Im Fall von Bildbearbeitung mappen wir die Threads wie in dem Bild und weisen jedem Pixel eine Funktion zu.

Der eindimensionale zelluläre Automat kann auf ein eindimensionales Grid gemappt werden.

Program Cycle

Das Wesentliche an der CUDA-Programmierung sind folgende drei Schritte:

  1. Kopieren der Daten beim Starten der Threads
  2. Warten, bis die GPU die Berechnung durchgeführt hat
  3. Kopieren des Ergebnisses von der GPU zum Host
Das obere Bild zeigt den typischen Ablauf eines CUDA-Programms im Einzelnen.
  1. Zuteilen des Arbeitsspeichers der GPU: GPU- und CPU-Speicher sind physikalisch getrennt, und der Programmierer muss die zugewiesenen Kopien verwalten.
  2. Der Speicher wird von der CPU zur GPU kopiert.
  3. Konfigurieren der Thread-Konfiguration: Der korrekte Block und die Grid-Dimension für das Problem werden ausgewählt.
  4. Die konfigurierten Threads werden gestartet.
  5. Durch die Synchronisation der CUDA-Threads wird sicher gestellt, dass die GPU alle Aufgaben beendet hat, um dann weitere Operationen auf dem Speicher der GPU auszuführen.
  6. Wenn die Operationen ausgeführt wurden, wird der Speicher von der GPU auf die CPU wieder zurückgeschrieben.
  7. Der GPU-Speicher wird geleert.

Wenn Mathematica genutzt wird, muss man sich über die vielen verschiedenen Schritte keine Gedanken mehr machen, sondern kann sich auf das Schreiben des CUDA Kernels konzentrieren.

Speicher-Hierarchie

CUDA-Speicher wird in verschiedene Bereiche getrennt; jeder dieser Speicherbereiche hat seine Vorteile und Beschränkungen. Das folgende Bild zeigt alle Typen von CUDA-Speicher.

Global Memory

Der Arbeitsspeicher beansprucht den größten Raum auf einer Grafikkarte und ist dabei auch am langsamsten. Alle Threads können Speicher aus dem "Global Memory" nehmen. Dies sollte man aber aus Gründen der Performance, wenn möglich, vermeiden.

Texture Memory

Das "Texture Memory" residiert in demselben Bereich wie das "Global Memory", ist aber nur lesbar. Dafür ist es sehr viel performanter. Dieser Speicher unterstützt nur die Werte string, char und int unterstützt.

Constant Memory

Hierbei handelt es sich um einen schnellen Speicher, der für jeden Thread aus dem Grid verfügbar ist. Der Speicher wird gecached, ist aber global auf 64 kB limitiert.

Shared Memory

Auch dies ist ein schneller Speicher, der mit jeweils einem Block verbunden ist. Unter aktueller Hardware ist dieser Speicher auf 16kB pro Block limitiert.

Local Memory

Dieser Speicher ist für jeden Thread lokal, residiert aber im "Global Memory", bis der Kompiler die Variablen in das Register schreibt. Auch hier sollte man die Zugriffe auf ein Minimum beschränken, selbst wenn dieser Speicher nicht ganz so langsam wie das "Global Memory" ist.

Compute Capability

Die "Compute Capability" gibt an, was die verwendete Hardware für Operationen unterstützt. Zurzeit sind folgende verfügbar:

Compute Capability:
Extra Features:
1.0 base implementation
1.1 atomic operations
1.2 shared atomic operations and warp vote functions
1.3 support for double-precision operations
2.0 double precision, L2 cache, and concurrent kernels

Sollten Sie CUDALink noch nicht importiert haben, holen Sie dies bitte jetzt nach:

Needs["CUDALink`"]

Folgender Befehl zeigt alle CUDA-fähigen Komponenten Ihres Systems an:

TabView[Table[
CUDAInformation[ii, "Name"] ->
CUDAInformation[ii,
"Compute Capabilities"], {ii, $CUDADeviceCount}]]

CUDA Devices

Wenn die Systemhardware es erlaubt, lässt CUDA den Anwender entscheiden, auf welcher GPU die Berechnungen durchgeführt werden sollen. Standardgemäß wird die schnellste genutzt, aber diese Einstellung kann von dem Benutzer außer Kraft gesetzt werden. Sobald eine GPU ausgewählt wurde, kann diese während der Kernel Session nicht mehr verändert werden.