1 ポイント 投稿者 GN⁺ 2024-04-07 | 1件のコメント | WhatsAppで共有
  • 歴史ある科学・工学計算資産である Fortran数値コード をブラウザに持ち込むには WebAssembly へのコンパイル経路が必要であり、webR はパッチを当てた LLVM flang-new を使用している
  • f2c、LFortran、Dragonegg、Classic Flang、LLVM Flang はそれぞれ モダンFortran対応・ターゲットアーキテクチャ・保守性 の面で限界があり、いまだに単純な標準解は存在しない
  • LLVM Flang は wasm32-unknown-emscripten ターゲットをそのままサポートしておらず、TargetWasm32 の追加が必要であり、ランタイムのリンクでは ホストとターゲットの long サイズ差 が問題を引き起こす
  • パッチ済みの flang-new、Emscripten、Fortran ランタイム静的ライブラリを組み合わせると、Fortran サブルーチンを C や JavaScript から呼び出せ、PRINTALLOCATECHARACTER 引数にも対応できる
  • BLAS 3.12.0 と LAPACK 3.12.0 のリファレンス実装を WebAssembly 静的ライブラリとしてビルドし、手書き数字分類多項式補間 のデモをブラウザで実行している

既存の Fortran 数値コードをブラウザへ持ち込む

  • Fortran は 1957 年に登場した古い言語だが、科学・工学計算で長く使われてきており、モダン Fortran は Fortran 77 の固定形式という制約の大半から脱している
  • 目標はモダン Fortran ルーチンを WebAssembly にコンパイルしてブラウザで実行し、数値引数を受け取り、BLAS・LAPACK ルーチンで計算したあと結果を返すかコンソールに出力すること
  • このアプローチにより、SciPy や R のように BLAS・LAPACK に依存する高水準プログラミング環境を Web に持ち込める
  • JavaScript や Rust で数値ルーチンを書き直すことなく、すでに検証済みの Fortran ベースのツールやライブラリを活用できる
  • webR プロジェクトは、パッチを当てた LLVM flang-new コンパイラで Fortran コードを WebAssembly にコンパイルしている
  • 現在の方法はハックに依存しており、より経験豊富なコンパイラ開発者の助けなしに変更を LLVM へコントリビュートするのは難しい

Fortran→WebAssembly ツールの現状

  • 2024 年時点で複数のツールやツールチェーンがあるが、完全な機能 を備えた単純な解決策はまだない
  • f2c

    • f2c は Fortran 77 を C コードに変換し、Emscripten がそれを WebAssembly にコンパイルできるようにする
    • Pyodide は Fortran コードを含む Python パッケージをコンパイルする際にこの方式を使っている
    • Pyodide のロードマップでもこの方式は「うまく動かない」と評価されている
    • モダン Fortran コードには適しておらず、変換後も致命的なエラーや大規模なパッチが必要になる
  • LFortran

    • LFortran はここ数年で機能が大きく増え、WebAssembly へ直接コンパイルできる
    • まだ アルファ段階 であり、実コードのコンパイルでは問題が起きることが予想されると開発者自身が述べている
    • MINPACK のような一部プロジェクトはコンパイルできるが、Fortran 仕様全体をサポートしていないため、より大きなプロジェクトは失敗する可能性がある
    • 開発目標は Fortran 2018 の全面サポートであり、Jupyter に似た対話型 Fortran REPL が際立った機能となっている
  • Dragonegg

    • Dragonegg は GCC フロントエンドを使って LLVM IR を出力する GCC プラグイン
    • LLVM バックエンドで WebAssembly 出力を作ることができ、webR が最初に Fortran ソースを WebAssembly にコンパイルした際に使った方法でもある
    • 最新対応版が gcc-4.8llvm-3.3 であるため、非常に古い GCC・LLVM が必要になる
    • ほとんどの利用者は VM か Docker コンテナが必要で、Dragonegg が出力する LLVM IR も WebAssembly 出力のために追加の後処理を要する
    • 2020 年には Fortran コードを WebAssembly にコンパイルする現実的な方法は事実上これしかなかった
  • Classic Flang

    • Classic Flang はオープンソース化された PGI/NVIDIA pgfortran ベースの LLVM 向け Fortran コンパイラ
    • 32 ビット出力をサポートしないため、wasm32 ターゲットには使えない
    • Firefox、Chrome、Node は執筆時点で wasm64 をサポートしているが、機能フラグの背後に隠されている
    • プロジェクト文書でも、新規プロジェクトで Classic Flang を選ぶのは良い考えではないかもしれないと案内している
  • LLVM Flang

    • LLVM Flang は LLVM 向け Fortran フロントエンドをゼロから再実装したプロジェクトで、LLVM 11 から LLVM プロジェクトに含まれている
    • まだ本番運用可能とは見なされていないが、flang-new のプレプロダクション版は実際の Fortran コードのコンパイルにかなり使える水準になっている
    • デフォルト状態では WebAssembly 出力を生成できない
    • LLVM のモジュール設計を活用すれば、Flang フロントエンドと LLVM WebAssembly バックエンドを組み合わせられる
    • 2020 年にも可能ではあったが、より大きな LLVM パッチ、独自の数学ルーチン注入、多段階のコンパイル工程が必要だった
    • 現在は flang-new フロントエンドの開発が進んだおかげで、LLVM ソースへの小さな変更だけで Fortran→WebAssembly コンパイラを作れる

LLVM Flang を WebAssembly 向けにビルドする

  • パッケージマネージャで導入した LLVM には flang-new バイナリが含まれないことがある
    • 例として macOS Homebrew の LLVM v17.0.6 では flang-new コマンドが見つからなかった
  • LLVM Flang のソースを修正する必要があるため、LLVM v18.1.1 のソースを自分でビルドする
  • CMake 設定ではデフォルトのターゲットトリプルを wasm32-unknown-emscripten にし、WebAssembly ターゲットと clang;flang;mlir プロジェクトを有効化する
  • ビルド後、build/bin/flang-new --version で次の情報を確認できる
    • バージョン: flang-new version 18.1.1
    • ターゲット: wasm32-unknown-emscripten
    • スレッドモデル: posix

最初の問題: wasm32 ターゲット未実装

  • 単純な Fortran サブルーチン foo.f08flang-new でコンパイルすると次のエラーが発生する
    • not yet implemented: target not implemented
  • 原因は、flang-newwasm32-unknown-emscripten ターゲットトリプルがまだ実装されていないため
  • 解決策は flang/lib/Optimizer/CodeGen/Target.cppTargetWasm32 のターゲット特性を追加するパッチ
    • デフォルト幅を 32 に設定する
    • 複素数引数と戻り値型を WebAssembly 側の LLVM 型へ変換するマーシャリングを定義する
    • llvm::Triple::ArchType::wasm32 分岐に TargetWasm32 を接続する
  • パッチ後に再ビルドすると、Fortran ソースは WebAssembly オブジェクトへコンパイルされる
    • file foo.o の結果: WebAssembly (wasm) binary module version 0x1 (MVP)
    • llvm-nm foo.ofoo_ シンボルを確認できる

C と JavaScript から Fortran サブルーチンを呼び出す

  • Fortran ルーチンは通常、引数を 参照渡し し、INTENT() で引数の使い方を宣言できる
  • 例の Fortran サブルーチン fooxyz の整数引数を受け取り、z = x + y を実行する
  • ネイティブビルドで gfortran -c foo.f08 -o foo.o を実行すると、シンボル名は foo_ のように末尾へアンダースコアが付くことがある
  • C から呼ぶ場合は外部シンボルを extern void foo_(int*, int*, int*); のように宣言し、引数をアドレスで渡す
  • WebAssembly では、flang-new で生成した foo.o を Emscripten で C コードとリンクできる
    • emcc main.c foo.o -o main.js
    • node main.js 出力: 1 + 1 = 2
  • JavaScript から直接呼ぶ

    • C コードなしで JavaScript から Fortran サブルーチンを直接呼ぶこともできる
    • Emscripten のリンク段階で _foo__malloc_free をエクスポートする
    • emcc foo.o -sEXPORTED_FUNCTIONS=_foo_,_malloc,_free -o foo.js
    • JavaScript は Module._malloc() で整数格納領域を確保し、Module.HEAPU32 に値を書き込んだあと Module._foo_(x, y, z) を呼び出す
    • 実行結果は次の通り
    • x = 123
    • y = 456
    • x + y = 579
    • ブラウザでは foo.jsstandalone.js を HTML から読み込み、同じ結果を JavaScript コンソールで確認できる

2 つ目の問題: Fortran ランタイムライブラリ

  • PRINT *, "Hello, World!" を含む Fortran サブルーチンをビルドすると、リンク段階でランタイムシンボルがないというエラーが発生する
    • _FortranAioBeginExternalListOutput
    • _FortranAioOutputAscii
    • _FortranAioEndIoStatement
  • 原因は LLVM Fortran ランタイムライブラリがまだ WebAssembly 向けにコンパイルされていないため
  • ランタイムライブラリは LLVM ソースツリーの llvm-project/flang/runtime に C++ で書かれている
  • Emscripten の em++emar でランタイムソースをビルドすると、静的ライブラリ build/flang/runtime/libFortranRuntime.a を作成できる
  • このライブラリをリンクすると Hello, World! のビルドは進むが、当初は関数シグネチャ不一致の警告が発生する

3 つ目の問題: ホストとターゲットの long サイズ差

  • hello.o と Fortran ランタイムライブラリをリンクすると _FortranAioOutputAscii のシグネチャ不一致警告が出る
    • Fortran オブジェクト側は (i32, i32, i64) -> i32 を期待する
    • Emscripten でビルドしたランタイム側は (i32, i32, i32) -> i32 として定義される
  • WebAssembly では、複数のコンパイル単位にまたがって定義されるシンボルの引数と戻り値型は一貫していなければならない
  • この問題は警告で済まず、Node 実行時に RuntimeError: unreachable で失敗する
  • LLVM Flang の RTBuilder.h には、sizeof の使用が build == host == target を仮定しているという TODO コメントがある
  • 現代の 64 ビット Unix 系ホストでは sizeof(long) は 8 バイトだが、wasm32-unknown-emscripten ターゲットでは 4 バイトであるべき
  • Fortran の CHARACTER 型引数を関数やサブルーチンに渡す際、文字列長を渡す 隠し引数 が追加されることがある
  • Fortran ランタイムライブラリではこの長さ引数が size_t として宣言され、typedef チェーンを経て unsigned long と等価になる
  • この隠し引数のサイズ差が i64i32 の不一致を引き起こす

暫定パッチ: 4 バイト値を強制

  • 理想的な解決策は、flang-new がクロスコンパイル時にホストに依存せず、ターゲットアーキテクチャとデータモデルに合った i32 または i64 を出力すること
  • 現状では wasm32 と Emscripten に合わせて long サイズを 4 バイトにハードコードするパッチを使っている
  • パッチ内容は 2 系統ある
    • RTBuilder.hlongunsigned long のモデル型を 8 * sizeof(...) ではなく 8 * 4 に強制する
    • CodeGen.cppmalloc() 呼び出し引数を 64 ビットではなく 32 ビット整数で生成し、割り当てサイズを i32 にキャストする
  • この変更により、Fortran 90 で導入された ALLOCATE() ベースの動的確保も修正される
  • 再ビルド後、hello.f08hello.c をランタイムライブラリとリンクすると警告なしでビルドでき、Node では次の出力になる
    • Hello, World!

BLAS を WebAssembly にビルドする

  • BLAS は、行列・ベクトル積など線形代数の共通演算を実行する低水準ルーチン群
  • 元の BLAS ルーチンは 1979 年に公開され、数値計算分野の事実上の標準となっている
  • リファレンス実装の BLAS 3.12.0 は Fortran 90 で書かれており、netlib から取得できる
  • make.inc で次のツールを指定してビルドする
    • FC = ../build/bin/flang-new
    • FFLAGS = -O2
    • AR = emar
    • RANLIB = emranlib
  • ビルド結果として静的ライブラリ blas_LINUX.a が生成される
  • 例の Fortran ルーチン bar は、BLAS level 2 ルーチン ZGEMV() を呼び出す
  • ZGEMV() は複素数行列・ベクトル演算を行い、例では COMPLEX(KIND=8) 引数と CHARACTER 設定引数 'N' を使う
  • C プログラムで複素数配列を作成して Fortran ルーチンへ渡し、その後に結果を出力する
  • Node 実行結果は次の通り
    • Y[0]: 23.000000 + 6.000000i
    • Y[1]: 18.000000 + 10.000000i
    • Y[2]: 6.000000 + 16.000000i
  • この結果により、Fortran 90 ソースからコンパイルした BLAS が WebAssembly 上で動作することを確認できる

ブラウザ例: 手書き数字分類器

  • デモは 多層パーセプトロン(MLP) 人工ニューラルネットワークで、手書きの 0〜9 の数字を分類する
  • 利用者はマウスやタッチスクリーンで数字を描け、ネットワークが予測した相対確率が右側のプロットに表示される
  • モデル重みは Python で事前学習されているが、分類自体は実行時に JavaScript と WebAssembly によってブラウザ内で行われる
  • MLP の分類処理は本質的に行列・ベクトルの加算と乗算の繰り返し
  • 重い計算は Fortran サブルーチン 1 つが BLAS level 2 ルーチン DGEMV() を使って処理する

LAPACK を WebAssembly にビルドする

  • LAPACK は線形代数問題を数値的に解くソフトウェアライブラリで、BLAS の上に構築されている
  • リファレンス実装の LAPACK 3.12.0 は netlib で提供され、修正 BSD ライセンスで配布されている
  • make.inc.examplemake.inc にコピーしたうえで、次の設定を変更する
    • FC をビルドした flang-new のフルパスに指定する
    • FFLAGS = -O2
    • AR = emar
    • RANLIB = emranlib
    • TIMER = INT_CPU_TIME
  • make lib コマンドで WebAssembly 静的ライブラリ liblapack.a を生成する
  • その後、LAPACK ルーチンは BLAS の例と同様の方法で呼び出せる

ブラウザ例: 線形代数による多項式補間

  • デモは点集合に対する補間多項式を求め、LAPACK ルーチンがブラウザ内で動作することを示す
  • 利用者がプロットをクリックして新しい点を追加すると、すべての点を通る補間多項式を求める
  • 方法には Vandermonde’s method を用いる
  • この方法で得られる線形代数方程式は、LAPACK の DGELS() ルーチンで数値的に解く
  • n 個のデータ点を正確に含む n-1 次多項式は常に見つけられる
  • n が大きくなると、多項式が連続するデータ点の間で大きく振動する Runge 現象が起きることがあり、スプライン補間で回避できる

残る課題と提供ツール

  • パッチ済み LLVM Flang を使えば、モダン Fortran コードを WebAssembly オブジェクトへコンパイルできる
  • この方式の利点は、Web 向け数値アルゴリズムを JavaScript で新たに書き直さず、既存の Fortran ツールやライブラリを利用できる点にある
  • flang-new が WebAssembly を公式サポートすれば、webR と R パッケージのために LLVM フォークを維持する負担が減る
  • 現在は、LLVM Flang とクロスコンパイルの問題をあらゆるターゲットで正しく修正する、より良い道筋が必要とされている
  • LLVM Flang を自分でビルドするのが難しい、または望まない利用者向けに、GitHub Container Registry で パッチ済み LLVM Flang バイナリ Docker コンテナ が提供されている

1件のコメント

 
GN⁺ 2024-04-07
Hacker News のコメント
  • 少し背景を補足すると、この Fortran 探求は、George が R をブラウザで動かすために進めている素晴らしい WebR の取り組みの一部です。
    R のソースには Fortran コードがかなり多く含まれていて、WebR はもともと f2c で Fortran をいったん C にコンパイルし、その C を wasm にコンパイルしていたはずです。
    LLVM Flang のパッチのおかげで、WebR を本物の Fortran コンパイラでビルドできるようになりました。
    George はブログ記事で直接そう言ってはいませんが、Flang がこのパッチを受け入れるか、より良い方法で実装してくれることを望んでいる、と話していたことがあります。
    そうなれば別途パッチを維持する必要がなくなり、未改変の Flang で wasm にコンパイルできるため、Fortran を使う他のプロジェクトにも利益があります。
    https://docs.r-wasm.org/webr/latest/

    • プルリクエストはいつでも歓迎です(https://github.com/llvm/llvm-project)。助けが必要なら LLVM Fortran 開発コミュニティ(https://discourse.llvm.org/c/subprojects/flang/33)に連絡できます。
      ただし私は Nvidia の Fortran 製品の開発完了に必要な作業に集中しているため、こうした作業に割く時間は残っていません。
    • ソース間変換で F77 を JavaScript に移すのもすでにかなり良いですが、WASM のほうが優れています。
  • 20年前に Xilinx で FORTRAN のコンパイル作業をしていましたが、覚えているのは f2c.h ヘッダーファイルに 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)

    • 全部大文字で FORTRAN と書くのは、私について何かを物語っているのでしょうか。私の目には Fortran がぎこちなく見えます。
  • 説明の方法として 最も単純な非自明な例を使っているのが本当に良いです。
    記事が「JavaScript から BLAS 関数を呼び出す」という具体的な問題に基づいているので、とても学びが多く、素晴らしい記事です。

  • 感嘆すべきなのか、戦慄すべきなのか分かりません。おそらく両方でしょう。
    f18 をビルドするときは llvm-project/main の最新ソースを使うことをおすすめします。
    プロジェクトは速く動いているので、すでに修正済みの問題をデバッグしたり、すでに実装済みの機能を見逃したりすると時間の無駄になります。

    • LLVM のソースを十分に理解していないので、要点をつかむのが難しかったです。
      WebAssembly ポートに取り組んで、中間コードが Fortran まで動作する地点に持っていこうとしているのでしょうか?
  • WebAssembly 開発のことはよく分かりません。
    消費者の立場から見て、WebAssembly は今すぐ何かを提供しているのでしょうか? それとも、プログラムが本当に移植可能になる未来のための基盤作りに近いのでしょうか?
    WebAssembly の仕組みによってネットワークやファイルなどへのアクセス制限がより簡単になる、という話は聞いたことがありますが、それが理論上の話なのか、すでに実装されているものなのかは分かりません。

    • 基本的に Wasm は 仮想マシンであり、移植可能という点では JVM と非常によく似ています。
      重要な違いは、Wasm 自体には標準ライブラリもなく、入出力関数も公開していないことです。
      そのためホスト、つまり VM を自分で作り、Wasm バイナリがインポートして使う関数を公開できます。そして Wasm バイナリは、その関数群を通じてのみ外界にアクセスできます。
      もう一つの利点は、バイナリ形式が独占的なものではなく仕様があるため、誰でも Wasm VM を実装できることです。
      ただし現時点では、まだ良い状態とは言いにくく、時期尚早です。W3C のようなグループで標準化中の新機能が多く、手続きも非常に遅いです。
    • 開発者または製品を配布する立場で 堅牢なサンドボックス化を求めているなら、WASM は今使える最良の選択肢にかなり近いです。
      たいていのターゲットに配布したり、クロスコンパイルしたりする方法もあります。
    • きちんと実装されていれば、顧客側は気づかないはずです。
      コンピュータの CPU が ARM なのか x86 なのかを普通は知らず、あまり気にしないのと似ています。
      なので、ネイティブコードで動いているのか、JVM・.NET・WASM のような VM 上で動いているのかといった詳細に関心がないなら、他の解決策より何を多く提供するのかを説明するのは難しいです。
      たいていは悪い事例だけが目につき、それが「すべての Electron プログラムは肥大化したリソース食いの怪物で、すべてのネイティブアプリは自動的に効率的なソフトウェア工学の驚異だ」といったミームに変わります。
  • 1981/82年に書いた Fortran 78 コードを保管しておけば、ここで動くか試せたのにと残念です。
    Jovial プログラミング言語のソースコードフォーマッタで、Fortran でやるような仕事ではありませんでしたが、当時はそれしか選択肢がありませんでした。

    • Orange County の Hughes で働いていたのですか?
  • JavaScript で使える、ある程度 本番投入可能な線形代数エコシステムはありますか?
    検索すると、たいてい10年ほど前の見慣れたライブラリの JavaScript ポート、たとえば emscripten 経由のポートばかり出てくるのですが、何か見落としているのか気になります。

    • WebGPUWebNN の上に、BLAS に相当するものはありますか?
  • LFortranをもっと深く取り上げていないのは不思議
    オンラインで動く優れた、驚くべきWASMの例もある
    https://dev.lfortran.org/

    • 記事には、LFortranコンパイラはここ数年で大きく進歩したと書かれている
      2020年には多くの機能が欠けており、Fortranのごく小さなサブセットしかサポートしていなかったが、今でははるかに広い言語機能をサポートし、かなり多くのFortranコードをコンパイルできる
      WebAssemblyへもネイティブにコンパイル可能
      ただし、LFortranを使うにはまだ荒削りな部分があり、プロジェクトは現在アルファ段階と見なされていて、実際のコードのコンパイルでは問題が起きる可能性があると開発者たちは述べている
      MINPACKのような一部のプロジェクトは正常にコンパイルできるが、Fortranの全仕様はまだサポートしていないため、より大きなプロジェクトの多くは依然としてコンパイルできない
      LFortranの開発者たちはFortran 2018の完全サポートを目標としており、目立つ機能としてはJupyterのような対話型Fortran REPLがある
      さらに数年開発が進めば、WebAssembly向けにFortranコードをコンパイルするための優れた選択肢になると見ている
      また、https://dev.lfortran.orgのLFortranデモを見るよう書かれているが、非常に印象的な一方で、私が最初に試した x * 2x * 3 に変える作業は、現在のコード生成器ではサポートされていなかった
  • .NETとJava上のFortranもある
    https://www.silverfrost.com/14/ftn95/ftn95_fortran_95_for_microsoft_dotnet_features.aspx
    https://dl.acm.org/doi/10.1145/376656.376833

  • https://medium.com/@tomasreimers/compiling-tensorflow-for-the-browser-f3387b8e1e1c の作業をしていたとき、TensorFlowがFortranで書かれた人気の数学ライブラリである BLAS/Lapack ではなく Eigen を使っていて本当に幸いだと思った
    そうでなければ作業量ははるかに多くなっていただろう