Sortieralgorithmus mit CUDA
(ashwanirathee.com)- So lässt sich mit CUDA über Parallel Computing die Performance eines Sortieralgorithmus verbessern
- Eine grundlegende Merge-Sort-Implementierung wird in CUDA umgesetzt, um das Potenzial für Performance-Verbesserungen zu untersuchen
Grundlegender rekursiver Merge Sort (CPU-Implementierung)
- Ein Sortieralgorithmus, der ein Array in zwei Teilarrays aufteilt, diese jeweils sortiert und anschließend zusammenführt
- Das Array wird rekursiv aufgeteilt; sobald die Größe 1 erreicht, wird der Merge-Schritt ausgeführt
- Wichtige Punkte zur Implementierung
- Verwendung von
uint8_t→ minimaler Speicherverbrauch für kleine Werte (0–255) - Verwendung von
long long→ Verarbeitung großer Arrays (bis zu 10¹⁸) möglich - Zur Validierung der Ergebnisse für den Performance-Vergleich wird
std::sortverwendet - Zeitkomplexität: durchschnittlich O(n log n)
- Speicherkomplexität: O(n)
- Verwendung von
Grundlegender rekursiver Merge Sort in CUDA
- Folgt demselben Muster wie die CPU-Implementierung
- Der Merge-Schritt ist so implementiert, dass er in CUDA parallel ausgeführt wird
- Wichtige Punkte zur Implementierung
- Verwendung von
cudaMalloc,cudaMemcpy,cudaFree→ GPU-Speicherzuweisung und Datenübertragung merge<<<1, 1>>>(...)→ führt den Merge-Schritt parallel auscudaDeviceSynchronize()→ Synchronisation bis zum Abschluss des Merges- Performance-Problem → CUDA ist für rekursive Verarbeitung ineffizient, daher ist ein iterativer Ansatz nötig
- Verwendung von
Vergleich von CPU- und GPU-Implementierung
- Da rekursive Aufrufe auf der CPU ausgeführt werden, kommt es zu Performance-Verlusten
- Rekursive Aufrufe in CUDA verursachen Probleme mit der Stack-Größe und Kernel-Launch-Overhead
- Methode zur Performance-Verbesserung: Umstellung auf einen iterativen Bottom-up-Ansatz
Iterativer Bottom-up-Merge Sort (CPU-Implementierung)
- Beginn mit kleinen Teilarrays und schrittweises Zusammenführen → in CUDA effizienter
- Die Größe der zu mergenden Arrays wird in den Schritten 1, 2, 4, 8, … erhöht
- Wichtige Codestruktur
MERGE_SORT(arr, temp, start, end) FOR sub_size ← 1 TO end STEP 2 × sub_size DO FOR left ← 0 TO end STEP 2 × sub_size DO mid ← MIN(left + sub_size - 1, end) right ← MIN(left + 2 * sub_size - 1, end) MERGE(arr, temp, left, mid, right) ENDFOR ENDFOR END MERGE_SORT - Wichtige Punkte zur Implementierung
- Falls die Array-Größe kein Vielfaches von 2 ist, wird das Problem durch Clamping der Indizes gelöst
- Der Merge-Schritt wird in Schleifen ausgeführt
- Großes Potenzial für Performance-Verbesserungen
Iterativer Bottom-up-Merge Sort (CUDA-Implementierung)
- Performance-Verbesserung durch parallele Ausführung des iterativen Merge Sort
- Zur parallelen Ausführung der Merge-Schritte werden Anzahl der Threads und Blöcke berechnet und dann gestartet
- Wichtige Codestruktur
void mergeSort(uint8_t* arr, uint8_t* temp, long long n) { bool flipflop = true; long long size; for (size = 1; size < n; size *= 2) { numThreads = max(n / (2 * size), (long long)1); gridSize = (numThreads + THREADS_PER_BLOCK - 1) / THREADS_PER_BLOCK; mergeKernel<<<gridSize, THREADS_PER_BLOCK>>>(flipflop ? arr : temp, flipflop ? temp : arr, size, n); CUDA_CHECK(cudaGetLastError()); CUDA_CHECK(cudaDeviceSynchronize()); flipflop = !flipflop; } if (!flipflop) CUDA_CHECK(cudaMemcpy(arr, temp, n * sizeof(uint8_t), cudaMemcpyDeviceToDevice)); } - Wichtige Punkte
flipflop→ Umschalten des Speicherorts für das Merge-ErgebnisgridSize,THREADS_PER_BLOCK→ Parallelisierung der Merge-AufgabenmergeKernel→ weist jedem Thread eine eigene Merge-Aufgabe zu- Indexverwaltung durch Berechnung von Thread- und Block-Indizes
Performance-Ergebnisse
- Bottom-up-Merge Sort (CUDA) → klare Performance-Verbesserung
- Kleine Arrays → CPU ist schneller
- Große Arrays → CUDA hat einen Performance-Vorteil
thrust::sort→ starke GPU-Performance bei großen Arrays- Die Performance-Verbesserung von CUDA wird durch den Overhead der Datenübertragung begrenzt
Fazit und zukünftige Arbeiten
- Erfolgreiche Performance-Verbesserung eines CUDA-basierten Merge Sort
- Wichtige Lernergebnisse:
- Verständnis der CUDA-Konzepte für parallele Verarbeitung und Strategien zum Performance-Tuning
- Iterativer Merge Sort → in CUDA effektiver als ein rekursiver Ansatz
- CUDA-spezifische Performance-Engpässe wie Thread-Synchronisation und Speicherübertragung identifiziert
- Künftige Verbesserungen:
- Aufgabentrennung und Optimierung zwischen CPU und GPU
- Performance-Tests mit noch größeren Arrays
- Kombination von
thrust::sortmit benutzerdefiniertem Implementierungscode - Performance-Optimierung durch Nutzung von Shared Memory
2 Kommentare
Das ist wohl mit Radix Sort in CUDA implementiert; ich habe die Erfahrung gemacht, es anhand der Referenz genauso umzusetzen.
Hacker-News-Kommentare
Es geht nicht darum, wie man auf der GPU schnell sortiert. Der schnellste Algorithmus in CUDA ist Onesweep; er nutzt die Parallelität der GPU und verwendet komplexe Techniken, um Einschränkungen zu überwinden.
Wie auch andere Kommentare anmerken, ist dieser Algorithmus nicht geeignet. Algorithmen wie Onesweep sind cool, aber schwer zu verstehen.
Als kleines Spielzeugproblem ist das interessant. Mit Optionen zur Thread-Abstimmung könnte man die Performance verbessern.
Die Sprache Futhark macht solche Algorithmen auf der GPU bequemer nutzbar.
Das erinnert mich an ein kleines Projekt aus meiner Studienzeit, in dem ich Bitonic Sort in CUDA implementiert habe.
Die Notizen, die das Konzept der GPU-Thread-Indizierung erklären, sind gut.
Ich mag die schnelle Radix-Sort-Implementierung.
Ich wollte das zusammen mit Unity verwenden, konnte den Flaschenhals beim Datentransfer aber nicht überwinden.
Damit sich Sortieren auf der GPU lohnt, braucht man große Arrays.
Um Zeit zu sparen, die Zusammenfassung: Jemand hat einen Sortieralgorithmus für die GPU geschrieben, aber er war langsam.