アドバンス トピック: 複数の計算ユニットおよびカーネルのストリーミング

前述のホスト コードおよびカーネルの移行に関する説明では、SDSoC™ 環境から Vitis 環境に移行するために必要な手順をわかりやすくするため、単純なアプリケーションを使用しました。次の例では、複数のカーネルを同時実行するより複雑なアプリケーションを使用して、アドバンス デザイン パターンを示します。

このセクションでは、複数のハードウェア アクセラレータを含むアプリをビルドする方法と、SDSoC 環境を Vitis 環境に移行する際のその他の要素を示します。サンプル デザインには、データを行列乗算にストリーミングする 2 つの transpose アクセラレータが含まれます。このセクションでは、次について説明します。

  • カーネルの複製
  • AXI4-Stream 接続を使用したカーネル間でのデータ移動
  • ホスト アプリケーションへのフロー制御のコード記述

複数の計算ユニット

アクセラレーション環境では、同じカーネルの複数のインスタンスを使用した並列処理も含め、アルゴリズムを複数のハードウェア関数を指定してインプリメントできます。

SDSoC 環境では、各ハードウェア アクセラレータのコピーが 1 つ必要だとツール想定されます。アクセラレータを複製するには、次に示すように main 関数にプラグマを追加します。

#pragma SDS resource(1) 
transpose(matA, matAT, col, row);
#pragma SDS resource(2)
transpose(matB, matBT, col, row);
mmult(matAT, matBT, matC, col, row);

各計算ユニットにアクセスするには、次に示すように、ホスト アプリケーションで異なる cl::Kernel() オブジェクトを作成して個別の名前を使用します。

cl::Kernel kernel_mmult(program,"mmult");
cl::Kernel kernel_transpose1(program,"transpose:{transpose_1}");
cl::Kernel kernel_transpose2(program,"transpose:{transpose_2}");

Vitis 環境では、v++ コマンドで --connectivity.nk オプションを使用してカーネル (計算ユニット) のインスタンス数を指定します。--connectivity.nk オプションは、xlcbin ファイルにインスタンシエートするカーネルの数と名前マップを指定します。次のコードでは、kernel_mmult の mmult という名前のインスタンスと、kernel_transpose の transpose_1 および transpose_2 という名前のインスタンスを定義しています。

v++ -l --connectivity.nk mmult:1:mmult_1 --connectivity.nk transpose:2:transpose_1.transpose_2

カーネル間ストリーミング

SDSoC 環境と同様、Vitis 環境では、データを 1 つのカーネルからグローバル メモリを介さずに別のカーネルに転送できます。次に、カーネル、ビルド スクリプト、およびホスト アプリに必要な変更を説明します。

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

データを別のカーネル ストリーミング インターフェイスに直接送信または受信するカーネル ストリーミング インターフェイスは、hls::stream で ap_axiu<D,0,0,0> データ型を使用して定義する必要があります。ap_axiu<D,0,0,0> データ型には、#include を使用して ap_axi_sdata.h を含める必要があります。次の transpose カーネル コードは、インクルード ファイルを追加した後、m_axi 入力とストリーミング出力をインプリメントします。

#include <ap_int.h>
#include <hls_stream.h>
#include <ap_axi_sdata.h>
#include "transpose.h"
 
void transpose(int A[BUFFER_SIZE*BUFFER_SIZE], int AT[BUFFER_SIZE*BUFFER_SIZE], int col, int row) {
    for (int i = 0; i < col; i++) {
#pragma PIPELINE II=1
        for (int j = 0; j < row; j++) {
            AT[i*col+j] = A[j*col+i];
        }
    }

    }

そして、ポートおよびポート インターフェイスを定義します。

void transpose(int *A, hls::stream<ap_axiu<32, 0, 0, 0> >& stream, const int col, const int row) {
#pragma HLS INTERFACE m_axi port=A offset=slave bundle=gmem
#pragma HLS INTERFACE axis port=stream
#pragma HLS INTERFACE s_axilite port=col bundle=control
#pragma HLS INTERFACE s_axilite port=row bundle=control
#pragma HLS INTERFACE s_axilite port=return bundle=control

pragma HLS INTERFACE axis port=stream は、ポート ストリームを AXI4-Stream に設定します。

mmult 関数は、ホストに 2 つのストリーミング入力と 1 つのメモリ マップド出力をインプリメントします。

カーネルの接続

アプリケーションをビルドする際、v++ --connectivity.sc オプションを使用してカーネル間の接続を定義します。次に、transpose_1 と transpose_2 のストリーミング出力ポートを mmult の入力ポートに接続する例を示します。

v++ -l …. --connectivity.sc transpose_1.stream:mmult_1.inputa --connectivity.sc transpose_2.stream:mmult_1.inputb

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

SDSoC 環境と同様、アプリケーションでカーネルとアクセラレータの間にフロー制御をインプリメントする必要があります。この例では、OpenCL API を使用して順不同のコマンド キューとイベントを設定してカーネルの実行を制御します。次の main() 関数コードの抜粋では、キューは次のように定義されています。

// Create Command Queue 
cl::CommandQueue q(context, device, CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE | CL_QUEUE_PROFILING_ENABLE);  

順不同のキューを使用する場合、イベントを使用して各コマンド間の依存性を指定する必要があります。XRT スケジューラは、これらのイベントを使用して、エンキューされているコマンドを実行するタイミングとその順序を判断します。

次に示すように、データをカーネルに書き込む際に制御がホスト プログラムに返されるように、CL_FALSE を使用して enqueueWriteBuffer() をノンブロッキングにします。

q.enqueueWriteBuffer(bufMatA, CL_FALSE, 0,
max_col*max_row*sizeof(int), matA, NULL, &events[0]);

q.enqueueWriteBuffer(bufMatB, CL_FALSE, 0, 
max_col*max_row*sizeof(int), matB, NULL, &events[1]);

次に示すように、ホストと計算ユニットの間にイベントを使用したフロー制御がインプリメントされます。

// Create Events
std::vector<cl::Event> events(4);
std::vector<cl::Event> kernel_events;
...

// Setup event dependencies for transpose_1
kernel_events.resize(0);
kernel_events.push_back(events[0]);
// place transpose_1 in the command queue with a ready for exectution flag
q.enqueueTask(kernel_transpose1, &kernel_events, NULL);

// Setup event dependencies for transpose_2
kernel_events.resize(0);
kernel_events.push_back(events[1]);
// place transpose_2 in the command queue with a ready for exectution flag
q.enqueueTask(kernel_transpose2, &kernel_events, NULL);

// place mmult in the command queue with an execution complete event
// axi stream protocol determins data flow control between the transpose and mmult compute units
q.enqueueTask(kernel_mmult, NULL, &events[2]);

enqueueTask() の 2 番目の引数は、このコマンドを実行する前に完了する必要のあるイベントをリストします。リストが NULL の場合、このコマンドは即実行できます。

enqueueTask() の 3 番目の引数は、このコマンドの完了イベントを指定します。この例では、matA および matB の転送に使用する enqueueWriteBuffer() コマンドの完了を確認するのに、events[0] と events[1] を使用しています。その後、これらの 2 つのイベントは transpose_1 および transpose_2 タスクの入力依存性として使用されます。transpose カーネルは matA および matB に対して演算を実行するので、カーネルが開始する前にこれらのバッファーの転送が完了している必要があります。

mmult は、ハードウェア レベルでの入力へのデータフロー制御に AXI4-Stream プロトコルを使用し、events[] への依存なしで実行を開始します。

複数のカーネルを使用する場合の詳細は、Vitis ソフトウェア プラットフォームでのアプリケーションのアクセラレーション手法 を参照してください。