データ駆動型および制御駆動型の混合モデル - 2023.2 日本語

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

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 日本語

次の表は、制御駆動型タスク レベル並列処理 (TLP) とデータ駆動型 TLP のどちらを適用するかを決定するのに役立つ HLS デザインの主な要因を示しています。

表 1. 制御駆動型およびデータ駆動型の TLP
制御駆動型 TLP データ駆動型 TLP
HLS デザインには、プロセスの開始/停止に制御信号が必要です。 HLS デザインは、プロセスの開始/停止のための制御信号を必要としない、完全にデータ駆動型のアプローチを使用
デザインにはローカル メモリ以外のアクセスが必要です。 デザインは純粋なストリーミング データ転送を使用
外部ソフトウェア アプリケーションとのインタラクションを必要とするデザイン データ依存のマルチレート ビヘイビアーのデザイン:
  • プロデューサーはデータを書き込み、コンシューマーはデータに依存したレートでデータを読み出し
  • プロセス間のフィードバックが必要なデザインのモデル化が容易
同じ実行数を実行する複数のプロセスを含むデザイン タスクレベルの並列処理は、RTL シミュレーションだけでなく、C シミュレーションでも確認可能。
並列処理の効果をモデル化する RTL シミュレーションが必要  

上記の表に示すように、2 つのタスクレベルの並列処理は、ユース ケースや利点が異なっています。ただし、アプリケーション全体をデータ駆動型 TLP のみで設計することは不可能でも、デザインの一部は純粋なストリーミング デザインとして構築できる場合があります。このようなアプリケーションの作成には、制御駆動型とデータ駆動型の混合モデルが有効です。GitHub の mixed_control_and_data_driven を例にあげます。

void dut(int in[N], int out[N], int n) {
#pragma HLS dataflow
  hls_thread_local hls::split::round_robin<int, NP> split1;
  hls_thread_local hls::merge::round_robin<int, NP> merge1;
 
  read_in(in, n, split1.in);
 
  // Task-Channels
  hls_thread_local hls::task t[NP];
  for (int i=0; i<NP; i++) {
#pragma HLS unroll
    t[i](worker, split1.out[i], merge1.in[i]);
  }
 
  write_out(merge1.out, out, n);
}

上記の例では、2 つの異なる領域があります。1 つは、シーケンシャル セマンティクスが保持される read_in/write_out 関数を含むデータフロー領域で、read_inwrite_out の前に実行されます。2 つ目は、4 つのタスク (この例では NP = 4 のため) の動的なインスタンシエーションに加えて split または merge チャネルと呼ばれる一部の特殊なタイプのチャネルが含まれるタスク チャネル領域です。HLS スプリット/マージ ライブラリ で説明するように、スプリット チャネルには、1 つの入力と複数の出力があり、この場合のスプリット チャネルには 4 つの出力があります。同様に、マージ チャネルには複数の入力がありますが、出力は 1 つのみです。

ポートに対しては、これらのチャネルは内部ジョブ スケジューラもサポートします。上記の例では、マージ チャネルとスプリット チャネルの両方が、入力データを 4 つのタスクそれぞれに割り当てるラウンド ロビン スケジューラを選択し、worker_U0 から順に 1 つずつ割り当てています。ロード バランシング スケジューラが選択された場合、入力データは最初に利用可能なワーカー タスクに割り当てられます (この順番はシミュレーションを実行するたびに異なる可能性があるため、非確定的なシミュレーションとなる)。これは純粋なタスク チャネル領域なので、入力ストリームにデータがあれば 4 つのタスクはすぐに並列実行されます。これらのコンセプトの例の詳細は、GitHub の merge_split を参照してください。

上記のコードでは、各タスクがループ内で呼び出され、ループ本体が実行されるたびに異なるチャネル ペアに接続されているかのように見えますが、実際は静的インスタンシエーションであるということに注意してください。

  • つまり、t[i](...) は、dut() が実行されるたびに 1 回だけ呼び出す必要があります。
  • i に対するループは完全に展開し、RTL で対応する 4 つのインスタンスを推論する必要があります。
  • dut() 関数は、テストベンチで 1 回だけ呼び出す必要があります。
  • 各スプリット出力またはマージ入力は、1 つの hls::task インスタンスにのみ関連付ける必要があります。

hls::task オブジェクトの場合、指定順序は任意ですが、制御駆動型データフロー ネットワークの場合 Vitis HLSread_in から write_out へといった一連のプロセスを認識する必要があります。この一連のプロセスを定義するために、Vitis HLS は呼び出し順を使用します。これは hls::tasks では宣言順でもあります。つまり、モデルは上記の例のように、read_in 関数から hls::task 領域へ、最後にデータフロー領域の write_out 関数へといった順序を明示的に定義する必要があります。

一般的な規則を次に示します。
  • 制御ベースのプロセス (通常のデータフロー) で hls::task のストリームが生成される場合、コード内でタスクを宣言する前に呼び出す必要があります。
  • 制御ベースのプロセスで hls::task からストリームが使用される場合、コード内でタスクを宣言した後に呼び出す必要があります。

NP hls::task の各インスタンスは t[i](...) の最初の呼び出しで使用されるチャネルに静的に関連付けられているため、上記の規則に違反すると予期しない結果が生じる可能性があります。

次の図に、タスク チャネルとデータフローが混在する例のグラフを示します。

図 1. タスク チャネルおよびデータフローの混合