エコな環境でのNx勉強と久しぶりのPR

# 0.はじめに
## 0-1.背景
いつもROMさせていただいている「Nxバックエンド勉強会」でLT会があるとのことで、主宰の@zacky1972先生からお誘い頂きました。

https://pelemay.connpass.com/event/252807/

![MAC-01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/447edaf1-3805-d7e0-0860-19c82fcef34f.jpeg)
IoTイベントなどでは何度かLTしたことはありますが、Elixirはガチの開発言語なので、LTはおろかQIITA記事さえ１本も書いたことがない・・・つまり完全な「ボチボチ趣味で触っているモード」です。
とてもLTなんて・・・というのが正直なところですが、誘われるとその気になっちゃう性格なもんで 無謀にもLTトライすることに。本記事はそのLT用です。
## 0-2.話すこと
ElixirやNxについて語る資格は全くないので、我が家の~~しょぼい~~エコな環境でどうやってNxを触っているのかを話します。
M1 Macやら、GPU乗った強強PCマシンとは縁遠い「えっ、そんな環境でもNx動くの？」ってとこだけ見て頂ければ幸いです。
## 0-3.話さないこと
技術的なことは話したくても話せません:sweat:

# 1.我が家の~~しょぼい~~エコなNx勉強環境

## 1-1. 2台の古いMac
エコ志向の強い我が家の家訓は「溶けるまで使う」です。
とっくに独立して巣立った２人の子供の中学時代の運動着短パンを今だに朝の散歩用に履いてたりします（運動着はすごく丈夫でなかなか溶けてくれない・・・)。

で、困っているのが古いPCです（PCも全然溶けてくれない:sweat:）。
また、仕事で2008年にMacbookに出会ってからは、それまでの自作DOS/Vをお蔵入りさせ、WindowsはMacのBOOTCAMP環境で使うようになりました。

てな訳で以下の2台が我が家の環境です。

### 1-1-1. 我が家のエースiMac(Late 2013)

![MAC-02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/a9abe091-f763-904f-085b-b53eb14ebcbd.jpeg)
一応NVIDIA GPUが載っていますが、compute capability(3.0) が低すぎてpipで入れる tensorflowなどはCUDAでは動きません。

https://developer.nvidia.com/cuda-gpus

そもそもmacOSでのNVIDIA GPUサポートは High Sierraまでのようです。なのでNVIDIA GPUは無いのと同じです:disappointed_relieved:

https://applech2.com/archives/20191124-nvidia-last-release-to-support-macos-for-cuda.html

PC版VRChat/NEOSVRなどを動かす時だけBOOTCAMPでWindows10を使いますが、基本はmacOSが動いています。
頑張ってくれていますが、Docker Desktopを動かすと顕著に重くなるのでDockerは余り使わなくなりました。
macOS Catalina がセキュリティサポート切れになったら、M1/M2 Macを買うかどうか家族会議予定ですが、このまま円安が続くと厳しそうです・・・

### 1-1-2. 我が家のダークホースMBP(Late 2011)
![MAC-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/8a542fa7-d687-7dfb-084f-53d779747dd1.png)
すでにセキュリティサポート切れのmacOS High Sierraは、brewコマンドも色々文句を言ってくるので基本的にBOOTCAMPでWindows10を動かしています。
普段使いはiMacなので、必要時にDockerの代わりにWSL使うって使い方しています。

## 1-2. LivebookでのNx勉強環境

こんな感じで動かしてます。LivebookってのはElixi版Jupyter Notebookみたいな奴です。
![MAC-03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/accb0ec0-ab39-f92f-dd44-31e45859e789.jpeg)


## 1-3.参考にさせていただいた情報
以下を我が家の~~しょぼい~~エコな環境に合わせて実行しました。

https://github.com/RyoWakabayashi/elixir-learning

手順は、iMac のDockerで作ったLivebookコンテナをExportして、それをWin10 WSLにインポートし、Win10 WSL上のLivebookに iMacのブラウザからアクセスするだけです。
ただ、WSLのLivebookに外部ネットワークからアクセスするのにちょっと工夫が必要です。その工夫について説明します。

# 2.iMac側作業

iMac側作業は[参考資料](https://github.com/RyoWakabayashi/elixir-learning)のまんまです。
久しぶりのDockerなのでUpdateしておきます。以下が実際に使用したVersionです。

```
iMac:~ kenzo$ docker version
Client:
 Cloud integration: v1.0.24
 Version:           20.10.17
 API version:       1.41
 Go version:        go1.17.11
 Git commit:        100c701
 Built:             Mon Jun  6 23:04:45 2022
 OS/Arch:           darwin/amd64
 Context:           default
 Experimental:      true

Server: Docker Desktop 4.10.0 (82025)
 Engine:
  Version:          20.10.17
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.17.11
  Git commit:       a89b842
  Built:            Mon Jun  6 23:01:23 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.6
  GitCommit:        10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
 runc:
  Version:          1.1.2
  GitCommit:        v1.1.2-0-ga916309
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```
git cloneして docker-compose upします

```
iMac:20220714_LT kenzo$ git clone https://github.com/RyoWakabayashi/elixir-learning.git
Cloning into 'elixir-learning'...
remote: Enumerating objects: 46, done.
remote: Counting objects: 100% (46/46), done.
remote: Compressing objects: 100% (37/37), done.
remote: Total 46 (delta 16), reused 34 (delta 8), pack-reused 0
Unpacking objects: 100% (46/46), done.
iMac:20220714_LT kenzo$ cd elixir-learning/
iMac:elixir-learning kenzo$ docker-compose up
....
[+] Running 2/2
 ⠿ Network elixir-learning_default       Created                                                       0.2s
 ⠿ Container elixir-learning-livebook-1  Created                                                       1.0s
Attaching to elixir-learning-livebook-1
elixir-learning-livebook-1  | [Livebook] Application running at http://0.0.0.0:8080/?token=f66wodtdejzpe72imtlju3ugjvtbdtve
```
表示されたURLにアクセスしてiMac DockerでのLivebook動作を確認します。

別ターミナルからコンテナに接続してElixirのバージョンだけ確認すると
Elixir 1.13.2 (compiled with Erlang/OTP 24)
のようです。

```
iMac:elixir-learning kenzo$ docker ps
CONTAINER ID   IMAGE                      COMMAND                  CREATED             STATUS              PORTS                                                      NAMES
2fbc8224ff4c   elixir-learning_livebook   "/app/bin/livebook s…"   About an hour ago   Up About a minute   0.0.0.0:4000->4000/tcp, 0.0.0.0:8080-8081->8080-8081/tcp   elixir-learning-livebook-1
iMac:elixir-learning kenzo$ docker exec -it 2fbc8224ff4c /bin/bash
root@2fbc8224ff4c:/data# 
root@2fbc8224ff4c:/data# which elixir
/usr/local/bin/elixir
root@2fbc8224ff4c:/data# elixir --version
Erlang/OTP 24 [erts-12.1.5] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit]

Elixir 1.13.2 (compiled with Erlang/OTP 24)
root@2fbc8224ff4c:/data# exit
exit
```
この稼働中コンテナをWSL用にExportします。

```
iMac:elixir-learning kenzo$ docker export elixir-learning-livebook-1 -o elixir.tar.gz
iMac:elixir-learning kenzo$ ls -l elixir.tar.gz 
-rw------- 1 kenzo staff 1307677696  7  2 18:50 elixir.tar.gz
```
このファイルをUSBメモリ経由でMBP(Win10)へ持っていきます。

# 3.MBP側作業

## 3-1.iMacから Livebook@WSLにアクセスするために

## 3-1-1.IPアドレスでアクセス
WSLで何も考えないでLivebookを起動するとWindows10ホスト側から [http://localhost:8080/](http://localhost:8080/)　でアクセス可能となります。
これは同じPCからアクセスする時は便利なのですが、今回は外部からアクセスするため明示的にIPアドレスでアクセスする必要があります。
WSLのIPアドレスを環境変数 LIVEBOOK_IPに設定すると良いみたいです。
https://github.com/livebook-dev/livebook#environment-variables
```
LIVEBOOK_IP - sets the ip address to start the web application on. Must be a valid IPv4 or IPv6 address.
```

## 3-1-2.パスワード指定
WSL Livebook起動時に表示されるパスワード付きURLだと、別マシンから入力するのが面倒なので、パスワードを指定したいです。
環境変数 LIVEBOOK_PASSWORDに設定すると良いみたいです。
https://github.com/livebook-dev/livebook#environment-variables
```
LIVEBOOK_PASSWORD - sets a password that must be used to access Livebook. Must be at least 12 characters. Defaults to token authentication.
```
## 3-1-3.WSLでLivebook起動
まずはiMacで作成したExportファイルをローカルにコピーして以下のWSLコマンドでImportします。
```
wsl --import elixir $env:userprofile\AppData\elixir elixir.tar.gz
```
![PC-01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/c3d9e7ca-3190-19f6-6795-3ebbafbaaf3a.png)
![PC-02.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/b132f246-e475-24d2-7552-880e7b365a65.png)


停止中のコンテナを以下のWSLコマンドで起動します。
```
wsl -d elixir
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/f416df66-d62c-8900-ffcd-eb279ef6879e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/093eab44-389c-9b98-c247-6575b026cd84.png)
環境変数LIVEBOOK_IPを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/ece7daf7-8eb6-8aa3-325d-27cb75cb5e0f.png)
環境変数LIVEBOOK_PASSWORDを設定します。12文字以上に注意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/5007331f-9420-60ee-338e-775157774052.png)
参考資料の以下コマンドでLivebookを起動します。
```
source /home/livebook/setup_for_wsl.sh
/app/bin/livebook start
```
![PC-11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/c27079ff-1df4-7793-cdb6-4c64a6d8483d.png)
WSLを動かしているMBPのブラウザで動作確認します
![PC-12.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/758ed776-8ff4-f9d1-1cf7-1cbbdefad48b.png)
![PC-13.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/dba1093a-bb8c-a2ca-09bd-8f36ad67347f.png)



## 3-1-4.Port転送設定
iMacからMBPの8080 Portにアクセスがあると、それをWSLに転送することでiMac→WSLの通信を実現します。これにはWindows10のnetsh.exeを使います。
ポート転送は、以前WSLで立ち上げたUbuntuにssh接続してた際に参考にさせて頂いた以下記事が分かりやすいです。

https://qiita.com/yabeenico/items/15532c703974dc40a7f5

netsh.exeのオフィシャルドキュメントはこちらにあります

https://docs.microsoft.com/ja-jp/windows-server/networking/technologies/netsh/netsh-interface-portproxy

まず転送設定が無いことを確認します。
![PC-14.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/8a334aa5-e6bc-fb90-8322-41aea145b360.png)
以下netshコマンドで転送設定します
netsh.exe interface portproxy add v4tov4 listenport=＜ポート番号＞ connectaddress=＜WSLのIPアドレス＞ connectport=＜ポート番号＞

```
netsh.exe interface portproxy add v4tov4 listenport=8080 connectaddress=172.19.46.113 connectport=8080
```
![PC-30.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/e0e5623e-fdc6-e768-5589-0bf2abd0f10f.png)

## 3-1-5.Firewall一時停止
Windows10のFirewallが邪魔をするので、一時的にプライベートネットワークで無効にしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/327a7971-f40e-f33a-9b9b-76de8d2aef93.png)


## 3-1-6.iMacからアクセス確認
iMacのブラウザからWSL稼働PCのIPアドレスを指定して動作確認します。
![MAC-10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/48194a50-49eb-7fe7-a563-314d1c57d795.jpeg)
![MAC-11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/26112dbd-15df-c34c-6d53-8f0fd13f7884.jpeg)
めでたくメインマシンのiMacからサブマシンのLivebook環境にアクセスできました。
調子が悪くなると、WSLのLivebook環境を消して、elixir.tar.gzファイルのインポートからやり直しできるので便利です。

## 3-1-7.Firewall穴あけ

プライベートネットワークとは言えFirewallを無効化しているとWindows10が文句を言ってくるので、Firewallの穴あけルールを設定して、プライベートネットワークFirewallを有効に戻しておきます。

以下を参考に設定しました。

https://www.buffalo.jp/support/faq/detail/792.html


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/3fc14f60-ace6-2685-a3c1-bd8e612f3832.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/bc1941d8-f806-923a-fe25-9fbb856b8bea.png)





## 3-2.ちょっと工夫

WSLのアドレスは起動毎に変更されるので、IPアドレスを固定化するか、起動スクリプトの工夫で自動設定できないか無いかと試行錯誤しました。IPアドレスを固定化は上手く行かなかったので、ホスト側とWSL側で環境変数を持ち回りして、ホスト側のPowerShellのコマンド操作だけで利用できるようにしました。これをスクリプトにまとめて自動起動なども出来ますが、そこまで熱心にNx勉強していません:sweat:

### 3-2-1 パスワード持ち回り

固定値設定でも動きますががホスト側の環境変数で制御したいです。
こちらのwslvarコマンドが使えそうです。

https://wslutiliti.es/wslu/

参考資料の設定にはwsluが元々組み込まれているので追加インストール不要で使えます。

![PC-16.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/62bc9619-9079-56a0-ff8f-254623a7425e.png)

### 3-2-2 IPアドレス持ち回り

WSLのIPアドレスは、WSLコンテナを立ち上げないとわかりません。
このIPアドレスは、WSL内ではlivebook起動前に環境変数LIVEBOOK_IPに設定する必要があり、ホスト側ではポート転送のためのnetshコマンドの引数として使います。
この処理は一度では無理なので、まずホスト側からWSLコンテナ起動時にIPアドレスを取得してポート転送設定を行い、その後でWSLの起動用スクリプト(新設)でLivebookを起動することにします。
IPアドレスの取得と持ち回りはこんな感じです。
![PC-17.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/ad236a1d-453c-e2a3-57f2-fed347bc8471.png)
ポート設定はこんな感じです。
![PC-19.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/1e31ed47-05a6-75d9-3070-f03f6e2c5c3e.png)

起動用スクリプトは元々用意されている/home/livebook/setup_for_wsl.shをコピーして、こんな感じで用意します。
```
root@MBP2-WIN:/home/livebook# cat boot_for_wsl.sh
#!/bin/bash

export HTTP_PROXY="$(wslvar HTTP_PROXY)"
export HTTPS_PROXY=$HTTP_PROXY
export HOME=/home/livebook
export EVISION_PREFER_PRECOMPILED=true

export LIVEBOOK_PASSWORD=$(wslvar LIVEBOOK_PASSWORD)
export LIVEBOOK_IP=$(wslvar LIVEBOOK_IP)

/app/bin/livebook start
root@MBP2-WIN:/home/livebook#
```

### 3-2-4 最終的な起動手順

最終的にはホスト側の管理者権限Powershellで以下を実行して起動しています。

```
$env:LIVEBOOK_IP = (wsl -d elixir exec hostname -I ).Trim()

netsh.exe interface portproxy delete    v4tov4 listenport=8080
netsh.exe interface portproxy add    v4tov4 listenport=8080 connectaddress=$env:LIVEBOOK_IP

wsl -d elixir exec /home/livebook/boot_for_wsl.sh
```
事前環境
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/f336737b-2dac-863a-e4ff-cc6013027cd3.png)
起動
![PC-20.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/bb026772-5f87-7059-8471-fc6a6c38a1fd.png)


# 4.おまけ：久しぶりのPR

こんな~~しょぼい~~エコな環境なもんで、当然弊害も多いです。一番痛いのはEXLA/AXONがマトモに動かない事。
まぁ、NxでMNISTなんかをやってるコードをボチボチ追ってても十分楽しいので、これにトライしました。

https://github.com/elixir-nx/nx/blob/main/exla/examples/mnist.exs

こんな感じで、EXLA動かない問題を回避
変更点①
```
Mix.install([
  {:httpoison, "~> 1.8.1"},
  {:nx, "~> 0.2.0", github: "elixir-nx/nx", sparse: "nx", override: true}
])
```
変更点②　EXLA コメントアウト
```
# EXLA.set_as_nx_default([:tpu, :cuda, :rocm, :host])
```
一晩放置
```
Training MNIST for 10 epochs...

Epoch 1 Time: 12598.080261s
Epoch 1 average loss: 2.421100616455078
Epoch 1 average accuracy: 0.7605682611465454

....

Epoch 10 Time: 12331.416516s
Epoch 10 average loss: 0.6645944118499756
Epoch 10 average accuracy: 0.9362679719924927
```
最後に推論処理を・・・なぬ、Predictでコケるゾォ〜

これは簡単、せっかく学習した推論パラメータをPredictに渡してないだけです。
変更点③：MNIST.predictに引数追加
```
IO.puts("The result of the first batch against the trained network")
IO.inspect(MNIST.predict(final_params,hd(train_images)))
```
折角なのでPR出しました。

https://github.com/elixir-nx/nx/pull/781

こんな小さなPRでもマージされると嬉しいです。
![MAC-05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79389/d89d21c4-3792-fd6a-c256-17aa3dafd358.jpeg)

おしまい。