インテル®グラフィックステクノロジー、つまり、オフロード、 ターゲット(gfx)およびターゲット(gfx_kernel)属性 、 __GFX__および__INTEL_OFFLOADマクロ 、組み込み関数用のオフロードおよびoffload_attributeプラグマについての議論を続けます 。非同期オフライン用のAPI関数のセット。 それが幸せに必要なすべてです。 私はほとんど忘れていました。もちろん、Intelのコンパイラとマジックオプション/ Qoffloadが必要です。
しかし、まず最初に。 主なアイデアの1つは、CPUで実行される既存のコードを比較的簡単に変更して、プロセッサに統合されたグラフィックスで実行することです。
これは、2つの配列を合計する簡単な例を使用して最も簡単に示されます。
void vector_add(float *a, float *b, float *c){ for(int i = 0; i < N; i++) c[i] = a[i] + b[i]; return; }
インテル®Cilk™Plusテクノロジーを使用すると、 forループをcilk_forに置き換えることで簡単に並列化できます。
void vector_add(float *a, float *b, float *c){ cilk_for(int i = 0; i < N; i++) c[i] = a[i] + b[i]; return; }
さて、次のステップでは、同期モードで#pragma offloadディレクティブを使用して、すでに計算をグラフィックに送ります。
void vector_add(float *a, float *b, float *c){ #pragma offload target(gfx) pin(a, b, c:length(N)) cilk_for(int i = 0; i < N; i++) c[i] = a[i] + b[i]; return; }
または、関数の前に__declspec(ターゲット(gfx_kernel))指定子を使用して、非同期実行用のカーネルを作成します。
__declspec(target(gfx_kernel)) void vector_add(float *a, float *b, float *c){ cilk_for(int i = 0; i < N; i++) c[i] = a[i] + b[i]; return; }
ちなみに、どこにでもGFXという文字があり、統合グラフィックス( GFX-Graphics )で作業しているのではなく、個別のグラフィックスとしてよく理解されているGPUで作業していると考える必要があります。
すでに理解したように、この手順には多くの機能があります。 まあ、まず、すべてがcilk_forループでのみ機能します 。 良い仕事のためにはコードの並列バージョンが必要であることは明らかですが、これまでのところ、サポートされているのはCilkからのループを操作するためのメカニズムです。つまり、同じOpenMPがチェックアウトを実行します。 グラフィックは、64ビットの「フリート」と整数(「ハードウェア」の機能)ではうまく機能しないため、このような操作で高いパフォーマンスを待つ必要はありません。
チャートでの計算には、同期モードと非同期モードの2つの主なモードがあります。 最初の実装ではコンパイラディレクティブが使用され、2番目のAPI関数セットではオフロードの実装では、実行のためにキューに宣言された関数(カーネル)を「入れる」必要があります。
同期モード
対象のcilk_forループの前に#pragma offload target(gfx)ディレクティブを使用して実装されます。
実際のアプリケーションでは、このループに何らかの関数の呼び出しがある可能性があるため、 __declspec(target(gfx))で宣言する必要があります。
同期性は、ホスト(CPU)でコードを実行するスレッドがグラフの計算の終了を待つという事実にあります。 同時に、コンパイラーはホストとグラフィックスの両方のコードを生成します。これにより、異なるハードウェアで作業する際の柔軟性が高まります。 オフラインがサポートされていない場合、すべてのコードの実行はCPU上で発生します。 最初の投稿で、これがどのように実装されているかについてすでに話しました。
このディレクティブでは、次のパラメーターを指定できます。
- if(条件) -条件が真の場合、コードはチャートでのみ実行されます
- in | out | inout | pin(variable_list:長さ(length_variable_in_elements))
in 、 out 、またはinout -CPUとグラフィックスの間でコピーされる変数を示します - pin -CPUとグラフィックスに共通の変数を設定します。 この場合、データのコピーは行われず、使用済みのメモリは交換できません。
- ポインターを使用する場合、 長さは必要なものです。 グラフィックメモリとの間でコピーするデータのサイズを設定するか、CPUと共有する必要があります。 タイプポインターの要素数として設定します。 配列へのポインターの場合、これは配列内の対応する要素の数です。
重要な注意- ピンを使用すると、オフライン使用のオーバーヘッドを大幅に削減できます。 データを前後にコピーする代わりに、ホスト(CPU)と統合グラフィックスの両方で使用可能な物理メモリへのアクセスを整理します。 データサイズが小さい場合、大幅な増加は見られません。
OSはプロセッサのグラフィックスがメモリを使用することを認識していないため、不快な状況を避けるために、使用されているメモリページをスワップできないようにすることを明確に決定しました。 したがって、多くの「キック」をしないように注意する必要があります。そうしないと、スワップを作成できないページが多数取得されます。 当然、システム全体のパフォーマンスはこれから向上しません。
2つの配列を合計する例では、 pinパラメーター(a、b、c:length(N))を使用するだけです。
#pragma offload target(gfx) pin(a, b, c:length(N))
つまり、配列aとbはグラフィックメモリにコピーされませんが、共有メモリで使用可能なままですが、対応するページは作業が完了するまでスワップしません。
ところで、 / Qoffload-オプションは、 プラグマを無視するために使用されます 。 さて、これは突然オフロードに飽きた場合です。 ちなみに、ifdefをキャンセルした人はいなかったため、この手法は依然として非常に重要です。
#ifdef __INTEL_OFFLOAD cout << "\nThis program is built with __INTEL_OFFLOAD.\n" << "The target(gfx) code will be executed on target if it is available\n"; #else cout << "\nThis program is built without __INTEL_OFFLOAD\n"; << "The target(gfx) code will be executed on CPU only.\n"; #endif
非同期モード
次に、API関数の使用に基づく別のオフラインモードを見てみましょう。 グラフィックスには独自の実行キューがあり、必要なのはカーネル(gfx_kernel)を作成してこのキューに入れることだけです。 カーネルは、関数の前に__declspec(ターゲット(gfx_kernel))指定子を使用して作成できます。 同時に、ホスト上のスレッドがカーネルを実行のためにキューに送信すると、実行を継続します。 ただし、 _GFX_wait()関数を使用して、チャート上で実行が完了するまで待つことができます。
同期操作では、オフラインで領域に入るたびにメモリをキックし(もちろんコピーしたくない場合)、ループを終了するときにこのプロセスを停止します。 これは暗黙的に行われ、構築を必要としません。 したがって、オフロードが何らかのサイクルで実行される場合、非常に大きなオーバーヘッド(オーバーヘッド)が発生します。 非同期の場合、メモリのキックを開始するタイミングとAPI関数の使用を終了するタイミングを明示的に示すことができます。
また、非同期モードでは、ホストとグラフィックスの両方に対してコード生成は提供されません。 したがって、ホストのコードのみを自分で実装する必要があります。
非同期モードで配列の合計を計算するためのコードは次のとおりです( vec_addのコードの非同期バージョンは上記で説明しました)。
float *a = new float[TOTALSIZE]; float *b = new float[TOTALSIZE]; float *c = new float[TOTALSIZE]; float *d = new float[TOTALSIZE]; a[0:TOTALSIZE] = 1; b[0:TOTALSIZE] = 1; c[0:TOTALSIZE] = 0; d[0:TOTALSIZE] = 0; _GFX_share(a, sizeof(float)*TOTALSIZE); _GFX_share(b, sizeof(float)*TOTALSIZE); _GFX_share(c, sizeof(float)*TOTALSIZE); _GFX_share(d, sizeof(float)*TOTALSIZE); _GFX_enqueue("vec_add", c, a, b, TOTALSIZE); _GFX_enqueue("vec_add", d, c, a, TOTALSIZE); _GFX_wait(); _GFX_unshare(a); _GFX_unshare(b); _GFX_unshare(c); _GFX_unshare(d);
したがって、4つの配列を宣言して初期化します。 _GFX_share関数を使用して、このメモリ(開始アドレスとバイト単位の長さは関数パラメーターによって設定されます)をキックする必要があると明示的に言います。つまり、CPUとグラフィックスに共有メモリを使用します。 その後、 __declspec(target(gfx_kernel))を使用して定義されている目的の関数vec_addをキューに入れます。 いつものように、 cilk_forループを使用します 。 ホスト上のストリームは、最初の実行を待たずに、新しいパラメーターを持つvec_add関数の2番目の計算ミスをキューに入れます。 _GFX_waitを使用すると、すべてのコアがキューで実行されることが期待されます。 そして最後に、 _GFX_unshareを使用してメモリの固定を明示的に停止します。
API関数を使用するには、ヘッダーファイルgfx_rt.hが必要であることを忘れないでください。 さらに、 cilk_forを使用するには、 cilk / cilk.hを接続する必要があります。
興味深い点は、インストールされたコンパイラがデフォルトでgfx_rt.hを見つけることができなかったことです -パパへのパスを登録する必要がありました( C:\ Program Files(x86)\ Intel \ Composer XE 2015 \ compiler \ include \ gfx )プロジェクト設定で。
また、以前の投稿でコンパイラーによるコード生成について話したとき、話をしなかった興味深いオプションを見つけました。 そのため、使用するハードウェアの種類が事前にわかっている場合は、 / Qgpu-archオプションを使用してコンパイラーに明示的に指定できます。 これまでのところ、2つのオプションしかありません: / Qgpu-arch:ivybridgeまたは/ Qgpu-arch:haswell 。 その結果、リンカはコンパイラを呼び出して、VISAアーキテクチャから必要なものにコードを変換し、JITを節約します。
最後に、Windows 7(およびDirectX 9)のオフロードに関する重要な注意事項。 ディスプレイがアクティブになっていることが重要です。アクティブでないと機能しません。 Windows 8にはそのような制限はありません。
さて、プロセッサに統合されたグラフィックスについて話していることを思い出してください。 説明した構造は、個別のグラフィックスでは機能しません。OpenCLを使用しています。