ジョブスケジューラーOpenPBS/PBS Proの使い方

## 概要

AltairのOpenPBS／PBS Proについて、基本的な操作とジョブスクリプトの書き方を備忘録として記す。

## OpenPBSとは

Portable Badge System（PBS）は、ジョブスケジューリングを行うソフトウェアの名称である。90年代にNASAのプロジェクトとして開発されたが、その後、以下のように分岐している。

- OpenPBS: オープンソース版の元祖（1998年にリリース）
- [TORQUE](https://adaptivecomputing.com/cherry-services/torque-resource-manager/): Adaptive Computing Enterprise Inc.により保守されているフォーク
  - バージョン6まではソースコードが公開されていたが、現在の最新版はクローズドソースとなっている
- [PBS Professional (PBS Pro)](https://www.altairjp.co.jp/pbs-professional/): Altair Engineeringにより保守されているバージョン
  - 商用と非商用の二重ライセンスで提供されていて、[非商用版は**OpenPBS**と呼ばれておりAGPL‐3で公開されている](https://github.com/openpbs/openpbs)

以下、本記事ではAltairのOpenPBSを指してPBSと言うことにする。

## 基本操作

次のように`qsub`のコマンドで、スクリプト`foo.sh`をPBSのキュー（queue）へ投入する（submit）ことができる。

```shell
% qsub foo.sh
140.host1
```

このコマンドを実行するとジョブIDが出力され、上の例では`140.host1`が投入したジョブのIDとなる。

キューの状態を見るには`qstat`コマンドを使用する。

```shell
% qstat
Job id            Name             User              Time Use S Queue
----------------  ---------------- ----------------  -------- - -----
140.host1         foo              shinobu                  0 R ALL
```

すべての情報を見たい場合は`-a`オプションを使用する。

```
% qstat -a 140.host1

host1:
                                                            Req'd  Req'd   Elap
Job ID          Username Queue    Jobname    SessID NDS TSK Memory Time  S Time
--------------- -------- -------- ---------- ------ --- --- ------ ----- - -----
140.host1       shinobu  Q1       foo        184916   2 128    --  00:01 R 00:00
```

ヘッダーの各カラムが示す意味を記しておこう。

- `Job ID`：PBSによって割り当てられるジョブの識別子
- `Username`：ジョブの所有者のユーザー名
- `Queue`：そのジョブが所属するキューの名前
- `SessID`：セッションID。ジョブの実行中にのみ表示される
- `NDS`：そのジョブが要求したchunkまたはvnodeの数
- `TSK`：そのジョブが要求したCPUの数
- `Req'd Memory`：そのジョブが要求したメモリの総量
- `Req'd Time`：そのジョブが要求したwalltimeまたはCPU時間
- `S`：ジョブの状態（State）を指す
  - `R`ならば、実行中、
  - `Q`ならば、キューに入れられて待機中、
  - `E`ならば、終了処理中である。（その他は`man qstat`を参照）
- `Elap Time`：実時間またはCPU時間での経過時間

## ジョブスクリプトの書き方

### 基本の書き方

PBSのジョブスクリプトは`qsub`コマンドによってPBSに渡され、ジョブが待ち行列（キュー）に入れられて、スクリプトの実行を待機することになる。以下に簡単なジョブスクリプトの例を示す。

```shell
#!/bin/bash
### sample script for PBS
#PBS -q Q1
#PBS -N foo
#PBS -l select=2:ncpus=64:mpiprocs=64
#PBS -l walltime=60

cd $PBS_O_WORKDIR
PETOT=$(wc -l < $PBS_NODEFILE)
mpirun -np $PETOT ./foo-executable
```

第1行は、シェバンである。OSにインタプリタを指定するヘッダーである。

第2行は、シェルのコメントであり、このスクリプトの概要を説明している。

行頭が`#PBS`で始まる第3行から第6行は、PBSへの命令である。
  - 第3行は、ジョブを投入するキューを`-q`オプションで指定する。この例では`Q1`という名前のキューを指定
  - 第4行は、ジョブの名前を`-N`オプションで指定する
  - 第5行は、リソースのリストを`-l`オプションで指定する。この例では2ノード×64並列のリソースを要求する
  - 第6行は、同様に`-l`オプションで、実行時間60秒を要求する

第8行は、カレントディレクトリをジョブを投入した（`qsub`を実行した）ディレクトリ（変数`PBS_O_WORKDIR`に保存されている）に移動している。これは最後の行での相対パスによる実行ファイルの指定を行うために必要となる。

第9行は、上のPBSへの命令で要求したCPU数を、変数`PBS_NODEFILE`に示されたファイルから`wc`コマンドでカウントして取得し、変数`PETOT`に代入している。

最後の行は、MPIの実行コマンドである。上で定義した変数`PETOT`を使用して並列数を指定し、実行している。

### リソースリスト

> ```
> Requesting Resources and Placing Jobs
>    Requesting  resources includes setting limits on resource usage and controlling
>    how the job is placed on vnodes.
> 
>    Resources are requested by using the -l option, either in job-wide requests us‐
>    ing <resource name>=<value> pairs, or in chunks inside of selection statements.
>    See the pbs_resources(7B) man page.
> 
>    Job-wide <resource name>=<value> requests are of the form:
>       -l <resource name>=<value>[,<resource name>=<value> ...]
> 
>    The selection statement is of the form:
>       -l select=[<N>:]<chunk>[+[<N>:]<chunk> ...]
>    where N specifies how many of that chunk, and a chunk is of the form:
>       <resource name>=<value>[:<resource name>=<value> ...]
> ```
>
> —`man qsub` より引用。

`-l`オプション（List）では、ジョブの処理に必要なリソースのリストを記述する。`select`の等号の次にノード数を指定し、後ろにコロン`:`区切りで、項目をノードあたりの情報（ドキュメントではchunkと呼ばれる）として指定する。リソースの項目には`ncpus`, `mpiprocs`, `mem`などがある。

上の例では、

```
#PBS -l select=2:ncpus=64:mpiprocs=64
```

としたので、これは、2ノード要求し各ノードに64CPUと64MPIプロセス、つまり合計で128のリソースを要求している。

指定できるリソースの詳細は`man pbs_resources`を参照。

#### ノードのリソースが一様でない場合

自作のクラスターマシンの場合など、すべてのノードで同じCPU数に揃えるのは難しいかもしれない。そのような場合は、`-l`オプションに渡す`select`の値に`+`の記号を使うことで、リソースの細かい指定ができる。

```
#PBS -l select=1:ncpus=64:mpiprocs=64+1:ncpus=16:mpiprocs=16
```

この例では、1ノードから64CPUをとり、更にもう1ノードから16CPUを取るように命令している。

`+1`以外にも（計算資源の許す範囲で）好きな非負整数を選ぶことができる。たとえば、64CPUが1ノード、8CPUが3ノード、4CPUが3ノードのリソースを使う場合は次のとおり：

```
#PBS -l select=1:ncpus=64:mpiprocs=64+3:ncpus=8:mpiprocs=8+3:ncpus=4:mpiprocs=4
```

#### `mpiprocs`

これを省略した場合、デフォルトではMPIプロセスがノードあたり1個と仮定されてしまうので、フラットMPI並列を実行する場合は必ず指定するのを忘れないこと（指定しなかった場合、具体的には、`$PBS_NODEFILE`の指すファイルの行数が1ノード1行となる）。

### 標準出力と標準エラー出力を1つのファイルに出力する

デフォルトでは、ジョブの実行で生じた標準出力と標準エラー出力は別々のファイルに保存される。上で投入したジョブを例にすると2つのファイル`foo.o140`と`foo.e140`の二つのファイルが生成される。

これらを1つのファイルに統合するには`-j`オプション（Join）を使用する。

```
#PBS -j oe
```

と記述すれば、標準エラー出力は標準出力（`foo.o140`）に統合され、

```
#PBS -j eo
```

と記述すれば、標準出力は標準エラー出力（`foo.e140`）に統合される。

```
#PBS -j n
```

と記述した場合は、標準エラー出力と標準出力は統合されない。

## 環境

Gentoo LinuxでビルドしたOpenPBSバージョン23.06.06とOpenMPI 4.1.6を使用した。

## 参考文献

cf. [Altair Community - Documentation](https://community.altair.com/community?id=altair_product_documentation)より

- "Altair PBS Professional 2024.1 - User's Guide"

その他、以下のコマンド

- `man qsub`
- `man qstat`
- `man pbs_resources`