制御駆動型のタスク レベル並列処理の制限 - 2023.2 日本語

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

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 日本語
ヒント: 制御駆動型 TLP では、DATAFLOW プラグマまたは指示子をコードの適切な位置に指定する必要があります。

制御駆動型の TLP モデルでは、タスク (関数およびループ) 間、および最大限のパフォーマンスを得るため理想的にはパイプライン処理された関数およびループ間のデータの流れが最適化されます。そのためタスクを順につなげる必要はありませんが、データの転送方法にいくつかの制限があります。次のような条件下では、データフロー モデルで実行可能なオーバーラップが阻止されたり制限されたりする可能性があります。

  • データフロー領域の中央での関数入力からの読み出しまたは関数出力への書き込み
  • シングル プロデューサー コンシューマー違反
  • タスクの条件付き実行
  • 複数の exit 条件を持つループ
重要: これらのコーディング スタイルのいずれかが使用されている場合、 HLS コンパイラで状況を説明するメッセージが表示されます。

入力からの読み出し/出力への書き込み

関数の入力の読み出しは、データフロー領域の開始で実行し、出力への書き込みはデータフロー領域の最後に実行する必要があります。関数のポートへの読み出し/書き込みにより、プロセスが重複して実行されるのではなく、順序どおりに実行され、パフォーマンスに悪影響を与える可能性があります。

シングル プロデューサー コンシューマー違反

ツールでデータフロー モデルを使用するには、タスク間で渡される要素すべてがシングル プロデューサー コンシューマー モデルに従っている必要があります。変数はそれぞれ 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 つのタスクから次のタスクに流れる必要があります。タスクをバイパスすると、データフロー モデルのパフォーマンスが低下する可能性があります。次の例では、Loop1temp1 および 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
重要: チャネルのサイズ設定も同様にパフォーマンスに影響します。FIFO/PIPO の深さが一致していないと、FIFO/PIPO からのバック プレッシャーにより、データフロー領域内の同期ポイントが誤って発生する可能性があります。

タスク間のフィードバック

フィードバックは、データフロー領域で 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 では初期値はサポートされないので、この方法を使用するとシングル プロデューサー コンシューマー規則に違反せずに初期値を指定できます。その後の反復では、firstProcbackwardIN インターフェイスを介して hls::stream から読み出します。

firstProc は値を処理してsecondProc に送信しますが、これには元の C++ 関数の実行順と順方向のストリームが使用されます。secondProcforwardIN の値を読み出し、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);
}