Was ist GPU-Programmierung?

In der Welt des Hochleistungsrechnens sind GPUs (Graphics Processing Units) allgegenwärtig, und Wissenschaftler in bestimmten Fachgebieten stehen vor einer wachsenden Herausforderung. Sie müssen ihre Computerprogramme anpassen, um diese fortschrittlichen GPU-Systeme optimal zu nutzen. Diese Aufgabe ist wichtig, um die volle Leistung dieser hochmodernen GPU-Architekturen zu erschließen. 

In diesem Artikel vermitteln wir ein Grundverständnis der GPU-Programmierung für alle, die Antworten auf die Frage suchen: „Was ist GPU-Programmierung?“ Außerdem lernen Sie die verschiedenen GPU-Ansätze und Frameworks kennen sowie CUDA, die beliebteste Plattform für GPU-Programmierung mit Python, und ihre Bedeutung für das Programmieren von GPUs.

Was ist eine GPU?

Ein Grafikprozessor (GPU) ist eine elektronische Schaltung, die die Erzeugung von Bildern für die Anzeige beschleunigt, indem sie Speicher schnell manipuliert und Daten gleichzeitig verarbeitet. Diese GPUs sind heute allgegenwärtig und finden sich in verschiedenen Rechengeräten wie PCs, Spielkonsolen, professionellen Workstations und sogar Smartphones. 

Im Laufe der Zeit haben GPUs CPUs in der Leistung überholt, da sie mehr Transistoren besitzen und beim parallelen Rechnen herausragen. GPUs haben sich zu komplexen Geräten mit eigenem Speicher, eigenen Bussen und Prozessoren entwickelt und können als zusätzliches Gehirn oder zusätzlicher Prozessor des Computersystems betrachtet werden.

Was ist GPU-Programmierung?

Bei der GPU-Programmierung werden GPU-Beschleuniger genutzt, um äußerst allgemeine Rechenoperationen gleichzeitig auszuführen. Früher wurden GPUs hauptsächlich für Computergrafik eingesetzt, heute werden sie aber auch für alle möglichen allgemeinen Rechenaufgaben verwendet. Neben der Grafikerzeugung werden GPUs heute häufig für wissenschaftliche Modellierung, maschinelles Lernen und andere Aufgaben eingesetzt, die sich schneller erledigen lassen, wenn viele Teilaufgaben gleichzeitig bearbeitet werden.

Was ist der Unterschied zwischen GPU und CPU?

Der wesentliche Unterschied zwischen dem Rechnen auf einer GPU und auf einer CPU liegt in ihrer unterschiedlichen Architektur. Aufgrund ihrer hochgradig parallelen Bauweise ist eine GPU bei Daten, die sich aufteilen und auf verschiedene Weise verarbeiten lassen, effektiver als eine CPU. GPUs sind speziell für rechenintensive Operationen wie Matrixarithmetik und Gleitkommaarithmetik optimiert.

Der Grund für den Unterschied in der Rechenleistung zwischen CPUs und GPUs liegt darin, dass GPUs für hochgradig parallele und rechenintensive Aufgaben ausgelegt sind, genau das, was das Rendern von Grafiken erfordert. 

In der Architektur von GPUs hat die Datenverarbeitung Vorrang vor Daten-Caching und Ablaufsteuerung. Bei der parallelen Lösung eines Problems ergeben sich zwei wesentliche Vorteile: Erstens wird jedes Element mit demselben Problem bearbeitet, was den Bedarf an komplexer Ablaufsteuerung verringert; zweitens senken ein großer Datensatz und eine hohe arithmetische Intensität den Bedarf an Speicher mit niedriger Latenz.

Welche Ansätze bzw. APIs für die GPU-Programmierung gibt es?

Die Nutzung von GPUs für Berechnungen lässt sich auf unterschiedliche Weise angehen. In den einfachsten Fällen, in denen der Code bereits geschrieben ist, kann es genügen, Parameter und Anfangskonfigurationen anzupassen. Alternativ lassen sich mit Bibliotheken die rechenintensivsten Teile des Codes abdecken. In komplexeren Szenarien kann jedoch etwas Programmierarbeit erforderlich sein.

Es gibt verschiedene Softwareumgebungen und APIs für die GPU-Programmierung, die sich in direktivenbasierte Modelle, nicht portable kernelbasierte Modelle und portable kernelbasierte Modelle sowie High-Level-Frameworks und Bibliotheken einteilen lassen.

Standard-C++/Fortran

Programme in Standard-C++ und Fortran können NVIDIA-GPUs ohne externe Bibliotheken nutzen. Die Compiler des NVIDIA SDK optimieren den Code für die Ausführung auf der GPU, wie in den bereitgestellten Richtlinien gezeigt.

Direktivenbasierte Programmierung

Bei direktivenbasierten Ansätzen wird vorhandener serieller Code mit Annotationen versehen, die Schleifen und Bereiche für die Ausführung auf der GPU kennzeichnen. Diese Methode, für die OpenACC und OpenMP Beispiele sind, setzt auf Produktivität und Benutzerfreundlichkeit, kann aber etwas Leistung kosten.

OpenACC: OpenACC ist ein 2010 gegründetes Konsortium, das ein standardisiertes, portables und skalierbares Programmiermodell für Beschleuniger einschließlich GPUs bereitstellen will. Anfangs wurden nur NVIDIA-GPUs unterstützt, doch die Unterstützung wird auf weitere Geräte und Architekturen ausgeweitet.

OpenMP: OpenMP war ursprünglich eine API für parallele Programmierung mit gemeinsamem Speicher auf Mehrkern-CPUs und unterstützt heute GPU-Offloading für verschiedene GPU-Typen.

Nicht portable kernelbasierte Modelle (native Programmiermodelle)

Die direkte GPU-Programmierung gibt Entwicklern mehr Kontrolle durch Low-Level-Code, der direkt mit der GPU kommuniziert. Beispiele sind CUDA (NVIDIA) und HIP (AMD), wobei HIP für die Kompatibilität zwischen NVIDIA- und AMD-GPUs konzipiert ist.

Portable kernelbasierte Modelle (plattformübergreifende Portabilitäts-Ökosysteme)

Plattformübergreifende Portabilitäts-Ökosysteme wie Alpaka, Kokkos, OpenCL, RAJA und SYCL bieten Abstraktionen auf höherer Ebene für eine bequeme und portable GPU-Programmierung. Sie zielen auf Leistungsportabilität mit einer Single-Source-Anwendung ab.

Kokkos: Kokkos wurde an den Sandia National Laboratories entwickelt und ist ein C++-basiertes Ökosystem, das effiziente und skalierbare parallele Anwendungen auf CPUs, GPUs und FPGAs unterstützt.

OpenCL: OpenCL ist eine API nach offenem Standard für allgemeines paralleles Rechnen auf unterschiedlicher Hardware und bietet eine Low-Level-Programmierschnittstelle für die GPU-Programmierung. Es unterstützt CPUs, GPUs und FPGAs verschiedener Hersteller.

SYCL: SYCL ist ein lizenzgebührenfreies C++-Programmiermodell nach offenem Standard und bietet ein High-Level-Single-Source-Programmiermodell für heterogene Systeme einschließlich GPUs. Es kann auf OpenCL, CUDA oder HIP aufsetzen.

Unterstützung durch Hochsprachen

Python unterstützt die GPU-Programmierung mit mehreren Bibliotheken, die jeweils eigene Zwecke erfüllen:

CuPy

  • GPU-basierte Bibliothek für Daten-Arrays.
  • Nahtlose Integration mit NumPy/SciPy.
  • Einfacher Umstieg auf GPU-Computing, indem 'numpy' und 'scipy' im Python-Code durch 'cupy' und 'cupyx.scipy' ersetzt werden.

cuDF (RAPIDS)

  • Teil von RAPIDS, implementiert CUDA-Funktionen mit Python-Bindings.
  • Entwickelt für die Bearbeitung von Dataframes auf der GPU.
  • Bietet eine pandas-ähnliche API, mit der Nutzer, die mit Pandas vertraut sind, ihre Arbeit ohne umfangreiche CUDA-Kenntnisse beschleunigen können.

PyCUDA

  • Python-Programmierumgebung für CUDA.
  • Ermöglicht den Zugriff auf die CUDA-API von NVIDIA aus Python.
  • Auf NVIDIA-GPUs beschränkt und erfordert Erfahrung in der CUDA-Programmierung.

Numba

  • Ermöglicht die Just-in-Time-Kompilierung (JIT) von Python-Code für die Ausführung auf der GPU.
  • Unterstützt NVIDIA-GPUs, eine Unterstützung für AMD-GPUs ist für die Zukunft vorgesehen.

Diese Bibliotheken bieten vielfältige Möglichkeiten für die GPU-Programmierung in Python und berücksichtigen unterschiedliche Vorlieben und Anforderungen der Nutzer. Ob beim Umstieg von NumPy/SciPy mit CuPy oder bei der Nutzung der pandas-ähnlichen API von cuDF: Python bietet Flexibilität für effiziente GPU-beschleunigte Berechnungen.

Was ist CUDA?

CUDA ist eine von NVIDIA entwickelte Plattform und API für paralleles Rechnen und markiert den Beginn der Ära weit verbreiteter GPU-Programmier-Frameworks. Mit dieser Technologie können Entwickler C++-ähnlichen Code schreiben, der auf der GPU ausgeführt wird.

CUDA bietet nicht nur eine Fülle von Bibliotheken und Tools für die Low-Level-GPU-Programmierung, sondern verbessert auch die Effizienz rechenintensiver Anwendungen erheblich. Zu beachten ist, dass CUDA trotz dieses starken Ökosystems auf NVIDIA-Hardware beschränkt ist.

CUDA unter Ubuntu 20.04 für die GPU-Programmierung installieren

Sie können CUDA unter Ubuntu 20.04 installieren, indem Sie die folgende Schritt-für-Schritt-Anleitung befolgen:

Schritt 1: Laden Sie zunächst mit dem Befehl ‚wget‘ oder ‚curl‘ die CUDA-Pin-Konfigurationsdatei (cuda-ubuntu2004.pin) herunter, die für die Einrichtung der CUDA-Repositorys unter Ubuntu 20.04 erforderlich ist.

$ wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-ubuntu2004.pin

Schritt 2: Verschieben Sie die heruntergeladene Pin-Konfigurationsdatei in das angegebene Verzeichnis, wodurch ihre Pin-Priorität auf 600 gesetzt wird.

$ sudo mv cuda-ubuntu2004.pin /etc/apt/preferences.d/cuda-repository-pin-600

Schritt 3: Laden Sie das CUDA-Repository-Paket herunter:

wget https://developer.download.nvidia.com/compute/cuda/11.4.2/local_installers/cuda-repo-ubuntu2004-11-4-local_11.4.2-470.57.02-1_amd64.deb

Schritt 4: Installieren Sie das heruntergeladene CUDA-Repository-Paket mit dem Paketmanager dpkg.

$ sudo dpkg -i cuda-repo-ubuntu2004-11-4-local_11.4.2-470.57.02-1_amd64.deb

Schritt 5: Fügen Sie den öffentlichen Schlüssel hinzu, der zur Authentifizierung des CUDA-Repositorys benötigt wird.

$ sudo apt-key add /var/cuda-repo-ubuntu2004-11-4-local/7fa2af80.pub

Schritt 6: Paketlisten aktualisieren:

$ sudo apt-get update

Aktualisieren Sie die lokalen Paketlisten mit den Informationen aus dem neu hinzugefügten CUDA-Repository.

Schritt 7: Installieren Sie nun mit dem folgenden Befehl das CUDA-Toolkit auf Ihrem Ubuntu-20.04-Rechner:

$ sudo apt-get -y install cuda

Mit den oben beschriebenen Schritten können Sie CUDA auf einem Rechner mit CUDA-fähiger GPU unter Ubuntu 20.04 installieren. Nun können Sie GPU-Code ausführen und die CUDA-Plattform auf Ihrem Linux-Rechner nutzen.

Fazit

Die Wahl einer Umgebung für die GPU-Programmierung hängt von Faktoren wie den Ziel-Hardwareplattformen, der Art der Berechnung und der Erfahrung der Entwickler ab. Die Kompatibilität mit bestimmten GPUs, die Übereinstimmung mit den Projektanforderungen und die Vorliebe der Entwickler für Einfachheit auf hoher Ebene oder Kontrolle auf niedriger Ebene sind entscheidende Kriterien für eine optimale Wahl.