0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ubuntu + Docker + JupyterHubでマルチユーザーGPU環境を構築する【RTX PRO 6000 Blackwell】

0
Posted at

Ubuntu + Docker + JupyterHubでマルチユーザーGPU環境を構築する【RTX PRO 6000 Blackwell】

はじめに

複数のユーザーがWebブラウザからGPUサーバーを利用できる環境として,JupyterHub + Dockerを用いたGPU共有環境を構築した.

本環境では,ユーザーがWebブラウザからJupyterHubへログインすると,DockerSpawnerによってユーザー専用のJupyterLabコンテナが起動する.

各ユーザーのコンテナからNVIDIA GPUを利用できるようにすることで,TensorFlowやPyTorchを用いた機械学習・深層学習をJupyterLab上で実行可能な環境とした.

主な構成は以下のとおりである.

  • Dockerによるユーザー環境の分離
  • DockerSpawnerによるユーザーコンテナの自動生成
  • NVIDIA GPUの利用
  • ユーザーごとのファイルの永続化
  • NativeAuthenticatorによるユーザー登録・管理者承認
  • nginxによるリバースプロキシ
  • Let's EncryptによるHTTPS化
  • JupyterLabの日本語化
  • TensorFlow / PyTorchのGPU対応

本記事では,実際に構築した環境をもとに,構築手順と構築時に遭遇した問題,その対処方法についてまとめる.

ドメイン名やIPアドレスなど,環境固有の情報はサンプル用の値に置き換えている.


構築環境

今回使用したサーバーの主な構成を以下に示す.

項目 構成
OS Ubuntu 24.04 LTS
CPU Intel Xeon Silver 4514Y ×2
CPUコア数 32コア
メモリ 1TB DDR5 ECC RDIMM
GPU NVIDIA RTX PRO 6000 Blackwell Max-Q ×4
GPUメモリ 96GB ×4
ストレージ NVMe SSD 3.84TB ×2
NVIDIA Driver 590.48.01
JupyterHub 5.3.0
DockerSpawner 14.0.0
NativeAuthenticator 1.3.0
JupyterLab 4.6.3
TensorFlow 2.21.0
PyTorch 2.12.1 + CUDA 13.0
torchvision 0.27.1 + CUDA 13.0

各バージョンは構築時点のものである.


システム構成

構築した環境の全体像は以下のとおりである.

                       Webブラウザ
                            │
                            │ HTTPS :443
                            ▼
                      ┌───────────┐
                      │   nginx   │
                      │  TLS終端  │
                      └─────┬─────┘
                            │
                        /jupyter/
                            │
                            ▼
                   ┌─────────────────┐
                   │   JupyterHub    │
                   │        +        │
                   │  DockerSpawner  │
                   └────────┬────────┘
                            │
               ┌────────────┼────────────┐
               ▼            ▼            ▼
        ┌────────────┐ ┌────────────┐ ┌────────────┐
        │   User A   │ │   User B   │ │   User C   │
        │ JupyterLab │ │ JupyterLab │ │ JupyterLab │
        └─────┬──────┘ └─────┬──────┘ └─────┬──────┘
              │              │              │
              └──────────────┼──────────────┘
                             ▼
                RTX PRO 6000 Blackwell ×4

JupyterHub本体と,各ユーザーが利用するJupyterLabは,別々のDockerコンテナとして動作する.

ユーザーがJupyterHubへログインしてサーバーを起動すると,DockerSpawnerによってユーザーごとのコンテナが生成される.

jupyter-user01
jupyter-user02
jupyter-user03

各ユーザーの作業ファイルはDocker Volumeに保存する.これにより,ユーザーコンテナを停止・再作成した場合でも,Notebookやデータを保持できる構成としている.


ディレクトリ構成

JupyterHub関連のファイルは,/opt/jupyterhub以下に配置する.

/opt/jupyterhub/
├── docker-compose.yml
├── .env
├── hub/
│   ├── Dockerfile
│   └── jupyterhub_config.py
├── singleuser/
│   └── Dockerfile
└── data/
    └── hub/

また,nginxはJupyterHubとは独立したDocker Composeプロジェクトとして構築する.

/opt/reverse-proxy/
├── docker-compose.yml
├── nginx/
│   └── conf.d/
├── certbot/
│   ├── conf/
│   └── www/
└── logs/

JupyterHubとnginxを分離することで,JupyterHubのイメージ更新や設定変更を行う場合でも,リバースプロキシを独立して管理できる構成とした.


1.NVIDIA GPUの動作確認

まず,UbuntuホストからGPUが正常に認識されていることを確認する.

nvidia-smi

今回の環境では,NVIDIA RTX PRO 6000 Blackwell Max-Qが4枚認識されている.

NVIDIAドライバのバージョンについても確認する.

cat /proc/driver/nvidia/version

本環境では,NVIDIA Driver 590.48.01を使用している.

JupyterHubやDockerの設定を開始する前に,この段階でホストOSからすべてのGPUが正常に認識されていることを確認しておくことが重要である.


2.DockerからGPUを利用できることを確認

各ユーザーのJupyterLabはDockerコンテナとして動作するため,DockerコンテナからNVIDIA GPUへアクセスできる必要がある.

そのため,NVIDIA Container Toolkitを導入する.

導入後,Dockerコンテナからnvidia-smiを実行してGPUが認識されることを確認する.

docker run --rm --gpus all \
  nvidia/cuda:13.0.0-base-ubuntu24.04 \
  nvidia-smi

ホストOSでnvidia-smiを実行した場合と同様に,GPUの一覧が表示されれば正常である.

ここでGPUが認識されない場合は,JupyterHubの構築へ進む前に,NVIDIA DriverおよびNVIDIA Container Toolkitの設定を確認した方が問題を切り分けやすい.


3.Dockerネットワークを作成する

nginxとJupyterHubは,別々のDocker Composeプロジェクトとして動作させる.

そのため,コンテナ間の通信に使用するDockerネットワークをあらかじめ作成する.

sudo docker network create proxy-network
sudo docker network create jupyterhub-network

それぞれのネットワークの役割は以下のとおりである.

ネットワーク 用途
proxy-network nginx ⇔ JupyterHub
jupyterhub-network JupyterHub ⇔ 各ユーザーのJupyterLab

proxy-networkにはnginxとJupyterHubを参加させ,jupyterhub-networkにはJupyterHubとDockerSpawnerによって生成されるユーザーコンテナを参加させる.

これにより,JupyterHubのポートを直接外部へ公開せず,nginxを経由してHTTPSでアクセスする構成とする.


4.JupyterHub用Dockerイメージを作成する

まず,JupyterHub本体を動作させるDockerイメージを作成する.

/opt/jupyterhub/hub/Dockerfileを作成する.

FROM jupyterhub/jupyterhub:5.3.0

USER root

RUN pip install --no-cache-dir \
    dockerspawner==14.0.0 \
    jupyterhub-nativeauthenticator==1.3.0 \
    jupyterhub-idle-culler

WORKDIR /srv/jupyterhub

CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"]

ここでは,JupyterHubに加えて,主に以下のパッケージを使用する.

パッケージ 用途
DockerSpawner ユーザーごとにDockerコンテナを起動する
NativeAuthenticator ユーザー登録および認証を行う
jupyterhub-idle-culler 一定時間使用されていないユーザーサーバーを停止する

DockerSpawnerを利用することで,JupyterHub本体とユーザーが実際に操作するJupyterLab環境を分離できる.


5.GPU対応JupyterLabイメージを作成する

次に,各ユーザーが実際に使用するJupyterLab環境を作成する.

今回は,Jupyter公式のTensorFlow Notebookイメージをベースとして使用した.

FROM quay.io/jupyter/tensorflow-notebook:<使用するタグまたはdigest>

実運用では,ベースイメージが意図せず更新されることを防ぐため,タグだけではなくdigestを固定しておく方法も有効である.

必要に応じて,データ分析や機械学習で使用するライブラリを追加する.

RUN pip install --no-cache-dir \
    pandas \
    scipy \
    scikit-learn \
    matplotlib \
    seaborn

今回の環境では,TensorFlowだけでなくPyTorchについてもGPUを利用できるようにした.


Blackwell GPUとPyTorchで発生した問題

今回の構築で特に注意が必要であったのが,Blackwell GPUとPyTorchの組み合わせである.

当初は,以下のPyTorch環境を使用していた.

PyTorch 2.12.1+cu126
CUDA 12.6

この状態でも,

import torch

print(torch.cuda.is_available())
print(torch.cuda.get_device_name(0))

を実行すると,GPU自体は正常に認識された.

しかし,実際にGPU上へテンソルを作成すると,

a = torch.randn(3000, 3000, device="cuda")

以下のエラーが発生した.

CUDA error: no kernel image is available for execution on the device

そこで,GPUのCompute Capabilityと,PyTorchが対応しているアーキテクチャを確認した.

import torch

print(torch.cuda.get_device_capability(0))
print(torch.cuda.get_arch_list())

GPUのCompute Capabilityは,

(12, 0)

であった.

一方,当初使用していたPyTorchでは,sm_120向けのCUDAカーネルが含まれていなかった.

そのため,CUDA 13.0版のPyTorchへ変更した.

RUN pip install --no-cache-dir \
    torch==2.12.1 \
    torchvision==0.27.1 \
    --index-url https://download.pytorch.org/whl/cu130

変更後は,

PyTorch 2.12.1+cu130
CUDA 13.0

となり,Blackwell GPU上で実際の計算を実行できるようになった.

torch.cuda.is_available()Trueであっても,実際のCUDAカーネルが対象GPUのCompute Capabilityに対応しているとは限らない.新しいGPUを使用する場合は,GPUを認識できるかだけではなく,実際にテンソル演算まで実行して確認することが重要である.

6.Docker ComposeでJupyterHubを起動する

JupyterHub本体はDocker Composeを使用して管理する.

/opt/jupyterhub/docker-compose.ymlを以下のように作成する.

services:
  jupyterhub:
    build:
      context: ./hub
    image: my-jupyterhub:1.0
    container_name: jupyterhub
    hostname: jupyterhub
    restart: unless-stopped

    expose:
      - "8000"
      - "8081"

    volumes:
      - ./hub/jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro
      - ./data/hub:/srv/jupyterhub/data
      - /var/run/docker.sock:/var/run/docker.sock

    environment:
      JUPYTERHUB_ADMIN: ${JUPYTERHUB_ADMIN}
      DOCKER_NOTEBOOK_IMAGE: ${DOCKER_NOTEBOOK_IMAGE}

    networks:
      - proxy-network
      - jupyterhub-network

    logging:
      driver: json-file
      options:
        max-size: "100m"
        max-file: "5"

networks:
  proxy-network:
    external: true

  jupyterhub-network:
    external: true

ここでは,ホスト側へ8000番ポートを直接公開するのではなく,exposeのみを指定している.

外部からJupyterHubへ直接アクセスさせず,同じproxy-networkに接続されたnginxからアクセスするためである.

また,JupyterHubコンテナは2つのDockerネットワークに参加させる.

proxy-network
    nginx
      │
      └── JupyterHub

jupyterhub-network
    JupyterHub
      │
      ├── User A
      ├── User B
      └── User C

proxy-networkはnginxとの通信に使用し,jupyterhub-networkはDockerSpawnerによって生成されるユーザーコンテナとの通信に使用する.


Docker Socketについて

DockerSpawnerは,JupyterHubからDockerコンテナを生成する必要がある.

そのため,以下の設定によってホストのDocker SocketをJupyterHubコンテナへマウントしている.

volumes:
  - /var/run/docker.sock:/var/run/docker.sock

これにより,JupyterHubコンテナからホストのDocker Engineを操作し,ユーザーごとのJupyterLabコンテナを生成できる.

/var/run/docker.sockへのアクセスは,実質的にホスト上のDockerを操作できる強い権限を与えることになる.そのため,JupyterHubコンテナ自体へのアクセス権限や,管理者権限の付与には注意が必要である.


環境変数を設定する

管理者ユーザー名と,ユーザー用JupyterLabイメージ名は.envで管理する.

/opt/jupyterhub/.envを作成する.

JUPYTERHUB_ADMIN=admin
DOCKER_NOTEBOOK_IMAGE=jupyterhub-ml:1.0

ここでは,管理者ユーザー名をadminとしている.

実際の環境では任意のユーザー名へ変更する.


7.JupyterHubを設定する

次に,JupyterHubの設定ファイルであるjupyterhub_config.pyを作成する.

今回の構成では,主に以下を設定する.

  • /jupyter/配下でJupyterHubを公開する
  • DockerSpawnerを使用する
  • ユーザーごとにJupyterLabコンテナを生成する
  • ユーザーごとのファイルをDocker Volumeへ保存する
  • GPUをユーザーコンテナから利用可能にする
  • NativeAuthenticatorを使用する
  • ユーザーごとのCPU・メモリ上限を設定する

/opt/jupyterhub/hub/jupyterhub_config.pyを作成する.

import os

c = get_config()

# --------------------------------------------------
# JupyterHub基本設定
# --------------------------------------------------

c.JupyterHub.base_url = "/jupyter/"
c.JupyterHub.bind_url = "http://0.0.0.0:8000/jupyter/"

c.JupyterHub.hub_ip = "0.0.0.0"
c.JupyterHub.hub_connect_ip = "jupyterhub"
c.JupyterHub.hub_port = 8081

# SQLiteデータベース
c.JupyterHub.db_url = \
    "sqlite:////srv/jupyterhub/data/jupyterhub.sqlite"


# --------------------------------------------------
# DockerSpawner
# --------------------------------------------------

c.JupyterHub.spawner_class = "dockerspawner.DockerSpawner"

c.DockerSpawner.image = os.environ.get(
    "DOCKER_NOTEBOOK_IMAGE",
    "jupyterhub-ml:1.0"
)

c.DockerSpawner.network_name = "jupyterhub-network"

c.DockerSpawner.name_template = "jupyter-{username}"

c.Spawner.default_url = "/lab"


# --------------------------------------------------
# ユーザーデータの保存
# --------------------------------------------------

c.DockerSpawner.notebook_dir = "/home/jovyan/work"

c.DockerSpawner.volumes = {
    "jupyterhub-user-{username}": "/home/jovyan/work"
}

c.DockerSpawner.remove = True


# --------------------------------------------------
# リソース制限
# --------------------------------------------------

c.Spawner.cpu_limit = 8
c.Spawner.mem_limit = "128G"

c.Spawner.start_timeout = 300
c.Spawner.http_timeout = 300


# --------------------------------------------------
# GPU
# --------------------------------------------------

c.DockerSpawner.extra_host_config = {
    "device_requests": [
        {
            "Driver": "nvidia",
            "Count": -1,
            "Capabilities": [["gpu"]],
        }
    ],
    "shm_size": "32G",
}

c.DockerSpawner.environment = {
    "TF_FORCE_GPU_ALLOW_GROWTH": "true",
    "PYTHONUNBUFFERED": "1",
}


# --------------------------------------------------
# NativeAuthenticator
# --------------------------------------------------

c.JupyterHub.authenticator_class = \
    "nativeauthenticator.NativeAuthenticator"

admin_user = os.environ.get("JUPYTERHUB_ADMIN", "admin")

c.Authenticator.admin_users = {admin_user}

c.NativeAuthenticator.open_signup = True
c.NativeAuthenticator.ask_email_on_signup = False

CPU数やメモリ上限はサーバーの構成や利用人数に応じて変更する必要がある.上記の8 CPU / 128GBは一例である.


8.ユーザーごとにDockerコンテナを生成する

今回の構成では,SpawnerとしてDockerSpawnerを使用する.

c.JupyterHub.spawner_class = "dockerspawner.DockerSpawner"

ユーザーがJupyterHub上でサーバーを起動すると,指定したDockerイメージを使用してJupyterLabコンテナが生成される.

c.DockerSpawner.image = os.environ.get(
    "DOCKER_NOTEBOOK_IMAGE",
    "jupyterhub-ml:1.0"
)

コンテナ名は以下の設定によって決定する.

c.DockerSpawner.name_template = "jupyter-{username}"

例えば,user01というユーザーがサーバーを起動した場合は,以下のようなコンテナが生成される.

jupyter-user01

実際に起動しているコンテナは,ホスト側から確認できる.

sudo docker ps

複数のユーザーが利用している場合は,以下のような構成となる.

JupyterHub
   │
   ├── jupyter-user01
   ├── jupyter-user02
   └── jupyter-user03

各ユーザーは独立したJupyterLabコンテナを使用するため,あるユーザーがインストールしたPythonパッケージや実行中のプロセスが,別のユーザーのコンテナへ直接影響することを防ぎやすい構成となる.


9.ユーザーデータを永続化する

DockerSpawnerによって生成されるユーザーコンテナは,一時的なコンテナとして扱う.

今回の設定では,

c.DockerSpawner.remove = True

としているため,ユーザーサーバーを停止するとコンテナ自体は削除される.

そのままではNotebookやデータも失われてしまうため,/home/jovyan/workをDocker Volumeへ保存する.

c.DockerSpawner.notebook_dir = "/home/jovyan/work"

c.DockerSpawner.volumes = {
    "jupyterhub-user-{username}": "/home/jovyan/work"
}

例えば,user01の場合は,

jupyterhub-user-user01

というDocker Volumeが作成される.

Volumeの一覧は以下のコマンドで確認できる.

sudo docker volume ls

これにより,

ユーザーサーバー起動
        ↓
JupyterLabコンテナ生成
        ↓
/home/jovyan/work
        ↓
ユーザー専用Docker Volume
        ↓
ユーザーサーバー停止
        ↓
コンテナ削除
        ↓
Volumeは残る
        ↓
次回起動時に再マウント

という動作になる.

永続化されるのは,ここでVolumeとして指定した/home/jovyan/workである.コンテナ内の他の場所に保存したファイルは,コンテナの削除とともに失われる可能性がある.


pipで追加したライブラリにも注意する

この構成ではユーザーコンテナ自体が削除されるため,ユーザーが以下のように追加したPythonパッケージについても注意が必要である.

%pip install transformers

Pythonパッケージのインストール先が永続化対象外であれば,ユーザーサーバーを停止してコンテナが再作成された際に,追加したパッケージは失われる.

そのため,Notebookの先頭に必要なパッケージのインストールセルを記述しておく方法も有効である.

%pip install transformers==<version>

これにより,コンテナが再作成された場合でも,Notebookを上から実行することで必要な環境を再現しやすくなる.

一方,TensorFlow,PyTorch,CUDA関連パッケージなど,GPU環境との依存関係が大きいパッケージについては,各ユーザーが個別に変更するのではなく,Dockerイメージ側でバージョンを管理する方が安全である.


10.GPUをユーザーコンテナへ公開する

DockerSpawnerから生成されるJupyterLabコンテナでGPUを使用するため,device_requestsを設定する.

c.DockerSpawner.extra_host_config = {
    "device_requests": [
        {
            "Driver": "nvidia",
            "Count": -1,
            "Capabilities": [["gpu"]],
        }
    ],
    "shm_size": "32G",
}

Count: -1を指定することで,ホストに搭載されているすべてのGPUをユーザーコンテナから認識できるようにしている.

例えば,4枚のGPUを搭載したサーバーで,

nvidia-smi

をJupyterLabのTerminalから実行すると,GPU 0~3が表示される.

今回の構成では,GPUをユーザーごとに自動割り当てするのではなく,利用者が空いているGPUを確認して使用する方式とした.

利用状況は以下で確認できる.

nvidia-smi

使用するGPUを指定する場合は,例えばGPU 2を使用するなら,

import os

os.environ["CUDA_VISIBLE_DEVICES"] = "2"

のように指定する.

Terminalから実行する場合は,

export CUDA_VISIBLE_DEVICES=2

とすることもできる.

CUDA_VISIBLE_DEVICESは,TensorFlowやPyTorchなどのGPUライブラリを読み込む前に設定する必要がある.


TensorFlowのGPUメモリを必要に応じて確保する

TensorFlowは設定によってはGPUメモリを大きく確保することがある.

複数ユーザーでGPUを共有することを考慮し,以下をユーザーコンテナの環境変数として設定した.

c.DockerSpawner.environment = {
    "TF_FORCE_GPU_ALLOW_GROWTH": "true",
    "PYTHONUNBUFFERED": "1",
}

TF_FORCE_GPU_ALLOW_GROWTH=trueを設定することで,TensorFlowが必要に応じてGPUメモリを確保する動作とする.

ただし,この設定だけでGPU利用の競合を防げるわけではないため,複数ユーザーでGPUを共有する場合は,利用者間でGPUの使用状況を確認する運用も必要となる.

11.NativeAuthenticatorでユーザー登録を行う

複数ユーザーでJupyterHubを利用するため,認証にはNativeAuthenticatorを使用する.

NativeAuthenticatorを利用すると,JupyterHub上にユーザー登録画面を用意できる.また,登録されたユーザーを管理者が確認し,承認してから利用を許可する運用が可能である.

jupyterhub_config.pyでは,Authenticatorを以下のように設定する.

c.JupyterHub.authenticator_class = \
    "nativeauthenticator.NativeAuthenticator"

admin_user = os.environ.get(
    "JUPYTERHUB_ADMIN",
    "admin"
)

c.Authenticator.admin_users = {admin_user}

c.NativeAuthenticator.open_signup = True
c.NativeAuthenticator.ask_email_on_signup = False

open_signup = Trueとすることで,ユーザー自身によるアカウント登録を許可する.

ただし,アカウントを登録しただけではJupyterHubを利用できるようにせず,管理者が承認する運用とする.

ユーザー登録から利用開始までの流れは以下のようになる.

ユーザー
   │
   ▼
アカウント登録
   │
   ▼
管理者の承認待ち
   │
   ▼
管理者がユーザーを承認
   │
   ▼
ログイン
   │
   ▼
JupyterLab起動

これにより,不特定のユーザーがアカウントを作成し,そのままGPUサーバーを利用することを防ぐことができる.


管理者ユーザーを設定する

管理者ユーザー名は,.envに設定した環境変数から取得する.

JUPYTERHUB_ADMIN=admin

jupyterhub_config.pyでは以下のように読み込む.

admin_user = os.environ.get(
    "JUPYTERHUB_ADMIN",
    "admin"
)

c.Authenticator.admin_users = {admin_user}

管理者としてログインすると,JupyterHubの管理画面からユーザーを確認できる.

NativeAuthenticatorの承認画面から,登録されたユーザーを確認し,利用を許可するユーザーを承認する.

管理者アカウントには一般ユーザーよりも強い権限が与えられるため,通常利用するアカウントと管理者アカウントを分けることも検討した方がよい.


12.JupyterHubを起動する

ここまで設定できたら,JupyterHub用イメージとユーザー用JupyterLabイメージをビルドする.

まず,ユーザー用JupyterLabイメージを作成する.

cd /opt/jupyterhub

sudo docker build \
  -t jupyterhub-ml:1.0 \
  ./singleuser

続いて,JupyterHub本体をビルドする.

sudo docker compose build

JupyterHubを起動する.

sudo docker compose up -d

起動状態を確認する.

sudo docker ps

JupyterHubコンテナが起動していれば,ログを確認する.

sudo docker compose logs -f jupyterhub

正常に起動すると,JupyterHubが8000番ポートで待ち受ける.

ただし,今回の構成ではportsではなくexposeを使用しているため,ホストの8000番ポートを外部へ直接公開していない.

外部からのアクセスは,次に構築するnginxを経由させる.


13.nginxをリバースプロキシとして構築する

JupyterHubをインターネットから直接公開するのではなく,nginxをリバースプロキシとして配置する.

構成は以下のようになる.

Webブラウザ
     │
     │ HTTPS :443
     ▼
┌─────────┐
│  nginx  │
└────┬────┘
     │
     │ HTTP
     ▼
┌────────────┐
│ JupyterHub │
│    :8000   │
└────────────┘

nginxでは以下を担当する.

  • HTTPS通信の終端
  • JupyterHubへのリバースプロキシ
  • HTTPからHTTPSへのリダイレクト
  • Let's Encryptの証明書利用

nginxもDockerコンテナとして動作させる.


nginx用ディレクトリを作成する

以下のディレクトリを作成する.

sudo mkdir -p /opt/reverse-proxy/nginx/conf.d
sudo mkdir -p /opt/reverse-proxy/certbot/conf
sudo mkdir -p /opt/reverse-proxy/certbot/www
sudo mkdir -p /opt/reverse-proxy/logs

構成は以下のようになる.

/opt/reverse-proxy/
├── docker-compose.yml
├── nginx/
│   └── conf.d/
│       └── default.conf
├── certbot/
│   ├── conf/
│   └── www/
└── logs/

14.nginx用Docker Composeを作成する

/opt/reverse-proxy/docker-compose.ymlを作成する.

services:
  nginx:
    image: nginx:1.28
    container_name: reverse-proxy
    restart: unless-stopped

    ports:
      - "80:80"
      - "443:443"

    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
      - ./logs:/var/log/nginx

    networks:
      - proxy-network

networks:
  proxy-network:
    external: true

外部へ公開するポートは,HTTPの80番とHTTPSの443番のみとする.

JupyterHubの8000番ポートは外部へ公開せず,Dockerネットワーク内部でnginxからアクセスする.


15.Let's EncryptでSSL証明書を取得する

HTTPSでJupyterHubを公開するため,Let's Encryptを使用してSSL/TLS証明書を取得する.

以下では,公開するドメインを例として,

gpu.example.com

とする.

実際に証明書を取得するには,使用するドメインのDNSが対象サーバーのグローバルIPアドレスを参照している必要がある.また,外部からTCP 80番ポートへアクセスできる必要がある.

まず,証明書取得時に使用するWebrootへアクセスできるよう,nginxをHTTPのみで起動する.

/opt/reverse-proxy/nginx/conf.d/default.confを一時的に以下のようにする.

server {
    listen 80;
    server_name gpu.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 200 "nginx is running\n";
        add_header Content-Type text/plain;
    }
}

nginxを起動する.

cd /opt/reverse-proxy

sudo docker compose up -d

設定ファイルに問題がないことを確認する.

sudo docker compose exec nginx nginx -t

Certbotで証明書を取得する

Certbotは,一時的なDockerコンテナとして実行できる.

sudo docker run --rm \
  -v /opt/reverse-proxy/certbot/conf:/etc/letsencrypt \
  -v /opt/reverse-proxy/certbot/www:/var/www/certbot \
  certbot/certbot certonly \
  --webroot \
  --webroot-path=/var/www/certbot \
  --email your-email@example.com \
  --agree-tos \
  --no-eff-email \
  -d gpu.example.com

証明書の取得に成功すると,以下のディレクトリに証明書が作成される.

/opt/reverse-proxy/certbot/conf/live/gpu.example.com/

主に使用するファイルは以下である.

fullchain.pem
privkey.pem

gpu.example.comおよびメールアドレスは,実際に使用するものへ変更する必要がある.


16.nginxをHTTPS対応にする

証明書を取得したら,default.confをHTTPS対応の設定へ変更する.

server {
    listen 80;
    server_name gpu.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name gpu.example.com;

    ssl_certificate \
        /etc/letsencrypt/live/gpu.example.com/fullchain.pem;

    ssl_certificate_key \
        /etc/letsencrypt/live/gpu.example.com/privkey.pem;

    location /jupyter/ {
        proxy_pass http://jupyterhub:8000;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For \
            $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_read_timeout 86400;
        proxy_send_timeout 86400;

        client_max_body_size 100G;
    }
}

ここで重要なのが,以下の設定である.

location /jupyter/ {
    proxy_pass http://jupyterhub:8000;
}

nginxとJupyterHubは同じproxy-networkに接続しているため,DockerのDNSによってコンテナ名jupyterhubを使用してアクセスできる.

そのため,JupyterHubのポートをホスト側へ公開する必要はない.


WebSocketを有効にする

JupyterLabでは,Kernelとの通信などにWebSocketが使用される.

そのため,以下のヘッダーをnginxから転送する.

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

これらが正しく設定されていない場合,JupyterLab自体は表示できても,Kernelとの接続などで問題が発生する可能性がある.


大きなファイルをアップロードできるようにする

JupyterLabでは,データセットなど比較的大きなファイルをアップロードする場合がある.

そのため,nginx側でアップロードサイズの上限を変更する.

client_max_body_size 100G;

この値は利用環境に応じて変更する.


nginxの設定を反映する

設定ファイルを変更したら,最初に構文を確認する.

cd /opt/reverse-proxy

sudo docker compose exec nginx nginx -t

以下のように表示されれば,構文上の問題はない.

syntax is ok
test is successful

問題がなければ,nginxの設定を再読み込みする.

sudo docker compose exec nginx nginx -s reload

設定変更のたびにコンテナを停止・再作成する必要はない.nginx -tで設定を確認した後,nginx -s reloadで設定を反映する方が安全である.


17.JupyterHubをサブパスに対応させる

今回は,JupyterHubをドメイン直下ではなく,

https://gpu.example.com/jupyter/

で公開する.

そのため,jupyterhub_config.pyで以下を設定する.

c.JupyterHub.base_url = "/jupyter/"
c.JupyterHub.bind_url = "http://0.0.0.0:8000/jupyter/"

また,外部からアクセスするURLを明示する場合は,以下のように設定する.

c.JupyterHub.public_url = \
    "https://gpu.example.com/jupyter/"

これにより,JupyterHubが生成するログイン画面やユーザーサーバーへのURLにも/jupyter/が反映される.

最終的なアクセスURLは以下となる.

https://gpu.example.com/jupyter/

18.動作確認

ここまで設定できたら,WebブラウザからJupyterHubへアクセスする.

https://gpu.example.com/jupyter/

初めて利用するユーザーはアカウントを登録する.

管理者がユーザーを承認した後,ユーザーがログインするとJupyterLabを起動できる.

ユーザーサーバーを起動した状態で,ホスト側からDockerコンテナを確認する.

sudo docker ps

例えば,user01がログインしている場合は,以下のようなコンテナが確認できる.

jupyterhub
reverse-proxy
jupyter-user01

この状態になれば,JupyterHubからDockerSpawnerを経由してユーザー用JupyterLabコンテナが正常に生成されている.

19.JupyterLabからGPUを確認する

ユーザー用JupyterLabコンテナが正常に起動したら,JupyterLabのTerminalからGPUが認識されていることを確認する.

nvidia-smi

今回の構成では,すべてのGPUをユーザーコンテナへ公開しているため,4枚のGPUが表示される.

GPU 0  NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition
GPU 1  NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition
GPU 2  NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition
GPU 3  NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition

ここでGPU名だけでなく,GPUメモリの使用量やGPU使用率も確認できる.

複数ユーザーでGPUを共有する場合は,プログラムを実行する前にnvidia-smiを確認し,使用するGPUを決める運用とした.


20.使用するGPUを指定する

今回の環境では,DockerSpawnerによるGPUの自動割り当ては行わず,各ユーザーが使用するGPUを指定する方式としている.

例えば,GPU 2を使用する場合は,Pythonプログラムの先頭で以下のように指定する.

import os

os.environ["CUDA_VISIBLE_DEVICES"] = "2"

この設定を行うと,TensorFlowやPyTorchからは,指定したGPUのみが利用可能なGPUとして認識される.

CUDA_VISIBLE_DEVICESは,TensorFlowやPyTorchをimportする前に設定する必要がある.

例えば,以下の順序で記述する.

import os

os.environ["CUDA_VISIBLE_DEVICES"] = "2"

import tensorflow as tf

PyTorchの場合も同様である.

import os

os.environ["CUDA_VISIBLE_DEVICES"] = "2"

import torch

Terminalで指定してからJupyterLabやPythonプログラムを実行する場合は,以下のように設定することもできる.

export CUDA_VISIBLE_DEVICES=2

CUDA_VISIBLE_DEVICES指定後のGPU番号に注意する

例えば,

os.environ["CUDA_VISIBLE_DEVICES"] = "2"

と指定した場合,物理GPU 2だけがプログラムから見える状態となる.

ただし,TensorFlowやPyTorchから見たGPU番号は,通常GPU 0となる.

つまり,

物理GPU             GPU 2
                      ↓
CUDA_VISIBLE_DEVICES="2"
                      ↓
プログラムから見えるGPU 0

という対応になる.

そのため,CUDA_VISIBLE_DEVICES="2"を指定した後にPyTorchで,

torch.cuda.get_device_name(0)

とするのは正常な動作である.


21.TensorFlowでGPUの動作を確認する

次に,TensorFlowからGPUを利用できることを確認する.

Notebookで以下を実行する.

import tensorflow as tf

print(tf.__version__)

gpus = tf.config.list_physical_devices("GPU")

print(gpus)

正常に認識されていれば,GPUがPhysicalDeviceとして表示される.

さらに,実際にGPU上で計算を実行する.

import tensorflow as tf

with tf.device("/GPU:0"):
    a = tf.random.normal([3000, 3000])
    b = tf.random.normal([3000, 3000])
    c = tf.matmul(a, b)

print(c.shape)

以下のように表示されれば,行列演算自体は正常に終了している.

(3000, 3000)

別のTerminalで,

nvidia-smi

を実行すると,TensorFlowによるGPU利用状況を確認できる.

単にGPU名が取得できることだけでなく,実際にGPU上で演算を実行して確認することが重要である.


22.PyTorchでGPUの動作を確認する

PyTorchについても同様に確認する.

import torch

print("PyTorch:", torch.__version__)
print("CUDA:", torch.version.cuda)
print("CUDA available:", torch.cuda.is_available())

GPU名を確認する.

print(torch.cuda.get_device_name(0))

さらに,Compute Capabilityを確認する.

print(torch.cuda.get_device_capability(0))

今回使用しているBlackwell GPUでは,以下のように表示される.

(12, 0)

最後に,実際にGPU上で行列演算を行う.

a = torch.randn(3000, 3000, device="cuda")
b = torch.randn(3000, 3000, device="cuda")

c = torch.matmul(a, b)

print(c.shape)
print(c.device)

正常に動作すれば,例えば以下のように表示される.

torch.Size([3000, 3000])
cuda:0

これにより,PyTorchからGPUを認識できるだけでなく,実際のCUDA演算まで正常に実行できることを確認できる.


23.JupyterLabを日本語化する

複数のユーザーが利用することを考慮し,JupyterLabの日本語化も行った.

ユーザーごとに設定するのではなく,ユーザー用Dockerイメージの段階で日本語Language Packをインストールする.

singleuser/Dockerfileへ以下を追加する.

RUN pip install --no-cache-dir \
    jupyterlab-language-pack-ja-JP

さらに,JupyterLabのデフォルト言語を日本語にするため,設定ファイルを作成する.

RUN mkdir -p \
    /home/jovyan/.jupyter/lab/user-settings/@jupyterlab/translation-extension && \
    printf '{\n  "locale": "ja_JP"\n}\n' \
    > /home/jovyan/.jupyter/lab/user-settings/@jupyterlab/translation-extension/plugin.jupyterlab-settings && \
    chown -R jovyan:users /home/jovyan/.jupyter

追加後,ユーザー用Dockerイメージを再ビルドする.

cd /opt/jupyterhub

sudo docker build \
  -t jupyterhub-ml:1.0 \
  ./singleuser

すでに起動しているユーザーコンテナには,新しくビルドしたイメージの内容は反映されない.ユーザーサーバーを停止し,次回起動時に新しいイメージからコンテナを生成する必要がある.


24.ユーザーが追加するPythonライブラリについて

ユーザーによっては,標準環境に含まれていないPythonライブラリが必要になる場合がある.

Notebookから追加する場合は,以下のように%pipを使用できる.

%pip install transformers

バージョンを固定する場合は,以下のようにする.

%pip install transformers==<version>

%pipは,現在使用しているNotebook KernelのPython環境に対してpipを実行するため,Notebookからライブラリを追加する場合に利用しやすい.

一方,以下のような記述も見られる.

!pip install transformers

!pipはシェル上のpipコマンドを実行するため,Notebook Kernelが使用しているPython環境と異なるpipを参照する可能性がある.

そのため,Notebookから追加する場合は%pipを使用する方が分かりやすい.

今回の構成では,ユーザーサーバー停止時にユーザーコンテナを削除する.そのため,コンテナ内へ追加したPythonライブラリは,次回コンテナ生成時には残っていない場合がある.

必要なライブラリをNotebookの先頭に記述しておくことで,環境を再現しやすくなる.

また,以下のようなGPU環境に強く依存するパッケージは,ユーザーが個別に変更しない運用とした方が安全である.

TensorFlow
PyTorch
torchvision
CUDA関連パッケージ

これらは管理者側でDockerイメージに組み込み,動作確認済みのバージョンを提供する.


25.Let's Encrypt証明書を更新する

Let's Encryptの証明書には有効期限があるため,定期的な更新が必要である.

更新処理は,Certbotコンテナから以下のように実行できる.

sudo docker run --rm \
  -v /opt/reverse-proxy/certbot/conf:/etc/letsencrypt \
  -v /opt/reverse-proxy/certbot/www:/var/www/certbot \
  certbot/certbot renew \
  --webroot \
  --webroot-path=/var/www/certbot

まずは実際に更新せず,更新処理が正常に実行できるか確認することもできる.

sudo docker run --rm \
  -v /opt/reverse-proxy/certbot/conf:/etc/letsencrypt \
  -v /opt/reverse-proxy/certbot/www:/var/www/certbot \
  certbot/certbot renew \
  --webroot \
  --webroot-path=/var/www/certbot \
  --dry-run

証明書を更新した後は,nginxに新しい証明書を読み込ませる.

cd /opt/reverse-proxy

sudo docker compose exec nginx nginx -t
sudo docker compose exec nginx nginx -s reload

実運用では,これらの処理をcronやsystemd timerなどで定期実行する構成が考えられる.


26.ログを確認する

問題が発生した場合は,まずJupyterHubとnginxのログを確認する.

JupyterHubのログは以下で確認できる.

cd /opt/jupyterhub

sudo docker compose logs --tail 100 jupyterhub

リアルタイムに確認する場合は,

sudo docker compose logs -f jupyterhub

とする.

nginxについても同様に確認できる.

cd /opt/reverse-proxy

sudo docker compose logs --tail 100 nginx

また,今回のnginxではログディレクトリをホストへマウントしているため,

/opt/reverse-proxy/logs/

からaccess.logerror.logを確認できる.

ユーザーコンテナの状態を確認する場合は,

sudo docker ps

を使用する.

特定のユーザーコンテナのログを確認する場合は,

sudo docker logs jupyter-user01

のように実行する.


27.運用時にGPUの利用状況を確認する

複数ユーザーでGPUを共有する場合,管理者側でもGPUの利用状況を確認することがある.

ホストOSで以下を実行する.

nvidia-smi

これにより,各GPUについて,

  • GPU使用率
  • GPUメモリ使用量
  • 温度
  • 消費電力
  • GPUを使用しているプロセス

などを確認できる.

継続的に確認する場合は,例えば以下のように実行する.

watch -n 1 nvidia-smi

1秒間隔でnvidia-smiの表示が更新される.

今回の構成ではGPUをユーザーごとに固定していないため,利用者にはプログラム実行前にGPUの使用状況を確認し,空いているGPUを選択してもらう運用としている.


28.運用上の注意点

今回の構成を複数ユーザーで運用する上で,特に注意が必要な点をまとめる.

ユーザーコンテナとデータは別に考える

ユーザーサーバーを停止すると,DockerSpawnerによってユーザーコンテナは削除される.

一方,/home/jovyan/workはDocker Volumeへ保存しているため,Notebookやデータは保持される.

コンテナ     → 一時的
Docker Volume → 永続的

ユーザーには,必要なファイルを/home/jovyan/work以下へ保存するよう周知する必要がある.


ブラウザを閉じただけではサーバーは停止しない

JupyterLabを使用した後,Webブラウザのタブを閉じただけでは,ユーザー用JupyterLabコンテナが停止しない場合がある.

その場合,NotebookやPythonプロセスが動作を継続し,GPUメモリを使用し続ける可能性がある.

利用終了時には,JupyterHubの画面からユーザーサーバーを停止する運用とすることが望ましい.


GPUメモリは共有資源である

今回の構成では,すべてのユーザーコンテナからすべてのGPUを認識できる.

そのため,同じGPUを複数ユーザーが同時に指定することも可能である.

CUDA_VISIBLE_DEVICESは利用するGPUを選択するための仕組みであり,他のユーザーによるGPU利用を排他的に制御するものではない.

したがって,GPUの利用状況を確認してから使用するという運用ルールが必要となる.


Docker Volumeのバックアップも必要である

ユーザーデータはDocker Volumeに保存されるため,コンテナを削除してもデータは保持される.

しかし,Docker Volume自体を削除した場合や,ストレージに障害が発生した場合までデータが保護されるわけではない.

重要なデータを扱う場合は,Docker Volumeを含めた定期的なバックアップを別途検討する必要がある.

docker compose down -v-vオプションはVolumeを削除するため,永続データを扱う環境では実行前に十分注意する必要がある.


29.今回の構成

最終的な構成を整理すると,以下のようになる.

                         Internet
                            │
                            │ HTTPS :443
                            ▼
                     ┌─────────────┐
                     │    nginx    │
                     │   TLS終端   │
                     └──────┬──────┘
                            │
                        /jupyter/
                            │
                            ▼
                     ┌─────────────┐
                     │ JupyterHub  │
                     │     +       │
                     │DockerSpawner│
                     └──────┬──────┘
                            │
              jupyterhub-network
                            │
          ┌─────────────────┼─────────────────┐
          │                 │                 │
          ▼                 ▼                 ▼
   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐
   │   User A    │   │   User B    │   │   User C    │
   │ JupyterLab  │   │ JupyterLab  │   │ JupyterLab  │
   └──────┬──────┘   └──────┬──────┘   └──────┬──────┘
          │                 │                 │
          └─────────────────┼─────────────────┘
                            │
                            ▼
             RTX PRO 6000 Blackwell ×4
                  96GB VRAM / GPU

各ユーザー
    │
    └── /home/jovyan/work
              │
              ▼
       ユーザー専用Docker Volume

JupyterHub本体,ユーザーのJupyterLab環境,リバースプロキシをDockerで分離することで,各コンポーネントを独立して管理できる構成となった.

また,DockerSpawnerを利用することで,ユーザーごとに独立したJupyterLab環境を提供しながら,ホストに搭載された複数のGPUを共有して利用できる.


30.まとめ

本記事では,Ubuntu上にDocker,JupyterHub,DockerSpawnerを使用したマルチユーザーGPU環境を構築した.

構築した環境では,ユーザーがWebブラウザからJupyterHubへログインすると,ユーザー専用のJupyterLabコンテナが生成される.各コンテナからNVIDIA GPUを利用でき,TensorFlowやPyTorchを用いたGPU計算を実行できる.

また,nginxをリバースプロキシとして配置し,Let's Encryptの証明書を利用することで,HTTPS経由でJupyterHubへアクセスできる構成とした.

今回の構築で特に重要であった点は,以下のとおりである.

  • DockerSpawnerによってユーザー環境を分離する
  • ユーザーデータをDocker Volumeへ保存する
  • JupyterHubのポートを直接外部へ公開しない
  • nginxを経由してHTTPSでアクセスする
  • NVIDIA Container Toolkitを使用してコンテナからGPUを利用する
  • CUDA_VISIBLE_DEVICESによって使用するGPUを選択する
  • GPUを認識できるだけでなく,実際のCUDA演算まで確認する
  • Blackwell GPUではPyTorchの対応アーキテクチャにも注意する
  • GPU関連ライブラリはDockerイメージ側でバージョンを管理する
  • コンテナと永続データを分離して考える

特に,新しいGPUではnvidia-smitorch.cuda.is_available()でGPUが認識されていても,実際のGPU演算が正常に動作するとは限らない.今回の環境でも,Blackwell GPUのCompute CapabilityにPyTorch側が対応していないことによる問題が発生した.

そのため,新しいGPUを導入した場合は,GPUの認識確認だけでなく,TensorFlowやPyTorchを使用した実際の演算まで確認することが重要である.

今回の構成により,複数ユーザーがWebブラウザからアクセスし,それぞれ独立したJupyterLab環境でGPUを利用できる環境を構築できた.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?