hls::split<>
または hls::merge<>
オブジェクトを使用するには、次の例に示すようにヘッダー ファイル hls_np_channel.h を含めます。データフロー プロセスでスプリット/マージ チャネルを使用すると、1 対多または多対 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: 作成されたチャネル オブジェクトの名前を示します。
#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], ...);
}