ジョブスケジューラPBSProでGPU計算クラスタを組みAIを効率的に学習させる方法 (後編)

# はじめに

こんにちは、(株)日立製作所 研究開発グループ サービスコンピューティング研究部の露木です。

[前編の記事](https://qiita.com/MasafumiTsuyuki/items/acf4b19b54937496e32b)ではPBSProのパッケージをインストール・設定して，CPUを利用するジョブを実行可能にするまでを実施しました。後編となる今回の記事では，GPUを利用したnvidia-dockerによるAI学習用のジョブを実行可能にする手順をご紹介します。

[前編](https://qiita.com/MasafumiTsuyuki/items/acf4b19b54937496e32b)で説明した内容は実施済みであることを前提としますので，未実施の場合はまず[前編](https://qiita.com/MasafumiTsuyuki/items/acf4b19b54937496e32b)をご覧ください。

また，同じアドベントカレンダーに[Slurm HPCクラスタとKubernetesを同居させてみた（前編） - Qiita](https://qiita.com/mk32/items/6b4f9a7e5ae82eb640d9) があり，こちらもHPC向けジョブスケジューラの話題になります。PBSは歴史のある枯れたジョブスケジューラですが，SlurmやKubernetesのような新しい技術に興味のある方はぜひ，そちらの記事もご覧ください。

## GPUを計算リソースとして管理する設定

早速ですが，GPUを計算リソースとして管理するために設定を追加していきます。

まず，管理ノードで以下のコマンドを実行します。

```
sudo /opt/pbs/bin/qmgr -c "create resource ngpus type=long, flag=nh"
```

管理ノードの設定ファイル`/var/spool/pbs/sched_priv/sched_config`を編集し，`resources:` の行に `ngpus`を追加します。追加後の`resources:`の行は下記のようになります。

```
resources: "ncpus, mem, arch, host, vnode, netwins, aoe, ngpus"
```

PBSProを再起動し，設定を反映させます。

```
sudo /etc/init.d/pbs restart
```

計算ノードごとにGPUの本数を登録します。例えば計算ノード`sio`にGPUが3本，別の計算ノード`miso`にGPUが2本ある場合は下記のコマンドを実行します。

```
sudo /opt/pbs/bin/qmgr -c "set node sio resources_available.ngpus=3"
sudo /opt/pbs/bin/qmgr -c "set node miso resources_available.ngpus=2"
```

ここで，GPUの登録を動作確認します。 `pbsnodes` コマンドで計算ノード一覧を見ると，追加した設定に対応してngpusの項目が増えていることがわかります。

```
$ pbsnodes -a

miso
     Mom = miso
     ntype = PBS
     state = free
     pcpus = 8
     resources_available.arch = linux
     resources_available.host = miso
     resources_available.mem = 32803912kb
     resources_available.ncpus = 8
     resources_available.ngpus = 2
     resources_available.vnode = miso
     resources_assigned.accelerator_memory = 0kb
     resources_assigned.hbmem = 0kb
     resources_assigned.mem = 0kb
     resources_assigned.naccelerators = 0
     resources_assigned.ncpus = 0
     resources_assigned.vmem = 0kb
     resv_enable = True
     sharing = default_shared
     last_state_change_time = Tue Aug  6 20:33:19 2019
     last_used_time = Tue Aug  6 19:16:10 2019

sio
     Mom = sio
     ntype = PBS
     state = free
     pcpus = 48
     resources_available.arch = linux
     resources_available.host = sio
     resources_available.mem = 198036808kb
     resources_available.ncpus = 48
     resources_available.ngpus = 3
     resources_available.vnode = sio
     resources_assigned.accelerator_memory = 0kb
     resources_assigned.hbmem = 0kb
     resources_assigned.mem = 0kb
     resources_assigned.naccelerators = 0
     resources_assigned.ncpus = 0
     resources_assigned.vmem = 0kb
     resv_enable = True
     sharing = default_shared
     last_state_change_time = Tue Aug  6 20:33:19 2019
```

動作確認のために，GPUの本数を指定してジョブを投入します。
具体的には nvidia-docker を使って nvida-smi コマンドを実行するジョブです。
上手く行けばGPUの情報がジョブ実行結果として保存されるはずです。

```
echo 'sleep 30; docker run --gpus 3 --rm nvidia/cuda nvidia-smi' | qsub -l select=host=miso:ngpus=1
echo 'sleep 30; docker run --gpus 3 --rm nvidia/cuda nvidia-smi' | qsub -l select=host=sio:ngpus=1
```

ジョブ投入結果を確認します。misoとsioに1つずつ，GPUを1本専有したジョブが流れている事がわかります。

```
$ qstat -s

miso:
                                                            Req'd  Req'd   Elap
Job ID          Username Queue    Jobname    SessID NDS TSK Memory Time  S Time
--------------- -------- -------- ---------- ------ --- --- ------ ----- - -----
715.miso        tsuyuki  batch    STDIN       27818   1   1    --  100:0 R 00:00
   Job run at Tue Aug 06 at 15:52 on (miso:ngpus=1:ncpus=1)
716.miso        tsuyuki  batch    STDIN       31988   1   1    --  100:0 R 00:00
   Job run at Tue Aug 06 at 15:52 on (sio:ngpus=1:ncpus=1)
```

しばらくするとジョブが終了し，`qstat`コマンドで何も表示されなくなります。

```
$ qstat
```

ジョブが正しく終了していれば，カレントディレクトリに実行結果のファイルが保存されているはずです。

```
$ ls
STDIN.e715
STDIN.e716
STDIN.o715
STDIN.o716
```

拡張子が `.e` のファイルには標準エラー出力が保存されています。今回はdockerコマンドをジョブとして実行したため，コンテナイメージを取得したログが保存されています。

```
$ cat STDIN.e716
Unable to find image 'nvidia/cuda:latest' locally
latest: Pulling from nvidia/cuda
7413c47ba209: Already exists
0fe7e7cbb2e8: Already exists
1d425c982345: Already exists
344da5c95cec: Already exists
43bcc41986db: Pulling fs layer
76661327d908: Pulling fs layer
abdc887b90e5: Pulling fs layer
eb38470de0e2: Pulling fs layer
0adb8d7f107f: Pulling fs layer
eb38470de0e2: Waiting
0adb8d7f107f: Waiting
abdc887b90e5: Verifying Checksum
abdc887b90e5: Download complete
43bcc41986db: Verifying Checksum
43bcc41986db: Download complete
76661327d908: Verifying Checksum
76661327d908: Download complete
43bcc41986db: Pull complete
76661327d908: Pull complete
abdc887b90e5: Pull complete
eb38470de0e2: Verifying Checksum
eb38470de0e2: Download complete
eb38470de0e2: Pull complete
0adb8d7f107f: Verifying Checksum
0adb8d7f107f: Download complete
0adb8d7f107f: Pull complete
Digest: sha256:b89fbc1c8c238f6e838d4394cc4a9fdbb4ea9a3c2f7058cc9700fab3e8c6651b
Status: Downloaded newer image for nvidia/cuda:latest
```

拡張子が `.o` のファイルには標準出力が保存されています。今回はdockerコンテナ内部で実行した`nvidia-smi`コマンドの出力結果が保存されることになります。

下記の`nvidia-smi`の実行結果をみればわかるように，GPUは3本認識されてしまっています。ジョブ投入時の `qsub`コマンドでは `ngpus=1` としてGPU1本だけ専有するように指定したのですが，ここまでの設定だけではジョブ実行時にリソースが正しく制限されていないことがわかります。PBSProで指定するGPUの本数とジョブが実際に利用するGPUの本数を一致させないと事故の元なので要注意です。

```
$ cat STDIN.o716
Tue Aug  6 15:51:11 2019
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 430.40       Driver Version: 430.40       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  Off  | 00000000:01:00.0 Off |                  N/A |
| 29%   29C    P8     7W / 250W |     10MiB / 11178MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  GeForce GTX 108...  Off  | 00000000:02:00.0 Off |                  N/A |
| 29%   29C    P8     8W / 250W |     10MiB / 11178MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   2  GeForce GTX 108...  Off  | 00000000:03:00.0 Off |                  N/A |
| 29%   27C    P8     7W / 250W |     10MiB / 11178MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+
```

## cgroupsによるGPUリソースの分離

ここまでの設定ではジョブの利用可能なリソースを制限する機能がありません。先の例で示したように，qsubコマンドでジョブ投入する際にはGPUを1本しか使わないと言いつつ，実際には3本を利用するような使い方ができてしまうわけです。それではGPUのメモリを使い果たして他人のジョブが異常終了するなど，障害が発生してしまいます。

そこで，Linuxのcgroupsを利用してリソース制限する設定を追加します。本節の設定を追加すれば，例えば `ngpus=2` を指定したジョブに対しては 環境変数`CUDA_VISIBLE_DEVICES=0,1` が自動的に設定され，GPUを2本までしか使えないようになります。 (細かい話をすると環境変数は本質ではなく，cgroupsによる制限で実際に2本までしかGPUが見えなくなるのですが詳細は割愛します)。

具体的には，以下の2つの設定を加えていきます。

1. PBSProの側に仮想的な計算ノードvnodeを設定する
2. Linuxカーネルのcgroupsを自動設定してGPUの分離を実現するhookを有効化

### vnodeの設定

まず，下記コマンドを実行して gpu_id` をリソースとして追加定義します。

```
sudo /opt/pbs/sbin/qmgr -c "create resource gpu_id type=string, flag=h"
```

`/var/spool/pbs/sched_priv/sched_config` の`resources:`の行に `gpu_id` を追加します。

```
resources: "ncpus, mem, arch, host, vnode, aoe, eoe, ngpus, gpu_id"
```

PBSProを再起動して設定変更を反映します。

```
sudo /etc/init.d/pbs restart
```

次に，GPUごとに仮想的な計算ノード`vnode`の設定ファイルを作成します。例えば，計算ノード`miso`の設定ファイルとして `miso_vnodes`というファイルを下記の内容で作成します。ここでは`miso`がGPUを2本積んでいる計算ノードなので，`miso[0]`と`miso[1]`という2つのvnodeを設定して管理することになります。また，`miso`は32GBのメモリと4コアのCPUを持つので，2つの`vnode`に均等に配分しています。

```
$configversion 2
miso: resources_available.ncpus = 0
miso: resources_available.mem = 0
miso[0]: resources_available.ncpus = 4
miso[0]: resources_available.mem = 16gb
miso[0]: resources_available.ngpus = 1
miso[0]: resources_available.gpu_id = gpu0
miso[0]: sharing = default_excl
miso[1]: resources_available.ncpus = 4
miso[1]: resources_available.mem = 16gb
miso[1]: resources_available.ngpus = 1
miso[1]: resources_available.gpu_id = gpu1
miso[1]: sharing = default_excl
```

以下のコマンドを実行し，作成したvnodeの設定ファイル`miso_vnodes`をPBSProの設定ファイル群があるディレクトリへコピーします。

```
/opt/pbs/sbin/pbs_mom -s insert miso_vnodes miso_vnodes
```

このようなvnodeの設定ファイルの作成とコピーをすべての計算ノードで実行します。
vnodeの設定が終わったら，設定ファイルのpbsのデーモンを再起動して設定変更を反映します。

```
sudo /etc/init.d/pbs restart
```

設定の反映を確認します。これでvnodeの設定は終了です。

```
$ pbsnodes -v 'miso[0]' 'miso[1]'
miso[0]
     Mom = miso
     ntype = PBS
     state = free
     resources_available.arch = linux
     resources_available.gpu_id = gpu0
     resources_available.host = miso
     resources_available.hpmem = 0b
     resources_available.mem = 33522974720b
     resources_available.ncpus = 4
     resources_available.ngpus = 3
     resources_available.vmem = 35602300928b
     resources_available.vnode = miso[0]
     resources_assigned.accelerator_memory = 0kb
     resources_assigned.hbmem = 0kb
     resources_assigned.mem = 0kb
     resources_assigned.naccelerators = 0
     resources_assigned.ncpus = 0
     resources_assigned.ngpus = 0
     resources_assigned.vmem = 0kb
     resv_enable = True
     sharing = default_excl
     last_state_change_time = Wed Aug  7 23:27:42 2019
     last_used_time = Wed Aug  7 23:27:42 2019

miso[1]
     Mom = miso
     ntype = PBS
     state = free
     pcpus = 4
     resources_available.arch = linux
     resources_available.gpu_id = gpu1
     resources_available.host = miso
     resources_available.mem = 16gb
     resources_available.ncpus = 4
     resources_available.ngpus = 1
     resources_available.vnode = miso[1]
     resources_assigned.accelerator_memory = 0kb
     resources_assigned.hbmem = 0kb
     resources_assigned.mem = 0kb
     resources_assigned.naccelerators = 0
     resources_assigned.ncpus = 0
     resources_assigned.vmem = 0kb
     resv_enable = True
     sharing = default_excl
     last_state_change_time = Wed Aug  7 23:16:34 2019
```

### cgroups用hookの設定

PBSProにおいてcgroupsの設定は，ジョブ実行時のhookでpythonスクリプトを実行して実現しています。そのPythonスクリプトの設定ファイルを編集するために，まずは現在の設定を`pbs_cgroups.json`というファイルにエクスポートします。

```
sudo /opt/pbs/bin/qmgr -c "export hook pbs_cgroups application/x-config default" > pbs_cgroups.json
```

この`pbs_cgroups.json`の中身を下記のように変更します。

- vnode_per_numa_nodeをtrueに変更 (vnodeで利用するCPUが同じ物理CPUに収まるように指定)
- devicesの enabled をtrueに変更 (cgroupによるデバイスのアクセス制限を有効化)
- devicesの allow にnvidia-uvm等のデバイスを追加 (nvidia-docker関連のデバイスは常にアクセスを許可する)

一部，環境に依存しますが変更後の `pbs_cgroups.json` の中身は以下のようになります。

```
{
    "cgroup_prefix"         : "pbspro",
    "exclude_hosts"         : [] ,
    "exclude_vntypes"       : ["no_cgroups"],
    "run_only_on_hosts"     : [],
    "periodic_resc_update"  : true,
    "vnode_per_numa_node"   : true,
    "online_offlined_nodes" : true,
    "use_hyperthreads"      : false,
    "ncpus_are_cores"       : false,
    "cgroup" : {
        "cpuacct" : {
            "enabled"            : true,
            "exclude_hosts"      : [],
            "exclude_vntypes"    : []
        },
        "cpuset" : {
            "enabled"            : true,
            "exclude_cpus"       : [],
            "exclude_hosts"      : [],
            "exclude_vntypes"    : [],
            "mem_fences"         : true,
            "mem_hardwall"       : false,
            "memory_spread_page" : false
        },
        "devices" : {
            "enabled"            : true,
            "exclude_hosts"      : [],
            "exclude_vntypes"    : [],
            "allow"              : [
                "b *:* m",
                "c *:* m",
                "c 195:* m",
                "c 136:* rwm",
                ["fuse","rwm"],
                ["net/tun","rwm"],
                ["tty","rwm"],
                ["ptmx","rwm"],
                ["console","rwm"],
                ["null","rwm"],
                ["zero","rwm"],
                ["full","rwm"],
                ["random","rwm"],
                ["urandom","rwm"],
                ["cpu/0/cpuid","rwm","*"],
                ["nvidia-modeset", "rwm"],
                ["nvidia-uvm", "rwm"],
                ["nvidia-uvm-tools", "rwm"],
                ["nvidiactl", "rwm"]
            ]
        },
        "hugetlb" : {
            "enabled"            : false,
            "exclude_hosts"      : [],
            "exclude_vntypes"    : [],
            "default"            : "0MB",
            "reserve_percent"    : 0,
            "reserve_amount"     : "0MB"
        },
        "memory" : {
            "enabled"            : true,
            "exclude_hosts"      : [],
            "exclude_vntypes"    : [],
            "soft_limit"         : false,
            "default"            : "256MB",
            "reserve_percent"    : 0,
            "reserve_amount"     : "64MB"
        },
        "memsw" : {
            "enabled"            : true,
            "exclude_hosts"      : [],
            "exclude_vntypes"    : [],
            "default"            : "256MB",
            "reserve_percent"    : 0,
            "reserve_amount"     : "64MB"
        }
    }
}
```

作成した設定ファイル `pbs_cgroups.json` をPBSProにインポートします。

```
sudo /opt/pbs/bin/qmgr -c "import hook pbs_cgroups application/x-config default pbs_cgroups.json"
```

cgroupのためのhookを有効化します。

```
sudo /opt/pbs/bin/qmgr -c "set hook pbs_cgroups enabled = true"
```

PBSを再起動し，設定を反映させます。

```
sudo /etc/init.d/pbs restart
```

### 動作確認

動作確認のために `ngpu=1` を指定したジョブを実行します。

```
$ echo 'hostname; echo CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES; nvidia-smi ' | qsub -l select=host=miso:ngpus=1 -N test;
1576.miso
```

実行結果を確認します。cgroupsを利用しない場合と比較して，環境変数`CUDA_VISIBLE_DEVICES=0`が自動的に設定されており，`nvidia-smi`でも1本しかGPUが見えていないことがわかります。このようにcgroupsによってリソースの利用を制限し，リソース枯渇による障害発生を防止できます。

```
$ cat test.o1576
miso
CUDA_VISIBLE_DEVICES=0
Wed Aug  7 17:12:29 2019
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 430.40       Driver Version: 430.40       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  Off  | 00000000:01:00.0 Off |                  N/A |
| 29%   29C    P8     7W / 250W |     10MiB / 11178MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+
```

# まとめ

ここまで設定すれば，nvidia-dockerを使ってAIを学習させるジョブを安全に実行可能になります。

従来は，例えばハイパーパラメータの異なる1000個の学習ジョブを一度に実行すると計算リソース不足で異常終了しますし，学習ジョブの終了を人手で確認して次のジョブを実行するのでは大変な手間がかかってしまっていました。そこで，本記事ではジョブスケジューラを整備し，qsubコマンドを使ってジョブを登録するようにしました。これにより，計算リソースの空きを待ってから次のジョブが実行されるようになりますので，効率よく大量のAIを学習できます。


## [補足] Dockerコンテナでジョブを実行する設定

PBSProには，明示的に `docker run` コマンドを指定しなくてもコンテナ内部でジョブを実行する設定もありますが，今回は設定しませんでした。その理由はジョブのスクリプト内部で`docker run`するほうが環境変数等を柔軟に設定できるからです。また，PBSProのコンテナ統合機能だと，GPU利用時に`nvidia-docker`コマンドを内部的に実行するため，最近のnvidia-docker2に対応しているのか不明な問題もありました。

もし，コンテナ向けの設定を追加したい場合は [PBS Professional with Docker Integration](https://www.altair.com/resource/detail/4497) を参考に実施してみてください。

# 参考URL

- [PBS Works Documentation](https://www.pbsworks.com/SupportGT.aspx?d=PBS-Professional,-Documentation)
- [Scheduling Jobs onto NVIDIA Tesla GPU Computing Processors using PBS Professional](https://www.altair.com/resource/detail/469)
- [PBS Professional with Docker Integration](https://www.altair.com/resource/detail/4497)
- [GitHub - PBSPro/pbspro: An HPC workload manager and job scheduler for desktops, clusters, and clouds.](https://github.com/PBSPro/pbspro)
- [Building and Packaging PBS on Debian/Ubuntu - PBS Pro - PBS Pro Confluence](https://pbspro.atlassian.net/wiki/spaces/PBSPro/pages/22183970/Building+and+Packaging+PBS+on+Debian+Ubuntu)
- [Building PBS Pro Using rpmbuild - PBS Pro - PBS Pro Confluence](https://pbspro.atlassian.net/wiki/spaces/PBSPro/pages/13991940/Building+PBS+Pro+Using+rpmbuild)
- [pbspro/INSTALL at master · PBSPro/pbspro · GitHub](https://github.com/PBSPro/pbspro/blob/master/INSTALL)
- [pbspro - Chaperone](http://chaperone.sakura.ne.jp/w/index.php?pbspro)