ホスト アプリケーションの移行

このセクションでは、main() とアクセラレーション関数を含む単純な SDSoC™ プログラムを参照し、変更が必要な要素を特定します。アプリケーションとハードウェア関数を Vitis 環境プラットフォームおよびツールに移行するプロセスを開始するには、まず main 関数とハードウェア関数のコードを調べます。ここに示すコードは、mmult サンプル アプリケーションのものです。

次のコードは、元の開発アプリケーション プロジェクトの main() 関数の例です。

#include <stdlib.h>
#include <iostream>
#include "mmult.h"
#include "sds_lib.h"

#define NUM_TESTS 5

void printMatrix(int *mat, int col, int row) {
	for (int i = 0; i < col; i++) {
		for (int j = 0; j < row; j++) {
			std::cout << mat[i*row+j] << "\t";
		}
		std::cout << std::endl;
	}
	std::cout << std::endl;
}

int main() {
	int col = BUFFER_SIZE;
	int row = BUFFER_SIZE;
	int *matA = (int*)sds_alloc(col*row*sizeof(int));
	int *matB = (int*)sds_alloc(col*row*sizeof(int));
	int *matC = (int*)sds_alloc(col*row*sizeof(int));

	std::cout << "Mat A" << std::endl;
	printMatrix(matA, col, row);

	std::cout << "Mat B" << std::endl;
	printMatrix(matB, col, row);

	//Run the hardware function multiple times
	for(int i = 0; i < NUM_TESTS; i++) {
		std::cout << "Test #: " << i << std::endl;
		// Populate matA and matB
		srand(time(NULL));
		for (int i = 0; i < col*row; i++) {
			matA[i] = rand()%10;
			matB[i] = rand()%10;
		}

		std::cout << "MatA * MatB" << std::endl;
		mmult(matA, matB, matC, col, row);

	}
	printMatrix(matC, col, row);

	return 0;

このコードは、1 次元配列として格納されている 3 つの 2 次元行列にメモリを割り当て、matA および matB にランダムな値を挿入し、matAmatB を乗算して結果 matC を算出します。結果は画面に表示し、テストを 10 回実行します。

Vitis 環境に移行する際、sds++ コンパイラおよびランタイムで自動的に処理されていたいくつかのタスクを、アプリケーション開発者が管理する必要があります。

インクルード ファイルのアップデート

次のセクションでは、main() 関数でのコード変更について説明します。

次の変更を加える必要があります。

#include <stdlib.h>   
#include <iostream>   
#include "mmult.h"   
//#include "sds_lib.h"   
#include <fstream>  
#include <vector>  
#include <ctime> 

この例では、Arm® コア クロス コンパイラで main() 関数がコンパイルされます。メモリ割り当てに sds_alloc() 関数は使用されなくなっているので、sds_lib の include 文をコメントアウトします。

#define CL_HPP_CL_1_2_DEFAULT_BUILD  
#define CL_HPP_TARGET_OPENCL_VERSION 120  
#define CL_HPP_MINIMUM_OPENCL_VERSION 120  
#define CL_HPP_ENABLE_PROGRAM_CONSTRUCTION_FROM_ARRAY_COMPATIBILITY 1  
#include <CL/cl2.hpp> 

このセクションでは、#define 文でプリプロセッサ マクロを定義して、アプリケーションに使用する OpenCL API のバージョンを指定します。デフォルトでは OpenCL API 2.0 フレームワークが指定されますが、ザイリンクス ツールでは OpenCL API 1.2 リリースがサポートされます。これらのプリプロセッサ マクロの詳細は、https://github.khronos.org/OpenCL-CLHPP の「OpenCL C++ Bindings」を参照してください。

OpenCL API では C および C++ 言語がサポートされますが、デフォルトの C ではなく C++ を使用するよう環境を設定できます。このコード例のように OpenCL C++ バインドを使用するには、#include 文を使用して cl2.hpp ヘッダー ファイルを指定する必要があります。

次の mmult コード例は別にコンパイルされます。

#include "mmult.h"
 
void mmult(int A[BUFFER_SIZE*BUFFER_SIZE], int B[BUFFER_SIZE*BUFFER_SIZE], int C[BUFFER_SIZE*BUFFER_SIZE], int col, int row) {
 
    int matA[BUFFER_SIZE*BUFFER_SIZE];
    int matB[BUFFER_SIZE*BUFFER_SIZE];
 
    readA: for(int i = 0; i < col*row; i++) {
#pragma HLS PIPELINE II=1
        matA[i] = A[i];
    }
 
    readB: for(int i = 0; i < col*row; i++) {
#pragma HLS PIPELINE II=1
        matB[i] = B[i];
    }
 
    for (int i = 0; i < col; i++) {
    #pragma HLS PIPELINE II=1
        for (int j = 0; j < row; j++) {
            int tmp = 0;
            for (int k = 0; k < row; k++) {
                tmp += matA[k+i*col] * matB[j+k*col];
            }
            //C[i+j*col] = tmp;
            C[i*row+j] = tmp;
        }
    }
}

main 関数の読み込み

OpenCL API 環境を初期化するには、ソフトウェア アプリケーションで FPGA バイナリ ファイル (.xclbin) を読み込む必要があります。この例では、argc/argv を使用して、アプリケーションのコマンド ライン引数としてこのファイルの名前を指定します。

注記: これは、可能な方法の 1 つです。xclbin ファイルの名前をアプリケーションにコードとして記述することもできます。

これらを変更した場合、アプリケーションは次のように実行します。

host.exe ./binary_container_1.xclbin

説明:

host.exe
Arm コアのコンパイル済み実行ファイル。
binary_container_1.xclbin
Vitis コンパイラで生成された FPGA バイナリ ファイル。

次に、エラー チェックを追加して必要なコマンド ライン引数が指定されていることを確認します。

// Check for valid arguments  
if (argc != 2) {  
printf("Usage: %s binary_container_1.xclbin\n", argv[0]);  
exit (EXIT_FAILURE);  
}  
// Get xclbin name  
char* xclbinFilename = argv[1];  

メモリ割り当てはコードの後の方で OpenCL バッファーを作成して別に実行するので、入力行列および出力行列の変数宣言も変更します。ここでは、行列データを保持するのに必要な 3 つのベクターを定義します。

OpenCL API の使用

SDSoC 開発環境と Vitis コア開発キットの主な違いは、OpenCL API を使用して main 関数とハードウェア アクセラレーション カーネルの通信を制御することです。コードのこのセクションの開始と終了は、次の文で示されます。

//OPENCL HOST CODE AREA STARTS  
//OPENCL HOST CODE AREA ENDS

ホスト コードを OpenCL C++ API を使用するよう変更し、カーネルとホスト アプリケーションの実行が XRT で制御されるようにします。これらの手順は、次の順で記述されます。

  1. セットアップ
    1. プラットフォームを指定します。
    2. カーネルを実行する OpenCL デバイスを選択します。
    3. OpenCL コンテキストを作成します。
    4. コマンド キューを作成します。
    5. OpenCL プログラムを作成します。
    6. ハードウェア カーネルのカーネル オブジェクトを作成します。
    7. OpenCL デバイスのメモリ バッファーを作成します。
  2. 実行
    1. カーネルの引数を定義します。
    2. データをホスト CPU からカーネルに転送します。
    3. カーネルを実行します。
    4. データをカーネルからホスト アプリケーションに戻します。

次のセクションでは、これらの各手順と必要なコード変更を詳しく説明します。

次のコードでは、プラットフォームとデバイスを指定しています。

// Get Platform  
std::vector<cl::Platform> platforms;  
cl::Platform::get(&platforms);  
cl::Platform platform = platforms[0];  
  
// Get Device  
std::vector<cl::Device> devices;  
cl::Device device;  
platform.getDevices(CL_DEVICE_TYPE_ACCELERATOR, &devices);  
device = devices[0];

プラットフォームは、XRT およびアクセラレータ ハードウェアを含む、OpenCL フレームワークのザイリンクス特定のイプリメンテーションです。デバイスは、OpenCL カーネルを実行するハードウェアです。

デバイスを選択したら、コマンド キュー、メモリ、プログラム、1 つまたは複数のデバイス上のカーネルをランタイムで制御するためのコンテキストを作成する必要があります。また、異なる要求を並列実行してスループットを向上するため、コマンドを順序どおりまたは順不同で実行するコマンド キューを作成する必要もあります。これは、次のように実行します。

// Create Context  
cl::Context context(device);  

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

前述のように、ホスト コードとカーネル コードは別々にコンパイルされて 2 つの出力が作成されます。カーネルは、Vitis コンパイラにより xclbin ファイルにコンパイルされます。XRT 用に、ホスト アプリケーションでこの xclbin ファイルを OpenCL プログラムとして指定して読み込む必要があります。プログラムはコンテキスト内で作成し、プログラムのカーネルを指定する必要があります。次のコードにこれら手順が示されています。

// Load xclbin  
std::cout << "Loading: " << xclbinFilename << "'\n";  
std::ifstream bin_file(xclbinFilename, std::ifstream::binary);  
bin_file.seekg (0, bin_file.end);  
unsigned nb = bin_file.tellg();  
bin_file.seekg (0, bin_file.beg);  
char *buf = new char [nb];  
bin_file.read(buf, nb);  
  
// Creating Program from Binary File  
cl::Program::Binaries bins;  
bins.push_back({buf,nb});  
cl::Program program(context, devices, bins);  
  
// Create Kernel object(s)  
cl::Kernel kernel_mmult(program,"mmult");

上記の例では、kernel_mmult オブジェクトでプログラム オブジェクト (xclbin) で指定されている mmult というカーネルを指定しています。後の方のセクションで、ハードウェア関数を SDSoC 環境から Vitis 環境に移行する手順を示します。

注記: xclbin には、ホスト アプリケーションで呼び出し、デバイスで実行されるカーネルが複数含まれることがあります。その例は、アドバンス トピック: 複数の計算ユニットおよびカーネルのストリーミング を参照してください。

カーネルを実行する前に、データをホスト アプリケーションからデバイスに転送する必要があります。SDSoC 環境では、data_copyzero_copy の 2 種類のデータ転送がサポートされます。Vitis 環境では、zero_copy のみがサポートされます。ホスト アプリケーションからカーネルへのデータ転送は、OpenCL バッファーを介して実行されます。データを転送するには、アプリケーションで OpenCL バッファー オブジェクトを宣言し、enqueueWriteBuffer() および enqueueReadBuffer() などの API 呼び出しを使用して実際の転送を実行します。XRT はデータをユーザー空間メモリから OS カーネル空間メモリの物理的に連続した領域にコピーし、ハードウェア関数はこの OS カーネル空間メモリに AXI バス インターフェイスを介してアクセスします。

次の例に示すように、カーネルのメモリ バッファーを定義し、カーネル引数を指定することから始めます。

// Create Buffers  
cl::Buffer bufMatA = cl::Buffer(context, CL_MEM_WRITE_ONLY, col*row*sizeof(int), NULL, NULL);  
cl::Buffer bufMatB = cl::Buffer(context, CL_MEM_WRITE_ONLY, col*row*sizeof(int), NULL, NULL);  
cl::Buffer bufMatC = cl::Buffer(context, CL_MEM_READ_ONLY, col*row*sizeof(int), NULL, NULL);  
  
// Assign Kernel arguments  
int narg = 0;  
kernel_mmult.setArg(narg++, bufMatA);  
kernel_mmult.setArg(narg++, bufMatB);  
kernel_mmult.setArg(narg++, bufMatC);  
kernel_mmult.setArg(narg++, col);
kernel_mmult.setArg(narg++, row); 

OpenCL API 呼び出しは指定のコンテキストにデータ バッファーを作成し、バッファーへの読み出し/書き込み機能を定義します。その後、これらのバッファーをハードウェア カーネルの引数として指定し、上記の例での col および row のように直接渡されるスカラー値も指定します。

main() 関数コードの次のセクションは変更しません。このセクションは、プライマリ for ループをインプリメントして、指定の回数 (NUM_TESTS) だけテストを実行し、入力行列 (matA および matB) にランダムな値を挿入して、printMatrix 関数を使用して行列値を出力します。この時点から、main() 関数は行列乗算 (mmult()) をハードウェア アクセラレータで実行します。

SDSoC 環境では、ハードウェア関数は直接呼び出されます。ハードウェア関数呼び出しはタスクとしてアクセラレータを実行し、関数への各引数が Arm プロセッサと PL 領域の間で転送されます。データ転送には、DMA エンジンなどのデータ ムーバーが使用され、sds++ コンパイラにより自動的にシステムに挿入されます。

Vitis 環境では、ホストからローカル メモリへのデータ転送をエンキューし、実行するカーネルをエンキューし、カーネルからホスト (またはプログラムでの必要に応じて別のカーネル) へのデータ転送をエンキューする必要があります。この例では、データはホストに返されます。

次のコードでは、入力行列がホストからデバイス メモリに転送され、カーネルが実行され、出力行列がホスト アプリケーションに戻されます。OpenCL API のエンキュー コマンドはノンブロッキングであり、実際のコマンドが完了する前に戻ります。q.finish() ブロックを呼び出すと、コマンド キューのすべてのコマンドが完了するまで実行されます。これにより、カーネルからデータが転送されるのをホストが待機するようになります。

// Enqueue Buffers  
q.enqueueWriteBuffer(bufMatA, CL_TRUE, 0, col*row*sizeof(int), matA.data(), NULL, NULL);
q.enqueueWriteBuffer(bufMatB, CL_TRUE, 0, col*row*sizeof(int), matB.data(), NULL, NULL);

// Launch Kernel   
q.enqueueTask(kernel_mmult);  

// Read Data Back from Kernel  
q.enqueueReadBuffer(bufMatC, CL_TRUE, 0, col*row*sizeof(int), matC.data(), NULL, NULL);

q.finish(); 

この後、行列乗算の結果を確認するため、出力行列が表示されます。NUM_TESTS で指定された回数テストが実行されると、main 関数に戻ります。

このように、main アプリケーションを SDSoC 環境から Vitis 環境に移行するのに必要な手順は、比較的簡単です。これは主に XRT により実行され、main() 関数とカーネルの間のデータ転送は OpenCL API で制御されます。