Fortran rodando em WebAssembly
(gws.phd)- Para levar código numérico legado em Fortran ao navegador, é preciso um caminho de compilação para WebAssembly, e o webR usa um
flang-newdo LLVM com patches f2c, LFortran, Dragonegg, Classic Flang e LLVM Flang têm limitações em suporte a Fortran moderno, arquiteturas-alvo e manutenção, então ainda não existe uma solução padrão simples- O LLVM Flang não oferece suporte direto ao alvo
wasm32-unknown-emscripten, exigindo a adição deTargetWasm32, e na ligação com o runtime surge o problema da diferença no tamanho delongentre host e alvo - Com
flang-newcom patches, Emscripten e bibliotecas estáticas do runtime Fortran, dá para chamar sub-rotinas Fortran a partir de C ou JavaScript e também lidar comPRINT,ALLOCATEe argumentosCHARACTER - As implementações de referência do BLAS 3.12.0 e do LAPACK 3.12.0 foram compiladas como bibliotecas estáticas WebAssembly, executando no navegador demos de classificação de dígitos manuscritos e interpolação polinomial
Levando código numérico Fortran existente para o navegador
- Fortran é uma linguagem antiga, surgida em 1957, mas foi usada por muito tempo em computação científica e de engenharia, e o Fortran moderno já se livrou em grande parte das restrições de formato fixo do Fortran 77
- O objetivo é compilar rotinas modernas em Fortran para WebAssembly, executá-las no navegador, receber argumentos numéricos, calcular rotinas BLAS e LAPACK e então retornar o resultado ou imprimi-lo no console
- Essa abordagem permite levar para a web ambientes de programação de mais alto nível que dependem de BLAS e LAPACK, como SciPy e R
- Em vez de reescrever rotinas numéricas em JavaScript ou Rust, é possível aproveitar ferramentas e bibliotecas já validadas baseadas em Fortran
- O projeto webR compila código Fortran para WebAssembly com um compilador
flang-newdo LLVM com patches - O método atual ainda depende de hacks, e sem ajuda de desenvolvedores de compilador mais experientes é difícil contribuir essas mudanças de volta ao LLVM
Situação das ferramentas Fortran→WebAssembly
- Em 2024 existem várias ferramentas e toolchains, mas ainda não há uma solução simples com funcionalidade completa
-
f2cf2cconverte Fortran 77 em código C, que o Emscripten pode então compilar para WebAssembly- O Pyodide usa essa abordagem ao compilar pacotes Python que incluem código Fortran
- Até no roadmap do Pyodide essa abordagem é avaliada como “não funciona bem”
- Ela não se encaixa em código Fortran moderno e, mesmo após a conversão, ainda exige erros críticos corrigidos e patches extensos
-
LFortran
- O LFortran ganhou muitos recursos nos últimos anos e consegue compilar diretamente para WebAssembly
- Os próprios desenvolvedores dizem que ele ainda está em fase alfa, então problemas ao compilar código real são esperados
- Alguns projetos como MINPACK podem ser compilados, mas projetos maiores podem falhar por falta de suporte à especificação completa de Fortran
- A meta de desenvolvimento é suporte completo ao Fortran 2018, e um dos recursos mais marcantes é um REPL interativo de Fortran, parecido com o Jupyter
-
Dragonegg
- O Dragonegg é um plugin do GCC que usa o frontend do GCC para exportar LLVM IR
- Ele pode gerar saída WebAssembly com o backend do LLVM e foi o método inicialmente usado pelo webR para compilar código Fortran para WebAssembly
- As versões mais novas suportadas são
gcc-4.8ellvm-3.3, então ele exige GCC e LLVM muito antigos - A maioria dos usuários precisa de VM ou container Docker, e o LLVM IR exportado pelo Dragonegg ainda precisa de pós-processamento adicional para gerar saída WebAssembly
- Em 2020, esse era praticamente o único caminho viável para compilar código Fortran para WebAssembly
-
Classic Flang
- O Classic Flang é um compilador Fortran com alvo LLVM baseado no
pgfortranda PGI/NVIDIA, que foi aberto como open source - Ele não suporta saída 32-bit, então não pode ser usado com o alvo
wasm32 - Firefox, Chrome e Node suportam
wasm64no momento da escrita, mas atrás de flags de recurso - A própria documentação do projeto avisa que talvez não seja uma boa ideia escolher o Classic Flang para novos projetos
- O Classic Flang é um compilador Fortran com alvo LLVM baseado no
-
LLVM Flang
- O LLVM Flang é um projeto que reimplementa do zero um frontend Fortran para o LLVM e faz parte do projeto LLVM desde o LLVM 11
- Ainda não é considerado pronto para produção, mas versões pré-produção do
flang-newjá estão num nível bem utilizável para compilar código Fortran real - No estado padrão, ele não consegue gerar saída WebAssembly
- Aproveitando o design modular do LLVM, dá para combinar o frontend do Flang com o backend WebAssembly do LLVM
- Isso já era possível em 2020, mas exigia patches maiores no LLVM, injeção de rotinas matemáticas customizadas e um processo de compilação em várias etapas
- Hoje, graças ao desenvolvimento do frontend
flang-new, já é possível criar um compilador Fortran→WebAssembly com pequenas mudanças no código-fonte do LLVM
Compilando o LLVM Flang para WebAssembly
- O LLVM instalado via gerenciador de pacotes pode não incluir o binário
flang-new- Por exemplo, no LLVM v17.0.6 do Homebrew no macOS, o comando
flang-newnão é encontrado
- Por exemplo, no LLVM v17.0.6 do Homebrew no macOS, o comando
- Como é necessário modificar o código-fonte do LLVM Flang, foi feito um build direto do código-fonte do LLVM v18.1.1
- A configuração do CMake define o triple-alvo padrão como
wasm32-unknown-emscriptene ativa o alvoWebAssemblye os projetosclang;flang;mlir - Depois do build,
build/bin/flang-new --versionmostra as seguintes informações- versão:
flang-new version 18.1.1 - alvo:
wasm32-unknown-emscripten - modelo de threads:
posix
- versão:
Primeiro problema: alvo wasm32 não implementado
- Ao compilar uma sub-rotina Fortran simples
foo.f08comflang-new, aparece o seguinte erronot yet implemented: target not implemented
- A causa é que o triple-alvo
wasm32-unknown-emscriptenainda não está implementado noflang-new - A solução é um patch que adiciona as características de alvo
TargetWasm32emflang/lib/Optimizer/CodeGen/Target.cpp- Define a largura padrão como 32
- Define o marshaling para converter argumentos complexos e tipos de retorno para tipos LLVM do lado WebAssembly
- Liga
TargetWasm32ao ramollvm::Triple::ArchType::wasm32
- Após aplicar o patch e recompilar, o código-fonte Fortran passa a ser compilado como objeto WebAssembly
- resultado de
file foo.o:WebAssembly (wasm) binary module version 0x1 (MVP) - em
llvm-nm foo.oé possível confirmar o símbolofoo_
- resultado de
Chamando sub-rotinas Fortran a partir de C e JavaScript
- Rotinas Fortran normalmente passam argumentos por referência, e
INTENT()pode declarar como esses argumentos são usados - A sub-rotina de exemplo
foorecebe os inteirosx,yeze fazz = x + y - Em build nativo, ao executar
gfortran -c foo.f08 -o foo.o, o nome do símbolo pode ganhar um underscore no final, comofoo_ - Ao chamar a partir de C, o símbolo externo é declarado como
extern void foo_(int*, int*, int*);, e os argumentos são passados por endereço - Em WebAssembly, o
foo.ogerado porflang-newpode ser ligado com código C via Emscriptenemcc main.c foo.o -o main.js- saída de
node main.js:1 + 1 = 2
-
Chamada direta via JavaScript
- Também é possível chamar a sub-rotina Fortran diretamente em JavaScript, sem código C
- Na etapa de link com Emscripten, são exportados
_foo_,_malloce_free emcc foo.o -sEXPORTED_FUNCTIONS=_foo_,_malloc,_free -o foo.js- O JavaScript aloca espaço para inteiros com
Module._malloc(), escreve valores emModule.HEAPU32e então chamaModule._foo_(x, y, z) - O resultado da execução é o seguinte
x = 123y = 456x + y = 579- No navegador, carregando
foo.jsestandalone.jsem um HTML, é possível ver o mesmo resultado no console JavaScript
Segundo problema: bibliotecas de runtime Fortran
- Ao compilar uma sub-rotina Fortran com
PRINT *, "Hello, World!", a etapa de link falha com erros de símbolos de runtime ausentes_FortranAioBeginExternalListOutput_FortranAioOutputAscii_FortranAioEndIoStatement
- A causa é que as bibliotecas de runtime Fortran do LLVM ainda não foram compiladas para WebAssembly
- As bibliotecas de runtime ficam em
llvm-project/flang/runtimeno código-fonte do LLVM e são escritas em C++ - Compilando os fontes do runtime com
em++eemar, é possível gerar a biblioteca estáticabuild/flang/runtime/libFortranRuntime.a - Ao ligar essa biblioteca, o build de
Hello, World!avança, mas inicialmente aparecem avisos de incompatibilidade de assinatura de função
Terceiro problema: diferença no tamanho de long entre host e alvo
- Ao ligar
hello.ocom a biblioteca de runtime Fortran, surge um aviso de incompatibilidade de assinatura em_FortranAioOutputAscii- o objeto Fortran espera
(i32, i32, i64) -> i32 - o runtime compilado com Emscripten define
(i32, i32, i32) -> i32
- o objeto Fortran espera
- Em WebAssembly, os tipos de argumentos e retorno de símbolos definidos em várias unidades de compilação precisam ser consistentes
- O problema não fica só no aviso: ao rodar no Node, falha com
RuntimeError: unreachable - Em
RTBuilder.h, do LLVM Flang, há um comentário TODO dizendo que o uso desizeofassumebuild == host == target - Em hosts Unix modernos de 64 bits,
sizeof(long)é 8 bytes, mas no alvowasm32-unknown-emscriptendeveria ser 4 bytes - Ao passar argumentos do tipo
CHARACTERem Fortran para funções ou sub-rotinas, pode ser adicionado um argumento oculto contendo o tamanho da string - Na biblioteca de runtime Fortran, esse argumento de tamanho é declarado como
size_te, por meio de uma cadeia detypedef, equivale aunsigned long - Essa diferença no tamanho do argumento oculto gera a incompatibilidade entre
i64ei32
Patch temporário: forçar valores de 4 bytes
- A solução ideal seria o
flang-newemitiri32oui64adequados à arquitetura e ao modelo de dados do alvo, independentemente do host, em compilação cruzada - Por enquanto, usa-se um patch que fixa o tamanho de
longem 4 bytes, ajustado parawasm32e Emscripten - O patch tem duas partes
- Em
RTBuilder.h, os tipos-modelo delongeunsigned longsão forçados para8 * 4em vez de8 * sizeof(...) - Em
CodeGen.cpp, os argumentos de chamadas amalloc()passam a ser gerados como inteiros de 32 bits em vez de 64 bits, e o tamanho de alocação é convertido parai32
- Em
- Essa mudança também corrige alocação dinâmica baseada em
ALLOCATE(), introduzida no Fortran 90 - Após recompilar, ao ligar
hello.f08ehello.ccom a biblioteca de runtime, o build ocorre sem avisos e o Node imprimeHello, World!
Compilando o BLAS para WebAssembly
- O BLAS é um conjunto de rotinas de baixo nível para operações comuns de álgebra linear, como multiplicação de matrizes e vetores
- As rotinas originais do BLAS foram publicadas em 1979 e se tornaram o padrão de fato em computação numérica
- A implementação de referência do BLAS 3.12.0 é escrita em Fortran 90 e pode ser obtida no netlib
- No
make.inc, as seguintes ferramentas são definidas para o buildFC = ../build/bin/flang-newFFLAGS = -O2AR = emarRANLIB = emranlib
- O resultado do build é a biblioteca estática
blas_LINUX.a - A rotina Fortran de exemplo
barchama a rotina BLAS nível 2ZGEMV() ZGEMV()executa operação de matriz-vetor complexa, e o exemplo usa argumentosCOMPLEX(KIND=8)e o argumento de configuraçãoCHARACTER'N'- Um programa em C cria arrays complexos, os passa à rotina Fortran e então imprime o resultado
- O resultado no Node é o seguinte
Y[0]: 23.000000 + 6.000000iY[1]: 18.000000 + 10.000000iY[2]: 6.000000 + 16.000000i
- Isso confirma que o BLAS compilado a partir de código-fonte Fortran 90 roda sob WebAssembly
Exemplo no navegador: classificador de dígitos manuscritos
- A demo classifica dígitos de 0 a 9 desenhados à mão com uma rede neural artificial do tipo multilayer perceptron (MLP)
- O usuário pode desenhar um número com mouse ou tela de toque, e as probabilidades relativas previstas pela rede aparecem no gráfico à direita
- Os pesos do modelo foram pré-treinados em Python, mas a classificação é feita em tempo de execução no navegador com JavaScript e WebAssembly
- O processo de classificação com MLP é essencialmente uma repetição de somas e multiplicações matriz-vetor
- O trabalho pesado de computação é feito por uma única sub-rotina Fortran usando a rotina BLAS nível 2
DGEMV()
Compilando o LAPACK para WebAssembly
- O LAPACK é uma biblioteca de software para resolver numericamente problemas de álgebra linear, construída sobre o BLAS
- A implementação de referência do LAPACK 3.12.0 é fornecida pelo netlib e distribuída sob licença BSD modificada
- Após copiar
make.inc.exampleparamake.inc, as seguintes configurações são alteradas- definir
FCcom o caminho completo doflang-newcompilado FFLAGS = -O2AR = emarRANLIB = emranlibTIMER = INT_CPU_TIME
- definir
- O comando
make libgera a biblioteca estática WebAssemblyliblapack.a - Depois disso, as rotinas LAPACK podem ser chamadas de forma parecida com o exemplo do BLAS
Exemplo no navegador: interpolação polinomial com álgebra linear
- A demo encontra um polinômio interpolador para um conjunto de pontos, mostrando rotinas LAPACK rodando no navegador
- Quando o usuário clica no gráfico para adicionar novos pontos, o sistema encontra o polinômio interpolador que passa por todos eles
- O método usado é o método de Vandermonde
- O sistema de álgebra linear obtido dessa forma é resolvido numericamente com a rotina
DGELS()do LAPACK - Sempre é possível encontrar um polinômio de grau
n-1que passa exatamente pornpontos de dados - Quando
ncresce, pode surgir o fenômeno de Runge, em que o polinômio oscila muito entre pontos consecutivos; isso pode ser evitado com interpolação por splines
Tarefas pendentes e ferramentas oferecidas
- Com um LLVM Flang com patches, é possível compilar código Fortran moderno em objetos WebAssembly
- A vantagem dessa abordagem é poder usar ferramentas e bibliotecas Fortran já existentes, sem reescrever algoritmos numéricos para a web em JavaScript
- Se o
flang-newpassar a ter suporte oficial a WebAssembly, o webR e pacotes R terão menos peso para manter um fork do LLVM - No momento, ainda é preciso um caminho melhor para corrigir adequadamente os problemas de compilação cruzada no LLVM Flang para todos os alvos
- Para quem não quer ou não consegue compilar o LLVM Flang manualmente, há um container Docker com binários do LLVM Flang com patches no GitHub Container Registry
1 comentários
Comentários do Hacker News
Para dar um pouco mais de contexto, esta exploração de Fortran faz parte do excelente trabalho WebR que George vem desenvolvendo para executar R no navegador
O código-fonte do R contém bastante código Fortran, e pelo que sei o WebR originalmente compilava primeiro o Fortran para C com f2c, e depois compilava esse C para wasm
Graças a um patch no LLVM Flang, agora é possível compilar o WebR com um compilador Fortran de verdade
Embora George não diga isso diretamente no post do blog, ele já comentou que espera que o Flang incorpore esse patch ou o implemente de uma forma melhor
Se isso acontecer, não será mais necessário manter um patch separado, e um Flang sem modificações poderá compilar para wasm, o que também beneficiaria outros projetos que usam Fortran
https://docs.r-wasm.org/webr/latest/
Mas eu estou focado no trabalho necessário para concluir o desenvolvimento do produto Fortran da Nvidia, então não tenho tempo sobrando para dedicar a esse tipo de tarefa
Há 20 anos eu trabalhei em um compilador FORTRAN na Xilinx, e tudo de que me lembro é que havia uma definição de
barfno arquivo de cabeçalho 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)
É realmente ótimo usar o exemplo não trivial mais simples possível como forma de explicar
Como o texto parte do problema concreto de “chamar uma função BLAS a partir de JavaScript”, senti que aprendi bastante; excelente artigo
[1] https://en.m.wikipedia.org/wiki/The_Theoretical_Minimum
Não sei se devo ficar impressionado ou horrorizado, e provavelmente são os dois
Ao compilar o f18, eu recomendaria usar a fonte mais recente de llvm-project/main
O projeto anda rápido, então é um desperdício de tempo depurar problemas que já foram corrigidos ou deixar passar recursos que já foram implementados
A intenção é trabalhar no port para WebAssembly para levar a cadeia intermediária até um ponto em que ela funcione também para Fortran?
Não entendo muito de desenvolvimento com WebAssembly
Do ponto de vista do consumidor, WebAssembly já oferece algo útil agora, ou isso está mais para um trabalho de base visando um futuro em que os programas sejam realmente portáveis?
Já ouvi dizer que dispositivos WebAssembly facilitam restringir acesso a coisas como rede ou arquivos, mas não sei se isso é só teórico ou se já foi implementado
A diferença principal é que o próprio Wasm não tem biblioteca padrão e não expõe funções de entrada e saída
Assim, o host — ou seja, quem implementa a VM — pode expor as funções que quiser para o binário Wasm usar, e o binário Wasm só consegue acessar o mundo externo por meio dessas funções
Outra vantagem é que o formato binário não é proprietário e há uma especificação, então qualquer pessoa pode implementar uma VM de Wasm
Dito isso, eu ainda não diria que está em um bom estado; ainda é cedo demais, há muitos novos recursos sendo padronizados por grupos parecidos com o W3C, e o processo é muito lento
Também há formas de distribuir para a maioria dos alvos ou fazer compilação cruzada
É parecido com o fato de que normalmente você não sabe nem se importa muito se a CPU do computador é ARM ou x86
Então, se você não liga para detalhes como rodar em código nativo ou sobre uma VM como JVM, .NET ou WASM, é difícil dizer o que exatamente isso oferece a mais em relação a outras soluções
Normalmente só os maus exemplos chamam atenção, e isso vira memes como “todo programa Electron é um monstro inchado que consome recursos, e todo app nativo é automaticamente uma maravilha da engenharia de software eficiente”
Se eu tivesse guardado o código Fortran 78 que escrevi em 1981/82, poderia testar se ele roda aqui; que pena
Era um formatador de código-fonte da linguagem de programação Jovial, e não era exatamente o tipo de coisa que se faria em Fortran, mas na época era a única opção
Existe algum ecossistema de álgebra linear em JavaScript minimamente pronto para produção?
Quando pesquiso, em geral só aparecem ports em JavaScript de bibliotecas conhecidas de uns 10 anos atrás, por exemplo via emscripten, então fico pensando se estou deixando passar alguma coisa
É estranho não entrar mais a fundo no LFortran
Também há um exemplo excelente e impressionante de WASM rodando online
https://dev.lfortran.org/
Em 2020, faltavam muitos recursos e ele suportava apenas um subconjunto muito pequeno de Fortran, mas agora suporta um conjunto bem mais amplo de recursos da linguagem e consegue compilar uma quantidade considerável de código Fortran
Também pode compilar para WebAssembly por padrão
Ainda assim, o LFortran continua um pouco bruto para uso, e os desenvolvedores afirmam que o projeto é atualmente considerado em estágio alfa e que podem surgir problemas ao compilar código real
Alguns projetos, como o MINPACK, podem ser compilados com sucesso, mas como ele ainda não oferece suporte a toda a especificação de Fortran, muitos projetos maiores ainda não podem ser compilados
Os desenvolvedores do LFortran têm como objetivo oferecer suporte completo ao Fortran 2018, e um recurso de destaque é o REPL interativo de Fortran, semelhante ao Jupyter
Com mais alguns anos de desenvolvimento, parece que pode se tornar uma excelente opção para compilar código Fortran para WebAssembly
Também é dito para ver a demonstração do LFortran em https://dev.lfortran.org, que é muito impressionante, mas a primeira coisa que tentei fazer, mudar
x * 2parax * 3, não era suportada pelo gerador de código atualTambém existe Fortran sobre .NET e Java
https://www.silverfrost.com/14/ftn95/ftn95_fortran_95_for_microsoft_dotnet_features.aspx
https://dl.acm.org/doi/10.1145/376656.376833
Quando trabalhei em https://medium.com/@tomasreimers/compiling-tensorflow-for-the-browser-f3387b8e1e1c, fiquei realmente aliviado que o TensorFlow usasse Eigen em vez de BLAS/Lapack, uma biblioteca matemática popular escrita em Fortran
Caso contrário, teria dado muito mais trabalho