1 pontos por GN⁺ 2024-04-07 | 1 comentários | Compartilhar no WhatsApp
  • 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-new do 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 de TargetWasm32, e na ligação com o runtime surge o problema da diferença no tamanho de long entre host e alvo
  • Com flang-new com 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 com PRINT, ALLOCATE e argumentos CHARACTER
  • 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-new do 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
  • f2c

    • f2c converte 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.8 e llvm-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 pgfortran da 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 wasm64 no 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
  • 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-new já 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-new não é encontrado
  • 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-emscripten e ativa o alvo WebAssembly e os projetos clang;flang;mlir
  • Depois do build, build/bin/flang-new --version mostra as seguintes informações
    • versão: flang-new version 18.1.1
    • alvo: wasm32-unknown-emscripten
    • modelo de threads: posix

Primeiro problema: alvo wasm32 não implementado

  • Ao compilar uma sub-rotina Fortran simples foo.f08 com flang-new, aparece o seguinte erro
    • not yet implemented: target not implemented
  • A causa é que o triple-alvo wasm32-unknown-emscripten ainda não está implementado no flang-new
  • A solução é um patch que adiciona as características de alvo TargetWasm32 em flang/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 TargetWasm32 ao ramo llvm::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ímbolo foo_

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 foo recebe os inteiros x, y e z e faz z = 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, como foo_
  • 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.o gerado por flang-new pode ser ligado com código C via Emscripten
    • emcc 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_, _malloc e _free
    • emcc foo.o -sEXPORTED_FUNCTIONS=_foo_,_malloc,_free -o foo.js
    • O JavaScript aloca espaço para inteiros com Module._malloc(), escreve valores em Module.HEAPU32 e então chama Module._foo_(x, y, z)
    • O resultado da execução é o seguinte
    • x = 123
    • y = 456
    • x + y = 579
    • No navegador, carregando foo.js e standalone.js em 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/runtime no código-fonte do LLVM e são escritas em C++
  • Compilando os fontes do runtime com em++ e emar, é possível gerar a biblioteca estática build/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.o com 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
  • 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 de sizeof assume build == host == target
  • Em hosts Unix modernos de 64 bits, sizeof(long) é 8 bytes, mas no alvo wasm32-unknown-emscripten deveria ser 4 bytes
  • Ao passar argumentos do tipo CHARACTER em 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_t e, por meio de uma cadeia de typedef, equivale a unsigned long
  • Essa diferença no tamanho do argumento oculto gera a incompatibilidade entre i64 e i32

Patch temporário: forçar valores de 4 bytes

  • A solução ideal seria o flang-new emitir i32 ou i64 adequados à 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 long em 4 bytes, ajustado para wasm32 e Emscripten
  • O patch tem duas partes
    • Em RTBuilder.h, os tipos-modelo de long e unsigned long são forçados para 8 * 4 em vez de 8 * sizeof(...)
    • Em CodeGen.cpp, os argumentos de chamadas a malloc() passam a ser gerados como inteiros de 32 bits em vez de 64 bits, e o tamanho de alocação é convertido para i32
  • Essa mudança também corrige alocação dinâmica baseada em ALLOCATE(), introduzida no Fortran 90
  • Após recompilar, ao ligar hello.f08 e hello.c com a biblioteca de runtime, o build ocorre sem avisos e o Node imprime
    • Hello, 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 build
    • FC = ../build/bin/flang-new
    • FFLAGS = -O2
    • AR = emar
    • RANLIB = emranlib
  • O resultado do build é a biblioteca estática blas_LINUX.a
  • A rotina Fortran de exemplo bar chama a rotina BLAS nível 2 ZGEMV()
  • ZGEMV() executa operação de matriz-vetor complexa, e o exemplo usa argumentos COMPLEX(KIND=8) e o argumento de configuração CHARACTER '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.000000i
    • Y[1]: 18.000000 + 10.000000i
    • Y[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.example para make.inc, as seguintes configurações são alteradas
    • definir FC com o caminho completo do flang-new compilado
    • FFLAGS = -O2
    • AR = emar
    • RANLIB = emranlib
    • TIMER = INT_CPU_TIME
  • O comando make lib gera a biblioteca estática WebAssembly liblapack.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-1 que passa exatamente por n pontos de dados
  • Quando n cresce, 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-new passar 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

 
GN⁺ 2024-04-07
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/

    • Pull requests são sempre bem-vindos (https://github.com/llvm/llvm-project) e, se precisar de ajuda, você pode entrar em contato com a comunidade de desenvolvimento do LLVM Fortran (https://discourse.llvm.org/c/subprojects/flang/33)
      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
    • Fazer a transpilação de F77 para JavaScript já funciona bastante bem, mas WASM é melhor
  • Há 20 anos eu trabalhei em um compilador FORTRAN na Xilinx, e tudo de que me lembro é que havia uma definição de barf no 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)

    • Será que escrever FORTRAN todo em maiúsculas diz alguma coisa sobre mim? Para mim, Fortran parece estranho
  • É 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

  • 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

    • Eu não entendia o suficiente do código-fonte do LLVM para captar bem a ideia
      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

    • Basicamente, Wasm é uma máquina virtual, muito parecida com a JVM no aspecto da portabilidade
      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
    • Se você é desenvolvedor ou distribui produtos e quer sandboxing robusto, o WASM está perto de ser a melhor opção disponível hoje
      Também há formas de distribuir para a maioria dos alvos ou fazer compilação cruzada
    • Se estiver bem implementado, do ponto de vista do cliente ele nem deveria perceber
      É 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

    • Você trabalhou na Hughes em Orange County?
  • 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

    • Existe algum equivalente a BLAS sobre WebGPU ou WebNN?
  • É 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/

    • O texto diz que o compilador LFortran evoluiu bastante nos últimos anos
      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 * 2 para x * 3, não era suportada pelo gerador de código atual
  • També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