プログラムの高速化・並列化サービスの事例

ケーススタディ : クアッドコア・システムにおけるCASTEPのベンチマークと最適化

[2016年1月掲載]

Asimina Maniopoulou, Chris Armstrong,
CSE Team, NAG Ltd.


1 イントロダクション

本調査は、クアッドコアAMD Opteronプロセッサーで構成されるフェーズ2a(IQシステムは幾らか異なる)のメインシステムとして構築されるHRCToR TDS(Test and Development System)上で実施されました。このシステムは64ノード構成で、総コア数は256です。


2 現状のCASTEP 4.4のベンチマーク

CASTEP 4.4はHECToR(デュアルコアノード)およびHECToR TDS(クアッドコア)上でPathscale(3.2)コンパイラーでコンパイルされました。コンパイルフラグは両者ともに、CASTEP dCSE Project report[1]で記述されているものを選びました。そのフラグは次のものです:

-O3 -OPT:Ofast -OPT:recip=ON -OPT:malloc_algorithm=1 -inline -INLINE:preempt=ON

さらに、クアッドコア専用の最適化フラグ(Pathscaleにおいては、-march=barcelona)およびクアッドコアに最適化されたCray libsciライブラリーへのリンク(必要に応じて)を追加するための、新しいモジュール"xtpe-quadcore"をコンパイル時にロードします。

クアッドコアシステム上で、CASTEPは全てのSCF計算の後にセグメンテーションフォールトで終了してしまうことが確認されました。フォールとしたルーチンは、Fundamental/basis.F90ファイル内のbasis sum recip gridでした。問題はこのルーチンの配列"grid"に関係していることが判明しました。このファイルのみをより低いレベルの最適化レベルにすることで、問題は回避されました。-O2でもフォールトするため、最適化フラグは以下のものを用いました:

-O1 -OPT:Ofast -OPT:recip=ON -OPT:malloc_algorithm=1 -inline -INLINE:preempt=ON

このオプションにしてもコードの性能は全く同等です。違うやり方として、basis.F90ファイル内に最適化指示行を挿入すれば、全ファイルではなく、問題のある関数のみに低い最適化レベルが適用されます。

表1-3はデュアルおよびクアッドコア上での3つのベンチマーク実行結果を示しています(ベンチマークの詳細は文献[1]を参照してください)。表中の計測持間は、対応するベンチマーク名.castep出力ファイル内に記録された、最後のSCFサイクルの時間[秒]です。太字で強調した値は、クアッドコアがデュアルコアより遅いケースです。

nproc dualcore quadcore speedup
8 398.31 327.16 1.22
16 231.60 218.77 1.06
32 174.02 218.91 0.79
64 194.57 301.10 0.65
表1:al1x1ベンチマーク性能(最終SCFサイクル:7)。単位:秒

2.1 ベンチマーク al1x1

32及び64コアにおいて性能が良くありませんが、castep.paramファイル内のマルチコア・オプション指定するとより良い性能となります(セクション5.2の表8をご覧ください)。このベンチマークでは性能向上は小さいものでした。この結果から、al1x1はケースとして小さく、16proc以上での性能劣化はより小さなサイズの場合よりも通信が支配していることが原因であり、クアッドコアにおいて各ノード内での内部的な通信路により多くの競合が存在することが見て取れます(この問題に関連する最適化はセクション5を参照していください)。


2.2 ベンチマーク al3x3

nproc dualcore quadcore speedup
32 5948 4759.03 1.25
64 3121.02 2500.48 1.25
128 1915.14 1696.30 1.13
256 1464.74 1526.11 0.96
表2:al3x3ベンチマーク性能(最終SCFサイクル:11)。単位:秒

表2において、32及び64コアでの時間は1.25倍の高速化を示しています。256コアでは、古いデュアルコアに比べスケーリング性能が0.96へ、al1x1と同様に劣化しています。


2.3 ベンチマーク TiN-mp

nproc dualcore quadcore speedup
64 785.09(42) 654.33(42) 1.20
128 572.12(39) 422.02(39) 1.35
256 370.71(42) 404.35(42) 0.92
表3:TiN-mpベンチマーク性能(最終SCFサイクルはコア数により変化しているため時間毎にカッコ内に記載)。単位:秒

ここでは64コアで1.2倍、128コアで1.35倍に高速化している。256コアでの性能悪化は明らかです。要するに、大規模ベンチマークでは1.2~1.35倍に高速化しています。CASTEPはデュアルコアシステムよりもクアッドコアシステムで、また大規模ケース(al3x3およびTiN-mp)において128コア以上で性能劣化しており、小規模ケース(al1x1)では32コアにおいてさえ問題が顕在化しています。このスケール問題はセクション5で扱います。


3 クアッドコアシステムにおけるCASTEPのチューニング


3.1 コンパイラーによる最適化

様々なコンパイルフラグを試しましたが、これ以上の最適化には至りませんでした。例えば以下のようなフラグです:

-ipa, -LNO:blocking=ON, -LNO:prefetch=3, -LNO:prefetch_ahead=3, -LNO:prefetch_ahead=4,
-LNO:full_unroll_outer=ON, -LNO:full_unroll_size=10000, -LNO:fu=12,
-OPT:unroll_times_max=16, -LNO:fission=2

フラグ-LNO:simd2によるよりアグレッシブなベクトル化を試みましたが、コンパイラのフォールトにより失敗しています。このフラグは最適化レベルを-O2へ減らして用いることが出来ますが、これらの組合せは性能を劣化させます。


3.2 環境変数による最適化

Cray XTシステムのhuge page fileサポートを含む様々なMPICH環境変数を試しました。これらの中で、MPICH_RANK_REORDER_METHODがCASTEPには有効です。スケーリング性能が悪化した場合に(例えばal3x3やTiN-mpベンチマークで256コアを利用する場合)、MPICH_ALLTOALLVW_RECVWIN=1により指定されたALLTOALLVアルゴリズムを変更することも有効です。


3.2.1 MPICH_RANK_REORDER_METHODによる最適化

デフォルトのMPIランク配置はSMPスタイルです。別のスキームを実験したところ、大規模ケースではfolded-rank配置がデフォルトに比べて1.15倍高速化しました。folded-rank配置を指定すると、ランクはそのリスト中の次のノードに置かれ、一旦各ノードが用いられれば、ランクはその最後のノードから最初の方へ折り返されます。以下の結果から、この環境変数は常に設定されるべきでしょう。

nproc default-smp folded speedup
8 327.16 325.87 1.00
16 218.77 219.44 1.00
32 218.91 214.78 1.02
64 301.10 294.05 1.02
表4:al1x1ベンチマーク性能(最終SCFサイクル:7)。単位:秒。小規模ベンチマークal1x1ではfolded-rank配置は僅かに有利です。

3.2.2 MPICH_ALLTOALLVW_RECVWINによる最適化

以下の表において、MPICH_RANK_REORDER=2およびMPICH_ALLTOALLVW_RECVWIN=1を指定した際のクアッドコアでの性能を比較しています。

nproc default-smp folded speedup
32 4759.03 4539.56 1.05
64 2500.48 2402 1.04
128 1696.30 1620.38 1.05
256 1526.11 1405.14 1.09
表5:al3x3ベンチマーク性能(最終SCFサイクル:11)。単位:秒

nproc default-smp folded speedup
64 654.33(42) 606.37(42) 1.08
128 422.02 (39) 376.66(39) 1.12
256 404.35 (42) 350.48(42) 1.15
表6:TiN-mpベンチマーク性能(最終SCFサイクルはコア数により変化しているため時間毎にカッコ内に記載)。単位:秒

デフォルトのSMPランクを用いてMPICH_ALLTOALLVW_RECVWIN=1を指定したケースは効果が大きいですが、両方の環境変数を指定した場合が最も良い性能が得られました。

benchmark nprocs default dualcore default quadcore folded folded+RECVWIN
al3x3 256 1464.74 1526.11 1405.14 1344.12
TiN-mp 256 370.71 404.35 350.48 344.03
表7:MPI環境変数による性能。最終SCFサイクルでの時間。単位:秒

MPICH_ALLTOALLVW_RECVWINおよび異なるプロセッサー数に対するSEND環境変数に対して実験を行うことが推奨されます。


4 プロファイリング

ここで、TiN-mpベンチマークによるクアッドコアシステム上のコードのプロファイル情報を採取し、主にベクトル化とクアッドコアシステムにおいて極めて重要なキャッシュ利用率を議論します。


4.1 ユーザー関数

64コアを用いて実行した際に最も時間の掛かるユーザー関数は以下の通りです:


|| Time% | Time       | D1 Cache hit, miss ratio | Function
|| 56.0% | 419.238296 |  94 %                    | main
||  9.7% |  72.656015 |  94 %                    | POT_NONGAMMA_APPLY_SLICE.in.POT
||  6.3% |  46.856810 | 96.6%                    | COMMS_TRANSPOSE_N.in.COMMS
||  3.7% |  27.553444 | 78.9%                    | WAVE_INITIALISE_SLICE.in.WAVE
||  1.4% |  10.700597 | 88.5%                    | ION_Q_RECIP_INTERPOLATION.in.ION
||  1.1% |   8.429017 | 89.6%                    | ION_APPLY_AND_ADD_YLM.in.ION

キャッシュヒット:ミス比率は多少低いことが示されています。また、関数ION_Q_RECIP_INTERPOLATION.in.IONおよびION_APPLY_AND_ADD_YLM.in.IONにおいてベクトル化の問題も見つかりました。Pathscaleのフラグ(-FLIST:=on -LNO:simd_verbose)を用いたコンパイル時に、キャッシュ利用率やベクトル化を阻害する非連続な配列アクセスが多く存在していることが判明しました。混合精度演算(倍精度複素数と倍精度実数)を用いる多くの箇所で、コンパイラがベクトル化に失敗していることもわかりました。


4.2 MPI関数


4.2.1 64コアの場合

64コアを用いてTiN-mpベンチマークを実行した際に、最も時間の掛かるMPI関数が以下のものです。各MPIコレクティブ通信の際にはバリアが設定され、同期に必要な時間がMPI function name (sync)に記録されます。各コレクティブ操作に必要な時間は個別に与えられます。


|Time % | Time       | Imb. Time | Imb.  | Calls    |Function
| 16.1% | 119.638932 | --        | --    | 285344.2 |MPI
||----------------------------------------------------------------
|| 13.9% | 103.655824 |  6.980603 |  6.4% | 207187.5 |mpi_alltoallv_
||  1.1% |   8.160020 |  1.956344 | 19.6% |    622.3 |mpi_recv_
||================================================================
|  5.2% |  39.001770 | --        | --    | 284091.5 |MPI_SYNC
||----------------------------------------------------------------
|| 2.2% |   16.375367 |  4.858887 | 23.2% | 207187.5 |mpi_alltoallv_(sync)
|| 2.2% |   16.000521 |  4.925778 | 23.9% |  58403.2 |mpi_allreduce_(sync)

4.2.2 256コアの場合


 100.0% | 1161.156466 | --        | --    | 1788338.6 |Total
|------------------------------------------------------------------
|  58.8% |  682.655539 | --        | --    |  513017.7 |MPI
||-----------------------------------------------------------------
||  54.9% |  636.979240 | 89.855737 | 12.4% |  392235.5 |mpi_alltoallv_
||   2.0% |   23.695335 |  5.885210 | 20.0% |   87424.8 |mpi_allreduce_
||   1.4% |   16.091737 |  1.547607 |  8.8% |    1317.8 |mpi_recv_
||=================================================================
||=================================================================
|  18.8% |  217.732094 | --        | --    |  510374.0 |MPI_SYNC
||-----------------------------------------------------------------
||   9.3% |  107.482739 | 82.866435 | 43.7% |   87424.8 |mpi_allreduce_(sync)
||   7.8% |   90.137755 | 68.867932 | 43.5% |  392235.5 |mpi_alltoallv_(sync)
||   1.0% |   11.947095 | 10.972329 | 48.1% |       7.0 |mpi_barrier_(sync)

同じベンチマーク実行でのMPI通信時間を64コアと256コアで比較すると、MPI通信は実行時間の主要部分を占めていることが解ります。セクション5と6における3D FFT演算で利用されているmpi_alltoallvが特に注目をひきます。これらMPI関数の呼出しの場所と目的が理解されるべきです。実行時間のほぼ19%が同期に消費されており、負荷分散問題が生じています。ベクトル化やキャッシュ効率の向上を行う必要がありますが、クアッドコアでの1.2~1.3倍の高速化はとても良い結果です。スケーリング性能は課題であり、クアッドコアでのCASTEP性能を改善するdCSEプロジェクトで克服しなくてはなりません。次の2つのセクションでこの2つの最適化の効果を調査します。


5 MPI_Alltoall通信のシェアードメモリーバッファ


5.1 背景

CASTEPで並列に3D FFTを実行する方法は、"ペンシル"状に分布したグリッドのサブセットに対してx,y,z方向で個別に1D変換を実施することです。9個のプロセッサーで構成された簡単な例を使ってこの方法を説明します。


図1 : 3D並列FFTでの領域分割。Step1:x方向でのFFT実行

図1は各プロセスがx方向に変換を行う際の領域分割を表します。各プロセスのデータを異なる色で表現しています。x,y,z方向にはそれぞれngx,ngy,ngz個のグリッドがあります。そこで各プロセスは図1のx方向に(ngy/nprocy)*(ngz/nprocz)個の変換を実行します。次にy方向の変換を実行するためにデータを回転(転置)させます。得られたデータは一は図2に与えられています。この手続きは繰り返され、図3で示すデータ配置で各プロセスはz方向の変換が可能になります。


図2 : 3D並列FFTでの領域分割。Step2:図1のデータを転置して、y方向でFFT実行


図3 : 3D並列FFTでの領域分割。Step3:図2のデータを転置して、z方向でFFT実行


5 MPI_Alltoall通信のシェアードメモリーバッファ

HECToR上でこの演算を実行する際に最も時間が掛かるのは、1D変換を跨ぐデータの転置です。これはMPI_Alltoallvを使って実装されます。CASTEPはこの転置操作に2つのオプションを持っています。デフォルトでは全てのプロセスがMPI_Alltoallv呼出しに参加します。他のオプションはマルチコアシステム向けで、ノード当たり1プロセスがMPI_Alltoallvを呼出します。これは、単一ノード内で単一の送信バッファへ送るデータをギャザーするMPI_Gatherを用いて行います。その指名された参加プロセスがMPI_Alltoallv呼出しに参加するので、内部通信の競合が最小化されます。参加プロセスにより受信したデータはノード内の他のプロセスへスキャッターされます。

マルチコアにおける主要なコストは、MPI_Alltoallv呼出しに対して正しい順序でマーシャリングし、アンマーシャリングしてMPI_Alltoallv呼出し後にスキャッターするという、ノード内のデータ収集と関連しています。例えば、マルチコアモードで16プロセッサーでal1x1ベンチマークを実行する場合、デフォルトモードでは222.9秒なのに対して292.1秒掛かります。デフォルトモードでは、データ転置を実行するルーチン(comms transpose exchange)において68.6秒掛かりますが、マルチコアモードでは115.8秒になります。ギャザリングとマーシャリングは17.5秒、MPI_Alltoallvは57.5秒、およびアンマーシャリングとスキャッターには58.9秒掛かっています(プロセッサー平均の時間)。

以下の表8から10で3種のベンチマーク性能を示します。デフォルトモードでは、送信/受信バッファーのパック/アンパックのオーバーヘッドが大きくなるため、より少ないプロセッサーの場合の方が速い傾向にあります。Shmによる最適化は、大規模ケースに対してより多くのプロセッサ使用に見合う性能を示しています。例えば、128プロセッサーでのal3x3の性能は1.44倍に高速化しています。より劇的な結果は、32プロセッサーでのal1x1の1.78倍の高速化です。Shmバッファの導入は全ての場合に対してコードのスケール性能を向上させます。

nprocs default multicore shm default:shm
8 334.9 545.3 387.9 0.86
16 222.9 292.1 207.6 1.07
32 223.5 157.3 125.5 1.78
表8:al1x1ベンチマーク性能(SCFサイクル 7)

nprocs default multicore shm default:shm
64 2260.6 3102.7 2665.3 0.85
128 1788.1 1783.9 1571.5 1.14
256 1563.9 1191.3 1086.6 1.44
表9:al3x3ベンチマーク性能(SCFサイクル 11)

nprocs default multicore shm default:shm
32 1324.53 1931.1 1404.9 0.94
64 655.0 993.5 748.9 0.87
128 431.3 550.1 415.6 1.04
表10:TiN-mpベンチマーク性能(SCFサイクル 39)

5.3 今後やるべきこと

現状のshmシェアードメモリー実装は、shmメモリーセグメントのアロケートとアタッチのためにC言語ルーチンを呼び出しています。本作業をCASTEPのコードへ組み込むには、C言語ルーチンはFortran2003のような標準的なインターフェイスを持つべきです。

現状の実装は、HECToRでのデフォルトのようにSMPスタイルのランク配置を仮定しています。しかしながら、セクション3.2.1では、CASTEPはfolded rank配置の方が得であることが示されました。shmとインターフェイスを取るCコードは、Crayのシステムファイルを読み込んでランク配置を決める関数を含んでいます。shmによる最適化の様々な方法の調査において、実行時にランク配置を決定するためのマシンに依存しないやり方については将来的な課題です。

上述のマルチコアオプションを使用するには、.paramファイルにてオプションnum_proc_in_smpを4にセット(つまり、1ノード内の4つ全てのコアを単一SMPグループにする)しましたが、別のHPCプロジェクトでは、MPI_Alltoallvメッセージサイズを最適化するためにノード内の最大コア数を指定するよりも小さな値にした方が有利である場合がある事が見受けられました。このことから、ここで用いられた環境を反映させて、メッセージサイズを最適化するための経験的なアルゴリズムを調べることが示唆されます。


6 バンド・ブロッキングFFTの効果に関する調査

セクション5.1で記述された3D FFT演算は、「バンド」と呼ばれる幾何学的に同等な3D領域に対して、結果的に独立した処理を実行します。よって、ある特定の方向における転置は全バンドに対して同等であることから、レイテンシーを隠蔽するために複数のバンドを一つのMPI_Alltoallv呼出しへ一緒に纏めてしまうことが出来ます。

ここでの調査は、サブルーチンbasis_real_recip_reduced_many内においてこのような"バンド・ブロッキング"を可能にする単一インスタンスの最適化実装に焦点を当てます。このサブルーチンは以下の形のループを持ちます:

do nb=1,nblock
 call basis_fft_wrapper(ngx,ngy,ngz,grid(1,nb),-1,'W')

このループはnblock個のバンドに対して3D FFTを実行しますが、bandblock個でブロック化したバンド群(bandblock bands)で纏めた場合は以下のように修正されます:

do nb=1,nblock/bandblock 
 call basis_fft_wrapper_bb(ngx,ngy,ngz,grid(1,(nb-1)*bandblock+1), &
 -1,'W',max_grid_points) 
(実際には剰余分のループも付け加えられます)。

この新しいFFTラッパールーチン(FFTW3ラッパーのみに実装しました)は、一度の呼出しでbandblock bandsのデータを転置する前に、bandblock回のdfftw_execute_dftの呼び出しを実行します。新しい転置操作はbandblock bandsをshm送信バッファにパッキングして、MPI_Alltoallvを実行し、受信バッファからbandblock bandsをアンパックします。送信/受信バッファのパッキングとアンパッキングは、データの正しい配置を保証するためにadapted indexing法を用いています。

図4から6はCASTEPの時間トレース機能を用いた3つのベンチマークに対する、このバンド・ブロッキング手法の性能を示しています。全てのケースにおいて、ブロック化されたバンド数に対して性能劣化が示されています。図7はなぜこの性能劣化が生じているかを示しています:12バンドまでMPI_Alltoallvのコストは減少しますが、シェアードメモリーバッファのパッキングとアンパッキングのコストはバンド数に応じて増加しています(ここでの時間は別途MPI_Wtimeを用いて計測しました)。このことから、バンド・ブロッキングのアイデアとして、レイテンシーを削減するためにより多くのデータを送信バッファへパッキングすることが考えられますが、一方そのバッファをパック/アンパックする時の新しいメモリーアクセスパターンが重要になってきます。バンド・ブロッキングを有効に活用するためには、シェアードメモリーバッファへ/からデータをコピーする方法を最適化することが重要です。


図4 : 8,16,32プロセッサーでのal1x1の性能。"basis"はbasis_real_recip_reduced_manyの計測時間、"trans"はcomms_transposeの計測時間。


図5 : 64,128プロセッサーでのal3x3の性能。"basis"はbasis_real_recip_reduced_manyの計測時間、"trans"はcomms_transposeの計測時間。


図6 : 32,128プロセッサーでのTiN-mpの性能。"basis"はbasis_real_recip_reduced_manyの計測時間、"trans"はcomms_transposeの計測時間。


図7 : 16プロセッサー/al1x1における、comms_transpose内の計測時間プロファイル。

表11は1バンドの性能で割った5バンドの性能をしましています(つまり、1より小さければ5バンド性能が優れ、1より小さければ劣化していることを示します)。

Time L1 misses L1 misses L1 misses TLB misses
pack 1.07 0.78 5.83 3.65 1.63
unpack 1.26 0.77 3.17 7.67 2.22
表11:1バンドに対する5バンドの性能劣化傾向

表12はbaseページファイルを用いた場合と比較した、hugeページファイルを用いた場合の性能です。
表12が示しているのは、baseページを用いた1バンドの場合と比較して、hugeページファイルを用いるとTLBミス時間がほぼ同等になることです。しかしながら、経過時間にはほとんど影響がなく、L2およびL3キャッシュミスの性能劣化が残存しています。このhugeページファイルに関しては性能に与える影響がありませんでしたが、1バンドにおいてhugeページを適用した場合は性能は増強され、パックおよびアンパックループはbaseページに対してTLBミスが0.09及び0.10に減少しましたが、経過時間比はほぼ1のままでした。

このことから、TLBミスは性能に影響を与えず、L2およびL3キャッシュミスが最小化されるべきことが示されました(ただし5バンドにおいてL1キャッシュミスが減少していることを記しておきます)。

Time L1 misses L1 misses L1 misses TLB misses
pack 1.00 0.78 5.73 3.72 1.08
unpack 1.24 0.77 3.15 7.59 1.02
表12:baseページファイルを用いた1バンドに対する、hugeページファイルを用いた5バンドの性能劣化傾向

6.1 今後について

シェアードメモリーバッファへ/からのデータコピー方法を変更すべきです。現状のアプローチは現状のインデックス配列を踏襲しており、これはメモリー上を広く走査して局所性を有効に使っていません。現状のメモリーアクセスパターン解析とより有効なパッキング/アンパッキング方法の調査が必要です。

図7は、バンド・ブロッキングはMPI_Alltoallv性能を改善可能であり、メモリーコピーの改善や、それらを残りのコードへ適用することによりさらに性能が改善されることを示しています。


謝辞

System Vシェアードメモリー管理コードの提供に関してDavid Tanquerayに、およびCASTEP入力に関してKeith Refsonに感謝します。


文献

[1] CASTEPにおけるバンド並列:http://www.hector.ac.uk/cse/distributedcse/reports/castep/

関連情報
MENU
Privacy Policy  /  Trademarks