Warum ist bei der GPU-Programmierung Arbeitseffizienz erwünscht?

13

Ich habe den folgenden Artikel zur Durchführung eines parallelen Scans in CUDA gelesen:

https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch39.html

In dem Artikel wird ein Schwerpunkt darauf gelegt, dass der Scan "effizient arbeitet". Mit anderen Worten, ein GPU-Algorithmus sollte nicht mehr Additionen ausführen als ein CPU-Algorithmus, O (n). Die Autoren präsentieren zwei Algorithmen, einen "naiven", der O (nlogn) -Additionen ausführt, und einen, den sie als "effizient arbeiten" betrachten. Der arbeitseffiziente Algorithmus führt jedoch doppelt so viele Schleifeniterationen aus.

Nach meinem Verständnis sind GPUs einfach riesige SIMD-Prozessoren und sollten im Gleichschritt arbeiten. Das Durchführen von doppelt so vielen Schleifen im Algorithmus "Arbeitseffizienz" scheint zu implizieren, dass viele Threads inaktiv sind und die Leistung auf lange Sicht verringern. Was vermisse ich?

Mokosha
quelle

Antworten:

21

Zuallererst gilt: "GPUs sind einfach riesige SIMD-Prozessoren und sollten im Gleichschritt arbeiten", es ist ein bisschen komplizierter. Die gesamte GPU läuft nicht im Gleichschritt. Shader-Threads sind in Gruppen von 32 Threads organisiert, die als "Warps" bezeichnet werden (bei NVIDIA sind es Gruppen von 64 Threads, die als "Wellenfronten" bezeichnet werden, aber dasselbe Konzept). Innerhalb eines Warps laufen alle Threads im Gleichschritt als SIMD-Array. Unterschiedliche Ketten sind jedoch nicht im Gleichschritt miteinander. Darüber hinaus können einige Warps aktiv ausgeführt werden, während andere angehalten werden, ähnlich wie bei CPU-Threads. Warps können entweder angehalten werden, weil sie auf etwas warten (z. B. auf die Rückkehr von Speichertransaktionen oder auf das Löschen von Barrieren), oder weil es keine gibt.

Nun zurück zu Ihrer Frage. Ich sehe zwei Möglichkeiten, wie der "arbeitseffiziente" Algorithmus aus dieser Veröffentlichung effizienter aussehen könnte als der "naive" Algorithmus.

  1. Die arbeitseffiziente Version benötigt zunächst halb so viele Threads. Im naiven Algorithmus haben sie einen Thread pro Array-Element; In der arbeitseffizienten Version wird jeder Thread jedoch auf zwei benachbarte Elemente des Arrays angewendet, sodass sie nur halb so viele Threads benötigen wie Array-Elemente. Weniger Fäden bedeuten weniger Verzerrungen, sodass ein größerer Teil der Verzerrungen aktiv ausgeführt werden kann.

  2. Obwohl die arbeitseffiziente Version mehr Schritte erfordert, wird dies durch die Tatsache ausgeglichen, dass die Anzahl der aktiven Threads schneller abnimmt und die Gesamtzahl der aktiven Threads über alle Iterationen erheblich kleiner ist. Wenn ein Warp während einer Iteration keine aktiven Threads hat, springt dieser Warp einfach zur folgenden Barriere und wird angehalten, sodass andere Warps ausgeführt werden können. Wenn Sie also weniger aktive Warps haben, macht sich dies häufig in der Ausführungszeit bezahlt. (Dies impliziert, dass der GPU-Code so entworfen werden muss, dass aktive Threads in möglichst wenigen Warps zusammengefasst werden. Sie möchten nicht, dass sie nur spärlich verstreut sind, da selbst ein aktiver Thread den gesamten Warp erzwingt aktiv bleiben.)

    Berücksichtigen Sie die Anzahl der aktiven Threads im naiven Algorithmus. In Abbildung 2 des Artikels sehen Sie, dass alle Threads mit Ausnahme der ersten 2 k in der k- ten Iteration aktiv sind. Bei N Threads beträgt die Anzahl der aktiven Threads also N - 2 k . Mit N = 1024 beträgt die Anzahl der aktiven Threads pro Iteration beispielsweise:

    1023, 1022, 1020, 1016, 1008, 992, 960, 896, 768, 512
    

    Wenn ich dies in die Anzahl der aktiven Warps umwandle (durch Teilen durch 32 und Aufrunden), erhalte ich:

    32, 32, 32, 32, 32, 31, 30, 28, 24, 16
    

    289. Andererseits beginnt der arbeitseffiziente Algorithmus mit halb so vielen Threads, halbiert dann die Anzahl der aktiven Threads bei jeder Iteration, bis er auf 1 sinkt, und beginnt dann zu verdoppeln, bis er wieder auf 1 sinkt nochmal die halbe Arraygröße:

     512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512
    

    Umwandlung in aktive Warps:

    16, 8, 4, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 4, 8, 16
    

    Die Summe ist 71, das ist nur ein Viertel so viel. Sie können also sehen, dass die Anzahl der aktiven Verzerrungen im Verlauf des gesamten Vorgangs mit dem arbeitseffizienten Algorithmus viel geringer ist. (Tatsächlich gibt es für einen längeren Lauf in der Mitte nur eine Handvoll aktiver Warps, was bedeutet, dass der größte Teil des Chips nicht belegt ist. Wenn zusätzliche Rechenaufgaben ausgeführt werden, z. B. von anderen CUDA-Streams, können sie erweitert werden, um diese zu füllen unbesetzter Raum.)

Trotzdem ist es bedauerlich, dass der GPU Gems-Artikel all dies nicht klar erklärt, sondern sich auf die Big-O-Analyse der Anzahl der Additionen konzentriert, die, obwohl sie nicht völlig irrelevant ist, viele Details darüber, warum dieser Algorithmus so ist, übersieht schneller.

Nathan Reed
quelle