データ転送とカーネル計算のオーバーラップ - 2020.1 Japanese

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

Document ID
UG1393
Release Date
2020-08-20
Version
2020.1 Japanese

データベース分析のようなアプリケーションでは、アクセラレーション デバイスで使用可能なメモリよりも大きなデータ セットが使用され、データ全体をブロック単位で転送して処理する必要があります。これらのアプリケーションで優れたパフォーマンスを達成するには、データ転送と計算をオーバーラップさせる手法が必要となります。

次は、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 つです。

  1. バッファー a の書き込み (Wa)
  2. バッファー b の書き込み (Wb)
  3. vadd カーネルを実行
  4. バッファー c の読み出し (Rc)

順不同コマンド キューを使用すると、次の図に示すように、データ転送とカーネル実行をオーバーラップできます。この例のホスト コードでは、カーネルが 1 セットのバッファーを処理している間に、ホストでもう 1 つのバッファーのセットを処理できるように、すべてのバッファーにダブル バッファリングが使用されます。

OpenCL event オブジェクトを使用すると、複雑な操作依存を簡単に設定して、ホスト スレッドとデバイス動作を同期できます。イベントは、操作のステータスを調べるための OpenCL オブジェクトです。イベント オブジェクトは、カーネル実行コマンド、メモリ オブジェクトに対する読み出し、書き込み、コピー コマンドにより作成されるか、clCreateUserEvent を使用して作成されたユーザー イベントです。これらのコマンドで返されるイベントをクエリすることにより、操作が完了したかどうかを確認できます。次の図の矢印は、最適なパフォーマンスを達成するために、イベント トリガーをどのように設定できるかを示しています。

図 1. イベント トリガーの設定

ホスト コードは、ループ内の 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 が継続的に実行されており、データ転送時間は完全に隠されています。

図 2. データ転送時間が隠された [Application Timeline] ビュー