Points Clés
- Des dispositifs hétérogènes sont maintenant présents dans presque tous les systèmes informatiques.
- Les programmeurs doivent gérer un ensemble de périphériques aussi large et diversifié, tels que les GPU, les FPGA ou tout autre matériel à venir.
- TornadoVM peut être considéré comme une plate-forme informatique hautes performances pour Java et la JVM qui fonctionne en combinaison avec les JDK existants.
- Avec TornadoVM, le même code source peut être exécuté en parallèle, en tirant parti des capacités du périphérique, telles que les CPU, les GPU ou les FPGA.
- Les API de TornadoVM permettent aux non-experts de tirer parti de l'informatique parallèle tout en permettant le portage du code CUDA et OpenCL vers Java et TornadoVM.
À QCon Plus, Juan Fumero a parlé de TornadoVM, une plate-forme de calcul haute performance pour la machine virtuelle Java (JVM). Il permet aux développeurs Java d'exécuter automatiquement des programmes sur des GPU, des FPGA ou des CPU multicœurs.
Les dispositifs hétérogènes tels que les GPU sont présents dans presque tous les systèmes informatiques aujourd'hui. Par exemple, les appareils mobiles contiennent un processeur multicœur plus un GPU intégré ; les ordinateurs portables ont généralement deux GPU : un intégré au processeur principal et un dédié, généralement pour les jeux. Même les centres de données intègrent également des dispositifs tels que les FPGA. Par conséquent, les appareils hétérogènes sont là pour rester.
Tous ces périphériquess contribuent à augmenter les performances et à exécuter des charges de travail plus efficacement. Les programmeurs de systèmes informatiques actuels et futurs doivent gérer l'exécution de programmes sur un ensemble large et diversifié de dispositifs informatiques. Cependant, de nombreux frameworks de programmation parallèle pour ces périphériques sont basés sur les langages de programmation C et C++. La programmation de tels systèmes à partir de langages de programmation gérés et de haut niveau tels que Java est presque absente. C'est pourquoi nous avons introduit TornadoVM.
En un mot, TornadoVM est une plate-forme de programmation high-computing pour Java et la JVM, permettant de délester, au moment de l'exécution, du code Java pour qu'il s'exécute sur des accélérateurs matériels hétérogènes.
TornadoVM propose une API Parallel Loop et une API Parallel Kernel. Dans cet article, nous expliquons chacun d'eux, ainsi que quelques tests de performance, puis comment Tornado traduit le code Java pour le matériel parallèle réel. Enfin, nous montrons comment TornadoVM est piloté dans l'industrie, y compris certains cas d'utilisation.
Accès rapide aux GPU et FPGA
Comment accède-t-on aujourd'hui à du matériel hétérogène à partir de langages de programmation de haut niveau ? L'image suivante présente quelques exemples de matériel (CPU, GPU, FPGA) et de langages de programmation de haut niveau tels que Java, R ou Python.
Si nous regardons Java, nous voyons qu'il s'exécute sur une machine virtuelle. Entre autres, OpenJDK, GraalVM et Corretto sont des implémentations de machines virtuelles (VM). Essentiellement, l'application est traduite du code source Java en bytecode Java, puis la machine virtuelle exécute ce bytecode. Si l'application est exécutée fréquemment, la machine virtuelle peut optimiser l'exécution en compilant des méthodes fréquemment exécutées dans un code machine optimisé, mais uniquement pour les processeurs.
Si les développeurs souhaitent accéder à des périphériques hétérogènes, tels que des GPU ou des FPGA, ils le font généralement via la bibliothèque Java Native Interface (JNI).
Essentiellement, les programmeurs doivent importer une bibliothèque et invoquer cette bibliothèque via des appels JNI. Notez qu'en utilisant ces bibliothèques, les programmeurs peuvent avoir une application optimisée pour un GPU particulier. Mais si l'application ou le GPU change, il peut être nécessaire de reconstruire l'application ou de réajuster les paramètres d'optimisation. De même, cela se produit également avec différents fournisseurs de FPGA ou même d'autres modèles de GPU.
Ainsi, il n'existe pas de compilateurs et d'environnements d'exécution JIT complets qui fonctionnent avec des périphériques hétérogènes de la même manière que les processeurs, en ce sens qu'ils peuvent détecter le code fréquemment exécuté et produire un code optimisé pour le matériel hétérogène. C'est là que TornadoVM entre en scène.
TornadoVM fonctionne en combinaison avec un JDK existant. Il s'agit d'un plugin du JDK qui permet aux programmeurs d'exécuter des applications sur du matériel hétérogène. Actuellement, TornadoVM peut fonctionner sur des processeurs multicœurs, des GPU et des FPGA.
Caractéristiques matérielles et parallélisme
La prochaine question qui se pose est, pourquoi tout ce matériel ? Trois architectures matérielles différentes sont envisagées : CPU, GPU et FPGA. Chaque architecture est optimisée pour différents types de charges de travail.
Par exemple, les processeurs sont optimisés pour les applications à faible latence, tandis que les GPU sont optimisés pour un débit élevé. Les FPGA sont un mélange des deux : les FPGA peuvent généralement atteindre une latence très faible et un débit élevé car les applications sont physiquement connectées au matériel.
Associons ces architectures aux types de parallélisme existants. Dans la littérature, nous pouvons trouver trois principaux types de parallélisme : la parallélisation des tâches, la parallélisation des données et la parallélisation des pipelines.
Habituellement, les processeurs sont optimisés pour la parallélisation des tâches, ce qui signifie que chaque cœur peut exécuter des tâches différentes et indépendantes. En revanche, les GPU sont optimisés pour exécuter la parallélisation des données, ce qui signifie que les fonctions et les noyaux exécutés sont les mêmes mais prennent des données d'entrée différentes. Enfin, les FPGA sont très appropriés pour exprimer la parallélisation de pipeline, dans laquelle l'exécution de différentes instructions se chevauche à travers les différentes étapes internes.
Idéalement, nous voulons un cadre de programmation parallèle de haut niveau qui peut exprimer les différents types de parallélisme afin de maximiser les performances pour chaque type de périphériques. Voyons maintenant comment TornadoVM est construit et comment les développeurs peuvent l'utiliser pour exprimer différents types de parallélisme.
Présentation de TornadoVM
TornadoVM est un plugin du JDK (Java Development Kit) qui permet aux développeurs Java d'exécuter automatiquement des programmes sur du matériel hétérogène. Les principales contributions de TornadoVM sont les suivantes :
Il dispose d'un compilateur JIT (Just In Time) optimisé qui spécialise le code par architecture. Cela signifie que, par exemple, le code généré pour les GPU est donc différent du code généré pour les CPU et les FPGA afin de maximiser les performances pour chaque architecture.
TornadoVM effectue une migration dynamique des tâches entre les architectures et entre les périphériques. Par exemple, il peut exécuter l'application sur un GPU pendant un certain temps, en migrant l'exécution plus tard sur un autre GPU, FPGA ou multicœur, si nécessaire et sans redémarrer l'application.
TornadoVM est entièrement indépendant du matériel : le code source de l'application à exécuter sur du matériel hétérogène est le même pour l'exécution sur des GPU, des CPU et des FPGA.
Enfin, il peut être utilisé avec plusieurs fournisseurs de JDK. Il est open-source (disponible sur GitHub), et les images Docker sont également disponibles pour s'exécuter sur des GPU NVIDIA et Intel intégrés distincts.
La pile système TornadoVM
Regardons la pile système de TornadoVM. Au niveau supérieur, TornadoVM expose une API. C'est parce qu'il exploite le parallélisme, mais il ne détecte pas la parallélisation. Ainsi, il a besoin d'un moyen d'identifier où la parallélisation est utilisée dans le code source du programme.
TornadoVM propose une API de programmation basée sur les tâches dans laquelle chaque tâche correspond à une méthode Java existante. Ainsi, TornadoVM compile le code au niveau de la méthode comme le JDK ou la JVM mais en code efficace pour les GPU et les FPGA. Les annotations peuvent également être utilisées pour indiquer le parallélisme au sein des méthodes. De plus, les méthodes peuvent être regroupées en tâches compilées ensemble dans une unité de compilation. Cette unité de compilation est appelée Task-Schedule : une Task-Schedule porte un nom (à des fins de débogage et d'optimisation) et contient un ensemble de tâches.
Le moteur TornadoVM prend ses expressions d'entrée au niveau du bytecode et génère automatiquement du code pour différentes architectures. Il dispose actuellement de trois backends qui génèrent du code OpenCL, CUDA et SPIR-V. Les développeurs peuvent sélectionner lequel utiliser. Alternativement, TornadoVM sélectionnera un backend par défaut.
Un filtre de flou comme exemple
Nous allons maintenant voir comment TornadoVM peut accélérer les applications Java avec un exemple : un filtre de flou. Essentiellement, nous avons une image et nous voulons appliquer un effet de flou à cette image.
Avant d'entrer dans les détails de sa programmation, regardons les performances de cette application fonctionnant sur du matériel hétérogène. L'image ci-dessous montre des références pour quatre implémentations différentes. La référence est une implémentation séquentielle en Java, et l'axe Y représente le gain de performances par rapport à cette référence, donc plus c'est élevé, mieux c'est.
Les deux premières colonnes à partir de la gauche représentent les exécutions basées sur le processeur. Le premier utilise des flux Java parallèles standard, tandis que le second utilise TornadoVM sur plusieurs cœurs de processeurs, ce qui donne une accélération de 11x et 17x, respectivement. TornadoVM produit un meilleur résultat car il génère de l'OpenCL pour le CPU, et OpenCL est très bon pour vectoriser le code pour utiliser des unités vectorielles. Si l'application est exécutée sur des graphiques intégrés, nous pouvons obtenir des performances jusqu'à 19 fois supérieures à celles de l'implémentation séquentielle Java. Si nous exécutons l'application sur un GPU NVIDIA distinct (2060), nous pouvons obtenir des performances jusqu'à 340x (en utilisant le backend OpenCL de TornadoVM). En comparant les accélérations que nous obtenons par rapport à la version parallèle des flux Java, que nous pouvons obtenir dès maintenant en Java, TornadoVM atteint des performances jusqu'à 30 fois supérieures lorsqu'il s'exécute sur le GPU NVIDIA.
Implémentation de l'exemple de filtre de flou
Le filtre de flou est un opérateur de transformation qui applique une fonction (le filtre d'effet de flou) pour chaque pixel d'image d'entrée. Ce modèle est idéal pour la parallélisation car chaque pixel peut être calculé indépendamment de tout autre pixel.
La première chose à faire dans TornadoVM est d'annoter le code dans chaque méthode Java pour indiquer à TornadoVM comment les paralléliser.
Étant donné que les calculs de chaque pixel peuvent se produire en parallèle, nous ajoutons l'annotation @Parallel
aux deux boucles les plus externes. Cela signale à TornadoVM de calculer ces deux boucles entièrement en parallèle. Les annotations dans le code définissent le modèle de parallélisation des données.
La deuxième chose est de définir les tâches. Étant donné que l'entrée est une image RVB, nous pouvons créer une tâche par canal de couleur - canaux rouge, vert et bleu (RVB). Par conséquent, nous allons traiter le filtre de flou par canal. Un objet TaskSchedule
qui contient trois tâches est utilisé à cette fin.
De plus, il est nécessaire de définir quelles données seront transférées vers et depuis le heap Java vers le périphérique (par exemple, un GPU). En effet, les GPU et FPGA distincts ne partagent généralement pas la mémoire. Par conséquent, nous avons besoin d'un moyen d'indiquer à TornadoVM quelles régions de mémoire (tableaux) doivent être copiées dans et hors du périphérique. Cela se fait via les fonctions streamIn()
et streamOut()
.
Ensuite, l'ensemble des tâches est défini, un par canal de couleur. Ils sont identifiés par un nom et composés d'une référence à la méthode à exécuter avec ses paramètres. Cette méthode peut maintenant être compilée dans un noyau.
Enfin, la fonction d'exécution est appelée pour exécuter les tâches en parallèle sur le périphérique. Voyons maintenant comment TornadoVM compile et exécute le code.
Comment TornadoVM lance les noyaux Java sur du matériel parallèle
Le code Java d'origine est monothread, même s'il a reçu des annotations @Parallel. Cependant, lorsque la fonction execute() est appelée, TornadoVM commence à optimiser le code.
Tout d'abord, le code est compilé dans une représentation intermédiaire pour l'optimisation (TornadoVM étend le compilateur Graal JIT ; toutes les optimisations se produisent à ce niveau). Ensuite, TornadoVM traduit le code optimisé en code PTX, OpenCL ou SPIR-V efficace.
À ce stade, le code est exécuté, ce qui provoque le lancement de centaines ou de milliers de threads. Le nombre de threads exécutés par TornadoVM dépend de l'application.
Dans cet exemple, le filtre de flou a deux boucles parallèles qui itèrent sur une dimension d'image chacune. Par conséquent, TornadoVM crée une grille de threads avec les mêmes dimensions que l'image d'entrée pendant la compilation à l'exécution. Chaque cellule de la grille - en d'autres termes, chaque pixel - est associé à un thread. Par exemple, si l'image a 2000 x 2000 pixels, TornadoVM lance 2000 x 2000 threads sur le périphérique cible (par exemple, un GPU) ;
TornadoVM peut également permettre la parallélisation de pipelines, qui se fait principalement sur les FPGA. Lorsque nous sélectionnons un FPGA à exécuter, ou que Tornado sélectionne le FPGA à exécuter, il insère automatiquement des informations dans le code généré dans les instructions du pipeline. Cette stratégie peut doubler les performances par rapport au code parallèle précédent.
L'API Parallel Loop par rapport à l'API Parallel Kernel
Parlons maintenant de la façon dont les noyaux de calcul peuvent être exprimés dans TornadoVM. TornadoVM a deux API : une API Parallel Loop comme nous l'avons décrit dans notre exemple de filtre de flou, et une API Parallel Kernel. L'API Parallel Loop de TornadoVM est basée sur des annotations. Avec cette API, les développeurs doivent raisonner sur leur code séquentiel, fournir une implémentation séquentielle, puis réfléchir à l'endroit où paralléliser les boucles.
D'une part, le développement est accéléré car les développeurs peuvent simplement ajouter des annotations dans le code séquentiel Java existant pour obtenir du code parallèle. L'API Parallel Loop convient aux utilisateurs non experts, qui n'ont pas besoin de connaître les détails des calculs GPU ou le matériel à utiliser.
D'autre part, l'API Parallel Loop est limitée dans le nombre de modèles qu'elle peut utiliser. Avec cette API, les développeurs peuvent exécuter des applications à l'aide du pattern map/reduce typique. Cependant, d'autres modèles parallèles, tels que des scans ou des gabarits complexes, sont difficiles à implémenter avec cette API. De plus, cette API ne permet pas au développeur de contrôler le matériel car elle est agnostique, mais certains développeurs ont besoin de ce contrôle. De plus, il peut être difficile de porter le code OpenCL et CUDA existant vers Java.
Pour surmonter ces limitations, nous avons ajouté l'API Parallel Kernel.
Implémentation du filtre de flou à l'aide de l'API Parallel Kernel
Revenons à notre exemple précédent : le filtre de flou. Nous avons deux boucles parallèles qui itèrent sur les deux dimensions de l'image et calculent le filtre. Cela peut être traduit dans l'API Kernel.
Au lieu d'avoir deux boucles, nous introduisons un parallélisme implicite à travers un contexte noyau. Un contexte est un objet TornadoVM dont l'utilisateur peut tirer parti, en donnant accès à l'identifiant de thread pour chaque dimension, ainsi qu'à la mémoire locale/partagée, aux primitives de synchronisation, etc.
Dans notre exemple, les coordonnées des axes X et Y du filtre sont extraites des attributs globalIdx et globalIdy du contexte, respectivement, et sont utilisées pour calculer le filtre comme d'habitude. Ce style de programmation est plus proche des modèles de programmation CUDA et OpenCL.
En remarque, TornadoVM ne peut pas déterminer le nombre nécessaire de threads lors de l'exécution avec l'API Kernel. L'utilisateur doit les configurer à la place en utilisant une worker-grid.
Dans cet exemple, un worker-grid 2D est créée avec les dimensions de l'image et associée au nom de la fonction. Lorsque le code de l'utilisateur appelle la fonction execute()
, la grille est transmise et le filtre est exécuté en conséquence.
Les points forts de TornadoVM
Mais, si l'API Parallel Kernel est plus proche des modèles de programmation de bas niveau, pourquoi utiliser Java au lieu d'OpenCL et PTX, ou CUDA et PTX, surtout s'il y a du code existant ?
TornadoVM possède également d'autres atouts, tels que la migration des tâches en direct, la gestion automatique de la mémoire et l'optimisation transparente du code, de sorte que le code est spécialisé en fonction de l'architecture.
Il fonctionne également sur des FPGA avec un flux de travail de programmation entièrement transparent et intégré. Vous pouvez utiliser votre IDE préféré, par exemple, IntelliJ ou Eclipse, pour exécuter du code sur un FPGA.
Il peut également être déployé sur le cloud, par exemple, des instances Amazon. Vous obtenez toutes ces fonctionnalités gratuitement en transférant ce code dans Java et TornadoVM.
Performance
Parlons performances. TornadoVM peut être utilisé pour plus que la simple application de filtres pour la retouche d'images informatique. Par exemple, pour la FinTech, ou des simulations mathématiques comme Monte Carlo ou Black-Scholes. Il peut également être utilisé pour les applications de vision par ordinateur, la simulation physique, le traitement du signal, parmi de nombreux autres domaines.
Le graphique de la figure précédente compare différentes exécutions d'applications sur des périphériques distincts. Encore une fois, la référence est une exécution séquentielle et les barres représentent les facteurs d'accélération, donc plus ils sont élevés, mieux c'est.
Comme on peut le voir, il est possible d'atteindre des accélérations très élevées ; par exemple, le traitement du signal ou la simulation physique peut être jusqu'à quatre mille fois plus rapide qu'une exécution séquentielle en Java. Pour une analyse détaillée de tous ces résultats, vous pouvez consulter la liste des publications académiques.
TornadoVM et l'industrie
Certaines entreprises du secteur pilotent également TornadoVM. La figure ci-dessus montre deux cas d'utilisation différents de TornadoVM en cours d'élaboration.
Un cas d'utilisation utilisant TornadoVM provient de la compagnie Neurocom au Luxembourg, qui exécute un algorithme de traitement du langage naturel. Jusqu'à présent, ils ont multiplié par 30 les performances en exécutant leurs algorithmes de clustering hiérarchique sur des GPU.
Un autre cas d'utilisation provient de la compagnie Spark Works, une société basée en Irlande qui traite les informations provenant de prériphériques IoT. Un GPU puissant, GPU100, est utilisé pour exécuter ce post-traitement. Ils peuvent obtenir des performances jusqu'à 460 fois supérieures à celles de Java, ce qui est plutôt bon.
Vous pouvez visiter le site Web de TornadoVM pour une liste complète des cas d'utilisation.
Résumé
Des dispositifs hétérogènes sont maintenant présents dans presque tous les systèmes informatiques. Il n'y a pas d'échapatoire. Ils sont là et ils resteront.
Par conséquent, les programmeurs de systèmes logiciels informatiques actuels et futurs doivent gérer la complexité d'avoir un ensemble large et diversifié de périphériques, tels que les GPU, les FPGA ou tout autre matériel à venir. Ils peuvent programmer ces périphériques via TornadoVM.
TornadoVM peut être considéré comme une plate-forme informatique hautes performances pour Java et la JVM qui fonctionne en combinaison avec les JDK existants. Par exemple, avec OpenJDK.
Cet article a présenté TornadoVM, ce qu'il est, et a brièvement expliqué comment cela fonctionne. De plus, il a montré comment les développeurs pouvaient bénéficier d'une exécution matérielle hétérogène à travers un exemple de photographie informatique implémentée en Java. Nous avons expliqué les deux API pour la programmation hétérogène dans TornadoVM : l'une utilise l'API Parallel Loop, adaptée aux non-experts en calcul parallèle ; l'autre s'appuie sur l'API Parallel Kernel, adaptée aux développeurs experts qui connaissent déjà CUDA et OpenCL et souhaitent porter du code existant dans TornadoVM.