ストリーミング接続

ホストとカーネル間 (H2K) のストリーミング データ転送

Vitis コア開発キットには、ホストからカーネルおよびカーネルからホストへのデータの直接ストリーミングをサポートするプログラミング モデルが含まれるので、中間段階としてデータをグローバル メモリに移行する必要がありません。このプログラミング モデルを使用すると、より大きく遅いグローバル メモリ バンクと比較して最小限のストレージしか使用しないので、パフォーマンスと消費電力の両方がかなり改善されます。

データ ストリームを使用する場合、次のような利点があります。

  • ホスト アプリケーションは、カーネルからのデータ サイズを知る必要がありません。
  • ホスト メモリのデータは、必要になると即座にカーネルに転送できます。
  • 処理されたデータを必要な場合にカーネルから転送し戻すことができます。

    ホストからカーネルおよびカーネルからホストへのストリーミングは、Alveo データセンター アクセラレータ カードなどの PCIe ベースのプラットフォームでのみサポートされます。ただし、カーネルどうし間のストリーミング データ転送は PCIe ベースのプラットフォームとエンベデッド プラットフォームの両方でサポートされます。また、この機能は Alveo データセンター アクセラレータ カード用の QDMA プラットフォームなどの特定のターゲット プラットフォームでのみ使用できます。プラットフォームがストリーミングをサポートするように設定されていないと、アプリケーションは実行されません。

ホスト コーディング ガイドライン

ザイリンクスでは、拡張 API としてストリーミング操作用に新しい OpenCL™ API を提供しています。

clCreateStream()
読み出しまたは書き込みストリームを作成します。
clReleaseStream()
作成したストリームと関連するメモリを空けます。
clWriteStream()
データをストリームに書き込みます。
clReadStream()
ストリームからデータを取得します。
clPollStreams()
デバイスのストリームをポーリングして終了します。ノンブロッキング ストリーム操作にのみ必要です。

典型的な API フローは次のようになります。

  • clCreateStream で必要な数の読み出し/書き込みストリームを作成します。
    • ストリームはコマンド キューを使用しないので、直接 OpenCL デバイス オブジェクトに接続する必要があります。ストリーム自体はデータを特定の方向 (ホストからカーネルまたはカーネルからホスト) に渡すだけのコマンド キューです。
    • 正しいオプションを使用して、ホスト プログラムのパースペクティブからストリームを CL_STREAM_READ_ONLY または CL_STREAM_WRITE_ONLY として記述する必要があります。
    • ストリームのデバイスへの接続方法を指定するには、ザイリンクス拡張ポインター オブジェクト(cl_mem_ext_ptr_t) を使用してカーネルとストリームが関連付けられた引数を指定します。

      重要: ストリーミング カーネルに複数の計算ユニットが含まれる場合、ホスト コードは各計算ユニットに一意の cl_kernel オブジェクトを使用する必要があります。ホスト コードには clCreateKernel<kernel_name>:{compute_unit_name} を付けて、各計算ユニットを取得して、それらのストリームを作成して、個別にエンキューする必要があります。

      次のコード例では、read_stream および write_stream が作成され、cl_kernel オブジェクトに関連付けられ、カーネル引数が指定されています。

      #include <CL/cl_ext_xilinx.h> // Required for Xilinx extension pointer
       
      // Device connection specification of the stream through extension pointer
      cl_mem_ext_ptr_t  ext;  // Extension pointer
      ext.param = kernel;     // The .param should be set to kernel 
      						  (cl_kernel type)
      ext.obj = nullptr;
       
      // The .flag should be used to denote the kernel argument
      // Create write stream for argument 3 of kernel
      ext.flags = 3;
      cl_stream write_stream = clCreateStream(device_id, CL_STREAM_WRITE_ONLY, CL_STREAM, &ext, &ret);
       
      // Create read stream for argument 4 of kernel
      ext.flags = 4;
      cl_stream read_stream = clCreateStream(device_id, CL_STREAM_READ_ONLY, CL_STREAM, &ext,&ret);
  • 残りのストリーミング以外のカーネル引数を設定し、カーネルをエンキューします。次のコードでは、典型的なカーネル引数 (バッファーやスカラーなどのストリーム以外の引数) とカーネル エンキューを設定しています。
    // Set kernel non-stream argument (if any)
    clSetKernelArg(kernel, 0,...,...);
    clSetKernelArg(kernel, 1,...,...);
    clSetKernelArg(kernel, 2,...,...);
    // Argument 3 and 4 are not set as those are already specified during 
    // the clCreateStream through the extension pointer
     
    // Schedule kernel enqueue
    clEnqueueTask(commands, kernel, . .. . );
  • clReadStream および clWriteStream コマンドで読み出しおよび書き込み転送を開始します。
    • 読み出しおよび書き込みリクエストに関連する CL_STREAM_XFER_REQ 属性が使用されているところに注意してください。
    • 転送メカニズムを示すには、.flag を使用します。
      CL_STREAM_EOT
      現在のところ、EOT (End of Transfer) 信号で転送の終わりを示すことで、問題のないストリーム転送メカニズムになるようになっています。このフラグは、現在のリリースでは必須です。
      CL_STREAM_NONBLOCKING
      デフォルトでは、読み出し転送および書き込み転送はブロッキングです。ノンブロッキング転送の場合は、CL_STREAM_NONBLOCKING を設定する必要があります。
    • 転送に関連するストリングを指定するには、.priv_data を使用します。これにより、ストリーミングの終了をポーリングする際に特定の転送が終了したことを示すことができます。ノンブロッキング バージョンの API を使用する場合にのみ必要です。

      次のコード例の場合、ストリーム読み出しおよび書き込み転送がノンブロッキング方式を使用して実行されます。

      // Initiate the READ transfer
      cl_stream_xfer_req rd_req {0};
       
      rd_req.flags = CL_STREAM_EOT | CL_STREAM_NONBLOCKING;
      rd_req.priv_data = (void*)"read"; // You can think this as tagging the 
      									 transfer with a name
       
      clReadStream(read_stream, host_read_ptr, max_read_size, &rd_req, &ret);
       
      // Initiating the WRITE transfer
      cl_stream_xfer_req wr_req {0};
       
      wr_req.flags = CL_STREAM_EOT | CL_STREAM_NONBLOCKING;
      wr_req.priv_data = (void*)"write";
       
      clWriteStream(write_stream, host_write_ptr, write_size, &wr_req , &ret);
  • すべてのストリームをポーリングして終了します。ノンブロッキング転送の場合、読み出し/書き込み転送を確実に終了するためのポーリング API が提供されています。ブロッキング バージョンの API の場合、ポーリングは必要ありません。
    • ポーリング結果は cl_streams_poll_req_completions 配列に格納され、ストリーミング イベント結果を検証および確認をするのに使用できます。
    • clPollStreams はブロッキング API です。すべてのストリーム リクエストが終了したことを示す通知を受け取るか、指定したタイムアウトになったら、ホスト コードに実行を戻します。
      // Checking the request completion
         cl_streams_poll_req_completions poll_req[2] {0, 0}; // 2 Requests
       
         auto num_compl = 2;
         clPollStreams(device_id, poll_req, 2, 2, &num_compl, 5000, &ret);
         // Blocking API, waits for 2 poll request completion or 5000ms, 
            whichever occurs first
  • ホストでストリーム データを読み出して使用します。
    • ポーリング リクエストが問題なく終了したら、ホストがホスト ポインターからデータを読み出すことができるようになります。
    • ホストはホストに転送されたデータのサイズをチェックすることもできます。この目的の場合、ホストは priv_data をマッチングさせて、cl_streams_poll_req_completions structure からの nbytes (転送されるバイト数) をフェッチして、正しいポーリング リクエストを見つける必要があります。
      for (auto i=0; i<2; ++i) { 
          if(rd_req.priv_data == poll_req[i].priv_data) { // Identifying the 
      													   read transfer
              // Getting read size, data size from kernel is unknown
              ssize_t result_size=poll_req[i].nbytes;      
              }
          }

関数プロトタイプおよび引数の説明を含むヘッダー ファイルは、ザイリンクス ランタイム GitHub リポジトリから入手できます。

カーネル コーディング ガイドライン

システム ベースの C カーネルを開発する基本的なガイドラインは、次のとおりです。

  • hls::streamqdma_axis<D,0,0,0> データ型で使用します。qdma_axis データ型には、ヘッダー ファイル ap_axi_sdata.h が必要です。
  • qdma_axis<D,0,0,0> は、ストリーミング プラットフォームを使用する際、ホストとカーネル間のデータ転送に使用される特殊なクラスで、ホストと関連するストリーミング カーネル インターフェイスでのみ使用され、ほかのカーネルと関連するものには使用されません。テンプレートのパラメーター <D> はデータ幅を示します。残りの 3 つのパラメーターは現在のリリースでは使用しないように、0 に設定する必要があります。
  • 次のコードは、入力ストリーム 1 つと出力ストリーム 1 つを含む単純なカーネル インターフェイスを示しています。
    #include "ap_axi_sdata.h"
    #include "hls_stream.h"
     
    //qdma_axis is the HLS class for stream data transfer between host and kernel for streaming platform
    //It contains "data" and two sideband signals (last and keep) exposed to the user via class member function. 
    typedef qdma_axis<64,0,0,0> datap;
     
    void kernel_top (
                 hls::stream<datap> &input,
                 hls::stream<datap> &output,
                 ..... , // Other Inputs/Outputs if any                   
                 )
    {
        #pragma HLS INTERFACE axis port=input
        #pragma HLS INTERFACE axis port=output
    }
  • qdma_axis データ型には、カーネル コード内で使用する必要のある 3 つの変数が含まれます。
    data
    qdma_axis には ap_uint<D> が含まれ、.get_data() および .set_data() メソッドでアクセスされます。
    • D は 8、16、32、64、128、256、または 512 ビット幅である必要があります。
    last
    last 変数は入力ストリームおよび出力ストリームの最後の値を示します。入力ストリームからの読み込みがあると、last でストリームの終わりが検出されます。同様に、カーネルが出力ストリームに書き込んでホストに転送される際にも、last を設定してストリームの終わりを示す必要があります。
    • get_last/set_last: ストリームの最後のデータを示すのに使用される last 変数を取得および設定します。
    keep
    keep は、特殊な状況で、last データをより少ないバイト数に切り捨てるために使用されます。ただし、keep はストリームの last データ以外のデータには使用できません。このため、ほとんどの場合、keep を 1 に設定してカーネルからすべてのデータを出力させるようにする必要があります。
    • get_keep/set_keep: keep 変数を取得/設定します。
    • last データの前のデータすべてに対して keep を 1 に設定し、データのすべてのバイトが有効であることを示す必要があります。
    • last データに対しては、カーネルにより少ないバイトを送信する柔軟性があります。たとえば、4 バイトを転送する際、カーネルは次の set_keep() 関数を使用して、1 バイト、2 バイト、または 3 バイトを送信して、last データを切り捨てることができます。
      • last データが 1 バイト ≥ .set_keep(1)
      • last データが 2 バイト ≥ .set_keep(3)
      • last データが 3 バイト ≥ .set_keep(7)
      • last データが 4 バイト (last 以外のデータすべてと同様) ≥ .set_keep(-1)
  • 次のコードは、input ストリームがどのように読み込まれるかを示しています。.last により、最後のデータが決定されています。
    // Stream Read
    // Using "last" flag to determine the end of input-stream
    // when kernel does not know the length of the input data
     hls::stream<ap_uint<64> >   internal_stream;
     while(true) {
            datap temp = input.read(); // "input" -> Input stream
            internal_stream << temp.get_data();  // Getting data from the 
    		stream
            if(temp.get_last())  // Getting last signal to determine the 
    		EOT (end of transfer). 
                break;
     }
  • 次のコードは、output ストリームがどのように書き出されるかを示しています。set_keep はすべてのデータに対して -1 に設定されています。また、カーネルは set_last() を使用して、ストリームの last データを指定しています。
    重要: ホストおよびカーネル システムが正しく機能するようにするには、last ビットを設定することが大変重要です。
    // Stream Write
    for(int j = 0; j <....; j++) {
          datap t;
          t.set_data(...);
          t.set_keep(-1);        // keep flag -1 , all bytes are valid
          if(... )               // check if this is last data to be write
             t.set_last(1);      // Setting last data of the stream
          else
             t.set_last(0);
          output.write(t);  	 // output stream from the kernel
    }

カーネル間 (K2K) のストリーミング データ転送

Vitis コア開発キットでは、2 つのカーネル間のストリーミング データ転送もサポートされます。1 つ目のカーネルが計算の一部を実行し、2 つ目のカーネルは 1 つ目のカーネルからの出力データを受信してから、演算を終了します。カーネル間のストリーミングがサポートされるので、グローバル メモリを介して転送し戻さなくてもデータはカーネル間を直接移動できます。これにより、パフォーマンスがかなり改善されます。
重要: この機能は Alveo データセンター アクセラレータ カード用の QDMA プラットフォームなどの特定のターゲット プラットフォームでのみ使用できます。プラットフォームがストリーミングをサポートするように設定されていないと、アプリケーションは実行されません。

ホスト コーディング ガイドライン

カーネル間のストリーミングに使用されるカーネル ポートは、ホスト コードからの clSetKernelArg を使用して設定する必要がありません。カーネル引数の設定 に示すように、ストリーミング接続に使用されないカーネル引数はすべて clSetKernelArg を使用して設定する必要があります。ただし、ストリーミングに使用されるカーネル ポートはそのカーネル内で定義されるので、ホスト プログラムでは呼び出されません。

ストリーミング カーネルのコーディング ガイドライン

データを直接別のカーネル ストリーミング インターフェイスに送信または受信するカーネルのストリーミング インターフェイスはap_axiu<D,0,0,0> データ型を使用して hls::stream で定義します。ap_axiu<D,0,0,0> データ型には、ap_axi_sdata.h ヘッダー ファイルを使用する必要があります。

重要: ホストからカーネルおよびカーネルからホストへのストリーミングには、ホストとカーネル間 (H2K) のストリーミング データ転送 で説明するように、qdma_axis データ型を使用する必要があります。ap_axiu および qdma_axis データ型はどちらも Vitis ソフトウェア プラットフォームのインストールに含まれる ap_axi_sdata.h ファイル内で定義されます。
次の例は、プロデューサーおよびコンシューマー カーネルのストリーミング インターフェイスを示しています。
// Producer kernel - provides output as a data stream
// The example kernel code does not show any other inputs or outputs.

void kernel1 (.... , hls::stream<ap_axiu<32, 0, 0, 0> >& stream_out) {
#pragma HLS interface axis port=stream_out
      
  for(int i = 0; i < ...; i++) {
    int a = ...... ;         // Internally generated data
    ap_axiu<32, 0, 0, 0> v;  // temporary storage for ap_axiu
    v.data = a;              // Writing the data
    stream_out.write(v);         // Writing to the output stream.
  }
}
 
// Consumer kernel - reads data stream as input
// The example kernel code does not show any other inputs or outputs.

void kernel2 (hls::stream<ap_axiu<32, 0, 0, 0> >& stream_in, .... ) {
#pragma HLS interface axis port=stream_in
 
  for(int i = 0; i < ....; i++) {
    ap_axiu<32, 0, 0, 0> v = stream_in.read(); // Reading the input stream
    int a = v.data; // Extract the data
          
    // Do further processing
  }
}
ヒント: 上記のサンプルのカーネルは、カーネル シグネチャのストリーミング入力/出力ポートの定義と、カーネル コード内の入力/出力ストリームの処理方法を示しています。kernel1 から kernel2 への接続は、計算ユニット間のストリーミング接続の指定 で説明するように、カーネル リンク プロセス中に定義する必要があります。

フリーランニング カーネル

Vitis コア開発キットでは、1 つまたは複数のフリーランニング カーネルがサポートされます。フリーランニング カーネルには制御信号ポートがないので、開始または停止できません。このフリーランニング カーネルの制御信号ではない信号には、次のような特徴があります。

  • フリーランニング カーネルにはメモリ入力または出力ポートがないので、ホストまたはその他のカーネル (標準カーネルまたは別のフリーランニング カーネル) とはストリームを介してしか通信できません。
  • FPGA がバイナリ コンテナー (xclbin) によりプログラムされると、フリーランニング カーネルが FPGA で開始されるので、ホスト コードからの clEnqueueTask コマンドは必要ありません。
  • カーネルがホストまたはその他のカーネルからストリーム データを受信開始するとすぐに処理し、使用可能なデータがなくなると停止するからです。
  • フリーランニング カーネルには、カーネル ボディ内に特殊なインターフェイス プラグマ ap_ctrl_none が必要です。

フリーランニング カーネルのホスト コーディング

ホストとカーネル間 (H2K) のストリーミング データ転送ホスト コーディング ガイドライン で説明したように、フリーランニング カーネルがホストと通信する場合は、ホスト コードで clCreateStream/clReadStream/clWriteStream で処理されるストリームが管理されるようにする必要があります。フリーランニング カーネルには、メモリ ポートや制御ポートなどの入力または出力以外のタイプのポートがないので、clSetKernelArg を指定する必要はありません。clEnqueueTask が使用されないのは、カーネルがホストまたはその他のカーネルからストリーム データを受信開始するとすぐに処理し、使用可能なデータがなくなると停止するからです。

フリーランニング カーネルのコーディング ガイドライン

前述したように、フリーラニング カーネルには hls::stream 入力と出力しか含まれません。推奨されるガイドラインは次のとおりです。

  • ポートがカーネルからの別のストリーミング ポートと接続される場合は、hls::stream<ap_axiu<D,0,0,0> > を使用。
  • ポートがホストと接続される場合は、hls::stream<qdma_axis<D,0,0,0> > を使用。

プラグマを使用するガイドラインは次のとおりです。

  • カーネル インターフェイスには #pragma HLS interface s_axilite または #pragma HLS interface m_axi を含めないようにします (メモリまたは制御ポートにはできないので)。
  • カーネル インターフェイスには、次の特殊なプラグマを含める必要があります。
    #pragma HLS interface ap_ctrl_none port=return

次のコード例は、入力 1 つと出力 1 つを含むフリーラニング カーネルが別のカーネルと通信するところを示しています。while(1) ループ構造には、カーネル コードの内容が含まれ、カーネルが実行される限り繰り返されます。

void kernel_top(hls::stream<ap_axiu<32, 0, 0, 0> >& input, 
   hls::stream<ap_axiu<32, 0, 0, 0> >& output) {
#pragma HLS interface axis port=input
#pragma HLS interface axis port=output
#pragma HLS interface ap_ctrl_none port=return  // Special pragma for free-running kernel
 
#pragma HLS DATAFLOW // The kernel is using DATAFLOW optimization
	while(1) {
		...
	}
}
ヒント: この例には、フリーラニング カーネルのストリーミング入力/出力ポートの定義が表示されていますが、フリーランニング カーネルと別のカーネル間のストリーミング接続は、計算ユニット間のストリーミング接続の指定 で説明するように、カーネル リンク プロセス中に定義する必要があります。