いわゆる「スパコン」の中ってどんなだった?③ABCIでSingularityを利用した事例
こんにちはー。入社6年目のはっしーです。
全4回に渡って紹介するスパコンの事例。
今回は第3回です。
実際に私が行った作業事例として、Docker環境をABCI上のSingularity環境に移行させた経験をご紹介したいと思います。
スパコンにまつわる一般知識
ABCIにまつわる一般知識
ABCIでSingularityを利用した事例 👈ここ!
ABCIでGPUを利用した事例
これまでの復習
全4回中、第3回まで来たので、これまでの復習をします。
これまでの内容は以下の通り。
スパコンの一般的な定義は、「普通のコンピュータよりはるかに計算が速いコンピュータ」。
ABCIは、国の研究所が提供しているスパコンで、性能はざっくりと「「京よりは上、富岳よりは下」。
ABCIは、計算ノード、インタラクティブノード、共有ストレージで構成される。OSはRed Hat系のディストリビューション。
ユーザは、インタラクティブノードでジョブを登録することで、多くの計算資源を必要とするプログラムを計算ノードで実行することができる。
なんとなく思い出せたでしょうか。
ここまでは、スパコンひいてはABCIの一般知識をお伝えしています。
ここからは、具体的な事例に入っていきます。
ABCIの導入経緯
そもそもなぜスパコンが必要になったのか。
導入経緯は以下のようなものでした。
あるプログラムでLLM(Large Language Models, 大規模言語モデル)を利用している。
そのプログラムは、現行ではOpenAIのAPIを利用してGPTでのみ動作している。
これを、お客様が独自に作成したLLMでも動作させられるようにしたい。
いわゆるOSS-LLM(オープンソースLLM)も使いたいということ。
しかし、そのお客様作成のLLMはGPTのようにWeb APIになっていない。
したがって、ローカル環境に直にモデルを置いて動かす必要がある。
これにはそれなりマシンスペックが必要。
じゃあ、スパコン使うか。
スパコンとしてABCIを選んだのは、お客様の紹介に基づくものです。
プログラムをGPTで動かしたい場合は、ローカル環境のマシンからGPTに対してWeb APIを叩けばいいけど、お客様が作成したLLMで動かしたい場合は、ABCI環境にプログラムとLLMを一式置いて動かすことになった、ということですね。
Docker環境からSingularity環境への移行
Singularityとは?
我々が作成したプログラムは、元々Docker環境で動作するように環境構築を行っていました。
主に、プログラムの主要な部分を担うPythonのコンテナと、冗長化された複数のDBコンテナ、それらが連携して動くような構成になっています。
しかしながら、ABCIにはDockerなんてものは用意されていません。代わりに存在していたコンテナ技術が、Singularityです。
Singularityは、HPC環境向け・科学計算向けに設計されたコンテナ・プラットフォームであり、ことにスパコンの分野・機械学習の分野では比較的多く見られる選択肢であるようです。
※HPC = High Performance Computing, 高性能コンピューティング
実のところ、私はこのプロジェクトに参画する前は、Docker環境の構築すらろくに行った経験がなく(dokcer-compose.ymlがちょっと読めるくらい)、ましてSingularityなんてものは存在すら存じ上げておりませんでした。
これが大変な苦労を招いた。
DockerとSingularityの違い
SingularityとDockerには大きく以下のような違いが見られます。
管理者ユーザではなくてもOSを動かすことができ、一般ユーザとしてコンテナ作成・起動が可能。
ホストOSのホームディレクトリのファイル・ディレクトリが自動的にコンテナにマウントされる。
Docker HUBからDockerイメージをダウンロードして使用することができる。
コンテナ内でのGPU使用が比較的簡単。
これらの特徴によって、今回のように複数人で一つの高性能のマシンを扱う場合には、DockerよりもSingularityの方が良いと見なされています。
また、ABCIにはDockerfileをSingulairtyのイメージ定義ファイルに変換するモジュールが用意されていました。SingularityはDocker HUBのイメージを利用することができるので、このモジュールを使えば、あまり苦労せずSingularityのイメージが作れるかもしれません。
と、思う方もいるかもしれません。私がそうでした。
しかし、実際にはDockerとは細かく概念やコマンドに違いがあり、それらの対応関係を調べながら、docker-compose.ymlやDockerfileに書かれた内容をイメージ作成用・起動用のシェルスクリプトに自分の手で書き直す必要がありました。
加えて、Singularityにはdocker comoseのような複数のコンテナを同時に立ち上げ、連携させるコマンドは存在しないようでした。したがって、コンテナ間の依存関係や通信も自力で調整する必要がありました。これはDBコンテナを冗長化していたこともあり、想定外の苦労を招きました。
例えば、あるPython環境を構築するのに、DockerとSingularityでは以下のように書き方が違います。
・[Docker] Dockerfile
FROM python:latest
# ワーキングディレクトリ設定
WORKDIR /usr/src
# 環境変数の設定
ENV TZ=Asia/Tokyo
# ファイルのコピー
COPY ./requirements.txt ./requirements.txt
# イメージ作成時の動作
RUN pip install -r ./requirements.txt
# コンテナ起動時の動作
CMD ["this", "is", "dockerfile", "cmd", "args."]
ENTRYPOINT ["python", "main.py"]
・[Docker] docker-compose.yml
services:
example_service:
build:
context: .
dockerfile: Dockerfile
image: example_image
container_name: example_container
volumes:
- .\:/usr/src
- .\local_etc:/etc
ports:
- "8000:8000"
command:
- this
- is
- docker-compose
- command
- args.
・[Singularity] イメージ定義ファイル(*.def)
Bootstrap: docker
From: python:latest
%environment
export TZ=Asia/Tokyo
%files
./requirements.txt ./requirements.txt
%post
pip install -r ./requirements.txt
%runscript
python main.py "$@"
%startscript
python main.py "$@"
・[Singularity] イメージ作成/インスタンス起動コマンド
# イメージ作成
singularity build \
--fakeroot \
example.sif \
example.def
# インスタンスの既定処理実行 (コンテナが起動し、runscriptが実行され、コンテナが停止(終了)する)
singularity run \
--bind ./local_etc:/etc \
example.sif \
this is runscript args.
# インスタンス起動 (コンテナが常駐的に起動し、startscriptが実行される)
singularity instance start \
--bind ./local_etc:/etc \
example.sif \
example_container \
this is startscript args.
# 起動済みのインスタンス内で処理を実行したい場合はこう書く (ここではbashを起動する)
singularity exec instance://example_container bash
難しいポイント: インスタンス起動時の挙動制御
私が難しいと感じたポイントは、「Dockerfileやdocker-compose.ymlで定義されたコンテナ起動時の動作を、Singularityのイメージ定義ファイルやインスタンス起動コマンドにどのように変換していくか」というところです。Dockerで言うところのCMDやENTRYPOINTについて、何をどうSingularityのrunscriptやstartscriptに変換していくか、ということですね。
これは、Singulartyの「ホストとバインドされた領域を除いて、作成後のインスタンスの書き換えができない」という仕様も合わせて考慮しなければなりません。
また、DockerではCMDとENTRYPOINTの動作が組み合わさった時の挙動が複雑になりがちなことにも注意が必要です。DockerfileにCMDだけが定義されている場合、ENTRYPOINTだけが定義されている場合、両方が定義されている場合、それらがdocker-compose.ymlにも定義されている場合、等々……これらの仕様についてある程度知っていなければなりません。
例えば、次のような処理がDockerfileで定義されていたとします。
コンテナ起動直後に発火する。(ENTRYPOINT, CMD)
あるDBモジュールのデーモンを起動させる。
デーモン起動時の処理によって、データ領域やログ領域を /var/配下に作成する。(/var/dataや/var/logなど)
インスタンス起動時の処理を書けばいいわけなので、これはSingularityのイメージ定義ファイル(*.def)では、startscriptとして定義し直せば良さそうです。
しかしながら、実際にこの処理をstartscriptに書いただけでは、インスタンス作成後にバインドされていない領域を書き換えることになるので、エラーが発生します。「インスタンスは起動しているもののデーモンがいつまで経っても起動しない」という状況に陥ります。
これを回避するには、インスタンス起動時に作られる/var配下のディレクトリをホーム領域のどこかにバインドするような工夫が必要です。
Singularityでは、ホスト-インスタンス間のバインド設定をインスタンス起動時にオプションとして渡すので、以下のようなオプション設定を書き加える必要が出てきます。
singularity instance start \
--bind ./db/data:/var/data \ 👈ここ!
--bind ./db/log:/var/log \ 👈ここ!
example_db.sif \
example_db_container
Docker環境からのSingularity環境への移行は、単にコマンドを読み替えるのではなく、このような調整を個々のコンテナ/インスタンスで細かく行う必要があります。
実際に私が前述の状況に陥った時は、Docker Hubからダウンロードされるイメージファイル内のソースを確認し、コンテナ起動時にどのような処理が起こるのかを調べながら、その都度イメージを作り直して試行錯誤することになりました。
これには非常に時間が掛かりました。
さて、今回はABCI上でのSingularity利用事例ということで、以上になります。
Docker環境からSingularity環境への移行という観点で、SingularityとDockerの違い、難しいポイントをご紹介しました。
「うわぁ…大変…」と感じてもらえたら嬉しいです。
では、次回でお会いしましょう🙇♂️