はじめに
このケース スタディの目的は、HLS メトリックを使用して読み出し/書き込みのループ/関数のスループットを向上させるため最適化方法を手順を追って示すことです。これらの最適化では、グローバル メモリからカーネルへの効率的なデータ転送を実行することで、システムのカーネル時間とスループットを向上させます。次の transfer_kernel
の例では、DDR の単純な (可変サイズで NUM_ITERATIONS の) 読み出し/書き込みを実行します。
1 #include "config.h"
2 #include "assert.h"
3 extern "C" {
4 void transfer_kernel(wd* in,wd* out, const int size, const int iter ) {
5 ···
6 wd buf[256];
7 int off = (size/16);
8
9 read_loop: for (int i = 0; i <off; i++)
10 {
11 buf[i] = in[i];
12 }
13
14 write_loop: L1: for (int i = 0; i < iter; i++) {
15 L2: for (int j = 0; j <off; j++) {
16 #pragma HLS PIPELINE II=1
17 out[j+off*i] = buf[j];
18 }
19 }
20 ···
21 }
22 }
このケース スタディは、次の 4 つの手順に分かれています。
- ポート幅を 512 ビット幅でカーネルの実行時間のベースラインを設定
- レイテンシ パラメーターを変更してパフォーマンスを向上
- 書き込みループの自動バースト推論を改善
- これ以上の複数ポートと未処理の書き込み数を使用した改善はなし
手順 1: 512 ビットのポート幅でカーネルのベースラインを設定
デフォルト設定を使用してカーネル時間のベースラインを設定します。この実行中、自動バーストは読み出しおよび書き込みループに対して次を推論します。
- ツールが連続するメモリ アクセス パターンを予測できるため、読み出しループはパイプライン バーストを達成します。このため、可変サイズの DDR からの読み出し要求がパイプライン処理されます。
- 書き込み外部ループの L1 は、シーケンシャル バーストを取得します。これは、コンパイラがすべての組み合わせを繰り返し、L2 ループの開始前に L1 ループに if 条件を挿入するかどうかを判断するからです (コンパイル時にサイズが不明であるため)。同時に、最も内側のループ L2 がパイプライン バーストを達成します。L2 ループは可変サイズの書き込み要求を要求し、L1 は L2 ループのすべてのデータが DDR から戻って L1 の次の反復を開始するまで待機します。
アプリケーションを構築して実行した後は、Vitis アナライザー ツールを使用してパフォーマンスを評価し、ビルド プロセスまたは実行サマリによって生成されたレポートを表示できます。Vitis HLS からの合成レポートに表示されるバースト サマリを確認します。読み出しループおよび書き込みループのバーストが成功したか、エラーになったかを確認します。
Vitis アナライザーでは、プロファイル サマリおよびタイムライン トレース レポートも、FPGA アクセラレーション アプリケーションのパフォーマンスを解析するのに役立ちます。プロファイル サマリでは、Kernels & Compute Unit:
Kernel Execution にベースライン ビルドで transfer_kernel
が必要とする合計時間がレポートされます。
手順 2: パフォーマンス レイテンシの改善
Vitis HLS はデフォルトの 64 カーネル サイクルのレイテンシを使用しますが、それが長すぎる場合もあります。レイテンシはシステムの特性によって異なります。この例では、レイテンシはデフォルトから 21 カーネル サイクルに減少しています。次の例に示すようにコードを変更し、INTERFACE プラグマまたは指示子を使用して、レイテンシを指定します。
1 #include "config.h"
2 #include "assert.h"
3 extern "C" {
4 void transfer_kernel(wd* in,wd* out, const int size, const int iter ) {
5 #pragma HLS INTERFACE m_axi port=in0_index offset=slave latency=21
6 #pragma HLS INTERFACE m_axi port=out offset=slave latency=21
7 ...
アプリケーションを構築して実行したら、Vitis アナライザーを使用して、ビルド プロセスまたは実行サマリによって生成されたレポートを表示できます。Vitis HLS の合成レポートの HW Interface の表で、指定したレイテンシが適用されていることを確認します。
Burst Summary でそのプロセスが成功したか、エラーになったを確認します。
プロファイル サマリ レポートの「Kernel Execution」で、インターフェイスのレイテンシを設定することでパフォーマンスが向上したことを確認します。
手順 3: 書き込みループの自動バースト推論を改善
コンパイル時にサイズとループ トリップ数が不明なため、コンパイラの自動バースト推論の見積もりは不必要に悪くなります。次に示すようにコードを変更すると、コンパイラがパイプライン バーストを推論できます。
1 #include "config.h"
2 #include "assert.h"
3 extern "C" {
4 void transfer_kernel(wd* in,wd* out, const int size, const int iter ) {
5 #pragma HLS INTERFACE m_axi port=in offset=slave latency=21
6 #pragma HLS INTERFACE m_axi port=out offset=slave latency=21
7
8 int k=0;
9 wd buf[256];
10 int off = (size/16);
11
12 read_loop: for (int i = 0; i <off; i++)
13 {
14 buf[i] = in[i];
15 }
16
17 write_loop: for (int j = 0; j <off*iter; j++) {
18 #pragma HLS PIPELINE II=1
19 out[k++] = buf[j%off];
20 }
21 }
22 }
アプリケーションを構築して実行したら、Vitis アナライザーを使用して、ビルド プロセスまたは実行サマリによって生成されたレポートを表示できます。合成レポートからは、コンパイラへのバースト ヒントによって書き込みループのシーケンシャル バーストが修正されたことが確認できます。Burst and Widening Missed というメッセージは、ポートを 512 ビットに拡張することに関連しています。この例では既にポート幅 512 を使用しているため、無視して問題ありません。コードで幅が 512 ビットになっていない場合は、これらのメッセージの示す問題を解決する必要があります。
プロファイル サマリ レポートの「Kernel Execution」からは、手順 2 のレイテンシ変更と現在の手順の書き込みループのパイプライン バーストの結果、パフォーマンスが改善したことがわかります。
概要
Vitis HLS インターフェイス メトリックから実行できるその他の改善はありません。ケース スタディの例では、同時読み出しまたは書き込みがないため、複数のポートをターゲットにするのは、この場合には役立ちません。この例では、ツールが最大スループットのパイプライン バーストを達成しているため、未処理の読み出しと書き込みの数も無視できます。カーネル時間からは、これ以上の改善は確認できません。
ケース スタディのように、効率的な load-store 関数のインプリメンテーションは、ポート幅、バースト アクセス、レイテンシ、複数ポート、および未処理の読み取りと書き込みの数などの HLS インターフェイス メトリックに依存します。AMDでは、システム パフォーマンスを向上させるために次のガイドラインを推奨します。
- ポート幅: ポートのデータ型として
hls::vector
またはap_(u)int<512>
を使用して、インターフェイスのポート幅 (各 AXI ポートのビット幅) を最大化します。 - 複数ポート: 同時メモリの読み出し/書き込みを解析し、同時アクセス用の専用/独立ポートを備えます。
- パイプライン バースト: AXI レイテンシ パラメーターはパイプライン バーストに影響を与えません。パフォーマンスを大幅に向上させることができるパイプライン バーストを達成するようにコードを記述することをお勧めします。
- シーケンシャル バースト: AXI レイテンシ パラメーターは、シーケンシャル バーストに大きな影響を与え、ツールのデフォルト レイテンシからレイテンシ数を減少させることで、パフォーマンスを向上します。
- 未処理の数: ほとんどのバースト長が 16 以上の場合、デフォルトの未処理の数 (num outstanding) で十分です。AMD では、バースト サイズが 16 未満の場合は、デフォルト (=16) から未処理の数のサイズを 2 倍にすることをお勧めしています。
- データの並べ替え: パイプライン バーストの達成は常に推奨されますが、メモリ アクセス パターンのために、コンパイラがシーケンシャル バーストのみを達成できる場合もあります。データをメモリに保存するさまざまな方法を検討して、パフォーマンスを改善することもできます。たとえば、DRAM 内のデータに列優先順でアクセスすると、効率性がかなり劣ることがあります。カーネルに専用のデータ ムーバーをインプリメントするよりも、ソフトウェア内のデータを置き換えて、行優先順で格納した方が良い場合があり、ハードウェア アクセス パターンをかなりシンプルにできます。