配列をストリーム オブ ブロックとして指定 - 2023.2 日本語

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

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

hls::stream_of_blocks 型は、データフロー コンテキスト内のプロセス レベルのインターフェイスのデータのストリーミング ブロックをサポートするユーザー同期ストリームを提供します。各ブロックは配列または多次元配列です。ストリーム オブ ブロックの目的は、データフロー領域内のプロセスのペア間の配列ベースの通信を置き換えることにあります。GitHub の using_stream_of_blocks の例を参照してください。

現在のところ、Vitis HLS は、プロデューサー プロセスで書き込まれてコンシューマー プロセスで読み出される配列をピンポン バッファー (PIPO) にマップすることで、データフロー領域にインプリメントしています。PIPO バッファーのバッファー交換は C++ でのプロデューサー関数の結果が戻った時とコンシューマー関数を呼び出した時に発生します。

ストリーム オブ ブロックの記述形式

一方、ストリーム オブ ブロックの場合、プロデューサーとコンシューマー間の通信は配列のようなオブジェクトのストリームとして記述され、PIPO を介した配列転送よりも利点があります。

コードでストリーム オブ ブロックを使用するには、次のインクルード ファイルが必要です。

#include "hls_streamofblocks.h"

ストリーム オブ ブロック オブジェクトのテンプレートは次のとおりです。

hls::stream_of_blocks<block_type, depth> v

説明:

  • <block_type> は、ストリーム オブ ブロックによって保持される配列または多次元配列のデータ型を指定します
  • <depth> は、hls::stream または PIPO と同様に深さ制御するオプションの引数で、指定した時間でプロデューサーが取得したブロックとコンシューマーが取得したブロックを含むブロックの合計数を指定します。デフォルト値は 2 です。
  • v は、ストリーム オブ ブロックの変数名を指定します。

ストリーム オブ ブロックのブロックにアクセスするには、次の手順を実行します。

  1. ストリームに最初にアクセスするプロデューサーまたはコンシューマー プロセスは、hls::write_lock または hls::read_lock オブジェクトを使用して、ストリームへのアクセス権を取得する必要があります。
  2. プロデューサーがロックを取得したら、取得したブロックの書き込み (読み出し) を開始できます。ブロックが完全に初期化されたら、write_lock オブジェクトが有効範囲外になったときに、プロデューサーにより解放できます。

    注記: 新しく取得されたバッファーには初期化されていないデータが含まれていると想定されるため、既に書き込まれた場所からの読み出すだけであれば、write_lock を使用したプロデューサー プロセスがブロックを読み出すこともできます。ブロックの書き込みと読み出しの機能は、プロデューサー プロセスに固有であり、コンシューマーではサポートされません。

  3. 次に、ブロックが FIFO 形式でブロック オブ ストリームのキューに入れられ、コンシューマーが read_lock オブジェクトを取得すると、このブロックをコンシューマー プロセスにより読み出すことができます。

前の例で示されている hls::stream_of_blocks と PIPO メカニズムの主な違いは、ブロックがプロデューサー プロセスの戻り値が出力される際だけでなく、write_lock が範囲外になるとすぐにコンシューマーに提供できるようになる点です。このため、ブロックのストリームの場合、PIPO を使用した場合と比較して、ストレージの量が大幅に少なくなります (つまり、2xMxN ではなく 2N になります)。

プロデューサーは、b という hls::write_lock オブジェクトを作成し、それを s という名前のストリーム オブ ブロック オブジェクトへの参照に渡して、ブロックを取得します。write_lock オブジェクトは、オーバーロードされた配列アクセス演算子を提供するので、次の例に示すように、基盤となるストレージにランダムな順序で配列としてアクセスできるようになります。

ロックの取得は write_lock/read_lock オブジェクトを構築することによって実行され、そのオブジェクトが範囲外になったときにそのオブジェクトが破棄されると、自動的に解放されます。このアプローチでは、よくある RAII (Resource Acquisition Is Initialization) 形式のロックおよびアンロックを使用します。

#include "hls_streamofblocks.h"
typedef int buf[N];
void producer (hls::stream_of_blocks<buf> &s, ...) {
  for (int i = 0; i < M; i++) {
    // Allocation of hls::write_lock acquires the block for the producer
    hls::write_lock<buf> b(s);
    for (int j = 0; j < N; j++)
      b[f(j)] = ...;
    // Deallocation of hls::write_lock releases the block for the consumer
  }
}
  
void consumer(hls::stream_of_blocks<buf> &s, ...) {
  for (int i = 0; i < M; i++) {
    // Allocation of hls::read_lock acquires the block for the consumer
    hls::read_lock<buf> b(s);
    for (int j = 0; j < N; j++)
       ... = b[g(j)] ...;
    // Deallocation of hls::write_lock releases the block to be reused by the producer
  }
}
  
void top(...) {
#pragma HLS dataflow
  hls::stream_of_blocks<buf> s;
  
  producer(b, ...);
  consumer(b, ...);
}

このアプローチの主な特長は、次のとおりです。

  • 上記のプロデューサーの外部ループのパフォーマンスは、全体の開始間隔 (II) = 1 を達成できるはずです。
  • ロックされたブロックは、解放されるまで、プロデューサーまたコンシューマー プロセスに対してプライベートであるかのように使用できます。
  • プロデューサーの配列オブジェクトの初期状態は未定義ですが、コンシューマー用にプロデューサーによって書き込まれた値が含まれます。
  • ストリーム オブ ブロックの主な利点は、コンシューマーとプロデューサーの複数の反復をオーバーラップして実行し、スループットを向上させるところです。

リソース使用量

デフォルト値の 2 よりも深さを増やす場合のリソース コストは、PIPO のリソース コストと似ています。つまり、1 増えるごとに、たとえば上記の N * 32 ビット ワードの例のように、ブロックには十分なメモリが必要となります。

ストリーム オブ ブロック オブジェクトは、たとえば最上位関数など、ストリーム オブ ブロックが宣言される場所に BIND_STORAGE プラグマを配置すると、特定の RAM タイプにバインドできます。ストリーム オブ ブロックは、デフォルトで 2 ポートのブロック RAM (type=RAM_2P) を使用します。