🍎

GPU付きWindows上のTensorflowをMacから開発する

2023/09/04に公開

概要と目標

WindowsにGPU入れたのでTensorflowを触ってみようと思ったのですが、ショートカットが違うのが嫌なのでMacから触れるようにしようと思います。色々方法があってどうするのが良いのか迷うのですが、まずtensorflowの環境はdockerのコンテナを使うのが手っ取り早いと思います。でMacからはSSHで繋ぎます。SSHがつながればVSCodeのDev Containersを使って開発ができます。

また、Windowsはポチッと電源ボタンをいれてWindowsのキーボードやマウスは触らずにMacから操作できるようにしたいのと、PowerShellは得意じゃないので慣れたLinux系のシェルでSSHログインを目指します。

で、それぞれdockerとSSHは下記のパターンがあると思います。

まずSSH接続。これには2種類方法があります。

  • WindowsにOpenSSHサーバーをインストールする。デフォルトのシェルはPowerShellですが変更可能です。
  • WSLにSSHをいれてそこに繋げる。

そしてdockerも2パターン(厳密には3種類?)あると思います。

  • WindowsのDockerDecktopを使う。
  • WSLにDockerを入れて使う。

3種類と言ったのはDockerDecktopはWSLと共有されてWSL内から操作可能です。なので、Windowsに直接SSH接続してDockerDecktopを操作することも、WSLにSSH接続して操作することも可能で、なんとなくニュアンスが違う気がしたので3種類かなと。

結論(暫定)

うまく動かないところがいくつかって、結局下記のパターンを採用しました。

  • SSHはWSLでSSHサーバーを立てる。
  • dockerはDockerDesktopは使わずにWSLにインストールする。

他の方法では目標を達成できなかったのでこの方法を採用しましたが、結果、全てがWSLの中に収まって綺麗な開発環境ができたような気がします。

試したことはすべて手順を載せます。最初記事を分けようかと思ったけど、設定やエラーが微妙に被っていってどう分けるのか悩んだのでまとめちゃました。長いし見づらいと思いますがご容赦ください。

環境

Windows

エディション Windows 11 Pro
バージョン 22H2
OS ビルド 22621.2134

WSL

WSL バージョン: 1.2.5.0
カーネル バージョン: 5.15.90.1

Ubuntu-22.04

WSLはバージョンが複雑でややこしいですが1.2.5.0だけどWSL2ですね。なぜ1がインストールされるんだ??としばらくハマりました。

Mac

Apple M1 Max(挙動と関係あるかもしれないので一応)
13.4 (22F66)

設定方法と解説

SSH接続

ではまずSSH接続から行きます。

WindowsのOpenSSHを使う

スタートメニュー>設定>アプリ>オプション機能>オプション機能を追加する[機能を表示]をクリック。リストにあるOpenSSHサーバーをクリックしてインストールします。sshd_configはC:\ProgramData\ssh\sshd_configにあります。ProgramDataは不可視になってるので注意してください。

下記のような設定があります。

# Match Group administrators
      AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys

これがONになっているとadministratorsグループに登録されてるユーザーのauthorized_keysC:\ProgramData\ssh\administrators_authorized_keysになるので注意が必要です。Linux系のと同じように~/.ssh/authorized_keysに公開鍵を置きたい場合はOFFにしてください。

ターミナル(PowerShell)を起動し下記のコマンドを実行してください。

# 自動で起動するように設定
Set-Service -Name sshd -StartupType Automatic

# 起動する
Start-Service -Name sshd

この方法でインストールするとファイアウォールは勝手に開くようですが、だめだったら自分で22を開けてください。

これでMacから接続できるのですが、Windows11だとMicrosoftアカウントでログインしてて、ユーザー名ってなんだっけ?となりませんか?そんな時はターミナルでwhoamiとコマンドを打つとmachinename\usernameのような形で表示されるので確認してください。なんかメールの@マーク前を変なところで切って勝手に名前になってますよね。ユーザーディレクトリも同じです。気持ち悪いなあ。ユーザー名は簡単に変更できるけど、ディレクトリ名は変更大変なのでやめました。気持ち悪いわあ。

WindowsのIPは固定しとくと便利だと思います。うちのルーターはスマホアプリから設定可能で固定する機能があったので簡単でしたが、そういうのがないとDHCPの振り分け範囲を狭くして、空けたところをWindows側に直指定し固定する等工夫が必要かも。

デフォルトのシェルについて

MacからSSHで接続した時のデフォルトのシェルは変更可能です。WindowsのコマンドやPowerShellに慣れてるなら特に問題ないですが、Macユーザーなら慣れたlinux系のコマンド使いたいですよね。

使用可能なシェルを指定するコマンドを列挙しておきます。

# Windowsのコマンド。これがデフォルト(だと思う確認する前に変えてしまったので)。
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\system32\cmd.exe" -PropertyType String -Force

# PowerShell。Windows11のデフォルトはバージョン5系。最新の7は別途インストール。
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force

# PowerShell7。5起動時に表示されるリンクの通り入れるとここに入ります。
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Program Files\PowerShell\7\pwsh.exe" -PropertyType String -Force

# GitBash。gitをインストールしていればあるはす。
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Program Files\Git\usr\bin\bash.exe" -PropertyType String -Force

# WSLのbash。これが一番おすすめだと思われる。
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\bash.exe" -PropertyType String -Force

ところがここでハマってしまいました。WSLのbashが一番良さそうなのでそれを指定しましたが「ファイルにアクセスできません。」と接続が切断されます。詳しいエラー内容はsuperuserに質問投げてるのでそちらを参照ください。解決方法をご存知の方がいたら教えてください。

それ以外のShellは起動できたのですが、どのシェルでもwslを実行すると「ファイルにアクセスできません。」と表示され実行できません。これも根は同じっぽいですね。ユーザーの権限関連かとも思いましたが、C:\Windows\System32\bash.exeをユーザーディレクトリにコピーし全権限を与えてみたけどダメでした。

WSLのSSHを使う

まずWSLをインストールします。インストールは簡単でした(Windows 11だからか?)。コマンドプロンプトを開き

wsl --install

とタイプするだけです。Ubuntuのインストールまで勝手にやってくれます。ただ、ここで入れるのは20.04です。LTSは22.04がリリースされていますのでそちらを入れました。インストールはMicorosoftStoreからUbuntuを検索してインストールできます(後述しますがこれをしたせいでトラブった可能性があります)。

WSLにSSHをインストールする。

sudo apt -y update
sudo apt -y install openssh-server

設定はお好みで・・・下記で編集、再起動が可能です。

sudo vi /etc/ssh/sshd_config
sudo systemctl restart sshd

公開鍵を~/.ssh/authorized_keysに登録します。(たしか)ディレクトリ自体ないので作っておきます。

mkdir ~/.ssh
touch ~/.ssh/authorized_keys
chmod 0700 ~/.ssh
chmod 0600 ~/.ssh/authorized_keys

ポートフォワード設定とWSLの自動起動

Macから10022ポートで接続してきたSSHをWSLの22にフォワードします。WSL2からはネットワークが別なのでIPアドレスが変わってしまう可能性があるようです。

こちらが詳しいですが、変わってしまうのにlocalhostで繋げるという情報がありますが、実はこれがとても不安定で頻繁に解決を失敗します。

そこでWindows起動時にWSLを起動してIPアドレスを取得し、ポートフォワードの設定を自動で登録します。

下記のファイルを任意の場所に作成します。ファイル名はwsl_ssh.batとしました。

wsl -d Ubuntu-22.04 -u root -- service ssh restart
netsh interface portproxy delete v4tov4 listenport=10022

for /F %%i in ('wsl -d Ubuntu-22.04 exec hostname -I') do set ip=%%i
netsh interface portproxy add v4tov4 listenport=10022 connectaddress=%ip% connectport=22

これをタスクスケジューラーに登録すれば自動で起動するはずだったのですが上手くいきませんでした。

ダメだった方法

タスクスケジューラーに登録する方法はこちらに詳しく載っていますがダメでした。ログにはエラーも残らず起動しません。IPアドレス自体の取得も失敗し、空が登録されてます。

WSLのISSUEを見つけました。タイトル的にこちらの方が分かりやすいので先に紹介しましたが、そちらは重複で閉じてます。本家はこちらです。どうやらタスクスケジューラからwslコマンドを実行できないようです。MicrosoftStoreからインストールしたものは起動できない(WSLインストール時に自動で入るやつはOK)というコメントも見ましたが試してません。

ここにサービスとして登録すると起動するという話も出てますがこれもダメでした。ISSUEを覗くと他にも「これで上手くいきました」という書き込みがあります。全部は試してませんがいくつか試してダメでした。

上手くいった方法

先ほどのwsl_ssh.batのショートカットをスタートアップフォルダに登録します。ショートカットのプロパティを表示しショートカット>詳細設定から管理者として実行にチェックを入れます。先ほどのサイトによればwslコマンドでサービスを起動すると自動終了しないという事なのでこれで上手くいくかと思いましたがダメでした。STATEはちょっとの間Runningになり、その間ならMacからSSH接続できました、ちょっと経つとStoppedに変わり切断されてしまいます。そこで、スタートメニュー>すべてのアプリからUbuntu 22.04.2 LTSを探してスタートアップフォルダにドロップし登録します。これで起動するようになりました。

Windows起動時に自動でログイン

タスクスケジューラーに登録する方法がうまくいけばユーザーにログインしなくても起動するようですが、スタートアップフォルダに入れたのでログインする必要があります。なのでWindowsに自動でログインするよう設定しました。

2つの設定変更が必要なようです。

スタートメニュー>設定>アカウント>サインインオプションで「セキュリティー向上のため、このデバイスではMicrosoftアカウント用にWindowsHelloログインのみを許可する(推奨)」をOFFにします。

その後Windows+Rを押して「ファイル名を指定して実行」からnetplwizを実行。リストから対象のユーザーを選択状態にして「ユーザーがこのコンピューターを使うには、ユーザー名とパスワードの入力が必要」のチェックを外します。パスワードを求められるので入力すれば完了です。

これで自動起動まで辿り着きました。

docker

dockerの準備です。

DockerDesktopを使う

https://www.docker.com/products/docker-desktop/
ここからインストーラーがダウンロードできます。Windowsに入れるのでWindows版を選びます。

インストール後起動し、設定(右上のギアーアイコン)からStart Docker Desktop when you log inを入れておくと勝手に起動します。

WSLからDockerDesktopを使う

WSLにdockerを入れなくてもDockerDesktopのdockerが使えます。設定がうまくいってないと使えません。コマンドを打ってみてください。下記のように表示されたら設定変更が必要です。

$ docker -v

The command 'docker' could not be found in this WSL 2 distro.
We recommend to activate the WSL integration in Docker Desktop settings.

For details about using Docker Desktop with WSL 2, visit:

https://docs.docker.com/go/wsl2/

コマンドを使うにはWSLのバージョンが2である必要があります。Windows11なので2になってるはずですが、wsl -l -vで確認ができます。1になっていたらwsl --set-version <distro name> 2で変更可能です。

また、DockerDesctopの設定>GeneralUse the WSL 2 based engineにチェックが入ってること、そしてRecources>WSL integrationで対象のWSL distroがONになっている必要があります。

WSLのDockerを使う

Ubuntuへのインストール方法がそのまま使えます。試してないですがDockerDesktopがWSLで有効になってると良くないと思うので先ほどの設定でintegrationをOFFにしてください。

https://docs.docker.com/engine/install/ubuntu/

nvidia-smiが無い

WSLのコマンドプロンプトでnvidia-smiコマンドを打つとGPUの情報がでますが、MacからSSH接続した時Command 'nvidia-smi' not foundになります。

$ nvidia-smi
Command 'nvidia-smi' not found, but can be installed with:
sudo apt install nvidia-utils-390         # version 390.157-0ubuntu0.22.04.2, or
sudo apt install nvidia-utils-418-server  # version 418.226.00-0ubuntu5~0.22.04.1
sudo apt install nvidia-utils-450-server  # version 450.248.02-0ubuntu0.22.04.1
sudo apt install nvidia-utils-470         # version 470.199.02-0ubuntu0.22.04.1
sudo apt install nvidia-utils-470-server  # version 470.199.02-0ubuntu0.22.04.1
sudo apt install nvidia-utils-525         # version 525.125.06-0ubuntu0.22.04.1
sudo apt install nvidia-utils-525-server  # version 525.125.06-0ubuntu0.22.04.1
sudo apt install nvidia-utils-535         # version 535.86.05-0ubuntu0.22.04.1
sudo apt install nvidia-utils-535-server  # version 535.54.03-0ubuntu0.22.04.1
sudo apt install nvidia-utils-510         # version 510.60.02-0ubuntu1
sudo apt install nvidia-utils-510-server  # version 510.47.03-0ubuntu3

これはMacからSSHで接続した時にパスが通ってないからですね。色々方法はあると思いますが~/.bashrcで通しました。nvidia-smi/usr/lib/wsl/libにありますが、Windowsで開いた時のPATHと見比べて足りないもの全部足しました(必要ないかも)。Windowで開いた時重複しないようにこちらの記事からコードを拝借してます。

# ~/.bashrc
PATH="$PATH:/usr/lib/wsl/lib:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/Program Files/Git/cmd:/mnt/c/Program Files/PowerShell/7/:/mnt/c/Program Files/Docker/Docker/resources/bin:/mnt/c/Program Files (x86)/NVIDIA Corporation/PhysX/Common:/mnt/c/Program Files/NVIDIA Corporation/NVIDIA NvDLISR:/mnt/c/Users/miyat/AppData/Local/Programs/Python/Python310/Scripts/:/mnt/c/Users/miyat/AppData/Local/Programs/Python/Python310/:/mnt/c/Users/miyat/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/miyat/AppData/Local/Programs/Microsoft VS Code/bin"

# 重複してるパスを取り除く
_path=""
for _p in $(echo $PATH | tr ':' ' '); do
  case ":${_path}:" in
    *:"${_p}":* )
      ;;
    * )
      if [ "$_path" ]; then
        _path="$_path:$_p"
      else
        _path=$_p
      fi
      ;;
  esac
done
export PATH=$_path

unset _p
unset _path

source ~/.bashrcで読み込んでやればnvidia-smiが実行できるはず。

$ nvidia-smi
Mon Sep  4 19:32:33 2023
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.86.05              Driver Version: 537.13       CUDA Version: 12.2     |
|-----------------------------------------+----------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |
|                                         |                      |               MIG M. |
|=========================================+======================+======================|
|   0  NVIDIA GeForce RTX 4070 Ti     On  | 00000000:01:00.0  On |                  N/A |
|  0%   33C    P8              11W / 285W |    934MiB / 12282MiB |     12%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|
|  No running processes found                                                           |
+---------------------------------------------------------------------------------------+

nvidia-container-toolkitをインストール

WSLにインストールしたdockerのコンテナでGPUを使うためにはnvidia-container-toolkitをインストールします。これをインストールせずにGPUをONにしたコンテナを起動すると下記のエラーが出ます。

docker run --name tensorflow --gpus all -p 11022:11022 -itd tensorflow/latest-gpu-ssh
df7227396bab11.....
docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]].

インストールする時に下記エラーが出るはずです。

/sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link

これはこの辺で議論されてますが、インストーラーがlibcuda.so.1libcuda.soをシンボリックリンクを想定しているけどWindowsはコピーしているので出るエラーです。シンボリックリンクにすればエラーは消えますが、そのままシンボリックリンクにしておくと、TensorflowがGPUを認識しなくなり、演算に使ってくれないので元に戻す必要があります。

# 一応現状を確認。
ls -al /usr/lib/wsl/lib/

# バックアップをとりつつシンボリックリンクに変更
cd /usr/lib/wsl/lib/
sudo mv libcuda.so libcuda.so.bak
sudo mv libcuda.so.1 libcuda.so.1.bak
sudo ln -s libcuda.so.1.1 libcuda.so
sudo ln -s libcuda.so.1.1 libcuda.so.1 

インストール方法はこちらにあります。

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list \
  && \
    sudo apt-get update
sudo apt-get install -y nvidia-container-runtime
sudo service docker restart

先ほどのシンボリックリンクをファイルに戻します。

cd /usr/lib/wsl/lib/
sudo rm libcuda.so
sudo rm libcuda.so.1
sudo mv libcuda.so.bak libcuda.so
sudo mv libcuda.so.1.bak libcuda.so.1

ポートフォワードの追加

コンテナにもSSH接続するのでWSLにSSH接続するのとは別のポートでSSHを繋げるようにします。今回は11022を使いました。先ほどのポートフォワード設定のbatファイルを以下のように変更。管理者権限で実行してください。

wsl -d Ubuntu-22.04 -u root -- service ssh restart
netsh interface portproxy delete v4tov4 listenport=10022

for /F %%i in ('wsl -d Ubuntu-22.04 exec hostname -I') do set ip=%%i
netsh interface portproxy add v4tov4 listenport=10022 connectaddress=%ip% connectport=22
netsh interface portproxy add v4tov4 listenport=11022 connectaddress=%ip% connectport=11022

11022はファイアウォールも開けてください。

コンテナを立ち上げる

どっちのSSHでも基本変わらないので章はまとめましたが、微妙に起きるエラーが違ってややこしくなっていまいました。ご了承ください。

Dockerfile

Tensorflowの開発環境のdocker containerを立ち上げてSSHでMacから接続するところまでです(きっとgitとかも入れなきゃならんでしょうな)。

FROM tensorflow/tensorflow:latest-gpu

RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
RUN sed -i 's/#Port 22/Port 11022/' /etc/ssh/sshd_config
RUN mkdir /root/.ssh && chmod 0700 /root/.ssh && touch /root/.ssh/authorized_keys && chmod 0600 /root/.ssh/authorized_keys
RUN echo "ssh-rsa ..." > /root/.ssh/authorized_keys
RUN mkdir /root/souces

EXPOSE 11022
CMD ["/usr/sbin/sshd", "-D"]

echo "ssh-rsa ..."の所は公開鍵を直書きしてます。まあ、公開鍵だし、ローカルのdockerなので問題ない気もするけど、githubなどに保存するならファイルにしてignoreした方がいいのかな?お好みで。鍵の作成方法は巷に溢れてるので省略します。

下記コマンドでビルド。コンテナを立ち上げます。名前は適当です。

docker build -t tensorflow/latest-gpu-ssh .
docker run --name tensorflow --gpus all -p 11022:11022 -itd tensorflow/latest-gpu-ssh

sshの接続はこんな感じ(コマンド引数ではなくconfigに書いちゃいました)。

Host windows-pc-tensorflow
  HostName 192.168.68.74
  User root
  Port 11022
  IdentityFile "~/.ssh/windows"

docker login

docker buildするためにtensorflow/tensorflow:latest-gpuのimageをPULLするのでコマンドラインでのログインが必要ですが、これが、MacからSSHで接続するごログインできませんでした。

WindowsにインストールしたSSHでもWSLに起動したSSHでも同様に失敗しました。エラーメッセージを見失いましたが、こちらのサイトの方法で解決できます。

~/.docker/config.jsonを開き

{
        "credsStore": "desktop.exe"
}

{
        "credStore": "desktop.exe"
}

に編集します。他にもこのファイルを削除すればいいとか、_credsStoreに変えるといけるとか色々ありましたが試してません。要するにこの設定ファイルにcredsStoreのキーがなければOKなんだと思います。

ただしWindowsにインストールしたSSHでは別のエラーが出ました。

Error saving credentials: error storing credentials - err: exit status 1, out: `A specified logon session does not exist. It may already have been terminated.`

これで検索するとこんなdockerのISSUEにたどり着きます。こんなPRが進行中なのでそのうち治ると思います。

ただ、変なんですけど、ログインできる時もあるんですよね。これ書いてる時にエラーメッセージをもう一回出そうと思って試したら入れました。Windows側のターミナルで一回docker loginするとできるのかもしれませんが、それでも出来なかった時もあります。

sudoなしでdockerコマンドを実行する

sudoなしでdocker psのような何かdockerのコマンドを打つと下記のようなエラーが出ます。

ERROR: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/_ping": dial unix /var/run/docker.sock: connect: permission denied

dockerグループにユーザーを追加することで解決します。


```bash
sudo gpasswd -a $(whoami) docker

docker再起動で反映できるという情報もありますが、WSLの場合はWSL自体の再起動をしないとログインするたびにdockerグループから外れてしまうようです。WSLの再起動はwsl --shutdownで終了後起動し直してください。

感想

こういうやり方の選択肢がいくつかあるのは、どれを採用するか迷いますね。他にもリモートデスクトップを使うとか、UbuntuをWindowsマシンにインストールしてデュアルブートにする方法なんかもありますね。

ちなみにリモートデスクトップはショートカットがWindowsになるので不採用。デュアルブートはRAIDを認識しなかったので試しませんでした。UbuntuServerだと認識するという噂もチラッと見ましたが、まあ、目的の環境はできたのでここで燃料切れです。

結構時間使っちゃったけど、WSLもさわれて楽しかったです。

とにかく色々試したので、細かいところで記憶違いとかあったらごめんなさい。

Discussion