Intel FPGA SDK for OpenCL: ベスト・プラクティス・ガイド
はじめに
OpenCL Specification version 1.0、Khronos Group™によるOpenCL Specification version 1.0で説明されているように、OpenCLの概念とアプリケーション・プログラミング・インターフェイス(API)に精通していることを前提としています。 また、OpenCLアプリケーションの作成経験があることを前提としています。
FPGA用のOpenCLアプリケーションの最高のパフォーマンスを実現するには、基礎となるハードウェアの詳細に慣れてください。 さらに、OpenCLアプリケーションをFPGAに変換してマッピングするコンパイラーの最適化についても理解してください。
OpenCL Specification version 1.0の詳細については、Khronos Group ウェブサイトのOpenCL Reference Pagesを参照してください。 OpenCL APIとプログラミング言語の詳細については、OpenCL Specification version 1.0を参照してください。
FPGA概要

FPGAでは、ビットマスキング、シフト、加算などの低レベルの動作がすべて設定可能です。また、これらの動作を任意の順序でアセンブルできます。計算パイプラインを実装するために、FPGAはルックアップ・テーブル(LUT)、レジスター、オンチップメモリー、および算術ハードウェア(例えば、デジタル信号プロセッサー(DSP)ブロック)の組み合わせを再構成可能な接続のネットワークを介して統合します。その結果、FPGAは高度なプログラマビリティを実現します。 LUTは、さまざまなロジック機能を実装します。例えば、LUTをリプログラミングすることにより、ビット単位のAND論理関数からビット単位のXOR論理関数に演算を変更することができます。
アルゴリズムを高速化するためにFPGAを使用することの主な利点は、幅広く異機種でユニークなパイプライン実装をサポートすることです。この特性は、対称型マルチプロセッサー、DSP、およびグラフィックス処理ユニット(GPU)などの多くの異なるタイプの処理ユニットとは対照的です。これらのタイプのデバイスでは、同じ汎用コンピューティング・ハードウェアを複数回複製することによって並列性が実現されます。しかし、FPGAでは、アルゴリズムが実行するロジックのみを複製することで並列処理を実現できます。
プロセッサーは、各クロックサイクルで実行できる作業量を制限する命令セットを実装します。たとえば、ほとんどのプロセッサーには、次のCコードを実行できる専用命令がありません。
E = (((A + B) ^ C) & D) >> 2;
このCコード例のための専用の命令がないと、CPU、DSP、またはGPUは、動作を実行するために複数の命令を実行する必要があります。対照的に、FPGAは、ソフトウェア・アルゴリズムに必要な命令セットを実装できるハードウェア・プラットフォームと考えることができます。上記のコード例を1クロックサイクルで実行する一連の動作を実行するようにFPGAをコンフィグレーションすることができます。 FPGA実装は、特殊な追加ハードウェアとビット単位のXORおよびAND演算を実行するLUTを接続します。その後、デバイスはプログラマブルな接続を使用して、ハードウェア・リソースを消費することなく2ビット右シフトを実行します。この動作の結果は、複雑なパイプラインを形成する後続の動作の一部になります。
パイプライン
マイクロプロセッサー、デジタル信号プロセッサー(DSP)、ハードウェア・アクセラレーター、およびデジタル・ハードウェアの他の高性能実装のデザインは、パイプライン・アーキテクチャを含むことが多いです。
たとえば、以下の図は、多段パイプラインとしての次のコード例を表しています。
for (i = 0; i < 1024; i++) { y[i] = (a[i] + b[i] + c[i] + d[i] + e[i] + f[i] + g[i] + h[i]) >> 3; }
パイプライン・アーキテクチャでは、各算術演算はパイプラインに一度に1つずつ渡されます。したがって、上の図に示すように、飽和パイプラインは、算術演算を同時に並列に計算する8つのステージから構成されています。さらに、多数のループ反復のために、パイプライン・ステージは、後続の各ループ反復に対してこれらの算術命令を同時に実行し続けます。
インテル® FPGA SDK for OpenCL™ のパイプライン・アプローチ
新しいパイプラインがデザインに基づいて構築されます。その結果、高度に構成可能なFPGAの性質に対応することができます。
次のOpenCLコードの断片を検討してください。
C = (A >> 5) + B; F = (D – E) << 3; G = C + F;
FPGA全体を同時に実行する複雑なパイプライン構造をインスタンス化するようにFPGAを設定することができます。この場合、SDKは、下の図に示すように、コードをパイプライン型加算器に入力する2つの独立したパイプライン・エンティティとして実装しています。
Intel® FPGA SDK for OpenCL™オフライン・コンパイラー多数のWork-Item内の動作を並行して実行できるようにすることで、計算を高速化するカスタム・パイプライン構造を提供します。オフライン・コンパイラーは、以下に示すように、クロックサイクルごとに変数C 、 F 、およびGの値を計算するカスタム・パイプラインを作成できます。ランプアップの後、パイプラインは1サイクルあたりSingle Work-Itemのスループットを維持します。
従来のプロセッサーは、共有レジスターのセットが限られています。最終的に、プロセッサーは、記憶されたデータをメモリーに書き出して、より多くのデータがレジスターを占めるようにしなければなりません。オフライン・コンパイラーは、パイプライン内のすべてのアクティブなWork-Itemsのデータを格納するのに十分なレジスターを生成することによって、データをライブ」に保ちます。次のコード例および図は、OpenCLパイプラインのライブ変数Cを示しています。
size_t index = get_global_id(0); C = A[index] + B[index]; E[index] = C – D[index];
シングル・ワーク・アイテム・カーネル対NDRangeカーネル
インテル® FPGA SDK for OpenCL™ ホストはSingle Work-Itemとしてカーネルを実行できます。これはNDRangeサイズが(1,1,1)のカーネルを起動するのと同じです。
OpenCL仕様バージョン1.0では、この動作モードがタスク・パラレル・プログラミングとして記述されています。 タスクとは、Single Work-Itemを含む1つのワークグループで実行されるカーネルのことです。
一般に、ホストはMultiple Work-Itemを並行して起動します。しかし、このデータ並列プログラミング・モデルは、並列Work-Item間で細かいデータを共有する必要がある場合には適していません。このような場合、カーネルをSingle Work-Itemとして表現することで、スループットを最大化することができます。 NDRangeカーネルとは異なり、Single Work-Itemカーネルは、Cプログラミングに似た自然なシーケンシャル・モデルに従います。特に、Work-Item間でデータを分割する必要はありません。
FPGA上でのハイスループットのWork-Itemベースのカーネル実行を確実にするために、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは、ある時点で複数のパイプライン・ステージを並列に処理する必要があります。この並列性は、ループの反復をパイプライン化することによって実現されます。
Single Work-Itemでの累積を示す次の簡単なコード例を検討してください。
1 kernel void accum_swg (global int* a, global int* c, int size, int k_size) { 2 int sum[1024]; 3 for (int k = 0; k < k_size; ++k) { 4 for (int i = 0; i < size; ++i) { 5 int j = k * size + i; 6 sum[k] += a[j]; 7 } 8 } 9 for (int k = 0; k < k_size; ++k) { 10 c[k] = sum[k]; 11 } 12 }


次の図は、iの各反復がどのようにブロックに入るかを示しています。
外部ループを観測すると、IIの値が1であるということは、スレッドの各反復がすべてのクロックサイクルに入ることもできることを意味します。この例では、k_sizeが20でサイズが4であると見なされます。これは、外側のループ反復0~7がそれをストールさせることなく入り込むことができるので、最初の8クロックサイクルにあてはまります。スレッド0が内部ループに入ると、それは4回の反復で終了します。スレッド1〜8は内部ループに入ることができず、スレッド0によって4サイクル停止します。スレッド0の反復が完了すると、スレッド1は内部ループに入ります。その結果、スレッド9は、クロックサイクル13で外側ループに入ります。スレッド9から20は、 サイズの値である4クロックサイクルごとにループに入ります。この例では、外部ループの動的開始間隔が静的に予測される開始間隔1よりも大きく、内部ループのトリップ数の関数であることがわかります。

- 次の関数のいずれかを使用すると、カーネルがNDRangeとして解釈されます。
- get_local_id()
- get_global_id()
- get_group_id()
- get_local_linear_id()
- barrier
- reqd_work_group_size属性が( 1,1,1)以外の値に指定されている場合、カーネルはNDRangeとして解釈されます。それ以外の場合、カーネルは Single Work-Itemカーネルとして解釈されます。
NDRangeで書かれた同じ累算例を検討してください。
kernel void accum_ndr (global int* a, global int* c, int size) { int k = get_global_id(0); int sum[1024]; for (int i = 0; i < size; ++i) { int j = k * size + i; sum[k] += a[j]; } c[k] = sum[k]; }


制限
OpenCLタスク並列プログラミング・モデルは、 Single Work-Itemの実行におけるバリアの概念をサポートしていません。バリア( barrier)をカーネル内のメモリーフェンス( mem_fence )に置き換えます。
マルチ・スレッド・ホスト・アプリケーション
パラレル・スレッドは、クロックサイクルごとに1つのスレッドでパイプライン方式で起動されます。この場合、ループ・パイプラインでパイプラインの並列処理とループ反復間の状態情報の通信が可能になります。ループの依存関係は、1クロックサイクルで解決されないことがあります。
以下の図は、シングル・スレッド・ホスト・アプリケーションが、カーネル実行間で並列の独立したデータパスをどのように処理するかを示しています。
シングル・スレッドのホスト・アプリケーションでは、 OpenCL™ホスト関数呼び出しの周りに外部同期機構を構築する必要があります。スレッドセーフな実行環境でマルチ・スレッド・ホスト・アプリケーションを使用すると、ホストコードを簡素化できます。さらに、ホスト内の複数のデータセットを同時に処理することで、カーネルの実行を高速化することができます。
次の図は、マルチ・スレッド・ホスト・アプリケーションがカーネル実行間で並列の独立したデータ・パスをどのように処理するかを示しています。
カーネルのreport.htmlファイルのレビュー
High Level Design レポートの概要
レポートメニュー
View reportsのプルダウンメニューから、レポートを選択してカーネル デザインの特定部分の分析を表示します。
Analysis ペイン
Analysis ペインは、View reportsのプルダウンメニューで選択したレポートの詳細情報を表示します。
Source Code ペイン
Source Code ペインは、カーネル デザイン内のすべてのソースファイルにおけるコードを表示します。
カーネル デザイン内の異なるソースファイル間を選択するには、Source Code ペイン上でプルダウンメニューをクリックします。Source Code ペインを折りたたむには、オプションが 2 つあります。
- Source Code ペインのプルダウンメニュー横のXアイコンをクリックします。
- レポートメニューの右側にある垂直の省略記号のアイコンをクリックし、Show/Hide source codeを選択します。
あらかじめ Source Code ペインを折りたたんだ状態で展開する場合は、レポートメニューにある垂直の省略記号のアイコンをクリックし、Show/Hide source codeを選択します。
Details ペイン
ループ分析またはエリアレポートに表示される各ラインは、使用可能な場合、Detail カラムレポートのコメントを詳しく述べる追加情報を表示します。Details ペインを閉じるために Details ペインを折りたたむには、オプションが 2 つあります。
- Detail Code ペインのプルダウンメニュー横のXアイコンをクリックします。
- レポートメニューの左側にある垂直の省略記号のアイコンをクリックし、Show/Hide detailsを選択します。
Report Summary のレビュー 、
Report Summary は、デザイン内の各カーネル の要約を含んだデザインのコンパイル結果のおおまかな概要と、デザイン内の各カーネル が使用する見積られたリソースの要約を提供します。
Report Summary は、Info、Kernel Summary 、Estimated Resource Usage、および Compile Warnings の 4 つ のセクションに分かれています。
Info
- プロジェクトの名前
- ターゲット FPGA ファミリー、デバイス、およびボード
- インテル® Quartus® Prime のバージョン
- AOC のバージョン
- デザインをコンパイルするために使用されたコマンド
- レポートが生成された日付と時間
Kernel Summary
- カーネルが NDRange か Single Work-Item kernel か
- autorun 属性が使用されているかどうか
- カーネルで要求されるワークグループ・サイズ
- 計算ユニットの数
- カーネルのベクトル化
- 最大グローバルワーク・ディメンション
- ワークグループの最大のサイズ
Estimated Resource Usage
Estimated Resource Usage セクションは、すべてのチャネルで使用される見積られたリソース、グローバル・インターコネクトの見積られたリソース、定数キャッシュ、およびボード・インターフェイスと同様に、デザインの各カーネルで使用される見積られるリソースの要約を表示します。
Compile Warnings
Compile Warnings セクションは、コンパイラーがコンパイル時に生成した警告のうちのいくつかを表示します。
ループ情報のレビュー
High Level Design レポート ( <your_kernel_filename>/reports/report.html) ファイルには、デザインのすべてのループとそれらのアンロールステータスに関する情報が含まれています。このループ分析レポートは、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーがカーネル のスループットを最大化できるかどうかを調べるのに役立ちます。
-
#pragma unroll
#pragma unrollについて詳しくは、Intel FPGA SDK for OpenCL Programming Guideの「Unrolling a Loop」を参照してください。
-
#pragma loop_coalesce
#pragma loop_coalesceについて詳しくは、Intel FPGA SDK for OpenCL Programming Guideの「Coalescing Nested Loops」を参照してください。
-
#pragma ii
#pragma iiについて詳しくは、Intel FPGA SDK for OpenCL Programming Guideの「Specifying a loop initiation interval (II)」を参照してください。
- View reports > Loop Analysisをクリックします。
- Analysis ペインでShow fully unrolled loopsを選択し、デザイン内のすべてのループの情報を取得します。
-
デザインのスループットを向上させるためのアクションを特定するには、下のフローチャートを参照します。
要確認: II はループのイニシエーション・インターバルを指し、新しいループ反復処理までの開始時間を示します。II の値 = 1 が理想的であり、すなわち、 パイプラインはクロックサイクルごとに新しいループ反復処理が可能なため、パイプラインが最大の効率で機能していることを意味します。
OpenCLのデザイン例のループ解析レポート
1 // ND-Range kernel with unrolled loops 2 __attribute((reqd_work_group_size(1024,1,1))) 3 kernel void t (global int * out, int N) { 4 int i = get_global_id(0); 5 int j = 1; 6 for (int k = 0; k < 4; k++) { 7 #pragma unroll 8 for (int n = 0; n < 4; n++) { 9 j += out[k+n]; 10 } 11 } 12 out[i] = j; 13 14 int m = 0; 15 #pragma unroll 1 16 for (int k = 0; k < N; k++) { 17 m += out[k/3]; 18 } 19 #pragma unroll 20 for (int k = 0; k < 6; k++) { 21 m += out[k]; 22 } 23 #pragma unroll 2 24 for (int k = 0; k < 6; k++) { 25 m += out[k]; 26 } 27 out[2] = m; 28 }
このデザイン例のループ分析レポートは、コードで定義されているさまざまな種類のループのアンロール戦略を強調しています。

Intel® FPGA SDK for OpenCL™オフライン・コンパイラーはソースコードに基づいて以下のループ・アンローリング戦略を実行します。
- 最初のループ(ライン6)を完全に展開します。
- #pragma unrollの仕様のため、最初のループ内(ライン8)で内側のループを完全に展開します。
- #pragma unroll 1仕様のため、2番目の外部ループBlock2(ライン16)をアンロールしません。
- #pragma unrollの仕様のため、3番目の外部ループ(ライン20)を完全に展開します。
- #pragma unroll 2の仕様のため、4番目の外部ループBlock4(ライン24)を2回展開します。
メモリー・アクセス・パターンの例の変更
kernel void big_lmem_4r_4w_nosplit (global int* restrict in, global int* restrict out) { local int lmem[4][1024]; int gi = get_global_id(0); int gs = get_global_size(0); int li = get_local_id(0); int ls = get_local_size(0); int res = in[gi]; #pragma unroll for (int i = 0; i < 4; i++) { lmem[i][(li*i) % ls] = res; res >>= 1; } // Global memory barrier barrier(CLK_GLOBAL_MEM_FENCE); res = 0; #pragma unroll for (int i = 0; i < 4; i++) { res ^= lmem[i][((ls-li)*i) % ls]; } out[gi] = res; }
この例のシステム・ビューア・レポートは、ストール可能なロードおよびストアを強調表示します。
ロードとストアの動作の間の最初のバンクでは、アービトレーションが高い2つのメモリーバンクしか作成されないことに注意してください。次に、次のコード例に示すように、バンク・インデックスを第2次元に切り替えます。
kernel void big_lmem_4r_4w_nosplit (global int* restrict in, global int* restrict out) { local int lmem[1024][4]; int gi = get_global_id(0); int gs = get_global_size(0); int li = get_local_id(0); int ls = get_local_size(0); int res = in[gi]; #pragma unroll for (int i = 0; i < 4; i++) { lmem[(li*i) % ls][i] = res; res >>= 1; } // Global memory barrier barrier(CLK_GLOBAL_MEM_FENCE); res = 0; #pragma unroll for (int i = 0; i < 4; i++) { res ^= lmem[((ls-li)*i) % ls][i]; } out[gi] = res; }
カーネル・メモリー・ビューアでは、4つのメモリーバンクが別々のロード・ストア・ユニットで作成されていることがわかります。すべてのロードストア命令はストールフリーです。
loop_coalesceを使用してネストループによって消費されるエリアの削減
ネストされたループのレイテンシーを削減する方法を説明するためにorigとlc_testカーネルを使用する次の例を検討してください。
origカーネルはネストされたループを4の深さまでがあります。ネストされたループは、次のレポートに示すように、変数が保持されているためにエリアを消費する余分なブロック(ブロック2,3,4,6,7,8)を作成しました。
ループの合体により、 lc_testのレイテンシーが短縮されていることがわかります。 origカーネルのブロック5とlc_testカーネルのブロック12は、最も内側のループです。
エリア情報の確認
地域レポートには、以下の目的があります。
- OpenCLシステム全体の詳細なエリア内訳を提供します。内訳はソースコードに関連しています。
- 生成されたハードウェアを洞察し、潜在的な非効率性を解決するための提案を提供します。
- システムエリア :
- これは、すべてのカーネル、チャネル、インターコネクト、ボードロジックで使用されます。
- カーネルエリア :
- オーバーヘッド(例えば、ディスパッチ・ロジック)を含めて、特定のカーネルによって使用されます。
- 基本ブロックエリア :
- これは、カーネル内の特定の基本ブロックによって使用されます。基本的なブロックエリアは、ソースコードのブランチのないセクション(ループ本体など)を表します。

ソース別エリア分析
4つのループを含むOpenCLカーネルの例:
1 // ND-Range kernel with unrolled loops 2 __attribute ((reqd_work_group_size(1024,1,1))) 3 kernel void t (global int * out, int N) { 4 int i = get_global_id(0); 5 int j = 1; 6 for (int k = 0; k < 4; k++) { 7 #pragma unroll 8 for (int n = 0; n < 4; n++) { 9 j += out[k+n]; 10 } 11 } 12 out[i] = j; 13 14 int m = 0; 15 #pragma unroll 1 16 for (int k = 0; k < N; k++) { 17 m += out[k/3]; 18 } 19 #pragma unroll 20 for (int k = 0; k < 6; k++) { 21 m += out[k]; 22 } 23 #pragma unroll 2 24 for (int k = 0; k < 6; k++) { 25 m += out[k]; 26 } 27 out[2] = m; 28 }
以下のエリアレポートには、カーネルシステム、ボード・インターフェイス、およびグローバル・インターコネクトのエリア使用量がリストされています。これらの要素はシステムレベルのIPで、デザインがターゲットとするカスタム・プラットフォームまたはリファレンス・プラットフォームに依存します。カーネルtはカーネルシステムの階層内にあり、ソースコードが始まる場所です。このレポートは、カーネルtの下でソースコード内で宣言されたすべての変数を指定し、残りのエリア情報を行番号でソートします。

この例では、コードラインj + = out [k + n]のコードライン(9行目)では、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラー追加を実行してグローバルメモリーからデータをロードするのに必要なエリアに基づいて推定エリア使用量を計算します。コードラインout [i] = j (12行目)の場合、オフライン・コンパイラーはポインター値を計算するために必要なエリアに基づいて推定エリア使用量を計算し、グローバルメモリーに格納し直します。
システムのエリア分析
4つのループを含むOpenCLカーネルの例:
1 // ND-Range kernel with unrolled loops 2 __attribute((reqd_work_group_size(1024,1,1))) 3 kernel void t (global int * out, int N) { 4 int i = get_global_id(0); 5 int j = 1; 6 for (int k = 0; k < 4; k++) { 7 #pragma unroll 8 for (int n = 0; n < 4; n++) { 9 j += out[k+n]; 10 } 11 } 12 out[i] = j; 13 14 int m = 0; 15 #pragma unroll 1 16 for (int k = 0; k < N; k++) { 17 m += out[k/3]; 18 } 19 #pragma unroll 20 for (int k = 0; k < 6; k++) { 21 m += out[k]; 22 } 23 #pragma unroll 2 24 for (int k = 0; k < 6; k++) { 25 m += out[k]; 26 } 27 out[2] = m; 28 }

システムビューでは、カーネルは論理ブロックに分割されています。ブロックに関連付けられているコードラインのエリア使用情報を表示するには、そのブロックのレポートエントリーを展開するだけです。この例では、コードラインout [i] = j (12行目)のエリア情報がBlock1の下で使用可能です。システムビューでのライン12の推定エリア使用量は、ソースビューでの見積もりと同じです。
メモリー・レプリケーションとストールに関する情報の確認
システムビューアには、OpenCLシステムの抽象ネットリストが表示されます。システムビューアでOpenCLデザインのグラフィカルな表示を確認すると、メモリーの複製を確認し、停止可能なロードおよびストア命令を識別できます。
System Viewerの機能
次の方法でシステムビューアと対話できます。
- マウスホイールを使用して、システムビューア内で拡大または縮小します。
- 赤い論理ブロックに関連付けられているデザインの部分を確認します。たとえば、High Initiation Interval (II)値を持つパイプライン・ループを持つロジックブロックは、High II値がデザインのスループットに影響を及ぼす可能性があるため、赤で強調表示されます。
- ブロック内の任意のノードにカーソルを合わせると、そのノードの情報がツールチップと詳細ペインに表示されます。
- 非表示にする接続のタイプをオフにして、システムビューアに含める接続のタイプを選択します。デフォルトでは、 ControlとMemoryの両方がシステムビューアでチェックされます。 Controlとは、ブロックとループの間の接続を指します。Memoryとは、グローバルメモリーまたはローカルメモリーとの接続を指します。デザインに読み出しチャンネルまたは書き込みチャンネルとの接続がある場合、システムビューアにChannelsオプションもあります。
Kernel Memory Viewer の特長
多くのアルゴリズムでは、データ移動がボトルネックになることがよくあります。High Level Design レポート (report.html) 内の Kernel memory viewer は、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーが カーネルのメモリーシステム上のデータ接続をどのように解釈するかを示しています。 kernel memory viewer を使用すると、 カーネルデザインのデータ移動のボトルネックの特定に役立ちます。
更に、メモリーアクセスの一部のパターンは LSU ( ロードストアー・ユニット ) で望まない調停を引き起こし、 カーネルのスループット性能に影響を与える可能性があります。 Kernel memory viewer を使用すると、LSU で望ましくない調停が発生する可能性のある位置を見つけることができます。
- Memory List
- Memory List ペインは、
カーネルの階層、その
カーネル内のメモリー、および対応するメモリーバンクを表示します。
リストにあるメモリー名をクリックすると、 Kernel memory viewer ペイン内のメモリーがグラフィック表示されます。また、メモリーを宣言したコード内のラインは、Source Code ペインでハイライトで表示されます。
メモリーバンクでのチェックボックスをクリアーすると、そのバンクが Kernel Memory Viewer ペインに表示され、複雑なメモリーデザインを表示する際に特定のメモリーバンクに焦点を当てるのに役立ちます。デフォルトでは、 カーネルメモリー内のすべてのバンクが選択され、 Kernel Memory Viewer ペインで表示されます。
- Kernel Memory Viewer
-
Kernel Memory Viewer ペインは、メモリーシステム内のバンク上のロジックポートを特定するためにロードとストアー間の接続を表示します。次のノードのタイプは
Kernel Memory Viewer ペイン内で表示される可能性があり、
kernelメモリーシステムによります。
- Memory node: メモリー
- Bank node: メモリー内のバンク。Memory List ペインで選択されているバンクのみ表示されます。Memory List ペイン内でのバンクの選択は複雑なメモリーデザインの表示に役立ちます。
-
Port node: バンクでのロジックポート。次の 3 種類のポートがあります。
- R: 読み取り専用ポート
- W: 書き込み専用ポート
- RW: 読み取りおよび書き込みを有するポート
- LSU node: メモリーに接続されるストアー (ST) またはロード (LD) ノード
- Arbitration node: アービトレーション (ARB) ノードは LSU が共有ポートノードへのアクセスを競合していることを示します。
- Port-sharing node: ポートシェアリング (SHARE) ノードは LSU が共有ポートノードへの排他的なアクセスを持つことを示しているため、ロードストアー・ユニットにはストールがありません。
任意のノードにカーソルを置くと、そのノードの属性が表示されます。
LSU ノードにカーソルを置くと、LSU ノードから LSU が接続するすべてのポートまでのパスがハイライトで表示されます。
ポートノードにカーソルを置くと、ポートノードからポートノードに格納されているすべての LSU までのパスがハイライトで表示されます。
ノードを選択するためにクリックし、Details ペインにノード属性を表示します。
- Details
- Details ペインは
Kernel Memory Viewer ペインで選択されたノードの属性を表示します。例えば、
カーネル内のメモリーを選択する際、 ペインはソースコードで指定した任意のユーザー定義の属性と同様に、メモリーバンクの幅や深さなどの情報を表示します。
Details ペインの内容は Kernel Memory Viewer ペイン内で異なるノードを選択するまで保持されます。
HTMLレポートの情報に基づいたOpenCLのデザイン例の最適化
行列の正方形AxAを実行するOpenCLのデザイン例:
// performs matrix square A*A // A is a square len*len matrix kernel void matrix_square (global float* restrict A, unsigned len, global float* restrict out) { for(unsigned oi = 0; oi < len*len; oi++) { float sum = 0; int row = oi % len; for (int col = 0; col < len; col++) { unsigned i = (row * len) + col; // 0, 1, 2, 3, 4,... unsigned j = (col * len) + row; // 0, 3, 6, 9, 1,... sum += A[i] * A[j]; } out[oi] = sum; } }
カーネルmatrix_squareのエリアレポートのシステムビューは、 ブロック3のフリップフロップ(FF)とRAMの推定使用量が高いことを示しています。システムビューアのBlock3をさらに調べると、Block3のレイテンシー値も高いことがわかります。

これらのパフォーマンスのボトルネックの原因は、システムがループ内からグローバルメモリーからデータをロードしているためです。したがって、次の変更されたコードに示すように、データをローカルメモリーにプリロードすることが、最初に行う最適化のステップです。
kernel void matrix_square_v1 (global float* restrict A, unsigned len, global float* restrict out) { // 1. preload the data into local memory // - suppose we know the max size is 4X4 local int cache_a[16];for(unsigned k = 0; k < len*len; k++) { cache_a[k] = A[k];} for(unsigned oi = 0; oi < len*len; oi++) { float sum = 0; int row = oi % len; for(int col = 0; col < len; col++) { unsigned i = (row * len) + col; // 0, 1, 2, 3, 4,... unsigned j = (col * len) + row; // 0, 3, 6, 9, 1,... sum += cache_a[i] * cache_a[j]; } out[oi] = sum; } }

エリアレポートとシステムビューアの結果に示されているように、ローカルメモリーにデータをプリロードすると、RAMの使用量が3分の1に減少し、レイテンシー値が255から97に低下します。
matrix_square_v1のエリアレポートをさらに調べると、以下のエリアレポートの行30であるコードラインint row = oi%lenは、法計算のために異常に大きなエリアが使用されます。

モジュラス計算を削除して列カウンターに置き換えると、修正されたカーネルmatrix_square_v2に示すように、適応ルックアップテーブル(ALUT)およびFF使用量を50%削減できます。
kernel void matrix_square_v2 (global float* restrict A, unsigned len, global float* restrict out) { // 1. preload the data into local memory // - suppose we know the max size is 4X4 // 2. remove the modulus computation local int cache_a[16]; for (unsigned k = 0; k < len*len; k++) { cache_a[k] = A[k]; } unsigned row = 0; unsigned ci = 0; for (unsigned oi = 0; oi < len*len; oi++) { float sum = 0; // keep a column counter to know when to increment rowif (ci == len) { ci = 0; row += 1;}ci += 1; for (int col = 0; col < len; col++) { unsigned i = (row * len) + col; // 0, 1, 2, 3, 4,... unsigned j = (col * len) + row; // 0, 3, 6, 9, 1,... sum += cache_a[i] * cache_a[j]; } out[oi] = sum; } }

matrix_square_v2のエリアレポートをさらに調べると、インデックスiとjの計算(つまり、符号なしi =(row * len)+ colおよび符号なしj =(col * len)+ row)ではALUTとFFの使用量の見積もりが非常に異なることがわかります。さらに、エリアレポートは、これらの2つの計算がデジタル信号処理(DSP)ブロックを使用していることも示しています。

インデックス計算のためにDSPとRAMブロックの使用を最適化する方法は、乗算計算を削除して、下記の修正されたカーネルmatrix_square_v3に示すように、加算を追跡するだけです。
kernel void matrix_square_v3 (global float* restrict A, unsigned len, global float* restrict out) { // 1. preload the data into local memory // - suppose we know the max size is 4X4 // 2. remove the modulus computation // 3. remove DSP and RAM blocks for index calculation helps reduce the latency local int cache_a[16]; for (unsigned k = 0; k < len*len; k++) { cache_a[k] = A[k]; } unsigned row_i = 0; unsigned row_j = 0; unsigned ci = 0; for (unsigned oi = 0; oi < len*len; oi++) { float sum = 0; unsigned i, j; // keep a column counter to know when to increment row if (ci == len) { ci = 0; row_i += len; row_j += 1; } ci += 1; i = row_i; // initialize i and jj = row_j;for (int col = 0; col < len; col++) {i += 1; // 0, 1, 2, 3, 0,...j += len; // 0, 3, 6, 9, 1,... sum += cache_a[i] * cache_a[j]; } out[oi] = sum; } }
乗算ステップを削除することで、下記のエリアレポートに示すように、DSP使用率を50%削減できます。さらに、この修正はレイテンシーを短縮するのに役立ちます。

レイテンシーをさらに削減するために、修正されたカーネルmatrix_square_v3のループ分析レポートを確認することができます。以下に示すように、解析ペインと詳細ペインでは、 sum + = cache_a [i] * cache_a [j]のループに依存する依存関係があるため、Block27にIIボトルネックが発生しています。

ループで運ばれる依存関係を解決するには、修正されたカーネルmatrix_square_v4で強調表示されているコードに示すように、計算の乗算部分と加算部分を分けることができます。
kernel void matrix_square_v4 (global float* restrict A, unsigned len, global float* restrict out) { // 1. preload the data into local memory // - suppose we know the max size is 4X4 // 2. remove the modulus computation // 3. remove DSP and RAM blocks for index calculation helps reduce the latency // 4. remove loop-carried dependency 'sum' to improve throughput by trading off area local int cache_a[16]; for (unsigned k = 0; k < len*len; k++) { cache_a[k] = A[k]; } unsigned row_i = 0; unsigned row_j = 0; unsigned ci = 0; for (unsigned oi = 0; oi < len*len; oi++) { float sum = 0; unsigned i, j; float prod[4]; // make register #pragma unroll for (unsigned k = 0; k < 4; k++) { prod[k] = 0; } // keep a column counter to know when to increment row if (ci == len) { ci = 0; row_i += len; row_j += 1; } ci += 1; i = row_i; // initialize i and j j = row_j; for (int col = 0; col < len; col++) { i += 1; // 0, 1, 2, 3, 0,... j += len; // 0, 3, 6, 9, 1,... prod[col] = cache_a[i] * cache_a[j]; } sum = prod[0];#pragma unrollfor (unsigned k = 1; k < 4; k++) { sum += prod[k];} out[oi] = sum; } }
以下のエリアレポートおよびシステムビューアの結果に示されているように、計算ステップを分割することで、エリア使用量の増加を犠牲にしてより高いスループットを達成できます。この変更により、ループのII値が1に減少し、レイテンシーが30から24に減少します。

HTMLレポート:エリア・レポート・メッセージ
- ボード・インターフェイスのエリア・レポート・メッセージ
エリアレポートは、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーカスタム・プラットフォームまたはボード・インターフェイス用に生成されます。 - 機能オーバヘッドのエリア・レポート・メッセージ
エリアレポートは、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーディスパッチカーネルなどのタスク用に生成されます。 - 州のエリア・レポートメッセージ
エリアレポートは、デザインがライブ値と制御ロジックに使用するリソースの量を示します。 - フィードバックのためのエリア・レポート・メッセージ
エリアレポートには、デザインがループに依存する依存関係に使用するリソースが指定されています。 - 定数メモリーのエリア・レポート・メッセージ
エリアレポートは、定数キャッシュメモリーのサイズを指定します。また、データ複製や読み出し動作の回数などの情報も提供します。 - プライベート変数ストレージのエリア・レポート・メッセージ
エリアレポートは、 OpenCL™デザインに基づいたプライベート・メモリーの実装に関する情報を提供します。
ボード・インターフェイスのエリア・レポート・メッセージ
メッセージ | 説明 |
---|---|
プラットフォーム・インターフェイス・ロジック。 | — |
機能オーバヘッドのエリア・レポート・メッセージ
メッセージ | 説明 |
---|---|
カーネルのディスパッチ・ロジック |
max_global_work_dim(0)カーネル属性を含むカーネルにはオーバーヘッドはありません。その結果、この行は対応するエリアレポートには存在しません。 |
州のエリア・レポートメッセージ
Stateの報告された面積消費量を減らすには、次のようにデザインを変更します。
- ローカル変数のサイズの低減
- ローカル変数の範囲を可能な限りローカライズして範囲の低減
- カーネル内のネストされたループの数を減らす
フィードバックのためのエリア・レポート・メッセージ
フィードバックの下で報告されたエリアの消費量を減らすには、デザイン内のループ実行変数の数とサイズを低減します。
定数メモリーのエリア・レポート・メッセージ
メッセージ | 説明 |
---|---|
<N>バイトの定数キャッシュはすべてのカーネルからアクセス可能で、カーネル呼び出し間で永続的です。 キャッシュ内のデータは、<Y>の読み出しをサポートするために<X>回複製されます。 ヒット、ミスに対して最適化されたキャッシュは、大きなペナルティを発生します。キャッシュ内のデータ量が少ない場合、値をカーネル引数として渡すことを検討してください。キャッシュの有効性を評価するために、キャッシュに対するアクセスのストールをチェックするには、Intel FPGA Dynamic Profiler for OpenCLを使用します。実際のキャッシュヒット率のプロファイリングは現在サポートされていません。 | — |
プライベート変数ストレージのエリア・レポート・メッセージ
メッセージ | 説明 |
---|---|
オンチップブロックRAMを用いたプライベート・メモリーの実現 | |
オンチップのブロックRAMに実装されたプライベート・メモリー。 | ブロックRAM実装は、NDRangeカーネルのローカルメモリーと同様のシステムを作成します。 |
オンチップブロックROMを用いたプライベート・メモリーの実現 | |
— | オンチップブロックROMを使用するたびに、オフライン・コンパイラーは同じROMの別のインスタンスを作成します。オフライン・コンパイラーがオンチップブロックROMに実装するプライベート変数の明示的な注釈はありません。 |
レジスターを用いたプライベート・メモリーの実装 | |
次のサイズのレジスターを使用して実装されています。 - <X> registers of width <Y> and depth <Z> [(depth was increased by a factor of <N> due to a loop initiation interval of <M>.)] - ... |
オフライン・コンパイラーがプライベート変数をレジスターに実装することを報告します。オフライン・コンパイラーは、多くのレジスターにプライベート変数を実装することがあります。このメッセージは、特定の幅と深さを持つレジスターのリストを提供します。 |
シフトレジスターを用いたプライベート・メモリーの実装 | |
<N>またはより少ないタップポイントを持つシフトレジスターとして実装されています。これは非常に効率的なストレージタイプです。 次のサイズのレジスターを使用して実装されています。 - <X> register(s) of width <Y> and depth <Z> - ... |
オフライン・コンパイラーがシフトレジスターにプライベート変数を実装することを報告します。このメッセージは、シフトレジスターの特定の幅と深さのリストを提供します。 オフライン・コンパイラーは、タップポイントに応じて、単一の配列を複数の小さなシフトレジスターに分割することがあります。
注: オフライン・コンパイラーはタップポイントの数を過大評価する可能性があります。
|
レジスター付きバレルシフタを用いたプライベート・メモリーの実装 | |
動的インデックス作成によるレジスター付きバレルシフタとして実装されています。これは高オーバーヘッド・ストレージ・タイプです。可能であれば、コンパイル時の既知の索引付けに変更します。この変数にアクセスするためのエリアコストは、アクセスが発生する行に表示されます。 次のサイズのレジスターを使用して実装されています。 - <X> registers of width <Y> and depth <Z> [(depth was increased by a factor of <N> due to a loop initiation interval of <M>.)] - ... |
オフライン・コンパイラーは、ダイナミック・インデックスのためにレジスターを持つバレルシフターにプライベート変数を実装することを報告します。 レポートのこの行には、プライベート変数の全エリア使用量が指定されていません。レポートには、変数にアクセスする行に関する追加のエリア使用情報が表示されます。 |
- エリアレポートは、実装によっては、プライベート・メモリーを宣言または使用するコードラインにメモリー情報を注釈します。
- オフライン・コンパイラーがオンチップブロックRAMにプライベート・メモリーを実装すると、エリアレポートは、関連するローカルメモリー固有のメッセージをプライベート・メモリーシステムに表示します。
HTMLレポート:カーネルデザインの概念

- Kernels
Intel® FPGA SDK for OpenCL™ Offline Compilerは、get_global_id()やget_local_id()などのビルトインのワークアイテム関数を使用しないカーネルを単一のワークアイテムカーネルとしてコンパイルします。 - Global Memory Interconnect
OpenCL™システムには、さまざまな種類のグローバルメモリーインターコネクトが存在します。メモリー相互接続は、 ロード・ストア・ユニット ( LSU )と呼ばれることもあります。 - ローカルメモリー
- Nested Loops
- Single Work-Itemカーネルのループ
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーデータ処理のパフォーマンスを最大限にするためにカーネルを最適化するアルゴリズムを実装しています。 - チャネル
Intel® FPGA SDK for OpenCL™のチャネルインプリメンテーションは、あるカーネルから別のカーネルにデータを渡してパフォーマンスを向上させる柔軟な方法を提供します。 - ロード・ストア・ユニット
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーさまざまな種類のロードストアユニット(LSU)を生成します。 LSUの種類によっては、コンパイラーがメモリー・アクセス・パターンやその他のメモリー属性に応じてLSUの動作やプロパティを変更することがあります。
Kernels
組み込みのWork-Item関数の詳細については、OpenCL Specification version 1.0の6.11.1: Work-Item Functionsのいセクションを参照してください。
単一のWork-Itemカーネルの場合、オフライン・コンパイラーはカーネル内のすべてのループをパイプライン化して、複数のループ反復を同時に実行できるようにします。コンパイラーがループの一部を効果的にパイプライン化できない場合、またはループをパイプライン化できない場合、カーネルのパフォーマンスが低下する可能性があります。
オフライン・コンパイラーは、NDRangeカーネルでループをパイプライン化できません。ただし、これらのループは複数のWork-Itemを同時に受け入れることができます。カーネルには複数のループがあり、それぞれにネストされたループがあります。外側のループごとにネストされたループの反復の総数を表にすると、カーネルのスループットは、通常、テーブル化した最大の反復の合計値だけ減少します。
効率的にNDRangeカーネルを実行するには、通常、多数のスレッドが必要です。
Global Memory Interconnect
GPUとは異なり、FPGAはアプリケーションに最適なカスタムLSUを構築できます。結果として、アプリケーションに理想的なLSUタイプを選択するOpenCLコードを書く能力が、デザインのパフォーマンスを大幅に向上させるのに役立つかもしれません。
デザインのHTMLエリアレポートを見直すと、システムレベルのグローバル・インターコネクトエントリーの値は、グローバル・メモリー・インターコネクトのサイズを表します。

HTMLレポートでは、メモリー・システム・ビューアは、グローバルメモリー相互接続を負荷(LD)、ストア(ST)、および接続(灰色線)として示しています。

Intel® FPGA SDK for OpenCL Offline Compilerは、デザインのメモリー・アクセス・パターンに基づいて、OpenCLシステム用の適切なタイプのLSUを選択します。例示的なLSUタイプには、連続アクセス(または連続アクセス)およびバースト・インターリーブ・アクセスが含まれています。 図 79そして図 78連続したメモリーアクセスとバースト・インターリーブされたメモリーアクセスとの間のアクセスパターンの違いをそれぞれ示しています。
ローカルメモリー
ローカルメモリーは複雑なシステムです。異なるレベルのキャッシュがある一般的なGPUアーキテクチャとは異なり、FPGAはローカルメモリーをFPGA内部の専用メモリーブロックに実装します。
ローカルメモリー特性
- ポート - ローカルメモリーの各バンクには、デザインが同時にアクセスできる書き込みポートと読み出しポートがあります。
- ダブルポンピング - ダブルポンピング機能により、各ローカル・メモリー・バンクは最大3つのリードポートをサポートします。詳細については、 ダブルポンピングのセクションを参照してください。
ローカルメモリーは複雑なシステムです。異なるレベルのキャッシュがある一般的なGPUアーキテクチャとは異なり、FPGAはローカルメモリーをFPGA内部の専用メモリーブロックに実装します。
カーネルのコードでは、 local型の変数としてローカルメモリーを宣言します。
local int lmem[1024];
Intel® FPGA SDK for OpenCL™ Offline Compilerは、幅、深さ、バンク、レプリケーション、相互接続などのローカルメモリープロパティをカスタマイズします。オフライン・コンパイラーは、コードに基づいてアクセスパターンを分析し、アクセス競合を最小限に抑えるためにローカルメモリーを最適化します。
下の図は、サイズ、幅、深さ、バンク、およびレプリケーションの基本的なローカルメモリープロパティを示しています。
HTMLレポートでは、ローカルメモリーの全体的な状態は最適であると報告されていますが、複製されており、潜在的に非効率的です。
高効率カーネルをデザインするための鍵は、決してストールしないメモリーアクセスを持つことです。この場合、データパス内のすべての同時メモリーアクセスサイトは、競合することなくメモリーにアクセスすることが保証されています。
複雑なカーネルでは、オフライン・コンパイラーは、メモリーアクセスに競合があるかどうかを推測するのに十分な情報がない可能性があります。その結果、オフライン・コンパイラーはローカルメモリーロードストアユニット(LSU)を推論してメモリーアクセスを調停します。しかし、LSUを推論することは非効率を引き起こすかもしれない。詳細については、 ローカルメモリーLSUを参照してください。
オフライン・コンパイラーは、指定した正確なサイズのローカルメモリーを実装するとは限りません。 FPGA RAMブロックは特定のディメンションを持つため、オフライン・コンパイラーはサポートされている次のRAMブロック・ディメンションに切り上げるローカルメモリーサイズを実装します。 RAMブロックの詳細については、デバイス固有の情報を参照してください。
ローカル・メモリー・バンク
ローカル・メモリー・バンクは、デフォルトで最小次元でのみ機能します。複数のバンクを有することにより、同時書込みが可能になる。次の図は、次のローカル変数宣言の実装を示しています。
local int lmem[1024][4];
ループ内の各ローカルメモリーアクセスには、別々のアドレスがあります。次のコード例では、オフライン・コンパイラーは4つの別々のバンクを作成するためにlmemを推論できます。ループはlmem [] []への4つの同時アクセスを可能にし、最適な構成を実現します。
kernel void bank_arb_consecutive_multidim (global int* restrict in, global int* restrict out) { local int lmem[1024][BANK_SIZE]; int gi = get_global_id(0); int gs = get_global_size(0); int li = get_local_id(0); int ls = get_local_size(0); int res = in[gi]; #pragma unroll for (int i = 0; i < BANK_SIZE; i++) { lmem[((li+i) & 0x7f)][i] = res + i; res >> 1; } int rdata = 0; barrier(CLK_GLOBAL_MEM_FENCE); #pragma unroll for (int i = 0; i < BANK_SIZE; i++) { rdata ^= lmem[((li+i) & 0x7f)][i]; } out[gi] = rdata; return; }
local int [4] [128] __attribute __((bank_bits(8,7)、bankwidth(4)));
#define BANK_SIZE 4 kernel void bank_arb_consecutive_multidim_origin (global int* restrict in, global int* restrict out) { local int a[BANK_SIZE][128] __attribute__((bank_bits(8,7),bankwidth(4))); int gi = get_global_id(0); int li = get_local_id(0); int res = in[gi]; #pragma unroll for (int i = 0; i < BANK_SIZE; i++) { a[i][((li+i) & 0x7f)] = res + i; res >> 1; } int rdata = 0; barrier(CLK_GLOBAL_MEM_FENCE); #pragma unroll for (int i = 0; i < BANK_SIZE; i++) { rdata ^= a[i][((li+i) & 0x7f)]; } out[gi] = rdata; return; }
結果のメモリーのビューは、最初の例の初期ビューと同じです。しかし、バンク・オンに間違ったビットを指定すると、メモリーアービトレーション・ロジックが変化します。
local int a[4][128] __attribute__((bank_bits(4,3),bankwidth(4)));

コンパイラーがローカルメモリーへのアクセスを別々のアドレスに推論できない場合、ローカルメモリーの相互接続を使用してアクセスを調停し、パフォーマンスを低下させます。
ローカルメモリー複製
ローカルメモリーの複製により、同時に読み出し動作が実行されます。オフライン・コンパイラーは、効率的なローカルメモリーアクセスのためにデザインを最適化して、全体的なパフォーマンスを最大化します。メモリー・レプリケーションは、場合によっては非効率なハードウェアにつながりますが、メモリー・レプリケーションは必ずしもRAMの使用を増加させるとは限りません。
オフライン・コンパイラーが3つ以上のワークグループが同時にローカルメモリーから読み出していることを認識すると、ローカルメモリーをレプリケートします。ローカルメモリーの複製がデザインエリアを大幅に増やす場合、カーネル内の障壁の数を減らすか、またはmax_work_group_size値を大きくして複製のファクタを下げることを検討してください。
ダブルポンピング
デフォルトでは、各ローカル・メモリー・バンクには1つの読み出しポートと1つの書き込みポートがあります。ダブルポンピング機能により、各ローカル・メモリー・バンクは最大3つの読み出しポートをサポートすることができます。
ダブルポンピングを可能にする基本的なメカニズムは、M20Kハードウェアにあります。最初のクロックサイクル中、M20Kブロックはダブルクロックになります。次に、第2のクロックサイクルの間、ポートは多重化されて2つの読み出しポートがさらに形成されます。
ダブルポンピング機能をイネーブルすると、オフライン・コンパイラーはエリア対最大周波数を交換します。オフライン・コンパイラーは、ヒューリスティック・アルゴリズムを使用して最適なメモリー構成を決定します。
ダブルポンピングの利点:
- 1つの読み出しポートから3つの読み出しポートに増加する
- RAM使用量を節約する
ダブルポンピングの短所:
- 冗長ロジックを実装する
- 最大周波数を下げる可能性がある
次のコード例は、8つの読み出しポートと1つの書き込みポートを持つローカルメモリーの実装を示しています。オフライン・コンパイラーは、ダブルポンピングを可能にし、ローカルメモリーを3回複製して、最大9つの読み出しポートをサポートできるメモリー構成を実装します。
#define NUM_WRITES 1 #define NUM_READS 8 #define NUM_BARRIERS 1 local int lmem[1024]; int li = get_local_id(0); int res = in[gi]; #pragma unroll for (int i = 0; i < NUM_WRITES; i++) { lmem[li - i] = res; res >>= 1; } // successive barriers are not optimized away #pragma unroll for (int i = 0; i < NUM_BARRIERS; i++) { barrier(CLK_GLOBAL_MEM_FENCE); } res = 0; #pragma unroll for (int i = 0; i < NUM_READS; i++) { res ^= lmem[li - i]; }
Nested Loops
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーループ反復の順序付けのためにパイプライン実行を推測しません。その結果、内部ループの反復回数は、異なるループ反復で異なる可能性があるため、外部ループ反復は、その後の内部ループに対して順序が乱れる可能性があります。
アウトオブオーダーのアウターループ反復の問題を解決するには、アウターループ反復間で変化しない上限と下限を持つ内側ループをデザインします。
Single Work-Itemの実行
FPGA上でのハイスループットのWork-Itemベースのカーネル実行を確実にするために、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーはある時点で複数のパイプライン・ステージを並列に処理する必要があります。この並列性は、ループの反復をパイプライン化することによって実現されます。
Single Work-Itemでの累積を示す次の簡単なコード例を検討してください。
1 kernel void accum_swg (global int* a, global int* c, int size, int k_size) { 2 int sum[1024]; 3 for (int k = 0; k < k_size; ++k) { 4 for (int i = 0; i < size; ++i) { 5 int j = k * size + i; 6 sum[k] += a[j]; 7 } 8 } 9 for (int k = 0; k < k_size; ++k) { 10 c[k] = sum[k]; 11 } 12 }


次の図は、iの各反復がどのようにブロックに入るかを示しています。
外部ループを観測すると、IIの値が1であるということは、スレッドの各反復がすべてのクロックサイクルに入ることもできることを意味します。この例では、 k_sizeが20でsizeが4であると見なされます。これは、最初の8クロック・サイクルでは真であり、外側のループ反復0〜7は、それをストールさせることなく下流に入ることができるからです。スレッド0が内部ループに入ると、それは4回の反復で終了します。スレッド1〜8は内部ループに入ることができず、スレッド0によって4サイクル停止します。スレッド0の反復が完了すると、スレッド1は内部ループに入ります。その結果、スレッド9は、クロックサイクル13で外側ループに入る。スレッド9から20は、 sizeの値である4クロックサイクルごとにループに入る。この例では、外部ループの動的開始間隔が静的に予測される開始間隔1よりも大きく、内部ループのトリップ数の関数であることがわかります。

非線形実行
ループ構造は線形実行をサポートしていません。次のコード例は、外部ループiに2つの分岐する内部ループが含まれていることを示しています。外側ループの各反復は、1つの内側ループまたは非直線的実行パターンである他方を実行することができます。
__kernel void structure (__global unsigned* restrict output1, __global unsigned* restrict output2, int N) { for (unsigned i = 0; i < N; i++) { if ((i & 3) == 0) { for (unsigned j = 0; j < N; j++) { output1[i+j] = i * j; } } else { for (unsigned j = 0; j < N; j++) { output2[i+j] = i * j; } } } }
アウトオブオーダーのループ反復
内側ループの反復回数は、外側ループの反復ごとに異なります。次のコード例を検討してください。
__kernel void order( __global unsigned* restrict input, __global unsigned* restrict output int N ) { unsigned sum = 0; for (unsigned i = 0; i < N; i++) { for (unsigned j = 0; j < i; j++) { sum += input[i+j]; } } output[0] = sum; }
この例は、 i = 0の場合、内側のループjがゼロ回反復することを示しています。 i = 1の場合、 jは 1回反復します。内部ループの反復回数が変わるため、オフライン・コンパイラーはパイプライン処理を推測できません。
シリアルリージョン
内部ループアクセスが外部ループ依存関係を引き起こすと、ネストされたループ内で直列エリアが発生することがあります。内部ループは、データまたはメモリーの依存性のために、外部ループの反復で直列エリアになります。
定常状態では、外側ループのII =内側ループのII *内側ループのトリップ数となります。 IIが1より大きい内部ループと、直列実行エリアがない外部ループの場合、スレッドを外部ループからインターリーブすることは可能です。
次の式を検討してみましょう。
kernel void serially_execute (global int * restrict A, global int * restrict B, global int * restrict result, unsigned N) { int sum = 0; for (unsigned i = 0; i < N; i++) { int res; for (int j = 0; j < N; j++) { sum += A[i*N+j]; } sum += B[i]; } *result = sum; }


Single Work-Itemカーネルのループ
新しいループ反復の開始頻度は開始間隔(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]; }
この図は、オフライン・コンパイラーが並列実行とループパイプライニングを使用して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の実際の、より複雑なハードウェア実装の簡略化した表現です。
加算演算とXORゲートによるフィードバックは、オフライン・コンパイラーが目標周波数を達成する能力を制限するクリティカルパスです。結果として得られるHTMLレポートは、クリティカルパスを構成するコントリビュータの内訳をパーセンテージで表したものです。

ループの起動間隔に影響を与えるループキャリー依存関係
ループがパイプライン化されているにもかかわらず、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の論理ビューを示しています 。

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


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

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

チャネル
カーネルコードでチャネルを宣言するときは、宣言の前にキーワードchannelを付けてください 。
例:channel long16 myCh __attribute__((depth(16)));
HTMLレポートでは、エリアレポートによってチャネルエリアがソースコードの宣言行にマップされます。チャネルおよびチャネルアレイは、その幅および深さとともに報告されます。
実装されたチャネル深度は、チャネル宣言で指定した深度と異なる場合があります。 OpenCLオフライン・コンパイラー用インテルFPGA SDKは、シフトレジスターまたはRAMブロックにチャネルを実装できます。オフライン・コンパイラーは、チャネル深度に基づいてチャネル実装のタイプを決定します。
ロード・ストア・ユニット
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーさまざまな種類のロードストアユニット(LSU)を生成します。 LSUの種類によっては、コンパイラーがメモリー・アクセス・パターンやその他のメモリー属性に応じてLSUの動作やプロパティを変更することがあります。
ロードストアのユニットタイプまたは修飾子を明示的に選択することはできませんが、コード内のメモリー・アクセス・パターン、使用可能なメモリーのタイプ、およびメモリーアクセスがローカルメモリーかグローバルメモリーかを変更することによって、コンパイラーがインスタンス化するLSUのタイプに影響を与えることができます 。
ロード・ストア・ユニットのタイプ
バースト合体のロード・ストア・ユニット
バースト合体LSUは、コンパイラーによってインスタンス化されるデフォルトのLSUタイプです。可能な限り大きなバーストが生成されるまで要求をバッファーします。バースト合体LSUは大域メモリーへの効率的なアクセスを提供できますが、相当量のFPGAリソースが必要です。
kernel void burst_coalesced (global int * restrict in, global int * restrict out) { int i = get_global_id(0); int value = in[i/2]; // Burst-coalesced LSU out[i] = value; }
ロード・ストア・ユニットのプリフェッチ
先読みLSUは、先行するアドレスに基づいてFIFOに有効なデータを完全に保持し、連続した読み出しを仮定するために、バーストがメモリーから大きなブロックを読み出すFIFO(時には名前付きパイプと呼ばれる)をインスタンス化します。不連続リードはサポートされていますが、FIFOをフラッシュして再充填する際に不利益が生じます。
kernel void prefetching (global int * restrict in, global int * restrict out, int N) { int res = 1; for (int i = 0; i < N; i++) { int v = in[i]; // Prefetching LSU res ^= v; } out[0] = res; }
ストリーミングロードストアユニット
ストリーミングLSUは、FIFOが有効なデータでいっぱいになるように、大きなブロックをメモリーからFFIFOをインスタンス化します。このデータブロックは、メモリーアクセスが順序通りであり、アドレスがベースアドレスからの単純なオフセットとして計算できる場合にのみ使用できます。
kernel void streaming (global int * restrict in, global int * restrict out) { int i = get_global_id(0); int idx = out[i]; // Streaming LSU int cached_value = in[idx]; out[i] = cached_value; // Streaming LSU }
セミストリーミングロードストアユニット
セミストリーミングLSUは、読み出し専用キャッシュをインスタンス化します。キャッシュにはエリアのオーバーヘッドがありますが、グローバルメモリー内の同じデータ位置に繰り返しアクセスする場合、パフォーマンスが向上します。カーネル内のストアによってデータが上書きされないようにする必要があります。これは、キャッシュの一貫性を損なうためです。 LSUキャッシュは、関連するカーネルが開始されるたびにフラッシュされます。
#define N 16 kernel void semi_streaming (global int * restrict in, global int * restrict out) { #pragma unroll 1 for (int i = 0; i < N; i++) { int value = in[i]; // Semi-streaming LSU out[i] = value; } }
ローカル・パイプラインロードストアユニット
ローカル・パイプライン化されたLSUは、ローカルメモリーにアクセスするために使用されるパイプライン化されたLSUです。リクエストは受信するとすぐに提出されます。メモリーアクセスはパイプライン化されているので、一度に複数のリクエストを飛行することができます。 LSUとローカルメモリーとの間にアービトレーションがない場合、ローカル・パイプライン化されたノーストールLSUが作成されます。
__attribute((reqd_work_group_size(1024,1,1))) kernel void local_pipelined (global int* restrict in, global int* restrict out) { local int lmem[1024]; int gi = get_global_id(0); int li = get_local_id(0); int res = in[gi]; for (int i = 0; i < 4; i++) { lmem[li - i] = res; // Local-pipelined LSU res >>= 1; } barrier(CLK_GLOBAL_MEM_FENCE); res = 0; for (int i = 0; i < 4; i++) { res ^= lmem[li - i]; // Local-pipelined LSU } out[gi] = res; }
Global Infrequent Load-Store Units
グローバルな頻度の低いLSUは、まれであることが証明できるグローバル・メモリー・アクセスに使用されるパイプライン型のLSUです。グローバルなまれなLSUは、ループに含まれていないメモリー動作に対してのみインスタンス化され、NDRangeカーネル内の単一のスレッドに対してのみアクティブです。
パイプライン化されたLSUは他のLSUタイプよりも小さいため、コンパイラーはパイプライン化されたLSUとしてグローバルなまれなLSUを実装します。パイプライン化されたLSUのスループットは低下する可能性がありますが、メモリーアクセスがまれであるため、このスループットのトレードオフは許容されます。
kernel void global_infrequent (global int * restrict in, global int * restrict out, int N) { int a = 0; if (get_global_id(0) == 0) a = in[0]; // Global Infrequent LSU for (int i = 0; i < N; i++) { out[i] = in[i] + a; } }
コンスタント・パイプライン・ロード・ストア・ユニット
一定のパイプライン化されたLSUは、主に定数キャッシュからの読み出しに使用されるパイプライン化されたLSUです。一定のパイプライン化されたLSUは、バースト合体LSUより少ない面積を消費する。一定パイプライン化されたLSUのスループットは、リードが定数キャッシュ内でヒットしたかどうかによって大きく異なります。キャッシュミスは高価です。
kernel void constant_pipelined (constant int *src, global int *dst) { int i = get_global_id(0); dst[i] = src[i]; // Constant pipelined LSU }
インスタンスIDについて詳しくは、キャッシュ・メモリーを参照してください。
原子パイプライン式ロード・ストア・ユニット
アトミックパイプライン化されたLSUは、すべてのアトミック動作に使用されます。アトミック動作を使用すると、カーネルのパフォーマンスが大幅に低下する可能
kernel void atomic_pipelined (global int* restrict out) { atomic_add(&out[0], 1); // Atomic LSU }
ロードストアユニット修飾子
カーネルのメモリー・アクセス・パターンに応じて、コンパイラーはいくつかのLSUを変更します。
キャッシュ
バースト合体LSUにはキャッシュが含まれることがあります。キャッシュは、メモリー・アクセス・パターンがデータ依存であるか、または繰り返しているように見える場合に作成されます。ロードで同じデータが必要な場合でも、キャッシュを他のロードと共有することはできません。キャッシュはカーネル開始時にフラッシュされ、キャッシュなしで同等のLSUより多くのハードウェア・リソースを消費します。キャッシュは、アクセスパターンを簡素化するか、ポインターを揮発性としてマークすることによって無効にすることができます。
kernel void cached (global int * restrict in, global int * restrict out) { int i = get_global_id(0); int idx = out[i]; int cached_value = in[idx]; // Burst-coalesced cached LSU out[i] = cached_value; }
ライト・アクノリッジ(ライト・アクノリッジ)
バースト集約されたストアLSUは、データの依存関係が存在する場合、書き込み確認信号を必要とすることがあります。ライトアクノリッジ信号を有するLSUは、追加のハードウェア資源を必要とする。複数のライトアクノリッジLSUが同じメモリーにアクセスすると、スループットが低下する可能性があります。
kernel void write_ack (global int * restrict in, global int * restrict out, int N) { for (int i = 0; i < N; i++) { if (i < 2) out[i] = 0; // Burst-coalesced write-ack LSU out[i] = in[i]; } }
非整列
バースト合体LSUが外部メモリー・ワード・サイズにアラインメントされていないメモリーにアクセスできる場合、アラインメントされていないLSUが作成されます。アラインメントされていないLSUを実装するには、追加のハードウェア・リソースが必要です。アラインメントされていない多くの要求を受信すると、アラインメントされていないLSUのスループットが低下する可能性があります。
kernel void non_aligned (global int * restrict in, global int * restrict out) { int i = get_global_id(0); // three loads are statically coalesced into one, creating a Burst-coalesced non-aligned LSU int a1 = in[3*i+0]; int a2 = in[3*i+1]; int a3 = in[3*i+2]; // three stores statically coalesced into one out[3*i+0] = a3; out[3*i+1] = a2; out[3*i+2] = a1; }
Never-stall
ローカル・パイプライン化されたLSUがアービトレーションせずにローカルメモリーに接続されている場合、メモリーへのすべてのアクセスがコンパイラーに知られている一定数のサイクルになるため、ストールしないLSUが作成されます。
次の例では、96ビット幅のメモリーアクセスの一部は2つのメモリーワードにまたがるため、メモリーから2つのフルラインのデータを読み出す必要があります。
__attribute((reqd_work_group_size(1024,1,1))) kernel void never_stall (global int* restrict in, global int* restrict out, int N) { local int lmem[1024]; int gi = get_global_id(0); int li = get_local_id(0); lmem[li] = in[gi]; // Local-pipelined never-stall LSU barrier(CLK_GLOBAL_MEM_FENCE); out[gi] = lmem[li] ^ lmem[li + 1]; }
OpenCLカーネルデザインのベスト・プラクティス
一般に、最初に単一のコンピューティング・ユニットをターゲットとするカーネルを最適化する必要があります。このコンピューティング・ユニットを最適化した後、ハードウェアをスケーリングしてFPGAの残りの部分を満たすようにパフォーマンスを上げてください。カーネルのハードウェア・フットプリントは、ハードウェアのコンパイルに要する時間と相関します。したがって、より小さなフットプリント(つまり、単一の計算単位)で実行できる最適化が増えるほど、一定の時間内に実行できるハードウェアの数が増えます。
データ処理とメモリーアクセスの最適化に加えて、カーネルの作成時には、必要に応じて次のデザイン方法を実装することを検討してください。
- データを経由して転送する インテル FPGA SDK for OpenCL チャネルまたはOpenCLパイプ
カーネル間のデータ転送効率を高めるには、 インテル® FPGA SDK for OpenCL™ カーネルプログラムのチャンネル拡張を実装します。チャネルの機能を活用したいが、他のSDKを使用してカーネルプログラムを実行できるようにするには、OpenCLパイプを実装します。 - ループのアンロール
OpenCLカーネルにループ反復が含まれている場合、ループを展開してパフォーマンスを向上させてください。 - 浮動小数点演算の最適化
浮動小数点演算の場合、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーハードウェアでより効率的なパイプライン構造を作成し、ハードウェア全体の使用を削減する最適化を実行します。 - アラインメントされたメモリーの割り当て
FPGAとの間でデータを転送するために使用されるホスト側のメモリーを割り当てる場合、メモリーは少なくとも64バイトに揃えられている必要があります。 - 構造体をパディング付きまたはパディングなしで整列する
適切に整列された構造体は、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーが最も効率的なハードウェアを生成します。 - ベクトル型要素の類似構造の維持
ベクトル型の1つの要素を更新する場合、ベクトルのすべての要素を更新します。 - ポインター・エイリアシングの回避
可能であれば、ポインター引数にrestrictキーワードを挿入します。 - 高価な機能の回避
一部の機能はFPGAで実装するのに費用がかかります。高価な機能は、カーネルのパフォーマンスを低下させるか、実装するために大量のハードウェアを必要とする可能性があります。 - Work-ItemID依存の後方分岐の回避
パフォーマンスを低下させるため、Work-ItemIDに依存する後方分岐(ループ内で発生する分岐)をカーネルに含めないでください。
データを経由して転送する インテル FPGA SDK for OpenCL チャネルまたはOpenCLパイプ
時には、FPGAからグローバルメモリーへの帯域幅は、カーネル間のデータ転送効率を制限します。理論上の最大FPGA対グローバルメモリー帯域幅は、対象のカスタム・プラットフォームおよびボードで使用可能なグローバルメモリーバンクの数によって異なります。ボードの理論上の最大帯域幅を決定するには、ボードベンダのマニュアルを参照してください。
実際には、カーネルは使用可能な最大グローバルメモリー帯域幅の100%使用を達成していません。使用率は、アルゴリズムのアクセスパターンによって異なります。
グローバルメモリー帯域幅がOpenCLカーネルのパフォーマンス制約条件である場合、まずアルゴリズムを複数の小さなカーネルに分解してみてください。次に、下の図に示すように、SDKカーネル間のデータ転送用のチャネルまたはOpenCLパイプを実装してグローバル・メモリー・アクセスの一部を削除します。
チャネルの使用方法の詳細については インテル® FPGA SDK for OpenCL™ プログラミング・ガイドの インテル® FPGA SDK for OpenCL™ チャネル拡張の実装のセクションを参照してください。
パイプの使用方法の詳細については、 インテル® FPGA SDK for OpenCL™ プログラミング・ガイドのOpenCLパイプの実装のセクションを参照してください。
チャネルとパイプの特性
基本動作
チャネルのデフォルト動作はブロックしています。パイプのデフォルト動作はノンブロッキングです。
複数のOpenCLカーネルの同時実行
複数のOpenCLカーネルを同時に実行することができます。同時実行をイネーブルするには、複数のコマンドキューをインスタンス化するようにホストコードを変更します。同時に実行される各カーネルは、別々のコマンド・キューに関連付けられます。
パイプ固有の考慮事項:
インテル® FPGA SDK for OpenCL™ プログラミング・ガイドの他のOpenCL SDKとの互換性の確保に記載されているOpenCLパイプの変更により、SDKでカーネルを実行することができます。ただし、カーネルのスループットを最大化するわけではありません。 OpenCL仕様バージョン2.0では、カーネルが空のパイプからの読み出しを行わないように、パイプ読み出しの前にパイプ書き込みを行う必要があります。その結果、カーネルは同時に実行できません。 インテル® FPGA SDK for OpenCL™ 同時実行をサポートするため、ホスト・アプリケーションとカーネルプログラムを変更してこの機能を使用することができます。この変更により、アプリケーションのスループットが向上します。ただし、カーネルを別のSDKに移植することはできません。この制限にもかかわらず、変更は最小限であり、両方のタイプのコードを維持するために多大な努力を必要としません。
パイプを含むカーネルの同時実行をイネーブルするには、カーネルコードのdepthの属性をblocking属性(つまり、 __attribute __((blocking))に置き換えます。 blockingの属性はread_pipeとwrite_pipe関数呼び出しにブロッキング動作を紹介します。コールサイトは、パイプの他端が準備完了になるまで、カーネルの実行をブロックします。
blockingの属性とdepthの属性の両方をカーネルに追加すると、パイプが空のときだけread_pipe呼び出しがブロックされ、パイプがいっぱいになったときにwrite_pipe呼び出しがブロックされます。ブロック動作により、カーネル間の暗黙的な同期が行われ、カーネル同士が互いにロックステップで実行されます。
暗黙のカーネル同期
チャネルをブロックするか、パイプの呼び出しをブロックすることで、カーネルを暗黙的に同期させます。次の例を検討してください。
ブロッキング・チャネルコールを持つカーネル | ブロッキング・パイプ・コールを持つカーネル |
---|---|
channel int c0; __kernel void producer (__global int * in_buf) { for (int i = 0; i < 10; i++) { write_channel_intel (c0, in_buf[i]); } } __kernel void consumer (__global int * ret_buf) { for (int i = 0; i < 10; i++) { ret_buf[i] = read_channel_intel(c0); } } |
__kernel void producer (__global int * in_buf, write_only pipe int __attribute__ ((blocking)) c0) { for (int i = 0; i < 10; i++) { write_pipe (c0, &in_buf[i]); } } __kernel void consumer (__global int * ret_buf, read_only pipe int __attribute__ ((blocking)) c0) { for (int i = 0; i < 10; i++) { int x; read_pipe (c0, &x); ret_buf[i] = x; } } |
producerカーネルがデータを書き、 consumerカーネルが各ループ反復中にデータを読み込むように、カーネルを同期させることができます。 producerでwrite_channel_intelまたはwrite_pipe呼び出しがread_channel_intelまたはread_pipeコールで任意のデータを書き込まない場合、producerが有効なデータを送信するまで(またはその逆)、consumerはread_channel_intelまたはread_pipeコールをブロックと待機します。
呼び出し間のデータの永続性
write_channel_intelコールがデータをチャネルに書き込んだり、 write_pipe呼び出しがパイプにデータを書き込んだ後も、データはワークグループおよびNDRange呼び出し間で永続的です。Work-Itemがチャネルまたはパイプに書き込むデータは、別のWork-Itemがそこから読み出されるまでそのチャネルまたはパイプに残ります。さらに、チャネルまたはパイプ内のデータの順序は、そのチャネルまたはパイプへの書き込み動作の順序と等価であり、順序は書き込み動作を実行するWork-Itemとは独立しています。
たとえば、複数のWork-Itemがチャネルまたはパイプに同時にアクセスしようとすると、Single Work-ItemだけがそのWork-Itemにアクセスできます。 write_channel_intelコールまたはwrite_pipeコールは、 DATAXという特定のWork-Itemデータをそれぞれチャネルまたはパイプに書き込みます。同様に、チャンネルまたはパイプにアクセスするための最初のWork-Itemは、そこからDATAXを読み出します。読み書き動作のこの順番は、チャネルとパイプをカーネル間でデータを共有するための有効な方法にします。
課された作業アイテムの注文
SDKは、チャネルまたはパイプの読み書き動作の一貫性を維持するためのWork-Itemの順序を強制します。
チャネルおよびパイプの実行順序
次のコードの例を検討してみましょう。
2つのリードチャネルコールを持つカーネル | 2つのパイプ呼び出しを読み込んだカーネル |
---|---|
__kernel void consumer (__global uint*restrict dst) { for (int i = 0; i < 5; i++) { dst[2*i] = read_channel_intel(c0); dst[2*i+2] = read_channel_intel(c1); } } |
__kernel void consumer (__global uint*restrict dst, read_only pipe uint __attribute__((blocking)) c0, read_only pipe uint __attribute__((blocking)) c1) { for (int i = 0; i < 5; i++) { read_pipe (c0, &dst[2*i]); read_pipe (c1, &dst[2*i+2]); } } |
左側のコード例は、2つの読み出しチャネル呼び出しを行います。右側のコード例は、2つの読み出しパイプ呼び出しを行います。ほとんどの場合、カーネルはこれらのチャネル呼び出しまたはパイプ呼び出しを並列に実行します。ただし、チャネルおよびパイプ・コールの実行が順不同で行われる可能性があります。アウト・オブ・シーケンス実行は、C1からの読み出し動作が発生し、C0からの読み出し動作の前に完了することができることを意味します。
チャネルまたはパイプのバッファー推論の最適化
コンパイル時に、オフライン・コンパイラーは、相互作用するチャネルまたはパイプ間のスケジューリングの不一致を計算します。これらの不一致は、読み出しと書き込みの動作の不均衡を引き起こす可能性があります。オフライン・コンパイラーは、不均衡を修正するためにバッファー推論最適化を自動的に実行します。
次の例を検討してみましょう。
チャンネル付きカーネル | パイプ付きカーネル |
---|---|
__kernel void producer ( __global const uint * restrict src, const uint iterations) { for(int i = 0; i < iteration; i++) { write_channel_intel(c0,src[2*i]); write_channel_intel(c1,src[2*i+1]); } } __kernel void consumer ( __global uint * restrict dst, const uint iterations) { for(int i = 0; i < iterations; i++) { dst[2*i] = read_channel_intel(c0); dst[2*i+1] = read_channel_intel(c1); } } |
__kernel void producer ( __global const uint * restrict src, const uint iterations, write_only pipe uint __attribute__((blocking)) c0, write_only pipe uint __attribute__((blocking)) c1) { for(int i = 0; i < iteration; i++) { write_pipe(c0,&src[2*i]); write_pipe(c1,&src[2*i+1]); } } __kernel void consumer ( __global uint * restrict dst, const uint iterations, read_only pipe uint __attribute__((blocking)) c0, read_only pipe uint __attribute__((blocking)) c1) { for(int i = 0; i < iterations; i++) { read_pipe(c0,&dst[2*i]); read_pipe(c1,&dst[2*i+1]); } } |
オフライン・コンパイラーは、カーネル間のチャネルまたはパイプがサイクルを形成できない場合、バッファー推論の最適化を実行します。カーネル間のcyclは、カーネルから、書き込みチャネルまたは書き込みパイプ呼び出しを経由して元のカーネルに戻るパスです。この例では、カーネルproducerのライトチャネルまたはライトパイプコールが10サイクル離れてスケジュールされ、リードチャネルまたはリードパイプコールが15サイクル離れてスケジュールされているとします。C1への読み出し動作が発生する前に、5つの余分な書き込み動作が発生するかもしれないのであり、読み出しが一時的に不一致が存在するとC1に書き込み動作を。この不均衡を修正するために、オフライン・コンパイラーは、ストールを避けるためにc1に5サイクルのバッファーサイズを割り当てます。追加のバッファー容量は、 producerカーネルのc1書き込み動作とconsumerカーネルのc1読み出し動作を切り離します。
チャネルとパイプのベスト・プラクティス
- マルチ・スレッド・カーネルよりもシングル・スレッド・カーネルを使用してください。
- デザインモデルがフィード・フォワード・データパス、例えばバックツーバックループまたは離散処理ステップでどのように表現できるかを検討します。デザインをチャネルで接続された複数のカーネルに分割する必要があるかどうかを判断します。
- カーネルの同じポイントでデータ全体が使用されている場合のみ、チャネル上のデータを集約します。
- カーネルあたりのチャネル数を妥当なものに保つようにしてください。
- データを待っているループ構造を使用している場合、非ブロッキング・チャネルまたはパイプを使用しないでください。非ブロッキング・チャネルは、ブロッキング・チャネルより多くのリソースを消費します。
ループのアンロール
各Work-Itemが配列内の4つの要素の累積を計算する責任がある並列アプリケーション用のOpenCLコードを検討してください。
__kernel void example ( __global const int * restrict x, __global int * restrict sum ) { int accum = 0; for (size_t i = 0; i < 4; i++) { accum += x[i + get_global_id(0) * 4]; } sum[get_global_id(0)] = accum; }
このカーネルでは、以下の3つの主要な動作が行われています。
- 入力xからのロード動作
- 累積
- オペレーションを出力sumに格納する
オフライン・コンパイラーは、OpenCLカーネルコードのデータフローセマンティクスに従って、これらの動作をパイプラインに配置します。たとえば、オフライン・コンパイラーは、ループ終了条件に応じて、パイプラインの最後からパイプラインの先頭に結果を転送することによってループを実装します。
OpenCLカーネルは、クロックサイクルごとに各Work-Itemの1回のループ反復を実行します。十分なハードウェア・リソースがあれば、ループを展開することでカーネルのパフォーマンスを向上させることができ、カーネルが実行する反復回数が減ります。ループをアンロールするには、次のコード例に示すように、 #pragma unrollディレクティブをメインループに追加します。ループをアンロールすると、オフライン・コンパイラーが作成するコンピューティング・ユニットの構造が大幅に変更されることに注意してください。
__kernel void example ( __global const int * restrict x, __global int * restrict sum ) { int accum = 0; #pragma unroll for (size_t i = 0; i < 4; i++) { accum += x[i + get_global_id(0) * 4]; } sum[get_global_id(0)] = accum; }
この例では、 #pragma unrollディレクティブにより、オフライン・コンパイラーはループの4つの繰り返しを完全に展開します。アンロールを達成するために、オフライン・コンパイラーは、加算演算の数を3倍にし、4倍のデータをロードすることによってパイプラインを拡張します。ループが除去されると、コンピューティング・ユニットはフィードフォワード構造を前提します。結果として、コンピューティング・ユニットは、初期ロード動作および加算の完了後にクロックサイクルごとにsum要素を記憶することができる。オフライン・コンパイラーは、4つのロード動作を統合することによってこのカーネルをさらに最適化し、コンピューティング・ユニットが1つのロード動作で結果を計算するために必要なすべての入力データをロードできるようにします。
ループをアンロールし、グローバルメモリーからロード動作を統合することにより、ハードウェアによるカーネルのインプリメンテーションでクロックサイクルごとに多くの動作を実行できます。一般に、OpenCLカーネルのパフォーマンスを向上させるために使用する方法は、次の結果を達成する必要があります。
- 並列動作の数を増やす
- 実装のメモリー帯域幅を増やす
- カーネルがハードウェアで実行できるクロックサイクルあたりの動作数を増やす
オフライン・コンパイラーは、以下の状況で完全にループを展開することができない場合があります。
- 非常に多数の反復を伴うデータ依存ループの完全なアンローリングを指定します。したがって、カーネルのハードウェア実装がFPGAに適合しない可能性があります。
- 完全な展開を指定し、ループの境界は定数ではありません。
- ループは、複雑な制御フローで構成されます(たとえば、コンパイル時に不明な複雑な配列インデックスまたは終了条件を含むループ)。
上記の最後の2つのケースでは、オフライン・コンパイラーは次の警告を発行します。
ループのフルアンロールが要求されますが、ループの境界を特定できません。ループはアンロールされません。
これらの状況でループのアンロールをイネーブルするには、 #pragma unroll <N> ディレクティブ(<N>はアンロール係数である)を指定します。アンロール係数は、オフライン・コンパイラーがアンロールする回数を制限します。たとえば、カーネル内のループが展開されないようにするには、そのループに#pragma unroll 1ディレクティブを追加します。
適切に構成されたループを構築するためのヒントについては、Good Design Practices for Single Work-Item Kernelを参照してください。
浮動小数点演算の最適化
ツリーのバランス
動作規則の順序はOpenCL™言語で適用されます。次の例では、オフライン・コンパイラーは最も内側の括弧内の演算から始まる厳密な順序で乗算と加算を実行します。
result = (((A * B) + C) + (D * E)) + (F * G);
デフォルトでは、オフライン・コンパイラーは、そのような計算のためにロングブレインに似た実装を作成します。
長くて不均衡な動作は、より高価なハードウェアにつながります。より効率的なハードウェア実装は、以下に示すようにバランスの取れたツリーです 。
バランスの取れたツリーの実装では、オフライン・コンパイラーは、長いブレインの浮動小数点加算器をツリーパイプライン構造に変換します。オフライン・コンパイラーは、浮動小数点演算の結果が異なるため、浮動小数点演算のツリー・バランシングを自動的に実行しません。その結果、この最適化はIEEE標準754-2008と矛盾します。
オフライン・コンパイラーで平衡ツリーを使用して浮動小数点演算を最適化し、プログラムが浮動小数点結果の小さな違いを許容できるようにするには、次のように-fp-relaxedオプションをaocコマンドに含めます。
aoc -fp-relaxed <your_kernel_filename>.cl
丸め動作
浮動小数点演算の平衡化ツリーの実装には、複数回の丸め演算が含まれます。これらの丸め処理では、一部のアプリケーションでは相当量のハードウェア・リソースが必要になる場合があります。オフライン・コンパイラーは、IEEE Standard 754-2008で要求される結果に違反するため、丸め処理の回数を自動的に減らすことはありません。
aocコマンドの-fpcオプションを使用して浮動小数点演算を実装するために必要なハードウェアの量を減らすことができます。プログラムで浮動小数点結果の小さな違いを許容できる場合、次のコマンドを呼び出します。
aoc -fpc <your_kernel_filename>.cl
-fpcオプションを指定すると、オフライン・コンパイラーは次のタスクを実行します。
- 可能であれば、浮動小数点丸め演算と変換を削除してください。
可能であれば、 -fpc引数は、浮動小数点演算のツリーの最後で、浮動小数点演算を1回だけ丸めるようにオフライン・コンパイラーに指示します。
- 精度を維持するために追加の仮数ビットを持ちます。
オフライン・コンパイラーは、浮動小数点演算を介して追加の精度ビットを持ち、浮動小数点演算のツリーの最後でこれらの精度ビットを削除します。
このタイプの最適化は、融合された浮動小数点演算を実行するハードウェアをもたらし、多くの新しいハードウェア処理システムの機能です。複数の浮動小数点演算を融合すると、丸めステップの数が最小限に抑えられ、より正確な結果が得られます。この最適化の一例は、新しいプロセッサーアーキテクチャで使用可能なFMAC(fused multiply-accumulate)命令です。オフライン・コンパイラーは、カーネル内の浮動小数点演算子の多くの組み合わせに対して、融合した浮動小数点数学機能を提供できます。
浮動小数点表現と固定小数点表現
OpenCL®標準は固定小数点表現をサポートしていません。整数データ型を使用して固定小数点表現を実装する必要があります。ハードウェア開発者は、通常、固定小数点データ表現を使用してハードウェアを節約し、計算を実行するために必要なデータ解決のみを保持します。OpenCL標準ではこれらのデータ解像度のみがサポートされているため、8,16,32、または64ビットのスカラデータ型を使用する必要があります。ただし、ハードウェアのコンパイルツールでハードウェア・リソースを節約するための最適化を実行できるように、ソースコードに適切なマスキング動作を組み込むことができます。
たとえば、アルゴリズムで17ビットのデータの固定小数点表現を使用する場合、値を格納するために32ビットのデータ型を使用する必要があります。その後、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラー 2つの17ビット固定小数点値を一緒に追加するには、オフライン・コンパイラーが余分な上位15ビットの加算を処理するために余分なハードウェアを作成する必要があります。この追加のハードウェアが存在しないようにするには、スタティックビットマスクを使用して、ハードウェアのコンパイル中に不要なビットを無視するようにハードウェアのコンパイルツールに指示します。以下のコードはこのマスキング動作を実装しています。
__kernel fixed_point_add (__global const unsigned int * restrict a, __global const unsigned int * restrict b, __global unsigned int * restrict result) { size_t gid = get_global_id(0); unsigned int temp; temp = 0x3_FFFF & ((0x1_FFFF & a[gid]) + ((0x1_FFFF & b[gid])); result[gid] = temp & 0x3_FFFF; }
このコード例では、入力aとbの上位15ビットがマスクされて一緒に加算されます。 2つの17ビット値を加算した結果は18ビットの分解能を超えることができないため、オフライン・コンパイラーは追加のマスクを適用して結果の上位14ビットをマスクします。最終的なハードウェアの実装は、完全な32ビットの追加ではなく、17ビットの追加です。この例のロジックの節約は、FPGAで使用可能なハードウェア・リソースの数に比べて比較的少ないです。しかし、これらの小さな節約は、頻繁に適用されると、FPGA全体でより大きなハードウェア節約に蓄積されます。
アラインメントされたメモリーの割り当て
ホスト側のメモリーを整列させることで、FPGAとのダイレクトメモリーアクセス(DMA)転送が可能になり、バッファー転送効率が向上します。
アラインメントされたメモリー割り当てを設定するには、ホストプログラムに次のソースコードを追加します。
- Windowsの場合
#define AOCL_ALIGNMENT 64 #include <malloc.h> void *ptr = _aligned_malloc (size, AOCL_ALIGNMENT);
整列メモリーブロックを解放するには、関数呼び出し_aligned_free(ptr);を含めます。
- Linuxの場合
#define AOCL_ALIGNMENT 64 #include <stdlib.h> void *ptr = NULL; posix_memalign (&ptr, AOCL_ALIGNMENT, size);
整列メモリーブロックを解放するには、関数呼び出しfree(ptr);を含めます。
構造体をパディング付きまたはパディングなしで整列する
typedef struct { char r,g,b,alpha; } Pixel_s; typedef union { Pixel_s p; int not_used; } Pixel;また、次のコード例に示すように、 aligned属性を使用して4バイトのアラインメントを強制することもできます。
typedef struct {char r、g、b、alpha; } __attribute __((aligned(4)))ピクセル;
オフライン・コンパイラーは、次のすべての基準を満たすために構造体のアラインメントを必要とするISO C標準に準拠しています。
- アラインメントは、すべての構造体メンバーのアラインメント間の最小公倍数の整数倍でなければなりません。
- このマクロの値は、2の累乗である必要があります。
カーネルコードにaligned( N )属性を含めることで、 structの整列を設定することができます。整列属性なしで、オフライン・コンパイラーは、 structのサイズに基づいて、 structのアレイ内の各structのアラインメントを決定します。次の例を検討してください。
__kernel void test (struct mystruct* A, struct mystruct* B) { A[get_global_id(0)] = B[get_global_id(0)]; }
mystructのサイズが101バイトの場合、各ロードまたはストアのアクセスは1バイトで整列されます。 mystructのサイズが128バイトの場合、各ロードまたはストアのアクセスは128バイトに整列され、最も効率的なハードウェアが生成されます。
構造体のフィールドは、structの中に整列されていない場合、オフラインのコンパイラーはそれらを整列するためにパディングを挿入します。 structフィールド間にパディングを挿入すると、次のようにハードウェアの効率に影響します。
- 構造体のサイズを増やす
- アラインメントに影響する可能性がある
オフライン・コンパイラーがパディングを挿入しないようにするには、カーネルコードにpacked属性を含めます。前述のISO C標準は、パックされたstructまたはアンパックされたstructのアラインメントを決定するときに適用されます。次の例を検討してください。
struct mystruct1 { char a; int b; };
mystruct1のサイズは8バイトです。したがって、 structは8バイトに整列され、カーネル内で効率的にアクセスされます。ここで別の例を検討してください。
struct mystruct2 { char a; int b; int c; };
mystruct2のサイズは12バイトで、 structは4バイトに整列しています。structフィールドはパディングされており、structはアラインメントされていないため、カーネル内のアクセスは非効率的です。
以下にpacked属性を含むstructの例を示します。
struct __attribute__((packed)) mystruct3 { char a; int b; int c; };
mystruct4のサイズは16バイトです。 mystruct4が整列され、 structフィールドの間にパディングがないため、このカーネルのアクセスはmystruct3のアクセスよりも効率的です 。
構造体にaligned( N )属性とpacked属性の両方を含めるには、次の例を考えてください。
struct __attribute__((packed)) __attribute__((aligned(16))) mystruct5 { char a; int b; int c; };
mystruct5のサイズは9バイトです。 aligned(16)属性のため、 structは配列内の16バイト整列アドレスに格納されます。 mystruct5は16バイトで整列されており、パディングもないため、このカーネルでのアクセスは効率的です。
structの整列とaligned( N )およびpacked属性の詳細については、以下の文書を参照してください。
- OpenCL仕様バージョン1.2.1のセクション6.11.1
- インテル® FPGA SDK for OpenCL™ プログラミング・ガイドのデータ構造の挿入のディセーブル
- Structアラインメントの指定のセクションの インテル® FPGA SDK for OpenCL™ プログラミング・ガイド
ベクトル型要素の類似構造の維持
次のコード例は、ベクトル要素を更新するシナリオを示しています。
__kernel void update (__global const float4 * restrict in, __global const float4 * restrict out) { size_t gid = get_global_id(0); out[gid].x = process(in[gid].x); out[gid].y = process(in[gid].y); out[gid].z = process(in[gid].z); out[gid].w = 0; //Update w even if that variable is not required. }
ポインター・エイリアシングの回避
restrictキーワードは、オフライン・コンパイラーに、ポインターが別のポインターのエイリアスではないことを通知します。たとえば、カーネルに大域メモリーAとBが重複しない2つのポインターがある場合、カーネルを次のように宣言します。
__kernel void myKernel (__global int * restrict A, __global int * restrict B)
高価な機能の回避
以下の機能は高価です。
- 整数除算とモジュロ(剰余)演算子
- 加算、乗算、絶対値、および比較を除くほとんどの浮動小数点演算子 注: 浮動小数点演算の最適化の詳細については、「浮動小数点演算の最適化のセクションを参照してください。
- アトミック関数
対照的に、安価な機能はカーネルの性能にはほとんど影響を与えず、その実装は最小のハードウェアしか消費しません。
以下の機能は安価です。
- AND、NAND、OR、NOR、XOR、およびXNORなどの2進論理演算
- 1つの定数引数による論理演算
- 定数でシフト
- 整数の乗算と2の累乗である定数による除算
高価な関数がワークグループ内のすべてのWork-Itemに対して新しいデータを生成する場合、それをカーネルでコード化することは有益です。これに対して、以下のコード例は、NDRangeの各Work-Itemで実行される高価な浮動小数点演算(除算)のケースを示しています。
__kernel void myKernel (__global const float * restrict a, __global float * restrict b, const float c, const float d) { size_t gid = get_global_id(0); //inefficient since each work-item must calculate c divided by d b[gid] = a[gid] * (c / d); }
この計算の結果は常に同じです。このような冗長かつハードウェアのリソース集約的な動作を回避するには、ホスト・アプリケーションで計算を実行し、使用するNDRange内のすべてのWork-Itemの引数として結果をカーネルに渡します。変更されたコードを以下に示します。
__kernel void myKernel (__global const float * restrict a, __global float * restrict b, const float c_divided_by_d) { size_t gid = get_global_id(0); /*host calculates c divided by d once and passes it into kernel to avoid redundant expensive calculations*/ b[gid] = a[gid] * c_divided_by_d; }
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは、NDRange全体でWork-Itemに依存しない動作を1つの動作に統合します。次に、すべてのWork-Itemにわたって結果を共有します。最初のコード例では、オフライン・コンパイラーは、 cによるdの除算がすべてのWork-Itemにわたって一定であるため、すべてのWork-Itemで共有される1つの除算ブロックを作成します。この最適化は冗長ハードウェアの量を最小限に抑えるのに役立ちますしかし、整数除算の実装には、相当量のハードウェア・リソースが必要です。したがって、除算演算をホスト・プロセッサーにオフロードし、結果を引数としてカーネルに渡してハードウェア・リソースを節約することが有益です。
Work-ItemID依存の後方分岐の回避
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは、特定の機能ユニットがいつアクティブになるかを示す単一のビットに条件文を畳ませます。オフライン・コンパイラーは、ループ構造に関係しない単純な制御フローを完全に排除し、フラットな制御構造とより効率的なハードウェアの使用をもたらします。オフライン・コンパイラーは、条件文などの前方分岐を効率的に含むカーネルを効率的にコンパイルします。
例えば、次のコードは、そのようなget_global_id又はget_local_idなどのWork-ItemIDを含む分岐示します。
for (size_t i = 0; i < get_global_id(0); i++) { // statements }
パフォーマンスのボトルネックを特定するためのカーネルのプロファイリング
次のOpenCLカーネルプログラムを検討してください。
__kernel void add (__global int * a, __global int * b, __global int * c) { int gid = get_global_id(0); c[gid] = a[gid]+b[gid]; }
下の図に示すように、Profiler計測器は、カーネルプログラム用に生成されたパイプライン全体で、デイジーチェインでパフォーマンス・カウンターを接続します。ホストは、これらのカウンターによって収集されたデータを読み出します。たとえば、 PCI Express® ( PCIe® )ベースのシステムでは、ホストはPCIe制御レジスターアクセス(CRA)または制御およびステータスレジスター(CSR)ポートを介してデータを読み出します。
Work-Itemの実行停止は、 インテル® FPGA SDK for OpenCL™ パイプラインのさまざまな段階で発生する可能性があります。大量のメモリーアクセスまたはロードおよびストア動作を伴うアプリケーションは、メモリー転送の完了を可能にするために頻繁に停止することがあります。Profilerは、カーネル・パイプライン内の大部分の停止を引き起こすロードおよびストア動作またはチャネルアクセスを識別するのに役立ちます。
Intel FPGA Dynamic Profiler for OpenCLの使用方法については、 インテル® FPGA SDK for OpenCL™ プログラミング・ガイドの「OpenCLカーネルのプロファイリングのセクションを参照してください。
Intel FPGA Dynamic Profiler for OpenCLのベスト・プラクティス
- パフォーマンス・カウンターをカーネルに挿入するには、開発中にaocコマンドに-profile Intel® FPGA SDK for OpenCL™オフライン・コンパイラーコマンドオプションを含めます。
- Profilerを使用せずにカーネルのfmaxとパフォーマンスを定期的にチェックしてください。
- プロファイラーのオーバーヘッドを減らすには、ローカルフォルダーからホスト・アプリケーションを実行します。リモートまたはNASフォルダからホストを実行しないでください。
- カーネルの実行時間が20 msより長いことを確認してください。そうしないと、Profilerのオーバーヘッドが引き継がれます。
- すべてのロードおよびストア動作とチャネルがデータフローでどのように接続されているかを理解します。
Intel FPGA Dynamic Profiler for OpenCL GUI
見出し | 変更内容 |
---|---|
ボード | カーネルのエミュレーションと実行中に Intel® FPGA SDK for OpenCL™オフライン・コンパイラーが使用するアクセラレータボードの名前。 |
Global Memory BW (DDR) | 各メモリータイプ(DDRなど)で使用可能な理論上のグローバルメモリー帯域幅の最大値。 |
要約見出ししのすぐ下に、使用可能なタブをクリックすると詳しいプロファイル情報を表示できます。
- ソース・コード・タブ
Intel FPGA Dynamic Profiler for OpenCL GUIでのSource Codeドタブには、ソースコード情報とメモリーおよびチャネルアクセスに関する詳細な統計情報が含まれています。 - カーネル実行タブ
Kernel ExecutionタブIntel FPGA Dynamic Profiler for OpenCL GUIは、カーネルプログラム全体の実行プロセスをグラフィカルに表示します。 - Autorun Capturesタブ
自動実行の統計データを表示するには、 Intel FPGA Dynamic Profiler for OpenCLは、エンキューされたカーネルのデータを表示する方法と同様です。自動実行およびエンキューされたカーネルの統計データは、いずれも、単一のprofile.monファイルに格納されます。
ソース・コード・タブ

ソSource Codeタブには、カーネルコードの特定の行に関する詳細情報が表示されます。
カラム | 変更内容 | アクセスタイプ |
---|---|---|
属性 | メモリータイプ(ローカルまたはグローバル)、対応するメモリーシステム(DDRまたはクワッドデータレート(QDR))、読み出しまたは書き込みアクセスなどのメモリーまたはチャネル属性情報。 | すべてのメモリーアクセスとチャネルアクセス |
ストールの割合 | メモリーまたはチャネル・アクセスがパイプライン・ストールの原因となっている時間の割合です。これは、メモリーまたはチャネルアクセスがアクセス要求を満たす能力の尺度です。 | すべてのメモリーアクセスとチャネルアクセス |
占有の割合 | 有効なWork-Itemがメモリーまたはチャネル命令を実行するときのプロファイリングされた時間フレーム全体の割合。 | すべてのメモリーアクセスとチャネルアクセス |
帯域幅 | メモリーアクセスが使用する平均メモリー帯域幅とその全体的な効率。 グローバル・メモリー・アクセスごとに、グローバル・メモリー・システムからデータを取得するためにFPGAリソースが割り当てられます。しかし、カーネルプログラムが使用するデータ量は、取得したデータ量より少なくなる可能性があります。全体的な効率は、カーネルプログラムが使用する、グローバル・メモリー・システムから取得した総バイト数の割合です。 |
グローバル・メモリー・アクセス |
1行のソースコードが複数のメモリー動作またはチャネル動作を指示する場合、プロファイル統計情報はドロップダウンリストボックスに表示され、関連情報を表示するように選択できます。

ツール・オプション

カラム | ツールヒント | 変更内容 | メッセージの例 | アクセスタイプ |
---|---|---|---|---|
属性 | キャッシュヒット | キャッシュを使用するメモリーアクセスの数。 キャッシュヒット率が高いと、メモリー帯域幅の使用率が低下します。 |
キャッシュヒット=30% | グローバルメモリー |
アラインメントされていないアクセス | アラインメントされていないメモリーアクセスの割合。 アラインされていないアクセス率が高いということは、非効率的なメモリーアクセスを意味します。効率を向上させるために、カーネルコードのアクセスパターンを変更することを検討してください。 |
アラインメントされていないアクセス=20% | グローバルメモリー | |
静的に融合 | ロードまたはストア・メモリー動作が静的に結合されているかどうかを示す。 一般に、スタティック・メモリー統合は、連続するメモリーアドレスにアクセスする複数のメモリーアクセスを単一のワイドアクセスにマージします。 |
融合 | グローバルまたはローカルメモリー | |
占有の割合 | アクティビティー | 述語チャネルまたはメモリー命令が有効になっている時間の割合(条件付き実行がtrueの場合)。 注: アクティビティーの割合は、命令の占有率よりも小さい可能性があります。
|
アクティビティー=20% | グローバルまたはローカルメモリー、およびチャネル |
帯域幅 | バースト・サイズ | メモリー動作の平均バーストサイズ。 メモリーシステムがバーストモード(例えば内蔵RAM)をサポートしていない場合、バースト情報は使用できません。 |
平均バーストサイズ= 7.6 (最大バースト= 16) |
グローバルメモリー |
カーネル実行タブ
たとえば、低速のネットワーク・ディスク・アクセスでネットワーク・ディレクトリーからホスト・アプリケーションを実行すると、ランタイムがプロファイル出力データをディスクに格納している間に、GUIはカーネル起動間の遅延を表示できます。

水平の棒グラフは、カーネルの実行時間を表します。最初のエントリー(fft1d)に示されている2つのバーの組み合わせは、合計時間を表します。 2番目と最後のエントリーは、時間間隔を占めるカーネルの実行を示します。これらのバーは、 output_kernelとinput_kernelの同時実行を表し、カーネルがメモリー帯域幅などの共通リソースを共有していることを示します。
Kernel Executionタブには、ホストとデバイス間のメモリー転送に関する情報も表示されます。

メモリー転送情報の表示をイネーブルするには、環境変数ACL_PROFILE_TIMERを値1に設定し、ホスト・アプリケーションを実行します。 ACL_PROFILE_TIMER環境変数を設定すると、メモリー転送の記録が可能になります。この情報はprofile.monファイルに格納され、Intel FPGA Dynamic Profiler for OpenCL GUIによって解析されます。
Autorun Capturesタブ
自動実行プロファイル・データは、エンキューされたプロファイル・データと同様に表示されます。ただし、自動実行カーネルは実行時タブが実行時表示になっていないため、自動実行カーネルは継続的に実行されます。
autorunカーネルを少なくとも1回プロファイルすると、Autorun CapturesタブがIntel FPGA Dynamic Profiler for OpenCL GUIに表示されます。このタブには、デバイスとカーネルによって構成されたすべての自動実行プロファイルキャプチャのテーブルが表示されます。特定のキャプチャの自動実行カーネルのプロファイル・データを表示するには、関連するボタンを選択すると、新しいプロファイラ・ウィンドウが開き、自動平均キャプチャのデータのみが表示されます(全体の平均ではなく)。
次の図には、4つの自動実行キャプチャインスタンスがあります。デバイス0のstreamer自動実行カーネルで0.03msで行われたキャプチャから自動プロファイル・データを表示するには、Device 0のストリーマー・ロウの0.03msボタンを選択します。

Profiler Capturesボタンには、キャプチャが開始された時間が表示されます。この時間は、ホストプログラムの開始に関連します。
プロファイリング情報の解釈
次に、Profilerレポートに記録されるIntel FPGA Dynamic Profiler for OpenCLメトリックについて説明します。
- ストール、占有、帯域幅
カーネルコードの特定の行については、Intel FPGA Dynamic Profiler for OpenCL GUIでのSource Codeでタブは、ストール率、占有率、平均メモリー帯域幅が表示されます。 - アクティビティー
Activityは、述語命令が有効になっている時間の割合、つまりLSUがそのデータを受け取る時間の割合を測定します。 - キャッシュヒット
キャッシュヒット率は、プライベート・キャッシュの有効性を測定します。 - OpenCLのデザインシナリオ例のプロファイラ解析
OpenCLのデザインシナリオの例と問題点を理解することで、デザインのProfilerメトリクスを活用してパフォーマンスを最適化することができます。 - 自動プロファイラーデータ
エンキューされたカーネルと同様に、自動実行プロファイラの統計データを表示するには、 次のaoclコマンドを使用してIntel FPGA Dynamic Profiler for OpenCL GUIを実行します。
ストール、占有、帯域幅
失速、占有、帯域幅の定義については、 表 9 を参照してください。
インテル® FPGA SDK for OpenCL™ は、Work-Itemがパイプライン・ステージを順に(パイプライン - パラレルに)横断するパイプライン・アーキテクチャを生成します。パイプライン・ステージが空になるとすぐに、Work-Itemがステージに入り、ステージを占有します。パイプラインの並列性は、パイプライン化されたループの反復にも適用されます。
以下は、Profilerがストール、占有、および帯域幅を計算することを説明する簡略化された式です。
理想的なカーネル・パイプライン条件:
- 失速率は0%に等しい
- 占有率は100%
- 帯域幅はボードの帯域幅に等しい
カーネル・パイプラインの特定の場所では、ストール率と占有率の合計が100%にほぼ等しい場合、プロファイラはその場所をストールソースとして識別します。ストール率が低い場合、プロファイラはその場所をストールの犠牲者として識別します。
Profilerは、オフライン・コンパイラーがカーネルから効率的なパイプラインを生成した場合(作業項目または反復がパイプライン・ステージを通過して停止することなく)、高い占有率を報告します。
すべてのLSUが同じ回数アクセスされた場合、同じ占有値を持ちます。
- Work-Itemが連続してパイプラインに入ることができない場合、パイプラインにバブルを挿入します。
- ループパイプライニングでは、反復間に存在する泡のために、ループに依存する依存関係もパイプラインに泡を形成します。
- LSUが他のLSUよりも頻繁にアクセスされない場合(LSUが他のLSUを含むループの外にある場合など)、このLSUの占有値は他のLSUよりも低くなります。
占有率に関する同じルールがチャネルに適用されます。
チャンネルの停止
たとえば、カーネルにイーサネットI/Oに対するチャネル読み出し呼び出しがあり、Profilerがストールを識別した場合、書込みチャネルは、イーサネットI/Oへのデータ書込みがカーネルの読み出し速度と同じ速度で行われていないことを意味します。
カーネル間チャネルの場合、チャネルの読み出し側と書き込み側の間に不均衡がある場合、または読み出しと書き込みのカーネルが同時に実行されていない場合にストールが発生します。
たとえば、読み出しが書き込みを実行するカーネルと同時に起動されないか、または読み出し操作が書き込み操作よりもはるかに遅い場合、Profilerは書き込みカーネル内のwrite_channel_intel呼び出しの停止を識別します。
アクティビティー
Intel FPGA Dynamic Profiler for OpenCL GUIでのSource Codタブは、占有率(Occupancy%)カラムのツールチップがActivityの割合を指定する場合があります。Activityは、以下に説明するように、アクティビティーが述語に関連する点で占有とは異なります。
各LSUには、ivalid信号の他に述語信号があります。 ivalidシグナルは、上流のロジックが有効なデータをLSUに提供していることを示します。述語信号は、LSUが受信するデータに作用するべきであることを示します。Work-Itemまたはループ反復は、述語であってもメモリー命令を占有することができます。分岐ステートメントにループが含まれていない場合、オフライン・コンパイラーは分岐を変換して制御フローを最小限に抑え、より効率的なハードウェアを実現します。変換の一部として、メモリーおよびチャネル命令を述語化し、出力結果をマルチプレクサ論理によって選択する必要があります。
次の式を検討してみましょう。
int addr = compute_address(); int x = 0; if (some_rare_condition) x = src[addr];
オフライン・コンパイラーは、コードを次のように変更します。
int addr = compute_address(); int x = 0; x = src[addr] if some_rare_condition;
この場合、 src []はクロックサイクルごとに有効なアドレスを受け取ります。パイプラインにSRC []自体は生成されないストールを想定すると、[] SRC用ivalid信号は、ほとんどの時間を高くなります。実際には、 src []は述語信号some_rare_conditionが真の場合にのみロードを実行します。したがって、この負荷動作では、占有率は高くなりますが、活動は低くなります。
ツールヒントで使用可能なアクティビティーの割合は、述語アクセスを考慮しないため、低いアクティビティーの割合に基づいて述語命令を識別できます。活動率が低いにもかかわらず、これらの指示には高い占有率があるかもしれません。
キャッシュヒット
Intel FPGA Dynamic Profiler for OpenCL GUIでのSource Codeタブは、 Attributesカラムのツールヒントがキャッシュヒット率を指定することがあります。一部のグローバル負荷ユニットでは、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーはプライベート・キャッシュをインスタンス化することがあります。この場合、オフライン・コンパイラーはこのキャッシュの有効性を測定するために追加のハードウェアカウンターを作成します。このプライベート・キャッシュの詳細は、HTMLエリアレポートで確認できます。
OpenCLのデザインシナリオ例のプロファイラ解析
ハイストール率
帯域幅の使用が非効率的である場合、また、アプリケーションの実行中に大量のデータ転送が必要な場合、メモリー命令が頻繁に停止します。非効率的なメモリーアクセスは、帯域幅の最適使用に至りません。そのような場合、カーネルのメモリーアクセスを分析して、改善が可能かどうかを確認してください。
チャネルへの読み出しアクセスと書き込みアクセスとの間に強い不均衡が存在する場合、チャネル命令はストールします。不均衡は、チャネル読み出しまたは異なる速度で動作する書き込みによって引き起こされる可能性があります。
たとえば、書き込みチャネル呼び出しのストール率が高いと判明した場合、読み出しチャネル呼び出しの占有率および活動度が低いかどうかを確認します。そうである場合、読み出しチャネル呼び出しを制御するカーネルの実行速度は、書き込みチャネル呼び出しを制御するカーネルにとって遅すぎるため、パフォーマンスのボトルネックにつながります。
メモリーまたはチャネルアクセスがパーセンテージの高いパイプラインストールを引き起こしている場合、メモリーまたはチャネルを指示するソースコードのラインは赤で強調表示されます。ストール率が20%以上になると、ストールの識別が高くなります。ストール率が高いほど、赤いハイライトが暗くなります。高失速率の値を簡単にトラバースするには、[ソースコード]タブの右下隅に右矢印と左矢印があります。

低い占有率
次の式を検討してみましょう。
__kernel void proc (__global int * a, ...) { for (int i = 0; i < N; i++) { for (int j = 0; j < 1000; j++) { write_channel_intel (c0, data0); } for (int k = 0; k < 3; k++) { write_channel_intel (c1, data1); } } }
すべてのループがパイプライン化されていると仮定すると、トリップカウントが1000の最初の内部ループがクリティカルループです。トリップカウントが3の2番目の内部ループは頻繁に実行されません。その結果、チャネルc0の占有率およびアクティビティーパーセンテージが高く、チャネルc1の占有率およびアクティビティーの割合が低いことが期待できます。
また、小さなワーク・グループ・サイズを定義すると、占有率が低くなる可能性があり、カーネルが十分なWork-Itemを受け取らない可能性があります。これは問題があります。なぜなら、一般にカーネルの実行中にパイプラインが空であり、パフォーマンスが低下するからです。
低帯域幅効率
メモリーアクセスを確認して、メモリーサイトへのアクセスが連続するメモリーエリアをアドレス指定するように書き換えることができるかどうかを確認します。
高い失業率と高い占有率
通常、ストール率と占有率の合計はほぼ100%になります。ロードおよびストア動作またはチャネルの失速率が高い場合、ロードおよびストア動作またはチャネルがすべてのサイクルを実行できるが、ストールが発生していることを意味します。
グローバルロードおよびストア動作をストールするためのソリューションは以下の通りです。
- ローカルメモリーを使用してデータをキャッシュします。
- データを読み出す回数を減らします。
- グローバル・メモリー・アクセスを改善します。
- より多くのグローバル・メモリー・フレンドリーなアドレッシング(例えば、ストライド・アクセスからシーケンシャル・アクセスへの変更)のアクセス・パターンを変更してください。
- カーネルを-no-interleaving=default Intel® FPGA SDK for OpenCL™オフライン・コンパイラーコマンドオプションでコンパイルし、読み出しバッファーと書き込みバッファーを別々のDDRバンクに分けます。
- より少ないが広いグローバル・メモリー・アクセスがあります。
- より多くの帯域幅を持つアクセラレーター・ボードを取得します(たとえば、2つのDDRの代わりに3つのDDRを持つボード)。
ローカルロードおよびストア動作をストールするためのソリューションは以下の通りです。
- HTMLエリアレポートを確認してローカルメモリーの構成を確認し、構成を変更してストールフリーにします。
チャネルをストールするためのソリューションは以下の通りです。
- チャンネルの反対側のストールを修正します。たとえば、チャネルの読み出しが停止した場合、チャネルへのライターがチャネルにデータを書き込む速度が十分でなく、調整が必要であることを意味します。
- デザインにチャンネルループがある場合、チャンネルの深さを指定します。
ノーストール、低占有率、低帯域幅効率

この例では、dst[]は、 FACTOR2ループの20回の反復とFACTOR1ループの回4回の繰り返しが実行されます。したがって、FACTOR2ループがボトルネックの原因となります。
ループボトルネックを解決するソリューションは以下の通りです。
- FACTOR1をアンロールし、 FACTOR2は均等にループします。 FACTOR1ループをさらに展開するだけでは、ボトルネックは解消されません
- カーネルをベクトル化し、各ループ反復中にMultiple Work-Itemを実行できるようにする
ノー・ストール、高占有率、低帯域幅効率

この例では、アクセラレーター・ボードは25600 MB/sの帯域幅を提供できます。しかし、 vector_addカーネルは、使用可能な帯域幅の14%を要求しています(2リード+ 1ライト)。x 4バイトx 294 MHz = 12バイト/サイクルx 294 MHz = 3528 MB/s。帯域幅を増やすには、各クロックサイクルで実行されるタスクの数を増やします。
低帯域幅のソリューションは以下の通りです。
- カーネルを自動的または手動でベクトル化してwider要求を作成する
- 最も内側のループをアンロールして、クロックサイクルごとにさらに多くの要求を実行する
- タスクの一部を別のカーネルに委譲する
高失速率と低占有率
自動プロファイラーデータ
aocl report <filename>.aocx profile.mon <filename>.source
Intel FPGA Dynamic Profiler for OpenCLの制限
- Profilerは、実行中のカーネルから1組のプロファイル・データのみを抽出することができます。
Profilerがカーネルの実行が完了した後でプロファイル・データを収集する場合、 profile.monファイルを複数回生成するようにホストAPIを呼び出すことができます。
カーネルの実行時にプロファイル・データを収集する方法の詳細については、 インテル® FPGA SDK for OpenCL™ プログラミング・ガイドのカーネル実行部の間に収集プロファイル・データのセクションを参照してください。
- プロファイル・データは、OpenCLプログラムまたは複数のデバイスで永続的ではありません。
プロファイル・データは、単一のOpenCLプログラムと単一のデバイスのみで要求できます。ホストが新しいカーネルプログラムをFPGAの内部と外部に交換すると、Profilerはプロファイル・データを保存しません。
- パフォーマンス・カウンターを使用してVerilogコードをインスツルメント化すると、ハードウェアリソースの使用率(つまりFPGAエリアの使用率)が増加し、通常はパフォーマンスが低下します。
パフォーマンス・カウンターを使用したVerilogコードのインストルメントについては、 インテル® FPGA SDK for OpenCL™ プログラミング・ガイドの「パフォーマンス・カウンターを使用したカーネル・パイプラインのインストルメントのセクションを参照してください。
Single Work-Itemカーネル・パフォーマンスを向上させるための戦略
- 最適化レポートのフィードバックに基づくてSingle Work-Itemカーネル依存関係のアドレッシング
多くの場合、 OpenCL™アプリケーションをSingle Work-Itemカーネルとしてデザインするだけで、追加の最適化手順を実行せずにパフォーマンスを最大化することができます。 - メモリー配列へのアクセスによるループキャリー依存関係の削除
単一のWork-Itemカーネルにivdepプラグマを含めて、メモリー配列へのアクセスがループに依存する依存関係を引き起こさないことを宣言します。 - Single Work-Itemカーネルの良いデザイン方法
OpenCL™カーネルにループ構造が含まれている場合は、 インテル® で推奨されるのガイドラインに従って、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーが効果的に解析できるようにカーネルを構築してください。
最適化レポートのフィードバックに基づくてSingle Work-Itemカーネル依存関係のアドレッシング
次のフローチャートは、デザインを反復し、Single Work-Itemカーネルを最適化するために取ることができるアプローチの概要を示しています。使用方法については、 インテル® FPGA SDK for OpenCL™ EmulatorとProfilerについては、 インテル® FPGA SDK for OpenCL™ プログラミング・ガイドのOpenCLカーネルのエミュレートとデバッグとOpenCLカーネルの プロファイリングを参照してください。 Intel FPGA Dynamic Profiler for OpenCL GUIおよびプロファイリング情報について詳しくは、 パフォーマンス・ボトルネックを特定するためのカーネルのプロファイルを参照してください。
インテル® は除去、緩和、単純化、およびローカルメモリーへの転送の順に、単一のWork-Itemカーネルループ搬送依存関係に対処するための以下の最適化オプションを推奨しています。
- ループ実行依存関係の削除
最適化レポートからのフィードバックに基づいて、より単純なメモリー・アクセス・パターンを実装することにより、ループで運ばれる依存関係を削除できます。 - Relaxing Loop Carriedの依存関係
最適化レポートからのフィードバックに基づいて、依存距離を増やすことでループに依存する依存関係を緩和することができます。 - Loop Carried依存関係の簡素化
カーネルでループに依存する依存関係を削除したり緩めることができない場合、依存関係を単純化して単一のwork-itemカーネルのパフォーマンスを向上させることができます。 - ループで運ばれた依存関係のローカルメモリーへの転送
削除できないループキャリー依存関係の場合は、グローバルメモリーからローカルメモリーへのループキャリー依存関係を持つ配列を移動してIIを改善します。 - シフトレジスターの推測によるループキャリー依存関係の削除
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーで倍精度浮動小数点演算を効率的に実行する1つの作業項目カーネルを処理できるようにするには、シフトレジスターを推論してループ実行依存関係を削除します。
ループ実行依存関係の削除
以下の項目について検討します。
1 #define N 128 2 3 __kernel void unoptimized (__global int * restrict A, 4 __global int * restrict B, 5 __global int* restrict result) 6 { 7 int sum = 0; 8 9 for (unsigned i = 0; i < N; i++) { 10 for (unsigned j = 0; j < N; j++) { 11 sum += A[i*N+j]; 12 } 13 sum += B[i]; 14 } 15 16 * result = sum; 17 }
unoptimizedカーネルの最適化レポートは、次のようになります。
=================================================================================== Kernel: unoptimized =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Loop "Block1" (file k.cl line 9) | Pipelined with successive iterations launched every 2 cycles due to: | | Pipeline structure: every terminating loop with subloops has iterations | launched at least 2 cycles apart. | Having successive iterations launched every two cycles should still lead to | good performance if the inner loop is pipelined well and has sufficiently | high number of iterations. | | Iterations executed serially across the region listed below. | Only a single loop iteration will execute inside the listed region. | This will cause performance degradation unless the region is pipelined well | (can process an iteration every cycle). | | Loop "Block2" (file k.cl line 10) | due to: | Data dependency on variable sum (file k.cl line 7) | | |-+ Loop "Block2" (file k.cl line 10) Pipelined well. Successive iterations are launched every cycle.
- レポートの最初のロウは、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは外部ループのパイプライン実行を正常に推定し、新しいループ反復が他のサイクルごとに起動します。
-
due to Pipeline structureというメッセージは、オフライン・コンパイラーが外側ループ反復を2サイクルごとに起動させるパイプライン構造を作成することを示します。この動作は、カーネルコードの構造の結果ではありません。注: 単一のWork-Itemカーネルを構成する方法の推奨事項については、単一のWork-Itemカーネルのための良いデザイン方法のセクションを参照してください。
- レポートの最初の行の残りのメッセージは、変数sumに対するデータ依存のために、ループがサブループ全体で一度に1回の繰り返しを実行することを示しています。このデータ依存関係は、各外部ループ反復が、内部ループが実行を開始する前に前の反復からのsumの値が必要であるために存在します。
- レポートの2番目のロウは、内部ループがパイプライン形式で実行され、パフォーマンス制限のあるループに依存する依存関係がないことを通知します。
外側のループの反復がサブループ渡って連続的に実行されないように、このカーネルのパフォーマンスを最適化するには、変数sumのデータの依存関係を削除します。 2つのループでsumを含む計算を切り離すには、次のタスクを実行します。
- 内部ループでのみ使用するローカル変数( sum2など )を定義します。
- ステップ1のローカル変数を使用して、 A [i * N + j]の累積値を内部ループの繰り返しとして格納します。
- 外部ループでは、 B [i]の累積値とローカル変数に格納されている値を格納する変数sumを格納します。
1 #define N 128 2 3 __kernel void optimized (__global int * restrict A, 4 __global int * restrict B, 5 __global int * restrict result) 6 { 7 int sum = 0; 8 9 for (unsigned i = 0; i < N; i++) { 10 // Step 1: Definition 11 int sum2 = 0; 12 13 // Step 2: Accumulation of array A values for one outer loop iteration 14 for (unsigned j = 0; j < N; j++) { 15 sum2 += A[i*N+j]; 16 } 17 18 // Step 3: Addition of array B value for an outer loop iteration 19 sum += sum2; 20 sum += B[i]; 21 } 22 23 * result = sum; 24 }
以下のような最適化レポートは、変数sumに対するループキャリー依存関係の削除に成功したことを示しています。
=================================================================================== Kernel: optimized =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Loop "Block1" (file optimized.cl line 9) | Pipelined with successive iterations launched every 2 cycles due to: | | Pipeline structure: every terminating loop with subloops has iterations | launched at least 2 cycles apart. | Having successive iterations launched every two cycles should still lead to | good performance if the inner loop is pipelined well and has sufficiently | high number of iterations. | | |-+ Loop "Block2" (file optimized.cl line 14) Pipelined well. Successive iterations are launched every cycle. ===================================================================================
最適化レポートに次のメッセージだけが表示されたら、ループキャリー依存関係の問題はすべて解決しました。
- Pipelined execution inferred ∸最も内側のループ用。
- Pipelined execution inferred. Successive iterations launched every 2 cycles due to: Pipeline structure ∸他のすべてのループ用。
Relaxing Loop Carriedの依存関係
次の式を検討してみましょう。
1 #define N 128 2 3 __kernel void unoptimized (__global float * restrict A, 4 __global float * restrict result) 5 { 6 float mul = 1.0f; 7 8 for (unsigned i = 0; i < N; i++) 9 mul *= A[i]; 10 11 * result = mul; 12 }
=================================================================================== Kernel: unoptimized =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Loop "Block1" (file unoptimized.cl line 8) Pipelined with successive iterations launched every 6 cycles due to: Data dependency on variable mul (file unoptimized.cl line 9) Largest Critical Path Contributor: 100%: Fmul Operation (file unoptimized.cl line 9) ===================================================================================
上記の最適化レポートでは、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーがループのパイプライン実行を首尾よく推測します。しかし、変数mulに対するループキャリーの依存関係は、6サイクルごとにループ反復を開始させます。この場合、ライン9上の浮動小数点乗算演算(すなわち、 mul * = A [i] )は、変数mulの計算に対する最大の遅延に寄与します。
ループ運搬のデータ依存性を緩和するために、代わりにすべてのMの繰り返しを、乗算結果を格納する変数のMコピー上で動作し、一つのコピーを使用するために単一の変数を使用します。
- 変数mulの複数のコピーを宣言します(たとえば、 mul_copiesという配列内 )。
- mul_copiesのすべてのコピーを初期化します。
- 乗算演算では、配列の最後のコピーを使用します。
- シフト演算を実行して、配列の最後の値をシフトレジスターの先頭に戻します。
- すべてのコピーをmulに削減し、最終値をresultに書き込みます。
1 #define N 128 2 #define M 8 3 4 __kernel void optimized (__global float * restrict A, 5 __global float * restrict result) 6 { 7 float mul = 1.0f; 8 9 // Step 1: Declare multiple copies of variable mul 10 float mul_copies[M]; 11 12 // Step 2: Initialize all copies 13 for (unsigned i = 0; i < M; i++) 14 mul_copies[i] = 1.0f; 15 16 for (unsigned i = 0; i < N; i++) { 17 // Step 3: Perform multiplication on the last copy 18 float cur = mul_copies[M-1] * A[i]; 19 20 // Step 4a: Shift copies 21 #pragma unroll 22 for (unsigned j = M-1; j > 0; j--) 23 mul_copies[j] = mul_copies[j-1]; 24 25 // Step 4b: Insert updated copy at the beginning 26 mul_copies[0] = cur; 27 } 28 29 // Step 5: Perform reduction on copies 30 #pragma unroll 31 for (unsigned i = 0; i < M; i++) 32 mul *= mul_copies[i]; 33 34 * result = mul; 35 }
以下のような最適化レポートは、変数mulに対するループキャリー依存関係の緩和に成功したことを示しています。
=================================================================================== Kernel: optimized =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Fully unrolled loop (file optimized2.cl line 13) Loop was automatically and fully unrolled. Add "#pragma unroll 1" to prevent automatic unrolling. + Loop "Block1" (file optimized2.cl line 16) | Pipelined well. Successive iterations are launched every cycle. | | |-+ Fully unrolled loop (file optimized2.cl line 22) Loop was fully unrolled due to "#pragma unroll" annotation. + Fully unrolled loop (file optimized2.cl line 31) Loop was fully unrolled due to "#pragma unroll" annotation.
Loop Carried依存関係の簡素化
次の式を検討してみましょう。
1 #define N 128 2 #define NUM_CH 3 3 4 channel uchar CH_DATA_IN[NUM_CH]; 5 channel uchar CH_DATA_OUT; 6 7 __kernel void unoptimized() 8 { 9 unsigned storage = 0; 10 unsigned num_bytes = 0; 11 12 for (unsigned i = 0; i < N; i++) { 13 14 #pragma unroll 15 for (unsigned j = 0; j < NUM_CH; j++) { 16 if (num_bytes < NUM_CH) { 17 bool valid = false; 18 uchar data_in = read_channel_nb_intel(CH_DATA_IN[j], &valid); 19 if (valid) { 20 storage <<= 8; 21 storage |= data_in; 22 num_bytes++; 23 } 24 } 25 } 26 27 if (num_bytes >= 1) { 28 num_bytes -= 1; 29 uchar data_out = storage >> (num_bytes*8); 30 write_channel_intel(CH_DATA_OUT, data_out); 31 } 32 } 33 }
このカーネルは、3つの入力チャンネルから1バイトのデータをノンブロッキング形式で読み込みます。次に、データを一度に1バイトずつ出力チャネルに書き込みます。変数storageを使用して最大4バイトのデータを格納し、変数num_bytesを使用して、格納されているバイト数を追跡します。 storageに使用可能なスペースがある場合、カーネルはチャネルの1つから1バイトのデータを読み出し、storageの最下位バイトに格納します。
次の最適化レポートは、変数num_bytesにループで運ばれる依存関係があることを示しています。
=================================================================================== Kernel: unoptimized =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Loop "Block1" (file unoptimized3.cl line 12) | Pipelined with successive iterations launched every 7 cycles due to: | | Data dependency on variable num_bytes (file unoptimized3.cl line 10) | Largest Critical Path Contributors: | 16%: Integer Compare Operation (file unoptimized3.cl line 16) | 16%: Integer Compare Operation (file unoptimized3.cl line 16) | 16%: Integer Compare Operation (file unoptimized3.cl line 16) | 7%: Integer Compare Operation (file unoptimized3.cl line 27) | 6%: Add Operation (file unoptimized3.cl line 10, line 22, line 28) | 6%: Add Operation (file unoptimized3.cl line 10, line 22, line 28) | 6%: Add Operation (file unoptimized3.cl line 10, line 22, line 28) | 3%: Non-Blocking Channel Read Operation (file unoptimized3.cl line 18) | 3%: Non-Blocking Channel Read Operation (file unoptimized3.cl line 18) | 3%: Non-Blocking Channel Read Operation (file unoptimized3.cl line 18) | | |-+ Fully unrolled loop (file unoptimized3.cl line 15) Loop was fully unrolled due to "#pragma unroll" annotation.
num_bytesの計算パスは次のとおりです。
- 16行目の比較( (num_bytes <NUM_CH)の場合 )
- 19行目の比較のために、18行目の非ブロッキング・チャネル読み出し操作でvalid変数の計算( uchar data_in = read_channel_nb_intel(CH_DATA_IN [j]、&valid) )。
- 22行目への追加( num_bytes ++ )。
- 27行目の比較( if(num_bytes> = 1) )。
- 28行目の減算( num_bytes - = 1 )。
14行目のunrollプラグマのために、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは、ループをアンロールし、ループ本体の比較と追加を3回繰り返します。最適化レポートは、比較がnum_bytesの計算パスで最も高価な演算であり、その後に22行目の加算が続くことを示しています。
num_bytesに対するループキャリーの依存関係を単純化するには、アプリケーションを再構築して次のタスクを実行することを検討してください 。
-
カーネルは、 storageに使用可能な十分なスペースがある場合にのみチャネルから読み出すことを確認し、すべてのチャネルの操作がデータを返す読み出すた場合に(つまり、 storage内の空きスペースの少なくとも3つのバイトがある)。
この条件を設定すると、比較回数を減らすことで変数num_bytesの計算パスが簡単になります。
- より簡単に3バイトのスペースしきい値を満たすために、 storageのサイズを4バイトから8バイトに増やします。
1 #define N 128 2 #define NUM_CH 3 3 4 channel uchar CH_DATA_IN[NUM_CH]; 5 channel uchar CH_DATA_OUT; 6 7 __kernel void optimized() 8 { 9 // Change storage to 64 bits 10 ulong storage = 0; 11 unsigned num_bytes = 0; 12 13 for (unsigned i = 0; i < N; i++) { 14 15 // Ensure that we have enough space if we read from ALL channels 16 if (num_bytes <= (8-NUM_CH)) { 17 #pragma unroll 18 for (unsigned j = 0; j < NUM_CH; j++) { 19 bool valid = false; 20 uchar data_in = read_channel_nb_intel(CH_DATA_IN[j], &valid); 21 if (valid) { 22 storage <<= 8; 23 storage |= data_in; 24 num_bytes++; 25 } 26 } 27 } 28 29 if (num_bytes >= 1) { 30 num_bytes -= 1; 31 uchar data_out = storage >> (num_bytes*8); 32 write_channel_intel(CH_DATA_OUT, data_out); 33 } 34 } 35 }
An optimization report similar to the one below indicates the successful simplification of the loop-carried dependency on the variable num_bytes:
=================================================================================== Kernel: optimized =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Loop "Block1" (file optimized3.cl line 13) | Pipelined well. Successive iterations are launched every cycle. | | |-+ Fully unrolled loop (file optimized3.cl line 18) Loop was fully unrolled due to "#pragma unroll" annotation.
ループで運ばれた依存関係のローカルメモリーへの転送
次の式を検討してみましょう。
1 #define N 128 2 3 __kernel void unoptimized( __global int* restrict A ) 4 { 5 for (unsigned i = 0; i < N; i++) 6 A[N-i] = A[i]; 7 }
=================================================================================== Kernel: unoptimized =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Loop "Block1" (file unoptimized4.cl line 5) Pipelined with successive iterations launched every 324 cycles due to: Memory dependency on Load Operation from: (file unoptimized4.cl line 6) Store Operation (file unoptimized4.cl line 6) Largest Critical Path Contributors: 49%: Load Operation (file unoptimized4.cl line 6) 49%: Store Operation (file unoptimized4.cl line 6)
グローバル・メモリー・アクセスには長いレイテンシーがあります。この例では、配列A [i]に対するループ搬送依存性は長いレイテンシーを引き起こす。このレイテンシーは、最適化レポートのIIが324に反映されています。ループで運ばれた依存関係をグローバルメモリーからローカルメモリーに転送してII値を減らすには、次のタスクを実行します。
- ループ実行された依存関係を持つ配列をローカルメモリーにコピーします。この例では、配列A[i]はローカルメモリーの配列B [i]になります。
- 配列B [i]に対してループで実行される依存関係を持つループを実行します。
- 配列をグローバルメモリーにコピーし直します。
以下は、再構成されたカーネル最適化です。
1 #define N 128 2 3 __kernel void optimized( __global int* restrict A ) 4 { 5 int B[N]; 6 7 for (unsigned i = 0; i < N; i++) 8 B[i] = A[i]; 9 10 for (unsigned i = 0; i < N; i++) 11 B[N-i] = B[i]; 12 13 for (unsigned i = 0; i < N; i++) 14 A[i] = B[i]; 15 }
下記のような最適化レポートは、324から2へのIIの減少の成功を示しています。
=================================================================================== Kernel: optimized =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Loop "Block1" (file optimized4.cl line 7) Pipelined well. Successive iterations are launched every cycle. + Loop "Block2" (file optimized4.cl line 10) Pipelined with successive iterations launched every 2 cycles due to: Memory dependency on Load Operation from: (file optimized4.cl line 11) Store Operation (file optimized4.cl line 11) Largest Critical Path Contributors: 65%: Load Operation (file optimized4.cl line 11) 34%: Store Operation (file optimized4.cl line 11) + Loop "Block3" (file optimized4.cl line 13) Pipelined well. Successive iterations are launched every cycle.
シフトレジスターの推測によるループキャリー依存関係の削除
以下の項目について検討します。
1 __kernel void double_add_1 (__global double *arr, 2 int N, 3 __global double *result) 4 { 5 double temp_sum = 0; 6 7 for (int i = 0; i < N; ++i) 8 { 9 temp_sum += arr[i]; 10 } 11 12 *result = temp_sum; 13 }
unoptimizedカーネルのOptimizationレポートは、次のようになります。
=================================================================================== Kernel: double_add_1 =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Loop "Block1" (file unoptimized5.cl line 7) Pipelined with successive iterations launched every 11 cycles due to: Data dependency on variable temp_sum (file unoptimized5.cl line 9) Largest Critical Path Contributor: 97%: Fadd Operation (file unoptimized5.cl line 9)
最適化されていないカーネルは、倍精度浮動小数点配列arr [i]の要素を合計する累算器です。各ループ反復に対して、オフライン・コンパイラーは加算の結果を計算するために11サイクルを要し、それを変数temp_sumに格納します 。各ループの反復では、以前のループ反復からのtemp_sumの値が必要です。これにより、 temp_sumにデータ依存関係が作成されます 。
以下は、再構成されたカーネルoptimizedです:
1 //Shift register size must be statically determinable 2 #define II_CYCLES 12 3 4 __kernel void double_add_2 (__global double *arr, 5 int N, 6 __global double *result) 7 { 8 //Create shift register with II_CYCLE+1 elements 9 double shift_reg[II_CYCLES+1]; 10 11 //Initialize all elements of the register to 0 12 for (int i = 0; i < II_CYCLES + 1; i++) 13 { 14 shift_reg[i] = 0; 15 } 16 17 //Iterate through every element of input array 18 for(int i = 0; i < N; ++i) 19 { 20 //Load ith element into end of shift register 21 //if N > II_CYCLE, add to shift_reg[0] to preserve values 22 shift_reg[II_CYCLES] = shift_reg[0] + arr[i]; 23 24 #pragma unroll 25 //Shift every element of shift register 26 for(int j = 0; j < II_CYCLES; ++j) 27 { 28 shift_reg[j] = shift_reg[j + 1]; 29 } 30 } 31 32 //Sum every element of shift register 33 double temp_sum = 0; 34 35 #pragma unroll 36 for(int i = 0; i < II_CYCLES; ++i) 37 { 38 temp_sum += shift_reg[i]; 39 } 40 41 *result = temp_sum; 42 }
次のOptimizationレポートは、シフトレジスターshift_reg [II_CYCLES]の推論が変数temp_sumのデータ依存性を正常に削除することを示しています。
=================================================================================== Kernel: double_add_2 =================================================================================== The kernel is compiled for single work-item execution. Loop Report: + Fully unrolled loop (file optimized5.cl line 12) Loop was automatically and fully unrolled. Add "#pragma unroll 1" to prevent automatic unrolling. + Loop "Block1" (file optimized5.cl line 18) | Pipelined well. Successive iterations are launched every cycle. | | |-+ Fully unrolled loop (file optimized5.cl line 26) Loop was fully unrolled due to "#pragma unroll" annotation. + Fully unrolled loop (file optimized5.cl line 36) Loop was fully unrolled due to "#pragma unroll" annotation.
メモリー配列へのアクセスによるループキャリー依存関係の削除
-
ループ内にあるメモリー配列へのすべてのアクセスがループキャリー依存関係を引き起こさない場合、カーネルコードのループの前に#pragma ivdep行を追加します。
Example kernel code:
// no loop-carried dependencies for A and B array accesses #pragma ivdep for (int i = 0; i < N; i++) { A[i] = A[i - X[i]]; B[i] = B[i - Y[i]]; }
-
ループ内の特定のメモリー配列へのアクセスがループに依存する依存関係を引き起こさないように指定するには、カーネルコードのループの前に#pragma ivdep array( array_name )という行を追加します。
ivdepプラグマで指定された配列は、ローカルまたはプライベートのメモリー配列、またはグローバル、ローカル、またはプライベートのメモリーストレージを指すポインター変数でなければなりません。指定された配列がポインターの場合、 ivdepプラグマは指定されたポインターで別名を持つ可能性があるすべての配列にも適用されます。
ivdepプラグマで指定された配列は、構造体の配列またはポインターメンバーでもあります。
カーネルコードの例 :
// No loop-carried dependencies for A array accesses // The offline compiler will insert hardware that reinforces dependency constraints for B #pragma ivdep array(A) for (int i = 0; i < N; i++) { A[i] = A[i - X[i]]; B[i] = B[i - Y[i]]; } // No loop-carried dependencies for array A inside struct #pragma ivdep array(S.A) for (int i = 0; i < N; i++) { S.A[i] = S.A[i - X[i]]; } // No loop-carried dependencies for array A inside the struct pointed by S #pragma ivdep array(S->X[2][3].A) for (int i = 0; i < N; i++) { S->X[2][3].A[i] = S.A[i - X[i]]; } // No loop-carried dependencies for A and B because ptr aliases // with both arrays int *ptr = select ? A : B; #pragma ivdep array(ptr) for (int i = 0; i < N; i++) { A[i] = A[i - X[i]]; B[i] = B[i - Y[i]]; } // No loop-carried dependencies for A because ptr only aliases with A int *ptr = &A[10]; #pragma ivdep array(ptr) for (int i = 0; i < N; i++) { A[i] = A[i - X[i]]; B[i] = B[i - Y[i]]; }
Single Work-Itemカーネルの良いデザイン方法
ポインター・エイリアシングの回避
可能であれば、ポインター引数にrestrictキーワードを挿入します。ポインター引数にrestrictキーワードを含めることにより、オフライン・コンパイラーは、競合しない読み出し動作と書き込み動作の間に不要なメモリー依存関係を作成することを防止します。各反復があるアレイからデータを読み出し、同じ物理メモリー内の別のアレイにデータを書き込むループを検討してください。これらのポインター引数にrestrictキーワードを含めることなく、オフライン・コンパイラーは2つの配列間の依存関係を想定し、その結果としてパイプラインの並列性を少なくします。
「整形式ループの構築
「正常に形成されたループは、整数境界と比較され、反復ごとに1の単純な誘導インクリメントを有する出口条件がありますカーネルに "整形式"ループを含めると、オフライン・コンパイラーがこれらのループを効率的に解析できるため、パフォーマンスが向上します。
次の例は、「整形式ループです。
for (i = 0; i < N; i++) { //statements }
次の例は、「整形式ネストループ構造です。
for (i = 0; i < N; i++) { //statements for(j = 0; j < M; j++) { //statements } }
ループ運搬依存関係の最小化
以下のループ構造は、各ループ反復が以前の反復によって書き込まれたデータを読み込むため、ループに依存する依存関係を作成します。その結果、前の反復からの書き込み動作が完了するまで、各読み出し動作を続行できません。ループに依存する依存関係が存在すると、オフライン・コンパイラーが達成できるパイプラインの並列性が低下し、カーネルのパフォーマンスが低下します。
for (int i = 0; i < N; i++) { A[i] = A[i - 1] + i; }
オフライン・コンパイラーは、ループで静的なメモリー依存分析を実行して、ループが達成できる並列度を判断します。場合によっては、オフライン・コンパイラーが2つの配列アクセス間の依存関係を想定し、その結果としてパイプラインの並列性が低下することがあります。オフライン・コンパイラーは、未知の変数のためにコンパイル時に依存関係を解決できない場合、または配列アクセスが複雑なアドレッシングを伴う場合、ループに依存する依存関係を前提としています。
可能な場合、ループに依存する依存関係を最小限に抑えるため、以下のガイドラインに従ってください。
-
ポインター演算を回避します。
カーネルが算術演算から導出されたポインター値を逆参照することによって配列にアクセスするとき、コンパイラーの出力は最適ではない。たとえば、次の方法で配列にアクセスしないでください。
for (int i = 0; i < N; i++) { int t = *(A++); *A = t; }
-
単純な配列インデックスを導入します。
オフライン・コンパイラーがそれらを効率的に解析できないため、次のタイプの複雑な配列インデックスは回避してください。コンパイラー出力が最適ではない場合があります。
- 配列インデックスの非定数。
たとえば、 A [K + i] ( iはループインデックス変数、 Kは未知変数)。
- 同じ添字の場所に複数の索引変数があります。
たとえば、 A [i + 2×j] ( iとjはダブルネストループのループインデックス変数)です。
注: オフライン・コンパイラーは、インデックス変数が異なる下付き文字であるため、効率的に配列インデックスA [i] [j]を解析できます。 - 非線形インデックス付け。
たとえば、 A [i&C]はiがループインデックス変数、 Cが定数または非定数変数です。
- 配列インデックスの非定数。
-
可能であれば、カーネル内の一定の境界を持つループを使用してください。
一定の境界を持つループにより、オフライン・コンパイラーは範囲分析を効果的に実行できます。
複雑なループ終了条件の回避
オフライン・コンパイラーは、終了条件を評価して、後続のループ反復がループパイプラインに入るかどうかを判断します。オフライン・コンパイラーが終了条件を評価するためにメモリーアクセスまたは複雑な動作を必要とすることがあります。このような場合、後続の反復は、評価が完了するまで開始できず、全体的なループのパフォーマンスが低下します。
ネステッド・ループから単一のループへの変換
パフォーマンスを最大にするには、ネステッド・ループを可能な限り単一のフォームに結合します。ネステッド・ループを単一のループに再構成することは、ループ反復間のハードウェア・フットプリントおよび計算オーバーヘッドを低減します。
次のコード例は、ネストされたループの単一ループへの変換を示しています。
ネストループ | 変換シングルループ |
---|---|
for (i = 0; i < N; i++) { //statements for (j = 0; j < M; j++) { //statements } //statements } |
for (i = 0; i < N*M; i++) { //statements } |
可能な限り最も深いスコープ内の変数の宣言
変数の実装に必要なハードウェア・リソースを減らすには、変数をループで使用する前に宣言します。変数を使用しないループ全体で変数データを保存する必要がないため、できるだけ深いスコープで変数を宣言すると、データの依存関係やハードウェアの使用が最小限に抑えられます。
次の式を検討してみましょう。
int a[N]; for (int i = 0; i < m; ++i) { int b[N]; for (int j = 0; j < n; ++j) { // statements } }
配列aは、配列bよりも多くのリソースを実装する必要があります。ハードウェアの使用を減らすには、外側ループの反復によってデータを維持する必要がない限り、配列aを内側ループの外側に宣言します。
NDRangeカーネルのデータ処理効率を向上させるための戦略
次のカーネルコードを検討してください。
__kernel void sum (__global const float * restrict a, __global const float * restrict b, __global float * restrict answer) { size_t gid = get_global_id(0); answer[gid] = a[gid] + b[gid]; }
このカーネルは、配列aとbを一度に1つずつ追加します。各Work-Itemは、2つの要素(各配列から1つずつ)を追加し、和を配列の回答に格納する役割を担います 。最適化がなければ、カーネルはWork-Itemごとに1回の追加を実行します。
- 最大ワーク・グループ・サイズまたは必要なワーク・グループ・サイズの指定
可能であれば 、カーネルのmax_work_group_size属性またはreqd_work_group_size属性を指定します。これらの属性により、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは余分なロジックなしでカーネルをハードウェア・リソースに一致させるための積極的な最適化を実行します。 - カーネルのベクトル化
カーネルのベクトル化により、Multiple Work-Itemを単一命令複数データ(SIMD)形式で実行することができます。 - 複数のコンピューティング・ユニット
より高いスループットを達成するために、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーはカーネルごとに複数のコンピューティング・ユニットを生成できます。 - コンピューティング・ユニット複製とカーネルSIMDベクトル化の組み合わせ
複製またはベクトル化されたOpenCLカーネルがFPGAに収まらない場合、コンピューティング・ユニットを複製してカーネルをベクトル化することによって、カーネルを変更できます。 - リソース駆動型最適化
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは、様々な値のカーネル属性を組み合わせる効果を自動的に分析し、リソース駆動型の最適化を実行します。 - HTMLレポートのカーネルプロパティとループアンロールステータスの確認
NDRangeカーネルをコンパイルすると、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは <your_kernel_filename>/reports/report.htmlファイルを生成して、選択されたカーネル・プロパティーとループアンロール・ステータスに関する情報を提供します。
最大ワーク・グループ・サイズまたは必要なワーク・グループ・サイズの指定
オフライン・コンパイラーは、コンパイル時および実行時に課される特定の制約に応じて、カーネルのデフォルトのワーク・グループ・サイズを想定しています。
オフライン・コンパイラーは、コンパイル時に次の制約を課します。
- reqd_work_group_size属性に値を指定すると、作業グループのサイズはこの値と一致する必要があります。
- max_work_group_size属性に値を指定すると、ワーク・グループ・サイズはこの値を超えてはなりません。
- reqd_work_group_sizeとmax_work_group_sizeの値を指定せず、カーネルに障壁がある場合、オフライン・コンパイラーのデフォルトの作業グループサイズは256です。
- 両方の属性に値を指定せず、カーネルにバリアが含まれていない場合、オフライン・コンパイラーはコンパイル時にワーク・グループ・サイズに制約を課すことはありません。
OpenCL™標準では、実行時に次の制約が課されます。
- 各ディメンションのワーク・グループ・サイズは、各ディメンションの要求されたNDRangeサイズに均等に分割する必要があります。
- ワーク・グループ・サイズは、 clGetDeviceInfo APIコールのCL_DEVICE_MAX_WORK_GROUP_SIZEおよびCL_DEVICE_MAX_WORK_ITEM_SIZESクエリで指定されたデバイス制約を超えてはなりません。
reqd_work_group_size属性とmax_work_group_size属性の両方に値を指定しない場合、ランタイムはデフォルトのワーク・グループ・サイズを次のように決定します。
- カーネルに障壁が含まれているかローカルのWork-Item IDを参照している場合、またはホストコードでclGetKernelWorkGroupInfoおよびclGetDeviceInfo API呼び出しを使用してワーク・グループ・サイズを照会すると、実行時はワーク・グループ・サイズからWork-Itemにデフォルトで実行します。
- カーネルに障壁がないか、ローカルWork-ItemIDを参照していない場合、またはホストコードがワーク・グループ・サイズを照会しない場合、デフォルトのワーク・グループ・サイズはグローバルNDRangeサイズです。
NDRangeカーネル(つまり、Single Work-Itemカーネルではない)をキューイングするときは、次の条件で明示的なワーク・グループ・サイズを指定します。
- カーネルがメモリーバリア、ローカルメモリー、またはローカルWork-ItemIDを使用している場合。
- ホストプログラムが作業グループのサイズを照会する場合。
カーネルでメモリーバリアが使用されている場合、次のいずれかの作業を実行して、ハードウェア・リソースを最小限に抑えます。
- reqd_work_group_size属性の値を指定します。
- max_work_group_size属性には、すべてのランタイム作業グループサイズ要求に対応する最小の作業グループサイズを割り当てます。
実行時にデフォルトより小さいワーク・グループ・サイズを指定すると、ハードウェアが過剰に消費される可能性があります。したがって、デフォルト以外のワーク・グループ・サイズが必要な場合、 max_work_group_size属性を指定して最大ワーク・グループ・サイズを設定します。すべてのカーネル呼び出しでワーク・グループ・サイズが一定のままである場合、 reqd_work_group_size属性を含めることによって、必要なワーク・グループ・サイズを指定します。reqd_work_group_size属性は、指定したワークグループごとのWork-Itemの数を管理するために、正確なハードウェア量を割り当てるようにオフライン・コンパイラーに指示します。この割り振りにより、ハードウェア・リソースが節約され、カーネルコンピューティング・ユニットの実装効率が向上します。 reqd_work_group_size属性を指定することにより、オフライン・コンパイラーが未知のサイズの作業グループをサポートするために追加のハードウェアを実装するのを防ぐこともできます。
たとえば、次のコードでは、ワークグループの固定サイズを64個のWork-Itemに割り当てることができます。
__attribute__((reqd_work_group_size(64,1,1))) __kernel void sum (__global const float * restrict a, __global const float * restrict b, __global float * restrict answer) { size_t gid = get_global_id(0); answer[gid] = a[gid] + b[gid]; }
カーネルのベクトル化
カーネルの本体を変更せずに、Work-Itemごとに追加の実行をオフライン・コンパイラーに指示するには、カーネルコードにnum_simd_work_items属性を含めます。次のコードは、ベクトル化係数4を元のカーネルコードに適用します。
__attribute__((num_simd_work_items(4))) __attribute__((reqd_work_group_size(64,1,1))) __kernel void sum (__global const float * restrict a, __global const float * restrict b, __global float * restrict answer) { size_t gid = get_global_id(0); answer[gid] = a[gid] + b[gid]; }
num_simd_work_items属性を使用するには、reqd_work_group_size属性を使用してカーネルの必要なワーク・グループ・サイズを指定する必要があります。reqd_work_group_sizeに指定したワークグループのサイズは、num_simd_work_itemsに割り当てる値で割り切れなければなりません。上記のコード例では、カーネルの作業グループサイズは64であり、ワークグループは固定されています。各ワークグループ内で、Work-Itemは4つのSIMDベクタレーンに均等に分散されます。オフライン・コンパイラーが4つのSIMDベクタレーンを実装した後、各Work-Itemは4倍の作業を実行するようになりました。
オフライン・コンパイラーはコードをベクトル化し、メモリーアクセスを結合する可能性があります。オフライン・コンパイラーはこれらの最適化を自動的に適用するため、カーネルコードまたはホストコードを変更する必要はありません。
カーネルコードを手動でベクトル化できますが、実装するベクトル化の量を反映するように、ホスト・アプリケーションのNDRangeを調整する必要があります。次の例は、カーネル内で動作を手動で複製するときのコードの変更を示しています。
__kernel void sum (__global const float * restrict a, __global const float * restrict b, __global float * restrict answer) { size_t gid = get_global_id(0); answer[gid * 4 + 0] = a[gid * 4 + 0] + b[gid * 4 + 0]; answer[gid * 4 + 1] = a[gid * 4 + 1] + b[gid * 4 + 1]; answer[gid * 4 + 2] = a[gid * 4 + 2] + b[gid * 4 + 2]; answer[gid * 4 + 3] = a[gid * 4 + 3] + b[gid * 4 + 3]; }
この形式では、カーネルは配列aとbから4つの要素を読み出し、合計を計算し、その結果を配列のanswerに格納します 。 FPGAパイプラインはメモリー内の隣接する場所にデータをロードして格納するため、手動でオフライン・コンパイラーに4つのロードおよびストア動作の各グループを結合させることができます。
スタティック・メモリー統合
次の図は、カーネルのパフォーマンスがスタティック・メモリー統合から利益を得られる一般的なケースを示しています。
次のベクトル化されたカーネルを検討してください。
__attribute__((num_simd_work_items(4))) __attribute__((reqd_work_group_size(64,1,1))) __kernel void sum (__global const float * restrict a, __global const float * restrict b, __global float * restrict answer) { size_t gid = get_global_id(0); answer[gid] = a[gid] + b[gid]; }
OpenCL™カーネルは、メモリー内の連続した場所にアクセスする4つのロード動作を実行します。競合する場所に4回のメモリーアクセスを実行する代わりに、オフライン・コンパイラーは4つの負荷を単一のより広いベクトル負荷に統合します。この最適化により、メモリーシステムへのアクセス回数が減少し、メモリー・アクセス・パターンが改善される可能性があります。
オフライン・コンパイラーは、カーネルをベクトル化する際にスタティック・メモリーの結合を自動的に実行しますが、効率的なメモリーアクセスを確保するために、可能な場合はいつでも、広いベクトルロードとストアをOpenCLコードに使用する必要があります。スタティック・メモリー統合を手動で実装するには、コンパイル時に順次アクセスパターンを識別できるようにコードを記述する必要があります。上の図に示されている元のカーネルコードは、スタティック・メモリー結合から恩恵を受けることができます。これは、バッファーaとbのすべてのインデックスが、コンパイル時に判明しているオフセットで増分するためです。これとは対照的に、次のコードでは、スタティック・メモリーの結合を行うことはできません。
__kernel void test (__global float * restrict a, __global float * restrict b, __global float * restrict answer; __global int * restrict offsets) { size_t gid = get_global_id(0); answer[gid*4 + 0] = a[gid*4 + 0 + offsets[gid]] + b[gid*4 + 0]; answer[gid*4 + 1] = a[gid*4 + 1 + offsets[gid]] + b[gid*4 + 1]; answer[gid*4 + 2] = a[gid*4 + 2 + offsets[gid]] + b[gid*4 + 2]; answer[gid*4 + 3] = a[gid*4 + 3 + offsets[gid]] + b[gid*4 + 3]; }
値のオフセット[gid]はコンパイル時には不明です。その結果、オフライン・コンパイラーは静的に読み出しをバッファーにアクセスを合体することはできません。
複数のコンピューティング・ユニット
全体のカーネルスループットを向上させるために、FPGAのハードウェア・スケジューラーはワークグループを追加の使用可能なコンピューティング・ユニットにディスパッチします。コンピューティング・ユニットは、フル・キャパシティーに達していない限り、ワークグループ割り当てに使用できます。
各作業グループは、実行を完了するのに同じ時間がかかるものとします。オフライン・コンパイラーが2つのコンピューティング・ユニットを実装する場合、各コンピューティング・ユニットは作業グループの半分を実行します。ハードウェア・スケジューラーがワークグループをディスパッチするため、このプロセスを独自のコードで管理する必要はありません。
オフライン・コンパイラーは、カーネルの最適な計算単位数を自動的に決定しません。カーネル実装のコンピューティング・ユニットの数を増やすには、以下のコードサンプルに示すように、 num_compute_units属性を使用してオフライン・コンパイラーが作成するコンピューティング・ユニットの数を指定する必要があります。
__attribute__((num_compute_units(2))) __kernel void sum (__global const float * restrict a, __global const float * restrict b, __global float * restrict answer) { size_t gid = get_global_id(0); answer[gid] = a[gid] + b[gid]; }
コンピューティング・ユニットの数を増やすと、スループットが向上します。ただし、以下の図に示すように、コンピューティング・ユニット間のグローバルメモリー帯域幅が増加するという犠牲を払って実行します。また、ハードウェア・リソースの使用率も向上します。
コンピューティング・ユニット複製対カーネルSIMDベクトル化
num_compute_units属性とnum_simd_work_items属性の両方は、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーがカーネルを実装するために使用するハードウェアの量を増やすことによってスループットを向上させます。num_compute_units属性は、ワークグループのスケジュールを設定できるコンピューティング・ユニットの数を変更し、カーネルがグローバルメモリーにアクセスする回数も変更します。対照的に、 num_simd_work_items属性は、コンピューティング・ユニットが単一のワークグループで並列に実行できる作業量を変更します。 num_simd_work_items属性は、各SIMD ベクタレーンでコントロールロジックを共有することによって、コンピューティング・ユニットのデータパスのみを複製します。
通常、num_simd_work_items属性を使用すると、num_compute_units属性を使用して同じ目標を達成するより効率的なハードウェアにつながります。num_simd_work_items属性を使用すると、オフライン・コンパイラーでメモリーアクセスを結合することもできます。
大域メモリーと競合する複数のコンピューティング・ユニッは、望ましくないメモリー・アクセス・パターンにつながる可能性があります。num_simd_work_itemsが num_compute_units属性の代わりに属性を導入することにより、望ましくないメモリー・アクセス・パターンを変更することができます。また、num_simd_work_itemsは、潜在的に属性num_compute_unitsがオファーを属性同等のカーネル演算ユニットの重複と同じ演算スループットを提供しています。
次のような状況では、 num_simd_work_items属性をカーネルに実装することはできません。
- num_simd_work_itemsに指定する値は、 2,4,8または16ではありません。
-
reqd_work_group_sizeの値はnum_simd_work_itemsで割り切れません。
たとえば、50が4で割り切れないため、次の宣言は正しくありません。
__attribute__((num_simd_work_items(4))) __attribute__((reqd_work_group_size(50,0,0)))
- 複雑な制御フローを持つカーネル。別のWork-Itemは、(例えば、制御パスがget_global_IDまたはget_local_IDに依存)異なる制御経路をたどるするカーネルをベクトル化することはできません。
カーネルコンパイル時に、オフライン・コンパイラーは、ベクトル化最適化の実装が成功したかどうかを通知するメッセージを発行します。報告されたベクトル化係数がnum_simd_work_items属性に指定した値と一致すると、カーネルのベクトル化は成功します。
コンピューティング・ユニット複製とカーネルSIMDベクトル化の組み合わせ
num_simd_work_items属性が16に設定されたカーネルがFPGAに収まらないケースを検討してください。より狭いSIMDカーネルコンピューティング・ユニットを複製することによってカーネルを修正すると、カーネルが適合するかもしれません。コンピューティング・ユニットの数とSIMD幅の最適なバランスを決定するには、いくつかの実験が必要になるかもしれません。たとえば、4レーン幅のSIMDカーネルコンピューティング・ユニットを3回複製すると、8レーン幅のSIMDカーネルコンピューティング・ユニットを2回複製するよりもスループットが向上する可能性があります。
次のコード例は、 OpenCL™コードでnum_compute_unitsおよびnum_simd_work_items属性を組み合わせる方法を示しています。
__attribute__((num_simd_work_items(4))) __attribute__((num_compute_units(3))) __attribute__((reqd_work_group_size(8,8,1))) __kernel void matrixMult(__global float * restrict C, __global float * restrict A, . . .
下の図は、上記のカーネルのデータフローを示しています。 num_compute_unitsは、3つの複製コンピューティング・ユニットを実装します。 num_simd_work_itemsは、4つのSIMD ベクタレーンを実装しています。
リソース駆動型最適化
コンパイル中、オフライン・コンパイラーは、さまざまな組み合わせのnum_compute_unitsおよびnum_simd_work_itemsカーネル属性の複数の値を調べ、一連のヒューリスティックを適用してベースデザインを段階的に改善します。オフライン・コンパイラーは、この1組の値を実装して、毎秒実行されるWork-Itemに関してカーネルのパフォーマンスを最大化します。
分析の結果に基づいて、オフライン・コンパイラーは、Work-Itemが頻繁に実行するコードブロックを最適化します。これらのコードブロックの場合、コンパイラーは追加のハードウェア・リソースを使用してより高いスループットで実装を実現します。Work-Itemが頻繁に実行されないコードブロックの場合、コンパイラーは同じハードウェアを再使用して複数の動作を実装しようとします。
発生するハードウェア共有の量は、 sharing degreeと呼ばれます。これは、同じ計算単位内で実行されるWork-Itemによって動作が共有される回数です。Work-Itemが頻繁に実行されないコードブロックは、より高い共有度につながる可能性があります。
オフライン・コンパイラーは、カーネル宣言で指定したカーネル属性またはプラグマの値は変更しません。オフライン・コンパイラーは、不特定の属性とプラグマのみを変更します。
最適化動作
リソース駆動型最適化の例を次に示します。
- カーネルがFPGAに適合しない場合にのみ、頻繁に実行されないコードブロックのリソース共有を試みます。
オフライン・コンパイラーがFPGA内で最適化されたカーネルを識別した後、最適化を適用してパフォーマンスを向上させます。
- マルチカーネルデザインでは、最初に最小限のパフォーマンスでカーネルを改善します。
カーネルの最適化が行われる順序は、1秒あたりのWork-Item数に基づいています。これらのカーネルをそれ以上最適化できない場合、以降のカーネルはスループットの見積もりの順に改善されます。リソース駆動型最適化の間、オフライン・コンパイラーは一連の高性能候補を保持し、それぞれに増分最適化を適用しようとします。これらの最適化は一般的により効率的なハードウェア実装をもたらすので、ループアンローリングおよびSIMDベクトル化は、コンピューティング・ユニット複製よりも好ましい最適化戦略です。
- リソース駆動型最適化の間、オフライン・コンパイラーは、所定の最適化ステップのセットを反復します。
多くの場合、オフライン・コンパイラーは最適化範囲を事前に推定します。たとえば、使用可能なメモリー帯域幅に基づいてコンピューティング・ユニットの最大数を決定します。オフライン・コンパイラーが最適化を実行できない場合、そのステップをスキップして他の最適化を試みます。
制限
静的最適化にはいくつかの固有の制限があります。制御フロー分析は、コンパイル時に未知である、ホストから渡されたカーネル引数の値を仮定します。たとえば、オフライン・コンパイラーでは、境界が未知のループが1024回反復すると想定しています。これらの前提に基づいて、オフライン・コンパイラーは、作業アイテムが予測よりも頻繁に実行されるコードブロックに向けて最適化を誘導することがあります。境界が未知のループの場合、 unrollプラグマを使用してコード内のアンロール係数を指定することによって、アンローリングの量を上書きできます。ループをアンロールする必要がない場合、アンロール係数を1に指定して、ループをアンローリングしないことを指定できます。
もう1つの制限要因は、ハードウェアのコンパイルが行われる前にすべての最適化が行われることです。性能予測は、ハードウェアコンパイラーが達成する最大動作周波数を正確に捕捉しないことがあります。同様に、リソース駆動型最適化で使用される推定リソース使用率は、実際のハードウェア・リソース使用率を反映しない場合があります。
共有とベクトル化の量には範囲の制限もあります。現在、最大共有度は8であり、SIMDベクタレーンの最大数は16です。
HTMLレポートのカーネルプロパティとループアンロールステータスの確認
メモリーアクセス効率向上のための戦略
相互接続トポロジーは、共有されたグローバル、定数、およびローカル・メモリー・システムをそれらの基礎となるメモリーに接続します。相互接続はメモリーポートへのアクセスアービトレーションを含みます。
メモリーアクセスは、共有メモリーリソース(つまり、グローバル、ローカル、および定数メモリー)を競合します。 OpenCLカーネルが多数のメモリーアクセスを実行する場合、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーはメモリーアクセス要求を処理するための複雑なアービトレーション・ロジックを生成する必要があります。複雑なアービトレーションロジックにより、最大動作周波数(f max )が低下し、カーネルの性能が低下する可能性があります。
以下のセクションでは、メモリーアクセスの最適化について詳しく説明します。要約すると、グローバル・メモリー・アクセスを最小限にすることは、以下の理由から有益である。
- 一般に、OpenCLカーネルのパフォーマンスが向上すると、グローバルメモリー帯域幅要件が増加します。
- グローバルメモリーの最大帯域幅は、最大ローカルメモリー帯域幅よりもずっと小さくなります。
- FPGAの最大計算帯域幅は、グローバルメモリー帯域幅よりもはるかに大きくなります。 重要: 可能であれば、ローカル、プライベート、または定数メモリーを使用して、カーネルのメモリー帯域幅を増やしてください。
- メモリーアクセスの最適化に関する一般的なガイドライン
OpenCL™カーネルのメモリーアクセスを最適化すると、カーネル全体のパフォーマンスが向上します。 - グローバル・メモリー・アクセスの最適化
オフライン・コンパイラーは、グローバルメモリーを各外部メモリーバンクにインターリーブします。 - 定数、ローカルまたはプライベート・メモリーを使用したカーネル計算の実行
メモリーアクセス効率を最適化するには、 OpenCL™カーネルの計算を定数メモリー、ローカルメモリー、またはプライベート・メモリーで実行することによって、グローバル・メモリー・アクセスの数を最小限に抑えます。 - ローカルメモリーのバンキングによるカーネル・パフォーマンスの向上
numbanks( N )およびbankwidth( M )の高度なカーネル属性を指定すると、並列メモリーアクセス用にローカル・メモリー・バンクを構成できます。 - メモリー・レプリケーションファクタの制御によるローカルメモリーへのアクセスの最適化
メモリー複製因子を制御するには、お使いのOpenCLカーネル™のsinglepumpまたはdoublepumpカーネル属性が含まれます。 - ループパイプラインのメモリー依存性の最小化
インテル® FPGA SDK for OpenCL™ Offline Compiler は、同じスレッドからのメモリーアクセスがプログラムの命令に従うことを保証します。 NDRangeカーネルをコンパイルするときは、障壁を使用して同じワークグループ内のスレッド間でメモリーアクセスを同期させます。
メモリーアクセスの最適化に関する一般的なガイドライン
可能であれば、メモリーアクセスを最適化するための以下の手法を実装することを検討してください。
-
OpenCLプログラムに一対のカーネルがある場合、一方はデータを生成し、もう一方はそのデータを消費します。両方の機能を実行する単一のカーネルにそれらを変換します。また、元の2つのカーネルの関数を論理的に分離するヘルパー関数を実装します。
FPGAの実装は、別々の小さなカーネルに比べて大きなカーネルを優先します。カーネル統一により、他のカーネルで同じデータをフェッチする前に、あるカーネルの結果をグローバルメモリーに一時的に書き込む必要がなくなります。
- Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは、FPGAのローカルメモリーをGPUとは非常に異なる方法で実装しています。 OpenCLカーネルにGPU固有のローカル・メモリー・バンクの競合を避けるコードが含まれている場合、オフライン・コンパイラーは可能な限り自動的にローカル・メモリー・バンクの競合を回避するハードウェアを生成するため、そのコードを削除します。
グローバル・メモリー・アクセスの最適化
ほとんどの場合、デフォルトのバースト・インターリーブ構成により、メモリーバンク間のロードバランシングが最適化されます。ただし、ロードバランシングを改善するために、2つのインターリーブされていない(および連続した)メモリーエリアとして手動でバンクを分割することが必要な場合もあります。
下の図は、バースト・インターリーブのメモリー・パーティションとインターリーブされていないメモリー・パーティションのメモリー・マッピング・パターンの違いを示しています。
連続メモリー・アクセス
次の式を検討してみましょう。
__kernel void sum ( __global const float * restrict a, __global const float * restrict b, __global float * restrict c ) { size_t gid = get_global_id(0); c[gid] = a[gid] + b[gid]; }
アレイaからのロード動作は、Work-ItemのグローバルIDの直接関数であるインデックスを使用します。配列インデックスをWork-ItemのグローバルIDに基づいて設定することにより、オフライン・コンパイラーは連続したロード動作を指示できます。これらのロード動作は、入力配列から順番にデータを検索し、必要に応じてパイプラインに読み取ったデータを送ります。次に、連続ストア動作は、計算パイプラインを出る結果の要素をグローバルメモリー内のシーケンシャルな場所に格納します。
次の図は、連続したメモリーアクセス最適化の例を示しています。
連続したロードおよびストア動作は、アクセス速度の向上およびハードウェア・リソースの必要性の低減につながるため、メモリーアクセス効率を向上させます。データは、パイプラインの計算部分に同時に出入りし、計算とメモリーアクセスの間に重複が可能です。可能な場合、グローバルメモリーにアクセスするロードおよびストア動作の連続するメモリー位置をインデックスするWork-ItemIDを使用します。グローバルメモリーへの順次アクセスは、理想的なアクセスパターンを提供するため、メモリー効率を向上させます。
グローバルメモリーの手動分割
カーネルが同じサイズの2つのバッファーをメモリーにアクセスする場合、負荷間の動的スケジューリングに関係なく、両方のメモリーバンクに同時にデータを分散できます。この最適化手順は、見かけ上のメモリー帯域幅を増加させる可能性があります。
異種メモリーバッファー
FPGAボードが異機種グローバル・メモリー・タイプを提供している場合、様々な効率で異なるメモリー・アクセスを処理することに注意してください。
例:
- 長いシーケンシャルアクセスにはDDR SDRAMを使用してください。
- ランダムアクセスにはQDR SDRAMを使用してください。
- ランダムな低レイテンシーアクセスには内蔵RAMを使用してください。
グローバルメモリーにバッファーを割り当てる方法と、異機種バッファーを使用するようにホスト・アプリケーションを変更する方法については、 インテル® FPGA SDK for OpenCL™ プログラミング・ガイドのグローバルメモリーのバッファーエリアの指定およびグローバルメモリーの手動パーティショニングのためのOpenCLバッファーの割り当てを参照してください。
定数、ローカルまたはプライベート・メモリーを使用したカーネル計算の実行
グローバル・メモリー・アクセスを最小限に抑えるには、まずグローバルメモリーから定数、ローカル、またはプライベート・メモリーへの計算グループからデータをプリロードする必要があります。プリロードされたデータに対してカーネル計算を実行し、その結果をグローバルメモリーに書き戻します。
キャッシュ・メモリー
デフォルトでは、定数キャッシュサイズは16 KBです。定数キャッシュサイズを指定するには、 -const-cache-bytes = <N>aocコマンドのオプション<N>バイト単位の一定のキャッシュサイズです。
長いメモリーレイテンシーを許容するための余分なハードウェアを持つグローバル・メモリー・アクセスとは異なり、定数キャッシュはキャッシュミスに対して大きなパフォーマンス上の不利益を被ります。 OpenCL™カーネルコードの__constant引数がキャッシュに収まらない場合、代わりに__global const引数を使用するとパフォーマンスが向上する可能性があります。ホスト・アプリケーションが定数キャッシュに既にロードされている定数メモリーに書き込む場合、キャッシュされたデータは定数キャッシュから破棄されます(つまり、無効化される)。
-const-cache-bytes=<N> のオプションについて詳しくは インテル® FPGA SDK for OpenCL™ プログラミング・ガイドの定数メモリーキャッシュサイズの設定 (-const-cache-bytes=<N>)のセクションを参照してください。
ローカルメモリーへのデータの事前ロード
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは、FPGAのオンチップメモリーブロックにOpenCL™ローカルメモリーを実装しています。オンチップメモリーブロックには2つの読み出しポートと書き込みポートがあり、OpenCLカーネルの動作周波数の2倍の動作周波数でクロックすることができます。このクロック周波数を2倍にすることで、メモリーを二重ポンプ」にすることができ、同じメモリーから2倍の帯域幅が得られます。その結果、各オンチップメモリーブロックは最大4つの同時アクセスをサポートします。
理想的には、各バンクへのアクセスは、バンクのオンチップメモリーブロックにわたって均一に分散される。 1クロックサイクルでオンチップメモリーブロックへの同時アクセスは4回しかできないため、アクセスを分散することでバンクの競合を回避できます。
このバンキング設定は通常有効です。ただし、オフライン・コンパイラーは、多数のバンクに対応するために複雑なメモリーシステムを作成する必要があります。多数のバンクが調停ネットワークを複雑にし、システム全体のパフォーマンスを低下させる可能性があります。
オフライン・コンパイラーはFPGAのオンチップメモリーブロックにあるローカルメモリーを実装するため、オフライン・コンパイラーはコンパイル時にローカル・メモリー・システムのサイズを選択する必要があります。オフライン・コンパイラーがローカル・メモリー・システムのサイズを決定する方法は、OpenCLコードで使用されるローカルデータ型によって異なります。
ローカルメモリーアクセスの最適化
ローカルメモリーアクセスの効率を最適化するには、次のガイドラインを考慮してください。
- ループアンローリングなどの特定の最適化手法を実装すると、より多くの同時メモリーアクセスが発生する可能性があります。 注意:メモリーアクセスの数を増やすと、メモリーシステムが複雑になり、パフォーマンスが低下する可能性があります。
- 可能であれば、カーネル内の固有のローカルメモリーアクセス数を4以下に制限することで、ローカルメモリーサブシステムを単純化してください。
ローカル・メモリー・システムへのメモリーアクセスが4つ以下の場合、最大のローカルメモリーパフォーマンスを達成します。特定のメモリーシステムへのアクセス回数が4より大きい場合、オフライン・コンパイラーは、メモリーシステムのオンチップメモリーブロックをバンク構成に配置します。
- 関数スコープのローカルデータがある場合、オフライン・コンパイラーは、コンパイル時に関数本体内で定義したローカルデータの静的なサイズを指定します。ローカルメモリーを定義するには、オフライン・コンパイラーにメモリーを必要なサイズに設定し、2の累乗に最も近い値に切り上げます。
-
__localカーネル引数を指すポインターの場合、ホストはclSetKernelArg呼び出しによって実行時に動的にメモリーサイズを割り当てます。ただし、オフライン・コンパイラーはコンパイル時にこれらの物理メモリーサイズを設定する必要があります。
デフォルトでは、 __localカーネル引数のポインターは16 KBです。割り当てサイズは、ポインター宣言にlocal_mem_size属性を含めることで指定できます。
注: clSetKernelArg呼び出しは、コンパイル時に物理的に割り振られたデータサイズよりも小さいデータサイズを要求できますが、決してそれより大きいサイズにはなりません。 - ローカルメモリーにアクセスする場合、可能な限り単純なアドレス計算を使用し、必須ではないポインター演算を避けてください。
インテル® は、オフライン・コンパイラーが静的コード解析を通じてアクセスパターンをより確実に保証できるようにすることにより、FPGAリソースの使用率を削減し、ローカルメモリー効率を向上させるために、このコーディング・スタイルを推奨しています。複雑なアドレス計算とポインター演算を使用すると、オフライン・コンパイラーがデータの異なる部分を表す独立したメモリーシステムを作成するのを防ぐことができ、エリア使用量が増加し、実行時パフォーマンスが低下します。
- 可能であれば、メモリーへのポインターの格納は避けてください。記憶されたポインターは、しばしばポインターがメモリーから引き出されたときに、アクセスされたデータセットを静的なコンパイラー分析が決定することを防止します。メモリーへのポインターの格納は、ほとんど常に最適ではないエリアとパフォーマンスの結果につながります。
local_mem_size属性の使用法については、 インテル® FPGA SDK for OpenCL™ プログラミング・ガイドのローカルメモリーのポインターサイズの指定を参照してください。
プライベート・メモリーに変数と配列の格納
レジスターを使用したプライベート・メモリーの実装の詳細については、 インテル® FPGA SDK for OpenCL™ プログラミング・ガイドのレジスターの推論のセクションを参照してください。
ローカルメモリーのバンキングによるカーネル・パフォーマンスの向上
次のコード例は、単一のバンクに実装された8 x 4ローカル・メモリー・システムを示しています。その結果、システム内の2つの要素に並列にアクセスすることはできません。
local int lmem[8][4]; #pragma unroll for(int i = 0; i<4; i+=2) { lmem[i][x] = …; }
パフォーマンスを向上させるために、 numbanks( N )とbankwidth( M )をコードに追加して、メモリーバンクの数とバンク幅をバイト単位で定義することができます。次のコードは、それぞれ16バイト幅の8つのメモリーバンクを実装しています。このメモリーバンク構成により、8×4アレイのパラレルメモリーアクセスが可能になります。
local int __attribute__((numbanks(8), bankwidth(16))) lmem[8][4]; #pragma unroll for (int i = 0; i < 4; i+=2) { lmem[i][x & 0x3] = …; }
パラレルアクセスをイネーブルするには、下位の配列インデックスの動的アクセスをマスクする必要があります。下位の配列インデックスに動的アクセスをマスクすると、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラー xはインデックスの下限を超えません。
numbanks( N )およびbankwidth( M )カーネル属性に異なる値を指定することで、パラレル・アクセス・パターンを変更できます。次のコードは、それぞれ4バイト幅の4つのメモリーバンクを実装しています。このメモリーバンク構成は、8×4アレイ全体にわたる並列メモリーアクセスをイネーブルします。
local int __attribute__((numbanks(4), bankwidth(4))) lmem[8][4]; #pragma unroll for (int i = 0; i < 4; i+=2) { lmem[x][i] = …; }
アレイインデックスに基づくローカル・メモリー・バンクの幾何学的構成の最適化
次のコード例は、 numbanksおよびbankwidthに割り当てた値に基づいて、バンクのジオメトリがどのように変化するかを示しています。
コードの例 | バンクの幾何学 |
---|---|
local int __attribute__((numbanks(2), bankwidth(16))) lmem[2][4]; |
|
local int __attribute__((numbanks(2), bankwidth(8))) lmem[2][4]; |
|
local int __attribute__((numbanks(2), bankwidth(4))) lmem[2][4]; |
|
local int __attribute__((numbanks(4), bankwidth(8))) lmem[2][4]; |
|
local int __attribute__((numbanks(4), bankwidth(4))) lmem[2][4]; |
メモリー・レプリケーションファクタの制御によるローカルメモリーへのアクセスの最適化
インテル® M20Kメモリーブロックには2つのphysicalポートがあります。各M20Kブロックで使用できる論理ポートの数は、ポンピングの程度によって異なります。ポンピングは、他のデザインと比較してM20Kブロックのクロック周波数の尺度です。
カーネルがローカル・メモリー・システムlmemのための3つの読み出しポートと1つの書き込みポートを指定するデザイン例を検討してください。以下のコード例に示すように、ローカル変数宣言のsinglepumpカーネル属性を含めて、M20Kブロックは残りのデザインと同じ頻度で実行されます。
int __attribute__((memory, numbanks(1), bankwidth(64), singlepump, numreadports(3), numwriteports(1))) lmem[16];
各シングルポンプM20Kブロックには2つのlogicaポートがあります。ローカル・メモリー・システムの各書き込みポートは、デザインがメモリーシステムを実装するために使用するすべてのM20Kブロックに接続する必要があります。ローカル・メモリー・システムの各読み出しポートは、1つのM20Kブロックに接続する必要があります。これらの接続の制約のため、 lmemに指定されたポート数を実装するには、3つのM20Kブロックが必要です 。
ローカル変数宣言にdoublepumpカーネル属性を含める場合、残りのデザインと同じ頻度でM20Kメモリーブロックを実行するように指定します。
int __attribute__((memory, numbanks(1), bankwidth(64), doublepump, numreadports(3), numwriteports(1))) lmem[16];
各ダブルポンプM20Kブロックには4つの論理ポートがあります。そのため、3つの読み出しポートと1つの書き込みポートをすべてlmemに実装するには、1つのM20Kブロックが必要です 。
- メモリーをダブルポンピングすると、リソースのオーバーヘッドが増加します。実際にM20Kを節約したり、パフォーマンスを向上させたり、その両方を達成した場合にのみ、 doublepumpカーネル属性を使用してください。
- ストアはすべてのレプリケートに接続されている必要があり、競合に遭ってはいけません。したがって、ストアが3つ以上ある場合、メモリーは複製されません。ローカルメモリー・レプリケーションは、単一のストアでうまく動作します。
- メモリーシステム全体が複製されるため、潜在的に大きなM20Kメモリーブロックが観察されることがあります。
ループパイプラインのメモリー依存性の最小化
ループの依存関係は、メモリーアクセスに関連するレイテンシーのためにSingle Work-Itemカーネルにボトルネックを招く可能性があります。オフライン・コンパイラーは、依存メモリー動作が完了するまでメモリー動作を延期します。これは、ループ開始間隔(II)に影響を与える可能性があります。オフライン・コンパイラーは、最適化レポートのメモリー依存性を示します。
- オフライン・コンパイラーが誤った依存関係を想定していないことを確認します。 スタティック・メモリー依存分析が依存関係が存在しないことを証明できない場合、オフライン・コンパイラーは依存関係が存在するとみなし、依存関係を強制するようにカーネル実行を変更します。メモリーシステムがストールフリーであれば、依存関係の影響は小さくなります。
- ロード・ストア・ユニットに対するデータ依存性を伴う読出し動作後の書込みは、わずか2クロック・サイクル(II = 2)が必要です。他のストールフリーのシナリオでは、最大7クロックサイクルかかることがあります。
- 読み出し後書き込み(制御依存)動作は、オフライン・コンパイラーによって完全に解決できます。
- 依存関係がないと確信できる場合、カーネルコードのループの前に#pragma ivdep行を追加してスタティック・メモリー依存解析を無効にします。
FPGAエリアの最適化のための戦略
カーネルのパフォーマンスを最適化するには、通常、追加のFPGAリソースが必要です。これとは対照的に、エリアの最適化はパフォーマンスの低下を招くことがあります。カーネル最適化の間、 インテル® は、最適なサイズとパフォーマンスのトレードオフを生成するカーネル・プログラミング戦略を決定するために、FPGAボード上で複数のバージョンのカーネルを実行することを推奨します。
コンパイルに関する考慮事項
- 画面上の推定資源使用量の要約を確認するには、 aocコマンドに-reportフラグを含めてカーネルをコンパイルします。カーネル固有のエリア使用量の情報を確認するには、 <your_kernel_filename> /reports/report.htmlファイル。
- 可能であれば、 aocコマンドの-fpcまたは-fp-relaxedオプションを使用してOpenCLカーネルをコンパイルし、浮動小数点計算を実行します。
-report 、 -fp-relaxedおよび-fpcオプションの詳細な使用方法については、 見積もりリソース使用量の要約を表示する(-report)」 、 浮動小数点演算の順序を緩和する(-fp-relaxed)」 、および 浮動小数点数を減らす動作(-fpc)のセクション インテル® FPGA SDK for OpenCL™ プログラミング・ガイド
浮動小数点演算の詳細については、浮動小数点演算の最適化を参照してください。
ボードバリアントの選択に関する考慮事項
たとえば、カーネルに1つの外部メモリーバンクが必要な場合、単一の外部メモリーバンクのみをサポートするボードバリアントをターゲットにします。複数の外部メモリーバンクを持つボードをターゲットにすると、カーネルのエリア使用量が不必要に増加します。
カスタム・プラットフォームでニーズに合ったボード・バリエーションが提供されない場合、ボード・バリエーションの作成を検討してください。詳細については、 カスタム・プラットフォームツールキットユーザーガイドを参照してください。
メモリーアクセスに関する考慮事項
- 外部メモリーへのアクセスポイントの数を最小限に抑えます。
可能であれば、ある場所から入力を読み出し、内部的にデータを処理し、出力を別の場所に書き込むようにカーネルを構造化します。
- ローカルまたはグローバル・メモリー・アクセスに頼るのではなく、可能であればシフトレジスター推論を使ってカーネルをSingle Work-Itemとして構造化してください。
- 外部メモリーにデータを書き込むカーネルと外部メモリーからデータを読み込むカーネルを作成する代わりに、直接データ転送のためにカーネル間に インテル® FPGA SDK for OpenCL™ チャネル拡張を実装します。
- OpenCLアプリケーションは、多くの独立した定数データアクセスが含まれている場合、代わりに__global constの__constant使用して対応するポインターを宣言します。 __global constを使用する宣言は、ロードまたはストア動作ごとにプライベート・キャッシュを作成します。一方、 __constantを使用した宣言では、単一の定数キャッシュがチップ上にのみ作成されます。 注意:カーネルがCyclone® Vデバイス(たとえば、Cyclone V SoC)をターゲットにしている場合、 __constantポインターカーネル引数を宣言すると、FPGAのパフォーマンスが低下する可能性があります。
- カーネルが少数の定数引数を渡した場合、それらをグローバルメモリーへのポインターではなく値として渡します。
例えば、代わり* COEF __constant INTを通過した後、10にインデックス0を有するCOEFを間接参照の値(INT16のCOEF)としてCOEFを渡します。 coefが__constantポインターの唯一の引数だった場合、それを値として渡すと、定数キャッシュとそれに対応するロードとストア動作が完全に削除されます。
- パイプライン・ループ内で大規模なシフトレジスターを条件付きでシフトすると、効率の悪いハードウェアが作成されます。たとえば、 if(K> 5)条件が存在する場合 、次のカーネルはより多くのリソースを消費します。
#define SHIFT_REG_LEN 1024 __kernel void bad_shift_reg (__global int * restrict src, __global int * restrict dst, int K) { float shift_reg[SHIFT_REG_LEN]; int sum = 0; for (unsigned i = 0; i < K; i++) { sum += shift_reg[0]; shift_reg[SHIFT_REG_LEN-1] = src[i]; // This condition will cause sever area bloat. if (K > 5) { #pragma unroll for (int m = 0; m < SHIFT_REG_LEN-1 ; m++) { shift_reg[m] = shift_reg[m + 1]; } } dst[i] = sum; } }
重要: 条件付きでシフトレジスターにアクセスしても、ハードウェアの効率は低下しません。 カーネルに大きなシフトレジスターの条件付きシフトを実装する必要がある場合は、ローカルメモリーを使用するようにコードを変更することを検討してください。
算術演算の考慮事項
- 必要なときにだけ浮動小数点演算を導入します。
-
Intel® FPGA SDK for OpenCL™オフライン・コンパイラーは、浮動小数点定数はデータ型を2倍にするようにデフォルト設定されています。 f指定を定数に追加して、単精度浮動小数点演算にします。
例えば、算術演算sin(1.0)は、倍精度浮動小数点サイン関数を表します。算術演算sin(1.0f)は、単精度浮動小数点正弦関数を表します。
- 複雑な関数に対して完全精度の結果を必要としない場合、より簡単な算術演算を計算して結果を近似します。以下のシナリオ例を検討してください。
- 関数pow(x、n)を計算する代わりに、 nが小さい値の場合、ハードウェア・リソースと面積が大幅に少なくて済むので、繰り返し二乗演算を実行して結果を近似します。
- 近似を使用して結果を計算すると、エリアの使用量が過剰になることがあるため、元のエリアと近似されたエリアの使用を認識していることを確認してください。たとえば、 sqrt関数はリソースを消費しません。大まかな近似以外に、実行時にホストが計算しなければならない算術演算でsqrt関数を置き換えると、エリアの使用量が大きくなる可能性があります。
- 少数の入力値で作業する場合、代わりにLUTを使用することを検討してください。
- コンパイル時にオフライン・コンパイラーが計算する定数(たとえばlog(PI/2.0) )を使用してカーネルが複雑な算術演算を実行する場合、代わりにホスト上で算術演算を実行し、ランタイムでその結果を引数としてカーネルに渡します。
データ型の選択に関する考慮事項
- アプリケーションに最適なデータ型を選択します。
たとえば、データ型shortが十分な場合、変数をfloatとして定義しないでください。
- 算術式の両側が同じデータ型に属していることを確認してください。
算術式の一方の側が浮動小数点値で、もう一方の側が整数である例を検討してください。一致しないデータ型は、 Intel® FPGA SDK for OpenCL™オフライン・コンパイラーが暗黙的な変換演算子を作成します。変換演算子は、多数存在する場合は高価になります。
- パディングがデータ構造内に存在する場合、パディングを使用します。
たとえば、 float4と同じサイズのfloat3データ型のみが必要な場合、データ型をfloat4に変更して、余分なディメンションを使用して関連のない値を使用することができます。
追加情報
改訂履歴
日付 | バージョン | 変更内容 |
---|---|---|
2017年12月 | 2017.12.08 |
|
2017年11月 | 2017.11.06 |
|
2017年5月 | 2017.05.08 |
|
2016年12月 | 2016.12.02 | 微細な編集上の更新。 |
2016年10月 | 2016.10.31 |
|
2016年5月 | 2016.05.02 |
|
2015年11月 | 2015.11.02 |
|
2015年5月 | 15.0.0 |
|
2014年12月 | 14.1.0 |
|
2014年6月 | 14.0.0 |
|
2013年12月 | 13.1.1 |
|
2013年11月 | 13.1.0 |
|
2013年6月 | 13.0 SP1.0 |
|
2013年5月 | 13.0.1 |
|
2013年5月 | 13.0.0 |
|
2012年11月 | 12.1.0 | 初版。 |