【VS Codeでほぼ完結】GPUマシンにssh接続，Dockerコンテナを開いてコードを実行【Ubuntu/Windows/macOS】

# 1. 背景
**sshでGPUマシンに接続し，Dockerコンテナに入ってJupyter Labを実行，
そして生成したURL（トークンなど）をブラウザに貼り付けてNotebookを実行する**，
といった流れでPythonなどのコードをNotebookで実行することはできますが，
手順が面倒くさかったり，terminalで色々叩かないといけないのが面倒くさかったりします．

そこで，**ほとんどの動作をVS Code上でクリック操作でできるようにしたら色々楽になった**ので，
備忘録としてやり方をまとめておきます．

※一部，以前書いた記事のコピペ部分があります．

# 2. 前提
GPUマシンはUbuntu，ローカルPCはMac or Windowsでのみ動作確認を行いました．
## PC環境
動作確認を行ったPCの情報です．
- GPUマシン（サーバー側）
    - Ubuntu 20.04 LTS
    - AMD EPYC 7402P 24-Core Processor
    - RTX 3090 ×2
    - メモリ128GB
    - Dockerインストール済み
        - Docker イメージ準備済み
    - OpenSSHインストール済み
- ローカルPC（ユーザー側）
    - MacBook Pro (13-inch, 2020, Thunderbolt 3ポートx 4)
    - メモリ16GB
    - macOS 12.5.1
    - 2.3GHzクアッドコアIntel Core i7
    - VS Codeインストール済み（日本語化済み）

# 3. 最初の準備
初めて手元のローカルPCをGPUマシンに接続するときは少し準備が必要になります．
初期設定的なやつです．
## 鍵発行
初めて接続する際はローカルPC側で秘密鍵と公開鍵を発行し，公開鍵をGPUマシンに送信し，保存しておく必要があります．
```zsh:terminal
cd #ホームディレクトリ（Users/ユーザー名/）に移動
cd .ssh #.sshフォルダがない場合はmkdirしてください
ssh-keygen [-f <output_keyfile>]
```
例：
```zsh:terminal
cd #ホームディレクトリ（Users/ユーザー名/）に移動
cd .ssh #.sshフォルダがない場合はmkdirしてください
ssh-keygen -f id_rsa_user1
```
パスフレーズはなくてもOKです（Enterキー押すだけ）．
例：`output_keyfile`を`id_rsa`などにすると，`.ssh`フォルダ内に`id_rsa`（秘密鍵）と`id_rsa.pub`（公開鍵）の2種類のファイルが生成されます．複数名でGPUマシンにアクセスすることを想定しているならば，`id_rsa_user1`など違いがあるとわかりやすいです（管理しやすく）．
参考：[ssh-keygenコマンドの使い方](https://qiita.com/hana_shin/items/2003698873a5782b7efd)

## ホスト名の簡略化（hostsファイルの編集）
まず，GPUマシンのIPアドレスを調べておきます．
```bash:terminal（サーバー側）
ip a
```
例：
```bash:terminal
>> inet 10.##.#.###  (わざと隠してます）
```
クライアント側のhostsファイルを開き，編集します．
```zsh:terminal（ローカルPC側）
sudo vi /etc/hosts
```
viの使い方は[こちら](https://eng-entrance.com/linux-command-vi)を参考にしました．
「コマンドモードと入力モードの切り替え」と「保存と終了」部分が参考になります．
hostsファイルに以下を追記します．
```text:hosts
[UbuntuPCのIPアドレス] [Tabキー] [任意のホスト名]
```
[UbuntuPCのIPアドレス]には先程調べたIPアドレスを入力してください．
[Tabキー]はTabスペースを空けるという意味です．
[任意のホスト名]は`gpumachine`など覚えやすいホスト名で構いません．

例：
```text:/etc/hosts
10.##.#.###   gpumachine
```

※Windowsの場合`C:\windows\system32\drivers\etc\hosts`にあります．これを編集してください（管理者権限が必要となるので，スタートメニューからWindows標準のメモ帳を探し，右クリック→管理者として実行，メモ帳が開いたら左上のメニューから「開く」を押して`C:\windows\system32\drivers\etc\hosts`のhostsファイルを開いて編集，編集後上書き保存が可能となります．）

## 公開鍵の転送・登録
先ほど作成した公開鍵をGPUマシンに転送し，登録します．
パスワードの入力が求められた時は，GPUマシンにログインする時のパスワードを入力してください．
**macの場合**
```zsh:terminal（クライアント側）
ssh-copy-id -i ~/.ssh/id_rsa.pub [gpuマシンのユーザー名]@[gpuマシンのホスト名]
```
例：
```zsh:terminal（クライアント側）
ssh-copy-id -i ~/.ssh/id_rsa_user1.pub user@gpumachine
```
**Windowsの場合**
[こちら](https://qiita.com/tabu_ichi2/items/446722c15e6b5678ccad)を参考にするとssh-copy-idと同様のことができます．

## リモート接続時のコマンドを簡単にする
ローカルPC側の「.ssh」ディレクトリの「config」ファイルにエイリアスを定義することで、リモート接続時のコマンドを省略できます．
```zsh:terminal
vi ~/.ssh/config
```

```text:config
Host [エイリアス１]
 HostName [リモートのホスト名]
 User [リモートユーザー]
 IdentityFile /Users/[user名]/.ssh/[秘密鍵]
 Port [ポート番号]
 ServerAliveInterval 60
 LocalForward   8888 localhost:8000
```
ポート番号は，あれば記述します（ngrok使用時など）．
ServerAliveIntervalはssh接続時に一定時間放置した時自動的に接続が切れてしまうのを防ぐために設定します．
LocalFowardはポートフォワーディングです．
コンテナにポートフォワーディングを設定する場合はここを設定しておいたほうが良いです．
**基本的にVS Codeでしか開かない，コンテナへの排他的アクセス制限は行わない，という場合はLocalForwardはなくても良い気がします（VS Code上でコンテナを開いたりする場合は自動的にPort番号が振られている）．**
セットアップ時にエラーが出る場合は全て終わった後，設定してもいいかもしれません．
私はポートフォワーディングをconfigに予め書いておけることを後で知り，セットアップ後にconfigに追記しました．
もし，LocalForwardを設定する場合は，未使用のポート番号を振り，ユーザー一人ひとりが違う番号を使うようにしましょう．

※（2022年5月9日追記）
ポートフォワーディングに関して，例で示しているport番号で実行すると
クライアント側のport 8000 ↔ GPUマシンのport 8888 が紐づけされ，
後述の`docker run`時の-p オプションにより（例：`docker run -it --gpus all --name user1lab -v ~/Desktop/user1:/container/user1 -p 8000:8888 condalab-cuda-torch-jp`），最終的にクライアント側のport 8000 ↔ （GPUマシン経由） ↔ dockerコンテナ内のport 8888が紐づけされるイメージになります（間違っていたらごめんなさい）．
ポート番号は排他的に使用しなければいけないので（実行中の1コンテナごとに違うポート番号），configファイルでLocalForwardを例のように固定してる場合，2個目のコンテナを並列で立ち上げるためにはクライアント側で2つ目のconfigファイルを活用しないssh接続を行うなどの方法があります．
例として，2つ目のターミナルウィンドウで`ssh user名:ホスト名 -L 8888:
localhost:8001`でGPUマシンにssh接続し，`docker run -it --gpus all --name コンテナ名 -v ~~~~ -p 8001:8888 dockerimage_name`でコンテナ作成しコンテナ内でjupyterの起動をし接続するやり方でうまくいきました．
また，GPUマシンを複数人で共有する場合，利用者それぞれでconfig内LocalForwardを違うport番号で設定しておき，portが被るのを防ぐなどのやり方があると思います．

※（2022年6月1日追記）
実際に複数人でコンテナを起動し試したところ，以下のようにするとスムーズに実行できました．

1人目はconfigファイル内で`LocalForward   8888 localhost:8000`としておき，コンテナ`run`実行時オプションにて`-p 8000:8888`，jupyterの起動時`jupyter lab --ip=0.0.0.0 --allow-root --port 8888`とport番号（8888はデフォルト値）の指定をします．

2人目はconfigファイル内で`LocalForward   8889 localhost:8001`としておき，コンテナ`run`実行時オプションにて`-p 8001:8889`，jupyterの起動時`jupyter lab --ip=0.0.0.0 --allow-root --port 8889`とport番号の指定をします．

このように一人ひとりが以上の箇所で違う数値を用いることにより，同時にコンテナを実行できることを確認しました．

※（2022年10月20日追記）
ポートフォワーディングに関してですが，VSCodeで主に使う場合，ポートを指定していない場合でも複数人が並行してDockerコンテナを開いて作業できることを確認しました．
Dockerにアクセスできる人を制限したい場合などはssh接続時にポートフォワーディングをし，さらにDocekrコンテナでもポート番号をオプションで指定するとよいかもしれません（`Docker run`のとき）．


例：
```text:config
Host gpumachine
 HostName gpumachine
 User user
 IdentityFile /Users/user1/.ssh/id_rsa_user1
 ServerAliveInterval 60
 LocalForward   8888 localhost:8000
```
これで，sshで接続したい時は以下のコマンドのみでパスワード入力無しでssh接続できるようになります．
```zsh:terminal（クライアント側）
ssh gpumachine
```
## VS Codeの拡張機能のインストール
以下2つのVS Codeの拡張機能をローカルPCのVS Codeにインストールしておきます．
- 名前: Remote - SSH
ID: ms-vscode-remote.remote-ssh
説明: Open any folder on a remote machine using SSH and take advantage of VS Code's full feature set.
バージョン: 0.84.0
パブリッシャー: Microsoft
VS Marketplace リンク: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh
- 名前: Remote - Containers
ID: ms-vscode-remote.remote-containers
説明: Open any folder or repository inside a Docker container and take advantage of Visual Studio Code's full feature set.
バージョン: 0.251.0
パブリッシャー: Microsoft
VS Marketplace リンク: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers

バージョンは通常の最新版でOKです．

# 4. sshでリモート接続
1. VS Codeを開きRemote-sshがインストールされていることで現れる左下のボタン（>< ←こんなマーク）を押します．
1. 「ホストに接続する」もしくは「現在のウインドウをホストに接続する」をクリックし，hostsファイルに登録しておいたホスト名（例：gpumachine）をクリックすることでGPUマシンへのssh接続が可能になります．接続が完了すると左下に「>< SSH: deepbox」といった接続済みを表す表示があるはずです．

# 5. 実行中のコンテナに入る
## 前提
Dockerfileなどを作っておいて，「Reopen in Container」からコンテナを作成することもできますが，今回はGPUマシンのterminal上でDockerイメージをpullし，runし，カスタマイズした後，docker commitしたDocker Imageをrunさせている状態でVS Codeからコンテナに入るというやり方で進めます（ここの詳細なやり方は[以前書いた記事](https://qiita.com/greentiger/items/7c71708cc108f73707df)を参考にしてください）．
## 方法
1. 4章で開いたssh接続済みのVS Codeウィンドウで左側のメニューバー中の「リモートエクスプローラー」を開き，メニュー内の上部にある「SSH ターゲット」と書いてあるプルダウンメニューをクリックしContainersを選択します．
1. 開きたいコンテナにマウスカーソルを合わせて，右端の方にある「Attach to Container」をクリックすることで新規ウィンドウが開き，コンテナ内に入ることができます．

# 6. 閉じ方
コンテナを開いているVS Codeウィンドウやssh接続中のVS Codeウィンドウを閉じたいときは，画面左下の「><」ボタンから「リモート接続を終了する」を押すことで通信を切断できます．

# 7. Tips
- VS Codeでssh接続＆dockerコンテナを開いている時，VS Codeの拡張機能をインストールすると，ローカル上ではなく，コンテナ環境に新たに拡張機能をインストールすることができます（ローカルPC上のVS Codeにはインストールされないが，リモート接続することで，GPUマシン上に拡張機能がインストールされている状態の環境を操作できる）．
- docker内でformatter（blackなど）をインストールした時，インストールしたのにショートカットキーなどで起動しないことがありますが，VS Codeで設定を開き「リモート」タブを開き，検索バーに「Python」と入力（事前にPythonに拡張機能を入れておく必要あり）
`Python > Formatting: Black Path`の部分にBlackへの完全なパス（docker内のterminalで`which black`を実行するとパスが分かります）を入れて`Python > Formatting: Provider`を`black`にし，保存しておくことで正常に動作するようになります．
- VS Code上でJupyter Notebookを開くためのショートカット：`⌘ + shift + P`→`Create: New Jupyter Notebook`
- LANの外（WAN）からGPUマシンに接続したい場合は，ngrokなどのツールを用いることで，外部から接続できるようになります（詳しくは[以前書いた記事](https://qiita.com/greentiger/items/7c71708cc108f73707df)の4章を参照しhostsファイルとconfigファイルに追記を行ってください)．
- VS Codeでdockerコンテナに入ると，その中で同期したディレクトリを開くことができ，ファイルのやり取りをスムーズにすることができます．
- VS Code上でターミナルを開くショートカット： `Ctrl + @`
- GPUマシンのGPU使用率などの確認： ssh接続しているVS Codeウィンドウ内のターミナルなどで`nvidia-smi`
何度も最新情報を更新したい時： `watch nvidia-smi`（`Ctrl + c`で終了）
※nvidiaのドライバを入れておく必要あり
- GPUマシンのCPU使用状況の確認： ssh接続しているVS Codeウィンドウ内のターミナルなどで`top`（`Ctrl + c`で終了）
- dockerに関する便利なコマンド集は[以前書いた記事](https://qiita.com/greentiger/items/7c71708cc108f73707df)の8章を参照してください．
- おすすめのdocker内のVS Codeのsettings.jsonの設定（blackとflake8はcondaで入れました）
```json:settings.json
{
	"python.formatting.provider": "black",
	"python.linting.pylintEnabled": false,
	"python.linting.flake8Enabled": true,
	"python.linting.enabled": true,
	"python.linting.lintOnSave": true,
	"editor.formatOnSave": true,
	"editor.formatOnType": true,
	"editor.formatOnPaste": false,
	"editor.bracketPairColorization.enabled": true,
	"files.insertFinalNewline": true,
	"files.trimFinalNewlines": true,
	"files.trimTrailingWhitespace": true,
	"notebook.lineNumbers": "on",
	"python.formatting.blackPath": "/root/anaconda3/bin/black",
	"python.linting.flake8Path": "/root/anaconda3/bin/flake8flake8"
}
```

# 8. 最後に
ここまで読んでくださりありがとうございます．
ご質問や修正すべき点などございましたら，コメント欄にお願いします．