Beispiel zum Erhöhen der Arbeit pro Thread in CUDA

7

Intro : Zuerst und als Einleitung bin ich ziemlich stolz darauf, meine erste Frage zu StackOverflow zu stellen. Ich hoffe, ich werde anderen Menschen genauso helfen können wie sie mir helfen.

Algorithmus :

Ich schreibe ein Programm mit CUDA und das Problem ist folgendes:

  • Zwei Matrizen A (n * 128) und B (m * 128)

  • Ich nehme die erste Zeile von A, und ich berechne den Abstand zwischen diesem Vektor und allen Reihen von B, eins nach dem anderen.

  • Ich schreibe das Ergebnis jeder Entfernung in eine Zeile einer Matrix C, so dass das Element C (i, j) von C den Abstand zwischen Zeile i von A und Zeile j von B enthält.

  • und ich fahre mit der nächsten Reihe von A fort.

Ich habe es so implementiert: Ich habe ein Raster aus (n * m) Blöcken und 128 Threads pro Block. (1 * 128).

FRAGE : Das Programm wird erfolgreich mit den erwarteten Ergebnissen ausgeführt, aber die Zeitausführung ist nur etwa 5 bis 10 mal schneller als die CPU-Version mit nur einem Thread. Daher würde ich gerne wissen, wie man die Arbeit pro Thread vor der Reduktion erhöht, um die Leistung zu steigern. . .

Kernel-Code (Original: Nicht optimiert)

%Vor%

AKTUALISIEREN

Jetzt benutze ich ein anderes Mapping: Anstatt ein Gitter von n by m Blöcke und einen Block von 128 threads zu nehmen, vergrößere ich die Anzahl der Threads innerhalb eines Blocks, um zu verringern die Anzahl der Blöcke.

Neues Mapping:

Block von 128 by 8 threads (insgesamt 1024 threads, was die maximale Größe ist)

Gitter von n/8 by m/8 Blöcke

Leider gibt es falsche Ergebnisse).

Optimierter Kernel-Code (wird aktualisiert)

%Vor%

HOST CODE (Zuweisungen + Kernel-Aufrufe)

%Vor%

PS : Ich habe CUDA 6.0 mit einer NVIDIA GTX 650 (compute function 3.0)

    
talonmies 12.06.2014, 14:31
quelle

1 Antwort

3

Es scheint, dass deine Frage zwei Komponenten hat:

  1. Warum funktioniert mein zweiter Kernel nicht?
  2. Wie mache ich meinen Code schneller?
  

Warum funktioniert mein zweiter Kernel nicht?

Sie hatten mehrere Probleme:

  1. Indexierungsprobleme bei der anfänglichen Berechnung von i , j sowie dem Index zum Speichern des C -Wertes.
  2. Verletzung der Nutzung von _syncthreads() in einem bedingten Block

Punkt 1 war das Schlüsselelement, um den Code zum Laufen zu bringen.

  

Wie mache ich meinen Code schneller?

Das ist komplizierter. Zunächst einmal hat Ihr Versuch, die Arbeit pro Thread zu erhöhen, nichts dergleichen getan, sondern lediglich die Anzahl der Threads pro Block erhöht (von 128 auf 8 * 128). Jeder Thread machte ungefähr die gleiche Menge an Arbeit. Außerdem glaube ich, dass bei dem Versuch, einen 2D-Threadblock für diesen Versuch zu verwenden, ein paar schlimme Dinge passiert sind:

  1. verschiedene Lade- und Speichermuster für Koaleszenz- und Shared-Memory-Bank-Konflikte wurden unterbrochen.
  2. Die effektive Belegung ist aufgrund der Menge an gemeinsam genutztem Speicher pro Block gesunken.

Der Nettoeffekt des zweiten Kernels bestand darin, die Ausführungszeit ungefähr zu verdoppeln. Das wollen wir also nicht.

Es kann jedoch sinnvoll sein, die Arbeit pro Thread zu erhöhen, gemeinsam genutzten Speicher zu verwenden sowie gute (globale, gemeinsame) Speicherzugriffsmuster beizubehalten und die Auslastung zu erhöhen.

Was folgt, ist ein Work-in-Progress in dieser Richtung. Im folgenden Code ist der zweite Kernel, die Timing-Infrastruktur, die vollständige Datenüberprüfung sowie zwei neue Kernel behoben. Der erste neue Kernel (# 3) würde ich als "naiven" Kernel bezeichnen. Es weist einfach einen Thread pro Ausgabepunkt zu, und jeder Thread durchläuft die erforderlichen Vektoren und berechnet sein individuelles Ergebnis. Keine Verwendung von gemeinsam genutztem Speicher oder viel Aufmerksamkeit für Koaleszenz oder andere Optimierungen. Allerdings mit einer Tweak zu Threadblock-Konfiguration (16,16) - & gt; (8,32) Threads, die ich aus @talonmypies Antwort (jetzt gelöscht) beobachtet habe, ist dieser Kernel deutlich (3x) schneller als dein "schneller" Kernel. Nach weiterem Nachdenken über die (8,32) Beobachtung kam ich zu dem Schluss, dass der nächste Optimierungsversuch sich auf Folgendes konzentrieren sollte:

  1. Maximierung des Nutzens aus dem Cache
  2. effiziente Nutzung von Shared Memory
  3. bestehen auf perfekter globaler Koaleszenz / perfekter Nutzung des gemeinsamen Speichers für alle Lese- und Schreibvorgänge

Punkt 4 hat die Frage in den Kommentaren "Kann ich die Matrizen transponieren?" Mit dieser Erlaubnis ist es möglich, die Daten neu zu organisieren, um den obigen Punkt 4 zu erleichtern. Punkt 2 oben wird in meinem "schnellen" Kernel (# 4) adressiert, indem der B Vektor in den gemeinsamen Speicher geladen wird, während der Cache sich hauptsächlich auf die Zwischenspeicherung der A Vektoren konzentriert und hoffentlich Cache-Thrashing reduziert (A ist der kleinere der 2) Vektorarrays, bei ungefähr 2 MB - Fermi L2 ist 768 K, Kepler L2 ist 1,5 MB). Indem A in transponierter Form geliefert wird und B auf dem Chip effektiv aus dem geteilten Speicher "transponiert" wird, ist es möglich, eine gerade For-Schleife zu verwenden, um die Vektorentfernung zu berechnen, während angrenzende Threads perfekt koaleszierte Lese- und Schreibvorgänge haben "Effiziente" Verwendung von gemeinsam genutztem Speicher (dh Nicht-Bank-Konfliktlasten und Broadcast-Lesevorgänge).

Für mein spezielles Timing (Quadro5000 cc2.0 GPU, CUDA 6, RHEL 5.5) sehe ich, dass dein "schneller" Kernel etwa 2 Sekunden benötigt, mein "naiver" Kernel etwa 0,7 Sekunden und mein "schneller" Kernel benötigt etwa 0,2 Sekunden, wenn auch mit transponierten Daten (A, C).

BEARBEITEN: Ich habe eine zusätzliche Optimierung vorgenommen, dh jeder Block berechnet mehrere Vektoren ( CHKSIZE ) B gleichzeitig. Sie können CHKSIZE auf 1 setzen, um das vorherige Ergebnis anzuzeigen (~ 0,2 Sekunden). Ich fand CHKSIZE von 4 gute Besserung. Dies ist ein Angriff bei dem Versuch, die Datenwiederverwendung von A auszunutzen. Mit dieser zusätzlichen Optimierung bei CHKSIZE von 4 sinkt die Kernelzeit für Kernel 4 auf etwa 0,1 Sekunden.

Nachstehend folgt der Code und ein Beispiel:

%Vor%

Hoffentlich bringt dich das mit mehr Ideen von Dingen zum Laufen. Sie können natürlich unterschiedliche Timings auf Ihrem cc3.0-Gerät erhalten.

Sind weitere Optimierungen möglich? Wahrscheinlich. Das erste Ziel, das ich betrachten würde, wäre, herauszufinden, wie man die Möglichkeiten der Wiederverwendung von Daten auf Vektor A ausnutzt. (Die Wiederverwendung von Vektor B wird bereits im Kernel 4 durch Laden in den gemeinsamen Speicher behandelt Möglichkeiten, einen gemeinsamen Speicher zu verwenden, um Teile von A zu speichern, damit der Code noch schneller ausgeführt wird.)

Ich denke, ich sollte auch erwähnen, dass dieser Code, der dem Code folgt, den Sie bereitgestellt haben, das Quadrat des euklidische Distanz . Eine triviale Änderung an den Kernen kann stattdessen die tatsächliche euklidische Entfernung berechnen lassen ( C[...] = sqrtf(...); ). Die von mir eingeschlossene Validierung geht jedoch davon aus, dass die Ergebnisse "in-range" für die perfekte Speicherung einer ganzzahligen Menge in float sind. Ihr Testfall erfüllt diese Anforderung, ansonsten müsste der Validierungscode geändert werden (wenn sqrtf verwendet wurde).

    
Robert Crovella 04.07.2014, 20:09
quelle

Tags und Links