ホーム > サポート > FAQ(よくある質問) > プロセスとスレッドの割り当て方

プロセスとスレッドの割り当て方

最終更新日:2019年4月1日
CPUソケットの優先順位についての情報を修正

サブシステムA/Bの各計算ノードにはそれぞれ2つのCPUが搭載されており、 メモリやGPU、ネットワークカードから各CPUへの距離には違いがあります。 そのためプログラムを最適化する際には、対象プログラムがどちらのCPU上の計算コアによって実行されているのか、 そのプログラムがアクセスしようとしているメモリやGPUやネットワークカードが どちらのCPUに近い位置に存在するのかを意識する必要があります。

このページではプロセスやスレッドの割り当てを最適化するためのいくつかの方法について説明します。

富士通コンパイラ・富士通MPIを使用する場合は バッチジョブスクリプトの設定により割り当てが行われます。 詳しくはバッチ利用法(富士通コンパイラ)をご覧ください。
また、「ノード内複数ジョブ共有可」なリソースグループを使う場合は、 バッチジョブ管理システムに割り当てられた情報とジョブスクリプト内で指定している情報に齟齬があると プログラム実行不能(エラー終了)などの問題が発生するため、 資源の割り当てに関する指定を厳しくはせずに実行することを推奨します。 (プロセスやスレッドの数に関する最低限の指定のみ行ってください。)



ハードウェア構成の理解

プロセスやスレッドの割り当てを考える上で、対象となる計算ノードの構成を理解しておくことは重要です。 サブシステムA/Bの各計算ノードのハードウェア構成(概要)は以下のようになっています。


サブシステムAのハードウェア構成


サブシステムBのハードウェア構成

CPU0上のプロセスやスレッドから見たとき、CPU1側のメモリへのアクセスはCPU0側のメモリへのアクセスよりも時間がかかります。 同様に、 GPU0からCPU1側のメモリへのアクセスはCPU0側のメモリへのアクセスと比べて時間がかかりますし、 CPU0側のメモリ上のデータを他のノードへ転送する際にはCPU1側のメモリ上のデータを転送するのと比べて時間がかかります。 大規模・複雑なプログラムにおいてこれらの性能差をすべて考慮して最適化を行うのは容易ではありませんが、 できるだけ高い性能を得たい場合には気を付けなくてはなりません。

なお、ITOの計算ノードのようにメモリとプロセッサの距離が均等でないアーキテクチャを non-unified memory architecture (NUMA)と呼びます。 これに対して、「京」コンピュータやFX10/FX100のようなメモリとプロセッサの距離が均等であるアーキテクチャを unified memory architecture (UMA)と呼びます。 現在のHPC向け計算機環境においてはNUMAが主流です。


単一プロセス複数スレッド(OpenMP並列化)プログラムにおけるスレッドの割り当てとファーストタッチ

numactlによるスレッドとメモリの割り当て

OpenMPプログラムにおいては各スレッドがノード内のメモリを共有して自由に利用することができますが、 スレッドとアクセスするメモリが同一のCPUソケット側にある場合と別々のCPUソケット側にある場合ではアクセス性能が異なります。 さらに、Skylake-SPに関わらず現代のHPC向けCPUはキャッシュを備えていますが、 スレッドと対象メモリアドレスに対するキャッシュが同一のCPUソケットに存在する場合と異なるCPUソケットに存在する場合ではアクセス性能に差が生じます。

具体的な最適化の方法としては、 スレッドと対象メモリが同じCPUソケットに割り当たるように配置することや、 キャッシュが適切な配置になるようファーストタッチを行うことが挙げられます。

プロセス・スレッドと計算コア・メモリの割り当ての対応を制御するにはnumactlコマンドを用います。 (※ここではNUMAアーキテクチャとnumactlコマンドの一般的な詳細解説は省略し、主な使用方法と推奨オプションのみ解説します。) サブシステムBにてnumactlを用いてハードウェア情報を取得すると以下のような情報が得られます。

$ numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
node 0 size: 195237 MB
node 0 free: 189453 MB
node 1 cpus: 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
node 1 size: 196608 MB
node 1 free: 190379 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

$ numactl -s
policy: default
preferred node: current
physcpubind: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
cpubind: 0 1
nodebind: 0 1
membind: 0 1
※サブシステムAでもメモリ容量が異なる以外は同様です


ITOのサブシステムA/Bの計算ノードは1CPUソケットあたり1NUMAの構成になっており、 CPU0上の全18コア(0-17)とそこから接続されたDDR4メモリがcpubind/nodebind/membindの0に対応、 CPU1上の全18コア(18-35)とそこから接続されたDDR4メモリがcpubind/nodebind/membindの1に対応しています。

対象プログラムの実行時に実行対象プロセス(対象の1プロセスの中に含まれる全スレッド)とアクセスするメモリが常に同一のCPU側に限定されるようにする簡単な方法は
$ numactl --localalloc ./a.out
という指定で実行することです。 さらに、実行されるCPUソケットを指定したい場合には
$ numactl --cpunodebind=1 --localalloc ./a.out
のように--cpunodebindオプションで使用するCPUソケットを指定します。 (--cpunodebind=1と--localallocの記述順序が逆でも問題ありません。-N 1でも--cpunodebind=1と同じ意味になります。--localallocは-lと書くこともできます。)

一方、実行対象プロセスにノード上のメモリ全体を自由に使わせたい場合には
$ numactl --interleave ./a.out
と指定します。 なお、--localalloc/--interleaveではなく--membindでメモリを明確に指定しても同様の効果が得られます。 (--membindで--interleave相当のことを指定する場合には--membind=0,1と指定します。) 対象プロセスが大容量のメモリに対して広範囲にランダム的にアクセスするようなプログラムにおいてはinterleaveの方が高い性能が期待できるでしょう。


ファーストタッチ

ファーストタッチは、対象変数(配列)に対するキャッシュが対象変数に最初にアクセスしたスレッドの存在する計算コアのキャッシュに割り当てられることを利用した最適化手法です。 具体的には、並列計算ループで行うのと同じ配列アクセスパターンの初期化ループを用意します。簡単な例を以下に示します。 (例はC言語ですが、Fortranでも同様となります。)

// 追加する初期化ループ(対象配列に対するアクセスが初めて生じる位置に追加する)
#pragma omp parallel
for(i=0; i<N; i++){
  dst[i] = src[i];
  /*
  // 重要なのは各スレッドが配列のどの要素にアクセスするかであるため、
  // 例えばこちらのような書き方でも同様の効果が得られる
  dst[i] = 0;
  srt[i] = 0;
  */
}
// 実際に計算をするループ
#pragma omp parallel
for(i=0; i<N; i++){
  dst[i] = src[i] * value;
}
なお、動的ループスケジューリングを行う場合など、 実行毎に各スレッドがアクセスする配列のインデックスが異なる場合には効果が得られません。 CPUソケット間のデータ転送性能が高いほどファーストタッチの効果は低下します。


具体的なスレッド割り当ての指定方法

1ノードに1プロセスを割り当ててOpenMPプログラムを実行する場合、 連続するIDのスレッドを同一のCPUソケットに割り当てるか異なるCPUソケットに割り当てるかは性能に大きく影響しうる問題です。 特にメモリアクセス性能とキャッシュ性能がともに性能に大きく影響するプログラムの場合はスレッド数を抑えた方が (36スレッドではなく20から30スレッド程度に抑えた方が) キャッシュを有効に活用できて性能が向上する可能性があります。 しかし割り当て方を間違えると各CPUソケットに割り当てられるスレッド数が不均等になり性能低下を招きます。 そのため、考えたとおりにスレッドを各CPUソケットに割り当てることはとても重要です。

以下、具体的なスレッド割り当ての方法を紹介します。

インテルコンパイラを用いる場合には、プログラム実行時に環境変数KMP_AFFINITYによってスレッド割り当てを制御できます。 いくつかの指定パターンがありますが、 連続するIDのスレッドを近づけたい場合(一方のソケットを埋めてからもう一方のソケットを埋める場合)は KMP_AFFINITY=granularity=fine,compact、 連続するIDのスレッドを遠ざけたい場合(各ソケットを均等に埋めたい場合)は KMP_AFFINITY=granularity=fine,scatterを指定するのが基本です。 (explicit指定により他のコンパイラのようにコア番号を直接指定することも可能ですが詳細は省略します。) なにも指定しない場合はどのスレッドがどの計算コア上で実行されるかわからないばかりか、 実行状況によってはスレッドがそれまで実行されていた計算コアとは別のコアに移動させられることもあります。 これはキャッシュの扱いなどの都合で性能が低下する要因であり、回避した方が良いですが、 物理コア数を超える多数のスレッドにより動的スケジューリングを行いたいなど一部のプログラムでは逆に望ましいこともあります。 各スレッドが異なる計算コア上に固定されることを保証するだけで良い場合は、 環境変数OMP_PROC_BIND=TRUEを指定します。

GNUコンパイラを用いる場合には、 GOMP_CPU_AFFINITY環境変数を用いて使用するCPUコア番号を指定します。 CPUコア番号は/proc/cpuinfoのphysical idに関連付いており、 CPU0は0-17、CPU1は18-35です。 各スレッドがGOMP_CPU_AFFINITYで与えられた番号を順番に拾っていく方式のため、 36スレッド実行で連続するIDのスレッドを近づけたい場合(一方のソケットを埋めてからもう一方のソケットを埋める場合)は GOMP_CPU_AFFINITY="0-35"と書けば十分ですが、 連続するIDのスレッドを遠ざけたい場合(各ソケットを均等に埋めたい場合)は GOMP_CPU_AFFINITY="0 18 1 19 2 20 3 21 4 22 5 23 6 24 7 25 8 26 9 27 10 28 11 29 12 30 13 31 14 32 15 33 16 34 17 35" のようにIDを列挙する必要があります。 なお、各スレッドが異なる計算コア上に固定されることを保証するだけで良い場合は 環境変数OMP_PROC_BIND=TRUEのみで十分です。

PGIコンパイラを用いる場合には、環境変数MP_BIND=yを指定したうえで、MP_BLIST環境変数を用いてCPUコア番号を列挙します。 連続するIDのスレッドを近づけたい場合(一方のソケットを埋めてからもう一方のソケットを埋める場合)は MP_BLIST="0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35"、 連続するIDのスレッドを遠ざけたい場合(各ソケットを均等に埋めたい場合)は MP_BLIST="0,18,1,19,2,20,3,21,4,22,5,23,6,24,7,25,8,26,9,27,10,28,11,29,12,30,13,31,14,32,15,33,16,34,17,35" などと記述します。 なお、各スレッドが異なる計算コア上に固定されることを保証するだけで良い場合は 環境変数MP_BIND=yのみで十分です。


ジョブスクリプト設定例(プログラム実行例)

1CPU(CPU1)のみで1プロセス18スレッドのOpenMPプログラムを実行する場合の例

# Intel コンパイラを用いたプログラムの場合
export OMP_NUM_THREADS=18
export KMP_AFFINITY=granularity=fine,compact,0,18
numactl --localalloc ./a.out
# ,0,18を付けているためCPU1から使用されます。付けない場合にはCPU0から使用されます。

# GNU コンパイラを用いたプログラムの場合
export OMP_NUM_THREADS=18
export GOMP_CPU_AFFINITY="18-35"
numactl --localalloc ./a.out
# 0-35を指定することでCPU1のみが使われます。0-17を指定するとCPU0のみが使用されます。

# PGI コンパイラを用いたプログラムの場合
export MP_BIND=y
export OMP_NUM_THREADS=18
export MP_BLIST="18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35"
numactl --localalloc ./a.out
# MP_BLISTの指定でCPU1のみの使用を指示しています。"0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17"を
  指定すればCPU0のみが使用されます。
1ノード上で1プロセス36スレッドのOpenMPプログラムを実行する例:連続するIDのスレッドを近いCPUコア(なるべく同一のCPUソケット)に割り当てる場合
# Intel コンパイラを用いたプログラムの場合
export OMP_NUM_THREADS=36
export KMP_AFFINITY=granularity=fine,compact
numactl -l ./a.out

# GNU コンパイラを用いたプログラムの場合
export OMP_NUM_THREADS=36
export GOMP_CPU_AFFINITY="0-35"
numactl -l ./a.out

# PGI コンパイラを用いたプログラムの場合
export MP_BIND=y
export OMP_NUM_THREADS=36
export MP_BLIST="0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35"
numactl -l ./a.out
1ノード上で1プロセス36スレッドのOpenMPプログラムを実行する例:連続するIDのスレッドを遠いCPUコア(異なるCPUソケット)に割り当てる場合
# Intel コンパイラを用いたプログラムの場合
export OMP_NUM_THREADS=36
export KMP_AFFINITY=granularity=fine,scatter
numactl -l ./a.out

# GNU コンパイラを用いたプログラムの場合
export OMP_NUM_THREADS=36
export GOMP_CPU_AFFINITY="0 18 1 19 2 20 3 21 4 22 5 23 6 24 7 25 8 26 9 27 10 28 11 29 12 30 13 31 14 32 15 33 16 34 17 35"
numactl -l ./a.out

# PGI コンパイラを用いたプログラムの場合
export MP_BIND=y
export OMP_NUM_THREADS=36
export MP_BLIST="0,18,1,19,2,20,3,21,4,22,5,23,6,24,7,25,8,26,9,27,10,28,11,29,12,30,13,31,14,32,15,33,16,34,17,35"
numactl -l ./a.out


複数プロセス単一スレッド(フラットMPI並列化)プログラムにおけるプロセスの割り当て

MPIプログラムのプロセス配置はMPI実行時のオプションや環境変数によって決まり、 その指定方法はMPI処理系によって異なります。 MPIプログラムを実行する上では、 全MPIプロセス数の指定はもちろん非常に重要ですが、 それ以外にも、ノードあたりのMPIプロセス数や、 計算ノード内の2CPUソケットへのMPIプロセス割り当て順序 (典型的には、CPU0を埋めてからCPU1に割り当てるのか、CPU0とCPU1に交互に割り当てるのか) については指定をしたいという需要が高いと思われるため、 これらを中心に解説します。

なおMPI実行時の引数については、同じ意味を持つオプションが複数存在していることが多いです。 また、環境変数とコマンドへの引数の両方で指定可能なオプションが多数あり、両方した場合は引数が優先されます。 詳細についてはmanコマンドや関連ドキュメントを読んで確認してください。



IntelMPIの場合

IntelMPIを使う場合は、mpiexec.hydraへの引数やI_MPI_から始まる環境変数などを用いて動作を制御します。 -print-rank-mapオプションを使うと簡易的なランク割り当て情報を確認することもできます。 環境変数I_MPI_DEBUGを指定すれば詳細なランク割り当て情報を確認することもできます。 正しい割り当て情報が確認できていない間はI_MPI_DEBUG=5で実行することをおすすめします。

IntelMPIでは、プロセス数は実行時オプション-n、 ノードあたりプロセス数は実行時オプション-perhostや環境変数I_MPI_PERHOST、 プロセスの配置方法は環境変数I_MPI_PIN_DOMAINやnumactlコマンドの併用により制御します。

4ノードで各ノードに1プロセスずつ立ち上げる例:

#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=4
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S
module load intel/2017
export I_MPI_HYDRA_BOOTSTRAP=rsh
export I_MPI_HYDRA_BOOTSTRAP_EXEC=/bin/pjrsh
export I_MPI_HYDRA_HOST_FILE=${PJM_O_NODEINF}
export I_MPI_FABRICS=shm:ofa
export I_MPI_PERHOST=1
export I_MPI_DEBUG=5
mpiexec.hydra -n 4 ./a.out
vnode/vnode-coreで4ノードを確保し、-n 4で全4プロセスでの実行を指示、I_MPI_PERHOST=1でホストあたり1プロセスを明示しています。 I_MPI_PERHOST以外のI_MPI_から始まる環境変数はIntelMPIを使う限りは特別な事情のない限りこれと同じ設定が使えます。 環境変数I_MPI_PERHOSTによる指定を 実行時オプションに置き換えて
mpiexec.hydra -n 4 -perhost 1 ./a.out
などと指定することもできます。 さらにプロセスの配置を細かく指定したい場合、例えば各計算ノードのCPU1にMPIプロセスを配置したい場合は、 OpenMPと同様にnumactlを利用します。 サブシステムAではCPU0(コア0-17)、BではCPU1(コア18-35)が優先して使われるように 設定されているため 計算コアへの細かい割り当てを行う際には注意が必要です。

例えば、4ノードで各1プロセスずつ立ち上げ、各ノードのMPIプロセスは各計算ノードのCPU1上の計算コアに割り当てる、という場合には
※ ジョブオプションおよび環境変数の設定は上記と同様につき省略
# CPU1に接続されたメモリのみを使いたい場合はこちら(メモリアクセス性能重視)
mpiexec.hydra -n 4 numactl --cpunodebind=1 --localalloc ./a.out
# ノード上のメモリをすべて使いたい場合にはこちら(メモリ容量重視)
mpiexec.hydra -n 4 numactl --cpunodebind=1 --interleave ./a.out
などと指定します。 サブシステムAの各計算ノードのNICはCPU0側、 サブシステムBの各計算ノードのNICはCPU1側に搭載されており、 NICに近いCPU側にプロセスを割り当てた方が高いMPI性能が期待できます。 --cpunodebind=0と--cpunodebind=1を比較すると性能差が確認できるでしょう。 なお、IntelMPIではサブシステムAではCPU0から、サブシステムBではCPU1側から優先的にプロセスが割り当てられます。

IntelMPIで1ノードあたり複数プロセス実行を行う場合の割り当てについては、環境変数I_MPI_PIN_DOMAINを用いる方法が容易です。 「I_MPI_PIN_DOMAINで指定した数分だけの連続コア数領域」を「I_MPI_PERHOSTで指定した組数」だけ確保し、各領域に1MPIプロセスを割り当てるという挙動となります。 I_MPI_PIN_DOMAIN=18 と I_MPI_PERHOST=2 を指定すれば各CPUソケットに1MPIプロセスでノードあたり2MPIプロセス、 I_MPI_PIN_DOMAIN=9 と I_MPI_PERHOST=4 を指定すれば各CPUソケットに2MPIプロセスでノードあたり4MPIプロセス、 というような挙動となります。 この割り当て方法はMPI+OpenMPハイブリッド並列化の際にも有用です(後述)。

4ノード、各ノードのソケットごとに2プロセス、合計16MPI並列の例(I_MPI_PIN_DOMAIN版):
#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=4
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S
module load intel/2017
export I_MPI_HYDRA_BOOTSTRAP=rsh
export I_MPI_HYDRA_BOOTSTRAP_EXEC=/bin/pjrsh
export I_MPI_HYDRA_HOST_FILE=${PJM_O_NODEINF}
export I_MPI_FABRICS=shm:ofa
export I_MPI_PERHOST=4
export I_MPI_PIN_DOMAIN=9
export I_MPI_DEBUG=5
mpiexec.hydra -n 16 numactl --localalloc ./a.out

ノードあたり複数MPIプロセス実行で各プロセスの配置を細かく指定する方法としては、ランク番号とnumactlを併用する方法がよく用いられます。 mpiexec.hydraによって起動されたプロセスはランク情報を含む環境変数(自ID=PMI_RANK, 全プロセス数=PMI_SIZE)をもっているため、 そのランク情報を元にnumactlの引数を生成して本来実行したい対象のプログラムを起動するという二段階のプロセス起動を行います。 I_MPI_PERHOSTを使う方法よりやや難しい手順となりますが、より自由なプロセス割り当てが可能です。 ただし、物理コア番号まで計算して割り当てるのは手間がかかるわりに大きなメリットは考えにくく、 一般的にはソケット単位の割り当てまでで十分です。 特殊なスレッド配置を行いたい場合などにはこの方法を用いてください。

4ノード、各ノードのソケットごとに2プロセス、合計16MPI並列の例(numactl版):
job1.sh
#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=4
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S
module load intel/2017
export I_MPI_HYDRA_BOOTSTRAP=rsh
export I_MPI_HYDRA_BOOTSTRAP_EXEC=/bin/pjrsh
export I_MPI_HYDRA_HOST_FILE=${PJM_O_NODEINF}
export I_MPI_FABRICS=shm:ofa
export I_MPI_PERHOST=4
export I_MPI_DEBUG=5
mpiexec.hydra -n 16 ./job2.sh


job2.sh (chmodで実行権を与えておく必要あり)
#!/bin/bash
# number of cores per CPU and NODE
CORES1=18
CORES2=36

# localsize = (cores per node) per process
LOCALSIZE=$(( ${CORES2} / ${I_MPI_PERHOST} ))

# localrank = rank in each node : 0 ~ i_mpi_perhost-1
# localrank2 = rank in each socket : 0 ~ (i_mpi_perhost/2)-1
LOCALRANK=$(( ${PMI_RANK} % ${I_MPI_PERHOST} ))
LOCALRANK2=$(( ${PMI_RANK} % ( ${I_MPI_PERHOST} / 2 ) ))

# socketID = localrank / 2 : 0 or 1
SOCKETID=$(( ${LOCALRANK} / 2 ))

# offset = reverse-sockid * cores per cpu : 0 or 18
OFFSET=$(( ( 1 - ${SOCKETID} ) * ${CORES1} ))

# phycpunode = offset + localrank2*localsize
P=$(( ${OFFSET} + ${LOCALRANK2} * ${LOCALSIZE} ))

numactl --physcpubind=${P} --localalloc ./a.out


※実行時にはjob1.shをpjsubで実行する。
 実際に実行すると、--physcpubindには各ノードともに18, 27, 0, 9が順に指定される。
 これはIntelMPIによるCPU番号指定順序と一致している。


参考資料:Intel MPI Library Documentation



OpenMPIの場合

OpenMPIを使う場合は、mpirunへの引数を使用して動作を制御します。 -display-mapオプションや-display-devel-mapオプションを使うとランク割り当て情報を確認することもできます。

OpenMPIでは、プロセス数は実行時オプション-n、 ノードあたりプロセス数は実行時オプション-npernode、 プロセスの配置方法は--map-byや--bind-toなどの実行時オプションやnumactlコマンドにより制御します。

4ノードで各ノードに1プロセスずつ立ち上げる例:

#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=4
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S
module load cuda/8.0
export MODULEPATH=$MODULEPATH:/home/exp/modulefiles
module load exp-openmpi/1.10.7
mpirun -n 4 -npernode 1 -display-map --mca plm_rsh_agent /bin/pjrsh -machinefile ${PJM_O_NODEINF} ./a.out

4ノードで各ノードの各ソケットに2プロセスずつ(ノードあたり4プロセス、合計16MPIプロセス)立ち上げる例:
#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=4
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S
module load cuda/8.0
export MODULEPATH=$MODULEPATH:/home/exp/modulefiles
module load exp-openmpi/1.10.7
mpirun -n 16 --map-by ppr:2:socket -display-devel-map --mca plm_rsh_agent /bin/pjrsh -machinefile ${PJM_O_NODEINF} ./a.out
単純にノード内の複数MPIプロセスをcompactに(CPU0を埋めてからCPU1を埋める)使用したい場合は--map-by coreオプション、 scatterに(CPU0とCPU1を交互に埋める)使用したい場合は--map-by socketオプションを使用するのが簡単です。

IntelMPI同様にnumactlを用いればより細かい制御も行えます。 特にCPU1から優先的に割り当てるなどの詳細な制御を行いたい場合にはnumactlが必要となるでしょう。 (具体的な割り当て方法についてはIntelMPIの例を参照。) OpenMPIはノード内のランク番号なども環境変数として提供しているため有用です: OMPI_COMM_WORLD_LOCAL_RANK、OMPI_COMM_WORLD_LOCAL_SIZE、OMPI_COMM_WORLD_NODE_RANK、OMPI_COMM_WORLD_RANK、OMPI_COMM_WORLD_SIZE

なお、全プロセス数およびノードあたりプロセス数のみを指定した場合は、 ノード内のプロセス割り当てはscatterタイプになります。

参考資料:Open MPI Documentation



MVAPICHの場合

MVAPICHを使う場合は、mpirunへの引数やMV2_から始まる環境変数を用いて動作を制御します。 環境変数MV2_SHOW_CPU_BINDING=1を使うとランク割り当て情報を確認することもできます。

MVAPICHでは、プロセス数は実行時オプション-n、 ノードあたりプロセス数は実行時オプション-ppn、 プロセスの配置方法は実行時オプション-bind-toや-map-by、 環境変数MV2_CPU_BINDING_POLICYやMV2_CPU_BINDING_LEVELおよびnumactlコマンドにより制御します。

4ノードで各ノードに1プロセスずつ立ち上げる例:

#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=4
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S
module load cuda/8.0
export MODULEPATH=$MODULEPATH:/
module load exp-mvapich2/2.2
export MV2_SHOW_CPU_BINDING=1
mpirun -n 4 -ppn 1 -launcher-exec /bin/pjrsh -machinefile ${PJM_O_NODEINF} ./a.out

4ノードで各ノードの各ソケットに2スレッドずつ(ノードあたり4プロセス、合計16MPIプロセス)立ち上げる例:
#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=4
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S
module load cuda/8.0
export MODULEPATH=$MODULEPATH:/
module load exp-mvapich2/2.2
export MV2_SHOW_CPU_BINDING=1
export MV2_CPU_BINDING_POLICY=scatter
mpirun -n 16 -ppn 4 -launcher-exec /bin/pjrsh -machinefile ${PJM_O_NODEINF} ./a.out

ノード内の複数MPIプロセスの割り当てについては、 環境変数MV2_CPU_BINDING_POLICY=bunchを指定すれば詰めた(compactな)配置、 MV2_CPU_BINDING_POLICY=scatterを指定すれば分散した(scatterな)配置となります。 CPU1から順に埋めるなどの細かい指定を行いたい場合にはnumactlを使う必要があります。 (具体的な割り当て方法についてはIntelMPIの例を参照。) numactlを用いて制御する場合は環境変数MV2_ENABLE_AFFINITY=0の指定が必要であることに注意してください。
MVAPICHIはノード内のランク番号なども環境変数として提供しているため有用です: MV2_COMM_WORLD_RANK、MV2_COMM_WORLD_SIZE、MV2_COMM_WORLD_LOCAL_RANK

なお、全プロセス数およびノードあたりプロセス数のみを指定した場合は、 ノード内のプロセス割り当てはcompactタイプになりますが、 NICが搭載されているCPU側から優先的に埋められます。
(サブシステムAの場合はCPU0から、サブシステムBの場合はCPU1から。)


参考資料:MVAPICH User Guide


複数プロセス複数スレッド(MPI-OpenMPハイブリッド並列化)プログラムにおけるプロセスの割り当て

MPI-OpenMPハイブリッド並列化の際には、OpenMP並列化の効率化を考えると、 同一MPIプロセス内のスレッドが固まって配置されていることが望ましいです。 現実的に想定されるプログラムの実行形態は、 「各ノードに1MPIプロセス、1MPIプロセス内で(最大36スレッドの)OpenMP並列化」または 「各ノードに2MPIプロセス、各MPIプロセスはCPUソケットに対応し、各MPIプロセス内で(最大18スレッドの)OpenMP並列化」 であると考えられるため、これらに対応したジョブの実行方法を中心に解説します。


IntelMPIの場合

環境変数I_MPI_PERHOSTとI_MPI_PIN_DOMAINで作られたドメイン内でOpenMP並列化を行うのが基本です。 I_MPI_PIN_DOMAIN=omp:comapct(:compactはデフォルト値なので付けなくても良い)としておけば OMP_NUM_THREADSの値に合わせて連続した領域を確保してくれます。 サブシステムA/Bのように18コアCPUの場合、対象プログラムの制限によりコアを余らせたい(2の冪乗数スレッドのみ対応) ことがあるかもしれませんが、その場合はompの代わりに具体的なドメインの大きさを指定すると良いでしょう。
例:8スレッド*4プロセスで32コアのみを使いたい場合、 I_MPI_PIN_DOMAIN=omp:compactだとソケットを跨いだMPIプロセスが作られてしまうかも知れないが(されないという記述が見つかっていない)、 I_MPI_PIN_DOMAIN=9:compactにしておけばソケットを跨いだMPIプロセスは生成されない。

また、除外コアを指定し16コアCPU*2のように見せることでも予期せぬ割り当てを防ぐことができます。
export I_MPI_PIN_PROCESSOR_EXCLUDE_LIST=0,1,18,19
の一行を加えるとコア番号2-17および20-35のみが利用可能となり、1ソケットに16プロセスまでしか割り当てされなくなります。



2ノードで各ノードの各ソケットに1MPIずつ配置、各MPIプロセス内で18スレッドOpenMPスレッド並列化を行う例(4MPIプロセス*18スレッド):
#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=2
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S

module load intel/2017
export I_MPI_HYDRA_BOOTSTRAP=rsh
export I_MPI_HYDRA_BOOTSTRAP_EXEC=/bin/pjrsh
export I_MPI_HYDRA_HOST_FILE=${PJM_O_NODEINF}
export I_MPI_FABRICS=shm:ofa
export I_MPI_PERHOST=2
export I_MPI_PIN_DOMAIN=omp
export I_MPI_DEBUG=5
export OMP_NUM_THREADS=18
mpiexec.hydra -n 4 -print-rank-map numactl -l ./a.out
もちろん、OpenMPIやMVAPICHの例と同様にMPIランク情報を元にnumactlで細かく制御することも可能です。 (フラットMPIの項で紹介したように、IntelMPIではMPIプロセスごとのローカルランク情報は提供されないため、 環境変数PMI_RANKを元に計算を行う必要があります。)


OpenMPIの場合

MPIレベルでプロセス単位の割り当てを行い、詳細なスレッド割り当てはnumactl等を用います。 OMPI_COMM_WORLD_LOCAL_RANKなどの環境変数も有用です。

2ノードで各ノードの各ソケットに1MPIずつ配置、各MPIプロセス内で18スレッドOpenMPスレッド並列化を行う例(4MPIプロセス*18スレッド):
ito1.sh

#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=2
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S

module load cuda/8.0
export MODULEPATH=$MODULEPATH:/home/exp/modulefiles
module load exp-openmpi/3.0.0-intel

export OMP_NUM_THREADS=18
mpirun -n 4 -npernode 2 -display-devel-map --mca plm_rsh_agent /bin/pjrsh -machinefile ${PJM_O_NODEINF} ./ito1_2.sh
ito1_2.sh
#!/bin/bash

ID=${OMPI_COMM_WORLD_LOCAL_RANK}

case ${ID} in
[0])
  numactl -N 1 -l ./a.out
  ;;
[1])
  numactl -N 0 -l ./a.out
  ;;
esac
numactlの-N(=--cpunodebinde)を用いることで、簡単にMPIプロセス単位でのCPUソケットへの割り当てが行えています。


MVAPICHの場合

OpenMPコンパイラによる割り当てに任せるというスタンスを取っています。 export MV2_ENABLE_AFFINITY=0を指定するとMPIによるプロセス割り当て処理が行われなくなるため、 あとはランク番号とnumactlを用いて割り当てを行います。 MV2_COMM_WORLD_LOCAL_RANKなどの環境変数も有用です。

2ノードで各ノードの各ソケットに1MPIずつ配置、各MPIプロセス内で18スレッドOpenMPスレッド並列化を行う例(4MPIプロセス*18スレッド):
ito1.sh

#!/bin/bash
#PJM -L rscunit=ito-b
#PJM -L rscgrp=ito-g-16-dbg
#PJM -L vnode=2
#PJM -L vnode-core=36
#PJM -L elapse=10:00
#PJM -j
#PJM -S

module load cuda/8.0
export MODULEPATH=$MODULEPATH:/home/exp/modulefiles
module load exp-mvapich2/2.2-intel

export MV2_SHOW_CPU_BINDING=1
export MV2_ENABLE_AFFINITY=0

mpirun -n 4 -ppn 2 -launcher-exec /bin/pjrsh -machinefile ${PJM_O_NODEINF} ./ito1_2.sh
ito1_2.sh
#!/bin/bash

ID=${MV2_COMM_WORLD_LOCAL_RANK}

case ${ID} in
[0])
  numactl -N 1 -l ./a.out
  ;;
[1])
  numactl -N 0 -l ./a.out
  ;;
esac
numactlの-N(=--cpunodebinde)を用いることで、簡単にMPIプロセス単位でのCPUソケットへの割り当てが行えています。


GPUの割り当て

GPUの割り当てについては複数GPUを活用するヒントにて解説しています。