Kobe University Advent Calendar 2019の 8日目の記事です。
8日目なんですが、私が最初らしいです。
これを見た人は Kobe University 関係なくてもいいので書きましょう!
皆さん GPU たくさん使っていますか? 僕は使っています。
この記事では、GPU サーバー上でたくさんのジョブをいい感じに並列に動かす方法を紹介します。
すぐできる割と基本的な手段を列挙しておいたので適当にやっている人は参考になるかもしれません。
PFN みたいに kubernetes でいい感じにやる話を期待された方はお帰りください。
なお、以下「複数のサーバー」のような表現をよく使いますが、すべて Single Node なジョブを複数のサーバーで動かす話で、Multi Node なジョブを動かす話はしないので注意してください。
0. 人力
- サーバーに入ってプログラムを動かします
- 終わる時間を見積もります
- 終わった頃にサーバーに入って次のプログラムを動かします
- 1~3 をすべての GPU,サーバーで繰り返します
1つのプログラムに数十時間かかるものであれば、これでもまあ良いのですが、つらいです。
朝とかに実行が終わると、その後数時間 GPU が空きます。
1. シェルスクリプトで for loop
export CUDA_VISIBLE_DEVICES="0"
for batchsize in 128 256; do
<command> --batchsize $batchsize
done
一番簡単な方法です。
端末をたくさん開いて、これを GPU の数だけ同時に実行すればしばらくの間サーバーを占領できます。
しかし、あらかじめ組み合わせを GPU の数に応じて分割しておく必要があります。
また、プログラムごとに実行時間が異なる場合、ループごとに終わる時間が大きく変わってくることがありそうです。
そもそも、GPU の数だけウィンドウがある状態はつらいです。
少し改善して、横方向に並列化する簡易的な方法としては以下のような方法があります。
N_GPU=8
GPU=0
for i in `seq 100`; do
export CUDA_VISIBLE_DEVICES=$GPU
<command> &
GPU=$(expr $GPU + 1)
if [ $GPU -eq $N_GPU ]; then wait; fi
GPU=$(expr $GPU % $N_GPU)
done
この方法はあらかじめ組み合わせを GPU の数に応じて分割しておく必要がなくなります。
しかし、この方法は、N_GPU
個のジョブの中で一番長いものが終わるまで次のN_GPU
個が実行されません。もったいないですね。
標準出力が崩壊する問題もあります(適切にファイルに出力させれば良い)
2. シェルスクリプトで for loop + GPU 割当
良い感じに空き GPU を管理するプログラムを作ります。
https://gist.github.com/cormoran/461c648060cd9522670df197d58423a2
できました。
良い感じに空き GPU を判断するのは実は結構難しくて、上のプログラムはちょっと適当にさぼっています。
function run() {
export CUDA_VISIBLE_DEVICES=`<wait for gpu(s) to be available & print available GPU number>`
$0
}
for batchsize in 128 256; do
run <command> --batchsize $batchsize &
done
こんな感じにすると、1つのサーバー上では、無駄なくプログラムが実行できるようになります。
ここまでで、実験に値するプログラムさえ持続的に供給できれば GPU を余すことなく占領し続けることができます。
ところで
1 と、2 の方法で注意すべきこととして、スクリプト実行中に実験プログラムを編集してはいけない点があります。
まだ実行が開始されていないプログラムがある状態で、実験プログラムを編集すると、その後は、新しい方のプログラムが動くことになります。
これだと、プログラムが動いている途中で別の実装を動かすのが難しくなります。
その場しのぎの対策として、別ディレクトリを作ってそちらで別の実装を動かすことや、サーバーごとに別の実装を動かす、などがあります(そしてよくやってしまう)。
こうしてサーバーにディレクトリが溢れていってよくわからなくなります。
3. シェルスクリプトで git-base な実行管理
git でバージョン管理していれば比較的簡単に解決できます。
一時ディレクトリを作ってプロジェクトを clone、指定 commit に移動し、そこでプログラムを動かします。
以下は実装イメージで、エラー対応なども必要なので注意。
COMMIT_HASH="HogeHoge"
function run() {
export CUDA_VISIBLE_DEVICES=`<wait for gpu(s) to be available & print available GPU number>`
WORK_DIR=`<いい感じに作る>`
git clone <git repo> $WORK_DIR
cd $WORK_DIR
git checkout -b <working branch name> $COMMIT_HASH
$0
mv $WORK_DIR <Trash>
}
for batchsize in 128 256; do
run <command> --batchsize $batchsize &
done
これで安心してソースコードをどんどん変えていけるようになりました。
しかし、複数のサーバーを使うことはできません。
8GPU サーバーが何台かあれば、実用上、これで最低限の人権は得られると思うのですが、せっかくなので全部使いたいですよね?
4. CI ツール (Gitlab Runner)
複数サーバーでジョブをさばく方法で考えられるのは
- HPC とかで使われそうなジョブキューシステムを使う
- Gitlab runner, Jenkins などの CI ツール
- 最近良く聞く kubernetes
とかだと思います(もっと良いものあったら教えて)。
研究室では Gitlab を建てて使っているのと、ドキュメント見た感じ一番簡単に使えそうだったので Gitlab Runner を使ってみました。(nvidia) docker を入れる以外にサーバーの root 環境を触る必要がなさそうだったのもポイントです。
Gitlab Runner に GPU 資源のサポートはないので、各サーバーで GPU の数だけ Runner を動かします(それぞれの環境変数にCUDA_VISIBLE_DEVICES
を設定)。
これによって、
- ジョブが git push で実行される
- 隔離された docker コンテナで、push したバージョンが動作
- 標準出力なども保存されて見返せる
という、かなり良い感じの環境ができました。
これでだいたい良いのですが、いくつか問題がありました。
- 複数 GPU 使うジョブに非対応
- GPU が使われているか見ないので、他人のプログラムに喧嘩を売りにかかる
- 他人が使っている GPU でも気にせず使い出す
- 共用サーバーでは問題
- Runner の停止が面倒(簡易的な UI を作って解決)
- ジョブの優先度が設定できない
複数 GPU は、(その場しのぎの対策ですが)1GPU 用 Runner を止めて、対応する N GPU 用 Runner を動かす方法がありますが面倒です。で、やめました。
5. 自作ジョブキュー
ここまで来たら、次は kubernetes な気がするんですが、
- swap 無効化しろとか、SELinux 止めてとか言われる
- 自分だけが使うと思うのであまりサーバーの設定変えたくない
- エコシステムを使わない他人のプログラムに喧嘩を売りにかかる
- プラグイン書けばできそうだけどドキュメントちゃんと読まないとできなさそう
- 使い始めたら、他の人にも全部 kubernetes 上でやってほしくなりそう
とかで面倒になって、結局自作の簡易ジョブキューを使っています。
自作のジョブキューは
- git-base な実行コード管理
- 必要最低限の環境分離(python venv or conda env)対応
- 他人が使っている GPU には手を出さない
- 複数 GPU 対応
- ジョブの優先度対応
- MySQL でジョブを管理していて、DataGrip でキューを直接編集できる
という感じで、現段階ではもうこれでいいのでは?という気分になっています。
もうちょっと詳細の話をしようとしていたのですが、長くなってしまったのと、書いているとabc147が始まってしまいそうなので、再来週くらいにもう一つ記事を書きます。
最後に
- kubernetes カスタマイズしたい、誰か教えて
- GPU をたくさん使ったからと言って論文が生えて来るわけではない
- でも空いているなら使いたいよね
- 修論…