Suche…


Einführung

In diesem Thema werden einige der grundlegenden Mechanismen des Parallel-Computing vorgestellt, die erforderlich sind, um OpenCL vollständig zu verstehen und anzuwenden.

Fäden und Ausführung

Der Schlüssel zum Parallelismus besteht darin, mehrere Threads zu verwenden, um ein Problem zu lösen (duh.). Es gibt jedoch einige Unterschiede zur klassischen Multithread-Programmierung in der Organisation von Threads.

Lassen Sie uns zunächst über Ihre typische GPU sprechen, der Einfachheit halber möchte ich mich darauf konzentrieren

Eine GPU verfügt über viele Prozessorkerne, wodurch sie ideal ist, um viele Threads parallel auszuführen. Diese Kerne sind in Streaming-Prozessoren (SM, NVidia-Begriff) organisiert, für die eine GPU eine bestimmte Anzahl hat.

Alle Threads, die in einem SM ausgeführt werden, werden als "Threadblock" bezeichnet. Es können mehr Threads auf einem SM vorhanden sein als über Kerne. Die Anzahl der Kerne definiert die sogenannte "Warp-Größe" (NVidia-Begriff). Threads innerhalb eines Threadblocks werden in sogenannten "Warps" abgelegt.

Ein kurzes Beispiel zum Nacharbeiten: Ein typisches NVidia SM verfügt über 32 Verarbeitungskerne, daher beträgt seine Warp-Größe 32. Wenn mein Thread-Block jetzt 128 Threads ausführen muss, werden diese in 4 Warps (4 Warps * 32 Warp Size = 128) gelöscht Fäden).

Die Warp-Größe ist ziemlich wichtig, wenn Sie später die Anzahl der Threads wählen.

Alle Threads in einem einzelnen Warp teilen sich einen Befehlszähler. Dies bedeutet, dass diese 32 Threads wirklich synchronisiert werden, indem jeder Thread jeden Befehl gleichzeitig ausführt. Hier liegt eine Performance-Fallstricke: Dies gilt auch für Verzweigungsaussagen in Ihrem Kernel!

Beispiel: Ich habe einen Kernel mit einer if-Anweisung und zwei Verzweigungen. 16 meiner Threads in einem Warp führen den ersten Zweig aus, der andere 16 Zweig zwei. Bis zur if-Anweisung sind alle Threads im Warp synchron. Jetzt wählt die Hälfte einen anderen Zweig. Was passiert, ist, dass die andere Hälfte ruht, bis die Ausführung der falschen Anweisung auf den ersten 16 Threads abgeschlossen ist. Dann sind diese Threads inaktiv, bis die anderen 16 Threads ihren Zweig beendet haben.

Wie Sie sehen, können schlechte Verzweigungsgewohnheiten Ihren Parallelcode erheblich verlangsamen, da im schlimmsten Fall beide Anweisungen ausgeführt werden. Wenn alle Threads in einem Warp entscheiden, dass sie nur eine der Anweisungen benötigen, wird die andere vollständig übersprungen und es tritt keine Verzögerung auf.

Das Synchronisieren von Threads ist auch keine einfache Angelegenheit. Sie können nur Threads synchronisieren einen einzelnen SM withing. Alles außerhalb des SMs ist vom Kernel aus nicht synchronisierbar. Sie müssen getrennte Kernel schreiben und nacheinander starten.

GPU-Speicher

Die GPU bietet sechs verschiedene Speicherbereiche. Sie unterscheiden sich in ihrer Latenz, Größe und Zugänglichkeit von verschiedenen Threads.

  • Globaler Speicher: Der größte verfügbare Speicher und einer der wenigen, um Daten mit dem Host auszutauschen. Dieser Speicher hat die höchste Latenzzeit und ist für alle Threads verfügbar.
  • Constant Memory: Ein Nur-Lese-Teil des globalen Speichers, der nur von anderen Threads gelesen werden kann. Ihr Vorteil ist die geringere Latenz im Vergleich zum globalen Speicher
  • Texture Memory: Auch Teil des Konstantenspeichers, der speziell für Texturen entwickelt wurde
  • Shared Memory: Dieser Speicherbereich befindet sich in der Nähe des SM und kann nur von einem einzelnen Thread-Block aufgerufen werden. Es bietet eine wesentlich geringere Latenz als der globale Speicher und etwas weniger Latenz als der konstante Speicher.
  • Register: Nur für einen einzelnen Thread und den schnellsten Speicher von allen zugänglich. Wenn der Compiler jedoch feststellt, dass nicht genügend Register für die Kernelanforderungen vorhanden sind, werden die Variablen in den lokalen Speicher ausgelagert.
  • Lokaler Speicher: Ein nur für Threads zugänglicher Teil des Speichers im globalen Speicherbereich. Wird als Backup für Register verwendet, wenn möglich zu vermeiden.

Speicherzugriff

Das typische Szenario für Ihre Speichernutzung ist das Speichern der Quelldaten und der verarbeiteten Daten im globalen Speicher. Beim Start eines Threadblocks werden zunächst alle relevanten Teile in den gemeinsam genutzten Speicher kopiert, bevor sie in die Register aufgenommen werden.

Die Speicherzugriffszeit hängt auch von Ihrer Speicherstrategie ab. Wenn Sie blind auf Daten zugreifen, erhalten Sie die schlechteste mögliche Leistung.

Die verschiedenen Erinnerungen sind in sogenannten "Banken" organisiert. Jede Speicheranforderung für eine Bank kann in einem einzigen Taktzyklus behandelt werden. Die Anzahl der Banken im Shared Memory entspricht der Warp-Größe. Die Speichergeschwindigkeit kann erhöht werden, indem ein Konflikt mit einer Bank innerhalb einer einzelnen Warp vermieden wird.

Um gemeinsam genutzten Speicher aus dem oder in den globalen Speicher zu kopieren, können Sie die Speicheraufrufe am schnellsten "ausrichten". Dies bedeutet, dass der erste Thread in einem Warp auf das erste Element in der Bank des gemeinsam genutzten und des globalen Speichers zugreifen soll. Der zweite Thread das zweite Element und so weiter. Dieser Aufruf wird zu einem einzigen Speicherübertragungsbefehl optimiert, der die gesamte Bank in einem Durchgang in den Zielspeicher kopiert.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow