制御駆動型の TLP モデルでは、タスク (関数およびループ) 間、および最大限のパフォーマンスを得るため理想的にはパイプライン処理された関数およびループ間のデータの流れが最適化されます。そのためタスクを順につなげる必要はありませんが、データの転送方法にいくつかの制限があります。次のような条件下では、データフロー モデルで実行可能なオーバーラップが阻止されたり制限されたりする可能性があります。
- データフロー領域の中央での関数入力からの読み出しまたは関数出力への書き込み
- シングル プロデューサー コンシューマー違反
- タスクの条件付き実行
- 複数の exit 条件を持つループ
入力からの読み出し/出力への書き込み
関数の入力の読み出しは、データフロー領域の開始で実行し、出力への書き込みはデータフロー領域の最後に実行する必要があります。関数のポートへの読み出し/書き込みにより、プロセスが重複して実行されるのではなく、順序どおりに実行され、パフォーマンスに悪影響を与える可能性があります。
シングル プロデューサー コンシューマー違反
ツールでデータフロー モデルを使用するには、タスク間で渡される要素すべてがシングル プロデューサー コンシューマー モデルに従っている必要があります。変数はそれぞれ 1 つのタスクから駆動され、1 つのタスクでのみ消費される必要があります。次のコード例では、temp1
がファンアウトして、Loop2
および Loop3
の両方に入力されています。これは、シングル プロデューサー コンシューマー モデルに違反しています。
void foo(int data_in[N], int scale, int data_out1[N], int data_out2[N]) {
int temp1[N];
Loop1: for(int i = 0; i < N; i++) {
temp1[i] = data_in[i] * scale;
}
Loop2: for(int j = 0; j < N; j++) {
data_out1[j] = temp1[j] * 123;
}
Loop3: for(int k = 0; k < N; k++) {
data_out2[k] = temp1[k] * 456;
}
}
これを修正したコードでは、Split
関数を使用して、シングル プロデューサー コンシューマー デザインを作成しています。次のコード ブロック例は、関数 Split
でデータがどのように流れるかを示しています。データが 4 つのタスクすべての間で流れるようになったので、データフロー モデルを使用できます。
void Split (in[N], out1[N], out2[N]) {
// Duplicated data
L1:for(int i=1;i<N;i++) {
out1[i] = in[i];
out2[i] = in[i];
}
}
void foo(int data_in[N], int scale, int data_out1[N], int data_out2[N]) {
int temp1[N], temp2[N]. temp3[N];
Loop1: for(int i = 0; i < N; i++) {
temp1[i] = data_in[i] * scale;
}
Split(temp1, temp2, temp3);
Loop2: for(int j = 0; j < N; j++) {
data_out1[j] = temp2[j] * 123;
}
Loop3: for(int k = 0; k < N; k++) {
data_out2[k] = temp3[k] * 456;
}
}
タスクのバイパスとチャネルのサイズ設定
また、データは通常 1 つのタスクから次のタスクに流れる必要があります。タスクをバイパスすると、データフロー モデルのパフォーマンスが低下する可能性があります。次の例では、Loop1
で temp1
および temp2
の値が生成されますが、次のタスク Loop2
では temp1
の値しか使用されません。temp2
の値は Loop2
の後まで使用されません。このため、temp2
はシーケンスの次のタスクをバイパスすることになり、データフロー モデルのパフォーマンスが制限される可能性があります。
void foo(int data_in[N], int scale, int data_out1[N], int data_out2[N]) {
int temp1[N], temp2[N]. temp3[N];
Loop1: for(int i = 0; i < N; i++) {
temp1[i] = data_in[i] * scale;
temp2[i] = data_in[i] >> scale;
}
Loop2: for(int j = 0; j < N; j++) {
temp3[j] = temp1[j] + 123;
}
Loop3: for(int k = 0; k < N; k++) {
data_out[k] = temp2[k] + temp3[k];
}
}
temp2
を格納するのに使用する PIPO バッファーの深さをデフォルトの 2 から 3 に増やす必要があります。これで Loop2
が実行された状態で、バッファーに Loop3
の値が格納できるようになります。同様に、2 つのプロセスをバイパスする PIPO の深さは 4 にする必要があります。バッファーの深さは、次のように STREAM プラグマまたは指示子で設定します。#pragma HLS STREAM type=pipo variable=temp2 depth=3
タスク間のフィードバック
フィードバックは、データフロー領域で 1 つのタスクからの出力が前のタスクで消費される場合に起こります。タスク間のフィードバックは、データフロー領域内では推奨されません。このため、HLS コンパイラでフィードバックが検出されると、状況によって警告メッセージが表示され、データフロー モデルは使用されません。
データフローでは、hls::streams
を使用するとフィードバックがサポートされます。この例外を次の例に示します。
#include "ap_axi_sdata.h"
#include "hls_stream.h"
void firstProc(hls::stream<int> &forwardOUT, hls::stream<int> &backwardIN) {
static bool first = true;
int fromSecond;
//Initialize stream
if (first)
fromSecond = 10; // Initial stream value
else
//Read from stream
fromSecond = backwardIN.read(); //Feedback value
first = false;
//Write to stream
forwardOUT.write(fromSecond*2);
}
void secondProc(hls::stream<int> &forwardIN, hls::stream<int> &backwardOUT) {
backwardOUT.write(forwardIN.read() + 1);
}
void top(...) {
#pragma HLS dataflow
hls::stream<int> forward, backward;
firstProc(forward, backward);
secondProc(forward, backward);
}
この例では、firstProc
が実行されると、入力の初期値として 10 が使用されます。hls::streams
では初期値はサポートされないので、この方法を使用するとシングル プロデューサー コンシューマー規則に違反せずに初期値を指定できます。その後の反復では、firstProc
は backwardIN
インターフェイスを介して hls::stream
から読み出します。
firstProc
は値を処理してsecondProc
に送信しますが、これには元の C++ 関数の実行順と順方向のストリームが使用されます。secondProc
は forwardIN
の値を読み出し、1 を追加して、実行順の逆方向のフィードバック ストリームを使用して firstProc
に戻します。
2 回目の実行からは、firstProc
はストリームから読み出した値を使用して計算を実行します。2 つのプロセスは、最初の実行には初期値を使用し、順方向と逆方向の両方の通信を使用して、恒久的に継続されます。
タスクの条件付き実行
データフロー モデルでは、条件付きで実行されるタスクは最適化されません。次は、この制限を示す例です。この例では、Loop1
および Loop2
の条件を実行すると、データが次のループに流れなくなり、これらのループ間のデータフローが最適化されなくなります。
void foo(int data_in1[N], int data_out[N], int sel) {
int temp1[N], temp2[N];
if (sel) {
Loop1: for(int i = 0; i < N; i++) {
temp1[i] = data_in[i] * 123;
temp2[i] = data_in[i];
}
} else {
Loop2: for(int j = 0; j < N; j++) {
temp1[j] = data_in[j] * 321;
temp2[j] = data_in[j];
}
}
Loop3: for(int k = 0; k < N; k++) {
data_out[k] = temp1[k] * temp2[k];
}
}
どの場合にも各ループが実行されるようにするには、コードを次のように変更する必要があります。この例の場合、条件文を最初のループに移動します。これで、両方のループが常に実行され、データが常に次のループに流れるようになります。
void foo(int data_in[N], int data_out[N], int sel) {
int temp1[N], temp2[N];
Loop1: for(int i = 0; i < N; i++) {
if (sel) {
temp1[i] = data_in[i] * 123;
} else {
temp1[i] = data_in[i] * 321;
}
}
Loop2: for(int j = 0; j < N; j++) {
temp2[j] = data_in[j];
}
Loop3: for(int k = 0; k < N; k++) {
data_out[k] = temp1[k] * temp2[k];
}
}
複数の終了条件を持つループ
複数の終了ポイントのあるループは、データフロー領域では使用できません。次の例では、Loop2
に 3 つの終了条件があります。
-
N
の値により終了 (ループはk>=N
の場合に終了)。 -
break
文による終了。 -
continue
文による終了。#include "ap_int.h" #define N 16 typedef ap_int<8> din_t; typedef ap_int<15> dout_t; typedef ap_uint<8> dsc_t; typedef ap_uint<1> dsel_t; void multi_exit(din_t data_in[N], dsc_t scale, dsel_t select, dout_t data_out[N]) { dout_t temp1[N], temp2[N]; int i,k; Loop1: for(i = 0; i < N; i++) { temp1[i] = data_in[i] * scale; temp2[i] = data_in[i] >> scale; } Loop2: for(k = 0; k < N; k++) { switch(select) { case 0: data_out[k] = temp1[k] + temp2[k]; case 1: continue; default: break; } } }
ループの終了条件は常にループ境界で定義されるので、
break
またはcontinue
文を使用すると、ループをデータフロー領域で使用できなくなります。最後に、データフロー モデルには階層インプリメンテーションはありません。サブ関数またはループにデータフロー モデルを使用すると効果的なタスクが含まれる場合、データフロー モデルをループまたはサブ関数に適用するか、サブ関数をインライン展開する必要があります。
std::complex
を使用することもできます。ただし、次の例に示すように __attribute__((no_ctor))
を使用する必要があります。void proc_1(std::complex<float> (&buffer)[50], const std::complex<float> *in);
void proc_2(hls::Stream<std::complex<float>> &fifo, const std::complex<float> (&buffer)[50], std::complex<float> &acc);
void proc_3(std::complex<float> *out, hls::Stream<std::complex<float>> &fifo, const std::complex<float> acc);
void top(std::complex<float> *out, const std::complex<float> *in) {
#pragma HLS DATAFLOW
std::complex<float> acc __attribute((no_ctor)); // Here
std::complex<float> buffer[50] __attribute__((no_ctor)); // Here
hls::Stream<std::complex<float>, 5> fifo; // Not here
proc_1(buffer, in);
proc_2(fifo, buffer, acc);
proc_3(out, fifo, acc);
}