1 points par GN⁺ 2024-04-07 | 1 commentaires | Partager sur WhatsApp
  • Pour porter dans le navigateur des actifs de calcul scientifique et d’ingénierie anciens, comme du code numérique Fortran, il faut une voie de compilation WebAssembly ; webR utilise un LLVM flang-new patché
  • f2c, LFortran, Dragonegg, Classic Flang et LLVM Flang ont chacun des limites en matière de prise en charge du Fortran moderne, d’architecture cible et de maintenabilité ; il n’existe donc pas encore de solution simple et standard
  • LLVM Flang ne prend pas directement en charge la cible wasm32-unknown-emscripten, ce qui nécessite l’ajout de TargetWasm32 ; côté liaison du runtime, la différence de taille de long entre l’hôte et la cible pose problème
  • En combinant flang-new patché, Emscripten et des bibliothèques statiques du runtime Fortran, on peut appeler des sous-programmes Fortran depuis C ou JavaScript et gérer aussi PRINT, ALLOCATE et les arguments CHARACTER
  • Les implémentations de référence BLAS 3.12.0 et LAPACK 3.12.0 ont été construites en bibliothèques statiques WebAssembly afin d’exécuter dans le navigateur des démos de classification de chiffres manuscrits et d’interpolation polynomiale

Porter du code numérique Fortran existant dans le navigateur

  • Fortran est un ancien langage apparu en 1957, mais il est utilisé depuis longtemps dans le calcul scientifique et d’ingénierie, et le Fortran moderne s’est largement affranchi des contraintes de format fixe de Fortran 77
  • L’objectif est de compiler des routines Fortran modernes en WebAssembly pour les exécuter dans le navigateur, recevoir des arguments numériques, calculer via des routines BLAS et LAPACK, puis renvoyer les résultats ou les afficher dans la console
  • Cette approche permet de porter sur le Web des environnements de programmation de plus haut niveau qui dépendent de BLAS et LAPACK, comme SciPy ou R
  • Il est possible d’exploiter des outils et bibliothèques Fortran déjà validés sans réécrire les routines numériques en JavaScript ou en Rust
  • Le projet webR compile du code Fortran en WebAssembly avec le compilateur flang-new de LLVM patché
  • L’approche actuelle repose sur des hacks, et sans l’aide de développeurs de compilateurs plus expérimentés, il est difficile de contribuer ces changements à LLVM

État des outils Fortran→WebAssembly

  • En 2024, il existe plusieurs outils et chaînes de compilation, mais toujours pas de solution simple aux fonctionnalités complètes
  • f2c

    • f2c convertit Fortran 77 en code C, qu’Emscripten peut ensuite compiler en WebAssembly
    • Pyodide utilise cette approche pour compiler des paquets Python contenant du code Fortran
    • Même la feuille de route de Pyodide estime que cette méthode « ne fonctionne pas bien »
    • Elle ne convient pas au code Fortran moderne et, même après conversion, nécessite de corriger des erreurs critiques et d’appliquer de nombreux patchs
  • LFortran

    • LFortran a fortement gagné en fonctionnalités ces dernières années et peut compiler directement vers WebAssembly
    • Les développeurs indiquent qu’il reste en phase alpha, et que des problèmes sont à prévoir lors de la compilation de code réel
    • Certains projets comme MINPACK peuvent être compilés, mais comme toute la spécification Fortran n’est pas prise en charge, des projets plus importants peuvent échouer
    • L’objectif de développement est la prise en charge complète de Fortran 2018, et l’une des fonctionnalités marquantes est un REPL Fortran interactif proche de Jupyter
  • Dragonegg

    • Dragonegg est un plugin GCC qui utilise le frontend GCC pour produire de l’IR LLVM
    • Il permet de produire une sortie WebAssembly via le backend LLVM, et c’est l’approche que webR a utilisée initialement pour compiler des sources Fortran en WebAssembly
    • Les dernières versions prises en charge sont gcc-4.8 et llvm-3.3, ce qui impose des versions très anciennes de GCC et LLVM
    • La plupart des utilisateurs ont besoin d’une VM ou d’un conteneur Docker, et l’IR LLVM produit par Dragonegg doit aussi subir un post-traitement supplémentaire pour générer une sortie WebAssembly
    • En 2020, c’était pratiquement la seule méthode concrète pour compiler du code Fortran en WebAssembly
  • Classic Flang

    • Classic Flang est un compilateur Fortran ciblant LLVM, fondé sur pgfortran de PGI/NVIDIA passé en open source
    • Il ne prend pas en charge les sorties 32 bits, donc il ne peut pas être utilisé pour la cible wasm32
    • Firefox, Chrome et Node prennent en charge wasm64 au moment de la rédaction, mais derrière un feature flag
    • La documentation du projet indique aussi que choisir Classic Flang pour un nouveau projet n’est peut-être pas une bonne idée
  • LLVM Flang

    • LLVM Flang est un projet de réimplémentation depuis zéro d’un frontend Fortran pour LLVM, intégré au projet LLVM depuis LLVM 11
    • Il n’est pas encore considéré comme prêt pour la production, mais la version pré-production de flang-new est devenue assez utilisable pour compiler du vrai code Fortran
    • Dans son état par défaut, il ne peut pas produire de sortie WebAssembly
    • La conception modulaire de LLVM permet d’utiliser ensemble le frontend Flang et le backend WebAssembly de LLVM
    • C’était déjà possible en 2020, mais cela nécessitait des patchs LLVM plus importants, l’injection de routines mathématiques personnalisées et un processus de compilation en plusieurs étapes
    • Aujourd’hui, grâce au développement du frontend flang-new, une petite modification des sources LLVM suffit à créer un compilateur Fortran→WebAssembly

Construire LLVM Flang pour WebAssembly

  • LLVM installé via un gestionnaire de paquets peut ne pas inclure le binaire flang-new
    • Par exemple, avec LLVM v17.0.6 de Homebrew sur macOS, la commande flang-new est introuvable
  • Comme les sources de LLVM Flang doivent être modifiées, LLVM v18.1.1 est construit directement depuis les sources
  • La configuration CMake utilise wasm32-unknown-emscripten comme triplet cible par défaut et active la cible WebAssembly ainsi que les projets clang;flang;mlir
  • Une fois la compilation terminée, build/bin/flang-new --version affiche les informations suivantes
    • Version : flang-new version 18.1.1
    • Cible : wasm32-unknown-emscripten
    • Modèle de threads : posix

Premier problème : cible wasm32 non implémentée

  • Compiler un simple sous-programme Fortran foo.f08 avec flang-new provoque l’erreur suivante
    • not yet implemented: target not implemented
  • La cause est que le triplet cible wasm32-unknown-emscripten n’est pas encore implémenté dans flang-new
  • La solution est un patch qui ajoute les caractéristiques de cible TargetWasm32 dans flang/lib/Optimizer/CodeGen/Target.cpp
    • La largeur par défaut est définie à 32
    • Le marshalling qui convertit les arguments complexes et les types de retour vers les types LLVM côté WebAssembly est défini
    • TargetWasm32 est raccordé à la branche llvm::Triple::ArchType::wasm32
  • Après le patch puis une nouvelle compilation, la source Fortran est compilée en objet WebAssembly
    • Résultat de file foo.o : WebAssembly (wasm) binary module version 0x1 (MVP)
    • Le symbole foo_ est visible avec llvm-nm foo.o

Appeler un sous-programme Fortran depuis C et JavaScript

  • Les routines Fortran passent généralement les arguments par référence, et INTENT() permet de déclarer l’usage des arguments
  • L’exemple de sous-programme Fortran foo reçoit les arguments entiers x, y, z et effectue z = x + y
  • Dans une build native, exécuter gfortran -c foo.f08 -o foo.o peut produire un nom de symbole avec un underscore final, comme foo_
  • Pour l’appeler depuis C, il faut déclarer le symbole externe comme extern void foo_(int*, int*, int*); et passer les arguments par adresse
  • En WebAssembly, l’objet foo.o produit par flang-new peut être lié avec du code C via Emscripten
    • emcc main.c foo.o -o main.js
    • Sortie de node main.js : 1 + 1 = 2
  • Appel direct depuis JavaScript

    • Il est aussi possible d’appeler directement un sous-programme Fortran depuis JavaScript, sans code C
    • À l’étape de liaison Emscripten, _foo_, _malloc et _free sont exportés
    • emcc foo.o -sEXPORTED_FUNCTIONS=_foo_,_malloc,_free -o foo.js
    • JavaScript alloue de l’espace pour stocker des entiers avec Module._malloc(), écrit les valeurs dans Module.HEAPU32, puis appelle Module._foo_(x, y, z)
    • Le résultat d’exécution est le suivant
    • x = 123
    • y = 456
    • x + y = 579
    • Dans le navigateur, charger foo.js et standalone.js depuis le HTML permet de voir le même résultat dans la console JavaScript

Deuxième problème : bibliothèque runtime Fortran

  • Construire un sous-programme Fortran contenant PRINT *, "Hello, World!" provoque, à l’étape de liaison, des erreurs indiquant l’absence de symboles du runtime
    • _FortranAioBeginExternalListOutput
    • _FortranAioOutputAscii
    • _FortranAioEndIoStatement
  • La cause est que la bibliothèque runtime Fortran de LLVM n’a pas encore été compilée pour WebAssembly
  • La bibliothèque runtime est écrite en C++ dans llvm-project/flang/runtime, dans l’arborescence des sources LLVM
  • En construisant les sources du runtime avec em++ et emar d’Emscripten, on peut créer la bibliothèque statique build/flang/runtime/libFortranRuntime.a
  • Lier cette bibliothèque permet de poursuivre la build de Hello, World!, mais produit d’abord des avertissements d’incompatibilité de signatures de fonctions

Troisième problème : différence de taille de long entre l’hôte et la cible

  • Lier hello.o avec la bibliothèque runtime Fortran déclenche un avertissement d’incompatibilité de signature pour _FortranAioOutputAscii
    • Côté objet Fortran, la signature attendue est (i32, i32, i64) -> i32
    • Côté runtime construit avec Emscripten, elle est définie comme (i32, i32, i32) -> i32
  • WebAssembly impose que les types d’arguments et de retour des symboles définis à travers plusieurs unités de compilation soient cohérents
  • Le problème ne se limite pas à un avertissement : l’exécution sous Node échoue avec RuntimeError: unreachable
  • Dans RTBuilder.h de LLVM Flang, un commentaire TODO indique que l’usage de sizeof suppose build == host == target
  • Sur les hôtes Unix modernes 64 bits, sizeof(long) vaut 8 octets, alors qu’il devrait valoir 4 octets pour la cible wasm32-unknown-emscripten
  • Lorsqu’un argument de type CHARACTER Fortran est passé à une fonction ou à un sous-programme, un argument caché indiquant la longueur de la chaîne peut être ajouté
  • Dans la bibliothèque runtime Fortran, cet argument de longueur est déclaré en size_t et, via une chaîne de typedef, devient équivalent à unsigned long
  • Cette différence de taille de l’argument caché provoque l’incompatibilité entre i64 et i32

Patch temporaire : forcer une valeur sur 4 octets

  • La solution idéale serait que flang-new, lors d’une compilation croisée, produise du i32 ou du i64 selon l’architecture cible et son modèle de données, indépendamment de l’hôte
  • Pour l’instant, un patch codant en dur une taille de long de 4 octets est utilisé pour wasm32 et Emscripten
  • Le patch se divise en deux volets
    • Dans RTBuilder.h, les types de modèle de long et unsigned long sont forcés à 8 * 4 au lieu de 8 * sizeof(...)
    • Dans CodeGen.cpp, l’argument de l’appel à malloc() est généré comme entier 32 bits plutôt que 64 bits, et la taille d’allocation est castée en i32
  • Ce changement corrige aussi l’allocation dynamique basée sur ALLOCATE(), introduite avec Fortran 90
  • Après recompilation, lier hello.f08 et hello.c avec la bibliothèque runtime se fait sans avertissement, et Node affiche la sortie suivante
    • Hello, World!

Construire BLAS en WebAssembly

  • BLAS est un ensemble de routines de bas niveau réalisant les opérations courantes d’algèbre linéaire, comme les multiplications matrice-vecteur
  • Les routines BLAS originales ont été publiées en 1979 et constituent un standard de fait dans le calcul numérique
  • L’implémentation de référence BLAS 3.12.0 est écrite en Fortran 90 et disponible auprès de netlib
  • La build se fait en indiquant les outils suivants dans make.inc
    • FC = ../build/bin/flang-new
    • FFLAGS = -O2
    • AR = emar
    • RANLIB = emranlib
  • Le résultat de build est la bibliothèque statique blas_LINUX.a
  • La routine Fortran d’exemple bar appelle la routine BLAS level 2 ZGEMV()
  • ZGEMV() effectue une opération matrice-vecteur complexe, et l’exemple utilise des arguments COMPLEX(KIND=8) ainsi qu’un argument de configuration CHARACTER 'N'
  • Le programme C crée des tableaux de nombres complexes, les passe à la routine Fortran, puis affiche le résultat
  • Le résultat sous Node est le suivant
    • Y[0]: 23.000000 + 6.000000i
    • Y[1]: 18.000000 + 10.000000i
    • Y[2]: 6.000000 + 16.000000i
  • Ce résultat confirme que BLAS compilé depuis des sources Fortran 90 s’exécute sous WebAssembly

Exemple navigateur : classificateur de chiffres manuscrits

  • La démo classe des chiffres de 0 à 9 dessinés à la main avec un réseau de neurones artificiels de type perceptron multicouche (MLP)
  • L’utilisateur peut dessiner un chiffre à la souris ou sur écran tactile, et les probabilités relatives prédites par le réseau sont affichées dans le graphe à droite
  • Les poids du modèle ont été préentraînés avec Python, mais la classification est effectuée à l’exécution dans le navigateur avec JavaScript et WebAssembly
  • Le processus de classification du MLP consiste essentiellement en des additions et multiplications matrice-vecteur répétées
  • Le calcul lourd est assuré par un sous-programme Fortran qui utilise la routine BLAS level 2 DGEMV()

Construire LAPACK en WebAssembly

  • LAPACK est une bibliothèque logicielle qui résout numériquement des problèmes d’algèbre linéaire, construite au-dessus de BLAS
  • L’implémentation de référence LAPACK 3.12.0 est fournie par netlib et distribuée sous licence BSD modifiée
  • Après avoir copié make.inc.example en make.inc, les réglages suivants sont modifiés
    • FC est défini avec le chemin complet du flang-new construit
    • FFLAGS = -O2
    • AR = emar
    • RANLIB = emranlib
    • TIMER = INT_CPU_TIME
  • La commande make lib génère la bibliothèque statique WebAssembly liblapack.a
  • Les routines LAPACK peuvent ensuite être appelées d’une manière proche de celle de l’exemple BLAS

Exemple navigateur : interpolation polynomiale par algèbre linéaire

  • La démo cherche un polynôme d’interpolation pour un ensemble de points et montre que des routines LAPACK s’exécutent dans le navigateur
  • Quand l’utilisateur clique sur le graphe pour ajouter un nouveau point, elle trouve le polynôme d’interpolation passant par tous les points
  • La méthode utilisée est la méthode de Vandermonde
  • Le système d’équations linéaires obtenu avec cette approche est résolu numériquement avec la routine DGELS() de LAPACK
  • Il est toujours possible de trouver un polynôme de degré n-1 passant exactement par n points de données
  • Lorsque n augmente, le polynôme peut fortement osciller entre des points de données consécutifs, un phénomène de Runge qui peut être évité avec une interpolation par splines

Travaux restants et outils fournis

  • Avec LLVM Flang patché, il est possible de compiler du code Fortran moderne en objets WebAssembly
  • L’avantage de cette approche est d’utiliser des outils et bibliothèques Fortran existants sans réécrire en JavaScript les algorithmes numériques destinés au Web
  • Si flang-new prend officiellement WebAssembly en charge, cela réduira la charge de maintenance d’un fork LLVM pour webR et les paquets R
  • Il faut encore une meilleure voie pour corriger correctement LLVM Flang et les problèmes de compilation croisée sur toutes les cibles
  • Pour les utilisateurs qui ne peuvent ou ne veulent pas construire LLVM Flang eux-mêmes, un conteneur Docker avec binaires LLVM Flang patchés est fourni sur GitHub Container Registry

1 commentaires

 
GN⁺ 2024-04-07
Commentaires sur Hacker News
  • Pour ajouter un peu de contexte, cette exploration de Fortran fait partie de l’excellent travail WebR que George mène pour exécuter R dans le navigateur
    Le code source de R contient pas mal de code Fortran, et il me semble qu’à l’origine WebR compilait d’abord le Fortran en C avec f2c, puis compilait ce C en wasm
    Grâce à un patch LLVM Flang, il est désormais possible de construire WebR avec un vrai compilateur Fortran
    George ne l’a pas dit explicitement dans son billet de blog, mais il a déjà indiqué qu’il espérait que Flang accepte ce patch ou l’implémente d’une meilleure manière
    Cela éviterait d’avoir à maintenir un patch séparé, et un Flang non modifié pourrait compiler vers wasm, ce qui profiterait aussi à d’autres projets utilisant Fortran
    https://docs.r-wasm.org/webr/latest/

    • Les pull requests sont toujours les bienvenues (https://github.com/llvm/llvm-project), et si vous avez besoin d’aide, vous pouvez contacter la communauté de développement Fortran de LLVM (https://discourse.llvm.org/c/subprojects/flang/33)
      Mais je suis concentré sur le travail nécessaire pour finaliser le développement du produit Fortran de Nvidia, donc je n’ai plus de temps à consacrer à ce genre de tâche
    • La traduction de source à source pour porter le F77 vers JavaScript est déjà plutôt correcte, mais WASM est meilleur
  • J’ai travaillé sur de la compilation FORTRAN chez Xilinx il y a 20 ans, et la seule chose dont je me souvienne, c’est qu’il y avait une définition de barf dans le fichier d’en-tête f2c.h
    /* f2c.h -- Standard Fortran to C header file /
    /* barf [ba:rf] 2. "He suggested using FORTRAN, and everybody barfed."
    (https://www.netlib.org/clapack/f2c.h)

    • Le fait que j’écrive FORTRAN tout en majuscules, est-ce que ça dit quelque chose sur moi ? À mes yeux, Fortran paraît bizarre
  • J’aime beaucoup le fait d’utiliser l’exemple non trivial le plus simple comme méthode d’explication
    L’article s’appuie sur un problème concret, « appeler des fonctions BLAS depuis JavaScript », donc j’ai eu l’impression d’apprendre beaucoup, excellent article

  • Je ne sais pas s’il faut être impressionné ou horrifié, sans doute les deux
    Pour compiler f18, je recommande d’utiliser les toutes dernières sources de llvm-project/main
    Le projet avance vite, donc c’est une perte de temps de déboguer des problèmes déjà corrigés ou de passer à côté de fonctionnalités déjà implémentées

    • Je ne comprenais pas assez bien le code source de LLVM, donc j’ai eu du mal à saisir l’idée principale
      Est-ce que le but est de travailler sur le port WebAssembly pour amener le code intermédiaire jusqu’au point où il fonctionne aussi avec Fortran ?
  • Je ne connais pas bien le développement WebAssembly
    Du point de vue de l’utilisateur, est-ce que WebAssembly apporte quelque chose dès maintenant ? Ou est-ce plutôt un travail de fond pour un futur où les programmes deviendront réellement portables ?
    J’ai entendu dire que les dispositifs WebAssembly facilitaient les restrictions d’accès, par exemple au réseau ou aux fichiers, mais je ne sais pas si c’est théorique ou déjà implémenté

    • En gros, Wasm est une machine virtuelle, très similaire à la JVM sur l’aspect portabilité
      La différence essentielle, c’est que Wasm lui-même n’a pas de bibliothèque standard et n’expose aucune fonction d’entrée/sortie
      Donc l’hôte, c’est-à-dire celui qui construit la VM, peut exposer les fonctions que le binaire Wasm pourra importer, et le binaire Wasm ne peut accéder au monde extérieur qu’à travers ces fonctions
      Autre avantage : le format binaire n’est pas propriétaire et il existe une spécification, donc n’importe qui peut implémenter une VM Wasm
      Cela dit, ce n’est pas encore dans un très bon état aujourd’hui, c’est encore trop tôt, il y a beaucoup de nouvelles fonctionnalités en cours de standardisation dans des groupes comparables au W3C, et le processus est très lent
    • Si vous êtes développeur ou que vous distribuez un produit et que vous voulez un sandboxing robuste, WASM est probablement l’une des meilleures options disponibles aujourd’hui
      Il existe aussi des moyens de le déployer ou de le cross-compiler vers la plupart des cibles
    • Si c’est bien implémenté, le client ne devrait même pas le remarquer
      Un peu comme on ne sait généralement pas si le CPU de son ordinateur est ARM ou x86, et qu’on s’en soucie rarement
      Donc si l’on ne s’intéresse pas aux détails comme le fait qu’un programme tourne en code natif ou sur une VM comme la JVM, .NET ou WASM, il est difficile de dire ce que cela apporte de plus que d’autres solutions
      En général, on ne remarque que les mauvais exemples, et cela finit en mèmes du genre « tous les programmes Electron sont des monstres gonflés et voraces en ressources, et toutes les applis natives sont automatiquement des merveilles d’ingénierie logicielle efficace »
  • C’est dommage que je n’aie pas conservé le code Fortran 78 que j’avais écrit en 1981/82, j’aurais pu tester s’il s’exécute ici
    C’était un formateur de code source pour le langage Jovial, pas vraiment le genre de chose qu’on ferait en Fortran, mais à l’époque c’était la seule option

    • Vous travailliez chez Hughes dans l’Orange County ?
  • Existe-t-il en JavaScript un écosystème d’algèbre linéaire à peu près prêt pour la production ?
    Quand je cherche, je ne tombe en général que sur des ports JavaScript de bibliothèques familières vieilles d’une dizaine d’années, par exemple via emscripten, donc je me demande si je rate quelque chose

    • Y a-t-il un équivalent de BLAS au-dessus de WebGPU ou de WebNN ?
  • Il est étrange de ne pas approfondir davantage LFortran
    Il existe aussi un excellent et étonnant exemple WASM qui tourne en ligne
    https://dev.lfortran.org/

    • L’article indique que le compilateur LFortran a énormément progressé au cours des dernières années
      En 2020, il lui manquait encore beaucoup de fonctionnalités et il ne prenait en charge qu’un très petit sous-ensemble de Fortran, mais aujourd’hui il prend en charge un éventail bien plus large de fonctionnalités du langage et peut compiler une quantité assez importante de code Fortran
      Il peut aussi compiler nativement vers WebAssembly
      Cela dit, LFortran reste encore rugueux à l’usage, et les développeurs précisent que le projet est actuellement considéré comme étant au stade alpha et que des problèmes peuvent survenir lors de la compilation de code réel
      Certains projets comme MINPACK peuvent être compilés avec succès, mais comme l’ensemble de la spécification Fortran n’est pas encore pris en charge, une bonne partie des projets plus importants ne peuvent toujours pas être compilés
      Les développeurs de LFortran visent une prise en charge complète de Fortran 2018, et une fonctionnalité notable est son REPL Fortran interactif, à la manière de Jupyter
      Avec encore quelques années de développement, cela pourrait devenir une excellente option pour compiler du code Fortran vers WebAssembly
      Il est aussi écrit d’aller voir la démo LFortran sur https://dev.lfortran.org, qui est très impressionnante, mais la première chose que j’ai essayée, modifier x * 2 en x * 3, n’était pas prise en charge par le générateur de code actuel
  • Il existe aussi du Fortran sur .NET et Java
    https://www.silverfrost.com/14/ftn95/ftn95_fortran_95_for_microsoft_dotnet_features.aspx
    https://dl.acm.org/doi/10.1145/376656.376833

  • Quand j’ai travaillé sur https://medium.com/@tomasreimers/compiling-tensorflow-for-the-browser-f3387b8e1e1c, je me suis vraiment dit que c’était une chance que TensorFlow utilise Eigen plutôt que BLAS/Lapack, les populaires bibliothèques mathématiques écrites en Fortran
    Sinon, il y aurait eu beaucoup plus de travail