データベース分析のようなアプリケーションでは、アクセラレーション デバイスで使用可能なメモリよりも大きなデータ セットが使用され、データ全体をブロック単位で転送して処理する必要があります。これらのアプリケーションで優れたパフォーマンスを達成するには、データ転送と計算をオーバーラップさせる手法が必要となります。
次は、GitHub の Vitis Accelerated Examples のホスト カテゴリにある overlap 例からの vadd
カーネルのコード例です。
#define BUFFER_SIZE 256
#define DATA_SIZE 1024
//TRIPCOUNT indentifier
const unsigned int c_len = DATA_SIZE / BUFFER_SIZE;
const unsigned int c_size = BUFFER_SIZE;
extern "C" {
void vadd(int *c, int *a, int *b, const int elements) {
int arrayA[BUFFER_SIZE];
int arrayB[BUFFER_SIZE];
for (int i = 0; i < elements; i += BUFFER_SIZE) {
#pragma HLS LOOP_TRIPCOUNT min=c_len max=c_len
int size = BUFFER_SIZE;
if (i + size > elements)
size = elements - i;
readA:
for (int j = 0; j < size; j++) {
#pragma HLS PIPELINE II=1
#pragma HLS LOOP_TRIPCOUNT min=c_size max=c_size
arrayA[j] = a[i + j];
}
readB:
for (int j = 0; j < size; j++) {
#pragma HLS PIPELINE II=1
#pragma HLS LOOP_TRIPCOUNT min=c_size max=c_size
arrayB[j] = b[i + j];
}
vadd_writeC:
for (int j = 0; j < size; j++) {
#pragma HLS PIPELINE II=1
#pragma HLS LOOP_TRIPCOUNT min=c_size max=c_size
c[i + j] = arrayA[j] + arrayB[j];
}
}
}
}
この例では、ホストで実行する必要のあるタスクは次の 4 つです。
- バッファー a の書き込み (Wa)
- バッファー b の書き込み (Wb)
- vadd カーネルを実行
- バッファー c の読み出し (Rc)
順不同コマンド キューを使用すると、次の図に示すように、データ転送とカーネル実行をオーバーラップできます。この例のホスト コードでは、カーネルが 1 セットのバッファーを処理している間に、ホストでもう 1 つのバッファーのセットを処理できるように、すべてのバッファーにダブル バッファリングが使用されます。
OpenCL
event
オブジェクトを使用すると、複雑な操作依存を簡単に設定して、ホスト スレッドとデバイス動作を同期できます。イベントは、操作のステータスを調べるための OpenCL オブジェクトです。イベント オブジェクトは、カーネル実行コマンド、メモリ オブジェクトに対する読み出し、書き込み、コピー コマンドにより作成されるか、clCreateUserEvent
を使用して作成されたユーザー イベントです。これらのコマンドで返されるイベントをクエリすることにより、操作が完了したかどうかを確認できます。次の図の矢印は、最適なパフォーマンスを達成するために、イベント トリガーをどのように設定できるかを示しています。
ホスト コードは、ループ内の 4 つのタスクをエンキューしてデータ セット全体を処理します。また、各タスクのデータ依存が満たされるように、異なるタスク間のイベント同期を設定します。ダブル バッファリングは、異なるメモリ オブジェクト値を clEnqueueMigrateMemObjects
API に渡すことにより設定します。イベント同期は、各 API 呼び出しがほかのイベントを待ち、その API が終了してからそれ自身のイベントをトリガーするようにすると達成できます。
// THIS PAIR OF EVENTS WILL BE USED TO TRACK WHEN A KERNEL IS FINISHED WITH
// THE INPUT BUFFERS. ONCE THE KERNEL IS FINISHED PROCESSING THE DATA, A NEW
// SET OF ELEMENTS WILL BE WRITTEN INTO THE BUFFER.
vector<cl::Event> kernel_events(2);
vector<cl::Event> read_events(2);
cl::Buffer buffer_a[2], buffer_b[2], buffer_c[2];
for (size_t iteration_idx = 0; iteration_idx < num_iterations; iteration_idx++) {
int flag = iteration_idx % 2;
if (iteration_idx >= 2) {
OCL_CHECK(err, err = read_events[flag].wait());
}
// Allocate Buffer in Global Memory
// Buffers are allocated using CL_MEM_USE_HOST_PTR for efficient memory and
// Device-to-host communication
std::cout << "Creating Buffers..." << std::endl;
OCL_CHECK(err,
buffer_a[flag] =
cl::Buffer(context,
CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR,
bytes_per_iteration,
&A[iteration_idx * elements_per_iteration],
&err));
OCL_CHECK(err,
buffer_b[flag] =
cl::Buffer(context,
CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR,
bytes_per_iteration,
&B[iteration_idx * elements_per_iteration],
&err));
OCL_CHECK(err,
buffer_c[flag] = cl::Buffer(
context,
CL_MEM_WRITE_ONLY | CL_MEM_USE_HOST_PTR,
bytes_per_iteration,
&device_result[iteration_idx * elements_per_iteration],
&err));
vector<cl::Event> write_event(1);
OCL_CHECK(err, err = krnl_vadd.setArg(0, buffer_c[flag]));
OCL_CHECK(err, err = krnl_vadd.setArg(1, buffer_a[flag]));
OCL_CHECK(err, err = krnl_vadd.setArg(2, buffer_b[flag]));
OCL_CHECK(err, err = krnl_vadd.setArg(3, int(elements_per_iteration)));
// Copy input data to device global memory
std::cout << "Copying data (Host to Device)..." << std::endl;
// Because we are passing the write_event, it returns an event object
// that identifies this particular command and can be used to query
// or queue a wait for this particular command to complete.
OCL_CHECK(
err,
err = q.enqueueMigrateMemObjects({buffer_a[flag], buffer_b[flag]},
0 /*0 means from host*/,
NULL,
&write_event[0]));
set_callback(write_event[0], "ooo_queue");
printf("Enqueueing NDRange kernel.\n");
// This event needs to wait for the write buffer operations to complete
// before executing. We are sending the write_events into its wait list to
// ensure that the order of operations is correct.
//Launch the Kernel
std::vector<cl::Event> waitList;
waitList.push_back(write_event[0]);
OCL_CHECK(err,
err = q.enqueueNDRangeKernel(
krnl_vadd, 0, 1, 1, &waitList, &kernel_events[flag]));
set_callback(kernel_events[flag], "ooo_queue");
// Copy Result from Device Global Memory to Host Local Memory
std::cout << "Getting Results (Device to Host)..." << std::endl;
std::vector<cl::Event> eventList;
eventList.push_back(kernel_events[flag]);
// This operation only needs to wait for the kernel call. This call will
// potentially overlap the next kernel call as well as the next read
// operations
OCL_CHECK(err,
err = q.enqueueMigrateMemObjects({buffer_c[flag]},
CL_MIGRATE_MEM_OBJECT_HOST,
&eventList,
&read_events[flag]));
set_callback(read_events[flag], "ooo_queue");
OCL_CHECK(err, err = read_events[flag].wait());
}
次に示す [Application Timeline] ビューでは、計算ユニット vadd_1
が継続的に実行されており、データ転送時間は完全に隠されています。