TSUBAME4.0でのESPnet

# はじめに
[ESPnet](https://espnet.github.io/espnet/) は、音声分野での機械学習の際に使用されるツールセットです。
Kaldiというツールセットからの流れを汲んでおり、レシピという名前のシェルスクリプトを通した実験フローによる実験や、python pipライブラリとしての利用、あるいはそれらの実験の成果物(huggingfaceなどで公開されているものもあり)のモデルを使用した推論などの利用ができます。
また、[TSUBAME](https://www.t4.gsic.titech.ac.jp/)はH100で構成されるスパコンです。

今回はEspnetについてTSUBAME4.0上で、`egs2/librispeech/asr1`を実行したい場合の例に説明します。
なお、セットアップの方法は[公式サイト](https://espnet.github.io/espnet/installation.html)に譲りますが、Option A)にあるconda自体のインストールを行なってconda環境を整えるのが楽かなと思います。この場合、module でcondaのloadは不要です。

この記事を見るのが面倒な人は
```bash:egs2/librispeech/asr1/job.sh
#!/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を使用する場合、ジョブスケジューラを使用することになりますが、様々なめんどくささがあります。

1. 通常のジョブは24時間までしか使えない
2. 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のスタイル](https://kaldi-asr.org/doc/queue.html)です。qsubで投げる時のオプションについての設定を記述します。[espnetのドキュメント](https://github.com/espnet/espnet/blob/master/doc/parallelization.md)も参考にしてください。
ジョブスケジューラで使用できるリソース制限(-l)は基本的にはTSUBAMEドキュメントの通りですが、`qconf -sc`の実行でも確認できます。また、-pe は`Parallel Environment`を表します。[TSUBAMEのdocument](https://www.t4.gsic.titech.ac.jp/docs/handbook.ja/software/#cuda_mpi)には触れられていないため、不要かもしれません。`qconf -spl`で使用できる並列環境がリストされます。

```egs2/librispeech/asr1/conf/queue.conf
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 mail=* -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のモニターページ](http://mon.t4.gsic.titech.ac.jp:3000/d/kK13bVxIk/dashboard-list?orgId=3)の通り、非常に混み合っているからです。特にnode_fなど資源が多く必要なときは厳しさが増します。~~TSUBAME3よりGPUでかくなってノード数が減ったらまぁ。。~~ 
[TSUBAMEの料金](https://www.somuka.titech.ac.jp/reiki_int/reiki_honbun/x385RG00001339.html)は、ノード、優先度係数、実行時間、実行見積もり時間によって変わるので、いろんな要素を加味して設定します。
```bash:egs2/librispeech/asr1/cmd.sh
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のドキュメント](https://espnet.github.io/espnet/espnet2_distributed.html)があり、それを参考にします。launch.pyはcomming soonとあるので実装としても未完成ということかもしれません。おそらく多種多様なジョブスケジューラへのサポートの際にインターフェースが面倒なのでしょう。
 TSUBAME4でのOpenMPIや環境変数に関しては[TSUBAME4のジョブスケジューリングシステム](https://www.t4.gsic.titech.ac.jp/docs/all/handbook.ja/jobs/)に書いてあります。
 必要に応じてTSUBAME4のベースとなっている[Sun Grid Engineのドキュメント](https://docs.oracle.com/cd/E19957-01/820-2163/820-2163.pdf)も見ても良いかもしれません。
 ```python:espnet2/bin/launch.py
     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`で
 ```bash:egs2/librispeech/asr1/asr.sh
if [ -n "${cuda_lm_cmd:-}" ]; then
    _cmd="${cuda_lm_cmd}"
else
    _cmd="${cuda_cmd}"
fi
```
をするなどしてlmの学習の実行で使用するperl スクリプトを変更します。

さらに、TSUBAMEで必要なモジュールをロードするため、`local/path.sh`に記述します。
```bash:egs2/librispeech/asr1/local/path.sh
module load cuda
module load openmpi
```
この辺りはTSUBAM上で`module avail`を実行の上確認し、必要なものを入れてください。condaを使用している場合、不要な場合があります。

実行の際は以下のようなジョブスクリプトをqsub -g [グループ名] job.shなどで実行します。
```bash:egs2/librispeech/asr1/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分以上のプログラムに該当し、推奨されません](https://www.t4.gsic.titech.ac.jp/docs/handbook.ja/start/#login_limit)。このログインノードでの実行はtmuxのセッション自体を丸々強制終了の可能性があるので、stage10でレシピに後処理があるなど、ジョブが完了した後に処理があるステージの場合は注意が必要です。Open OnDemandとして提供されるVSCode上のターミナルの場合、無料で最大1日実行できるため、レシピスクリプトを動かすには最適だと思います。そうでない場合は、レシピスクリプト自体をcpu_4ノードなどで実行するか、レシピによって生成されたジョブスクリプトを手動で投げるのが良いかと思います。

```bash:egs2/librispeech/asr1/job_lm.sh
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あたりに置いておく。

```bash:espnet/tools/mytool.sh
#!/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
```
```bash:espnet/tools/myenv.sh
#!/bin/bash
export SLACK_API_KEY=[SLACK_API_KEY]
export LINE_API_KEY=[LINE_API_KEY]
export MY_PLACE=tsubame4
```
ジョブスクリプトへのAPIキーの直書きはやめましょう。
TSUBAMEの設定的には、ジョブスクリプトは(多分)全員が見ることが可能ですが、環境変数はおそらく権限がないと見られないので、環境変数で設定すると良いと思います。環境変数をスクリプトに書く場合、そのスクリプト自体のアクセス制限をかけておきましょう。

```bash:cmd.sh
cmd_backend=${cmd_backend:-local}
```
```bash:egs2/librispeech/asr1/local/path.sh
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
```
```bash:egs2/librispeech/asr1/job.sh
# ~~~ スケジューラ関係処理 ~~~
. ./../../../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をつけることをお勧めします。



# 実験コードの編集
ESPNetはレシピ、pythonコード本体の二つのレイヤーで成り立ちます。
このツールセットは研究者らが中心となってメンテを行なっており、製品というよりは研究者向けのツールセット(言い方を変えるとレシピの集合体)であり、全体での整合性が取れていない部分が多くあります。CIによってテストが走るようになっていますが網羅的なものではなく、自力でプログラムをきちんと管理する力が求められます。
- とんでもないバグがある
    - 層の数を幾つにしても1層として学習されるPythonコードが存在しました。(流石にコミュニティーとしてまずいと感じたのでPull Requestを投げてマージされました)
        - コードの網羅性テストはクリアしており、表示上もN層に見えるため、プログラムを読まないと間違いに気づかないようなものだった
    - 多くの人が使うが、コミュニティーベースであり、個別のレシピ単位で見ると利用者、特にコードをきちんと精査する人は多くないので注意する
- 定義されている引数が実装されていない
    - 別のレシピの引数をグローバルに反映させていることなどが原因
    - エラーが発生すればラッキーだが、エラーが発生しないで通過することもあるので確認する
- Pythonのドキュメントが不十分である
    - 例 データのイテレータのキーについて、設定によっては暗黙のフォルダ配置に左右される。iter_typeがsequenceを使用する場合、shape_fileをキーにしてdata_path_and_name_and_typeでキーに対応するファイルパスを記載したファイルを指定するが、categoryの場合、data_path_and_name_and_typeで与えるパスの兄弟として存在するcategory2uttというファイル名をキーとして使用する
    - 既存のレシピで使用しているところを見てみれば整合性の取り方がわかります
- 条件の切り替えについて、nn.Moduleのself.training:boolを使用したりmode:stringを使用する、あるいはインスタンス挿入(Dependency Indjection)であったり条件分岐で制御する、同じクラスを継承しているのにその使い方に関して明らかにおかしな点がある
    - 地道にコードを追うしかないです
- huggingfaceで保存されているレシピの中には再現不可能(不明)なものが多く存在する
    - 自身の環境で行なった実験について、結果のみをespnetの公式リポジトリにマージしてあるケースがある
    - ほとんどのレシピではREADMEとしてscripts/utils/TEMPLATE_HF_README.mdをそのまま使用するため、run.shに引数として与えて実行してもそれらは保存されない。特に、recipe上では使用するが、trainのpythonに引数として与えない性質のもの(asrのspeed_perturbやspkのnormalize、num_nodesなど)は保存されないため、推測(world_sizeやログのimage、結果の値から)はできても本当の値は保存されているわけではない

いろいろ書きましたが、それでも今までの環境に比べると再現実験も拡張性もだいぶ良くなってきたんだと思います。

# 実験コードの編集ツール
VSCode に慣れた私にとって、ftpやscpでの共有はだいぶつらいものがありました。手動共有は共有したコードが何かわからなくなるし、VSCode拡張機能などでの自動共有は下手すると大事なコードが消えます。
TSUBAMEでは[VSCode Serverのサービスを公開](https://www.t4.gsic.titech.ac.jp/docs/ood/login/#overview)しており、なんと学内生なら無料で利用できます。これを使わない手はないので積極的に使いましょう。なお、そちらの機能を使わないで、ローカルのVSCodeの拡張機能として VSCode Remoteを使用した場合、拡張機能のプロセスをTSUBAMEに乱立させて迷惑をかけます([制限を喰らいます](https://www.t4.gsic.titech.ac.jp/node/47))。
ローカルのサーバーなどとの共有は実験結果を除いてgithub経由で共有しました。
どれがどこまで共有されているかがgitだと確かにできるので心配事が減りました。

# TSUBAMEとジョブスケジューラーへの慣れについて
TSUBAMEには-gをつけないことで[お試し実行](https://www.t4.gsic.titech.ac.jp/docs/all/handbook.ja/jobs/#trial_run)の機能があり、10分までなら2ノードまで学内生なら無料で使えます。ジョブスケジューラの使い方などはその環境で以下のような適当なジョブスクリプトを実行してみると良いと思います。
```sh
#!/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
```