OpenCL API の使用 - 2019.2 Japanese

Vitis 統合ソフトウェア プラットフォームの資料: アプリケーション アクセラレーション開発 (UG1393)

Document ID
UG1393
Release Date
2020-02-28
Version
2019.2 Japanese

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 で制御されます。