Vitis ソフトウェア プラットフォームでのアプリケーションのアクセラレーション手法

概要

内容はデータセンター アプリケーションおよび PCIe® ベースのアクセラレーション カードに焦点を置いていますが、同じ概念は一般的なエンベデッド アプリケーションにも適用可能です。

対象者および内容

このガイドでは、システム アーキテクトおよびソフトウェア開発者を対象に、次の情報を提供します。

  • デバイス アクセラレーション アプリケーションの設計手法
  • C/C++ カーネルの開発手法
注記: このガイドでは、プロセッサ ロジック (PL) アクセラレーションについて説明します。

アクセラレーション: 工場のたとえ

CPU、GPU、およびプログラマブル デバイスの間には、明確な違いがあります。これらの違いを理解することで、それぞれのデバイス用のアプリケーションを効率的に開発し、最適なアクセラレーションを達成できます。

CPU および GPU はどちらも定義済みのアーキテクチャで、決まった数のコアおよび命令セットのほか、柔軟性の低いメモリ アーキテクチャが含まれます。GPU のパフォーマンスは、コアの数と SIMD/SIMIT の並列処理で決まります。一方、プログラマブル デバイスは完全にカスタマイズ可能なアーキテクチャです。開発者は、アプリケーションの要件に合わせて計算ユニットを作成します。計算ユニット数を増やすのではなく、データパスをパイプライン処理することによりパフォーマンスを達成します。

たとえば、CPU は熟練作業員がそれぞれ 1 人ずつ働く作業場を集めたようなもので、作業員はほとんど何でも作成できる汎用ツールを使用できます。それぞれの作業員が異なるツールを使用して、原材料から 1 つずつ完成品を仕上げていきます。この順次的なプロセスには、その仕事の特性によって、多くの手順が必要となることがあります。作業場はそれぞれ独立しており、作業員は互いに邪魔したり、問題を調整したりすることなく、個別に作業を進めます。

GPU の場合も作業員と作業場がありますが、作業員の数が多く、それぞれがより専門化している点が異なります。使用できるのは特殊なツールのみなので、できることは少なくなりますが、作業を効率的に実行できます。GPU の作業員は、小数の同じ作業を繰り返したり、全員が同じ作業を同時に実行したりする場合などに最適です。作業員が多いので、全員に同じ命令をする方が効率的です。

プログラマブル デバイスは、この作業場のたとえで言えば産業化時代への変化のようなものです。CPU と GPU が順次手順を実行して入力を出力に変換する個別の作業員の集まりであるとすると、プログラマブル デバイスは組立ラインとベルトコンベヤーのある工場のようなものです。組立ラインに配備された作業員グループが、原材料を徐々に完成品に仕上げていきます。それぞれの作業員が同じ仕事を繰り返して、ベルトコンベヤーで流れ作業をするので、製品化までの処理能力は大幅に向上します。

また、プログラマブル デバイスでは、CPU および GPU の作業場および作業員とは異なり、工場および組立ラインが実際には存在しないという点も異なります。上記のたとえを言い換えると、プログラマブル デバイスは開発待機中の空地を集めたようなものです。つまり、デバイスの開発では、汎用ツールを使用するのではなく、工場、組立ライン、ワークステーションを構築し、それらを必要な作業に合わせてカスタマイズします。敷地の大きさに限りがあるように、デバイスの不動産も無限にあるわけではないので、デバイス内で建設可能な工場の数と大きさには限りがあります。このため、デバイス プログラム プロセスではこれらの工場を正しく建設して設定することが重要となります。

従来のソフトウェア開発では定義済みのアーキテクチャに機能をプログラムしますが、プログラマブル デバイス開発では必要な機能をインプリメントするアーキテクチャをプログラムします。

設計手法の概要

この設計手法には、主に 2 つの段階があります。

  1. アプリケーションの設計
  2. C/C++ カーネルの開発

最初の段階では、どのソフトウェア関数をデバイス カーネルにマップするか、どれくらい並列処理が必要か、どのように転送するかなど、アプリケーション アーキテクチャに関する重要事項を決定します。

第 2 段階では、カーネルをインプリメントします。この段階では主に、パフォーマンス目標を達成できるようにソース コードを構築し、必要なコンパイラ プラグマを指定します。

1: 設計手法の概要

パフォーマンス最適化は、反復作業です。アクセラレーションしたアプリケーションの最初のバージョンでは、最適な結果は得られないことがほとんどです。このガイドで説明する設計手法では、パフォーマンス解析を繰り返し、インプリメンテーションのあらゆる点に変更を加えていきます。

推奨事項

この設計手法を使用してプロジェクトを開始するには、Vitis™ 統合ソフトウェア プラットフォームのプログラムと実行モデルを理解しておくことが重要です。Vitis ソフトウェア プラットフォームを使用して生産性を高めるために必要な情報は、次の資料から入手できます。

Vitis ソフトウェア プラットフォームの主要な点に加え、次のトピックを理解しておくと、この設計手法を使用して最適な結果を達成するのに役立ちます。

  • アプリケーション ドメイン
  • ソフトウェア アクセラレーションの原則
  • デバイスの概念、機能、およびアーキテクチャ
  • ターゲットのデバイス アクセラレータ カードおよび対応ターゲット プラットフォーム
  • ハードウェア インプリメンテーションの並列処理 (http://kastner.ucsd.edu/hlsbook/)

デバイス アクセラレーション アプリケーションの設計手法

アクセラレーション アプリケーションの開発を始める前に、正しく設計しておくことが重要です。この段階では、どのソフトウェア関数をデバイス カーネルにマップするか、どれくらい並列処理が必要か、どのように適用するかなど、アプリケーションのアーキテクチャに関する重要事項を決定します。

2: アプリケーションの設計手法

このセクションでは、このプロセスに関連するさまざまな手順を説明します。反復的な方法を使用することで、解析を調整して、より適した設定を見つけることができます。

手順 1: アプリケーションのパフォーマンス ベースラインと目標の設定

まず、ランタイムおよびスループット パフォーマンスを計測し、既存のプラットフォームで実行している現在のアプリケーションのボトルネックを特定します。これらのパフォーマンス数値は、アプリケーション全体だけでなく、アプリケーション内の主な関数ごとに生成する必要があります。最も効果的なのは、valgrindcallgrind および GNU gprof などのプロファイリング ツールを使用してアプリケーションを実行する方法です。これらのツールで生成されるプロファイリング データには、すべての関数呼び出しの回数と実行時間を示すコールグラフが表示されます。これらの数値は、後続のほとんどの解析プロセスのベースラインとなります。実行時間が最も長い関数は、FPGA でのアクセラレーションの良い候補となります。

実行時間の計測

実行時間の計測は、ソフトウェア開発の標準的手法です。これには、gprof などの一般的なソフトウェア プロファイリング ツールを使用するか、タイマーおよびパフォーマンス カウンターを含めてコードを記述します。

次に、gprof を使用して生成したプロファイリング レポート例を示します。このようなレポートには、関数の呼び出し回数とかかった時間 (ランタイム) が表示されます。

3: gprof 出力の例

スループットの計測

スループットとは、データの処理レートです。指定した関数のスループットを計測するには、関数が処理するデータ量を関数の実行時間で除算します。

TSW = max(VINPUT, VOUTPUT) / 実行時間

一部の関数は、あらかじめ決められた量のデータを処理します。この場合、単純なコード検査を使用してこのデータ量を決定できます。データ量が変数になっている関数の場合は、アプリケーション コードにカウンターを含め、データ量を動的に計測すると便利です。

スループットの計測は、実行時間の計測と同様に重要です。デバイス カーネルを使用すると、全体的な実行時間を短縮できますが、アプリケーション スループットにより大きく影響します。このため、スループットを主な最適化目標として考慮することが重要です。

達成可能な最大スループットの判断

ほとんどのデバイス アクセラレーション システムで達成可能な最大スループットは、PCIe® バスによって制限されます。PCIe のパフォーマンスは、マザーボード、ドライバー、ターゲット プラットフォーム、転送サイズなどのさまざまな要因の影響を受けます。xbutil dma テストのような DMA テストを前もって実行し、PCIe 転送の効率的なスループットを計測すると、アクセラレーションの上限を特定できます。

4: Alveo U200 データセンター アクセラレータ カードの dmatest のサンプル結果

アクセラレーションの目標がこのスループット上限を超える場合、システムが I/O の限界に到達してしまうので、目標を達成することはできません。カーネル パフォーマンスおよび I/O 要件を定義する際も同様に、この上限を考慮してください。

全体的なアクセラレーション目標の設定

アクセラレーション目標とベースライン パフォーマンスの比が解析と詳細設定プロセスに影響するので、アクセラレーション目標は開発の早期に決定する必要があります。

アクセラレーション目標は、ハードまたはソフトにできます。たとえば、リアルタイム ビデオ アプリケーションには、毎秒 60 フレームを処理するというハード要件があります。データ サイエンス アプリケーションには、別のインプリメンテーションよりも 10 倍高速に実行するというソフト目標を設定できます。

どちらの方法でも、達成可能な意味のあるアクセラレーション目標を設定するには、専門的な知識が重要となります。

手順 2: アクセラレーションする関数の特定

パフォーマンス ベースラインを求めたら、次にどの関数をデバイスでアクセラレーションするかを決定します。
ヒント: この時点では、既存のコードへの変更は最小限にして、FPGA で動作するデザインをすばやく生成し、パフォーマンス ベースラインとリソース数を取得するようにします。

ハードウェアでアクセラレーションする関数を選択する際、次の 2 点を考慮する必要があります。

パフォーマンス ボトルネック
アプリケーションのホット スポットにどの関数が含まれるか。
アクセラレーションの可能性
これらの関数はアクセラレーション可能か。

パフォーマンス ボトルネックの検出

純粋な順次アプリケーションの場合、プロファイリング レポートを確認すると、簡単にパフォーマンス ボトルネックを確認できますが、実際によく使用されるアプリケーションはマルチスレッドなので、パフォーマンス ボトルネックを確認するには、並列処理の影響を考慮することが重要となります。

次の図に、2 つの並列パスを使用したアプリケーションのパフォーマンス プロファイルを示します。各矩形の幅は、各関数のパフォーマンスに比例します。

5: 2 つの並列パスを使用したアプリケーション

上の図の例では、2 つのパスのうちの 1 つだけをアクセラレーションしても、アプリケーション全体のパフォーマンスは改善しません。パス A および B は再合流するので、これらの終了は互いに依存しています。同様に、A2 を 100 倍にアクセラレーションしたとしても、上のパスのパフォーマンスにはそれほど影響しません。このため、この例のパフォーマンス ボトルネックは関数 A1、B1、B2、および B3 です。

アクセラレーションの候補を探す場合は、個別の関数のパフォーマンスだけでなく、アプリケーション全体のパフォーマンスを考慮してください。

アクセラレーションの可能性の特定

ソフトウェア アプリケーションのボトルネックとなっている関数は、必ずしもデバイスで高速に実行できるとは限りません。その関数をアクセラレーションする価値があるかどうかを実際に決定するには、詳細な解析を実行する必要があります。関数をハードウェア アクセラレーションできるかどうかは、次のガイドラインを使用して判断できます。

  • 関数の計算の複雑性

    計算の複雑性は、関数を実行するのに必要な基本的な演算の数です。プログラマブル デバイスでは、並列処理を多用し、パイプラインを深くしたデータパスを作成することにより、アクセラレーションを達成できます。これらは、前述のたとえの「組立ライン」です。組立ラインが長くなり、工程が増えるほど、作業場内で手順を順次実行する作業員の場合と比較して、効率的になります。

    アクセラレーションの良い候補は、入力サンプルごとに深い演算シーケンスを実行して出力サンプルを生成する必要のある関数です。

  • 関数の計算負荷

    関数の計算負荷は、入力データおよび出力データの総量に対する演算の総数の比で表されます。計算負荷の高い関数は、アクセラレータにデータを移動するオーバーヘッドが比較的低いので、アクセラレーションの良い候補です。

  • 関数のデータ アクセス局所性プロファイル

    データの再利用、空間局所性、時間的局所性の概念は、データをアクセラレータに移動するオーバーヘッドをどの程度最適化できるかを評価するのに有益です。空間局所性は、連続するメモリ アクセス操作の平均距離を表します。時間的局所性は、プログラムの実行中にメモリの特定のアドレスに対するアクセス操作の平均数を表します。これらの値が低いほど、データをアクセラレータに簡単にキャッシュでき、グローバル メモリにアクセスする頻度を下げることができます。

  • 関数のスループットをデバイスで達成可能な最大スループットと比較

    デバイス アクセラレーション アプリケーションは、分散マルチプロセス システムです。アプリケーション全体のスループットは、最低速の関数のスループット以下になります。このボトルネックの特性はアプリケーション特定であり、I/O、計算、データの移動など、原因はさまざまです。最低速の関数のスループットを選択した関数のスループットで除算すると、達成可能な最大アクセラレーションを判断できます。

    達成可能な最大アクセラレーション = TMin / TSW

    Alveo データセンター アクセラレータ カードでは、PCIe バスがデータ転送のスループットを制限します。これがアプリケーションの実際のボトルネックになるとは限りませんが、上限となり得るので、早期の見積もりに使用できます。たとえば、PCIe のスループットが 10 GB/s で、ソフトウェアのスループットが 50 MB/s の場合、この関数の最大アクセラレーション係数は 200 倍になります。

これらの 4 つの条件を満たしていれば、必ずアクセラレーションできるというわけではありませんが、デバイスでアクセラレーションする関数を特定するのには有益です。

手順 3: デバイスで必要な並列処理の判断

アクセラレーションする関数を特定し、全体的なアクセラレーションの目標を設定したら、次にその目標を達成するにはどのレベルの並列処理が必要なのかを判断します。

前述の工場のたとえを使用して、カーネル内でどのような並列処理が可能なのかを説明します。

たとえば、組立ラインを使用すると、入力を段階的に同時処理できます。ハードウェアでは、このような並列処理は「パイプライン処理」と呼ばれます。組立ラインの工程数は、ハードウェア パイプラインの段数に相当します。

カーネル内での並列処理には、複数のサンプルを同時に処理できるという利点もあります。これは、ベルトコンベヤーに 1 つだけではなく、同時に複数サンプルを置くようなものです。これを実現するには、組立ラインで複数のサンプルを並列処理できるようにカスタマイズします。これは、カーネル内のデータパスの幅を効率的に定義することに相当します。

パフォーマンスは、組立ラインの数を増やすことでさらに向上できます。これは、工場に複数の組立ラインを設置したり、1 つまたは複数の組立ラインを含む同じ工場を複数建設したりすることで達成できます。

開発者は、どの並列手法を組み合わせるとアクセラレーションの目標を最も効率的に達成できるのかを特定する必要があります。

並列処理を使用しない場合のハードウェア スループットの見積もり

並列処理を使用しないカーネルのスループットは、次の式で見積もることができます。

THW = 周波数 / 計算負荷 = 周波数 * max(VINPUT, VOUTPUT) / VOPS

周波数」はカーネルのクロック周波数です。この値は、ターゲット アクセラレーション プラットフォームまたはターゲット プラットフォームによって決まります。たとえば、Alveo U200 データセンター アクセラレータ カードの最大カーネル クロックは 300 MHz です。

前述のとおり、関数の計算負荷は、入力データおよび出力データの総量に対する演算の総数の比で表されます。上記の式からわかるように、演算量が多く、データ量が少ない関数がアクセラレーションの良い候補です。

必要な並列処理の量を判断

上記の式を計算したら、ハードウェアとソフトウェアのパフォーマンス比の初期見積もりを求めることができます。

スピードアップ = THW/TSW = Fmax * 実行時間 /Vops

並列処理を使用しない場合、初期スピードアップは 1 未満であると予測されます。

次に、パフォーマンス目標を達成するためにどれだけの並列処理が必要かを計算します。

必要な並列処理 = TGoal / THW = TGoal * Vops / (Fmax * max(VINPUT, VOUTPUT))

並列処理は、データパスの幅を広げる、複数のエンジンを使用する、複数のカーネル インスタンスを使用するなど、さまざまな方法でインプリメントできます。この後、必要性とアプリケーションの特性にあった最適な組み合わせを決める必要があります。

データパスで並列処理が必要なサンプル数を判断

たとえば、計算をアクセラレーションするのに、データパスの幅を広くして、より多くのサンプルが並列処理されるようにする方法があります。アルゴリズムには、この方法が使えるものと使えないものがあります。アルゴリズムの特性を理解して、この方法が使えるのかどうか、使えるのであれば、パフォーマンス目標を満たすのに並列処理する必要のあるサンプル数がいくつなのかを理解しておくことが重要です。

データパスの幅を広くして、並列処理するサンプル数を多くすると、アクセラレーションされた関数のレイテンシ (実行時間) を削減できるので、パフォーマンスが向上します。

デバイスにインスタンシエート可能なカーネルの数とインスタンシエートが必要なカーネルの数を判断

データパスを並列処理できない場合 (または十分に並列処理できない場合) は、複数のカーネル インスタンスの作成 に説明されているように、カーネル インスタンスをさらに追加してみます。この方法は、通常「複数の計算ユニット (CU) の使用」と呼ばれます。

カーネル インスタンスを追加すると、次の図に示すように、より多くのターゲット関数を並列実行できるので、アプリケーションのパフォーマンスが向上し、複数のデータ セットを異なるインスタンスで同時に処理できます。ホスト アプリケーションがカーネルを継続的にビジー状態にしておくことができる場合は、アプリケーションのパフォーマンスはインスタンス数に比例して向上します。

6: 複数の計算ユニットを使用してパフォーマンスを向上

複数の計算ユニットの使用チュートリアルに説明されているように、Vitis テクノロジではインスタンスを追加することにより簡単にパフォーマンスを向上できます。

ここまでで、パフォーマンス目標を達成するためにハードウェアに必要な並列処理の量と、データ幅とカーネル インスタンスを組み合わせることにより並列処理を達成する方法について理解できたはずです。

手順 4: ソフトウェア アプリケーションで必要な並列処理の判断

ハードウェア デバイスおよびそのカーネルを並列処理できるように設計したら、この利点を活かすようにソフトウェア アプリケーションを設計する必要があります。

ソフトウェア アプリケーションを並列処理すると、ホスト アプリケーションで次を実行できるようになります。

  • デバイス カーネルがアイドル状態である時間を最小限にし、ほかのタスクを実行させる。
  • デバイス カーネルをアクティブな状態に保ち、新しい計算をできるだけ早く頻繁に実行させる。
  • デバイスとのデータ転送を最適化する。
7: ソフトウェア最適化の目標

工場および組立ラインのたとえを使用すると、ホスト アプリケーションは常に忙しく次世代の製品を計画する本社のようなもので、現世代の製品を作成するのは工場の仕事です。

本社には、工場の物品輸送や要望の伝達などを指揮する役割もあります。物流部門が原材料や作成する製品の設計図を配達できなければ、工場を多く建設する意味がありません。

デバイス カーネル実行中に CPU がアイドル状態である時間を最小限に抑える

デバイス アクセラレーションとは、ホスト プロセッサの特定の計算負荷をデバイスのカーネルに移動することです。上の図に示すように、純粋なシーケンシャル モデルでは、アプリケーションは結果が準備されて処理を再開できるようになるまで、アイドル状態で待機します。

このようなアイドル サイクルが発生しないように、ソフトウェア アプリケーションを設計します。まず、カーネルの結果に依存しないアプリケーション部分を特定し、ホストでこれらの関数をデバイスで実行されているカーネルと並行して実行できるような構造にします。

デバイス カーネルを継続的に使用する

カーネルはデバイス内に含まれるかもしれませんが、実行されるのはアプリケーションから要求があった場合のみです。最高のパフォーマンスを達成するには、カーネルを常にビジー状態にするアプリケーションを設計します。

これは概念的には、現在の要求が終了する前に次の要求を送信することで達成できます。この結果、次の図に示すように、実行がパイプライン処理されてオーバーラップするので、カーネルが最適に使用されるようになります。

8: アクセラレータのパイプライン処理

この例の場合、元のアプリケーションは func1、func2、func3 を繰り返し呼び出します。この 3 つの関数に対して、K1、K2、K3 というカーネルがそれぞれ作成されています。そのままのインプリメンテーションでは、これら 3 つのカーネルが元のソフトウェア アプリケーションと同様に順次的に実行されますが、各カーネルは 3 分の 1 の時間しかアクティブになりません。このような場合、カーネルにパイプライン処理された要求を送信できるようなソフトウェア アプリケーション構造にする方が効率的です。つまり、K2 が最初の K1 の出力を処理し始めるのと同時に、K1 が新しいデータ セットを処理し始めるようにします。この方法を使用すると、3 つのカーネルが最大の使用率で一定して実行されるようになります。

ソフトウェアのパイプライン処理の詳細は、Vitis アプリケーション アクセラレーション開発フロー チュートリアルを参照してください。

デバイスとのデータ転送を最適化

アクセラレーション アプリケーションでは、特に PCIe ベースのアプリケーションでは、データをホストからデバイスに転送する必要があります。これによりレイテンシが発生し、アプリケーションの全体的なパフォーマンスに悪影響をもたらすことがあります。

データが正しいタイミングで転送されないと、カーネルでそのデータを使用できるようになるまで待つ必要があるので、アプリケーションは遅くなります。このため、カーネルでデータが必要となる前に、データを転送しておくことが重要です。これには、デバイス カーネルを継続的に使用する で説明したように、データ転送とカーネル実行をオーバーラップさせる必要があります。上の図のシーケンスに示すように、この方法を使用すると、データ転送のレイテンシ オーバーヘッドを隠すことができ、データが準備されるのをカーネルが待つ必要がなくなります。

データ転送には、最適にサイズ指定されたバッファーを転送するという方法もあります。次の図に示すように、PCIe スループットは転送バッファー サイズによって大きく異なります。バッファーが大きいほど、スループットも良くなり、アクセラレータに常に演算する必要のあるデータが含まれるようになるので、無駄なサイクルがなくなります。通常は、データ転送を 1 MB 以上にすることをお勧めします。DMA テストを前もって実行しておくと、最適なバッファー サイズを判断するのに役立ちます。最適なバッファー サイズを判断する際は、大型バッファーを使用した場合のリソース使用率と転送レイテンシへの影響も考慮してください。

データ転送には、最適にサイズ指定されたバッファーを転送するという方法もあります。有効データ転送スループットは、転送するバッファーのサイズによって大きく異なります。バッファーが大きいほど、スループットも良くなり、アクセラレータに常に演算する必要のあるデータが含まれるようになるので、無駄なサイクルがなくなります。

次の図に示すように、PCIe ベースのシステムでは、データ転送を 1 MB 以上にすることをお勧めします。xbutil ユーティリティを使用して DMA テストを実行しておくと、最適なバッファー サイズを判断するのに役立ちます。詳細は、dmatest を参照してください。

9: バッファー サイズによる PCIe 転送のパフォーマンス

複数のデータ セットを 1 つのバッファーにまとめて、できるだけ高いスループットを達成するようにすることをお勧めします。

アプリケーション タイムラインの概念化

ここまでで、どのような関数をアクセラレーションする必要があるのか、パフォーマンス目標を達成するのにどのような並列処理が必要なのか、アプリケーションをどのように配布するのかを理解できたはずです。

この段階で、この情報を予測されるアプリケーション タイムラインとしてまとめておくと便利です。デバイス カーネルを継続的に使用する で説明したようなアプリケーション タイムライン シーケンスは、アプリケーションを実行した際のパフォーマンスおよび並列処理を表示する効果的な方法です。これにより、アーキテクチャ内に並列処理を組み込んだときに、アプリケーションがどのように動作するかがわかります。

10: アプリケーション タイムライン

Vitis ソフトウェア プラットフォームは、実際のアプリケーションの実行からタイムライン ビューを生成します。必要なタイムラインがわかっている場合は、上の図に示すように、実際の結果と比較して問題を特定し、最適な結果が得られるように反復して調整していきます。

手順 5: アーキテクチャの詳細設定

アプリケーションおよびそのカーネルの開発に進む前に、最上位の決定事項から 2 次的なアーキテクチャの詳細を決定します。

カーネル境界を設定

前述のように、パフォーマンスは複数のカーネル インスタンス (計算ユニット) を作成すると改善しますが、計算ユニット (CU) を追加すると、I/O ポート、帯域幅とリソース幅が増加します。

Vitis ソフトウェア プラットフォーム フローでは、カーネル ポートの最大幅が 512 ビット (64 バイト) で、デバイス リソースの使用量は決まっています。最も重要なのは、ターゲット プラットフォームで使用可能な最大ポート数に限りがあるということです。これらの制限を考慮して、これらのポートおよび帯域幅を最適に使用するようにします。

複数の計算ユニットを使用する代わりに、カーネル内に複数のエンジンを追加する方法もあります。この場合、複数のデータ セットがカーネル内の別のエンジンで同時に処理されるので、CU を追加する場合と同様にパフォーマンスを改善できます。

同じカーネル内に複数のエンジンを含めると、カーネルの I/O ポートの帯域幅が最大限に使用されます。データパス エンジンがポートの全幅を必要としない場合は、1 つのエンジンを使用して複数の CU を作成するよりも、カーネルにエンジンを追加する方が効率的です。

カーネル内に複数のエンジンを含めると、ポートの数およびグローバル メモリへのトランザクション (アービトレーションが必要) の数を削減して、帯域幅を改善することもできます。

ただし、これにはカーネル内で明示的な I/O 多重化動作をコード記述する必要があるので、トレードオフを考慮してください。

カーネルの配置と接続を決定

カーネルの境界を決定すると、インスタンシエートするカーネルの数が正確にわかるので、グローバル メモリ リソースに接続する必要のあるポート数もわかります。

この段階では、ターゲット プラットフォームの機能と使用可能なグローバル メモリ リソースについて理解しておくことが重要です。たとえば、Alveo™ U200 データセンター アクセラレータ カードには DDR4 の 16 GB バンクが 4 つ、PLRAM の 128 KB バンクが 3 つあり、3 つの SLR (Super Logic Region) に分配されています。詳細は、Vitis 2020.1 ソフトウェア プラットフォーム リリース ノート を参照してください。

カーネルが工場だとすると、グローバル メモリ バンクは工場を行き来する物品を保管する倉庫です。SLR は、倉庫や工場が建設される前の工業地帯のようなものです。ある工場地帯の倉庫から別の工場地帯の工場に物品を移動することはできますが、遅延や複雑性が増します。

複数の DDR を使用すると、データ転送の負荷を調整してパフォーマンスを向上できますが、各 DDR コントローラーがデバイス リソースを消費するので、コストが増加します。カーネル ポートをメモリ バンクに接続する方法を決定する際は、これらを考慮してください。カーネル ポートのグローバル メモリへのマップ で説明したように、これらの接続はコンパイラ オプションを使用して設定できるので、必要に応じて設定を簡単に変更できます。

アーキテクチャの詳細を設定すると、カーネルのインプリメントを開始して、アプリケーション全体をアセンブルするのに必要な情報がすべて揃います。

C/C++ カーネルの開発手法

Vitis ソフトウェア プラットフォームでは、C/C++ または RTL (Verilog、VHDL、SystemVerilog) のいずれかで記述されたカーネルがサポートされます。この設計手法ガイドは C/C++ カーネル用です。RTL カーネルの詳細は、RTL カーネル を参照してください。

最適なアプリケーション パフォーマンスに必要な次のカーネル要件は、アーキテクチャの定義段階で既に特定されているはずです。

  • スループット目標
  • レイテンシ目標
  • データパス幅
  • エンジン数
  • インターフェイスの帯域幅

これらの要件により、カーネル開発および最適化プロセスが決まります。全体的なアプリケーション パフォーマンスは、各カーネルが指定したスループットを満たすことにより予測されるので、カーネルのスループット目標を達成することが主な目的となります。

このため、カーネル開発手法はスループット ドリブン方法に従って、アウトサイドインで開発していきます。この手法には、次の 2 つの段階が含まれます。

  1. カーネルのマクロ アーキテクチャを定義してインプリメント
  2. カーネルのマイクロ アーキテクチャをコード記述して最適化

カーネル開発プロセスを開始する前に、機能、アルゴリズム、アーキテクチャの違いと、これらがカーネル開発プロセスにどのように関係するかを理解しておくことが重要です。

  • 機能は、入力パラメーターと出力結果の数学的な関係です。
  • アルゴリズムは、特定の機能を実行するための一連の手順です。ある機能は、異なるアルゴリズムを使用して実行できます。たとえば、並べ替えは quick sort または bubble sort アルゴリズムを使用してインプリメントできます。
  • アーキテクチャは、このコンテキストでは、アルゴリズムの基になるハードウェア インプリメンテーションの特性を意味します。たとえば特定の並べ替えアルゴリズムは、並列実行するコンパレータ、RAM またはレジスタ ベースのストレージなどでインプリメントできます。

Vitis コンパイラでは、C/C++ で記述されたアルゴリズムから最適化されたハードウェア アーキテクチャが生成されますが、特定のアルゴリズムが別のアルゴリズムに変換されることはありません。

アルゴリズムはデータ アクセスの局所性および計算の並列処理に直接影響するので、コンパイラの能力やユーザー指定のプラグマよりも、選択するアルゴリズムが達成可能なパフォーマンスに大きく関係します。

次の設計手法では、アクセラレーションする機能に適したアルゴリズムが特定されていることを想定しています。

11: カーネル開発手法

高位合成コンパイラの概要

カーネル開発プロセスを開始する前に、高位合成 (HLS) の概念を理解しておく必要があります。HLS コンパイラは、C/C++ コードをデバイス ファブリックにマップ可能な RTL デザインに変換します。

HLS コンパイラには、標準的なソフトウェア コンパイラよりも多くの制限があります。たとえば、システム関数呼び出し、ダイナミック メモリ割り当て、再帰関数など、サポートされないコンストラクトがあります。詳細は、Vitis HLS フローサポートされない C コンストラクトを参照してください。

より重要なのは、C/C++ ソース コードの構造が、生成されるハードウェア インプリメンテーションのパフォーマンスに大きく影響するということです。このガイドでは、アプリケーションのスループット目標を達成するような構造のコードを記述する方法を説明します。カーネルのプログラムの詳細は、C/C ++ カーネル を参照してください。

検証での注意点

このガイドで説明する手法は反復的に実行し、コードを継続的に修正していく必要があります。ザイリンクスでは、コードを修正するたびに検証することをお勧めします。これには、標準的なソフトウェア検証方法を使用するか、Vitis 統合デザイン環境 (IDE) のソフトウェアまたはハードウェア エミュレーション フローを使用します。どちらの場合も、適用範囲と検証の質が十分であることをテストしてください。

手順 1: コードをロード/計算/ストア パターンに分割

カーネルは、基本的には必要な機能用に最適化されたカスタム データパスと、それに関連するデータ ストレージおよびモーション ネットワークです。「メモリ アーキテクチャ」または「メモリ階層」とも呼ばれるカーネルのデータ ストレージおよびモーション ネットワークの役割りは、データをカーネルに入出力し、カスタム データパスを介してできるだけ効率的に移動することです。

カーネルがグローバル メモリにアクセスするのはコストが高く、帯域幅も限られるので、カーネルのこの部分を注意深く設計することが重要です。

そのため、カーネル開発手法の最初の手順では、カーネル コードをロード/計算/ストア パターンの構造に変更する必要があります。

これには、次を使用して最上位関数を作成します。

  • 必要なカーネル インターフェイスと同じインターフェイス パラメーター。
  • ロード、計算、ストアの 3 つのサブ関数。
  • これらの関数間でデータを転送するためのローカル配列または hls::stream 変数。
12: ロード/計算/ストア パターン

カーネル コードの構造をこのようにすると、タスク レベルのパイプライン処理 (HLS データフローとも呼ぶ) が使用できるようになります。このコンパイラ最適化により、各関数を同時に実行できるようになり、タスクを同時実行するパイプラインが作成されます。これが工場の組立ラインです。必要なスループットを達成して保持するには、この構造が重要です。HLS データフローの詳細は、データフロー最適化 を参照してください。

ロード関数は、カーネル外 (グローバル メモリなど) からカーネル内の計算関数にデータを移動します。この関数はデータ処理は実行しませんが、必要に応じて、バッファリングおよびキャッシングなどの効率的なデータ転送を実行します。

計算関数は、その名前のとおり、すべてのデータ処理を実行します。開発フローのこの段階では、計算関数の内部構造は重要ではありません。

ストア関数はロード関数の逆で、計算関数から結果を取り出してそのデータをカーネル外に移動し、グローバル メモリに転送します。

パフォーマンス目標を達成するロード/計算/ストア構造を作成するには、まずカーネル内のデータの流れを設計します。この際、次の点に注意してください。

  • カーネル外からカーネル内にどのようにデータが流れるか。
  • このデータを処理するためにカーネルをどれくらいの速さにする必要があるか。
  • 処理されたデータをカーネル外にどのように書き出すか。

データ移動をブロック図を使用して可視化すると、カーネル内の異なる関数を分割して構成するのに役立ちます。

ロード/計算/ストア パターンの例は、Vitis Examples GitHub リポジトリを参照してください。

必要なインターフェイスを使用した最上位関数を作成

Vitis テクノロジでは、最上位関数のパラメーターからカーネル インターフェイスが推論されます。このため、まず必要なインターフェイスと同じパラメーターを使用してカーネル最上位関数を記述することから始めます。

入力パラメーターはスカラーとして渡す必要があります。入力および出力データのブロックは、ポインターとして渡す必要があります。インターフェイス定義を最終的に決定するには、コンパイラ プラグマを使用する必要があります。詳細は、インターフェイス を参照してください。

ロードおよびストア関数のコードを記述

カーネルとグローバル メモリ間のデータ転送は、全体的なシステム パフォーマンスに大きく影響します。正しく設定しないと、カーネルのパフォーマンスが低下します。このため、カーネルに対してデータが効率的に移動するようにロードおよびストア関数を最適化し、計算関数に最適に入力されるようにすることが重要です。

グローバル メモリのデータ レイアウトは、ソフトウェア アプリケーションのデータ レイアウトと同じにする必要があります。ロードおよびストア関数を記述する際は、このレイアウトがわかっていることが必要です。カーネルのデータ移動に特定のデータ レイアウトの方が適している場合は、ソフトウェア アプリケーションのバッファー レイアウトを使用できます。どちらの方法でも、カーネル開発者とアプリケーション開発者の間でバッファーとグローバル メモリのデータ構造について合意しておく必要があります。

次に、カーネルのデータ転送の効率を改善するためのガイドラインを示します。

ポート幅とデータパス幅を揃える

Vitis ソフトウェア プラットフォームでは、カーネルのポートは最大 512 ビット幅までに設定できるので、カーネルでポートごとに各クロック サイクルで 64 バイトまで読み出しまたは書き込みできます。

カーネル ポートの幅と計算関数のデータパスの幅は同じにすることをお勧めします。たとえば、必要なスループットを達成するためにデータパスで 16 バイトを並列処理する必要がある場合、ポートは 128 ビット幅にして、16 バイトを並列で読み出しおよび書き込みできるようにします。

データパスで必要なくても、インターフェイスの幅すべてのビットにアクセスできるようにすると有益な場合があります。このようにすると、多くのカーネルが同じグローバル メモリ バンクにアクセスしようとする場合の競合は削減できますが、通常はバッファリングとカーネル内の内部メモリ リソースが増加します。

バースト転送を使用

グローバル メモリへの最初の読み出しまたは書き込み要求は、時間およびリソースを多く費やしますが、後続の演算はそうではありません。データをバースト転送すると、メモリ アクセスのレイテンシが隠され、帯域幅の使用率およびメモリ コントローラーの効率が改善します。

グローバル メモリへのアトミック アクセスは、絶対に必要な場合以外は使用しないようにしてください。ロードおよびストア関数は、常にバースト トランザクションを推論するようにコード記述する必要があります。これには、GitHub の例vadd.cpp ファイルに示すように memcpy 演算を使用するか、アプリケーションの開発インターフェイス に説明されているように必要なすべての値に順次アクセスする厳密な for ループを作成します。

グローバル メモリからのデータ転送数を最小限に抑える

グローバル メモリにアクセスするとアプリケーションのレイテンシが大幅に増加するので、必要な転送だけを実行します。

必要な値だけを読み出して書き込み、1 回だけ実行するようにしてください。計算関数で同じ値を複数回使用する必要がある場合は、グローバル メモリから再び読み出すのではなく、値をローカルのバッファーに格納します。スループット目標を達成するためには、バッファリングおよびキャッシング構造を正しく設定することが重要となります。

計算関数のコード記述

計算関数は、すべての処理を実行します。この手法の最初の手順では、最上位構造を正しく設定して、データ移動を最適化します。正しいインターフェイスを使用した関数を作成し、機能が正しいかどうかを確認することが優先されます。次のセクションで、計算関数の内部構造について説明します。

ロード、計算、およびストア関数の接続

標準 C/C++ 変数および配列を使用すると、最上位インターフェイスと load、compute、store 関数を接続できます。また、ストリーミング動作を記述する hls::stream クラスを使用すると便利です。

ストリーミングとは、データ サンプルが最初のサンプルから順に送信されるデータ転送のことで、アドレス管理を必要とせず、FIFO を使用してインプリメントできます。hls::stream クラスの詳細は、Vitis HLS フローHLS ストリームの使用 を参照してください。

関数を接続する際は、HLS コンパイラで必要な正規化形式を使用します。詳細は、データフロー最適化 を参照してください。これにより、コンパイラで、データフロー最適化を使用してスループットの大きなタスク セットが構築されます。主な推奨事項は、次のとおりです。

  • データは順方向にのみ転送し、できる限りフィードバックしないようにします。
  • 接続ごとに、1 つのプロデューサーと 1 つのコンシューマーが必要です。
  • カーネルのプライマリ インターフェイスにアクセスするのは、ロードおよびストア関数のみにします。

これで、カーネルの最上位関数が作成され、カーネルにデータを必要なスループットで転送するインターフェイスとロード/ストア関数のコードが記述されました。

手順 2: 計算ブロックの小型関数への分割

この手順では、次の図に示すように、メインの計算関数をサブ関数のシーケンスに分割します。

13: 計算関数のサブ関数

スループット目標を満たすため分割

この方法で作成したデータフロー システムでは、一番遅いタスクがボトルネックです。

Throughput(Kernel) = min(Throughput(Task1), Throughput(Task2), …, Throughput(TaskN))

このため、分割プロセスでは、常にカーネル スループット目標を考慮して、各サブ関数がこのスループット目標を達成できるかどうかを確認します。

この手法の次の手順では、Vitis HLS コンパイラを実行して実際のスループット値を取得します。結果を改善できない場合は、反復してさらに計算段階を分割する必要があります。

関数に含めるループ ネストは 1 つのみにする

原則的には、関数に順次ループが含まれる場合、これらのループは HLS コンパイラで生成されたハードウェア インプリメンテーションで順次実行されます。順次実行はスループットを低下させるので、通常は望ましくありません。

ただし、これらの順次ループを順次関数に挿入すると、HLS コンパイラでデータフロー最適化を適用し、各タスクをパイプライン処理してオーバーラップして実行するインプリメンテーションを生成できるようになります。データフロー最適化の詳細は、Vitis HLS フロータスク レベルの同時処理: データフロー最適化 を参照してください。

この分割では、順次ループを個別の関数に含めます。理想的には、最下位の計算ブロックに完全にネストされたループを 1 つのみ含めるようにします。ループの詳細は、ループ を参照してください。

データフロー正規化形式を使用して計算関数を接続

最上位関数内の接続に関する規則は、計算関数を分割する際にも適用できます。フィード フォワード接続で、各接続変数ごとに 1 つのプロデューサーとコンシューマーが含まれるようにします。変数を複数の関数で消費されるようにする必要がある場合は、明示的に複製する必要があります。

データのブロックを 1 つの計算ブロックから別のブロックに移動する際は、配列または hls::stream オブジェクトを使用できます。

配列を使用すると、コード変更が少なくすむので、通常は分割プロセスを短時間で実行できますが、hls::stream オブジェクトを使用すると、メモリ リソース量が少なくなり、レイテンシも短くなります。また、カーネルを介してデータがどのように移動するかを判断しやすくなります。これは、スループットを最大にするよう最適化する場合に重要です。

通常は hls::stream オブジェクトを使用することをお勧めしますが、配列をいつストリームに変換するかは開発者が決定できます。早期に実行する開発者もいれば、最終的な最適化段階で実行する開発者もいます。これには、pragma HLS dataflow を使用することもできます。

この段階では、カーネルのアーキテクチャを図にしておくと、データ依存性、データ移動、データ管理、制御フロー、および同時処理を決定するのに役立ちます。

手順 3: 最適化を必要とするループの特定

ここまでで、カーネルのスループット目標を達成するためにデータ モーションと処理関数を使用したデータフロー構造を作成しました。次に、目標のスループットを達成するように各処理関数をインプリメントします。

前述のように、関数のスループットは、処理されるデータ量を関数のレイテンシまたは実行時間で除算して算出します。

T = max(VINPUT, VOUTPUT) / レイテンシ

この設計手法で説明する「アウトサイドイン」分割プロセスのこの段階では、目標のスループットも、関数で消費および生成されるデータ量もわかっているので、簡単に各関数のレイテンシ ターゲットを算出できます。

Vitis HLS コンパイラでは、関数およびループのスループットとレイテンシに関する詳細なレポートが生成されます。ターゲット レイテンシが決定されたら、HLS レポート に説明されているように、HLS レポートからどの関数およびループがレイテンシ ターゲットを満たしていないか、どれに注意が必要かを特定します。

ループのレイテンシは、次のように算出できます。

LatencyLoop = (Steps + II x (TripCount – 1)) x ClockPeriod

説明:

Steps
1 回のループ反復にかかる時間 (クロック サイクル数)。
TripCount
ループ内の反復回数。
II
開始間隔 (2 つの連続する反復間のクロック サイクル数)。ループがパイプライン処理されない場合、II は Steps と同じになります。

クロック周期が一定だとすると、ループのレイテンシを削減し、その結果の関数のスループットを改善するには、次の 2 つの方法があります。

  • ループ内の Steps を削減する (1 つの反復の実行にかかる時間を削減)。
  • TripCount を減らして、ループの反復回数を減す。
  • II (開始間隔) を削減して、開始できるループ反復の回数を増やす。

TripCount が Steps よりもかなり大きい場合、II または TripCount のいずれかを 1/2 にすると、ループのスループットを倍にできます。

レイテンシがターゲットを超えているループを最適化するには、この情報を理解しておくことが重要です。Vitis HLS コンパイラでは、デフォルトでは II できるだけ小さくなるようにループ インプリメンテーションを生成することが試みられます。まず、トリップカウントまたはステップ数を減らしてレイテンシを改善する方法から試してください。詳細は、ループ を参照してください。

手順 4: ループ レイテンシの改善

レイテンシがターゲットを超えているループを特定したら、まずはループを展開してみます。

ループ展開

ループ展開では、ループをほどいて複数の反復を同時に実行できるようにし、ループの全体的なトリップカウントを減らします。

前述の工場のたとえを使用すると、工場がカーネル、組立ラインがデータフロー パイプライン、工程が計算関数となります。ループを展開すると、ベルトコンベヤーに同時に到着する複数のオブジェクトを処理可能な工程が作成されるので、パフォーマンスが向上します。

14: ループ展開

ループを展開すると、指定した係数によって結果のデータパスの幅を広くできます。通常これにより並列処理されるサンプル数が多くなるため、帯域幅要件が増加します。次の 2 点に注意してください。

  • 関数 I/O の幅とデータパスの幅は同じである必要があります。
  • I/O 要件がカーネル ポートの最大サイズ (512 ビット/64 バイト) を超えるほどループを展開してデータパスの幅を広くする必要はありません。

ループ展開を最適に使用するには、次のガイドラインに従ってください。

  • ループ ネスト内の最内ループから開始します。
  • すべてのループ運搬依存をなくす展開係数を特定します。
  • より効率的な結果を得るには、ループを固定トリップカウント数で展開します。
  • 展開されたループ内に関数呼び出しがある場合は、これらの関数をインライン処理すると、リソース共有が良くなり、結果を改善できますが、合成にかかる時間は長くなります。また、インターコネクトが複雑になるほど、後で配線で問題が発生する可能性が高くなることに注意してください。
  • ループはむやみに展開せず、特定の結果を念頭に置いて実行するようにしてください。

配列分割

ループを展開すると、I/O 要件および関数のデータ アクセス パターンが変わります。ほとんどのループは配列アクセスを作成しますが、その場合は結果のデータパスが必要なすべてのデータに並列にアクセスできるようにしてください。

ループを展開しても思ったようにパフォーマンスが改善しない場合、通常原因はメモリ アクセスのボトルネックにあります。

デフォルトでは、Vitis HLS コンパイラは大型配列を 1 つの配列要素のサイズと同じワード幅でメモリ リソースにマップします。このデフォルトのマップは、ループ展開を使用するほとんどの場合、変更する必要があります。

配列の構成 で説明するように、HLS コンパイラでは配列を分割および再形成するためのさまざまなプラグマがサポートされています。必要なレベルの並列アクセスが可能なメモリ構造を作成するには、ループ展開の際にこれらのプラグマを使用することを考慮してみてください。

配列を展開および分割するだけで、ターゲット ループのレイテンシおよびスループット目標を達成できることがあります。その場合は、次のループに進みます。そうでない場合は、スループットを改善するその他の最適化を検証します。

手順 5: ループのスループットの改善

トリップカウントを減らしてもループのレイテンシを改善できない場合は、開始間隔 (II) を削減する方法を試します。

ループの II は、2 つのループ反復開始の間のクロック サイクル数です。Vitis HLS コンパイラは常にループをパイプライン処理し、II を最小限に抑えて、ループをできるだけ早く開始し、クロック サイクルごとに新しい反復が開始 (II=1) するようにすることを目指します。

II を制限する主な要因は、次の 2 つです。

  • I/O 競合
  • ループ運搬依存

HLS スケジュール ビューアーでは、II を制限するループ依存が自動的にハイライトされます。これは、ループの II を改善するには、非常に便利な可視化ツールです。

I/O の競合を削減

I/O の競合は、ループ反復ごとに内部メモリ リソースの I/O ポートに複数回アクセスする必要がある場合に発生します。II がループ反復ごとに I/O リソースにアクセスする回数よりも小さい場合、ループをパイプライン処理することはできません。シングル ポート RAM では、1 回のループ反復でポート A に 4 回アクセスする必要がある場合、可能な最小 II は 4 です。

これらの I/O アクセスが必要かどうか、削除可能かどうかは、開発者が判断する必要があります。I/O の競合を削減するのに最もよく使用される方法は、次のとおりです。

  • 内部キャッシュ構造の作成

    問題のある I/O アクセスの中に以前のループ反復で既にアクセス済みのデータへのアクセスが含まれる場合は、以前の反復でアクセスされた値のローカル コピーを作成するようにコードを変更します。ローカル データ キャッシュを使用すると、外部 I/O にアクセスする回数が減るので、ループの II を改善できます。

    Vitis Accel Examples GitHub リポジトリのこの例は、シフト レジスタをローカルで使用し、前に読み込んだ値をキャッシュして、フィルターのスループットを改善する方法を示しています。

  • I/O およびメモリの再構成

    このセクションの前の方のレイテンシ改善方法で説明したように、HLS コンパイラでは配列がメモリにマップされ、デフォルトのメモリ構成で必要なスループットを達成するのに十分な帯域幅がありません。配列分割およびプラグマの再形成を使用して帯域幅を増加したメモリ構造を作成することもでき、ループの II を改善できます。

ループ運搬依存を削減

ループ運搬依存の最もよくあるケースは、ループ反復が前の反復で計算された値に依存する場合です。依存が配列なのかスカラー変数なのかによって違いがあります。詳細は、Vitis HLS フロー最適なループ展開によるパイプラインの改善を参照してください。

  • 配列の依存性の削減

    HLS コンパイラは、インデックス解析を実行して配列依存 (read-after-write、write-after-read、write-after-write) があるかどうかを判断しますが、必ずしもすべての依存性を解決できるわけではなく、その場合はフォルス依存がレポートされます。

    これらの依存性は、特定のコンパイラ プラグマで上書きすると、デザインの II を改善できます。この場合、有効な依存を上書きしないように注意してください。

  • スカラーの依存性の削減

    スカラーの依存性は、通常、複数クロック サイクルにわたってスケジュールされた計算を含むフィードバック パスがある場合に発生します。これらのフィードバック パスには、乗算、除算、剰余演算などの複雑な算術演算があります。フィードバック パスのサイクル数は II を直接制限するので、II およびスループットを改善するには、フィードバック パスのサイクル数を削減する必要があります。これには、フィードバック パスを解析して、短くする必要性があるか、どうやったら短くできるかを調べる必要があります。HLS スケジューリング制約の使用、ビット幅の削減などのコード変更がその方法の例です。

アドバンス手法

通常は II を 1 にするのが理想的ですが、それだけで十分なことはめったにありません。目標は、レイテンシとスループットの目標を達成することです。これには通常、さまざまな II と展開係数の組み合わせを試します。

このガイドの最適化設計手法および方法は、この目標を達成するのに有益です。HLS コンパイラでは、さらに多くの最適化オプションがサポートされているので、特定の状況下で使用すると役立つことがあります。これらの最適化の詳細は、HLS プラグマ を参照してください。