Fortran
WebAssembly

Fortran で WebAssembly ~または地獄めぐり~

WebAssembly

WebAssembly とはブラウザを仮想機械として、機械語・アセンブリコードを高速に実行しようという共通規格のようです。昔の p-code 機械がブラウザに乗った感じでしょうか?

たまたま WebAssembly のページにたどり着いて、 llvm 系のコンパイラならすぐにもやれる感じだったので、flang (llvm 系の Fortran コンパイラ)でも一丁やってみるかと軽い気持ちでやり始めたら、地獄めぐりになってしまいました。

一応は動いたので、地獄めぐり旅行記をメモっておきます。地獄はいくつかに分けていますが、実際は flang 生成以外の地獄には、同時にはまって彷徨しています。なお、本内容は bash on Ubuntu on windows 上で実行しました。

なお今のところ数学関数は使えず、動的メモリー確保(割り付け変数)もできてません。また intrinsic module も使えません。READ/WRITE 系の I/O もできません。

webassembly のアドベント・カレンダー

https://qiita.com/advent-calendar/2017/webassembly
救い主は来るのか?

第一地獄 flang 

flang とは llvm をバックエンドにもつ Fortran コンパイラです。
https://github.com/flang-compiler/flang

flang のコンパイル

flang のコンパイルは、下記の flang のページの手引き従って実行すれば、一応出来ます。しかし指示にあるように、flang のコンパイルには flang が要ります。 鬼の所業です。
[追記:flang のページの手引きに従う時に、1~4まで順番に実行することと、sudo make install のあと bash 立ち上げ直しをすると、flang として2番で生成された clang が使われるようになり、コンパイルできるようです。鬼の目にも涙w]

試しに gcc/g++/gfortran (Ver. 7.2.0) の組み合わせで試みましたが、うまくいきませんでした。また clang/clang++/gfortran の組み合わせもダメでした。(なお make -j 8 で 8 スレッド実行で早く終わります。)

  • gcc-7/g++-7/gfortran-7 の -7 を消す方法。
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 10

参考ページ:https://www.scivision.co/selecting-compiler-versions-with-update-alternatives/

結局、 flang のページにある代替手段 Spack による方法で、一度 flang を作り、それを使って今一度 flang をコンパイルして、以降これを用いることとして、Spack を消去しました。

Spack とは、LLNL がスパコン用に作ったバージョン管理システムで、root権限なしで?あらゆる依存性の組み合わせ環境が作られる反面、すべてを1から構築し始めるので昼飯前に始めて、晩飯の後ぐらいに終わるレベルの時間がかかります。パスはハッシュ生成された謎数字のついたものとなります。

https://github.com/flang-compiler/flang

flang を WebAssembly 対応へ

なお、指示通りに作ると WebAssembly には対応しないので、指示の 1 番 llvm の build のところで cmake にオプションを足してやる必要があります。これにより、flang の出力ターゲットに WebAssembly が加わります。

cmake -LLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly ..  

参考ページ:https://qiita.com/Hiroki_M/items/89975a9e8205ced3603f

なお参考ページでは flang の指示に無い compiler-rt 等を入れていますが、無くても大丈夫でした。

第二地獄 WebAssembly

上記の作業により flang は、WebAssembly に変換できる llvm コードを出せるようになります。次にこの出力を WebAssembly に変換するプログラムを用意します。それが binaryen です。

binaryen のインストール

これは問題なくゆきます。

$ git clone --depth=1 https://github.com/WebAssembly/binaryen.git
$ cd binaryen
$ cmake . 
$ make -j 8
$ sudo make -j 8 install

flang から wasm へ

ソースプログラムを test.f90 として以下のような4段階をへて変換します。

$ flang -emit-llvm --target=wasm32 -S -Oz test.f90
$ llc test.ll -march=wasm32
$ s2wasm test.s > test.wast
$ wasm-as test.wast -o sample.wasm 

ここで、重要なのは flang によるコンパイル時に最適化オプション -Oz を加えておくことです。これがないと、関数の頭部に余計なコードが生成されその部分が実行時エラーを引き起こします。これが分かるまで、毎回生成されたアセンブリコードから手動でエラーの出る部分を修正していました。
 エラーの出るところは、実際は何も有効なことをしていないので、最適化をかけると削除されてエラーも出なくなります。鬼の所業です。

参考ページ:
https://qiita.com/Hiroki_M/items/89975a9e8205ced3603f
https://qiita.com/umamichi/items/c62d18b7ed81fdba63c2

第三地獄 Web

WebAssembly を実際に動かすには HTML からの javascript による呼び出しの記述が必要となります。しかし HTML といえば 1.0/2.0、CERN の文書システムという知識レベルではいささか心もとなく、丸写しにたよります。

Web サーバー

Web サーバーを立てて、サーバー側に HTML ファイルと wasm ファイルの二つを置いてやる必要があるようです。以下のページに依って、Windows 上で KantanWEBserver を用いて解決しました。

参考ページ:https://qiita.com/massie_g/items/2913066e596dae197539

ブラウザでの実行時にはファンクションキー F12 を押して、コンソール窓を開いておくと甚だ便利です。

世知辛いことに、HTML ファイルと wasm ファイルの二つをローカルにおいて、ファイルとして実行することはできないようです。Chrome/IE/Edge などで試しましたがダメでした。また、Chrome の --allow-file-access-from-files オプションも WebAssembly は適用外でした。鬼の所業です。

HTML ファイル

fetch('./pai.wasm') のところに生成した wasm ファイル名を指定します。また、const result = ex.pai(65) の ex. 以下 pai(65) のところにソースコードで定義した関数名および引数を書きます。

fetch のところでは、WebAssembly の機械語ファイルを読み込み、const result = ex.pai(65) のことろで関数呼び出しをして、実行結果を result 変数に返しています。

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <body>
    <script>
      fetch('./pai.wasm').then(response => response.arrayBuffer()
      ).then(bytes => {
        return WebAssembly.instantiate(bytes, {})
      }).then( r => {
        const ex = r.instance.exports
        console.log(ex)
        const result = ex.pai(65)
        console.log(result)
        document.querySelector("#root").innerHTML = result
      })
    </script>
    <div id="root"></div>
  </body>
</html>

恥ずかしながら、肝心の丸パクリ引用元が分からなくなるという大失態を犯してしまいました。中々うまくゆかなかったので色々な HTML を次々と丸写ししたためです。この先、見つけることができたら参考サイトとして引用したいと思います。

第四地獄 Fortran

最近の Fortran は C 言語とのインターフェース整備も進んでいるため、C 言語で出来ていれば、Fortran でも苦労なくできることが多いです。

今回も WEB サーバーを立てて、関数を呼び出すところまでは C 言語での例を参考に進むことができました。エラーは出るものの関数呼び出しまで実行されていることは、ブラウザのデバッガーで追うことが出来ました。また中途生成されるアセンブリを手で書き換えれば、エラーを出さずに実行できることも分かりました。第二地獄 WebAssembly で書いたように、これは最適化をかけることで抑止できることも分かりました。

以下では実例により Fortan/flang 固有の問題を挙げます。

基本事項

何らかの返り値が要求されているようなので、function 文による記述をします。呼び出し HTML 側から見える function 名は bind(c, name = '何某') によって明示的に指定することにします。Fortran はオブジェクト名を処理系ごとに適当に修飾するので(よくあるのはアンダースコアの付加、大文字化)、bind 指定によって固定します。特にモジュール内のサブルーチン/関数のオブジェクト名は、モジュール名で修飾することが多いので、bind しておくと安心です。

引数無し 野良ルーチン

最も簡単な例で、アセンブリコードを眺めるのに丁度よいです。

function test() result(ires) bind(c, name = 'test')
  integer :: ires
  ires = 2
end function test

引数あり Module 内変数・関数

Fortran での引数は参照呼出しがデフォルトですので、ここでは引数の値呼び出しの指定をしてやる必要があります。アセンブリを見ると、引数にどんな型を与えても整数型に変換されているので、ドはまりしました。参照呼出し以外を使うのは、鬼の所業です。

module m_mod
  integer, parameter :: kd = kind(1.0d0)
  real(kd), parameter :: pi = 4 * atan(1.0_kd)
contains
  real(kd) function test(x) result(res) bind(c, name = 'test')
    real(kd), value :: x
    res = pi * x
  end function test
end module m_mod

制限事項

以上の基本事項のほかに、様々な制限事項があるようです。それが本質的なものなのか、私のオプション指定の誤りなどによるものなのかよく分かっていません。コンパイル時はエラーは出ませんが、実行時に外部モジュールが見つからないといって叱られます。

  • 各種数学関数 sin,cos,gamma 等利用不可
  • 割り付け変数の allocate 不可
  • 引数に依存するサイズの自動変数の配列 不可
  • use, intrinsic :: ISO_C_BINDING 不可
  • READ/WRITE 系 I/O 不可

また flang は Nvidia の Fortran コンパイラ (PGI Fortran) のフロントエンドを用いているため、Fortran 2008 規格への対応状況は遅れています。

実際の実行例

ここでは数学関数を用いないで計算できる例として、円周率の近似値 355/113 と円周率 π の差を、積分による厳密な式
$$ \int_0^1{x^8(1-x)^8(25+816x^2)\over3164(1+x^2)}dx = {355\over113}-\pi$$
に従って計算してみます。

ソースプログラム

ここでプログラムでは、左辺を台形積分で求める関数 pai(n) [但し引数 n は積分区間の分割の数]、および右辺を求める関数 pai2() を定義しています。なお配列を固定長で確保する必要があったので、積分区間の分割は 129 までしか取れません。

参考サイト:http://fortran66.hatenablog.com/entry/2017/02/23/023021

      module m_pai
        implicit none
        integer, parameter :: kd = kind(1.0d0)
        real(kd), parameter :: pi = 4 * atan(1.0_kd)
      contains
        real(kd) function pai(n) bind(c, name = 'pai')
          integer, value :: n
          real(kd) :: x(129), y(129), h
          integer :: i
          h = 1.0_kd / (n - 1)
          forall (i = 1:n) x(i) = (i - 1) * h
          y = f(x(1:n))
          pai = h * ( sum(y(1:n)) - 0.5_kd *(y(1) + y(n)) )
        end function pai

        real(kd) pure elemental function f(x)
          real(kd), intent(in) :: x
          f = x**8 * (1.0_kd - x)**8 * (25.0_kd + 816.0_kd * x**2) / (3164.0_kd * (1.0_kd + x**2))
        end function f


        real(kd) function pai2() bind(c, name = 'pai2')
          pai2 = 355.0_kd / 113.0_kd - pi
        end function pai2
      end module m_pai

コンパイル

flang は warning を出しますが無視しています。

$ flang -emit-llvm --target=wasm32 -S -Oz  pai.f90
warning: overriding the module target triple with wasm32 [-Woverride-module]
1 warning generated.
$ llc pai.ll -march=wasm32
$ s2wasm pai.s > pai.wast
$ wasm-as pai.wast  -o pai.wasm

出来あがった pai.wasm ファイルと上に示した HTML ファイルを WEB サーバーソフトで指定したディレクトリにコピーします。

実行結果

実行結果を見ると理論値通り小数点以下 6 桁まであっているようで、単精度くらいの精度があります。まぁ 355 と 113 と 6 個の数字を与えているのだから 6 桁まであっていて当然と言えば当然ですが、奇数 1,3,5 の順の繰り返しと思えば霊験あらかたかも。

  • ブラウザ画面
    台形積分による結果 64 分割 (引数 n=65)
    2.667641890624305e-7
    pai1.png

  • F12で開くコンソール窓
    pai2.png

  • 355/113 - π
    2.667641894049666e-7
    pai3.png