2 つの関数入力に任意精度型を使用するだけで、Vitis HLS で 24 ビット乗算器を使用するデザインが作成され、12 ビ ット型がデザインに伝搬されます。ザイリンクスでは、階層内のすべての関数の引数のサイズを正しく記述すること を勧めしています。
通常は、変数が関数インターフェイスから、特に最上位関数インターフェイスから直接駆動されると、最適化の中に 実行されないものがでてくることがあります。典型的な例は、入力がループ インデックスの上位制限として使用され る場合です。
dsel_t x;
LOOP_X:for (x=0;x<width; x++) { out_accum += A[x];
}
return out_accum;
}
上記のデザインを最適化しようとすると、可変ループ境界による問題がわかります。可変ループ境界に関する最初の 問題は、Vitis HLS でループのレイテンシが決定できなくなる点です。Vitis HLS はループの一巡目が終了するまでの レイテンシは決定できますが、可変幅の正確な値はスタティックに決定できないので、何度繰り返されるのかは判断 できず、ループ レイテンシ (ループの繰り返しすべてを 完全に実行するまでのサイクル数) はレポートできません。
ループ境界が可変の場合、Vitis HLS ではそのレイテンシが正確な値ではなく、クエスチョン マーク (?) でレポートさ れます。次は、上記の例の合成後の結果を示しています。
+ Summary of overall latency (clock cycles):
* Best-case latency: ? * Worst-case latency: ?
+ Summary of loop latency (clock cycles):
+ LOOP_X:
* Trip count: ? * Latency: ?
可変ループ境界には、デザインのパフォーマンスが未知になるという別の問題もあります。この問題は次の 2 つの方 法で回避できます。
• pragma HLS loop_tripcount または set_directive_loop_tripcount を使用します。
• C コードで assert マクロをコード内で使用します。
tripcount 指示子を使用すると、ループに指定する最小および最大の両方またはいずれかの tripcount を指定で
きます。tripcount はループの繰り返し回数を意味します。最初の例のように最大の tripcount の 32 がLOOP_Xに適
用されると、レポートは次のようにアップデートされます。
+ Summary of overall latency (clock cycles):
* Best-case latency: 2 * Worst-case latency: 34
+ Summary of loop latency (clock cycles):
+ LOOP_X:
* Trip count: 0 ~ 32 * Latency: 0 ~ 32
tripcount 指示子に対してユーザーが指定した値は、レポートにのみ使用されます。tripcount の値は Vitis HLS で数が レポートされるので、さまざまなソリューションからのレポートを比較できます。この同じループ境界情報を合成に 使用するには、C コードをアップデートする必要があります。
次は、より小さい開始間隔で最初の例を最適化する手順を示しています。
• ループを展開し、並列累算が実行されるようにします。
• 配列入力を分割しないと、並列累算が 1 つのメモリ ポートに制限されます。
これらの最適化が適用されると、Vitis HLS で可変境界ループに関する最大の問題を示すメッセージが表示されます。
@W [XFORM-503] Cannot unroll loop 'LOOP_X' in function 'code028': cannot completely
unroll a loop with a variable trip count.
可変境界ループは展開できないので、unroll 指示子が適用できないだけでなく、そのループの上のレベルのパイプラ イン処理もできません。
重要: ループまたは関数がパイプライン処理されると、Vitis HLS はその関数またはループの下の階層ですべてのルー プを展開します。この階層に可変境界を含むループがあると、パイプライン処理はできなくなります。
この問題は、ループ内で条件付き実行を使用し、ループの繰り返し回数を固定値にすると回避できます。可変ループ 境界のコードは、次のコード例に示すように書き直すことができます。この例では、ループ境界は可変幅の最大値に 設定され、ループ本体は条件付きで実行されます。
#include "ap_cint.h"
#define N 32
typedef int8 din_t;
typedef int13 dout_t;
typedef uint5 dsel_t;
dout_t loop_max_bounds(din_t A[N], dsel_t width) { dout_t out_accum=0;
dsel_t x;
LOOP_X:for (x=0;x<N; x++) { if (x<width) {
out_accum += A[x];
} }
return out_accum;
}
上記の例の for ループ (LOOP_X) は、展開できます。これは、ループに上位境界があり、Vitis HLS でハードウェアが どれくらい作成されるか認識されるからです。RTL デザインのループ本体には、N(32) 個のコピーがあります。この ループ本体の各コピーには、それに関する条件付きロジックが含まれ、可変幅の値によって実行されます。
ループのパイプライン処理
ループをパイプライン処理する際は、通常一番内部のループをパイプライン処理すると、エリアとパフォーマンスの 最適なバランスがわかります。これにより、ランタイムも最速になります。次のコード例は、ループおよび関数をパ イプライン処理した場合のトレードオフを示しています。
#include "loop_pipeline.h"
dout_t loop_pipeline(din_t A[N]) { int i,j;
static dout_t acc;
LOOP_I:for(i=0; i < 20; i++){
LOOP_J: for(j=0; j < 20; j++){
acc += A[i] * j;
} }
return acc;
}
一番内側のループ (LOOP_J) がパイプライン処理されると、ハードウェア (単一の乗算器) には LOOP_J のコピーが 1 つできます。Vitis™ HLS では、できるだけループをフラットにするので、この場合は 20x20 の繰り返しの 1 つの新し いループが作成されます。スケジューリングする必要があるのは、乗算器演算 1 つと配列アクセス 1 回のみです。こ れらをスケジューリングしておくと、ループの繰り返しを 1 つのループ本体のエンティティ (20X20 のループ繰り返 し) としてスケジューリングできます。
ヒント: ループまたは関数がパイプライン処理される場合は、そのループまたは関数の下の階層にあるループを展開 する必要があります。
外側のループ (LOOP_I) がパイプライン処理されると、内部のループ (LOOP_J) が展開され、そのループの本体のコピ ーが 20 個作成されるので、乗算器 20 個と配列アクセス 20 回をスケジューリングする必要があります。こうしてお くと、LOOP_I の各繰り返しを 1 つのエンティティとしてスケジューリングできるようになります 。
最上位関数がパイプライン処理される場合は、どちらのループも展開する必要があります。この場合、乗算器 400 個 と配列アクセス 400 回をスケジューリングする必要があります。ただし、Vitis HLS で 400 個の乗算器が作成される ことはほぼありません。これは、ほとんどのデザインで、データ依存性のために最大の並列処理ができないことがよ くあるからです。たとえば、この例の場合、デュアル ポート RAM が A[N] に使用されても、デザインはクロック サ イクルの A[N] の 2 つの値にしかアクセスできません。
パイプライン処理する階層レベルを選択すると、たとえば一番内側のループをパイプライン処理したときに、ほとん どのアプリケーションで一般的に許容されるスループットで最小のハードウェアが提供されます。階層の上位をパイ プライン処理すると、すべてのサブループが 展開されるので、スケジューリングするためにさらに多くの演算が作成 されますが (ランタイムとメモリ容量に影響する可能性あり)、スループットとレイテンシの観点から、通常はパフォ ーマンスの最も高いデザインになります。
上記のオプションをまとめると、次のようになります。
• パイプライン LOOP_J
レイテンシは約 400 サイクル (20X20) になり、100 個未満の LUT およびレジスタが必要になります (I/O 制御およ び FSM は常にあります)。
• パイプライン LOOP_I
レイテンシは約 20 サイクルになりますが、数百個の LUT およびレジスタが必要になります。ロジック数は、最 初のオプションの約 20 倍の数からロジック最適化で処理されるロジックを引いた数になります。
• パイプライン関数 loop_pipeline
レイテンシは約 10 個 (デュアル ポート アクセス 20 回) になりますが、何千個もの LUT およびレジスタが必要と なります。ロジック数は最初のオプションの約 400 倍からロジック最適化で処理されるロジックを引いた数にな ります。
不完全な入れ子のループ
一番内側のループ階層がパイプライン処理されると、Vitis HLS は、内側のループをフラット化し、ループの遷移 (ル ープの入出時にループ インデックスで実行されるチェック) によるサイクルを削除することにより、レイテンシを削 減してスループット全体を改善します。このようなチェックは、1 つのループから次のループへの遷移の際にクロッ ク遅延を発生させます。
不完全な入れ子のループの場合、またはループをフラット化できない場合、ループの入出のためにクロック サイクル が追加されます。デザインに入れ子のループが含まれる場合は、結果を解析して、なるべく多くのループがフラット 化されるようにします。ログ ファイルまたは合成レポートで、ループ ラベルが結合されていること (LOOP_I と LOOP_J が LOOP_I_LOOP_J としてレポートされるなど) を確認してください。
ループの並列処理
Vitis™ HLS では、レイテンシを削減するために、ロジックおよび関数ができるだけ早い段階でスケジューリングされ ます。これを実行するため、なるべく多くのロジック演算および関数が並列にスケジューリングされますが、ループ を並列に実行することはできません。
次のコード例が合成されると、SUM_X ループがスケジューリングされ、その後 SUM_Y ループがスケジューリングさ れます。SUM_Y ループの開始は SUM_X ループの完了を待つ必要がなくても、SUM_X の後にスケジューリングされま す。
#include "loop_sequential.h"
void loop_sequential(din_t A[N], din_t B[N], dout_t X[N], dout_t Y[N], dsel_t xlimit, dsel_t ylimit) {
dout_t X_accum=0;
dout_t Y_accum=0;
int i,j;
SUM_X:for (i=0;i<xlimit; i++) { X_accum += A[i];
X[i] = X_accum;
}
SUM_Y:for (i=0;i<ylimit; i++) { Y_accum += B[i];
Y[i] = Y_accum;
}}
これらのループには異なる境界 (xlimit および ylimit) があるため、統合はできません。次のコード例のようにル ープを別の関数に含めると、まったく同じ機能を達成でき、どちらのループも処理できます。
#include "loop_functions.h"
void sub_func(din_t I[N], dout_t O[N], dsel_t limit) { int i;
dout_t accum=0;
SUM:for (i=0;i<limit; i++) { accum += I[i];
O[i] = accum;
} }
void loop_functions(din_t A[N], din_t B[N], dout_t X[N], dout_t Y[N], dsel_t xlimit, dsel_t ylimit) {
sub_func(A,X,xlimit);
sub_func(B,Y,ylimit);
}
前の例が合成されると、レイテンシはシーケンシャル ループの例の半分になります。これは、ループが関数として並 列に実行できるようになったからです。
シーケンシャル ループの例には、dataflow 最適化も使用できます。ここに示す並列処理のため、関数にループを取 り込む方法は、dataflow 最適化が使用できない場合に使用します。たとえば、より大型のデザイン例では、
dataflow 最適化を最上位のすべてのループ/関数、および各最上位ループおよび関数間に配置されたメモリに適用 できます。
ループ依存性
ループ依存性は、ループを最適化されないように (通常はパイプライン処理) するデータ依存性のことです。これら は、ループの 1 回の反復内またはループ内の異なる反復間にできます。
ループ依存性を理解するには、極端な例を見てみるのがわかりやすいです。次の例では、ループの結果がそのループ の継続/終了条件として使用されています。次のループを開始するには、前のループの各反復が終了する必要がありま す。
Minim_Loop: while (a != b) { if (a > b)
a -= b;
else b -= a;
}
このループはパイプライン処理できません。ループの前の反復が終了するまで次の反復を開始できないからです。す べてのループ依存性がこのように極端なわけではありませんが、ほかの演算が終了するまで開始できない演算がある ことに注意してください。ソリューションとしては、最初の演算ができるだけ早い段階で実行されるようにします。
ループ依存性はすべてのデータ型で発生する可能性はありますが、特に配列を使用する場合によく発生します。