はじめに
ESPnet は、音声分野での機械学習の際に使用されるツールセットです。
Kaldiというツールセットからの流れを汲んでおり、レシピという名前のシェルスクリプトを通した実験フローによる実験や、python pipライブラリとしての利用、あるいはそれらの実験の成果物(huggingfaceなどで公開されているものもあり)のモデルを使用した推論などの利用ができます。
また、TSUBAMEはH100で構成されるスパコンです。
今回はEspnetについてTSUBAME4.0上で、egs2/librispeech/asr1
を実行したい場合の例に説明します。
なお、セットアップの方法は公式サイトに譲りますが、Option A)にあるconda自体のインストールを行なってconda環境を整えるのが楽かなと思います。この場合、module でcondaのloadは不要です。
この記事を見るのが面倒な人は
#!/bin/sh
#$ -cwd
#$ -l node_f=1
#$ -l h_rt=24:00:00
#$ -v PATH # レシピのプロセスに対して、qsubコマンドなどのパスを引き継がせるため
#$ -v LD_LIBRARY_PATH
#$ -M メールアドレス
#$ -m abe
ngpu=4
stage=1
stop_stage=13
module load cuda
./run.sh --ngpu $ngpu \
--stage $stage \
--stop_stage $stop_stage \
--asr_tag "1-1_asr_train_baseline"
などをステージをまとめてqsub -g [GROUPNAME] job.sh
を実行するか、ステージごとに資源を変更するなどして実行してください。
(この記事を最後まで見た人向けに : cmd_bacend=local
のままで良いです)
上記の方法は以下のメリット、デメリットがあります。
メリット
- 何も設定しなくてもTSUBAMEを使える
デメリット
- 資源を最小限にするにはジョブスクリプトを複数用意する必要がある
- マルチノード学習に未対応
- 複数プロセスを動かす時にアレイジョブではなく、フォーク(cmd_backend=local)になるので、cpuのコア数の最大値の制約を受けるが、アレイジョブにするとノードごとに1プロセスを実行するので制約を受けない。
-----------------------------------完-----------------------------------
はい、ここからは実験管理や資源最適化がめんどくさくなった人のための記事です。
実験の仕方の例
レシピはデータのダウンロードや前処理、学習、評価などのステージに分かれています。
例えばダウンロード済みであったら、ダウンロードをスキップするなどの処理があることがほとんどですが、そうでない場合もあるので、ステージを明示的に指定して実行した方が良いです。すなわち、以前の実行結果のキャッシュを使用するということです。
TSUBAMEを使用する場合、ジョブスケジューラを使用することになりますが、様々なめんどくささがあります。
- 通常のジョブは24時間までしか使えない
- Espnetのレシピはステージごとに適切なリソースが異なる
1.については予約ノードなどにより24時間以上使えますが、そうでない人もいるでしょう。幸いなことにEspnetの学習ステージは途中で割り込み終了があっても途中のEpochから再開してくれます。trapなどで24時間を超えた時の強制終了のSIG_KILLに対して終了間際にジョブを自動で投げても良いのですが、実行失敗だとか実験がうまくいっていないことが多く、無限ループを避けたりチェックしたりするために、私は手動で同じジョブスクリプトを投げて再開させています。
2.については色々と修正が必要なので、ここで共有します。
まず、ざっくりとESPnetでの実行の仕組みです。
-
run.sh
が実行 -
asr.sh
が呼ばれる -
launch.py
を経由しない場合、_cmdによる実行-
cmd_backend=local
(デフォルト設定)の場合、run.pl
が呼ばれ、その子プロセスとしてジョブが実行される -
cmd_backend=tsubame4
(今回の設定)の場合、queue.pl
が呼ばれ、ステージ専用のジョブスクリプトが実験フォルダ以下に作成され、ジョブが投げられる
-
-
launch.py
を経由する場合、launch.pyの中で_cmd系の処理が行われる。流れは上記と同じ
まず、queue.confを以下のように変更します。このフォーマットはKaldiのスタイルです。qsubで投げる時のオプションについての設定を記述します。espnetのドキュメントも参考にしてください。
ジョブスケジューラで使用できるリソース制限(-l)は基本的にはTSUBAMEドキュメントの通りですが、qconf -sc
の実行でも確認できます。また、-pe はParallel Environment
を表します。TSUBAMEのdocumentには触れられていないため、不要かもしれません。qconf -spl
で使用できる並列環境がリストされます。
command qsub -v PATH -cwd -S /bin/bash -j y -l arch=*64*
option name=* -N $0
option mem=* -l mem_free=$0,ram_free=$0
option mem=0 # Do not add anything to qsub_opts
option num_threads=* -v TMP$0 # gpuパラメタと同じく。
option num_threads=1 # Do not add anything to qsub_opts
option max_jobs_run=* -tc $0
option num_nodes=* -pe openmpi $0 # PE(Parallel Enviroinment. Check by qconf -spl) as allocation_rule=1
default gpu=0 # gpu per node
option gpu=0
option gpu=* -v TMP$0 # gpuパラメタはTSUBAMEには存在しない。launch.pyで指定した引数を処理しないとqueue.plでエラーが発生するので意味を持たない環境変数として処理。
# option gpu=0 -l cpu_4=1
# option gpu=1 -l node_q=1
# などすると、ノード選択が不要となるが、マルチノードや細かいノード選択に対応できない。
option group=* -g $0
default h_rt=0:09:59 # 無料枠
option h_rt=* -l h_rt=$0
option mails=* -m abe -M $0
option priority=* -p $0
default node_f=0
option node_f=0
option node_f=* -l node_f=$0
default node_h=0
option node_h=0
option node_h=* -l node_h=$0
default node_q=0
option node_q=0
option node_q=* -l node_q=$0
default node_o=0
option node_o=0
option node_o=* -l node_o=$0
default gpu_1=0
option gpu_1=0
option gpu_1=* -l gpu_1=$0
default gpu_h=0
option gpu_h=0
option gpu_h=* -l gpu_h=$0
default cpu_160=0
option cpu_160=0
option cpu_160=* -l cpu_160=$0
default cpu_80=0
option cpu_80=0
option cpu_80=* -l cpu_80=$0
default cpu_40=0
option cpu_40=0
option cpu_40=* -l cpu_40=$0
default cpu_16=0
option cpu_16=0
option cpu_16=* -l cpu_16=$0
default cpu_8=0
option cpu_8=0
option cpu_8=* -l cpu_8=$0
default cpu_4=0
option cpu_4=0
option cpu_4=* -l cpu_4=$0
また、cmd.sh
に追記します。queconfで定義したパラメタに対して必要なら変数を渡します。
優先度係数をかけているのは、TSUBAME4.0のモニターページの通り、非常に混み合っているからです。特にnode_fなど資源が多く必要なときは厳しさが増します。TSUBAME3よりGPUでかくなってノード数が減ったらまぁ。。
TSUBAMEの料金は、ノード、優先度係数、実行時間、実行見積もり時間によって変わるので、いろんな要素を加味して設定します。
cmd_backend=tsubame4
elif [ "${cmd_backend}" = tsubame4 ]; then
export train_cmd="queue.pl --group [tga-HOGE] --cpu_4 1 --h_rt 6:00:00"
cuda_cmd="queue.pl --group [tga-HOGE]"
cuda_cmd+=" ${cuda_cmd_nodetype:-"--node_f 1"}"
cuda_cmd+=" --h_rt ${cuda_cmd_timelimit:-"24:00:00"}"
cuda_cmd+=" --mail [通知を送りたいメアド]"
cuda_cmd+=" --priority ${cuda_cmd_priority:-"-4"}" # default -5 max -3
export cuda_cmd
cuda_single_cmd="queue.pl --group [tga-HOGE]"
cuda_single_cmd+=" ${cuda_single_cmd_nodetype:-"--node_q 1"}"
cuda_single_cmd+=" --h_rt ${cuda_single_cmd_timelimit:-"2:00:00"}"
# cuda_single_cmd+=" --mail [通知を送りたいメアド]" # アレイジョブで使用するため送信しない。
cuda_single_cmd+=" --priority ${cuda_single_cmd_priority:-"-5"}" # default -5 max -3
export cuda_single_cmd
cuda_lm_cmd="queue.pl --group [tga-HOGE]"
cuda_lm_cmd+=" ${cuda_lm_cmd_nodetype:-"--node_f 4"}"
cuda_lm_cmd+=" --h_rt ${cuda_lm_cmd_timelimit:-"24:00:00"}"
cuda_lm_cmd+=" --mail [通知を送りたいメアド]"
cuda_lm_cmd+=" --priority ${cuda_lm_cmd_priority:-"-4"}" # default -5 max -3
export cuda_lm_cmd
export decode_cmd="queue.pl --group [tga-HOGE] --cpu_4 1 --h_rt 4:00:00"
さらに、espnet2/bin/launch.py
が分散ノード学習をできるように以下のように追記します。
分散学習に関しては、Espnetのドキュメントがあり、それを参考にします。launch.pyはcomming soonとあるので実装としても未完成ということかもしれません。おそらく多種多様なジョブスケジューラへのサポートの際にインターフェースが面倒なのでしょう。
TSUBAME4でのOpenMPIや環境変数に関してはTSUBAME4のジョブスケジューリングシステムに書いてあります。
必要に応じてTSUBAME4のベースとなっているSun Grid Engineのドキュメントも見ても良いかもしれません。
else:
# This pattern can also works with Slurm.
logging.info(f"{args.num_nodes}nodes and {args.ngpu}gpu-per-node using mpirun")
cmd = (
args.cmd # queue.pl など
# arguments for ${cmd}
+ [
"--gpu",
str(args.ngpu),
"--num_threads",
str(max(args.ngpu, 1)),
# Make sure scheduler setting, i.e. conf/queue.conf
# so that --num_nodes requires 1process-per-node
"--num_nodes",
str(args.num_nodes),
args.log,
"mpirun",
# -np option can be omitted with Torque/PBS
"-np",
str(args.num_nodes),
"-host", # 追加
'$(awk \'{print $1}\' "$PE_HOSTFILE" 2>/dev/null | paste -sd "," -)' # 追加
]
+ 略
)
ここまで用意が終わったらレシピに変更を加えます。
今回新たにcuda_single_cmdとcuda_lm_cmdを加えました。
例えばlmの学習をこの設定で行いたいなら、asr.sh
で
if [ -n "${cuda_lm_cmd:-}" ]; then
_cmd="${cuda_lm_cmd}"
else
_cmd="${cuda_cmd}"
fi
をするなどしてlmの学習の実行で使用するperl スクリプトを変更します。
さらに、TSUBAMEで必要なモジュールをロードするため、local/path.sh
に記述します。
module load cuda
module load openmpi
この辺りはTSUBAM上でmodule avail
を実行の上確認し、必要なものを入れてください。condaを使用している場合、不要な場合があります。
実行の際は以下のようなジョブスクリプトをqsub -g [グループ名] job.shなどで実行します。
#!/bin/sh
#$ -cwd
#$ -l cpu_4=1 # レシピの実行のため
#$ -l h_rt=24:00:00
#$ -v PATH # レシピのプロセスに対して、qsubコマンドなどのパスを引き継がせるため
#$ -v LD_LIBRARY_PATH
#$ -M メールアドレス
#$ -m abe
ngpu=4
stage=10
stop_stage=12
export cuda_cmd_priority=-4
lm_exp=pretrained/librispeech_conformer_hop_length160/exp/lm_train_lm_transformer2_bpe5000_scheduler_confwarmup_steps25000_batch_bins500000000_accum_grad2_use_amptrue
inference_lm=valid.loss.ave_10best.pth
./run.sh --ngpu $ngpu \
--stage $stage \
--stop_stage $stop_stage \
--lm_exp "${lm_exp}" \
--inference_lm "${inference_lm}" \
--asr_tag "1-1_asr_train_baseline"
なお、以下を考慮して、ログインノード上で./job.shを直接実行はしない方が良いです。
- 前処理はログインノード上で実行になるので負荷がかかる
- ログインノード上で負荷がかかったプロセスなどは勝手にkillされる
- 横着しない人は、
-hold_jid
によって多段的に実行すれば良いとは思いますが、そもそも学習ステップなど24時間以上かかる場合、どのみち失敗します
LMの学習のレシピはデフォルトで16GPUも使っています。大抵の場合、LM自体は研究ターゲットではないと思うので、学習済みの公開モデルを使えば良いと思います。
git clone https://huggingface.co/pyf98/librispeech_conformer_hop_length160
などでクローンするなどしておけば、公開モデルを使用できます。
そうは言っても16GPUでLMを学習させてみたい人は例えば以下のスクリプトを実行します。num_nodeはasrのtrainでも使用されるため、lmのステージのみを指定して実行してください。tmux上で./job_lm.sh
のように実行すると、これはログインノード上で実行されてしまいますが、ジョブを投げて待つだけの簡単な処理で負荷がかからないため、ファイル転送を除く10分以上のプログラムに該当し、推奨されないものの、問題ないかもしれません。このログインノードでの実行はtmuxのセッション自体を丸々強制終了の可能性があるので、stage10でレシピに後処理があるなど、ジョブが完了した後に処理があるステージの場合は注意が必要です。
お作法について気になる人はレシピ用のcpuノードも使うか、ログインノードなどでlauch.pyでシェルスクリプトを生成させた直後にプロセスを終了させ、そのスクリプトをジョブスケジューラーに投げると良いと思います。
ngpu=4 # gpu per node
num_node=4
stage=7
stop_stage=7
./run.sh --ngpu $ngpu \
--stage $stage \
--stop_stage $stop_stage \
--num_nodes $num_node \
--expdir $expdir
その他ツールなど
Slack通知や環境依存設定をespnet/toolsあたりに置いておく。
#!/usr/bin/env bash
. "$(dirname $0)/myenv.sh"
script="${BASH_SOURCE[1]}" # 呼び出したスクリプト
notify() {
message=$1
text="hostname : $(hostname)
path : $(pwd)
data : $(date)
message :
${message}"
echo $message
if [[ -n "$SLACK_API_KEY" ]]; then
curl -s -X POST 'https://slack.com/api/chat.postMessage' -d "token=${SLACK_API_KEY}" -d 'channel=#notification' -d "text=${text}" > /dev/null
fi
# LINE notifyは2025/3でサービス終了。。。
if [[ -n "$LINE_API_KEY" ]]; then
curl -s -X POST -H "Authorization:Bearer ${LINE_API_KEY}" --data-urlencode "message=${text}" https://notify-api.line.me/api/notify > /dev/null
fi
}
on_exit() {
if [ $? -eq 0 ]; then
notify "'$script' successfuly end!!!"
else
notify "'$script' unexpectedly end...\n "
fi
}
on_sigint() {
notify "'$script' stopped by sigint..."
trap - EXIT
}
trap on_sigint SIGINT
trap on_exit EXIT
#!/bin/bash
export SLACK_API_KEY=[SLACK_API_KEY]
export LINE_API_KEY=[LINE_API_KEY]
export MY_PLACE=tsubame4
ジョブスクリプトへのAPIキーの直書きはやめましょう。
TSUBAMEの設定的には、ジョブスクリプトは(多分)全員が見ることが可能ですが、環境変数はおそらく権限がないと見れないので、環境変数で設定すると良いと思います。ただ、環境変数をスクリプトに書く場合、そのスクリプト自体のアクセス制限をかけておきましょう。
cmd_backend=${cmd_backend:-local}
if [ "$MY_PLACE" == *tsubame4* ] ; then
cmd_backend=tsubame4
module purge
module load cuda/12.1.0
module load openmpi
elif [ "$MY_PLACE" == *lab* ] ; then
cmd_backend=local
module purge
module load cuda/12.1
else
:
fi
# ~~~ スケジューラ関係処理 ~~~
. ./../../../tools/mytool.sh
./run.sh
trapの処理をlocal.sh
に書くと、アレイジョブの通知が鬱陶しいのでお勧めしません。また、設定によっては環境変数の引き継ぎのために、queue.conf
などに-v MY_PLACE
などが必要です。
計算時間と資源の効率化
- データの置き場について、TSUBAMEはどこに置いても高速です。しかし、TSUBAMEで注意すべきなのが、容量よりもinodeです。データセットが容量に対してディレクトリやファイルが多いと、先にinodeの制限に引っ掛かります。TSUBAMEではない場合、例えば計算サーバーからデータへのアクセスをネットワークマウントして実行している場合、可能であれば計算サーバ上へデータを移した方が良いです。使用状況にもよりますが、HDD、SSD以前に大抵のボトルネックはネットワーク回線にあります。コマンドでも確認できますが、このような時のためにGrafanaを立てておくとボトルネックの可視化ができて効果的な解決策を実行しやすくなります。
- espnetのdumpやdataについて、espnetのキャッシュがどうなっているか理解しないと、設定ファイルに書いてあるにも関わらず反映されなかったり、暗黙のデフォルト値が使われるなどします。例えばインファレンスの時は
asr_config
の値は使用されません。この辺が実はかなりめんどくさいです。また、パラメータはあるのに実装されていないケースが結構あります。例えば巨大な事前学習済みモデルを特徴量抽出機として使いたい場合にasr_statsでキャッシュできそうなパラメタがありましたが、実装されていません。デフォルトのレシピが動けばラッキーだと思って、concig設定の変更はよく確認した方が良いかと思います。 - ダウンロードディレクトリなど、レシピを跨いでシンボリックリンクで共有した方が時間やストレージ容量的に楽なことがありますが、十分注意しましょう。例えば、dataディレクトリをlibrispeech_100とlibrispeechでdataディレクトリなどを共有した結果、レシピ上でハードコーディングされたトークンリストが共有され、非常に面倒なことになりました。
実験管理
ジョブスケジューラーでqstat
で表示される範囲だと名前の前半しか見えません。また、後からディレクトリを見た場合、名前が長すぎてつらいです。そのため、実験で使用するタグ(asr_tagなど)のプレフィックスに実験idをつけることをお勧めします。
実験コードの編集
VSCode に慣れた私にとって、ftpやscpでの共有はだいぶつらいものがありました。手動共有は共有したコードが何かわからなくなるし、VSCode拡張機能などでの自動共有は下手すると大事なコードが消えます。
TSUBAMEではVSCode Serverのサービスを公開しており、なんと学内生なら無料で利用できます。これを使わない手はないので積極的に使いましょう。なお、VSCodeの拡張機能として VSCode Remoteを使用した場合、拡張機能のプロセスをTSUBAMEに乱立させて迷惑をかけます(制限を喰らいます)。
ローカルのサーバーなどとの共有は実験結果を除いてgithub経由で共有しました。
どれがどこまで共有されているかがgitだと確かにできるので心配事が減りました。
TSUBAMEとジョブスケジューラーへの慣れについて
TSUBAMEには-gをつけないことでお試し実行の機能があり、10分までなら2ノードまで学内生なら無料で使えます。ジョブスケジューラの使い方などはその環境で以下のような適当なジョブスクリプトを実行してみると良いと思います。
#!/usr/bin/env bash
#$ -cwd
#$ -l node_h=2
#$ -l h_rt=0:01:00
#$ -v PATH
echo $PE_HOSTFILE
cat $PE_HOSTFILE
echo "$(awk '{print $1}' "$PE_HOSTFILE" 2>/dev/null | paste -sd "," -)"
hostname
echo $PATH
nvidia-smi
module avail
qsub test2.sh
/apps/t4/rhel9/uge/latest/bin/lx-amd64/qsub test2.sh