Intel® FPGA SDK for OpenCL™: ベスト・プラクティス・ガイド

ID 683521
日付 12/08/2017
Public
ドキュメント目次

2.8.5. Single Work-Itemカーネルのループ

Intel® FPGA SDK for OpenCL™オフライン・コンパイラーデータ処理のパフォーマンスを最大限にするためにカーネルを最適化するアルゴリズムを実装しています。
単一のWork-Itemカーネル内のループのデータパスは、飛行中に複数の反復を含むことができます。この動作は、NDRangeカーネルのループにMultiple Work-Itemが含まれている点で、NDRangeカーネル内のループとは異なります。最適にアンロールされたループとは、クロックサイクルごとに起動されるループの繰り返しです。クロックサイクルごとに1回のループ反復を開始すると、パイプラインの効率が最大化され、最高のパフォーマンスが得られます。下の図に示すように、クロックサイクルごとに1つのループを起動することで、カーネルの処理速度が向上します。
図 49. パイプライン化されていないループとパイプライン化されたループの間のループ反復の起動頻度の比較

新しいループ反復の開始頻度は開始間隔(II)と呼ばれます。 IIは、パイプラインが次のループ反復を処理する前に待機しなければならないハードウェアクロックサイクルの数を示します。最適にアンロールされたループは、1つのループ反復がクロックサイクルごとに処理されるため、IIの値が1です。

HTMLレポートでは、最適に展開されていないループのループ分析で、オフライン・コンパイラーがループを正常にパイプライン処理したことが示されます。

次の式を検討してみましょう。

kernel void simple_loop (unsigned N, global unsigned* restrict b, global unsigned* restrict c, global unsigned* restrict out) { for (unsigned i = 1; i < N; i++) { c[i] = c[i-1] + b[i]; } out[0] = c[N-1]; }
図 50. カーネルのハードウェア表現simple_loop

この図は、オフライン・コンパイラーが並列実行とループパイプライニングを使用してsimple_loopを効率的に実行する方法を示しています。このsimple_loopカーネルのループ解析レポートは、 for.bodyループの場合、 Pipelined列はYesを示し、 II列は1を示します。

クリティカルパスと最大周波数のトレードオフ

可能であれば、オフライン・コンパイラーは与えられたループに対してIIの値1を達成しようと試みます。いくつかのケースでは、オフライン・コンパイラーは、ターゲットfmaxが低下して1になるように努力するかもしれません。

次の式を検討してみましょう。

kernel void nd (global int *dst, int N) { int res = N; #pragma unroll 9 for (int i = 0; i < N; i++) { res += 1; res ^= i; } dst[0] = res; }

次の論理図は、カーネルNDの実際の、より複雑なハードウェア実装の簡略化した表現です。

図 51. カーネルにおけるループの論理図
図 52. カーネルの実際のハードウェア実装

加算演算とXORゲートによるフィードバックは、オフライン・コンパイラーが目標周波数を達成する能力を制限するクリティカルパスです。結果として得られるHTMLレポートは、クリティカルパスを構成するコントリビュータの内訳をパーセンテージで表したものです。

図 53. カーネルのループ解析レポートの詳細ペイン"9%: Add Operation (fmax_report.cl:5)"行は、フィードバックごとに1回で9回繰り返されます。

ループの起動間隔に影響を与えるループキャリー依存関係

ループがパイプライン化されているにもかかわらず、IIの値が1にならない場合があります。これらのケースは、通常、データ依存性またはループ内のメモリー依存性によって発生します。

データ依存とは、ループ反復で以前の反復に依存する変数を使用する状況を指します。この場合、ループはパイプライン化できますが、そのII値は1より大きくなります。次の例を検討してください。

1 // An example that shows data dependency 2 // choose(n, k) = n! / (k! * (n-k)!) 3 4 kernel void choose( unsigned n, unsigned k, 5 global unsigned* restrict result ) 6 { 7 unsigned product = 1; 8 unsigned j = 1; 9 10 for( unsigned i = k; i <= n; i++ ) { 11 product *= i; 12 if( j <= n-k ) { 13 product /= j; 14 } 15 j++; 16 } 17 18 *result = product; 19 }

すべてのループ反復において、カーネルchooseにおけるproduct変数の値は、インデックスiの現在の値に前回の反復からのproductの値を掛けて計算されます。その結果、現在の反復が処理を終了するまで、ループの新しい反復を開始することはできません。下の図は、システムビューアに表示されるカーネルchooseの論理ビューを示しています 。

図 54. カーネルの論理ビュー

カーネル選択のループ分析レポートは、ブロック1のII値が13であることを示します。さらに、詳細ペインでは、高II値が製品へのデータ依存によって発生し、クリティカルパスへの最大貢献者は整数13行目の除算演算。

図 55. カーネルchooseのループ分析レポート
図 56. カーネルchooseのループ分析レポートの詳細ペインの情報

メモリー依存とは、前のループ反復からのメモリーアクセスが完了するまで、ループ反復におけるメモリーアクセスが進まない状況を指す。次の例を検討してください。

1 kernel void mirror_content( unsigned max_i, 2 global int* restrict out) 3 { 4 for (int i = 1; i < max_i; i++) { 5 out[max_i*2-i] = out[i]; 6 } 7 }
図 57. カーネルmirror_contentの論理ビュー

カーネルmirror_contentのループ解析では、詳細ペインはメモリー依存のソースと、ブロック2のクリティカルパスへのコントリビュータの割合(%)を示します。

図 58. カーネルmirror_contentのループ分析の詳細ペイン