Algorithme de tri utilisant CUDA
(ashwanirathee.com)- Comment améliorer les performances d’un algorithme de tri via le calcul parallèle avec CUDA
- Exploration du potentiel de gain de performances en implémentant un tri par fusion (
merge sort) de base avec CUDA
Tri par fusion récursif de base (implémentation CPU)
- Algorithme de tri qui divise le tableau en deux sous-tableaux, les trie séparément, puis les fusionne
- Le tableau est divisé récursivement, puis l’opération de fusion est effectuée lorsque la taille atteint 1
- Points clés de l’implémentation
- Utilisation de
uint8_t→ réduit l’usage mémoire avec de petites valeurs (0 à 255) - Utilisation de
long long→ permet de traiter de grands tableaux (jusqu’à 10¹⁸) - Validation des résultats avec
std::sortpour comparer les performances - Complexité temporelle : O(n log n) en moyenne
- Complexité spatiale : O(n)
- Utilisation de
Tri par fusion récursif de base avec CUDA
- Suit le même schéma que l’implémentation CPU
- Implémentation de l’opération de fusion pour une exécution parallèle avec CUDA
- Points clés de l’implémentation
- Utilisation de
cudaMalloc,cudaMemcpy,cudaFree→ allocation de mémoire GPU et transfert de données merge<<<1, 1>>>(...)→ exécution parallèle de l’opération de fusioncudaDeviceSynchronize()→ synchronisation jusqu’à la fin de la fusion- Problème de performance → CUDA étant inefficace pour la récursion, une approche itérative est nécessaire
- Utilisation de
Comparaison entre les implémentations CPU et GPU
- Baisse de performances car les appels récursifs s’exécutent sur le CPU
- Avec CUDA, les appels récursifs posent des problèmes de taille de pile et de surcharge d’exécution des kernels
- Méthode d’amélioration des performances : passer à une approche itérative (bottom-up)
Tri par fusion itératif bottom-up (implémentation CPU)
- Fusion progressive à partir de petits sous-tableaux → plus efficace avec CUDA
- La taille des blocs à fusionner augmente selon 1, 2, 4, 8, …
- Structure principale du code
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 - Points clés de l’implémentation
- Si la taille du tableau n’est pas un multiple de 2, le problème est résolu en limitant les index
- Les opérations de fusion sont exécutées dans une boucle
- Fort potentiel d’amélioration des performances
Tri par fusion itératif bottom-up (implémentation CUDA)
- Amélioration des performances en exécutant le tri par fusion itératif en parallèle
- Exécution après calcul du nombre de threads et de blocs pour paralléliser l’opération de fusion
- Structure principale du code
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)); } - Points clés
flipflop→ alterne l’emplacement de stockage du résultat de la fusiongridSize,THREADS_PER_BLOCK→ parallélisation des opérations de fusionmergeKernel→ assigne à chaque thread une opération de fusion propre- Gestion des index via le calcul des index des threads et des blocs
Résultats de performance
- Tri par fusion bottom-up (CUDA) → amélioration nette des performances
- Petits tableaux → le CPU est plus rapide
- Grands tableaux → CUDA prend l’avantage en performances
thrust::sort→ meilleures performances GPU sur les grands tableaux- Le gain de performances de CUDA est limité par la surcharge des transferts de données
Conclusion et travaux futurs
- Amélioration réussie des performances du tri par fusion basé sur CUDA
- Principaux enseignements :
- Compréhension des concepts de traitement parallèle de CUDA et des stratégies de réglage des performances
- Le tri par fusion itératif est plus efficace avec CUDA qu’une approche récursive
- Identification de goulets d’étranglement propres à CUDA, comme la synchronisation des threads et les transferts mémoire
- Améliorations futures :
- Séparer et optimiser davantage le travail entre CPU et GPU
- Tester les performances sur des tableaux plus grands
- Combiner
thrust::sortavec une implémentation personnalisée - Optimiser les performances via l’utilisation de mémoire partagée
2 commentaires
Il semble que cela ait été implémenté en radix sort dans CUDA, et j’ai déjà eu l’expérience de reproduire exactement la même implémentation en m’en inspirant.
Avis Hacker News
Il ne s’agit pas d’une méthode rapide pour trier sur GPU. L’algorithme le plus rapide sur CUDA est Onesweep, qui utilise des techniques complexes pour exploiter le parallélisme du GPU et dépasser ses limites
Comme d’autres l’ont signalé, cet algorithme n’est pas approprié. Des algorithmes comme Onesweep sont impressionnants, mais difficiles à comprendre
C’est un problème-jouet intéressant. En utilisant les options de coordination des threads, il pourrait y avoir des gains de performance
Le langage Futhark permet d’utiliser plus commodément ce type d’algorithmes sur GPU
Cela rappelle un petit projet universitaire où un tri bitonique avait été implémenté en CUDA
Les notes expliquant le concept d’indexation des threads GPU sont bonnes
J’aime cette implémentation rapide de tri par base
J’ai essayé de l’utiliser avec Unity, mais je n’ai pas réussi à surmonter le goulot d’étranglement des transferts de données
Pour que le tri sur GPU en vaille la peine, il faut de grands tableaux
Pour résumer et faire gagner du temps : quelqu’un a écrit un algorithme de tri pour GPU, mais il était lent