NUMA環境で,スレッドやメモリの配置を明示的に指示する方法を調べたので,メモしておく. 1つの筐体に複数のマルチコアCPUを搭載する環境では,メモリはCPUごとに接続される. このような環境では, バスを介して直接つながったメモリ(ローカルメモリ)とCPUの間で,高速にデータを転送できる. 一方,直接つながっていないメモリ(リモートメモリ)にデータを転送するためには, QPI(QuickPath Interconnect)などのインタコネクトを介して, 他のCPUソケットを経由する必要がある. このようにメモリアクセスの仕組みが複数存在する環境は, NUMA(Nun Uniform Memory Access)と呼ばれる.

NUMA環境で,マルチスレッドプログラムをチューイングするためには, スレッドをどのコアに割り当てるか,データをどのメモリに配置するかが重要になる. これらの制御は,numactlコマンドあるいはlibnumaライブラリによって実現できる.

NUMA環境の例

numactlによる制御

ソースコードを修正できない状況(あるいは面倒くさい状況)では, numactlコマンドを使って,アフィニティを設定する. --cpubind=<nodemask>オプションでスレッドをどのノードで実行するか指示し, --membind=<nodemask>オプションでデータをどのノードのメモリに配置するかを指示する. <nodemask>には,--membind=0,1のようにノード番号をカンマ区切りで記述する. 他にも, 優先してメモリを割り当てるノードを指示する--preferred=<nodenumber>オプションや 複数のノードにインタリーブでメモリを割り当てる--interleave=<nodemask>オプションがある. ノード番号などハードウェア情報は,--hardwareオプションで確認できる. また,プロセスに割り当てられたポリシーを確認するには,--showオプションを使う.

# スレッドをノード0,データをノード0および1に配置
$ numactl --cpubind=0 --membind=0,1 ./a.out
# メモリをインタリーブに配置し,numactl --showで確認
$ numactl --interleave=all numactl --show

libnumaによる制御

ソースコードを修正できる状況では,libnumaを使う. numactlはプログラム全体のメモリ割付けを制御するが, libnumaは個々のメモリ領域を個別に制御する. libnumaを利用するには,コードにnuma.hをヘッダを追加し, 共有ライブラリをリンクする(-lnuma). ノード番号の集合<nodemask>は,nodemask_t型変数に格納する. メモリの確保では,どのようにメモリを確保するのかに従って, 適切なnuma_alloc_*ファミリの関数を利用する. メモリの解放には,共通してnuma_free関数を利用する. さらに,numa_run_on_nodeあるいはnuma_run_on_node_mask関数を使うことで, スレッドをどのノードで実行するかを明示できる.

nodemask_t m;              // ノード番号の集合を格納する変数m
nodemask_zero(&m);         // mを初期化
nodemask_set(&m, 2);       // ノード番号2を有効に
nodemask_clr(&m, 2);       // ノード番号2を無効に
nodemask_all_nodes(&m);    // すべてのノードを有効に
nodemask_no_nodes(&m);     // 空集合に
nodemask_isset(&m, 2);     // ノード番号2がセットされていれば真

size_t s = 4 * 1024;       // データサイズ4KB

// 2番目のノードに確保
void *mem1 = numa_alloc_onnode(s, 2);
// すべてのノードにインタリーブに確保
void *mem2 = numa_alloc_interleaaved(s);
// mで示されたノードにインタリーブに確保
void *mem3 = numa_alloc_interleaaved_subset(s, m);
// ローカルメモリに確保
void *mem4 = numa_alloc_local(s);

// メモリ解放
numa_free(mem1,s); numa_free(mem2,s); numa_free(mem3,s); numa_free(mem4,s);

// 現在のスレッドをノード1で実行
numa_run_on_node(1);

// 現在のスレッドをmに含まれるどこかのノードで実行
nodemask_zero(&m);
nodemask_set(&m,1);
nodemask_set(&m,2);
numa_run_on_node_mask(m);

OpenMPによる制御

OpenMPを使ってマルチスレッドを実現している場合には,環境変数によってアフィニティを制御する. 制御方法は,コンパイラごとに異なり,例えば, PGIコンパイラであればMP_BINDおよびMP_BLISTを設定する.

参考