DockerイメージをApptainerから利用(#4)

はじめに

先日、初めてTSUBAME4というスーパーコンピュータでで実験用のプログラムを実行する機会がありました。

普段は研究室サーバーでDockerを利用して環境の分離を実践しているにも関わらず、本番だけTSUBAME4上のけしからんPythonを使って実験データを収集することは再現性の観点からも好ましくありません。

本記事では、HPC向けコンテナ環境であるApptainerからDockerを使用してスムーズにPythonプログラムを実行する方法を紹介します。

1. Apptainerとは

以下の公式ユーザーガイドと重複するため割愛させてください。

apptainer.org

「Dockerで十分じゃないか」と思うかもしれませんが、TSUBAME4のようなスーパーコンピュータにはコンテナ環境としてApptainer(旧Singularity)しか整備されていない場合がほとんどです。

公式ユーザーガイドに挙げられていたApptainerの4つのメリットのうち、(ピンと来なかった1つを除いた)3つを紹介します。

  1. GPU/高速ネットワーク/並列ファイルシステムをデフォルトで容易に利用可能(分離より統合
  2. 単一ファイルのSIFコンテナ形式は、持ち運びや共有が容易(ポータビリティ
  3. コンテナ内部でも外部と同じユーザーであり、デフォルトではホストシステム上で追加の権限を得ることはできない(シンプルで効果的なセキュリティモデル

1.について、コンテナ内からデフォルトでGPUを利用できるのはDockerと比較して便利だと感じました。2.については、Dockerでもわざわざレイヤーを1つずつ移動する人はいないし、レジストリを利用すれば問題にならないと思いました。3.はDockerユーザーの思考と相性が悪いと思いました。デフォルトでユーザーのホームディレクトリが丸ごとコンテナにマウントされていたのを初めて見た時には思わず二度見してしまいました。

これらの特徴によって、TSUBAME4をはじめとするスーパーコンピュータで、Dockerネイティブな人がApptainerを利用する心理的ハードルは依然として高いのが現状です。

2. なぜコンテナ環境が必要か

「なぜ必要か」の前に、「何がしたかったか」と「何ができなかったか」を説明します。

以下は、研究室のGPUサーバーで利用しているDockerfileを簡略化した例です。

FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04

RUN apt-get update && \
    apt-get -y upgrade && \
    apt-get install -y \
        build-essential \
        curl \
        git \
        jq \
        libbz2-dev \
        libffi-dev \
        libgdbm-dev \
        liblzma-dev \
        libncurses5-dev \
        libnss3-dev \
        libreadline-dev \
        libsqlite3-dev \
        libssl-dev \
        wget \
        zlib1g-dev && \
    apt-get clean

ENV PYTHON_VERSION=3.12.5
ENV PATH=/root/.local/bin:/root/.pyenv/shims:/root/.pyenv/bin:$PATH

RUN set -ex && \
    curl https://pyenv.run | bash && \
    pyenv install $PYTHON_VERSION && \
    pyenv global $PYTHON_VERSION && \
    curl -sSL https://install.python-poetry.org | python -

WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN poetry install

COPY src/ ./src/

親イメージにはnvidia/cudaを使用していています。まず、apt-getで必要なパッケージをインストールします。それから、pyenvで指定したバージョンのPythonをインストールして、Poetryでプロジェクトの依存関係をインストールします。残りはpoetry run pythonというコマンドでPythonプログラムを実行するだけです。

次に、TSUBAME4でのPythonプログラム実行を考えます。

コンテナ環境を利用せず、指定したバージョンのPythonをインストールすることは結論から言うと難しいです*1。なぜなら、現時点でTSUBAME4にインストールされているPythonバージョンは1つのみであるためです。

[uc0xxxx@login1 ~]$ python --version
Python 3.9.18

管理者がインストールした「けしからんPython」を使用したくないユーザーは、次にpyenvの導入を検討すると思います。

[uc0xxxx@login1 ~]$ curl https://pyenv.run | bash
# 省略
[uc0xxxx@login1 ~]$ pyenv install --list | grep -e '^  3.12'
  3.12.0
  3.12-dev
  3.12.1
  3.12.2
  3.12.3
  3.12.4
  3.12.5
[uc0xxxx@login1 ~]$ pyenv install 3.12.5
Downloading Python-3.12.5.tar.xz...
-> https://www.python.org/ftp/python/3.12.5/Python-3.12.5.tar.xz
Installing Python-3.12.5...

BUILD FAILED (Red Hat Enterprise Linux 9.3 using python-build 2.4.10)

Inspect or clean up the working tree at /tmp/python-build.20240820171118.832519
Results logged to /tmp/python-build.20240820171118.832519.log

Last 10 log lines:
# 省略
make: /bin/sh: Operation not permitted
make: *** [Makefile:2738: Python/hashtable.o] Error 127
make: *** Waiting for unfinished jobs....

残念ながら、pyenvで指定バージョンのPythonをインストールすることですら権限の関係で一筋縄ではいきませんでした。Ubuntuでは一般ユーザーでもインストールできたことと、検索しても類似のエラーが出てこないことから、潔く諦めました*2

私にroot権限はないのでソースビルドも難しく、ここでコンテナ環境を利用する機運が高まりました。つまりコンテナ環境が必要な理由は、自分が指定したPythonバージョンを利用するためです。

3. DockerイメージをApptainerから利用する

TSUBAME4のコンテナ環境としてはApptainerしか整備されていないため、Apptainerを利用します。

Apptainerの定義ファイルを書いてコンテナをビルドし、プログラムを実行するのも1つの方法です。その際は、以下の公式ユーザーガイドをしっかり確認してください。

apptainer.org

ただしユーザーガイドの説明は長く、そもそもApptainerがDockerほど普及していないことからモチベーションが低下したため、この方法は採用しませんでした。

仮に気になった論文のソースコードが公開されていたとします。GitHubリポジトリを覗きに行ったらApptainerの.defファイルだけ置かれていた状況を想像してみてください。大変恐ろしいです。

代わりに、ApptainerからDockerイメージを利用してコンテナをビルドする方法を採用しました。概要は以下の公式ユーザーガイドで紹介されています。

apptainer.org

例えばTSUBAME4で適当なインタラクティブジョブを実行して、パブリックなコンテナを実行することができます。

[uc0xxxx@login2 ~]$ qrsh -l gpu_1=1 -l h_rt=0:10:00
[uc0xxxx@r17n10 ~]$ apptainer run docker://sylabsio/lolcow:latest

実行結果(牛がカラフルで嬉しい)

まずは、利用するDockerイメージを何らかのレジストリにpushする必要があります。プライベートなレポジトリが利用できることと、料金を最小限に留めることを重視した結果、GitHub Container RegistryDocker Hubの2つが候補として挙げられました。

ソースコードGitHubで管理しているため、GitHub Actionsで最新のDockerイメージをビルドし、GitHub Container RegistryまたはDocker Hubにpushすることを検討していました。しかし、イメージサイズが10GBを超えているせいなのか、ワークフローの実行中に以下のようなディスク容量不足のエラーが起きてしまいました。

ワークフローの実行結果

GitHub Actionsの利用は一旦諦め、ローカルでビルドしたDockerイメージをpushしようと考えた時に既にGitHub全体に対するモチベーションが低下していたため、Docker Hubを採用しました*3。Docker Hubにイメージをpushする方法は公式ドキュメントを参考にしてください。

なんだかんだ言って使いやすいDocker Hubにイメージをpush

ここからが本題のApptainerです。

まずは、Apptainerイメージをキャッシュするディレクトリ(デフォルトは$HOME/.apptainer/cache)を指定します。TSUBAME上でホームディレクトリは1ユーザーあたり25GiBしか利用できないため、グループディスクの利用を強くオススメします。以下のようなコマンドを実行してApptainerコンテナを作成します。

[uc0xxxx@login2 ~]$ mkdir /gs/bs/tga-hogehoge/uc0xxxx/piyopiyo
[uc0xxxx@login2 ~]$ export APPTAINER_CACHEDIR=/gs/bs/tga-hogehoge/uc0xxxx
[uc0xxxx@login2 ~]$ apptainer build \
--docker-login \
--force \
-s /gs/bs/tga-hogehoge/uc0xxxx/piyopiyo \
docker://<REPOSITORY>:<TAG>

Enter Docker Username:
Enter Docker Password:
INFO:    Starting build...
# 省略
INFO:    Creating sandbox directory...
INFO:    Build complete: /gs/bs/tga-hogehoge/uc0xxxx/piyopiyo/

--docker-loginオプションを利用することでdocker loginコマンドのようにインタラクティブレジストリにログインすることができます。--forceオプションは既存のイメージを強制的に上書きします。また、-s--sandbox)オプションによってコンテナ内で書き込み可能になりますが、デフォルトでは読み込みだけ可能なSIFフォーマットになる(apptainer build ***.sifを実行する)ので用途によって使い分けるべきだと思いました。

その他のビルドオプションについては、公式ユーザーガイドを参照してください。

対象のディレクトリ内を確認します。

[uc0xxxx@login2 ~]$ ls /gs/bs/tga-hogehoge/uc0xxxx/piyopiyo/
app  boot                        dev          etc   lib    lib64   media  NGC-DL-CONTAINER-LICENSE  proc  run   singularity  sys  usr
bin  cuda-keyring_1.0-1_all.deb  environment  home  lib32  libx32  mnt    opt                       root  sbin  srv          tmp  var
[uc0xxxx@login2 ~]$ ls /gs/bs/tga-hogehoge/uc0xxxx/cache/
blob  library  net  oci-tmp  oras  shub

それでは最後に、コンテナと対話*4します。適当なインタラクティブジョブを開始してapptainer shellコマンドを実行します。

[uc0xxxx@login2 ~]$ qrsh -l gpu_1=1 -l h_rt=0:10:00
[uc0xxxx@r17n10 ~]$ apptainer shell -f --no-home -w --nvccli /gs/bs/tga-hogehoge/uc0xxxx/piyopiyo
INFO:    User not listed in /etc/subuid, trying root-mapped namespace
INFO:    Using fakeroot command combined with root-mapped namespace
WARNING: Skipping mount /etc/localtime [binds]: /etc/localtime doesn't exist in container

Dockerネイティブな人の心理的ハードルを下げるために重要なオプションとして、-f--fakeroot)と--no-homeがあります。

fakerootは、一般ユーザーがコンテナをrootユーザーとして実行するのに必要です。no-homeについては、Apptainerがデフォルトでユーザーのホームディレクトリなど*5をマウントするというありがた迷惑な機能を無効化するためのオプションです。この--no-homeオプションを付けない場合、/rootディレクトリ以下が丸ごとapptainerコマンドを実行したユーザーのホームディレクトリで上書きされ*6、せっかくDockerイメージでインストールしたpyenvpoetrypythonが実行できなくなります。

-w--writable)オプションはファイルシステムを読み書き可能にする場合に必要です。データセットのダウンロードなどで書き込みができないのは困ります。

--nvccliオプションについては、似たものとして--nvオプションがあります。詳細は公式ドキュメントに委ねますが、--nvがホストのCUDAライブラリをコンテナにバインドするのに対して、--nvccliNVIDIAによってメンテナンスされているnvidia-container-cliを使用してセットアップを行うことでコンテナ内のバージョンのCUDAライブラリが使用できると個人的に解釈しています。

コマンドの実行に成功すると、Apptainer>に続けて入力できるのでPythonバージョンとGPUを使用可能かどうか確認します。

Apptainer> nvidia-smi
Wed Aug 21 09:29:53 2024
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 545.23.08              Driver Version: 545.23.08    CUDA Version: 12.3     |
|-----------------------------------------+----------------------+----------------------+
| 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 H100                    On  | 00000000:04:00.0 Off |                    0 |
| N/A   23C    P0              69W / 699W |      4MiB / 95830MiB |      0%      Default |
|                                         |                      |             Disabled |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|
|  No running processes found                                                           |
+---------------------------------------------------------------------------------------+
Apptainer> cd /app
Apptainer> poetry run python
Python 3.12.5 (main, Aug 21 2024, 06:37:37) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> torch.cuda.is_available()
True
>>>

Pythonバージョンは指定した3.12.5であり、GPUも使用可能であることが確認できました。

今回はインタラクティブジョブとして実行しましたが、バッチジョブで実行する場合にはapptainer run(またはapptainer exec)の引数としてpythonコマンドなどを実行することになります。

まとめ

ApptainerからDockerを利用して、pyenvとPoetryでPythonプログラムを実行する方法を紹介しました。

Apptainerに関する知見の共有はまだまだ少ないと感じたので、公式ユーザーガイド(なかなか奥が深い)を読んで学んだことがあれば、また記事にしたいと思います。

いつかApptainerが脚光を浴びる時代も来るかもしれません。

*1:minicondaユーザーの場合はPythonバージョンを指定して仮想環境を作成できそうですhttps://www.t4.gsic.titech.ac.jp/docs/handbook.ja/freesoft/#miniconda

*2:衝撃的なことに、後ほどログインノードではなく計算ノードでpyenv install 3.12.5を実行することでインストールに成功しました(本記事の価値が無事に半減しました)

*3:執筆時点でDocker Hubの無料枠ではプライベートレポジトリを1つまでしか作成できないため、複数のプライベートレポジトリを利用したい場合はGitHub Container Registryをおすすめします

*4:コンテナを実行する方法はshell、exec、runの3種類ありますが、今回は理解が捗るようにshellを利用します Quick Start — Apptainer User Guide main documentation

*5:Apptainerのデフォルトのバインドマウント一覧 Bind Paths and Mounts — Apptainer User Guide main documentation

*6:--no-homeオプションでもカレントディレクトリがマウントされるため、カレントディレクトリもマウントしたくなければ--no-mount home,cwdのように両方無効化すべきだと書かれていましたが、なぜか--no-homeを指定した場合と同じ挙動でした Bind Paths and Mounts — Apptainer User Guide main documentation