コマンド一発でリモートの Jupyter をローカルで開く

(余談: Jupyter Notebook か Jupter Lab か)

自分は最初 Notebook から入って途中 Lab の方がよくない?となったけれど最終的に Notebook でいいやとなった。

その理由は、

  • [Lab の方がよくない?]

    • Lab だと複数の Notebook を開いても1つのブラウザタブで済む
    • リモートシェルを Lab 内で起動できるからローカルのターミナルで ssh する必要がない(ターミナルから解放される)
    • あとは UI が洗練されている
  • [やっぱ Notebook でいいや]

    • 1つの Notebook を1つのタブでそれぞれ開くことは自分にとっては苦ではなかった(むしろブラウザのタブ移動コマンドで Notebook を移動できるので楽)
    • なんだかんだローカルのターミナルの方が操作しやすい。Alacritty + tmux で OK。あと Lab のターミナルは emacs 開いたりするとなぜか全部の文字に下線が引かれたりする(ls すると直る。なぜだ)
    • UI よりも使いやすさ(別に Notebook も UI 悪くない)。Lab はデフォルトのフォントが等幅フォントなのがとても良いが、別に Notebook でも custom.css をいじれば変えられる
    • そして何よりも大事なのが、Notebook の方が圧倒的に動作が軽い。これが一番の決め手で、Plotly とかで大きいデータの図を書くと Lab は途端に動作がものすごく重くなって、使い物にならなかった

なので以下では Jupyter Notebook を想定しているが、基本的に jupyter notebookjupyter lab に変えれば同じはず。

コマンド一発じゃないバージョンだとどうなるか

少なくとも以下の手順を踏む必要がある。

  1. リモートで Jupyter を実行
  2. ローカルでポートフォワードする ssh を実行
  3. ブラウザでローカルホストの URL を開く

具体的には以下の通り。

まずはリモートでポートフォワードを含めた諸々の Jupyter Notebook の設定。

Running a notebook server — Jupyter Notebook 6.0.0.dev0 documentation

後は(最初の方は上と被っているが)以下を参考にして、

coderwall.com

次のような流れで Jupyter をローカルのブラウザ上で実行することができる。

$ ssh ホスト名
> jupyter notebook
(別のターミナルウィンドウやタブを開く)
$ ssh -N -f -L ローカルでのポート番号:localhost:リモートのJUPYTERで設定したポート番号 ホスト名
(ブラウザを開いて "http://localhost:ローカルでのポート番号" にアクセス)

これを Jupyter Notebook を開くために毎回実行するのはすごくめんどくさい。ターミナルのタブも2つ使うし。

シェルスクリプトにまとめてコマンド一発で開く

結論から述べると、以下のようなスクリプト(remote_jupyter.sh とする)を書く (Mac 環境)。コードの中で日本語になっている箇所は適宜自分のものに書き換えることに注意。

HOST_NAME=${1:-任意のSSHホスト名}
PORT_ID_LOCAL=${2:-任意のローカルポート番号}
PORT_ID_REMOTE=${3:-上で指定したリモートJupyterのポート番号}

# 指定されたリモートホストで既に Jupyter が動いているか確認して、無かった場合にだけ起動する
N_JUPYTER_LAB_PROC=$(ssh ${HOST_NAME} "ps x" | grep "jupyter" | wc -l)
if [ ${N_JUPYTER_LAB_PROC} = 0 ]
then
    ssh ${HOST_NAME} "source ~/.bash_profile; nohup jupyter notebook >/dev/null 2>&1 &"
fi

# SSHでポートフォワードして、接続後に chrome のタブを開く
ssh -N \
    -f \
    -L localhost:${PORT_ID_LOCAL}:localhost:${PORT_ID_REMOTE} \
    -o PermitLocalCommand=yes \
    -o LocalCommand="open -a '/Applications/Google Chrome.app' http://localhost:${PORT_ID_LOCAL}" \
    ${HOST_NAME}

アピールポイントとしては、

  1. Jupyter を実行したいリモート先(とローカルで使用するポート番号)を指定できる(SSHホスト名は ~/.ssh/configHostNameUser などが指定されている必要がある)
  2. 最初に既に存在する Jupyter プロセスを確認することで、余剰プロセスを起動することを防げる
  3. 自動でブラウザのタブを開いてくれる

具体的な使い方は、上のスクリプトを記述したファイルを

$ bash script_file.sh [任意のSSHホスト名] [任意のローカルポート番号] [上で指定したリモートJupyterのポート番号]

のように(必要に応じて引数を指定して)実行するだけ。

ただし、一度接続した ssh はターミナルを閉じたりしても生き残り続ける(インターネット接続が切れたりすると死ぬ)ので、ssh プロセスが生きている限りはブラウザのタブを閉じてもアクセスし直すだけで大丈夫。なのでローカルホストの URL (http://localhost:ポート番号) をブックマークしておくと便利。ssh が死んだ時(これは URL を開いた時の表示で分かる)にだけ再度スクリプトを実行すれば良い。

リモートの Jupter を SGE や SLURM のようなスケジューラの上で走らせたい場合

共用のクラスターマシンとかだとコマンドを直走りさせるのは嫌われる行為なので、qsubsbatch を使って Jupyter Notebook を実行したい。

SGE だと普通に、

#!/bin/bash
#$ -N jupyter_nb
#$ -o sge.log
#$ -j y
#$ -S /bin/bash
#$ -cwd
#$ -V
#$ -q ノード名
#$ -pe smp 4

jupyter notebook

のようなスクリプト(run_jupyter.sh という名前でホームディレクトリに置いておく)を書いて qsub で投げるだけで良いので、上の remote_jupyter.sh

nohup jupyter notebook >/dev/null 2>&1 &

の部分を

qsub run_jupyter.sh

に変更するだけで対応できる。

一方で SLURM だとそもそも Jupyter Notebook を起動する際に PermissionError: [Errno 13] Permission denied: '/run/user/XXXX のようなエラーが出る。さらに、リモートの localhost で Jupyter Notebook を起動するとなぜかローカルから Port Forwarding できなくなってしまう。

これらの問題を回避するために、

#!/bin/bash
#SBATCH -p ノード名
#SBATCH -J jupyter_nb
#SBATCH -o sbatch_stdout
#SBATCH -e sbatch_stderr
#SBATCH -n 1
#SBATCH -N 1
#SBATCH -c 4
#SBATCH -t 24:00:00

jupyter notebook --ip=0.0.0.0

のようなスクリプト (run_jupyter.sh; SGE と同じくホームディレクトリに置いておく) を用意して、上の remote_jupyter.sh

nohup jupyter notebook >/dev/null 2>&1 &

の部分を

unset XDG_RUNTIME_DIR; sbatch run_jupyter.sh

に変更する。

Automatorシェルスクリプト実行をアプリケーション化する

自分はわざわざターミナルを開いてシェルスクリプトを実行するのも面倒なので、MacAutomatorシェルスクリプトを実行するアプリケーションを作って Dock に置いている。

具体的には、

nohup bash remote_jupyter.sh > /dev/null 2>&1 &

というシェルスクリプトを実行する Application を作って Dock に置いている。

github.com

記事中のスクリプトは↑のレポジトリにも置いてある。