IRPF90, un générateur de code Fortran pour le calcul scientifique
By   |  March 10, 2014

Apporter des modifications à un code peut vite devenir une tâche dantesque tant il faut s’assurer qu’aucune autre fonctionnalité n’est cassée. C’est encore plus vrai avec les anciens codes Fortran contenant des milliers de lignes. Mais voici IRPF90, un outil qui non seulement gère pour vous ces dépendances mais peut aussi optimiser vos codes…

Anthony Scemama, Ph.D. – Laboratoire de Chimie et Physique Quantiques, CNRS-Université de Toulouse.

Aujourd’hui, de par leur taille, certains codes scientifiques Fortran deviennent extrêmement difficiles à maintenir. Cette complexité provient essentiellement des dépendances entre les entités qui composent ces codes. Plus elles sont interdépendantes, plus l’application est complexe. Pour que le code reste sous contrôle, le développeur qui y apporte des modifications doit en connaître les conséquences sur tous les chemins d’exécution. Et lorsque, comme c’est souvent le cas, le code a été écrit par plusieurs développeurs et qu’il met en musique des centaines de milliers de lignes, la difficulté peut vite devenir ingérable. La machine, en revanche, est tout-à-fait capable de faire ce travail à notre place. Elle peut gérer les dépendances entre variables à la manière d’un Makefile qui contrôle les dépendances entre les unités de compilation.IRPF90 est un générateur de code Fortran qui, de façon schématique, permet au développeur d’écrire les noyaux de calcul sans se soucier des dépendances. IRPF90 génère le code de liaison entre ces noyaux et produit un code final qui tient compte des relations de dépendance entre les variables du programme. Ainsi, même les codes de très grande taille peuvent maintenant rester entièrement sous contrôle.

I – Présentation d’IRPF90

Un programme (ou sous-programme) scientifique est une fonction complexe de ses données. Il peut être représenté en arbre dont la racine est la sortie du programme, les feuilles les données, les nœuds les variables intermédiaires et les segments les relations “a besoin de”.

Par exemple, le programme qui calcule t( u(d1,d2), v( u(d3,d4), w(d5) ) ) avec les fonctions t, u, v, w définies au listing 1 peut être représenté par l’arbre de la Figure 1. L’écriture de ce programme en Fortran demanderait au développeur d’avoir cet arbre en tête pour écrire le code du listing 2 qui instruit la machine de réaliser les opérations étape par étape.

On parle alors de programmation impérative. Si les étapes ne sont pas réalisées dans le bon ordre alors le programme est faux. C’est pourquoi l’état de l’ensemble du programme, et donc ses dépendances entre variables, doit être connu à chacune de ses lignes. Dans cette approche, la conception du programme est réalisée en parcourant l’arbre représentatif des feuilles vers la racine.Ce même programme peut maintenant être écrit, comme au listing 3, selon une autre approche. Au lieu d’instruire la machine de ce qu’elle doit faire étape par étape, on peut lui indiquer ce qu’on désire réaliser. Cela revient à concevoir le programme en parcourant son arbre de la racine vers les feuilles.

Ici, les relations de dépendance entre les variables résident dans l’appel à la fonction t, de sorte que le développeur n’a plus à spécifier une séquence particulière d’opérations. Il ne connaît d’ailleurs pas a priori l’ordre dans lequel s’effectuent les appels aux fonctions w(d5) et u(d3,d4). Mais pour écrire ce programme il doit en revanche toujours conserver son arbre en tête.

Avec IRPF90, plus besoin de connaître l’arbre d’un programme. Il est automatiquement calculé et concevoir le même programme que précédemment reviendrait tout simplement à écrire le code du listing 4.

De cette façon, le développeur ne fait que demander à la machine “Imprime t à l’écran.” Il n’a absolument pas besoin de savoir de quoi dépend t, comment t est calculé ou si t a déjà été calculé précédemment. Le développeur ne fait que demander le résultat t et rien de plus. Cela simplifie considérablement le développement collaboratif.

Dans ce programme, pour chaque nœud de l’arbre est écrit un provider, c’est-à-dire une sous-routine dont le rôle est de construire la variable associée au nœud. Il est par contre nécessaire que la variable soit construite correctement dans le provider. Et pour s’en assurer, il est possible d’ajouter des assertions avec le mot-clé ASSERT qui réalisent des vérifications à l’exécution du programme, comme illustré dans le listing 5. En cas d’échec, la sortie du programme du listing 5 serait celle donnée par le listing 6.

IRPF90 analyse le code de tous les fichiers *.irp.f du répertoire courant et calcule les dépendances entre les variables. Par exemple, dans le code du listing 4, le provider de v a besoin de u2 et de w. IRPF90 garantit ainsi que les providers de u2 et de w seront exécutés avant celui de v, et que les variables u2 et w seront donc valides. En conséquence, le développeur n’a pas à connaître le moment exact où le provider de tel ou tel nœud doit être exécuté. L’utilisation de t dans le programme principal déclenche l’exploration récursive de l’arbre avant l’impression de la valeur de t à l’écran. Cela revient exactement à utiliser la fonction t( u(d1,d2), v( u(d3,d4), w(d5) ) ) où les paramètres des fonctions sont maintenant implicites (IRPF90 signifiant Implicit Reference to Parameters in Fortran 90).

Dès qu’un provider est exécuté, la variable associée est marquée comme valide. Elle ne sera donc pas reconstruite mais tout simplement réutilisée si un autre provider en a besoin.

1.1 – Compilation

En tant que générateur de code Fortran 90, IRPF90 est exécuté avant la compilation afin de produire le code réalisant l’exploration de l’arbre. Etant réalisée à la compilation, toute la gestion de l’arbre est statique, ce qui ne nuit absolument pas à la vitesse d’exécution du programme qui reste du Fortran90 standard. Parce que la modification du code peut induire de nouvelles dépendances entre les variables, l’arbre est recalculé avant chaque compilation. Notez qu’il est bien entendu possible d’utiliser toutes les bibliothèques compatibles Fortran (MPI, OpenMP, BLAS/Lapack, etc.).

Il est aussi possible d’écrire dans le même répertoire plusieurs programmes qui utilisent des providers communs. Cela est très utile pour construire des tests unitaires. Par exemple, pour réaliser un test sur la variable u1, il suffira d’écrire un programme principal qui imprime u1 à l’écran : seul le sous-arbre de u1 sera généré à l’exécution.

Puisque IRPF90 connaît les dépendances entre les variables, il est aussi capable de déterminer les dépendances entre les fichiers pour produire automatiquement un Makefile qui compilera tous les programmes du répertoire courant.

1.2 – Modification dynamique des valeurs des nœuds

Les programmes scientifiques utilisent souvent des processus itératifs qui se basent sur le même arbre de production à chaque itération mais dont la valeur des feuilles à l’itération n+1 dépend de la valeur de la racine à l’itération n. Cela implique de pouvoir modifier la valeur d’une variable à l’extérieur de son provider, et d’informer le système de cette modification afin que les dépendances entre variables soient mises à jour et que la nouvelle racine soit construite correctement.

Le mot-clé TOUCH a ainsi été introduit pour gérer ce type de modifications à l’extérieur des providers. TOUCH rend valide la variable modifiée (touchée) et invalide tous les nœuds qui ont besoin directement ou indirectement de cette variable.

Dans l’exemple de la Figure 2, la variable z a besoin de x et y pour être construite. L’arbre (a) de la Figure 2 est dans un état où tout est construit et valide : les nœuds sont représentés en vert. L’arbre (b) représente l’état où la valeur de x est modifiée et TOUCH x exécuté. Dans l’arbre (c), on voit donc que x est valide mais tous les nœuds entre lui et la racine ont été invalidés (en rouge). Ainsi, si la valeur de z est redemandée, alors seul le nœud sous celui de z (en rouge) sera recalculé pour construire la nouvelle valeur de z.

TOUCH peut par exemple être utilisée dans le calcul d’une dérivée par différence finie afin de vérifier l’implémentation de la dérivée d’une fonction. C’est ce qui est montré avec le code du listing 7 qui calcule la dérivée de F par rapport à x.

1.3 – Variables tableaux

Un tableau est considéré comme valide lorsque toutes ses valeurs ont été calculées. Les dimensions des tableaux sont soit des variables qui ont des providers, soit des constantes, soit des intervalles.

Dans l’exemple du listing 8, le tableau fact dépend de sa variable de dimensionnement fact_max. La modification de la dimension du tableau avec TOUCH fact_max entraînera donc l’invalidation du tableau fact qui sera recalculé avec la bonne dimension à sa prochaine utilisation. Toutes les allocations sont vérifiées et un message d’erreur apparaît à l’exécution en cas d’impossibilité d’allocation du tableau.

La mémoire réservée pour un tableau peut être libérée en utilisant le mot-clé FREE. Dans l’exemple du listing 9, lorsque array1 est libéré, le nœud correspondant est marqué comme invalide. Ainsi, si array1 est redemandé plus tard, il sera préalablement réalloué et reconstruit.

1.4 – Documentation du code

A l’intérieur de chaque provider, il est recommandé d’écrire quelques lignes de documentation qui décrivent la variable construite comme illustré dans le listing 10.

Lors de la compilation, la liste de toutes les variables est écrite dans un fichier nommé irpf90_entities, et une man page est créée pour chaque variable. Cette page contient la documentation présente dans les blocs délimités par BEGIN_DOC et END_DOC et elle indique aussi les variables nécessaires à la construction de la variable courante ainsi que celles qui en dépendent. Ces pages sont affichées avec l’outil irpman. Le listing 11 en est une illustration.

1.5 – Templates

Il peut arriver qu’on veuille écrire plusieurs parties de code utilisant un même schéma. En C++, on utiliserait les templates de fonctions, de classes ou d’expressions. IRPF90 permet également de créer plusieurs providers et fonctions semblables, comme illustré au listing 12. Cet exemple permet de générer automatiquement un provider et une fonction *_is_zero pour chaque couple qui apparaît sous le mot-clé SUBST.

1.6 – Interaction avec le shell et métaprogrammation

On peut aussi vouloir insérer au milieu d’un programme le résultat d’une commande du shell qui sera exécutée à la compilation. Prenons un cas typique (listing 13) où la date de compilation et la version de git à laquelle elle correspond sont imprimées à l’écran.

L’insertion de sorties de scripts dans le code permet également d’aller au-delà des templates et de produire du code de façon mécanique. L’exemple du listing 14 illustre cela. Il génère des fonctions particulières calculant x à la puissance n en utilisant un minimum de multiplications. Le listing 15 fournit un échantillon du code généré par IRPF90 à partir du code du listing 14.

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