3 つのパラダイムの組み合わせ - 2023.2 日本語

Vitis 高位合成ユーザー ガイド (UG1399)

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 日本語

関数とループには、ユーザーのプログラムのほとんどの最適化で最も注意する必要があります。今日の最適化ツールは、通常関数/プロシージャ レベルで動作します。関数は、それぞれ特定のハードウェア コンポーネントに変換できます。このようなハードウェ アコンポーネントはそれぞれクラス定義のようなものであり、このコンポーネントの多くのオブジェクト (またはインスタンス) は作成して、最終的なハードウェア デザインにインスタンシエートできるようになっています。各ハードウェア コンポーネントは、通常、加算、減算、乗算などの基本的な関数をインプリメントする、より小さな定義済みのコンポーネントで構成されます。再帰はサポートされませんが、関数はほかの関数を呼び出すことができます。小さく、あまり呼び出されない関数は、ソフトウェア関数の場合と同じようにインライン展開することもできます。この場合、関数をインプリメントするのに必要なリソースを呼び出し元の関数のコンポーネントに組み込むと、共通のリソースをより効率的に共有できることがあります。デザインを通信する関数どうしのセットとして構築すると、これらの関数を実行する際の並列処理の推論がしやすくなります。

ループは、プログラムで最も重要なコンストラクトの 1 つです。ループの本体は何度も繰り返されるため、このプロパティを使用すると、簡単により優れた並列処理を達成できます。効率的な並列実行を達成するために、ループおよびループ ネストに対して実行可能な複数の変換方法 (パイプライン処理展開など) があります。これらの変換により、メモリ システムの最適化と、マルチコアおよび SIMD 実行リソースへのマッピングの両方が可能になります。科学およびエンジニアリング アプリケーションのプログラムの多くは、大規模なデータ構造上の演算として表されます。この構造とは、配列または行列の単純な要素別演算や、ループ運搬依存のある複雑なループ ネスト (たとえばループ反復間のデータ依存など) のことです。このようなデータ依存関係は、ループで達成可能な並列処理に影響します。このような場合、最新の並列プラットフォーム上でループ反復を効率的かつ並列に実行できるように、コードを再構成する必要があることが多くあります。

次の図は、連続する A、B、C、および D という 4 つのタスク (C/C++ 関数など) の単純な例を使用して、さまざまなオーバーラップ実行を示しています。A が B と C のデータを 2 つの異なる配列で生成し、D が B と C で生成された 2 つの異なる配列のデータを消費します。この「ダイヤモンド」通信パターンは 2 回 (2 回の呼び出し) 実行され、これら 2 つの実行は独立していると想定します。

void diamond(data_t vecIn[N], data_t vecOut[N])
{
   data_t c1[N], c2[N], c3[N], c4[N];
   #pragma HLS dataflow
   A(vecIn, c1, c2);
   B(c1, c3);
   C(c2, c4);
   D(c3, c4, vecOut);
}

上記のコード例は、これらの関数がどのように呼び出されるかを示す C/C++ ソース コード部分です。タスク B と C には互いにデータ依存性はありません。次の図は、完全に連続した実行を示しており、黒い円は直列のインプリメントに使用される同期の形式を表しています。

図 1. 直列実行 - 2 回の実行

ダイヤモンドの例では、B と C は完全に独立しています。共有メモリ リソースとの通信もアクセスもないので、計算リソースの共有が不要な場合は、並列で実行できます。これにより、次の図に示すように、1 回の実行内でフォーク/ジョイン並列処理が実行されます。B と C は A の終了後に並列に実行され、D は B と C の両方を待ちますが、その次の実行は連続して実行されます。

図 2. 実行内のタスクの並列処理

このような実行は、 (A; (B || C); D); (A; (B || C); D) と表され、; は直列を、|| は完全な並列処理を表します。この形式のネストされたフォーク/ジョイン並列処理は、依存タスクのサブクラス、つまり直列/並列タスク グラフに対応します。より一般的に、依存タスクの DAG (有向非巡回グラフ) は、個別のフォーク アンド ジョイン タイプの同期を使用してインプリメントできます。これは、マルチスレッド プログラムが複数のスレッドを持つ CPU で共有メモリを使用して実行される方法とまったく同じであることに注意することも重要です。

FPGA では、ほかにどのような並列処理が可能かを試すことができます。前の実行パターンでは、呼び出し内のタスク レベルの並列処理が使用されていました。連続する実行のオーバーラップはどうでしょうか。これらは完全に独立していますが、各関数 (A、B、C、または D) が前の実行と同じ計算ハードウェアを再利用する場合でも、たとえば A の 2 回目の呼び出しが B と C の最初の呼び出しと並行して実行される場合などでも、実行する必要のあることがあります。これは、呼び出し間でのタスク レベルのパイプライン処理の方式の 1 つです。詳細は、次の図を参照してください。スループットは、遅延の合計ではなく、すべてのタスクの最大遅延によって制限されるため、改善されました。各実行のレイテンシは変わりませんが、複数実行の全体的なレイテンシは短くなります。

図 3. パイプライン処理を使用したタスク並列処理

ただし、B の 1 回目の実行が A の 1 回目の結果を入れたメモリから読み出される際に、A の 2 回目の実行が既に同じメモリに書き込まれている可能性があります。データが消費される前にデータが上書きされないようにするには、ダブル バッファリングまたは PIPO という形式のメモリ拡張を使用して、このインターリーブを実行できます。これは、タスク間の黒い円で表しています。

スループットを向上させ、計算リソースを再利用するための効率的な手法は、演算子、ループ、関数をパイプライン処理することです。各タスクがそれ自体とオーバーラップするようになった場合は、実行内でタスクの並列処理を同時に実行し、実行間でタスクのパイプライン処理を実行できます。これは、どちらもマクロ レベルの並列処理の例です。タスク内のパイプライン処理は、マイクロ レベルの並列処理の例です。実行の全体的なスループットは、最大遅延ではなく、タスク間の最小スループットに依存するため、さらに向上します。最後に、伝達されたデータがどのように同期されるかに応じて、すべてのデータが生成された (PIPO) 後またはより多くの要素を使用した方法 (FIFO) 後でのみ、実行内でさらにオーバーラップが発生する可能性があります。たとえば、次の図では、B と C の両方が先に開始され、A に対してパイプライン処理された方法で実行されます。D は B と C の完了を待つ必要があると想定されますが、A が FIFO ストリーミング アクセス (円のない行として表示) を介して B と C と通信する場合は、実行内のこの最後のタイプのオーバーラップを達成できます。同様に、チャネルが PIPO でなく FIFO であれば、D は B と C とオーバーラップすることもできます。ただし、前のすべての実行パターンとは異なり、FIFO を使用することでデッドロックが発生する可能性があるので、これらのストリーミング FIFO のサイズは正しく設定する必要があります。

図 4. タスク並列処理および実行内でのパイプライン処理、実行のパイプライン処理、およびタスク内でのパイプライン処理

まとめると、前のセクションで示した 3 つのパラダイムは、マルチスレッドや並列プログラミング言語の複雑さを必要とせずに、デザインで並列処理を実現する方法を示しています。プロデューサー/コンシューマー パラダイムをストリーミング チャネルと組み合わせることで、小規模から大規模のシステムを簡単に構成できます。前述のように、ストリーミング インターフェイスを使用すると、並列タスクだけでなく、階層データフロー ネットワークですら容易に結合できます。これは、そのような仕様をサポートするプログラミング言語 (C/C++) の柔軟性と、今日の FPGA デバイスで利用可能なヘテロジニアス コンピューティング プラットフォームにインプリメントするためのツールのおかげです。