はじめに
色々あって、M1チップ搭載のMacBook Airを手に入れました。
自分自身、WIndows上で数値計算にC# + Math.NETを用いているので、Macでも同様にMath.NETを使って書いたコードを動かせるようにしました。
しかしながら、なぜかMath.NETのManagedコード*1しか動きませんでした。
しばらく格闘した末に、Math.NETでOpenBLASを使えるようにできたので、その記録を残します。
Math.NETの導入
Windowsで導入する場合と特に変わりません。NuGetからインストールするだけです。今回はOpenBLASを用いたいので、MathNet .Numerics.Provider.OpenBLASをインストールします。
末尾に.Winと書かれたものを間違えてインストールしないでください。こいつはWindows版です。
OpenBLASが使えない!!
ここまでの手順でOpenBLASを使えると思ったのですが、なんとLinearAlgebraControl.TryUseNativeOpenBLASメソッドがfalseを返しやがります。
ここで原因を調べるために、MathNet.Numerics.Providers.OpenBLAS.OpenBlasProvider.Loadメソッドを呼び出してみました。すると、libMathNetNumericsOpenBLAS.dylibが見つからないという例外が発生しました。どうやら、Math.NETは直接OpenBLASを呼び出しているわけではなく、libMathNetNumericsOpenBLASという名前のラッパーからOpenBLASの関数を呼び出しているようです。そこで、Math.NET公式のGitHubリポジトリでコードを確認してみます。
"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の利用が想定されているようです。今までMacはIntelプロセッサを採用していましたからね。しかしながら、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向けの最適化)
実は、AppleはBLASやLAPACKを公式に提供しています。ですので、Math.NETの"src/NativeProviders/Common"に従って、AppleのBLASをラップすれば、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コードを確認すると、かなり単純な実装なので当たり前と言えば当たり前です。