パフォーマンスの最適化

ホスト最適化

このセクションでは、ホスト プログラムの最適化について説明し、OpenCL™ API を使用して、個別の計算ユニットの実行および FPGA とのデータ転送をスケジューリングします。データ転送および計算呼び出しを最適化するには、OpenCL コマンド キューを使用したタスクの並行実行について考慮する必要があります。このセクションでは、よくあるミスと、それらの見つけ方および解決方法を説明します。

カーネルのキュー追加のオーバーヘッドの削減

OpenCL API 実行モデルでは、データ並列とタスク並列のプログラミング モデルがサポートされます。OpenCL ホストは通常、異なるカーネルを複数回呼び出す必要があります。これらの呼び出しは、特定のシーケンスまたは順不同コマンド キューのいずれかでコマンド キューに入れられます。このあと、計算リソースとタスク データがどれだけ使用可能かによって、デバイス上での実行がスケジュールされます。

カーネル呼び出しは、clEnqueueTask を使用してコマンド キューで実行されるようにキューに入ります。送信プロセスがホスト プロセッサで実行されます。送信元が、カーネル引数をデバイス上で実行されているアクセラレータに転送した後、カーネル実行を呼び出します。送信元は、下位レベルのザイリンクス ランタイム (XRT) ライブラリを使用して、カーネル引数を転送し、計算を開始するためのトリガー コマンドを発行します。アクセラレータへのコマンドおよび引数の送信のオーバーヘッドは、カーネルの引数セットの数によって 30 µs ~ 60 µs になります。このオーバーヘッドの影響は、カーネルを実行する必要のある回数と clEnqueueTask への呼び出しを最低限に抑えると減らすことができます。理想的なのは、すべての計算が clEnqueueTask の呼び出し 1 つで終了するようにすることです。

データをバッチ処理してカーネルを 1 回呼び出すと、clEnqueueTask への呼び出しを最小限に抑えることができます。ループは元のインプリメンテーションにラップされ、複数のエンキュー呼び出しのオーバーヘッドを回避できます。また、多数の小さなデータ パケットではなく、少数の大きなデータ パケットを転送することで、ホストとアクセラレータ間のデータ転送パフォーマンスを向上させることもできます。カーネル実行のオーバーヘッド削減の詳細は、カーネル実行 を参照してください。

次の例は、指定された作業またはデータサイズを処理する単純なカーネルを示しています。
#define SIZE 256
extern "C" {
    void add(int *a , int *b, int inc){
        int buff_a[SIZE];
        for(int i=0;i<size;i++)
        {
            buff_a[i] = a[i];
        }
        for(int i=0;i<size;i++)
        {
            b[i] = a[i]+inc;
        }
    }
}
次の例は、バッチ データを処理するように最適化された同じ単純なカーネルを示しています。num_batches 引数によっては、カーネルは 1 回の呼び出しで 256 のサイズの入力を複数処理し、複数の clEnqueueTask 呼び出しのオーバーヘッドを回避できます。ホスト アプリケーションは、データとバッファーを SIZE * num_batches のチャンク単位で割り当てるように変更し、メモリ割り当てとホスト グローバル メモリおよびデバイス メモリ間のデータ転送をバッチ処理します。
#define SIZE 256
extern "C" {
    void add(int *a , int *b, int inc, int num_batches){
        int buff_a[SIZE];
        for(int j=0;j<num_batches;j++)
        {
            for(int i=0;i<size;i++)
            {
                buff_a[i] = a[i];
            }
            for(int i=0;i<size;i++)
            {
                b[i] = a[i]+inc;
            }
       }   
    }
}

データ移動の最適化

1: データ移動の最適化フロー

OpenCL 実行モデルでは、すべてのデータがまずホスト メイン メモリからグローバル デバイス メモリに転送されて、グローバル メモリからカーネルに転送されて計算されます。この計算結果はカーネルからグローバル メモリに戻され、最後にグローバル デバイス メモリからホスト メモリに転送されます。カーネルのデータ移動最適化のストラテジを決定する際は、異なるメモリ レベル間でデータを効率的に移動する方法を理解し、すべてのメモリ インタフェースで帯域幅を効率的に使用できるようにすることが重要です。

注記: 計算を最適化する前に、アプリケーション内のデータ移動を最適化します。

データ移動の最適化では、計算が非効率であるとデータ移動がストールすることがあるので、データ転送コードを計算コードと分離することが重要です。この最適化手順では、ホストとカーネル コードのデータ転送ロジックの変更に焦点を当てる必要があります。目標は、データ転送の帯域幅とデバイス グローバル メモリの帯域幅が最大限に使用されるようにして、システム レベルのデータ スループットを最大にすることです。最適なパフォーマンスを達成するには、通常ソフトウェア エミュレーション、ハードウェア エミュレーション、およびハードウェアでの実行を何度か繰り返す必要があります。

データ転送とカーネル計算のオーバーラップ

データベース分析のようなアプリケーションでは、アクセラレーション デバイスで使用可能なグローバル デバイス メモリよりも大きなデータ セットが使用され、データ全体をブロック単位で転送して処理する必要があります。これらのアプリケーションで優れたパフォーマンスを達成するには、データ転送と計算をオーバーラップさせる手法が必要となります。

この例は、GitHub の Vitis Accelerated Exampleshost カテゴリにある overlap 例からの vadd カーネルのものです。この例は、アプリケーションでホスト (CPU) と FPGA の計算をオーバーラップさせる手法を示します。カーネルは 2 つの配列をまとめて追加し、出力に書き込んでいます。この例では、ホストで実行する必要のあるタスクは次の 4 つです。

  1. バッファー a の書き込み (Wa)
  2. バッファー b の書き込み (Wb)
  3. vadd カーネルの実行
  4. バッファー c の読み出し (Rc)

データ転送の最適化をせずに単純な順番どおりのコマンド キューを使用すると、実行タイムライン全体のトレースは次のようになります。

2: タスクのホスト ビュー

順不同コマンド キューを使用すると、次の図に示すように、データ転送とカーネル実行をオーバーラップできます。この例のホスト コードでは、カーネルが 1 セットのバッファーを処理している間に、ホストでもう 1 つのバッファーのセットを処理できるように、すべてのバッファーにダブル バッファリングが使用されます。

OpenCL event オブジェクトを使用すると、複雑な操作依存を簡単に設定して、ホスト スレッドとデバイス動作を同期できます。イベントは、操作のステータスを調べるための OpenCL オブジェクトです。イベント オブジェクトは、readwrite、およびメモリ オブジェクトの copy コマンドで作成されるか、clCreateUserEvent を使用して作成されたユーザー イベントです。

これらのコマンドで返されるイベントをクエリすることにより、操作が完了したかどうかを確認できます。次の図の矢印は、最適なパフォーマンスを達成するために、イベント トリガーをどのように設定できるかを示しています。

3: イベント トリガー設定

例では、ホスト コード (host.cpp) は、ループ内の 4 つのタスクをエン キューしてデータ セット全体を処理します。また、各タスクのデータ依存が満たされるように、異なるタスク間のイベント同期を設定します。ダブル バッファリングは、異なるメモリ オブジェクト値を clEnqueueMigrateMemObjects API に渡すことにより設定します。イベント同期は、各 API 呼び出しがほかのイベントを待ち、その API が終了してからそれ自身のイベントをトリガーするようにすると達成できます。

次に示す [Application Timeline] ビューでは、計算ユニット vadd_1 が継続的に実行されており、データ転送時間は完全に隠されています。

4: データ転送時間が隠された [Application Timeline] ビュー

バッファー メモリの分割

メモリ バッファーの割り当ておよび割り当て解除により、DDR コントローラーでメモリが分割されることがあります。これにより、計算ユニットが論理的には並列実行できるはずなのに、最適なパフォーマンスが得られない可能性があります。

この問題は、異なる計算ユニットに対して複数の pthread が使用されており、スレッドでカーネルがエンキューされるたびに異なるサイズのデバイス バッファーが多数割り当てられ、解放される場合によく発生します。この場合、タイムライン トレースでカーネル実行間にギャップが表示され、プロセスがスリープ状態になっているように見えます。

ランタイムで割り当てられる各バッファーは、ハードウェアで連続している必要があります。大型メモリの場合、多数のバッファーの割り当ておよび割り当て解除が発生すると、その空間が空くのを待機する時間が長くなることがあります。これは、デバイス バッファーを割り当て、カーネルの異なるエンキュー間で再利用すると回避できます。

計算ユニットのスケジューリング

スケジューリング カーネルの動作は、全体的なシステム パフォーマンスに大きく影響します。これは、複数の計算ユニット (同じカーネルまたは別のカーネルのもの) をインプリメントする場合などは、さらに重要となってきます。このセクションでは、カーネルのスケジューリングに関連するさまざまなコマンド キューについて説明します。

複数の順序どおりのコマンド キュー

次の図に、2 つの順序どおりのコマンド キュー (CQ0 および CQ1) の例を示します。スケジューラは各キューからのコマンドを順序どおりに実行しますが、CQ0 および CQ1 からのコマンドはどの順序でも取り出すことができます。必要な場合は、CQ0 および CQ1 間の同期を管理する必要があります。

5: 2 つの順序どおりのコマンド キューの例

次は、concurrent_kernel_execution_c 例の host.cpp からのコードで、複数の順序どおりのコマンド キューを設定し、各キューにコマンドをエンキューしています。

   OCL_CHECK(
        err,
        cl::CommandQueue ooo_queue(context,
                                   device,
                                   CL_QUEUE_PROFILING_ENABLE |
                                       CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE,
                                   &err));
...
    printf("[OOO Queue]: Enqueueing scale kernel\n");
    OCL_CHECK(
        err,
        err = ooo_queue.enqueueTask(
            kernel_mscale,nullptr,&ooo_events[0]));
    set_callback(ooo_events[0], "scale");
...
    // This is an out of order queue, events can be executed in any order. Since
    // this call depends on the results of the previous call we must pass the
    // event object from the previous call to this kernel's event wait list.
    printf("[OOO Queue]: Enqueueing addition kernel (Depends on scale)\n");
    kernel_wait_events.resize(0);
    kernel_wait_events.push_back(ooo_events[0]);
    OCL_CHECK(err,
              err = ooo_queue.enqueueTask(
                  kernel_madd,
                  &kernel_wait_events, // Event from previous call
                  &ooo_events[1]));
    set_callback(ooo_events[1], "addition");
...
    // This call does not depend on previous calls so we are passing nullptr
    // into the event wait list. The runtime should schedule this kernel in
    // parallel to the previous calls.
    printf("[OOO Queue]: Enqueueing matrix multiplication kernel\n");
    OCL_CHECK(err,
              err = ooo_queue.enqueueTask(
                  kernel_mmult,
                  nullptr,
                  &ooo_events[2]));
    set_callback(ooo_events[2], "matrix multiplication");

1 つの順不同コマンド キュー

次の図に、1 つの順不同コマンド キューの例を示します。スケジューラは、キューからのコマンドをどの順序でも実行できます。必要に応じて、ユーザーがイベントの依存性と同期を手動で定義する必要があります。

6: 1 つの順不同コマンド キュー

次は、concurrent_kernel_execution_c 例の host.cpp からのコードで、1 つの順不同コマンド キューを設定し、必要に応じてコマンドをエンキューしています。

    OCL_CHECK(
        err,
        cl::CommandQueue ooo_queue(context,
                                   device,
                                   CL_QUEUE_PROFILING_ENABLE |
                                       CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE,
                                   &err));
...
    printf("[OOO Queue]: Enqueueing scale kernel\n");
    OCL_CHECK(
        err,
        err = ooo_queue.enqueueTask(
            kernel_mscale,nullptr, &ooo_events[0]));
    set_callback(ooo_events[0], "scale");
...
    // This is an out of order queue, events can be executed in any order. Since
    // this call depends on the results of the previous call we must pass the
    // event object from the previous call to this kernel's event wait list.
    printf("[OOO Queue]: Enqueueing addition kernel (Depends on scale)\n");
    kernel_wait_events.resize(0);
    kernel_wait_events.push_back(ooo_events[0]);
    OCL_CHECK(err,
              err = ooo_queue.enqueueTask(
                  kernel_madd,
                  &kernel_wait_events, // Event from previous call
                  &ooo_events[1]));
    set_callback(ooo_events[1], "addition");
    // This call does not depend on previous calls so we are passing nullptr
    // into the event wait list. The runtime should schedule this kernel in
    // parallel to the previous calls.
    printf("[OOO Queue]: Enqueueing matrix multiplication kernel\n");
    OCL_CHECK(err,
              err = ooo_queue.enqueueTask(
                  kernel_mmult,
                  nullptr,
                  &ooo_events[2]));
    set_callback(ooo_events[2], "matrix multiplication");

[Application Timeline] ビューには、複数の順番キュー手法と単一の順不同キュー手法の両方を使用して、計算ユニット mmult_1 が計算ユニット mscale_1 および madd_1 と並列で実行されているとことが示されます。

7: mult_1 が mscale_1 と madd_1 と並列実行されていることを示す [Application Timeline] ビュー

カーネル最適化

FPGA を使用する利点の 1 つは、特定のアルゴリズム用にカスタマイズしたデザインを作成できる柔軟性と機能です。これにより、アルゴリズムのスループットと消費電力をトレードオフするさまざまなインプリメンテーションを使用できます。次のガイドラインを使用すると、デザインの複雑性を制御して、必要なデザイン目標を達成しやすくなります。

カーネル計算の最適化

8: カーネル計算最適化のフロー

カーネル最適化は、カーネル インターフェイスにデータが到達したらすぐにすべてのデータを消費できるプロセッシング ロジックを作成することを目的として実行されます。主要なメトリクスは、開始間隔 (II) と呼ばれる、カーネルが新しい入力データを受信できるようになるまでのクロック サイクル数です。II の最適化は、これは通常、関数のパイプライン処理、ループ展開、配列分割、データフローなどの手法を使用してデータパスを一致させるようにプロセッシング コードを展開することによって達成されます。

インターフェイス属性 (詳細なカーネル トレース)

詳細なカーネル トレースには、AXI トランザクションおよびそのプロパティが表示されます。AXI トランザクションは、グローバル メモリ側と AXI インターコネクトのカーネル側 ([Kernel "pass" 1:1:1]) に対して表示されます。次の図に、新しくアクセラレーションされたアルゴリズムの典型的なカーネル トレースを示します。

9: アクセラレーションされたアルゴリズムのカーネル トレース

パフォーマンスに関して注目すべきフィールドは、次のとおりです。

[Burst Length]
1 つのトランザクションで送信されるパッケージ数を示します。
[Burst Size]
1 つのパッケージの一部として転送されるバイト数を示します。

たとえば、[Burst Length] が 1 でパッケージごとに 4 バイトだとすると、ある程度の量のデータを転送するのに個別の AXI トランザクションが多く必要になります。

注記: Vitis コア開発キットでは、サイズが 4 バイト未満のバーストは、それより小さいデータが送信される場合でも作成されません。この場合、AXI バーストをイネーブルせずに連続のアイテムにアクセスすると、同じアドレスに対して複数の AXI 読み出しが見られることがあります。

そのため、バースト長が短く、バースト サイズが 512 ビットよりもかなり小さい場合、インターフェイス パフォーマンスを最適化できる可能性があります。

バースト データ転送の使用

データをバースト転送すると、メモリ アクセスのレイテンシは表示されず、帯域幅の使用およびメモリ コントローラーの効率が改善されます。HLS レポートでバースト情報も確認してください。

注記: バースト転送は、連続したアドレス位置からの連続するデータ要求から推論されます。詳細は、バースト読み出しおよび書き込み を参照してください。

バースト転送が発生すると、詳細なカーネル トレースに表示されるバースト率とバースト長の値が大きくなります。

10: 詳細なカーネル トレースを使用したバースト データ転送

上の図では、AXI インターコネクトの後のメモリ データ転送も異なる方法でインプリメントされているのがわかります (トランザクション時間が短縮)。これらのトランザクション上にカーソルを置くと、AXI インターコネクトが 16 x 4 バイトのトランザクションを 1 つの 1 x 64 バイトのパッケージ トランザクションにパックしたことがわかります。この方が、AXI4 帯域幅がより効率的に使用されます。次のセクションでは、この最適化手法について詳細に説明します。

バースト インターフェイスはコーディング スタイルとアクセス パターンによって大きく異なります。ただし、次のコード例に示すように、データ転送と計算を分離すると、バースト検出が容易になり、パフォーマンスが改善します。

void kernel(T in[1024], T out[1024]) {
    T tmpIn[1024];
    T tmpOu[1024];
    read(in, tmpIn);
    process(tmpIn, tmpOut);
    write(tmpOut, out);
}

つまり、read 関数が AXI 入力から内部変数 (tmpIn) に読み込みをします。計算は、内部変数 tmpIn および tmpOut で動作する process 関数でインプリメントされます。write 関数は生成された出力を取り込んで AXI 出力に書き出しますバーストの詳細は、『Vitis 高位合成ユーザー ガイド』 (UG1399: 英語版日本語版) を参照してください。

計算結果からの read および write 関数を分離すると、次のようになります。

  • 読み出し/書き込み関数の制御構造 (ループ) がシンプルになり、バースト検出がシンプルになります。
  • AXI インターフェイスから計算関数を分離すると、可能なカーネル最適化が単純になります。詳細は、カーネル最適化 を参照してください。
  • 内部変数はオンチップ メモリにマップされるので、AXI トランザクションよりも高速にアクセスできます。Vitis コア開発キットでサポートされるアクセラレーション プラットフォームには最大 10 MB のオンチップ メモリがあり、パイプ、ローカル メモリ、およびプライベート メモリとして使用できます。これらのリソースを効率的に使用することで、アプリケーションの効率およびパフォーマンスを大幅に向上できます。

全 AXI データ幅の使用

Vitis コンパイラでは、カーネル引数のデータ型に基づいてカーネルおよびメモリ コントローラー間のユーザー データ幅を設定できます。ザイリンクスでは、データ スループットを最大にするため、ユーザーがメモリ コントローラーの全データ幅にマップされるデータ型を選択することをお勧めします。サポートされるアクセラレーション カードすべてのメモリ コントローラーで 512 ビットのユーザー インターフェイスがサポートされており、C/C++ 任意精度データ型 ap_int<512>int16 などの OpenCL ベクター データ型にマップできます。

メモリ インターフェイスの幅に関する考慮事項 に示すように、デフォルトでは、Vitis HLS ツールでカーネル インターフェイス ポートのサイズが最大 512 ビットまで変更され、バースト アクセスを向上するようになっています。次の図では、バースト AXI トランザクション (バースト長 16) およびパッケージ サイズ 512 ビット (バースト サイズ 64 バイト) です。

11: バースト AXI トランザクション

この例は、AXI データ幅を最大にした良いインターフェイス設定と、実際のバースト トランザクションを示しています。

インターフェイスを宣言するのに使用される複雑な構造体またはクラスがあると、メモリ レイアウトやデータ パッケージの違いにより、ハードウェア インターフェイスが複雑になることがあります。これにより、複雑なシステムでデバッグするのが困難な問題が発生する可能性があります。

注記: カーネル引数には、32 ビット境界にパック可能なシンプルな構造体を使用することをお勧めします。構造体の使用に推奨される方法は、GitHub のザイリンクス オンボーディング例kernel_to_gmem カテゴリの Custom Data Type Example を参照してください。

カーネル間通信の最適化

ストリームを介して通信するハードウェア アクセラレータ パイプラインのサポートは、FPGA および FPGA ベースの SoC の主な利点の 1 つであり、通信システムだけでなく DSP および画像処理アプリケーションでも使用されています。カーネル間 (K2K) のストリーミング データ転送 で説明するように、AXI4-Stream インターフェイスを使用すると、データを外部メモリなしで 1 つのカーネルから別のカーネルにストリーミングでき、全体的なシステム レイテンシを大幅に向上できます。

ストリーミングに使用されるカーネル ポートはそのカーネル内で定義されるので、ホスト プログラムでは呼び出されません。データは、転送されて処理される前に、グローバルメモリに戻す必要はありません。計算ユニット間のストリーミング接続の指定 で説明するように、カーネル間の接続は v++ リンク プロセス中に直接定義されます。

メモリ アーキテクチャの最適化

メモリ アーキテクチャはインプリメンテーションの重要な側面です。帯域幅のアクセスには制限があり、次の図に示すように全体的なパフォーマンスに大きく影響することがあります。


void run (ap_uint<16> in[256][4],
          ap_uint<16> out[256]
         ) {
  ...
  ap_uint<16> inMem[256][4];
  ap_uint<16> outMem[256];

  ... Preprocess input to local memory
  
  for( int j=0; j<256; j++) {
    #pragma HLS PIPELINE OFF
    ap_uint<16> sum = 0;
    for( int i = 0; i<4; i++) {

      sum += inMem[j][i];
    }
    outMem[j] = sum;
  } 

  ... Postprocess write local memory to output
}

このコードでは、2 次元入力配列の内部次元に関連する 4 つの値が追加されます。これ以上変更をしないでインプリメントすると、次のような見積もりになります。

12: パフォーマンス見積もり

全体的なレイテンシが 4608 (Loop 2) なのは、18 サイクル (内部ループ 16 サイクル + 合計のリセット + 書き出される出力) が 256 回反復されているからです。これは、HLS プロジェクトのスケジュール ビューアーで確認できます。見積もりは、内部ループを展開すると大幅に改善されます。

13: パフォーマンス見積もり

ただし、このように改善されるのは、主にプロセスがデュアル ポート メモリの両方のポートを使用しているからです。これは、プロジェクトのスケジュール ビューアーから確認できます。

14: スケジュール ビューアー

メモリからのすべての値にアクセスして合計を計算するために、2 つの読み出しがサイクルごとに実行されています。これにより、メモリへのアクセスが完全にブロックされてしまうので、望ましくない結果になることがあります。結果をさらに改善するには、2 番目の次元を使用してメモリを 4 つの小型メモリに分割します。

#pragma HLS ARRAY_PARTITION variable=inMem complete dim=2

詳細は、pragma HLS array_partition を参照してください。

これにより、4 つの配列読み出しになり、すべてが 1 つのポートを使用して異なるメモリで実行されます。

15: 4 つの配列の実行結果

Loop 2 に合計 256 x 4 サイクル = 1024 サイクルが使用されます。

16: パフォーマンス見積もり

または、メモリを 4 ワードの 1 つのメモリに再形成します。これには、次のプラグマを使用します。

#pragma HLS array_reshape variable=inMem complete dim=2

詳細は、pragma HLS array_reshape を参照してください。

これにより、配列分割と同じレイテンシになりますが、この場合は 1 つのポートを使用した 1 つのメモリになります。

17: レイテンシ結果

どちらのソリューションでも全体的なレイテンシおよび使用量は同じようになりますが、配列を再形成した方がインターフェイスがきれいで、配線の密集は少なくなります。

注記: これで配列最適化は終了です。実際のデザインでは、ループを並列処理するとレイテンシがさらに改善できることがあります (ループの並列処理 を参照)。
void run (ap_uint<16> in[256][4],
	  ap_uint<16> out[256]
	  ) {
  ...

  ap_uint<16> inMem[256][4];
  ap_uint<16> outMem[256];
  #pragma HLS array_reshape variable=inMem complete dim=2
  
  ... Preprocess input to local memory
  
  for( int j=0; j<256; j++) {
    #pragma HLS PIPELINE OFF
    ap_uint<16> sum = 0;
    for( int i = 0; i<4; i++) {
      #pragma HLS UNROLL
      sum += inMem[j][i];
    }
    outMem[j] = sum;
  } 

  ... Postprocess write local memory to output

}

計算の並列処理の最適化

デフォルトでは、C/C++ ではアルゴリズムは常に順に実行されるので、計算の並列処理は記述できません。FPGA のような完全にコンフィギャラブルな計算エンジンは柔軟性が高いので、計算の並列処理を試してみることができます。

データ並列処理のコード記述

アルゴリズムの FPGA へのインプリメンテーションで計算の並列処理を活用するには、まずソース コードの計算の並列処理が合成ツールで認識されるようにする必要があります。ループおよび関数は、ソース記述で計算の並列処理および計算ユニットを反映する主な候補ですが、ソース コードの構造によっては Vitis テクノロジで必要な変換を適用できないことがあるので、インプリメンテーションで計算の並列処理の利点が活かされているかどうかを検証することが重要です。

計算の並列処理には、ソース コードに反映されないものもあるので、ソース コードに追加する必要があることもあります。たとえば、カーネルは 1 つの入力値に演算を実行するよう記述されているのに、FPGA インプリメンテーションでは計算が効率的に複数の値に対して並列に実行されるようになることがあります。このような並列モデルについては、タスクの並列処理 を参照してください。

512 ビットのインターフェイスは、int16 または C/C++ 任意精度データ型 ap_int<512> などの OpenCL ベクター データ型を使用して作成できます。これらのベクター型も、カーネル内のデータの並列処理をモデリングする方法として使用できます (int16 の場合は 16 個までのデータパスを並列処理可能)。ベクター型の使用に推奨される方法は、GitHub の Xilinx Getting Started Examplesvision カテゴリから Median Filter Example を参照してください。

ループの並列処理

ループは、アルゴリズム コードの繰り返しを示す基本的な C/C++/OpenCL API 手法です。次の例に、ループ構造のさまざまな側面を示します。

  for(int i = 0; i<255; i++) {
    out[i] = in[i]+in[i+1];
  }
  out[255] = in[255];

このコードは、最後の値を除き、配列のすべての値に対して実行され、2 つの連続する値を加算します。このループが記述どおりにインプリメントされると、ループの各反復に 2 サイクルかかるので、合計 510 サイクルかかります。この詳細は、HLS プロジェクトのスケジュール ビューアーで確認できます。

18: スケジュール ビューアーに表示されたインプリメント済みループ構造

Vivado 合成結果では、これは合計とレイテンシで示されます。

19: パフォーマンス見積もりの合成結果

ここで重要なのは、レイテンシ値と LUT の使用数です。たとえば、コンフィギュレーションによって、レイテンシが 511、LUT の使用数が 47 個になることがあります。これらの値は、インプリメンテーションでの選択によって大きく異なります。この例では、必要なエリアは小さいですが、レイテンシは長くなります。

ループの展開

ループを展開すると、モデルを完全に並列処理できるようになります。展開するループをマークしておくと、ツールで並列処理を最大限にできるようにインプリメンテションが作成されます。展開するループをマークするには、OpenCL ループに UNROLL 属性を指定します。

__attribute__((opencl_unroll_hint))

または、C/C++ ループで unroll プラグマを使用します。

#pragma HLS UNROLL

詳細は、ループ展開 を参照してください。

この例に適用すると、HLS プロジェクトのスケジュール ビューアーに次のように示されます。

20: スケジュール ビューアー

次の図に、パフォーマンスの見積もりを示します。

21: パフォーマンス見積もり

同じ計算を並列実行することにより、総レイテンシは大幅に改善されて 127 サイクルになり、計算ハードウェアは 4845 個の LUT に増加しています。

for ループを確認すると、各加算が前のループ反復とは完全に別なので、このアルゴリズムが 1 サイクルでインプリメントできないことがわかります。これは、out 変数にメモリ インターフェイスが使用されるからです。Vitis コア開発キットでは、配列に対してデュアル ポート メモリがデフォルトで使用されます。つまり、各サイクルでメモリに書き込むことができるのは、最大 2 つの値までだということです。このため、完全な並列インプリメンテーションを達成するには、次の例に示すように、out 変数をレジスタ内に保持する必要があります。

#pragma HLS array_partition variable= out complete dim= 0

詳細は、pragma HLS array_partition を参照してください。

この変換の結果は、スケジュール ビューアーで確認できます。

22: スケジュール ビューアーの変換結果

この場合、見積もりは次のようになります。

23: 変換後のパフォーマンス見積もり

このコードは、組み合わせ関数としてインプリメントでき、何分の 1 かのサイクルで完了できます。

ループのパイプライン処理

ループのパイプライン処理 に説明されているように、ループをパイプライン処理すると、ループの反復を時間的にオーバーラップさせることができます。反復を同時に実行できるようにすると、リソースを反復間で共有でき (リソース使用量を削減)、展開されないループと比較して実行時間が短くなります。

パイプライン処理を C/C++ でイネーブルにするには、pragma HLS pipeline を使用します。

#pragma HLS PIPELINE

OpenCL API では、xcl_pipeline_loop 属性を使用します。

__attribute__((xcl_pipeline_loop))
注記: OpenCL API では、ループのパイプライン処理に xcl_pipeline_workitems を使用する方法もあります。ワーク アイテム ループは明示的に記述されないので、これらのループをパイプライン処理するにはこの属性が必要です。
__attribute__((xcl_pipeline_workitems))

この例の場合、HLS プロジェクトのスケジュール ビューアーの表示は次のようになります。

24: スケジュール ビューアーに表示されたパイプライン処理済みのループ

全体的な見積もりは次のようになります。

25: パフォーマンス見積もり

ループの各反復のレイテンシは 2 サイクルなので、オーバーラップする反復は 1 つだけです。これにより、総レイテンシは処理前の 1/2 の 257 サイクルになります。ループ展開よりも少ないリソースでレイテンシを削減できます。

ほとんどの場合、ループのパイプライン処理だけで全体的なパフォーマンスを改善できますが、パイプライン処理がどれだけ効率的かはループの構造によって異なります。一般的な制限事項は次のとおりです。

  • メモリ ポートまたはプロセス チャネルなどのようにリソースに限りがある場合、反復のオーバーラップ (開始間隔) が制限されます。
  • ループ運搬依存 (1 つの反復で計算された変数条件が次の反復に影響する) により、パイプラインの II が増加することがあります。

これらは高位合成中にレポートされ、スケジュール ビューアーで確認できます。最高のパフォーマンスを得るには、コードを修正してこれらの制限要素を取り除くか、依存性を取り除く (配列のメモリ インプリメンテーションを再構築、依存を完全になくすなど) ようにツールに命令する必要があります。

タスクの並列処理

タスクの並列処理を使用すると、データフロー並列処理の利点を活かすことができます。ループの並列処理とは異なり、タスクの並列処理ではタスク間で発生するバッファリングの利点を活かして、全実行ユニット (タスク) を並列実行できます。

次に例を示します。

void run (ap_uint<16> in[1024],
	  ap_uint<16> out[1024]
	  ) {
  ap_uint<16> tmp[128];
  for(int i = 0; i<8; i++) {
    processA(&(in[i*128]), tmp);
    processB(tmp, &(out[i*128]));
  }
}

このコードを実行すると、processA および processB 関数が順に 128 回実行されます。ループ内の processA および processB のレイテンシが合わせて 278 だとすると、総レイテンシは次のように見積もられます。

26: パフォーマンス見積もり

余分なサイクルはループ設定が原因で、これはスケジュール ビューアーで確認できます。

C/C++ コードの場合、タスク並列処理は for ループに DATAFLOW プラグマを追加すると実行されます。

#pragma HLS DATAFLOW

OpenCL API コードでは、for ループ前に次の属性を追加します。

__attribute__ ((xcl_dataflow))

このトピックの詳細は、データフロー最適化HLS プラグマ、および OpenCL 属性 を参照してください。

HLS レポートの見積もりで示したように、タスク間にダブル (ピンポン) バッファーを使用すると、全体的なパフォーマンスを大幅に改善できます。

27: パフォーマンス見積もり

この場合、異なる反復で異なるタスクが並行実行されるので、デザインの全体的なレイテンシがほぼ 1/2 になります。各関数の処理に 139 サイクルかかり、128 反復が完全にオーバーラップしているので、総レイテンシは次のようになります。

(1x only processA + 127x both processes + 1x only processB) * 139 cycles = 17931 cycles 

タスクの並列処理は、インプリメンテーションでパフォーマンスを改善できる効果的な手法ですが、DATAFLOW プラグマを特定の任意のコード部分に適用するのがどれくらい効率的かは、状況によってかなり異なることがあります。DATAFLOW プラグマの最終的なインプリメンテーションを理解するには、個々のタスクの実行パターンを確認することが必要です。Vitis コア開発キットには、同時実行を示す Detailed Kernel Trace が提供されています。

28: 詳細なカーネル トレース

前の図に示すように、この Detailed Kernel Trace の場合、データフロー フロー ループの始点が表示されます。processA はループの最初に即座に開始し、processBprocessA の終了を待ってから最初の反復を開始します。processB がループの最初の反復を完了している間に、processA は 2 回目の反復の演算を開始します。

この情報のより抽象的な表示は、ホストとデバイスのアクティビティの アプリケーション タイムライン に示されます。

デバイス リソースの最適化

データ幅

パフォーマンスに関しては、インプリメンテーションに必要なデータ幅が重要な要素の 1 つです。ツールはアルゴリズム全体にポート幅を伝搬します。アルゴリズム記述から開始した場合は特に、C/C++/OpenCL コードで、デザインのポートにも、整数型などの大型データ型のみが使用されることがあります。ただし、アルゴリズムが完全にコンフィギャラブルなインプリメンテーションにマップされていくと、10 ビットまたは 12 ビットなどのより小型のデータ型で十分なこともあります。このため、最適化中に HLS 合成レポートで基本的な演算のサイズを確認することをお勧めします。

通常は、Vitis コア開発キットでアルゴリズムを FPGA にマップする際、C/C++/OpenCL API 構造を理解て動作依存を抽出するため、多くの処理が必要になります。このマップを実行するため、Vitis 開発キットによりソース コードが演算ユニットに分割され、FPGA にマップされます。これらの演算ユニット (ops) の数およびサイズには、さまざまな要素が影響します。

次の図では、基本的な演算とそのビット幅がレポートされています。

29: 演算の使用量の見積もり

アルゴリズム記述でよく使用される典型的なビット幅 (16、32、64 ビット) を探して、C/C++/OpenCL API ソースからの関連する演算にこれほどのビット幅が本当に必要なのかを検証します。演算が小さいほど計算時間も短くなるので、これによりアルゴリズムのインプリメンテーションが大幅に改善する可能性があります。

固定小数点の演算

アプリケーションによっては、ほかのハードウェア アーキテクチャ用に最適化されているというだけの理由で、浮動小数点計算が使用されているものがあります。深層学習のようなアプリケーションに固定小数点演算を使用すると、精度を同程度に保ちながら、消費電力とエリアを大幅に節約できます。

注記: ザイリンクスでは、浮動小数点演算を使用することを確定する前に、アプリケーションに固定小数点演算を使用することを検討してみることをお勧めします。

マクロ演算

より大きな計算エレメントについて考慮した方が良い場合もあります。ツールでは、ソース コードが残りのソース コードとは別に実行され、アルゴリズムが周囲の演算を考慮せずに FPGA にマップされます。この場合、Vitis テクノロジで演算の境界が維持され、特定のコードに対して実質的にマクロ演算が作成されます。これには、次の原則が使用されます。

  • マップ プロセスに対する演算局所性
  • 経験則のための複雑性の削減

これにより、結果が大きく異なるものになることがあります。C/C++ では、マクロ演算は #pragma HLS inline off を使用すると作成されます。 OpenCL API では、関数を定義する際に次の属性を指定しないことにより、同様のマクロ演算を生成できます。

__attribute__((always_inline))

詳細は、pragma HLS inline を参照してください。

最適化済みライブラリの使用

OpenCL 仕様には、多数の数学ビルトイン関数が含まれます。native_ 接頭辞が付いた数学ビルトイン関数はすべて 1 つまたは複数のネイティブ デバイス命令にマップされ、通常は対応する関数 (native_ 接頭語なし) よりも優れたパフォーマンスになります。これらの関数の精度と入力範囲 (場合による) は、インプリメンテーションで定義されます。Vitis テクノロジでは、これらの native_ ビルトイン関数に対して、ザイリンクス FPGA 用にエリアおよびパフォーマンスを最適済みの Vitis HLS ツールの Math ライブラリに含まれる同等の関数が使用されます。

注記: ザイリンクスでは、精度がアプリケーション要件を満たす場合は、native_ ビルトイン関数または HLS ツールの Math ライブラリを使用することをお勧めします。

Vitis HLS を使用したカーネル最適化

OpenCL または C/C++ を使用したカーネルの最適化は、Vitis コア開発キットから実行できます。このセクションで説明されているような主な最適化制約 (関数およびループのパイプライン処理、関数およびループ間で同時処理を増加するためのデータフローの適用、ループの展開など) は、Vitis HLS ツールで実行されます。

Vitis コア開発キットは、自動的に HLS ツールを呼び出しますが、GUI の解析機能を使用するには、Vitis テクノロジ内から HLS ツールを直接起動する必要があります。Vitis HLS でのカーネルのコンパイル に説明されているように、スタンドアロン モードで HLS ツールを使用すると、最適化を次のように実行できます。

  • エミュレーションを実行する必要はないので、カーネルの最適化のみに集中できます。
  • 複数のソリューションを作成し、結果を比較し、ソリューション スペースを調べて、最適なデザインを見つけることができます。
  • インタラクティブな [Analysis] パースペクティブを使用してデザイン パフォーマンスを解析できます。
重要: Vitis コア開発キットに戻すのはカーネル ソース コードのみです。最適化スペースを調べたら、すべての最適化がカーネル ソース コードに OpenCL 属性または C/C++ プラグマとして適用されるようにします。

HLS ツールをスタンドアロン モードで開くには、[Assistant] ビューでハードウェア関数オブジェクトを右クリックし、Open HLS Project をクリックします (次の図を参照)。

30: HLS プロジェクトを開く

トポロジ最適化

このセクションでは、トポロジ最適化について、複数の計算ユニットの大まかなレイアウトとインプリメンテーションに関連する属性と、パフォーマンスへの影響について説明します。

複数の計算ユニット

ターゲット デバイスで使用可能なリソースによって、同じカーネルまたは異なるカーネルの複数の計算ユニットを作成して並列で実行することで、システムの処理時間とスループットを改善できます。詳細は、複数のカーネル インスタンスの作成 を参照してください。

複数 DDR バンクの使用

Vitis テクノロジでサポートされるアクセラレーション カードには、最大 80 GB/s の生 DDR 帯域幅の 1、2、または 4 つの DDR バンクが含まれます。FPGA と DDR の間で大量のデータを移動するカーネルの場合、ザイリンクスでは Vitis コンパイラおよびランタイム ライブラリで複数の DDR バンクを使用するように指示することをお勧めします。

ホスト アプリケーションは、DDR バンクだけでなく、カーネルに直接データを転送する PLRAM にアクセスできます。この機能をイネーブルにするには、コンフィギュレーション ファイルに connnectivity.sp オプションを含め、v++ --config でそのコンフィギュレーション ファイルを指定します。この最適化の使用法の詳細は カーネル ポートのメモリへのマップ、グローバル メモリ バンクへのデータ転送に関する詳細は メモリマップド インターフェイス を参照してください。

複数の DDR バンクの利点を活かすには、ホスト コードで CL メモリ バッファーを異なるバンクに割り当て、xclbin ファイルを v++ コマンド ラインでのバンク割り当てと同じになるように設定する必要があります。

次の図に、GitHub の Vitis Examples からの Global Memory Two Banks (C) 例のブロック図を示します。この例では、カーネルの入力ポインター インターフェイスを DDR バンク 0 に、出力ポインター インターフェイスを DDR バンク 1 に接続しています。

31: グローバル メモリの 2 つのバンクの例

ホスト コードでの DDR バンクの割り当て

重要: これはオプションであり、次に説明するような特殊な場合にのみ必要となります。

Vitis ツール フローでは、カーネル ポートのメモリへのマップ に説明するように --connectivity.sp オプションを使用すると、カーネル ポートからメモリ バンクへの接続ができます。v++ で生成された xclbin には、XRT でバッファーを適切に割り当てることができるように、カーネル ポートとメモリの接続に関する情報が含まれます。ホスト コードでバッファーが作成されると、XRT は自動的に xclbin カーネルからメモリにバッファーを割り当て、内部でバッファーを管理します。1 つのカーネル ポートが複数のメモリ バンクに接続される場合、XRT は常に番号の小さいバンクから開始します。

ほとんどの場合これで問題ありませんが、場合によっては、ホスト コードでバッファーの場所 (または特殊なプロパティ) を手動で割り当てる必要があります。このため、ザイリンクスOpenCL ベンダー拡張機能には、ホスト コードでのバンク割り当てを管理する CL_MEM_XRT_PTR_XILINX というバッファー拡張機能があります。次の例は、入力および出力バッファーを DDR バンク 0 とバンク 1 に割り当てるのに必要なヘッダー ファイルとコードを示しています。

#include <CL/cl_ext.h>
…
int main(int argc, char** argv) 
{
…
    cl_mem_ext_ptr_t inExt, outExt;  // Declaring two extensions for both buffers
    inExt.flags  = 0|XCL_MEM_TOPOLOGY; // Specify Bank0 Memory for input memory
    outExt.flags = 1|XCL_MEM_TOPOLOGY; // Specify Bank1 Memory for output Memory
    inExt.obj = 0   ; outExt.obj = 0; // Setting Obj and Param to Zero
    inExt.param = 0 ; outExt.param = 0;

    int err;
    //Allocate Buffer in Bank0 of Global Memory for Input Image using Xilinx Extension
    cl_mem buffer_inImage = clCreateBuffer(world.context, CL_MEM_READ_ONLY | CL_MEM_EXT_PTR_XILINX,
            image_size_bytes, &inExt, &err);
    if (err != CL_SUCCESS){
        std::cout << "Error: Failed to allocate device Memory" << std::endl;
        return EXIT_FAILURE;
    }
    //Allocate Buffer in Bank1 of Global Memory for Input Image using Xilinx Extension
    cl_mem buffer_outImage = clCreateBuffer(world.context, CL_MEM_WRITE_ONLY | CL_MEM_EXT_PTR_XILINX,
            image_size_bytes, &outExt, NULL);
    if (err != CL_SUCCESS){
        std::cout << "Error: Failed to allocate device Memory" << std::endl;
        return EXIT_FAILURE;
    }
…
}

cl_mem_ext_ptr_t 拡張ポインターは次のように定義される struct です。

typedef struct{
    unsigned flags;
    void *obj;
    void *param;
  } cl_mem_ext_ptr_t;
  • flags の有効な値は、次のとおりです。
    • XCL_MEM_DDR_BANK0
    • XCL_MEM_DDR_BANK1
    • XCL_MEM_DDR_BANK2
    • XCL_MEM_DDR_BANK3
    • <id> | XCL_MEM_TOPOLOGY
      注記: <id> は xxx.xclbin ファイルの次に生成される xxx.xclbin.info ファイルの [Memory Configuration] セクションから判断されます。xxx.xclbin.info ファイルには、グローバル メモリ (DDR、HBM、PLRAM など) が <id> を示すインデックスと共にリストされます。
  • obj: CL_MEM_USE_HOST_PTR フラグが clCreateBuffer API に渡された場合に CL メモリ バッファーに割り当てられるホスト メモリに関連付けられているポインターです。それ以外の場合は NULL に設定されます。
  • param: 今後の使用のために予約されています。常に 0 または NULL に設定します。

拡張ポインターは、次のような特殊なケースで使用できます。

P2P バッファー
説明と例については、https://xilinx.github.io/XRT/master/html/p2p.html を参照してください
ホスト メモリ バッファー
説明と例については、https://xilinx.github.io/XRT/master/html/sb.html を参照してください
カーネル ポートが複数のバンクに接続される場合、特定のバンクにホスト バッファーを割り当てます。
たとえば、DDR[0:1] のようにします。このユース ケースについては、Vitis チュートリアルのアクセラレーション FPGA アプリケーションの最適化: ブルーム フィルターの例「複数 DDR バンクの使用」を参照してください。
特定のバンクへのホスト バッファーの割り当ての例

前述の 3 番目のケースのように cl_mem_ext_ptr_t を使用する必要があるのは、たとえばホストとカーネルが同時に DDR バンクにアクセスしていて、カーネルとホストがメモリ バンクにピンポン形式でアクセスできるようにデータを分割する場合です。ホストが特定のメモリ バンクに書き込み/ 読み出しをする際に、カーネルは別のバンクから書き込み/読み出しをしているので、これらのホスト/カーネル アクセスが競合したり、パフォーマンスに影響を与えることはありません。このような場合、バッファー割り当てを自分で管理する必要があります。

xclbin のカーネルポートは、DDR バンク 1 およびバンク 2 に接続され、これらのバンクからデータを読み取ることもできます。接続は、Vitis コンパイラが --connectivity.sp オプションを使用してリンクするときにされます。

[connectivity]
sp=runOnfpga_1.input_words:DDR[1:2]

ホスト コードから、input_words データを DDR バンク 1 および 2 に送信することもできます。次のコード例に示すように、2 つのザイリンクス拡張ポインタ (cl_mem_ext_ptr_t) オブジェクトが作成されます。このオブジェクト フラグにより、各バッファーが割り当てられる DDR バンクが決まり、カーネルがアクセスできるようになります。カーネル引数は、連続するカーネル エンキューに対して input_words[0]input_words[1] に設定できます。

#include <CL/cl_ext.h>
…
int main(int argc, char** argv) 
{
cl_mem_ext_ptr_t buffer_words_ext[2];

buffer_words_ext[0].flags = 1 | XCL_MEM_TOPOLOGY; // DDR[1]
buffer_words_ext[0].param = 0;
buffer_words_ext[0].obj   = input_doc_words;
buffer_words_ext[1].flags = 2 | XCL_MEM_TOPOLOGY; // DDR[2]
buffer_words_ext[1].param = 0;
buffer_words_ext[1].obj   = input_doc_words;
…

カーネル コードのためのグローバル メモリの割り当て

複数の AXI インターフェイスの作成

OpenCL カーネル、C/C++ カーネル、および RTL カーネルでは、AXI インターフェイスへの関数パラメーターの割り当て方法はそれぞれ異なります。

  • OpenCL カーネルでは、カーネル引数の各グローバル ポインターに AXI4 インターフェイスを 1 つ生成するのに --max_memory_ports オプションが必要です。AXI4 インターフェイス名は、引数リストのグローバル ポインターの順序に基づいて付けられます。

    次のコード例は、GitHub の Vitis Accel Examplesocl_kernels カテゴリにある gmem_2banks_ocl 例からのものです。

    __kernel __attribute__ ((reqd_work_group_size(1, 1, 1)))
    void apply_watermark(__global const TYPE * __restrict input, 
    __global TYPE * __restrict output, int width, int height) {
     ...
    }

    この例では、1 つ目のグローバル ポインター inputAXI4M_AXI_GMEM0 を、2 つ目のグローバル ポインター outputM_AXI_GMEM1 を割り当てています。

  • C/C++ カーネルでは、異なるグローバル ポインターの HLS INTERFACE プラグマに異なる bundle 名を指定することで、複数の AXI4 インターフェイスが生成されます。詳細は、カーネル インターフェイス を参照してください。

    次の gmem_2banks からのコード例では、input ポインターをバンドル gmem0 に、output ポインターをバンドル gmem1 に割り当てています。バンドル名には任意の有効な C 文字列を使用でき、生成される AXI4 インターフェイス名は M_AXI_<bundle_name> になります。この例の場合、入力ポインターの AXI4 インターフェイス名は M_AXI_gmem0、出力ポインター名は M_AXI_gmem1 になります。詳細は、pragma HLS interface を参照してください。
    #pragma HLS INTERFACE m_axi port=input  offset=slave bundle=gmem0
    #pragma HLS INTERFACE m_axi port=output offset=slave bundle=gmem1
    
  • RTL カーネルでは、RTL Kernel ウィザードでのインポート プロセスでポート名が生成されます。RTL Kernel ウィザードで付けられるデフォルト名は m00_axi および m01_axi です。変更しない場合は、これらの名前をコンフィギュレーション ファイルの connectivity.sp オプションで DDR バンクを割り当てるときに使用する必要があります。詳細は、カーネル ポートのメモリへのマップ を参照してください。
DDR バンクへの AXI インターフェイスの割り当て
重要: ザイリンクスでは、DDR インターフェイスを複数使用する場合は、各カーネル/CU に DDR メモリ バンクを指定し、カーネルを配置する SLR を配置することを要件としています。詳細は、カーネル ポートのメモリへのマップ および 計算ユニットの SLR への割り当て を参照してください。

次に、connectivity.sp オプションを指定する設定ファイルの例と、入力ポインター (M_AXI_GMEM0) を DDR バンク 0 に、出力ポインター (M_AXI_GMEM1) を DDR バンク 1 に接続する v++ のコマンド ライン例を示します。

config_sp.cfg ファイル:

[connectivity] 
sp=apply_watermark_1.m_axi_gmem0:DDR[0] 
sp=apply_watermark_1.m_axi_gmem1:DDR[1]

v++ コマンド ライン:

v++ apply_watermark --config config_sp.cfg

[Device Hardware Transaction] ビューを使用すると、実際の DDR バンクの通信を確認して DDR の使用を解析できます。

32: [Device Hardware Transaction] ビューの DDR バンクのトランザクション
PLRAM への AXI インターフェイスの割り当て

一部のプラットフォームでは、PLRAM がサポートされます。その場合、DDR バンクへの AXI インターフェイスの割り当て で説明されているのと同じ --connectivity.sp オプションを使用しますが、名前は PLRAM[id] を使用します。特定のプラットフォームでサポートされる有効な名前は、xclbin と共に生成される xclibin.info ファイルの [Memory Configuration] セクションに表示されます。

カーネル SLR および DDR メモリの割り当て

デザインの周波数およびリソース要件を満たすには、カーネル計算ユニット (CU) インスタンスおよび DDR メモリ リソースのフロアプランが重要となります。フロアプランでは、CU (カーネル インスタンス) を明示的に SLR に割り当てたり、CU を DDR メモリ リソースにマップしたりします。フロアプランする際、CU のリソース使用率と DDR メモリの帯域幅の要件を考慮してください。

最大のザイリンクス FPGA は複数のスタックド シリコン ダイで構成されています。各スタックは SLR (Super Logic Region) と呼ばれ、DDR インターフェイスなど、決まった量のリソースおよびメモリが含まれます。カスタム ロジックに使用可能なデバイス SLR リソースについては、Vitis ソフトウェア プラットフォームのリリース ノート を参照してください。または、platforminfo ユーティリティ で説明する platforminfo ユーティリティを使用して表示することもできます。

実際のカーネル リソース使用率を使用して CU を複数の SLR に分配すると、特定の SLR での密集を削減できます。システム見積もりレポートでは、デザイン サイクルの早期に、カーネルで使用される多くのリソース (LUT、フリップフロップ、BRAM など) を確認できます。このレポートは、コマンド ラインまたは GUI を使用して、ハードウェア エミュレーションおよびシステム コンパイル中に生成できます。詳細は、システム見積もりレポート を参照してください。

この情報と使用可能な SLR リソースの情報を使用して、1 つの SLR が過剰に使用されないように、CU を SLR に割り当てます。SLR の密集が少ないほど、ツールでデザインを FPGA リソースに適切にマップしやすくなり、パフォーマンス ターゲットを満たすことができます。メモリ リソースと CU のマップについては カーネル ポートのメモリへのマップ および 計算ユニットの SLR への割り当て を参照してください。

注記: 計算ユニットは使用可能な DDR メモリ リソースのいずれにでも接続できますが、SLR に割り当てる際は、カーネルの帯域幅要件を考慮する必要もあります。

CU を SLR に割り当てたら、CU マスター AXI ポートを DDR メモリ リソースにマップします。ザイリンクスでは、CU と同じ SLR にある DDR メモリ リソースに接続することをお勧めします。そのようにすると、数が決まっている SLR をまたぐ接続リソースの競合を削減できます。また、SLR 間の接続には SLL (Super Long Line) 配線リソースが使用されるので、標準の SLR 内の配線よりも遅延が大きくなります。

SLR 領域をまたいで別の SLR にある DDR リソースに接続することが必要なことはありますが、connectivity.sp および connectivity.slr 指示子の両方が明示的に指定されている場合は、ツールで自動的にクロッシング ロジックが追加され、SLL 遅延の影響を最小限に抑えて、タイミング クロージャが達成されるようになっています。

複数のメモリ バンクにアクセスするカーネルのガイドライン

DDR メモリ リソースは、プラットフォームの SLR (Super Logic Region) をまたいで分配されます。SLR をまたぐ接続の数は制限されるので、カーネルを DDR メモリ リソースとの接続数が最も多い SLR に配置するのが一般的です。これにより、SLR をまたぐ接続の競合が削減し、SLR をまたぐためにロジック リソースが消費されるのを回避できます。

33: 同じ SLR 内のカーネルおよびメモリ
注記: 左の図では、1 つの AXI インターフェイスが 1 つのメモリ バンクに接続されています。右の図では、複数の AXI インターフェイスが同じメモリ バンクに接続されています。

上の図に示すように、カーネルに 1 つの AXI インターフェイスがあり、1 つのメモリ バンクにのみマップされる場合、platforminfo ユーティリティ で説明される platforminfo ユーティリティにより、カーネルのメモリ バンクに接続されている SLR がリストされるので、これがカーネルが最適に配置される SLR です。この場合、追加の入力なしでもデザイン ツールによりカーネルが自動的にその SLR に配置される可能性はありますが、次の場合は明示的に SLR を割り当てる必要があります。

  • デザインに同じメモリ バンクにアクセスするカーネルが多数含まれる場合。
  • カーネルにメモリ バンクの SLR に含まれない特殊なロジック リソースが必要な場合。

カーネルに複数の AXI インターフェイスがあり、すべてのインターフェイスが同じメモリ バンクにアクセスする場合、1 つの AXI インターフェイスを含むカーネルと同様に処理でき、カーネルを AXI インターフェイスがマップされているメモリ バンクと同じ SLR に配置する必要があります。

34: 隣接する SLR のメモリ バンク
注記: 左の図では、カーネルが SLR0 にあり、SLR をまたぐ接続が 1 つ必要です。右の図では、カーネルがメモリ バンクにアクセスするために、SLR をまたぐ接続が 2 つ必要です。

カーネルに複数の AXI インターフェイスがあり、異なる SLR にある複数のメモリ バンクに接続されている場合は、カーネルがアクセスするメモリ バンクの大部分が含まれる SLR にカーネルを配置することをお勧めします。これにより、このカーネルで必要な SLR をまたぐ接続数が最小限に抑えられ、ユーザー デザイン内のほかのカーネルでメモリ バンクに接続するために使用可能な SLR をまたぐリソースが増えます。

カーネルが別の SLR にあるメモリ バンクに接続される場合は、カーネル SLR および DDR メモリの割り当て のように SLR の割り当てを明示的に指定します。

35: 2 つ離れた SLR のメモリ バンク
注記: 左の図では、マップされているすべてのメモリ バンクにアクセスするために SLR をまたぐ接続が 2 つ必要です。右の図では、マップされているすべてのメモリ バンクにアクセスするために SLR をまたぐ接続が 3 つ必要です。

プラットフォームに含まれる SLR が 3 つ以上になると、上の図に示すように、最も多くマップされるメモリ バンクのすぐ隣ではない SLR のメモリ バンクにカーネルがマップされることもあります。このような場合、離れたメモリ バンクにアクセスするために複数の SLR 境界をまたぐ必要があるので、SLR をまたぐリソースの使用量が増加します。このようなリソース使用量の増加を避けるには、カーネルを中央の SLR に配置して、隣接する SLR にまたぐリソースのみが使用されるようにします。