HLS スプリット/マージ ライブラリ - 2023.2 日本語

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

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 日本語
重要: コードで hls::split<> または hls::merge<> オブジェクトを使用するには、次の例に示すようにヘッダー ファイル hls_np_channel.h を含めます。

データフロー プロセスでスプリット/マージ チャネルを使用すると、1 対多または多対 1 のチャネルを作成し、複数のタスクにデータを分配したり、複数のタスクのデータを集約したりできます。これらのチャネルにはジョブ スケジューラが組み込まれており、チャネル間でデータを順次に分配または収集するラウンド ロビン方式と、チャネルの空き状況に基づいて決定するロード バランシング方式のいずれかが採用されています。

ヒント: ロード バランシングでは、RTL/協調シミュレーションの結果が非確定的なものとなる可能性があります。この場合、結果の順番に依存しないテストベンチを記述する必要があります。

下の図に示すように、入力ストリームから読み出されたデータは、ラウンド ロビン方式のスケジューラ メカニズムによって分割され、関連するワーカー タスクに分配されます。ワーカーはタスクを完了すると、ここでもラウンド ロビン スケジューラを使用してマージされた出力を 1 つのストリームに書き込みます。

図 1. スプリット/マージ データフロー

スプリット チャネルには、1 つのプロデューサーと複数のコンシューマーがあり、これらを使用してタスクをワーカー セットに分配し、分配ロジックを抽象化して RTL でインプリメントすることでパフォーマンス向上とリソース削減の両方を実現します。次の方式を使用して、1 つの入力を N 個の出力のうちの 1 つへ分配します。

  • ラウンド ロビン: コンシューマーが入力データを一定の巡回順で読み出すため、動作は確定的なものとなりますが、ワーカーの計算負荷を動的に変更するロード シェアリングは許可されません。
  • ロード バランシング: 最初に読み出しを試みるコンシューマーが最初の入力データを読み出すため、優れた負荷分散となりますが、結果は非確定的となります。

マージ チャネルは、複数のプロデューサーと 1 つのコンシューマーを使用し、次のとおり逆の論理で動作します。

  • ラウンド ロビン: プロデューサーの出力データが一定の巡回順でマージされるため、動作は確実に確定的なものとなりますが、ワーカーの計算負荷を動的に変更するロード シェアリングは使用できません。
  • ロード バランシング: この方式のマージ チャネルで、最初にタスクを完了したプロデューサーが、非確定的な結果をチャネルに書き込みます。

スプリットとマージの一般的な考え方は、round_robin スケジューラによって、スプリットの場合はワーカーにデータが分配され、マージの場合はワーカーからデータが読み出されるというものです。そのため、すべてのワーカーが同じ関数を計算した場合、結果は 1 つのワーカーで計算されたものと同じになりますが、パフォーマンスは向上します。

ワーカーが異なる関数を実行する場合、正しいデータ アイテムがワーカーのラウンド ロビン順で (out[0] または in[0] から開始) 正しい関数に送信されるように設計する必要があります。

仕様

スプリット/マージ チャネルの設定は次のとおりです。
hls::split::load_balancing<DATATYPE, NUM_PORTS[, DEPTH, N_PORT_DEPTH]> name; 
hls::split::round_robin<DATATYPE, NUM_PORTS[, DEPTH]> name
hls::merge::load_balancing<DATATYPE, NUM_PORTS[, DEPTH]> name
hls::merge::round_robin<DATATYPE, NUM_PORTS[, DEPTH]> name
説明:
  • round_robin/load_balancing: チャネルで使用するスケジューラ メカニズムのタイプを指定します。
  • DATATYPE: チャネルのデータ型を指定します。この制限は、標準の hls::stream と同様です。DATATYPE には次を設定できます。
    • C++ ネイティブ データ型
    • Vitis HLS 任意精度型 (ap_int<> ap_ufixed<> など)
    • 上記のいずれかの型を含んだユーザー定義の構造体
  • NUM_PORTS: スプリット (1:num) 操作に必要な書き込みポート数、マージ (num:1) 操作に必要な読み出しポート数を示します。
  • DEPTH: メイン バッファーの深さを示すオプションの引数で、スプリット前またはマージ後に配置されます。これはオプションで、指定しない場合のデフォルトの深さは 2 です。
  • N_PORT_DEPTH: スプリット後またはマージ前に適用される出力バッファーの深さを指定するラウンド ロビン方式用のオプションのフィールドです。これはオプションで、指定しない場合のデフォルトの深さは 0 です。
    ヒント: オプションの N_PORT_DEPTH の値を指定するには、DEPTH も指定する必要があります。
  • name: 作成されたチャネル オブジェクトの名前を示します。
次に、GitHub で公開されている simple_data_driven の例を示します。
#include "hls_np_channel.h"

const int N = 16;
const int NP = 4;

void dut(int in[N], int out[N], int n) {
#pragma HLS dataflow
  hls::split::round_robin<int, NP> split1;
  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);
}
ヒント: 上記の例では、ワーカーを hls::task オブジェクトとしてインプリメントしています。ただし、これは単に例の機能であり、split/merge チャネルの要件ではありません。

スプリット/マージの応用

スプリットおよびマージの主な用途は、複数の計算エンジンのインスタンシエーションをサポートして DDR または HBM ポートの帯域幅を十分に活用することです。この場合のプロデューサーは MAXI からデータ バーストを読み出すロード プロセスで、個々のパケットのデータをスプリット チャネルを介して複数のワーカーに渡して処理します。ワーカーに同程度の時間がかかる場合はラウンド ロビン プロトコルを使用し、入力ごとの実行時間が異なる場合はロード バランシングを使用します。コンシューマーはこれとは逆のプロセスを実行し、データを DRAM に再度書き込みます。

ヒント: 再書き込みアドレスは、ロード バランシングの場合はスプリットおよびマージを介してデータと共に渡すことができます。

これらのチャネルは、スプリット チャネルまたはマージ チャネルの両端に hls::stream オブジェクトをインプリメントしてモデル化されています。つまり、スプリットまたはマージ チャネルの端は、hls::stream を入力または出力として受け取るプロセスに接続できます。このプロセスでは、チャネル接続の種類は不問です。したがって、標準的なデータフローと hls::task オブジェクトの両方に使用できます。

次の例は、1 つのプロデューサーと複数のコンシューマーでスプリットを使用する方法を示しています。

#include "hls_np_channel.h"
 
void producer(hls::stream<int> &s) {
  s.write(xxx);
}
 
void consumer1(hls::stream<int> &s) {
  ... = s.read();
}
 
void consumer2(hls::stream<int> &s) {
  ... = s.read();
}

void top-func() {
#pragma HLS dataflow
  hls::split::load_balancing<int, 4, 6> s; // NUM_PORTS=4, DEPTH=6
 
  producer(s.in, ...);
  consumer1(s.out[0], ...);
  consumer2(s.out[1], ...);
  consumer3(s.out[2], ...);
  consumer4(s.out[3], ...);
}