13. XeonPhi

13.1 XeonPhiとは

XeonPhiとはインテルからリリースされている高速計算環境です。 PCの拡張ボードにとりつけるハードウェア(コプロセッサーと呼びます) と開発環境(インテルコンパイラー、言語はC/C++/Fortran)から成ります。
XeonPhiは約60個のコアから成り、それぞれは512ビットのベクトルレジスタ(AVX-512)を持っています。
多数のコアによる並列計算にはOpenMPを利用し、 ベクトル演算にはコンパイラーの自動ベクトル化機能を利用します。
既にプログラムがOpenMPで並列化されて十分な性能を発揮しており、 かつ計算の主要部がベクトル化可能であれば、 ソースコードをほとんど変更することなくXeonPhiに移植することができ高い性能が得られます。 この高い生産性がXeonPhiの特長です。
なお、現在サポートされているOSはLinuxのみです。

13.2 XeonPhiプログラミング

並列化
OpenMPを用いた並列化の方法は通常のCPU(プロセッサー)と同じです。
ただし、XeonPhiの逐次計算能力(シリアル計算能力)は現在の標準的なCPUの約1/10と低いので、 少しでも計算時間のかかる部分はすべて並列化する必要があります。
並列化により約60倍速くなります。(標準的なCPUの約6倍)
なお、インテルによるOpenMP拡張は頭に"OMP_"の代わりに"KMP_"がつきます。

ベクトル化
XeonPhiの性能を生かすには計算の主要部分をSIMD演算によりベクトル化することが必須です。
ベクトル化は一番内側のループについて行われます。
XeonPhiのベクトルレジスターは512ビットなので理想的には単精度で512/32=16倍速くなりますが、 通常は4倍程度です。
コンパイラーの自動ベクトル化機能を利用しますので、 コンパイラーがベクトル化しやすいようにコーディングすることが大切です。 ベクトル化向けのコーディングの指針については[22]p.148を参考にして下さい。
ベクトル化できないコードを"#pragma simd"文で強制的にベクトル化すると間違った結果が得られます。 一方ベクトル化向けによくチューニングされたコードは#pragma文を挿入しなくても十分な性能が得られます。 (要するに#pragma文に頼ってはいけません)

速度比
以上から1コアの逐次計算に比べて、並列化により60倍、ベクトル化により4倍速くなります。 結局、現在の標準的なCPUの1コアで逐次計算するときに比べて、 1/10 x 60 x 4 = 24 倍程度速くなります。

13.3 コンパイル・リンク・実行方法

ここでは4.3のソースコードを使用しベクトルの内積の計算時間を計測します。 ただし単精度とします。

コンパイル・リンク方法
コンパイラーにはインテルC/C++(icc)を用います。 コンパイルオプションとリンクオプションに"-mmic"が必要です。
$ icc -O3 -ipo -openmp -mmic sdot_omp.c -o sdot_mic
なお、コンパイルオプション"-ipo"をつけないと大幅に遅くなるケースがありますので、 通常つけることをお勧めします。
また、コンパイルオプション"-openmp-report=2 -vec-report=2" をつけると以下のようなメッセージが出力されます。 これにより並列化とベクトル化の結果を確認することができます。

$ icc -O3 -ipo -mmic -openmp -openmp-report=2 -vec-report=2 sdot_omp.c -o sdot_mic
sdot_omp.c(80): (col. 10) remark: OpenMP 定義ループが並列化されました。
sdot_omp.c(65): (col. 2) remark: ループがベクトル化されました。
sdot_omp.c(79): (col. 2) remark: ループはベクトル化されませんでした: ベクトル依存関係が存在しています。
sdot_omp.c(80): (col. 10) remark: ループがベクトル化されました。
メッセージの意味は以下の通りです。
"OpenMP 定義ループが並列化されました。" : OpenMPによる並列化が行われた
"ループがベクトル化されました。" : ループがベクトル化された
"ループはベクトル化されませんでした: ベクトル依存関係が存在しています。" : アルゴリズム上ベクトル化できない。(hot spotであれば)書き直しが必要
"ループはベクトル化されませんでした: 内部ループではありません。" : ベクトル化は一番内側以外のループについては行われない

プログラムの実行方法
コンパイル・リンクしてできた実行プログラムをコプロセッサー (ホスト名をここではmic0としています)にコピーし、 その後コプロセッサーにloginして下さい。
$ scp sdot_mic mic0:.
$ ssh mic0
プログラムを実行する前に以下のコマンドを実行して下さい。
$ export KMP_AFFINITY=scatter
これはスレッドをどのようにコアに配分するか指定するものです。 "scatter"のときはコアについて先に配分され、"compact"のときはコア内スレッドについて先に配分されます。 通常"scatter"と指定して下さい。
プログラムの実行方法は以下の通りです。
$ sdot_mic 配列の大きさ 繰り返し回数 スレッド数
例えば以下のようになります。
$ sdot_mic 1000000 1000 114
繰り返し回数は計算時間の測定誤差を小さくするためです。 スレッド数はコア数の1/2/3/4倍として下さい。実際は以下で述べるように計算時間が一番短い値を指定して下さい。

13.4 XeonPhiの計算時間

計算時間を測定した環境は以下の通りです。

表13-1に計算時間を示します。
配列の大きさ(=N)と繰り返し回数(=L)の積は一定(=10^10)です。従って全体の演算量は同じです。
No.1-2では57スレッドでKMP_AFFINITY=compactのとき以外は計算時間はほぼ同じです。
No.3-4ではスレッド起動回数が多いために計算時間が増えています。 スレッド起動回数は繰り返し回数と同じです。 1,000,000回のスレッドを起動するのに50秒程度時間がかかっており、 これからスレッドを起動する時間(オーバーヘッド)は約50μsecと見積もることができます。 これはCPUのOpenMPの約100倍であり、マルチスレッドと同じオーダーです。

表13-1 XeonPhiの計算時間(単精度)
No.配列の大きさN繰り返し回数LKMP_AFFINITY=scatterKMP_AFFINITY=compact
57スレッド114スレッド171スレッド228スレッド57スレッド114スレッド171スレッド228スレッド
110,000,0001,000 0.81秒 0.90秒 0.94秒 0.99秒 1.53秒 1.11秒 0.90秒 0.97秒
21,000,00010,000 0.76秒 0.73秒 0.87秒 0.95秒 1.70秒 1.09秒 0.98秒 0.94秒
3100,000100,000 4.29秒 4.73秒 5.66秒 5.84秒 3.85秒 4.35秒 4.82秒 5.37秒
410,0001,000,00039.85秒46.32秒53.64秒59.76秒34.20秒40.10秒44.08秒52.33秒