OpenACC 2.0 : Les nouvelles fonctions de gestion de données
By   |  January 06, 2014

Après une première release qui a beaucoup fait pour faciliter le portage du calcul parallèle sur les accélérateurs – et pour le standardiser – OpenACC connaît aujourd’hui une mise à jour majeure. Penchons-nous donc sans plus attendre sur ses améliorations très attendues, en commençant cette série de trois articles par les nouvelles fonctions de gestion de données – un domaine qui méritait vraiment d’être repensé…

C’est en 2011 que PGI, Cray et NVIDIA ont, avec l’aide de CAPS, sorti la première spécification d’OpenACC. Il s’agissait alors d’une première mouture de la future extension de directives pour accélérateurs d’OpenMP. Après deux ans d’améliorations continues apportées notamment par les nouveaux entrants au consortium, la version 2.0 d’OpenACC vise à devenir le standard de fait de la programmation des accélérateurs par directives en C/C++ et Fortran.

L’une des faiblesses les plus régulièrement reprochées à OpenACC 1.0 est son manque de flexibilité dans la gestion des données. Dans cette version initiale, le chargement de données sur l’accélérateur s’effectue en créant une région parfaitement imbriquée dans le code. Le constructeur data est le moyen le plus simple de créer de telles régions, mais d’autres sont aussi implicitement créées autour des constructeurs de calcul (parallel et kernels). On peut également passer par la directive indépendante declare pour définir une région dans le contexte où la directive est utilisée – région qui peut être soit locale à la procédure courante, soit globale à l’application toute entière. Dans tous les cas, en OpenACC 1.0, les règles de chargement des variables suivent celles du contexte des variables du langage source.

Ce choix ne manque pas de sens du point de vue du compilateur sachant que, par construction, la durée de vie d’une région sur l’accélérateur est entièrement comprise dans la durée de vie de la variable à laquelle elle réfère sur le processeur hôte. Mais une telle stratégie pose des problèmes dans des applications qui utilisent des structures de données plus complexes. OpenACC 1.0 ne fournit par exemple aucun moyen de gérer un nombre dynamique de données sur l’accélérateur – à part bien sûr quelques astuces telles que la création d’une fonction récursive pour encapsuler une région de données autant de fois que nécessaire, ce qui manque clairement d’élégance. Il est par ailleurs impossible d’écrire du code dans lequel les données sur l’accélérateur sont allouées et libérées depuis des fonctions différentes. Or, c’est typiquement le cas des applications orientées objets où ces manipulations sont logiquement réalisées dans les constructeurs et destructeurs.

Ce dernier cas est illustré au listing 1. Une fonction doit traiter par paires un nombre arbitraire n de structures de données fournies par un vecteur de pointeurs. Etant donné qu’OpenACC 1.0. ne fournit aucune solution pour charger les n structures simultanément, le code ne garde en mémoire que deux structures à la fois, ce qui n’est évidemment pas satisfaisant puisque chacune des n structures doit être chargée en moyenne n/2 fois. Les performances pourraient sans doute être améliorées en utilisant l’astuce récursive mentionnée plus haut, ou en gérant plus d’une structure par région de données, mais cela nécessiterait de modifier en profondeur le code d’origine, ce qui n’est pas exactement l’objectif d’une API de programmation par directives telle qu’OpenACC.

La gestion de données devient dynamique

OpenACC 2.0 résout la plupart de ces problèmes en introduisant de nouvelles fonctionnalités de gestion dynamique des données. Deux directives indépendantes, enter data et exit data, peuvent être appelées pour respectivement créer ou détruire les données sur l’accélérateur. Utilisées ensemble, elles correspondent à une construction data, à ceci près qu’elles ne nécessitent aucune imbrication. Elles peuvent donc être placées n’importe où dans le code et, contrairement aux autres constructeurs, peuvent même être exécutées de façon asynchrone.

Le listing 2 montre comment le code de notre premier exemple peut être optimisé avec OpenACC 2.0. Chacune des n structures est maintenant chargée sur l’accélérateur en une seule fois (un copyin et un copyout). Notez que les directives enter data et exit data ont leur équivalent dans l’API (appels acc_copyin, acc_copyout, acc_delete, etc.).

Cela étant, comme du point de vue langage les directives enter data et exit data ne créent pas une région de données en tant que telle, la clause present reste nécessaire dans le constructeur parallel pour rendre les données accessibles depuis la région parallèle. Cette clause ne provoque aucune affectation ni aucun transfert de données. Les directives enter data et exit data ainsi que leurs équivalents dans l’API sont donc particulièrement adaptées à l’écriture de bibliothèques dans lesquelles les données sont gérées et utilisées par de multiples fonctions.

La plupart des fonctionnalités de gestion de données présentes dans les directives ou les constructeurs sont maintenant également proposées dans l’API. C’est le cas par exemple de acc_copyin(), acc_present_or_copyin(), acc_create() et acc_present_or_create(), qui correspondent aux clauses autorisées dans la directive enter data, tandis qu’acc_copyout() et acc_delete() correspondent aux clauses de la directive exit data. Toutes ces fonctions sont malheureusement synchrones mais il est probable que des alternatives asynchrones seront disponibles dans une prochaine version d’OpenACC.

Les développeurs qui souhaiteraient implémenter leurs propres fonctions de gestion des données trouveront ce qu’il faut parmi les fonctions de bas niveau de l’API : acc_malloc() et acc_free() allouent directement la mémoire sur l’accélérateur, acc_map_data() et acc_unmap_data() associent les adresses des variables sur le processeur et sur l’accélérateur, acc_deviceptr() résout l’adresse accélérateur associée à une adresse CPU cependant que acc_hostptr() réalise l’opération l’inverse. Enfin, acc_memcpy_to_device() et acc_memcpy_from_device() transfèrent des régions mémoire arbitraires entre hôte et accélérateur.

Les directives de données indépendantes (et leurs appels API correspondants) peuvent être utilisées avec les constructeurs de données mais les données sur l’accélérateur qui ont été créées avec un certain type de directive ne peuvent être détruites avec un autre. Les directives de gestion des données n’ont pas connaissance les unes des autres (souvenez-vous, elles sont “indépendantes”), de sorte qu’il serait incorrect de vouloir changer la sémantique d’une clause present_or_copyin simplement en appelant acc_present_or_copyin() puis acc_delete(). La bonne façon de faire, illustrée au listing 3, implique de ne réaliser ces deux appels que si les données ne sont pas déjà présentes sur l’accélérateur.

Navigation

<12>

© HPC Today 2024 - All rights reserved.

Thank you for reading HPC Today.

Express poll

Do you use multi-screen
visualization technologies?

Industry news

Brands / Products index