ocaml-torchをApple Siliconのマシンにインストールして動かすまで

最終更新:

OCaml 深層学習 メモ

ocaml-torchをApple Siliconのマシンにインストールし,ひとまずCPU上で動かすことはできたので,手順をメモ書き的に残します.わりと乱雑なので,適宜修正するかもしれません.

はじめに

深層学習関連のライブラリやツールは特定の計算機アーキテクチャやABIに依存した実装になっていることがしばしばあり,M1 Mac・M2 MacなどApple Siliconのマシンにインストールして動かすのは概して未だに厄介なようです.例えば,PyTorch 2.x自体をインストールして動かすのは比較的易しいようですが,.to(device) などを各所で指定する必要のある,低級な部分をあまり覆い隠さないAPIになっていることもあってか,PyTorchに依存しているツールがx86_64前提であったりCUDAがサポートされている環境で動くことを前提した実装になっていてMPSでは使えない,といったこともよくあります.

PyTorchのOCamlバインディングであるocaml-torch(OPAM上のパッケージ名としては torch)もこの点例外ではないようです.torchlibtorch というPyTorch C++ APIをラップしたOPAMパッケージに依存しており,この libtorch のリリースが1.7.0以降はx86_64を前提していてApple Siliconはサポートされていません:

$ opam info libtorch

<><> libtorch: information on all versions ><><><><><><><><><><><><><><><><>  🐫
name                   libtorch
(中略)
all-versions           1.0.0  1.0.1  1.1.0  1.2.0  1.3.0  1.3.1  1.4.0  1.5.0  1.6.0  1.7.0+linux-x86_64
                       1.7.0+macos-x86_64  1.8.0+linux-x86_64  1.8.0+macos-x86_64  1.9.0+linux-x86_64
                       1.9.0+macos-x86_64  1.10.0+linux-x86_64  1.10.0+macos-x86_64  1.12.0+linux-x86_64
                       1.12.0+macos-x86_64  1.13.0+linux-x86_64  1.13.0+macos-x86_64  2.0.0+linux-x86_64
                       2.0.0+macos-x86_64  2.1.2+linux-x86_64  2.2.1+linux-x86_64

torchlibtorch もGitHubリポジトリ github.com/janestreet/torch で開発されており1,これをcloneして opam pin によりインストールを試みることもできますが,実際Apple Siliconのマシンでインストールしようとすると libtorch の方のビルドで失敗するようで,Issueが立てられています:

で,これにはしっかりとした回答が寄せられています:

It looks like your problem is that the native libtorch binaries that you downloaded are for Intel rather than Apple Silicon. You can use these binaries by running an Intel build of OCaml through Rosetta. However, if you want to run natively, read on:

I added more instructions on this in LaurentMazare/ocaml-torch#76 (comment) but will copy here:

  • Download libtorch binaries (or build libtorch in your Mac). At the moment there are no official pre-build binaries. I downloaded my (unofficial) binaries from https://github.com/mlverse/libtorch-mac-m1/releases .
  • Install OCaml >= 4.14 (see here: https://opam.ocaml.org/packages/torch/)
  • Double check what libtorch version is compatible with the current version of OCaml torch. Version 1.13.1 is the one you want with v0.16.0 version of OCaml torch.
  • Set the LIBTORCH environment variable to the directory that includes the include and lib directories.

To install with opam:

opam install torch.v0.16.0 --ignore-constraints-on libtorch

Note that I had to ignore-constraints to avoid failing because libtorch (an OCaml/OPAM library that contains pre-build libtorch binaries for some architectures that do not include M1/M2) is disabled for M1 Mac. Note also that I explicitly had to set the version of the package (JaneStreet version starts with v0 rather than 0) as I otherwise resolve to version 0.10 which is way too old.

要するに,単にバインドされるLibTorch(ややこしいですが,OPAMパッケージとしての libtorch ではなくPyTorch C++ APIの実装のことです.上記回答中ではこれを指して「libtorch binaries」とか単に「libtorch」と表記されています)がx86_64をターゲットにしてビルドされていることが要因なようで,自前でApple Silicon向けにビルドしたLibTorchを用意して環境変数 LIBTORCH にそのパスを設定してからOPAMパッケージをビルド・インストールするとよい,ということのようです.加えて,ありがたいことにLibTorchのそういったビルド結果をリリースしてくれている方がいるようです:

実際このリポジトリのReleasesにビルド済みのバイナリ群が公開されています:

というわけでひとまずこれを信頼して使わせてもらいましょう.どのバージョンにすべきかですが,以下のような考慮の結果上記回答と同じく1.13.1を使うことにしました:

  • torch の最新版は v0.17.0 だが,これはOCaml 5.1.0以上を要請する.筆者の環境ではまだOCaml 4.14台を使い続けているので,手っ取り早く動かすために一旦これはやめておく(どうしても無理ならOCaml 5.1.0以上にアップグレードすることにする).
  • torch の1つ前のリリース v0.16.0 はOCaml 4.14で動くので,ひとまずこれを使うことを考える.これの依存パッケージ libtorch に関するバージョン制約は "libtorch" {>= "1.13.0" & < "1.14.0"} という記載になっている.
  • パッケージ libtorch のバージョン番号は,バインドされているLibTorchと同じものを使っているらしい.
  • というわけで,上記リリースのうち libtorch-v1.13.1.zip をダウンロードして使うとよさそう.

1 まずはインストールが一旦成功するまで

1-1 LibTorchの準備

まずは Releases · mlverse/libtorch-mac-m1 から libtorch-v1.13.1.zip をダウンロードし,解凍して配置します.置く場所はどこでもよいですが,筆者はとりあえず $HOME/cloned/libtorch に置きました.すなわち以下のような具合です:

$HOME/cloned/
└── libtorch/
    ├── bin/
    ├── include/
    ├── lib/
    └── share/

1-2 環境変数 LIBTORCH の設定

1で配置したディレクトリを環境変数 LIBTORCH に設定します(適切に読み替えてください):

$ export LIBTORCH="$HOME/cloned/libtorch"

1-3 janestreet/torchv0.16.0 をclone

これはやるだけ(1と同様に cloned/ を使っていますが自分の好みのディレクトリに読み替えてください):

$ cd $HOME/cloned
$ git clone https://github.com/janestreet/torch
$ cd torch
$ git checkout v0.16.0

1-4 libtorchtorch をビルド

さて,ここが少しハマりどころです.基本的には以下を実行すればインストールできるのですが,私の環境ではこれだけではダメでした:

$ opam pin add .
# 上記コマンドを叩くだけで libtorch と torch をインストールするか訊かれるかもしれないが,`n`(=NO)をタイプしてインストールしない
$ opam install torch.v0.16.0 --ignore-constraints-on libtorch

具体的には,libtorch のインストールには成功しますが,torch のビルドでC言語の水準のエラーが出て失敗しました:

$ opam install torch.v0.16.0 --ignore-constraints-on libtorch
The following actions will be performed:
=== install 2 packages
  ∗ libtorch 1.6.0   [required by torch]
  ∗ torch    v0.16.0

Proceed with ∗ 2 installations? [y/n] y

<><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><>  🐫
⬇ retrieved libtorch.1.6.0  (cached)
⬇ retrieved torch.v0.16.0  (cached)
∗ installed libtorch.1.6.0
[ERROR] The compilation of torch.v0.16.0 failed at "dune build -p torch -j 7".

#=== ERROR while compiling torch.v0.16.0 ======================================#
# context     2.2.1 | macos/arm64 | ocaml-base-compiler.4.14.1 | https://opam.ocaml.org#56e31a3bc1fd0bfd87e5251972e806b8f78082a5
# path        ~/.opam/4.14.1/.opam-switch/build/torch.v0.16.0
# command     ~/.opam/opam-init/hooks/sandbox.sh build dune build -p torch -j 7
# exit-code   1
# env-file    ~/.opam/log/torch-25254-41b03d.env
# output-file ~/.opam/log/torch-25254-41b03d.out
### output ###
# torch_stubs.c:38971:36: warning: passing 'const char *' to parameter of type 'char *' discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers]
# [...]
#                                    ^~~~~~
# ./torch_api_generated.h:2300:180: note: passing argument to parameter 'pad_mode' here
# void atg_stft_center(tensor *, tensor self, int64_t n_fft, int64_t hop_length_v, int hop_length_null, int64_t win_length_v, int win_length_null, tensor window, int center, char * pad_mode, int normalized, int onesided, int return_complex);
#                                                                                                                                                                                    ^
# 121 warnings and 1 error generated.
(中略)

<><> Error report <><><><><><><><><><><><><><><><><><><><><><><><><><><><><>  🐫
┌─ The following actions failed
│ λ build torch v0.16.0
└─
┌─ The following changes have been performed
│ ∗ install libtorch 1.6.0
└─

ちなみにここでOPAMパッケージとしては libtorch1.6.0 が入ってしまいますが,バインドされるLibTorchは1.13.1になるので問題ないようです.

かなり多くのwarningが出ているのはさておき,ログファイル(上記の場合は ~/.opam/log/torch-22992-7fcee8.out)をgrepしてみると,以下のようなerrorが /usr/bin/cc によって出てビルドに失敗していました:

torch_stubs.c:295:27: error: incompatible function pointer types passing 'void (*)(const char *, void *)' to parameter of type 'void (*)(char *, tensor)' (aka 'void (*)(char *, void *)') [-Wincompatible-function-pointer-types]
   at_load_callback(x280, x281);
                          ^~~~
./torch_api.h:85:46: note: passing argument to parameter 'f' here
void at_load_callback(char *filename, void (*f)(char *, tensor));

筆者の環境の /usr/bin/cc のバージョン等は以下です:

$ /usr/bin/cc --version
Apple clang version 15.0.0 (clang-1500.3.9.4)
Target: arm64-apple-darwin23.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

エラー報告にも出ているとおり,-Wincompatible-function-pointer-types という設定に基づいたエラーであるはずなので,これを無効にするフラグを与えられれば通りそうです.というわけで,src/wrapper/dune にそのような指定を追加します:

 (library (name torch_core) (public_name torch.core) (c_names torch_stubs)
+ (c_flags :standard -Wno-incompatible-function-pointer-types)
  (c_library_flags :standard -lstdc++ (:include c_library_flags.sexp))
  (cxx_names torch_api) (cxx_flags -std=c++14 -fPIC (:include cxx_flags.sexp))
  (libraries bigarray ctypes.foreign ctypes.stubs ctypes)
  (preprocess (pps ppx_jane)))

 (rule (targets cxx_flags.sexp c_library_flags.sexp)
  (deps ../config/discover.exe) (action (bash %{deps})))

 (rule (targets torch_bindings.ml) (deps ../stubs/torch_bindings.ml)
  (action (bash "cp ../stubs/torch_bindings.ml torch_bindings.ml")))

 (rule (targets torch_bindings_generated.ml)
  (deps ../stubs/torch_bindings_generated.ml)
  (action
   (bash
    "cp ../stubs/torch_bindings_generated.ml torch_bindings_generated.ml")))

 (rule (targets torch_stubs.c torch_generated.ml)
  (deps ../stubs/torch_gen.exe) (action (bash ./%{deps})))

branchを切ってこのような変更をcommitします(commitしないと,デフォルトではOPAMが無視してしまうためです.--working-dir つきで opam pin すればcommitしていない変更も加味されるのですが,いずれにしても作業履歴を残しておくのに便利なのでここではcommitします):

$ git checkout -b temp-patch
$ git add -A
$ git commit -m "patch \`dune\` as to \`c_flags\`"

この後にあらためて以下を叩くと torch がインストールできました:

$ opam pin add .
# libtorchに関する依存制約が充足されていないので [ERROR] と出たりするが,無視してOK
$ opam install torch.v0.16.0 --ignore-constraints-on libtorch

というわけでここまででインストールはできましたが,実はこのままだと動かないので次章で修正を施していきます.

2 動かないところを直す

2-1 LibTorchのファイルを実行可能にする

まずは試しに utop2torch が読み込めるか試してみましょう:

$ utop
(中略)
utop # #require "torch";;
Cannot load required shared library dlltorch_core_stubs.
Reason: $HOME/.opam/4.14.1/lib/stublibs/dlltorch_core_stubs.so: dlopen($HOME/.opam/4.14.1/lib/stublibs/dlltorch_core_stubs.so, 0x000A): Library not loaded: @rpath/libc10.dylib
  Referenced from: <93108957-433A-3A67-8E42-77C8F6184B1D> $HOME/.opam/4.14.1/lib/stublibs/dlltorch_core_stubs.so
  Reason: tried: '$HOME/cloned/libtorch/lib/libc10.dylib' (code signature in <(中略)> '$HOME/cloned/libtorch/lib/libc10.dylib' not valid for use in process: library load disallowed by system policy), '(中略)' (no such file), '$HOME/cloned/libtorch/lib/libc10.dylib' (code signature in <(中略)> '$HOME/cloned/libtorch/lib/libc10.dylib' not valid for use in process: library load disallowed by system policy), '(中略)' (no such file).
Error: Reference to undefined global `Torch_core__Wrapper'

筆者の環境だとOSによって $LIBTORCH/lib/libc10.dylib の読み込みがブロックされました.permission上は実行可能でしたが,ネットワーク経由で落としてきたファイルなので,OSがpermissionとは別の仕組みによって実行できないものと扱っているようです.LibTorchを自己責任で信頼することにし,実行可能にしましょう.これは xattr というコマンドで設定できるようです.まず,libc10.dylib の実行がたしかにブロックされていることを確認しましょう:

$ cd $LIBTORCH
$ xattr lib/libc10.dylib
com.apple.quarantine

実行がブロックされるファイルには com.apple.quarantine が付与されています.このファイルを信用してよいと判断したら,-d オプションによって com.apple.quarantine を削除します:

$ xattr -d com.apple.quarantine lib/libc10.dylib

これで実行できるようになりました.同様の処理をあと2つのファイルに対しても行ないます:

$ xattr -d com.apple.quarantine lib/libtorch_cpu.dylib
$ xattr -d com.apple.quarantine lib/libtorch.dylib

これでひとまず読み込み自体はできるようになります:

$ utop
(中略)
utop # #require "torch";;

2-2 実装例 examples/mnist/*.ml を動かす準備

MNISTの学習の例 examples/mnist/{linear,nn,conv}.ml がリポジトリにあるので,動かしてみましょう.examples/mnist/README.md によると,リポジトリに data/ ディレクトリをつくってMNISTの訓練用データとテスト用データを配置する必要があるとのことなので,まずはそのように配置します.MNISTのデータセットは以下で配布されているようです:

……なのですが,少なくとも筆者が見たタイミングだとHTTPSの証明書が切れており,かつデータセットへのリンクにアクセスするとForbiddenでダウンロードできませんでした.Kaggleで配布されていたのでこちらを使わせてもらうことにします:

ここで配布されているものはファイル名が大元の配布と若干違っているので,直して data/ に以下のように配置します(それぞれ途中のピリオドをハイフンに変える必要がある.{t10k,train}-images-idx3-ubyte はそれぞれzipファイルを解凍したもの):

torch/
└── data/
      ├── t10k-images-idx3-ubyte
      ├── t10k-labels-idx1-ubyte
      ├── train-images-idx3-ubyte
      └── train-labels-idx1-ubyte

また,examples/mnist/dunev0.16.0 だと空ファイルになっているので,ビルドできるようにするために以下を書き込みます(v0.17.0 から取ってきたもの):

(executables
 (modes byte exe)
 (names conv linear nn)
 (libraries stdio torch unix)
 (preprocess
  (pps ppx_jane)))

これで準備完了で,実際まず単純な線型回帰である linear.ml をビルドしてみると成功はします:

$ dune build examples/mnist/linear.exe

しかし,できあがったバイナリを実行してみると以下のように落ちます3

$ ./_build/default/examples/mnist/linear.exe
libc++abi: terminating due to uncaught exception of type c10::Error: The size of tensor a (10) must match the size of tensor b (10000) at non-singleton dimension 0
Exception raised from infer_size_impl at (中略)/libtorch-mac-m1/libtorch-mac-m1/pytorch/aten/src/ATen/ExpandUtils.cpp:35 (most recent call first):
frame #0: c10::detail::torchCheckFail(char const*, char const*, unsigned int, std::__1::basic_string, std::__1::allocator> const&) + 92 (0x100c1952c in libc10.dylib)
frame #1: at::infer_size_dimvector(c10::ArrayRef, c10::ArrayRef) + 376 (0x10ac6e35c in libtorch_cpu.dylib)
frame #2: at::TensorIteratorBase::compute_shape(at::TensorIteratorConfig const&) + 456 (0x10acbba90 in libtorch_cpu.dylib)
frame #3: at::TensorIteratorBase::build(at::TensorIteratorConfig&) + 520 (0x10acb6f8c in libtorch_cpu.dylib)
frame #4: at::TensorIteratorBase::build_borrowing_comparison_op(at::TensorBase const&, at::TensorBase const&, at::TensorBase const&) + 232 (0x10acb7960 in libtorch_cpu.dylib)
frame #5: at::(anonymous namespace)::wrapper_eq_Tensor(at::Tensor const&, at::Tensor const&) + 104 (0x10bf34c80 in libtorch_cpu.dylib)
frame #6: c10::impl::wrap_kernel_functor_unboxed_, at::Tensor, c10::guts::typelist::typelist>, at::Tensor (c10::DispatchKeySet, at::Tensor const&, at::Tensor const&)>::call(c10::OperatorKernel*, c10::DispatchKeySet, at::Tensor const&, at::Tensor const&) + 116 (0x10d456a08 in libtorch_cpu.dylib)
frame #7: at::_ops::eq_Tensor::call(at::Tensor const&, at::Tensor const&) + 284 (0x10b887ecc in libtorch_cpu.dylib)
frame #8: at::eq(at::Tensor const&, at::Tensor const&) + 40 (0x101457cb0 in dlltorch_core_stubs.so)
frame #9: atg_eq_tensor + 40 (0x101457b88 in dlltorch_core_stubs.so)
frame #10: caml__1017_atg_eq_tensor + 36 (0x1013ada70 in dlltorch_core_stubs.so)
frame #11: caml_interprete + 7760 (0x10093b370 in ocamlrun)
frame #12: caml_main + 1244 (0x10093d8e4 in ocamlrun)
frame #13: main + 16 (0x100960a14 in ocamlrun)
frame #14: start + 2476 (0x1816b3154 in dyld)

ログを見ると,どうやらテンソルのサイズの不整合により失敗しているようです.というわけで次節ではこれを直します.

2-3 不具合の修正

前節最後で出てきたエラーの原因の調査には数時間ほど費やされましたが,結論から言うと torchv0.16.0 に潜んでいる重大な不具合が原因でした.また,原因に気づいた後に確認したところ,v0.17.0 では修正されているようです.

具体的には,Torch.Tensor.argmax の実装に間違いがあり,また具体例中での argmax の使い方にも誤りがありました.以下のような修正が必要です(ついでに argmin も修正しています):

src/wrapper/wrapper_generated.ml:

 let argmax self ~dim ~keepdim =
   let out__ = CArray.make t 1 in
-  stubs_argmax (CArray.start out__) self  (match dim with | None -> Int64.zero | Some v -> Int64.of_int v) (match dim with | Some _ -> 1 | None -> 0)  (if keepdim then 1 else 0);
+  stubs_argmax (CArray.start out__) self  (match dim with | None -> Int64.zero | Some v -> Int64.of_int v) (match dim with | Some _ -> 0 | None -> 1) (if keepdim then 1 else 0);
   let t0 = CArray.get out__ 0 in
   Gc.finalise C.Tensor.free t0;
   t0

 let argmax_out ~out self ~dim ~keepdim =
   let out__ = CArray.make t 1 in
-  stubs_argmax_out (CArray.start out__) out self  (match dim with | None -> Int64.zero | Some v -> Int64.of_int v) (match dim with | Some _ -> 1 | None -> 0)  (if keepdim then 1 else 0);
+  stubs_argmax_out (CArray.start out__) out self  (match dim with | None -> Int64.zero | Some v -> Int64.of_int v) (match dim with | Some _ -> 0 | None -> 1)  (if keepdim then 1 else 0);
   let t0 = CArray.get out__ 0 in
   Gc.finalise C.Tensor.free t0;
   t0

 let argmin self ~dim ~keepdim =
   let out__ = CArray.make t 1 in
-  stubs_argmin (CArray.start out__) self  (match dim with | None -> Int64.zero | Some v -> Int64.of_int v) (match dim with | Some _ -> 1 | None -> 0)  (if keepdim then 1 else 0);
+  stubs_argmin (CArray.start out__) self  (match dim with | None -> Int64.zero | Some v -> Int64.of_int v) (match dim with | Some _ -> 0 | None -> 1)  (if keepdim then 1 else 0);
   let t0 = CArray.get out__ 0 in
   Gc.finalise C.Tensor.free t0;
   t0

 let argmin_out ~out self ~dim ~keepdim =
   let out__ = CArray.make t 1 in
-  stubs_argmin_out (CArray.start out__) out self  (match dim with | None -> Int64.zero | Some v -> Int64.of_int v) (match dim with | Some _ -> 1 | None -> 0)  (if keepdim then 1 else 0);
+  stubs_argmin_out (CArray.start out__) out self  (match dim with | None -> Int64.zero | Some v -> Int64.of_int v) (match dim with | Some _ -> 0 | None -> 1)  (if keepdim then 1 else 0);
   let t0 = CArray.get out__ 0 in
   Gc.finalise C.Tensor.free t0;
   t0

examples/mnist/linear.ml:

     (* Compute the validation error. *)
     let test_accuracy =
-      Tensor.(argmax (model test_images) = test_labels)
+      Tensor.(argmax ~dim:1 (model test_images) = test_labels)
       |> Tensor.to_kind ~kind:(T Float)
       |> Tensor.sum
       |> Tensor.float_value

src/torch/dataset_helper.ml:

       let batch_accuracy =
-        Tensor.(argmax predicted_labels = labels)
+        Tensor.(argmax ~dim:1 predicted_labels = labels)
         |> Tensor.to_kind ~kind:(T Float)
         |> Tensor.sum
         |> Tensor.float_value

これでMNISTの学習がそれぞれ動くようになります:

$ ./_build/default/examples/mnist/linear.exe
1 2.302585 68.08%
2 1.508147 60.77%
3 1.387576 52.54%
4 1.578871 64.46%
5 1.706852 60.46%
6 1.193671 61.55%
7 1.395118 70.66%
8 1.290251 70.44%
9 0.891794 66.76%
10 0.934383 71.84%
11 1.107920 73.78%
(中略)
193 0.315388 91.65%
194 0.315182 91.67%
195 0.314978 91.66%
196 0.314774 91.66%
197 0.314573 91.67%
198 0.314373 91.68%
199 0.314174 91.68%
200 0.313977 91.68%
$ dune build examples/mnist/nn.exe
$ ./_build/default/examples/mnist/nn.exe
50 0.423307 89.46%
100 0.290042 92.06%
150 0.235908 93.36%
200 0.195982 94.31%
250 0.165280 94.96%
(中略)
800 0.037816 97.30%
850 0.033508 97.34%
900 0.029695 97.39%
950 0.026319 97.42%
1000 0.023346 97.44%
$ dune build examples/mnist/conv.exe
$ ./_build/default/examples/mnist/conv.exe
50 0.304574 93.58%
100 0.160071 96.40%
150 0.094311 97.60%
200 0.110899 97.92%
250 0.088540 97.92%
(中略)
4600 0.021709 99.12%
4650 0.030229 99.18%
4700 0.013466 99.01%
4750 0.005627 99.04%
4800 0.000020 99.11%
4850 0.002961 99.02%
4900 0.010990 99.09%
4950 0.013369 99.11%
5000 0.000920 99.20%

正解率から実際に学習もうまくいっていることがわかります.やったぜ.

まとめ

というわけでいろいろな落とし穴がありましたがひとまずCPUでocaml-torchを動かすことはできました.あとはMPSで動かせたら万々歳で,ocaml-torchのAPI上は多少拡張すれば可能そうではあるので,そのうち加筆するかもしれません.


  1. もともと github.com/LaurentMazare/ocaml-torch で個人開発されていたものが2023年4月頃に現リポジトリへと移管されたようです. ↩︎

  2. OCamlの対話環境のひとつ.opam install utop でインストールできます. ↩︎

  3. ちなみに,ここのエラーで出ているテキストで「(中略)」とした部分にはおそらくLibTorchのApple Silicon向けビルドをつくった作者の方の作業ディレクトリであろうパスが表示されます.意図せずバイナリ中にハードコードされてしまったものと思われるので,念のため伏せました. ↩︎