Apple Silicon上でMath.NETを使う(OpenBLASの利用)

はじめに

色々あって、M1チップ搭載のMacBook Airを手に入れました。
自分自身、WIndows上で数値計算C# + Math.NETを用いているので、Macでも同様にMath.NETを使って書いたコードを動かせるようにしました。
しかしながら、なぜかMath.NETのManagedコード*1しか動きませんでした。
しばらく格闘した末に、Math.NETでOpenBLASを使えるようにできたので、その記録を残します。

環境

チップ: Apple M1
メモリ: 8 GB

つまりは2020年のMacBook Airです。

Math.NETの導入

Windowsで導入する場合と特に変わりません。NuGetからインストールするだけです。今回はOpenBLASを用いたいので、MathNet .Numerics.Provider.OpenBLASをインストールします。
末尾に.Winと書かれたものを間違えてインストールしないでください。こいつはWindows版です。

OpenBLASのインストール

以下の記事に書かれている通りにインストールします。

qiita.com

OpenBLASが使えない!!

ここまでの手順でOpenBLASを使えると思ったのですが、なんとLinearAlgebraControl.TryUseNativeOpenBLASメソッドがfalseを返しやがります。
ここで原因を調べるために、MathNet.Numerics.Providers.OpenBLAS.OpenBlasProvider.Loadメソッドを呼び出してみました。すると、libMathNetNumericsOpenBLAS.dylibが見つからないという例外が発生しました。どうやら、Math.NETは直接OpenBLASを呼び出しているわけではなく、libMathNetNumericsOpenBLASという名前のラッパーからOpenBLASの関数を呼び出しているようです。そこで、Math.NET公式のGitHubリポジトリでコードを確認してみます。

"src/NativeProviders/"にラッパーらしきものを見つけました。

src/NativeProviders/

Commonディレクトリ内にはラッパーのインターフェースがあり、その他のディレクトリには、名前の通りMKLやOpenBLAS、CUDAのラッパーのコードがあります。
Macに関係ありそうなOSXディレクトリには以下のshファイルがあるのみでした。

export INTEL=/opt/intel
export MKL=$INTEL/mkl
export OPENMP=$INTEL/lib
export OUT=../../../out/MKL/OSX

mkdir -p $OUT/x64
mkdir -p $OUT/x86

clang++ -std=c++11 -D_M_X64 -DGCC -m64 --shared -fPIC -o $OUT/x64/libMathNetNumericsMKL.dylib -I$MKL/include -I../Common -I../MKL ../MKL/memory.c ../MKL/capabilities.cpp ../MKL/vector_functions.c ../Common/blas.c ../Common/lapack.cpp ../MKL/fft.cpp  $MKL/lib/libmkl_intel_lp64.a $MKL/lib/libmkl_core.a $MKL/lib/libmkl_intel_thread.a -L$OPENMP -liomp5 -lpthread -lm

cp $OPENMP/libiomp5.dylib  $OUT/x64/

clang++ -std=c++11 -D_M_IX86 -DGCC -m32 --shared -fPIC -o $OUT/x86/libMathNetNumericsMKL.dylib -I$MKL/include -I../Common -I../MKL ../MKL/memory.c ../MKL/capabilities.cpp ../MKL/vector_functions.c ../Common/blas.c ../Common/lapack.cpp ../MKL/fft.cpp  $MKL/lib/libmkl_intel_lp64.a $MKL/lib/libmkl_core.a $MKL/lib/libmkl_intel_thread.a -L$OPENMP -liomp5 -lpthread -lm

cp $OPENMP/libiomp5.dylib  $OUT/x86/

どうやらMacでは、Intel MKLの利用が想定されているようです。今までMacIntelプロセッサを採用していましたからね。しかしながら、Apple SiliconはARMベースです。Intel MKLは動きません*2。そこで、OpenBLASのラッパーをMac上でビルドします。

OpenBLASラッパーのビルド

まずは、"src/NativeProviders/"内のCommonディレクトリとOpenBLASディレクトリ内の全てのファイルを同じディレクトリにまとめます。そして、そのディレクトリをカレントディレクトリとしてターミナルを開き、以下のコマンドでコンパイルします。

clang++ -std=c++11 --shared -fPIC -o libMathNetNumericsOpenBLAS.dylib -I/opt/homebrew/opt/openblas/include capabilities.cpp blas.c lapack.cpp -L/opt/homebrew/opt/openblas/lib/ -lopenblas

あとは吐き出されたlibMathNetNumericsOpenBLAS.dylibを実行ファイルと同じ階層におけば、無事Math.NETからOpenBLASが使えます。

余談(Apple Silicon向けの最適化)

実は、AppleBLASLAPACK公式に提供しています。ですので、Math.NETの"src/NativeProviders/Common"に従って、AppleBLASをラップすれば、Apple Silicon向けに最適化できるはずです。時間があればいつかやりたいと思っています。

ベンチマーク

とりあえず4096x4096の巨大な行列積の計算速度をManagedとOpenBLASで比較してみましょう。
以下のコードがベンチマークに用いたコードです。USE_MANAGED定数をtrueにするとManagedモードで、falseにするとOpenBLASモードでMath.NETが動作します。

using System;
using System.Diagnostics;

using MathNet.Numerics.Providers.LinearAlgebra;
using MathNet.Numerics.LinearAlgebra.Single;
using MathNet.Numerics.Distributions;


const bool USE_MANAGED = true;

const int NUM_ROWS = 4096;
const int NUM_COLS = 4096;

var rand = new ContinuousUniform();
var lhs = DenseMatrix.CreateRandom(NUM_ROWS, NUM_COLS, rand);
var rhs = DenseMatrix.CreateRandom(NUM_ROWS, NUM_COLS, rand);
var product = DenseMatrix.Create(NUM_ROWS, NUM_COLS, 0.0f);

if(USE_MANAGED)
    LinearAlgebraControl.UseManaged();
else if(LinearAlgebraControl.TryUseNativeOpenBLAS())
    Console.WriteLine("Use OpenBLAS");

var sw = new Stopwatch();

sw.Start();
lhs.Multiply(rhs, product);
sw.Stop();

Console.WriteLine($"{sw.ElapsedMilliseconds}[ms]");

結果は以下の表の通りです。

実行時間
Managed 10901[ms]
OpenBLAS 524[ms]

20倍くらい違いますね。
まあ、Managedコードを確認すると、かなり単純な実装なので当たり前と言えば当たり前です。

終わりに

やはりWindowsからMacに移行すると色々と苦労します(逆も然り)。
しかも今のMacはプロセッサまで異なりますからね。でも、.NETランタイムの導入はとても簡単だったのが良かったです。
今ではC#やF#もJava並みに何処でも動きますね。

*1:C#で実装された数値計算ルーチンのこと。.NETランタイムが動く環境であれば必ず動作するが、使い物にならないくらい遅い。ソースコードを覗くと、特に工夫がなされているわけではなかったので、おそらく互換性のためにあるだけだと思われる。

*2:Rosettaを使えば、それなりの速度で動きますけど今回は扱いません。