Fortran exécuté dans WebAssembly
(gws.phd)- 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-newpatché 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 deTargetWasm32; côté liaison du runtime, la différence de taille delongentre l’hôte et la cible pose problème - En combinant
flang-newpatché, Emscripten et des bibliothèques statiques du runtime Fortran, on peut appeler des sous-programmes Fortran depuis C ou JavaScript et gérer aussiPRINT,ALLOCATEet les argumentsCHARACTER - 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-newde 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
-
f2cf2cconvertit 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.8etllvm-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
pgfortrande 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
wasm64au 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
- Classic Flang est un compilateur Fortran ciblant LLVM, fondé sur
-
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-newest 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-newest introuvable
- Par exemple, avec LLVM v17.0.6 de Homebrew sur macOS, la commande
- 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-emscriptencomme triplet cible par défaut et active la cibleWebAssemblyainsi que les projetsclang;flang;mlir - Une fois la compilation terminée,
build/bin/flang-new --versionaffiche les informations suivantes- Version :
flang-new version 18.1.1 - Cible :
wasm32-unknown-emscripten - Modèle de threads :
posix
- Version :
Premier problème : cible wasm32 non implémentée
- Compiler un simple sous-programme Fortran
foo.f08avecflang-newprovoque l’erreur suivantenot yet implemented: target not implemented
- La cause est que le triplet cible
wasm32-unknown-emscriptenn’est pas encore implémenté dansflang-new - La solution est un patch qui ajoute les caractéristiques de cible
TargetWasm32dansflang/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
TargetWasm32est raccordé à la branchellvm::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 avecllvm-nm foo.o
- Résultat de
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
fooreçoit les arguments entiersx,y,zet effectuez = x + y - Dans une build native, exécuter
gfortran -c foo.f08 -o foo.opeut produire un nom de symbole avec un underscore final, commefoo_ - 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.oproduit parflang-newpeut être lié avec du code C via Emscriptenemcc 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_,_mallocet_freesont 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 dansModule.HEAPU32, puis appelleModule._foo_(x, y, z) - Le résultat d’exécution est le suivant
x = 123y = 456x + y = 579- Dans le navigateur, charger
foo.jsetstandalone.jsdepuis 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++etemard’Emscripten, on peut créer la bibliothèque statiquebuild/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.oavec 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
- Côté objet Fortran, la signature attendue est
- 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.hde LLVM Flang, un commentaire TODO indique que l’usage desizeofsupposebuild == host == target - Sur les hôtes Unix modernes 64 bits,
sizeof(long)vaut 8 octets, alors qu’il devrait valoir 4 octets pour la ciblewasm32-unknown-emscripten - Lorsqu’un argument de type
CHARACTERFortran 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_tet, via une chaîne detypedef, devient équivalent àunsigned long - Cette différence de taille de l’argument caché provoque l’incompatibilité entre
i64eti32
Patch temporaire : forcer une valeur sur 4 octets
- La solution idéale serait que
flang-new, lors d’une compilation croisée, produise dui32ou dui64selon 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
longde 4 octets est utilisé pourwasm32et Emscripten - Le patch se divise en deux volets
- Dans
RTBuilder.h, les types de modèle delongetunsigned longsont forcés à8 * 4au lieu de8 * 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 eni32
- Dans
- Ce changement corrige aussi l’allocation dynamique basée sur
ALLOCATE(), introduite avec Fortran 90 - Après recompilation, lier
hello.f08ethello.cavec la bibliothèque runtime se fait sans avertissement, et Node affiche la sortie suivanteHello, 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.incFC = ../build/bin/flang-newFFLAGS = -O2AR = emarRANLIB = emranlib
- Le résultat de build est la bibliothèque statique
blas_LINUX.a - La routine Fortran d’exemple
barappelle la routine BLAS level 2ZGEMV() ZGEMV()effectue une opération matrice-vecteur complexe, et l’exemple utilise des argumentsCOMPLEX(KIND=8)ainsi qu’un argument de configurationCHARACTER'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.000000iY[1]: 18.000000 + 10.000000iY[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.exampleenmake.inc, les réglages suivants sont modifiésFCest défini avec le chemin complet duflang-newconstruitFFLAGS = -O2AR = emarRANLIB = emranlibTIMER = INT_CPU_TIME
- La commande
make libgénère la bibliothèque statique WebAssemblyliblapack.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-1passant exactement parnpoints de données - Lorsque
naugmente, 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-newprend 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
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/
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
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
barfdans 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)
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
[1] https://en.m.wikipedia.org/wiki/The_Theoretical_Minimum
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
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é
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
Il existe aussi des moyens de le déployer ou de le cross-compiler vers la plupart des cibles
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
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
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/
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 * 2enx * 3, n’était pas prise en charge par le générateur de code actuelIl 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