- 歴史ある科学・工学計算資産である 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 から呼び出せ、PRINT、ALLOCATE、CHARACTER 引数にも対応できる
- 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.8 と llvm-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.f08 を flang-new でコンパイルすると次のエラーが発生する
not yet implemented: target not implemented
- 原因は、
flang-new に wasm32-unknown-emscripten ターゲットトリプルがまだ実装されていないため
- 解決策は
flang/lib/Optimizer/CodeGen/Target.cpp に TargetWasm32 のターゲット特性を追加するパッチ
- デフォルト幅を 32 に設定する
- 複素数引数と戻り値型を WebAssembly 側の LLVM 型へ変換するマーシャリングを定義する
llvm::Triple::ArchType::wasm32 分岐に TargetWasm32 を接続する
- パッチ後に再ビルドすると、Fortran ソースは WebAssembly オブジェクトへコンパイルされる
file foo.o の結果: WebAssembly (wasm) binary module version 0x1 (MVP)
llvm-nm foo.o で foo_ シンボルを確認できる
C と JavaScript から Fortran サブルーチンを呼び出す
- Fortran ルーチンは通常、引数を 参照渡し し、
INTENT() で引数の使い方を宣言できる
- 例の Fortran サブルーチン
foo は x、y、z の整数引数を受け取り、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.js と standalone.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 と等価になる
- この隠し引数のサイズ差が
i64 と i32 の不一致を引き起こす
暫定パッチ: 4 バイト値を強制
- 理想的な解決策は、
flang-new がクロスコンパイル時にホストに依存せず、ターゲットアーキテクチャとデータモデルに合った i32 または i64 を出力すること
- 現状では
wasm32 と Emscripten に合わせて long サイズを 4 バイトにハードコードするパッチを使っている
- パッチ内容は 2 系統ある
RTBuilder.h で long と unsigned long のモデル型を 8 * sizeof(...) ではなく 8 * 4 に強制する
CodeGen.cpp で malloc() 呼び出し引数を 64 ビットではなく 32 ビット整数で生成し、割り当てサイズを i32 にキャストする
- この変更により、Fortran 90 で導入された
ALLOCATE() ベースの動的確保も修正される
- 再ビルド後、
hello.f08 と hello.c をランタイムライブラリとリンクすると警告なしでビルドでき、Node では次の出力になる
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.example を make.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件のコメント
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/
ただし私は Nvidia の Fortran 製品の開発完了に必要な作業に集中しているため、こうした作業に割く時間は残っていません。
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)
説明の方法として 最も単純な非自明な例を使っているのが本当に良いです。
記事が「JavaScript から BLAS 関数を呼び出す」という具体的な問題に基づいているので、とても学びが多く、素晴らしい記事です。
[1] https://en.m.wikipedia.org/wiki/The_Theoretical_Minimum
感嘆すべきなのか、戦慄すべきなのか分かりません。おそらく両方でしょう。
f18 をビルドするときは llvm-project/main の最新ソースを使うことをおすすめします。
プロジェクトは速く動いているので、すでに修正済みの問題をデバッグしたり、すでに実装済みの機能を見逃したりすると時間の無駄になります。
WebAssembly ポートに取り組んで、中間コードが Fortran まで動作する地点に持っていこうとしているのでしょうか?
WebAssembly 開発のことはよく分かりません。
消費者の立場から見て、WebAssembly は今すぐ何かを提供しているのでしょうか? それとも、プログラムが本当に移植可能になる未来のための基盤作りに近いのでしょうか?
WebAssembly の仕組みによってネットワークやファイルなどへのアクセス制限がより簡単になる、という話は聞いたことがありますが、それが理論上の話なのか、すでに実装されているものなのかは分かりません。
重要な違いは、Wasm 自体には標準ライブラリもなく、入出力関数も公開していないことです。
そのためホスト、つまり VM を自分で作り、Wasm バイナリがインポートして使う関数を公開できます。そして Wasm バイナリは、その関数群を通じてのみ外界にアクセスできます。
もう一つの利点は、バイナリ形式が独占的なものではなく仕様があるため、誰でも Wasm VM を実装できることです。
ただし現時点では、まだ良い状態とは言いにくく、時期尚早です。W3C のようなグループで標準化中の新機能が多く、手続きも非常に遅いです。
たいていのターゲットに配布したり、クロスコンパイルしたりする方法もあります。
コンピュータの CPU が ARM なのか x86 なのかを普通は知らず、あまり気にしないのと似ています。
なので、ネイティブコードで動いているのか、JVM・.NET・WASM のような VM 上で動いているのかといった詳細に関心がないなら、他の解決策より何を多く提供するのかを説明するのは難しいです。
たいていは悪い事例だけが目につき、それが「すべての Electron プログラムは肥大化したリソース食いの怪物で、すべてのネイティブアプリは自動的に効率的なソフトウェア工学の驚異だ」といったミームに変わります。
1981/82年に書いた Fortran 78 コードを保管しておけば、ここで動くか試せたのにと残念です。
Jovial プログラミング言語のソースコードフォーマッタで、Fortran でやるような仕事ではありませんでしたが、当時はそれしか選択肢がありませんでした。
JavaScript で使える、ある程度 本番投入可能な線形代数エコシステムはありますか?
検索すると、たいてい10年ほど前の見慣れたライブラリの JavaScript ポート、たとえば emscripten 経由のポートばかり出てくるのですが、何か見落としているのか気になります。
LFortranをもっと深く取り上げていないのは不思議
オンラインで動く優れた、驚くべきWASMの例もある
https://dev.lfortran.org/
2020年には多くの機能が欠けており、Fortranのごく小さなサブセットしかサポートしていなかったが、今でははるかに広い言語機能をサポートし、かなり多くのFortranコードをコンパイルできる
WebAssemblyへもネイティブにコンパイル可能
ただし、LFortranを使うにはまだ荒削りな部分があり、プロジェクトは現在アルファ段階と見なされていて、実際のコードのコンパイルでは問題が起きる可能性があると開発者たちは述べている
MINPACKのような一部のプロジェクトは正常にコンパイルできるが、Fortranの全仕様はまだサポートしていないため、より大きなプロジェクトの多くは依然としてコンパイルできない
LFortranの開発者たちはFortran 2018の完全サポートを目標としており、目立つ機能としてはJupyterのような対話型Fortran REPLがある
さらに数年開発が進めば、WebAssembly向けにFortranコードをコンパイルするための優れた選択肢になると見ている
また、https://dev.lfortran.orgのLFortranデモを見るよう書かれているが、非常に印象的な一方で、私が最初に試した
x * 2をx * 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 を使っていて本当に幸いだと思った
そうでなければ作業量ははるかに多くなっていただろう