- La loi de Moore atteint ses limites, et le matériel évolue d’une accélération du cœur unique vers une approche centrée sur le multicœur et le traitement parallèle
- Le traitement parallèle se décline en plusieurs formes, comme le parallélisme de données, le parallélisme de modèle et le parallélisme par pipeline, qui sont utilisés de manière hybride dans les systèmes modernes de deep learning
- La parallélisation s’effectue à différentes couches, notamment avec le SIMD (parallélisme de données au niveau des instructions), le parallélisme des threads/cœurs et le parallélisme massif des GPU
- Les langages, bibliothèques et outils pour le calcul parallèle sont le plus souvent des extensions « greffées » à des langages séquentiels existants ; la tendance consistant à intégrer nativement le parallélisme dans le langage lui-même (comme avec Mojo) attire donc l’attention
- Des optimisations concrètes de performance, comme l’optimisation du partage de lignes de cache (interactions inutiles), le partitionnement efficace de la mémoire et la vectorisation automatique, constituent des enjeux majeurs
Motivation du parallélisme et évolution du matériel
- Au départ, les performances augmentaient naturellement grâce à la miniaturisation des transistors et à la hausse de la fréquence d’horloge, mais des limites physiques liées à la dissipation thermique et aux procédés de fabrication ont fini par être atteintes
- Par la suite, les architectures multicœurs sont devenues la norme, avec quelques unités jusqu’à plusieurs centaines de cœurs intégrés dans un même CPU
Formes générales du parallélisme
- Parallélisme de données : application simultanée de la même opération à de nombreuses données (ex. : addition de vecteurs)
- Parallélisme de modèle : répartition d’un même modèle sur plusieurs appareils
- Parallélisme par pipeline : division du calcul en plusieurs étapes, chacune fonctionnant en parallèle
SIMD (Single Instruction, Multiple Data) et vectorisation
- Le SIMD consiste à traiter plusieurs données (vecteurs) avec une seule instruction ; il est pris en charge par différentes ISA comme ARM NEON ou x86 SSE/AVX
- Les intrinsics C/C++ permettent de contrôler explicitement les opérations vectorielles ; la vectorisation automatique du compilateur est également disponible, mais elle a ses limites
- En pratique, on traite les données par blocs correspondant à la longueur vectorielle, puis les éléments restants sont corrigés via des opérations scalaires
Parallélisation sur CPU
- Les threads permettent une exécution parallèle sur des architectures multicœurs, avec le support des API propres aux langages et de l’ordonnanceur du système d’exploitation
- Comme le coût de création et de destruction des threads est élevé, il est plus efficace de répartir le travail avec un nombre de threads adapté à la taille des données, généralement proche du nombre de cœurs
- L’optimisation du « false sharing » des lignes de cache (baisse de performances lorsque différents threads accèdent à des variables indépendantes situées sur la même ligne de cache) est importante ; on peut par exemple utiliser
std::hardware_destructive_interference_size de C++17
- Il faut appliquer du padding/de l’alignement afin que chaque thread écrive dans une zone de données distincte
Parallélisation sur GPU
- Les GPU sont spécialisés dans le traitement parallèle massif de données grâce à des milliers de petits cœurs
- CUDA/OpenCL : les fonctions kernel s’exécutent par dizaines jusqu’à dizaines de milliers de threads/blocs, selon un modèle interne de SIMT (Single Instruction, Multiple Threads)
- Le fonctionnement par work groups/warps rend la minimisation de la divergence de branchement (branch divergence) cruciale pour les performances
- La hiérarchie mémoire exige des optimisations à plusieurs niveaux : registres de thread, mémoire partagée au niveau du bloc, mémoire globale, etc.
Triton : un DSL de kernels GPU basé sur Python
- Triton est un DSL embarqué dans Python, qui prend en charge la compilation JIT et plusieurs backends (MLIR/LLVM/PTX, etc.)
- Le code des kernels s’écrit en Python de haut niveau, avec prise en charge de la parallélisation automatique, du partitionnement, du masking, etc.
- Il atteint 75 à 90 % des performances de NVIDIA cuDNN tout en réduisant fortement la complexité de développement
Le parallélisme comme composant natif du langage : Mojo
- Mojo est un nouveau langage créé par Chris Lattner, développeur de LLVM/MLIR, qui prend en charge le parallélisme et la compilation spécialisée matériel au niveau du langage
- Types vectoriels SIMD, fonctions vectorisées, distinction entre mémoire hôte et mémoire device : le système de types et la structure du langage intègrent le parallélisme en natif
- Même les boucles de style Python peuvent être vectorisées automatiquement, ce qui permet d’obtenir de bonnes performances sans contrôle explicite de bas niveau
Conclusion et perspectives
- La programmation parallèle moderne repose sur une combinaison de différents matériels et de plusieurs modèles de parallélisme, et le support direct par le langage devient de plus en plus important
- Avec l’essor de langages et outils parallèles de nouvelle génération comme Mojo, Triton ou JAX, la parallélisation évolue vers une approche plus intuitive et plus productive
- La programmation parallèle ne peut maximiser les performances réelles que si l’architecture matérielle, l’optimisation mémoire et le support du langage sont étroitement articulés
Aucun commentaire pour le moment.