1 puntos por GN⁺ 2024-04-07 | 1 comentarios | Compartir por WhatsApp
  • Para llevar código numérico Fortran heredado de ciencia e ingeniería al navegador, hace falta una ruta de compilación a WebAssembly, y webR usa un flang-new parcheado de LLVM
  • f2c, LFortran, Dragonegg, Classic Flang y LLVM Flang tienen limitaciones respectivamente en soporte de Fortran moderno, arquitecturas objetivo y mantenibilidad, así que todavía no existe una solución estándar simple
  • LLVM Flang no soporta directamente el objetivo wasm32-unknown-emscripten, por lo que hace falta agregar TargetWasm32, y al enlazar el runtime aparece un problema por la diferencia en el tamaño de long entre host y objetivo
  • Combinando flang-new parcheado, Emscripten y bibliotecas estáticas del runtime de Fortran, se pueden llamar subrutinas Fortran desde C o JavaScript y también manejar PRINT, ALLOCATE y argumentos CHARACTER
  • Las implementaciones de referencia de BLAS 3.12.0 y LAPACK 3.12.0 se compilaron como bibliotecas estáticas para WebAssembly, y se ejecutaron en el navegador demos de clasificación de dígitos escritos a mano e interpolación polinómica

Llevar código numérico Fortran existente al navegador

  • Fortran es un lenguaje antiguo que apareció en 1957, pero se ha usado durante mucho tiempo en cálculo científico e ingenieril, y Fortran moderno ya se apartó en gran medida de las restricciones de formato fijo de Fortran 77
  • El objetivo es compilar rutinas modernas de Fortran a WebAssembly para ejecutarlas en el navegador, recibir argumentos numéricos, ejecutar rutinas de BLAS y LAPACK, y luego devolver resultados o imprimirlos en la consola
  • Este enfoque permite llevar a la web entornos de programación de más alto nivel que dependen de BLAS y LAPACK, como SciPy o R
  • En lugar de reescribir rutinas numéricas en JavaScript o Rust, se pueden aprovechar herramientas y bibliotecas ya validadas basadas en Fortran
  • El proyecto webR compila código Fortran a WebAssembly usando el compilador parcheado flang-new de LLVM
  • El método actual depende de hacks, y sin ayuda de desarrolladores de compiladores con más experiencia es difícil contribuir estos cambios a LLVM

Estado de las herramientas Fortran→WebAssembly

  • A 2024 existen varias herramientas y toolchains, pero todavía no hay una solución simple con funcionalidad completa
  • f2c

    • f2c convierte Fortran 77 a código C, y Emscripten luego puede compilarlo a WebAssembly
    • Pyodide usa este método al compilar paquetes de Python que incluyen código Fortran
    • Incluso en la hoja de ruta de Pyodide, este enfoque se evalúa como “no funciona bien”
    • No encaja con código Fortran moderno, y aun después de la conversión requiere errores graves corregidos y parches extensivos
  • LFortran

    • LFortran ha ampliado bastante sus capacidades en los últimos años y puede compilar directamente a WebAssembly
    • Sus desarrolladores señalan que sigue en fase alfa, así que se esperan problemas al compilar código real
    • Algunos proyectos como MINPACK sí se pueden compilar, pero como no soporta toda la especificación de Fortran, proyectos más grandes pueden fallar
    • Su meta de desarrollo es soportar completamente Fortran 2018, y una de sus funciones más destacadas es un REPL interactivo de Fortran similar a Jupyter
  • Dragonegg

    • Dragonegg es un plugin de GCC que usa el frontend de GCC para exportar LLVM IR
    • Puede generar salida WebAssembly mediante el backend de LLVM, y así fue como webR compiló por primera vez código fuente Fortran a WebAssembly
    • Sus últimas versiones soportadas son gcc-4.8 y llvm-3.3, así que requiere GCC y LLVM muy antiguos
    • La mayoría de usuarios necesita una VM o un contenedor Docker, y además el LLVM IR exportado por Dragonegg requiere postprocesamiento adicional para producir salida WebAssembly
    • En 2020, en la práctica, casi era la única forma de compilar código Fortran a WebAssembly
  • Classic Flang

    • Classic Flang es un compilador Fortran orientado a LLVM basado en el pgfortran de PGI/NVIDIA liberado como código abierto
    • No soporta salida de 32 bits, así que no se puede usar para el objetivo wasm32
    • Firefox, Chrome y Node soportan wasm64 al momento de escribir esto, pero está oculto detrás de flags de funcionalidad
    • La documentación del proyecto también indica que quizá no sea buena idea elegir Classic Flang para proyectos nuevos
  • LLVM Flang

    • LLVM Flang es un proyecto que reimplementa desde cero un frontend Fortran para LLVM, y forma parte del proyecto LLVM desde LLVM 11
    • Todavía no se considera listo para producción, pero la versión preproducción de flang-new ya es bastante utilizable para compilar código Fortran real
    • En su estado predeterminado no puede generar salida WebAssembly
    • Aprovechando el diseño modular de LLVM, se puede usar el frontend de Flang junto con el backend de WebAssembly de LLVM
    • Ya era posible en 2020, pero requería parches más grandes a LLVM, inyección de rutinas matemáticas personalizadas y un proceso de compilación de múltiples etapas
    • Ahora, gracias al desarrollo del frontend flang-new, se puede construir un compilador Fortran→WebAssembly con cambios pequeños en el código fuente de LLVM

Compilar LLVM Flang para WebAssembly

  • El LLVM instalado con un gestor de paquetes puede no incluir el binario flang-new
    • Por ejemplo, en LLVM v17.0.6 de Homebrew para macOS no se encuentra el comando flang-new
  • Como hay que modificar el código fuente de LLVM Flang, se compiló directamente el código fuente de LLVM v18.1.1
  • La configuración de CMake deja el triple objetivo por defecto como wasm32-unknown-emscripten y habilita el objetivo WebAssembly y los proyectos clang;flang;mlir
  • Al terminar la compilación, build/bin/flang-new --version permite verificar la siguiente información
    • Versión: flang-new version 18.1.1
    • Objetivo: wasm32-unknown-emscripten
    • Modelo de hilos: posix

Primer problema: el objetivo wasm32 no está implementado

  • Al compilar una subrutina Fortran simple foo.f08 con flang-new, aparece el siguiente error
    • not yet implemented: target not implemented
  • La causa es que el triple objetivo wasm32-unknown-emscripten todavía no está implementado en flang-new
  • La solución es un parche que agrega las características del objetivo TargetWasm32 en flang/lib/Optimizer/CodeGen/Target.cpp
    • Configura el ancho por defecto en 32
    • Define el marshaling para convertir argumentos y tipos de retorno complejos a tipos LLVM del lado de WebAssembly
    • Conecta TargetWasm32 en la rama llvm::Triple::ArchType::wasm32
  • Después de aplicar el parche y recompilar, el código fuente Fortran se compila como objeto WebAssembly
    • Resultado de file foo.o: WebAssembly (wasm) binary module version 0x1 (MVP)
    • En llvm-nm foo.o se puede verificar el símbolo foo_

Llamar subrutinas Fortran desde C y JavaScript

  • Las rutinas Fortran normalmente pasan argumentos por referencia, y INTENT() puede declarar cómo se usa cada argumento
  • La subrutina Fortran de ejemplo foo recibe argumentos enteros x, y, z y realiza z = x + y
  • En una compilación nativa, al ejecutar gfortran -c foo.f08 -o foo.o, el nombre del símbolo puede llevar un guion bajo al final, como foo_
  • Al llamarlo desde C, el símbolo externo se declara como extern void foo_(int*, int*, int*); y los argumentos se pasan por dirección
  • En WebAssembly, foo.o generado con flang-new se puede enlazar con código C usando Emscripten
    • emcc main.c foo.o -o main.js
    • Salida de node main.js: 1 + 1 = 2
  • Llamada directa desde JavaScript

    • También se puede llamar una subrutina Fortran directamente desde JavaScript sin código C intermedio
    • En la etapa de enlace con Emscripten se exportan _foo_, _malloc y _free
    • emcc foo.o -sEXPORTED_FUNCTIONS=_foo_,_malloc,_free -o foo.js
    • JavaScript asigna memoria para enteros con Module._malloc(), escribe valores en Module.HEAPU32 y luego llama Module._foo_(x, y, z)
    • El resultado de ejecución es el siguiente
    • x = 123
    • y = 456
    • x + y = 579
    • En el navegador, cargando foo.js y standalone.js desde HTML, se puede ver el mismo resultado en la consola de JavaScript

Segundo problema: la biblioteca runtime de Fortran

  • Al compilar una subrutina Fortran con PRINT *, "Hello, World!", en la etapa de enlace aparecen errores por símbolos runtime faltantes
    • _FortranAioBeginExternalListOutput
    • _FortranAioOutputAscii
    • _FortranAioEndIoStatement
  • La causa es que la biblioteca runtime de LLVM Fortran todavía no se había compilado para WebAssembly
  • La biblioteca runtime está escrita en C++ dentro de llvm-project/flang/runtime en el árbol de código fuente de LLVM
  • Si se compilan las fuentes runtime con em++ y emar de Emscripten, se puede crear la biblioteca estática build/flang/runtime/libFortranRuntime.a
  • Al enlazar esa biblioteca, la compilación de Hello, World! avanza, pero al principio aparecen advertencias por incompatibilidad de firmas de funciones

Tercer problema: diferencia en el tamaño de long entre host y objetivo

  • Al enlazar hello.o con la biblioteca runtime de Fortran, aparece una advertencia de incompatibilidad de firma para _FortranAioOutputAscii
    • Del lado del objeto Fortran se espera (i32, i32, i64) -> i32
    • Del lado del runtime compilado con Emscripten se define como (i32, i32, i32) -> i32
  • En WebAssembly, los tipos de argumentos y retorno de símbolos definidos en múltiples unidades de compilación deben ser consistentes
  • Este problema no se queda en advertencia: al ejecutar en Node falla con RuntimeError: unreachable
  • En RTBuilder.h de LLVM Flang hay un comentario TODO que indica que el uso de sizeof asume build == host == target
  • En hosts Unix modernos de 64 bits, sizeof(long) es de 8 bytes, pero en el objetivo wasm32-unknown-emscripten debería ser de 4 bytes
  • Al pasar argumentos de tipo CHARACTER de Fortran a funciones o subrutinas, puede agregarse un argumento oculto para transmitir la longitud de la cadena
  • En la biblioteca runtime de Fortran, este argumento de longitud se declara como size_t, que a través de una cadena de typedef termina siendo equivalente a unsigned long
  • Esa diferencia de tamaño en el argumento oculto causa la incompatibilidad entre i64 e i32

Parche temporal: forzar valores de 4 bytes

  • La solución ideal sería que flang-new emita i32 o i64 de acuerdo con la arquitectura objetivo y el modelo de datos, sin depender del host al hacer compilación cruzada
  • Por ahora se usa un parche que fija por fuerza el tamaño de long en 4 bytes para ajustarse a wasm32 y Emscripten
  • El parche tiene dos partes
    • En RTBuilder.h, los tipos modelo para long y unsigned long se fuerzan a 8 * 4 en lugar de 8 * sizeof(...)
    • En CodeGen.cpp, los argumentos de llamadas a malloc() se generan como enteros de 32 bits en vez de 64 bits, y el tamaño de asignación se castea a i32
  • Este cambio también corrige la asignación dinámica basada en ALLOCATE() introducida en Fortran 90
  • Tras recompilar, al enlazar hello.f08 y hello.c con la biblioteca runtime, la compilación ya no muestra advertencias y en Node aparece la siguiente salida
    • Hello, World!

Compilar BLAS para WebAssembly

  • BLAS es un conjunto de rutinas de bajo nivel para operaciones comunes de álgebra lineal, como multiplicación de matrices y vectores
  • Las rutinas originales de BLAS se publicaron en 1979 y son un estándar de facto en computación numérica
  • La implementación de referencia BLAS 3.12.0 está escrita en Fortran 90 y puede obtenerse desde netlib
  • En make.inc se configuran las siguientes herramientas para compilar
    • FC = ../build/bin/flang-new
    • FFLAGS = -O2
    • AR = emar
    • RANLIB = emranlib
  • El resultado de compilación es la biblioteca estática blas_LINUX.a
  • La rutina Fortran de ejemplo bar llama a la rutina BLAS de nivel 2 ZGEMV()
  • ZGEMV() realiza una operación matriz-vector compleja, y el ejemplo usa argumentos COMPLEX(KIND=8) y el argumento de configuración CHARACTER 'N'
  • Un programa en C crea arreglos complejos, los pasa a la rutina Fortran e imprime el resultado
  • La salida al ejecutar en Node es la siguiente
    • Y[0]: 23.000000 + 6.000000i
    • Y[1]: 18.000000 + 10.000000i
    • Y[2]: 6.000000 + 16.000000i
  • Esto confirma que BLAS compilado desde código fuente Fortran 90 se ejecuta sobre WebAssembly

Ejemplo en navegador: clasificador de dígitos escritos a mano

  • La demo clasifica dígitos del 0 al 9 dibujados a mano usando una red neuronal artificial perceptrón multicapa (MLP)
  • El usuario puede dibujar un número con mouse o pantalla táctil, y las probabilidades relativas predichas por la red se muestran en la gráfica de la derecha
  • Los pesos del modelo se entrenaron previamente en Python, pero la clasificación se realiza en tiempo de ejecución en el navegador con JavaScript y WebAssembly
  • El proceso de clasificación del MLP consiste esencialmente en repeticiones de sumas y multiplicaciones matriz-vector
  • El cálculo pesado lo realiza una sola subrutina Fortran usando la rutina BLAS de nivel 2 DGEMV()

Compilar LAPACK para WebAssembly

  • LAPACK es una biblioteca de software para resolver numéricamente problemas de álgebra lineal, construida sobre BLAS
  • La implementación de referencia LAPACK 3.12.0 la ofrece netlib y se distribuye bajo una licencia BSD modificada
  • Después de copiar make.inc.example a make.inc, se cambian las siguientes opciones
    • FC se define con la ruta completa al flang-new compilado
    • FFLAGS = -O2
    • AR = emar
    • RANLIB = emranlib
    • TIMER = INT_CPU_TIME
  • El comando make lib genera la biblioteca estática para WebAssembly liblapack.a
  • Luego las rutinas de LAPACK pueden llamarse de forma similar al ejemplo de BLAS

Ejemplo en navegador: interpolación polinómica con álgebra lineal

  • La demo encuentra el polinomio interpolante para un conjunto de puntos y muestra que las rutinas de LAPACK se ejecutan en el navegador
  • Cuando el usuario hace clic en la gráfica para agregar un nuevo punto, se encuentra el polinomio interpolante que pasa por todos los puntos
  • El método usa Vandermonde’s method
  • El sistema de álgebra lineal obtenido con este enfoque se resuelve numéricamente con la rutina DGELS() de LAPACK
  • Siempre es posible encontrar un polinomio de grado n-1 que incluya exactamente n puntos de datos
  • Cuando n crece, puede aparecer el fenómeno de Runge, en el que el polinomio oscila fuertemente entre puntos de datos consecutivos, y eso puede evitarse con interpolación por splines

Tareas pendientes y herramientas disponibles

  • Con LLVM Flang parcheado, es posible compilar código moderno de Fortran a objetos WebAssembly
  • La ventaja de este enfoque es que permite usar herramientas y bibliotecas Fortran ya existentes, sin reescribir algoritmos numéricos para la web en JavaScript
  • Si flang-new soporta oficialmente WebAssembly, se reducirá la carga de mantener un fork de LLVM para webR y los paquetes de R
  • Por ahora todavía hace falta una mejor ruta para corregir correctamente los problemas de compilación cruzada en LLVM Flang para todos los objetivos
  • Para usuarios que no pueden o no quieren compilar LLVM Flang directamente, se ofrece en GitHub Container Registry un contenedor Docker binario de LLVM Flang parcheado

1 comentarios

 
GN⁺ 2024-04-07
Opiniones de Hacker News
  • Para dar un poco más de contexto, esta exploración de Fortran forma parte del excelente trabajo de WebR que George está haciendo para ejecutar R en el navegador.
    El código fuente de R tiene bastante código Fortran y, según entiendo, WebR originalmente compilaba Fortran a C primero con f2c, y luego compilaba ese C a wasm.
    Gracias al parche de LLVM Flang, ahora se puede compilar WebR con un compilador Fortran de verdad.
    George no lo dijo directamente en la entrada del blog, pero ha dicho que espera que Flang acepte este parche o lo implemente de una mejor manera.
    Eso eliminaría la necesidad de mantener un parche aparte, y permitiría que Flang sin modificaciones compile a wasm, lo que también beneficiaría a otros proyectos que usan Fortran.
    https://docs.r-wasm.org/webr/latest/

    • Los pull requests siempre son bienvenidos (https://github.com/llvm/llvm-project), y si necesitan ayuda pueden contactar a la comunidad de desarrollo de LLVM Fortran (https://discourse.llvm.org/c/subprojects/flang/33).
      Pero yo estoy enfocado en el trabajo necesario para terminar el desarrollo del producto Fortran de Nvidia, así que no me queda tiempo para esto.
    • Convertir F77 a JavaScript mediante transformación de código fuente ya es bastante decente, pero WASM es mejor.
  • Hace 20 años trabajé en compilación de FORTRAN en Xilinx, y lo único que recuerdo es que el archivo de encabezado f2c.h tenía una definición de barf.
    /* 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)

    • ¿Que lo escriba todo en mayúsculas, FORTRAN, dice algo de mí? A mis ojos, Fortran se ve raro.
  • Me gusta mucho que se use el ejemplo no trivial más simple como forma de explicación.
    Como el artículo se basa en un problema concreto, “llamar funciones BLAS desde JavaScript”, siento que aprendí mucho; es un gran artículo.

  • No sé si estar maravillado u horrorizado; probablemente ambas cosas.
    Al compilar f18, recomiendo usar el código fuente más reciente de llvm-project/main.
    El proyecto avanza rápido, así que se pierde tiempo si uno depura problemas que ya fueron corregidos o se pierde funciones que ya fueron implementadas.

    • No entendí lo suficiente el código fuente de LLVM como para captar la idea principal.
      ¿Se trata de trabajar en el port a WebAssembly para llevar el código intermedio hasta el punto en que funcione con Fortran?
  • No sé mucho de desarrollo con WebAssembly.
    Desde el punto de vista del consumidor, ¿WebAssembly ofrece algo ahora mismo? ¿O es más bien trabajo de base para un futuro en el que los programas sean verdaderamente portables?
    He escuchado que los dispositivos WebAssembly facilitan limitar accesos como red o archivos, pero no sé si eso es teórico o si ya está implementado.

    • Básicamente, Wasm es una máquina virtual, y se parece mucho a la JVM en cuanto a portabilidad.
      La diferencia clave es que Wasm en sí no tiene biblioteca estándar ni expone funciones de entrada/salida.
      Por eso, como host, puedes crear la VM tú mismo y exponer funciones para que el binario Wasm las importe, y el binario Wasm solo puede acceder al mundo exterior a través de esas funciones.
      Otra ventaja es que el formato binario no es propietario y tiene una especificación, así que cualquiera puede implementar una VM de Wasm.
      Dicho eso, por ahora todavía no diría que esté en buen estado: es demasiado temprano, hay muchas funciones nuevas en estandarización por grupos como el W3C, y el proceso es muy lento.
    • Si eres desarrollador o estás distribuyendo un producto y quieres sandboxing robusto, WASM es probablemente una de las mejores opciones disponibles hoy.
      También hay formas de distribuir o hacer compilación cruzada para la mayoría de los objetivos.
    • Si está bien implementado, como cliente no deberías notarlo.
      Es parecido a cómo normalmente no sabes ni te importa mucho si la CPU de tu computadora es ARM o x86.
      Así que, si no te interesan detalles como si corre como código nativo o sobre una VM como JVM, .NET o WASM, es difícil decir qué ofrece por encima de otras soluciones.
      Normalmente solo se notan los malos casos, y eso se convierte en memes como “todos los programas Electron son monstruos inflados que devoran recursos, y todas las apps nativas son automáticamente maravillas de ingeniería de software eficiente”.
  • Qué lástima no haber guardado el código Fortran 78 que escribí en 1981/82; habría podido probar si corre aquí.
    Era un formateador de código fuente para el lenguaje de programación Jovial, y no era algo que tuviera sentido hacer en Fortran, pero en ese momento era la única opción.

    • ¿Trabajabas en Hughes en Orange County?
  • ¿Existe algún ecosistema de álgebra lineal listo para producción, o al menos bastante listo, que se pueda usar desde JavaScript?
    Al buscar, casi siempre aparecen ports a JavaScript de bibliotecas conocidas de hace unos 10 años, por ejemplo ports vía emscripten, y me pregunto si me estoy perdiendo algo.

    • ¿Hay algún equivalente de BLAS sobre WebGPU o WebNN?
  • Es raro que no se profundice más en LFortran
    También tiene un ejemplo WASM excelente y sorprendente que corre en línea
    https://dev.lfortran.org/

    • El artículo dice que el compilador LFortran ha avanzado mucho en los últimos años
      En 2020 le faltaban muchas funciones y solo soportaba un subconjunto muy pequeño de Fortran, pero ahora soporta un conjunto mucho más amplio de características del lenguaje y puede compilar bastante código Fortran
      También puede compilar de forma nativa a WebAssembly
      Sin embargo, todavía hay aspectos poco pulidos para usar LFortran, y los desarrolladores afirman que el proyecto actualmente se considera en etapa alfa y que pueden surgir problemas al compilar código real
      Algunos proyectos, como MINPACK, se pueden compilar con éxito, pero como aún no soporta toda la especificación de Fortran, muchos proyectos más grandes todavía no se pueden compilar
      Los desarrolladores de LFortran apuntan a soporte completo de Fortran 2018, y una característica destacada es un REPL interactivo de Fortran al estilo de Jupyter
      Con unos años más de desarrollo, parece que será una excelente opción para compilar código Fortran para WebAssembly
      Además, se recomienda ver la demo de LFortran en https://dev.lfortran.org, que es muy impresionante, aunque mi primer intento de cambiar x * 2 por x * 3 no fue soportado por el generador de código actual
  • También existe Fortran sobre .NET y Java
    https://www.silverfrost.com/14/ftn95/ftn95_fortran_95_for_microsoft_dotnet_features.aspx
    https://dl.acm.org/doi/10.1145/376656.376833

  • Cuando trabajé en https://medium.com/@tomasreimers/compiling-tensorflow-for-the-browser-f3387b8e1e1c, pensé que fue una gran suerte que TensorFlow usara Eigen y no BLAS/Lapack, la popular biblioteca matemática escrita en Fortran
    De lo contrario, habría sido mucho más trabajo