Notice
Recent Posts
Recent Comments
Today
Total
04-26 17:29
Archives
관리 메뉴

Jeongchul Kim

OpenCL[3] Memory Object, Kernel Execution 본문

OpenCL

OpenCL[3] Memory Object, Kernel Execution

김 정출 2018. 8. 23. 17:09


OpenCL[3] Memory Object, Kernel Execution


OpenCL application execution scenario


OpenCL platform model

device 마다 global memory와 constant memory를 가지고 있습니다.

host와 device가 둘 다 접근할 수 있습니다. device에서는 host memory(main memory)에 바로 접근할 수 없습니다.


Memory Object

device의 global/constant memory에 공간을 할당하고 데이터를 저장하기 위해 필요합니다.

host program에서 memory object에 데이터를 읽고 쓸 수 있습니다.

- kernel 함수에서 memory object를 인자로 받을 수 있습니다.

- buffer object: 일반적인 배열(array)과 동일

- image object: 1~3 차원의 데이터를 처리하기 위한 특수 object, 텍스처(texture), 프레임 버퍼(frame buffer), 이미지


Create Buffer

buffer object 생성하기

cl_mem clCreateBuffer ( cl_context context, cl_mem_flags flags, size_t size, void *host_ptr, cl_int *errcode_ret)


flags: [Table 5.3] CL_MEM_READ_WRITE, CL_MEM_{WRITE, READ}_ONLY, CL_MEM_HOST_NO_ACCESS, CL_MEM_HO

여러가지 flag를 줄 수 있습니다.

- CL_MEM_READ_WRITE, CL_MEM_READ_ONLY, CL_MEM_WRITE_ONLY

  > kernel에서 이 buffer를 읽기만 하는지, 쓰기만 하는지, 둘 다 하는지 알려준다.

  > runtime 시스템에서 최적화를 할 수 있도록 합니다.

- CL_MEM_COPY_HOST_PTR

   > buffer object를 host_ptr이 가르키는 곳의 데이터로 초기화 합니다.

- 두 가지 이상의 flag는 bitwise OR(|)로 연결합니다.


다음의 코드는 float 값 10개를 저장할 buffer object를 생성합니다.

cl_context context;
cl_mem buffer;
cl_int err;
buffer = clCreateBuffer(context, 0, sizeof(float) * 10, NULL, &err);





다음의 코드는 kernel에서 읽기(read)만 가능한 256 byte buffer object를 생성합니다.

cl_mem buffer;
cl_int err;
buffer = clCreateBuffer(context, CL_MEM_READ_ONLY, 256, NULL, &err);


Command-queue

device에 command(kernel 실행, 데이터 전송, 동기화)를 보내기 위해 command-queue가 필요합니다.

host 프로그램이 command-queue에 command를 넣으면, OpenCL 프레임워크의 런타임 시스템에서 이것을 빼내어 실행합니다.


Write Buffer

cl_int clEnqueueWriteBuffer ( cl_command_queue command_queue, cl_mem buffer, cl_bool blocking_write, size_t offset, size_t size, const void *ptr, cl_uint num_events_in_wait_list, const cl_event *event_wait_list, cl_event *event)

동기화(blocking_write가 CL_TRUE)

- buffer 쓰기가 완료된 다음에 리턴

- 함수 호출이 끝난 다음 바로 ptr을 해제하거나 다른 용도로 사용해도 무방합니다.


비동기화(blocking_write가 CL_FALSE)

- command가 command_queue에 enqueue되자마자 리턴

- 완료 시점 파악을 위해 이벤트가 사용됩니다.


다음의 코드는 Main memory의 A, B 배열 데이터를 buffer에 씁니다.

cl_command_queue queue;
float *A, *B;
cl_mem bufA, bufB;
int N = 10000;
int i;
A = (float*)malloc(sizeof(float) * N);
B = (float*)malloc(sizeof(float) * N);
for(i = 0 ; i < N; i++) {
 A[i] = rand() % 100;
 B[i] = rand() % 100;
}
...
bufA = clCreateBuffer(context, CL_MEM_READ_WRITE, sizeof(float) * N, NULL, &err);
bufB = clCreateBuffer(context, CL_MEM_READ_WRITE, sizeof(float) * N, NULL, &err);
...
err = clEnqueueWriteBuffer(queue, bufA, CL_FALSE, 0, sizeof(float) * N, A, 0, NULL, NULL);
err = clEnqueueWriteBuffer(queue, bufB, CL_FALSE, 0, sizeof(float) * N, B, 0, NULL, NULL);


Read Buffer

buffer에 쓰여진 데이터를 읽어옵니다.

cl_int clEnqueueReadBuffer ( cl_command_queue command_queue, cl_mem buffer, cl_bool blocking_read, size_t offset, size_t size, void *ptr, cl_uint num_events_in_wait_list, const cl_event *event_wait_list, cl_event *event)


다음의 코드는 buffer의 데이터를 main memory의 C 배열로 읽어와 출력합니다.

cl_command_queue queue;
float *C;
cl_mem bufC;
int N = 10000;
int i;
C = (float*)malloc(sizeof(float) * N);
bufC = clCreateBuffer(context, CL_MEM_READ_WRITE, sizeof(float) * N, NULL, &err);
err = clEnqueueReadBuffer(queue, bufC, CL_TRUE, 0, sizeof(float) * N, C, 0, NULL, NULL);

for(i = 0; i < N; i++)
 printf("idx : %d , value : %f\n", i, C[i]);



Copy Buffer

buffer에서 buffer로 복사합니다.

cl_int clEnqueueCopyBuffer ( cl_command_queue command_queue, cl_mem src_buffer, cl_mem dst_buffer, size_t src_offset, size_t dst_offset, size_t size, cl_uint num_events_in_wait_list, const cl_event *event_wait_list, cl_event *event)


kernel에서 buffer에 접근하는 코드는 다음과 같습니다.

__kernel void vec_add(__global float *A, __global float *B, __global float *C) {
 int i = get_global_id(0);
 C[i] = A[i] + B[i];
}


Cautions : memory object

메모리 object는 추상적인 메모리 영역입니다.

- 특정 device의 메모리에 dedicated 된 것이 아닙니다.

- 실제로 device에서 사용될 때(해당 device의 command queue에 command를 넣어서 실행할 때) 비로소 global memory 에 할당되고 저장됩니다.

- 여러 device를 동시에 할당할 수 도 있습니다. 런타임 시스템에서 device사이의 consistency를 관리합니다.

- 똑같은 buffer를 여러 device에 할당하는 것은 성능이 좋지 않습니다.


Constant Memory : 4KB 이내의 공유할만한 데이터가 아닌 이상 사용할 일이 거의 없습니다.


Share memory in host and device

데이터를 복사하는 overhead가 엄청 큽니다.

Multi-core CPU

AMD APU(Accelerated Processing Unit)

- CPU와 GPU가 같은 칩에 장착됩니다.

- HSA(Heterogeneous System Architecture) 지원 : CPU와 GPU가 같은 가상 메모리 주소 공간을 공유하며, 캐시 일관성 지원

CPU + FPGA

- Intel에서 데이터 센터 대상의 서버용 프로세서 출시 계획


OpenCL 1.x

flags에 CL_MEM_USE_HOST_PTR 설정

- host_ptr이 가르키는 곳(host memory)을 버퍼로 사용할 수 있습니다.

- 불필요한 복사를 줄일 수 있습니다.


int N = 1000;
int i;
int *A = (int*)malloc(sizeof(int) * N);
for(i = 0; i < N; i++) A[i] = i;
buffer = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR, sizeof(int) * N, A, &err);


한계

- host와 device 사이에 동기화를 보장하지 않습니다. 주소 공간을 공유하는 것은 아닙니다. point 기반의 자료 구조(linked list, tree 등)를 공유하기 어렵습니다.

- clEnqueueMapBuffer : device에 write한 것을 device에 업데이트

- clEnqueueUnmapMemObject : host에서 write한 것을 device에 업데이트


OpenCL 2.X : SVM(Shared Virtural Memory)

3가지 종류의 SVM을 지원합니다.

device마다 지원하는 사항은 다르나, Coarse-grained buffer SVM은 필수로 있습니다.


OpenCL application execution scenario


OpenCL kernel function

SPMD(Single Program, Multiple Data)

같은 kernel 코드를 다른 데이터 아이템에 동시에 실행합니다. data parallelism을 활용합니다.


work-item

인덱스(index) 공간의 각 점마다 kernel 인스턴스(instance)가 하나씩 실행됩니다.

kernel 함수 안에서 OpenCL C의 built-in 함수(get_global_id 등)를 사용해 index 공간에서의 work-item의 id를 가져올 수 있습니다.


work-group

여러 work-item 들이 work-group 단위로 묶여 있습니다.

모든 work-group의 크기는 동일합니다.


하나의 work-group은 CU에서 실행됩니다.

- work-group 안의 여러 work-item 들이 CU안의 여러 PE에서 나뉘어 실행됩니다.

- 같은 work-group 안의 work-item들은 local memory를 공유합니다.

- work-group의 크기가 성능에 큰 영향을 미칩니다.



Set kernel arguments

buffer를 전달하거나, 값을 넘깁니다.

cl_int clSetKernelArg (cl_kernel kernel, cl_uint arg_index, size_t arg_size, const void *arg_value)



cl_mem buf;
cl_float f = 3.141569;
err = clSetKernelArg(kernel, 0, sizeof(cl_mem), &buf);
err = clSetKernelArg(kernel, 1, sizeof(cl_float), &A);


cl_int clEnqueueNDRangeKernel ( cl_command_queue command_queue, cl_kernel kernel, cl_uint work_dim, const size_t *global_work_offset, const size_t *global_work_size, const size_t *local_work_size, cl_uint num_events_in_wait_list, const cl_event *event_wait_list, cl_event *event)


work-group 크기에서 global_work_size[i]는 local_work_size[i]로 항상 나누어 떨어져야 합니다.


다음의 코드는 1차원 index 공간 생성합니다.

size_t global_size = 1024;
size_t local_size = 32;
clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &global_size, &local_size, 0, NULL, NULL);


다음의 코드는 2차원 index 공간 생성합니다.

size_t global_size[2] = {256, 256};
size_t local_size[2] = {16, 16};
clEnqueueNDRangeKernel(queue, kernel, 2, NULL, global_size, local_size, 0, NULL, NULL);]


Vector Add

크기가 N개인 int형 vector 3개를 생성합니다.

A와 B값을 초기화하고, GPU에서 계산을 하고, C를 가져와서 CPU에서 계산한 값과 비교합니다.

https://github.com/KimJeongChul/opencl/tree/master/project-3-vector-addition


kernel 함수 코드는 다음과 같습니다.

__kernel void vec_add(__global int *A, __global int *B, __global int *C) {
 int i = get_global_id(0);
 C[i] = A[i] + B[i];
}


main 함수는 다음과 같습니다.


library를 가져오고 변수를 정의합니다. VECTOR_SIZE는 LOCAL_SIZE 약수로 나뉘어져야 합니다.

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
#include <CL/cl.h>

#define VECTOR_SIZE 32768
#define LOCAL_SIZE 256

에러를 체크하고 시간을 가져오는 함수입니다.

#define CHECK_ERROR(err) \
 if (err != CL_SUCCESS) { \
   printf("[%s:%d] OpenCL error %d\n", __FILE__, __LINE__, err); \
   exit(EXIT_FAILURE); \
 }

double get_time() {
 struct timeval tv;
 gettimeofday(&tv, NULL);
 return (double)tv.tv_sec + (double)1e-6 * tv.tv_usec;
}


file에서 kernel 코드를 가져옵니다.

char *get_source_code(const char *file_name, size_t *len) {
 char *source_code;
 size_t length;
 FILE *file = fopen(file_name, "r");
 if (file == NULL) {
   printf("[%s:%d] Failed to open %s\n", __FILE__, __LINE__, file_name);
   exit(EXIT_FAILURE);
 }

 fseek(file, 0, SEEK_END);
 length = (size_t)ftell(file);
 rewind(file);

 source_code = (char *)malloc(length + 1);
 fread(source_code, length, 1, file);
 source_code[length] = '\0';

 fclose(file);

 *len = length;
 return source_code;
}


main 함수는 다음과 같습니다.

int main() {
 // OpenCl variables
 cl_platform_id platform;
 cl_device_id device;
 cl_context context;
 cl_command_queue queue;
 cl_mem bufferA, bufferB, bufferC;
 cl_program program;
 char *kernel_source;
 size_t kernel_source_size;
 cl_kernel kernel;
 cl_int err;

 // Time variables
 double start;
 double end;

 // Get platform
 err = clGetPlatformIDs(1, &platform, NULL);
 CHECK_ERROR(err);

 // Get GPU device
 err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL);
 CHECK_ERROR(err);

 // Create context
 context = clCreateContext(NULL, 1, &device, NULL, NULL, &err);
 CHECK_ERROR(err);

 // Get kernel code
 kernel_source = get_source_code("kernel.cl", &kernel_source_size);

 // Create program
 program = clCreateProgramWithSource(context, 1, (const char**)&kernel_source,
   &kernel_source_size, &err);
 CHECK_ERROR(err);

 // Build program
 err = clBuildProgram(program, 1, &device, "", NULL, NULL);
 if(err == CL_BUILD_PROGRAM_FAILURE) {
   size_t log_size;
   char *log;

   // Get program build
   err = clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG,
     0, NULL, &log_size);
   CHECK_ERROR(err);

   // Get build log
   log = (char*)malloc(log_size + 1);
   err = clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG,
     log_size, log, NULL);
   CHECK_ERROR(err);

   log[log_size] = '\0';
   printf("Compiler error : \n%s\n", log);
   free(log);
   exit(0);
 }
 CHECK_ERROR(err);

 // Create Vector A, B, C
 int *A = (int*)malloc(sizeof(int) * VECTOR_SIZE);
 int *B = (int*)malloc(sizeof(int) * VECTOR_SIZE);
 int *C = (int*)malloc(sizeof(int) * VECTOR_SIZE);

 // Initial Vector A, B
 cl_ushort idx;
 for(idx = 0; idx < VECTOR_SIZE; idx++) {
   A[idx] = rand() % 100;
   B[idx] = rand() % 100;
 }

 // Create kernel
 kernel = clCreateKernel(program, "vec_add", &err);
 CHECK_ERROR(err);

 // Create Buffer
 bufferA = clCreateBuffer(context, CL_MEM_READ_ONLY, sizeof(int) * VECTOR_SIZE, NULL, &err);
 CHECK_ERROR(err);

 bufferB = clCreateBuffer(context, CL_MEM_READ_ONLY, sizeof(int) * VECTOR_SIZE, NULL, &err);
 CHECK_ERROR(err);

 bufferC = clCreateBuffer(context, CL_MEM_WRITE_ONLY, sizeof(int) * VECTOR_SIZE, NULL, &err);
 CHECK_ERROR(err);

 // Create command-queue
 queue = clCreateCommandQueue(context, device, 0, &err);
 CHECK_ERROR(err);

 // Write Buffer
 err = clEnqueueWriteBuffer(queue, bufferA, CL_FALSE, 0, sizeof(int) * VECTOR_SIZE, A, 0, NULL, NULL);
 CHECK_ERROR(err);

 err = clEnqueueWriteBuffer(queue, bufferB, CL_FALSE, 0, sizeof(int) * VECTOR_SIZE, B, 0, NULL, NULL);
 CHECK_ERROR(err);

 // Set Kernel arguments
 start = get_time();
 err = clSetKernelArg(kernel, 0, sizeof(cl_mem), &bufferA);
 CHECK_ERROR(err);

 err = clSetKernelArg(kernel, 1, sizeof(cl_mem), &bufferB);
 CHECK_ERROR(err);

 err = clSetKernelArg(kernel, 2, sizeof(cl_mem), &bufferC);
 CHECK_ERROR(err);
 end = get_time();

 printf("Send Vector A, B to GPU : %f seconds elapsed\n", end - start);

 start = get_time();
 // Execute Kernel
 size_t global_size = VECTOR_SIZE;
 size_t local_size = LOCAL_SIZE;
 clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &global_size, &local_size, 0, NULL, NULL);
 CHECK_ERROR(err);
 end = get_time();

 printf("Calculate C : %f seconds elapsed\n", end - start);

 // Read Buffer
 start = get_time();
 err = clEnqueueReadBuffer(queue, bufferC, CL_TRUE, 0, sizeof(int) * VECTOR_SIZE, C, 0, NULL, NULL);
 CHECK_ERROR(err);

 end = get_time();
 printf("Receive C from GPU : %f seconds elapsed\n", end - start);

 // Evaluate Vector C
 for(idx = 0; idx < VECTOR_SIZE; idx++) {
   if(A[idx] + B[idx] != C[idx]) {
     printf("Verification failed! A[%d] = %d, B[%d] = %d, C[%d] = %d\n", idx, A[idx], idx, B[idx], idx, C[idx]);
     break;
   }
 }
 if (idx == VECTOR_SIZE) {
   printf("Verification success!\n");
 }

 // Release OpenCL object
 clReleaseMemObject(bufferA);
 clReleaseMemObject(bufferB);
 clReleaseMemObject(bufferC);
 free(A);
 free(B);
 free(C);
 clReleaseKernel(kernel);
 clReleaseProgram(program);
 clReleaseCommandQueue(queue);
 clReleaseContext(context);

 return 0;
}


컴파일을 하고 실행해 봅시다.

$ gcc -o vector main.c -lOpenCL

$ ./vector



OpenCL Kernel Programming

OS가 없기 때문에 표준 C99 헤더 파일을 지원되지 않습니다.

함수 포인터와 재귀 함수가 지원되지 않습니다.


OpenCL C program

여러 함수(function) 정의가 나열되어 있습니다.

2 가지 종류의 함수가 있습니다. kernel 함수와 일반 함수


host 프로그램에서는 kernel 함수만 호출 가능합니다.


Declare Kernel

Function qualifier

- __kernel 또는 kernel이 붙습니다.

- OpenCL C의 예약어로 kernel 선언 이외에 사용할 수 없습니다.

- return 타입은 void 여야 합니다.


__kernel void square(__global int *input, __global int *output)
{
 int id = get_global_id(0);
 output[id] = input[id] * input[id];
}


Kernel Attribute

kernel 함수에 특별한 정보를 추가하기 위하여 __attribute__ 키워드 사용합니다.

__kernel void => __kernel __attribute__((...)) void


__attribute__((vec_type_hint(<type>)))

- kernel의 주된 계산 너비(computational width)

- 컴파일러 힌트 : 자동 벡터화(auto-vectorization)에 도움을 줄 수 있습니다.

- <type>은 빌트인 스칼라(scala) 타입 또는 벡터(vector) 타입 중의 하나입니다.

- 지정되지 않은 경우 __attribute__((vect_type_hint(float4)))


__attribute__((work_group_size_hint(X, Y, Z)))

- kernel이 주로 사용할 work-group 크기에 대한 compiler에 도움을 줍니다.


__attribute__((reqd_work_group_size(X, Y, Z)))

- kernel execution을 위해 꼭 사용해야 할 work-group 크기

- kernel command를 command-queue에 넣을 때 잘못 지정하면, runtime error 발생합니다.


Scala Type

host API와 host 프로그램에서 사용되는 scala type

- OpenCL C의 대부분의 scala type에 대응합니다.


Vector Type


Address Space Qualifier

__global, __constant, __local, __private를 사용하면 각각 데이터가 global, constant, local, private 메모리에 있다는 것을 나타냅니다. kernel 함수의 변수 선언 앞에 붙으면 어떤 메모리 영역에 할당될지를 사용합니다.


- __global, __constant : buffer를 만들어서 인자로 넘깁니다.

- __local : host에서 값은 못쓰지만, 할당은 가능합니다.

- __private : 사용할 수 없습니다.

- 전역 변수는 선언과 동시에 초기화를 해야합니다.


__kernel void add(__global int *A, __constant int *B)
{
 int i = get_global_id(0);
 __private int x;
 __local float y[20];
 A[i] = A[i] + B
}


work-itme function

dimindx 는 0부터 get_work_dim() -1 사이의 값입니다.


index 공간의 function

- 차원(dimensions) : uint get_work_dim()

- 전체 크기 : size_t get_global_size(uint dimindx)

- work-group 크기 :  size_t get_local_size(uint dimindx)

- work-group 개수 :  size_t get_num_groups(uint dimindx)


현재 work-item의 function

- global id : size_t get_global_id(uint dimindx)

- local id : size_t get_local_id(uint dimidx)


현재 work-group의 function

- work-group id : size_t get_group_id(uint dimidx)


Local Memory

Compute Unit(CU)마다 local memory가 하나씩 있습니다. 해당 CU에서만 접근이 가능합니다.

GPU의 경우, global memory에 접근하는 것보다 빠릅니다.

- global memory :  GPU의 off-chip memory

- local memory : GPU SM 안의 Shared context


kernel 함수에서 할당해야 합니다.

- kernel 함수 안에서 __local 을 붙여 변수/배열 선언합니다.

- kernel 함수에서 __local 포인터를 인자로 받습니다.

-  sizeof(int)*64 = 256Byte로 할당해라

- 할당과 동시에 초기화를 할 수 없습니다. kernel 실행 중에 따로 초기화를 해주어야 합니다.

Global memory의 캐시(cache) 역할을 합니다.

- global memory의 같은 위치를 여러 work-item이 읽을 경우

- 처음 한 번만 읽어서 local memory에 저장합니다.


OpenCL Memory Consistency

메모리의 내용이 서로 다른 work-item에게 항상 같게 보일 필요는 없기 때문에 위의 상황에서 문제가 발생합니다.

동기화(synchronization)을 사용해야 합니다.

- work-group barrier

- atomic operation


work-group Barrier

kernel에서 barrier() 함수를 이용합니다.

- void barrier(cl_mem_fence_flags flags)

- work-group 안의 모든 work-item이 barrier를 동시에 통과합니다. 모든 work-item이 barrier까지 코드를 실행 후에 그 다음 코드를 실행합니다.

- local memory에 대한 consistency 보장 : barrier(CLK_LOCAL_MEM_FENCE)

- global memory에 대한 consistency 보장 : barrier(CLK_GLOBAL_MEM_FENCE)


서로 다른 work-group 간의 동기화는 지원하지 않는 다는 것을 유의해야 합니다.


work-group 안의 모든 work-item이 barrier()를 실행하거나, 전체가 실행하지 않아야 합니다. (if 문 분기점을 조심해야 합니다.)


Matrix Multiplication

A는 ROW_A X COL_A 행렬

B는 ROW_B X COL_B 행렬

C = A*B 를 구합니다.


프로그램은 인자 값으로 0과 1을 받아 sequential 또는 parallel로 실행을 합니다.


kernel.cl의 코드는 다음과 같습니다.

__kernel void mat_mul(__global float *A,
__global float *B,
__global float *C,
int ROW_A,
int COL_A,
int COL_B) {

int i = get_global_id(1);
int j = get_global_id(0);
int k;
float sum = 0.0f;

if( i < ROW_A && j < COL_B) {
for(k = 0; k < COL_A; k++) {
sum += A[i * COL_A + k] * B[k * COL_B + j];
}
C[i * COL_B + j] = sum;
}
}


sequential 코드는 다음과 같습니다.

mat_mul_seq.c

void mat_mul_seq(float *A, float *B, float *C,
                int ROW_A, int COL_A, int COL_B) {
 int i, j, k;
 for (i = 0; i < ROW_A; i++) {
   for (j = 0; j < COL_B; j++) {
     C[i * COL_B + j] = 0.0f;
     for (k = 0; k < COL_A; k++) {
       C[i * COL_B + j] += A[i * COL_A + k] * B[k * COL_B + j];
     }
   }
 }
}


병렬로 처리되는 parallel OpenCL 코드는 다음과 같습니다.

mat_mul_opencl.c

#include <stdio.h>
#include <stdlib.h>
#include <CL/cl.h>

#define CHECK_ERROR(err) \
 if (err != CL_SUCCESS) { \
   printf("[%s:%d] OpenCL error %d\n", __FILE__, __LINE__, err); \
   exit(EXIT_FAILURE); \
 }

double get_time(); // use the get_time() function in mat_mul.c

char *get_source_code(const char *file_name, size_t *len) {
 char *source_code;
 size_t length;
 FILE *file = fopen(file_name, "r");
 if (file == NULL) {
   printf("[%s:%d] Failed to open %s\n", __FILE__, __LINE__, file_name);
   exit(EXIT_FAILURE);
 }

 fseek(file, 0, SEEK_END);
 length = (size_t)ftell(file);
 rewind(file);

 source_code = (char *)malloc(length + 1);
 fread(source_code, length, 1, file);
 source_code[length] = '\0';

 fclose(file);

 *len = length;
 return source_code;
}

void mat_mul_opencl(float *A, float *B, float*C, int ROW_A, int COL_A, int COL_B) {
 cl_platform_id platform;
 cl_device_id device;
 cl_context context;
 cl_command_queue queue;
 cl_program program;
 char *kernel_source;
 size_t kernel_source_size;
 cl_kernel kernel;
 cl_int err;

 // Get platform
 err = clGetPlatformIDs(1, &platform, NULL);
 CHECK_ERROR(err);

 // Get device gpu
 err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL);
 CHECK_ERROR(err);

 // Create context
 context = clCreateContext(NULL, 1, &device, NULL, NULL, &err);
 CHECK_ERROR(err);

 // Create Queue
 queue = clCreateCommandQueue(context, device, 0, &err);
 CHECK_ERROR(err);

 // Get kernel source
 kernel_source = get_source_code("kernel.cl", &kernel_source_size);

 // Create program
 program = clCreateProgramWithSource(context, 1, (const char**)&kernel_source,
   &kernel_source_size, &err);
 CHECK_ERROR(err);

 // Build program
 err = clBuildProgram(program, 1, &device, "", NULL, NULL);
 if(err == CL_BUILD_PROGRAM_FAILURE) {
   size_t log_size;
   char *log;

   // Get program build
   err = clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG,
     0, NULL, &log_size);
   CHECK_ERROR(err);

   // Get build log
   log = (char*)malloc(log_size + 1);
   err = clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG,
     log_size, log, NULL);
   CHECK_ERROR(err);

   log[log_size] = '\0';
   printf("Compiler error : \n%s\n", log);
   free(log);
   exit(0);
 }
 CHECK_ERROR(err);

 // Create kernel
 kernel = clCreateKernel(program, "mat_mul", &err);
 CHECK_ERROR(err);

 cl_mem bufferA, bufferB, bufferC;

 // Create buffer
 bufferA = clCreateBuffer(context, CL_MEM_READ_ONLY, sizeof(float)*ROW_A*COL_A,
   NULL, &err);
 CHECK_ERROR(err);

 bufferB = clCreateBuffer(context, CL_MEM_READ_ONLY, sizeof(float)*COL_A*COL_B,
   NULL, &err);
 CHECK_ERROR(err);

 bufferC = clCreateBuffer(context, CL_MEM_READ_WRITE, sizeof(float)*ROW_A*COL_B,
   NULL, &err);
 CHECK_ERROR(err);

 double start_time = get_time();

 // Write buffer
 err = clEnqueueWriteBuffer(queue, bufferA, CL_FALSE, 0, sizeof(float)*ROW_A*COL_A, A, 0, NULL, NULL);
 CHECK_ERROR(err);

 err = clEnqueueWriteBuffer(queue, bufferB, CL_FALSE, 0, sizeof(float)*COL_A*COL_B, B, 0, NULL, NULL);
 CHECK_ERROR(err);

 // Set kernel arguments
 err = clSetKernelArg(kernel, 0, sizeof(cl_mem), &bufferA);
 CHECK_ERROR(err);

 err = clSetKernelArg(kernel, 1, sizeof(cl_mem), &bufferB);
 CHECK_ERROR(err);

 err = clSetKernelArg(kernel, 2, sizeof(cl_mem), &bufferC);
 CHECK_ERROR(err);

 err = clSetKernelArg(kernel, 3, sizeof(cl_int), &ROW_A);
 CHECK_ERROR(err);

 err = clSetKernelArg(kernel, 4, sizeof(cl_int), &COL_A);
 CHECK_ERROR(err);

 err = clSetKernelArg(kernel, 5, sizeof(cl_int), &COL_B);
 CHECK_ERROR(err);

 // Set global, local size
 size_t global_size[2] = {COL_B, ROW_A};
 size_t local_size[2] = {16, 16};

 global_size[0] = (global_size[0] + local_size[0] - 1) / local_size[0] * local_size[0];
 global_size[1] = (global_size[1] + local_size[1] - 1) / local_size[1] * local_size[1];

 // Enquque nd range kernel
 err = clEnqueueNDRangeKernel(queue, kernel, 2, NULL, global_size, local_size, 0, NULL, NULL);
 CHECK_ERROR(err);

 err = clEnqueueReadBuffer(queue, bufferC, CL_TRUE, 0, sizeof(float)*ROW_A*COL_B, C, 0, NULL, NULL);
 CHECK_ERROR(err);

 double end_time = get_time();
 printf("Elasped Time(excl. initialization): %f sec\n", end_time - start_time);

 // Release OpenCL object
 clReleaseMemObject(bufferA);
 clReleaseMemObject(bufferB);
 clReleaseMemObject(bufferC);
 clReleaseKernel(kernel);
 clReleaseProgram(program);
 clReleaseCommandQueue(queue);
 clReleaseContext(context);

 printf("Finished!\n");
}


그리고 실제 main되는 코드는 다음과 같습니다.

mat_mul.c

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/time.h>
#include <unistd.h>

static int ROW_A = 1000;
static int COL_A = 1000;
static int COL_B = 1000;

double get_time() {
 struct timeval tv;
 gettimeofday(&tv, NULL);
 return (double)tv.tv_sec + (double)1e-6 * tv.tv_usec;
}

void mat_mul_seq(float *A, float *B, float *C,
                int ROW_A, int COL_A, int COL_B);
void mat_mul_opencl(float *A, float *B, float *C,
                   int ROW_A, int COL_A, int COL_B);
void verify(float *A, float *B, float *C,
           int ROW_A, int COL_A, int COL_B);

int main(int argc, char *argv[]) {
 if (argc < 2) {
   printf("Usage: %s <option>\n", argv[0]);
   exit(EXIT_FAILURE);
 }

 int option = atoi(argv[1]);

 float *A = (float*)malloc(sizeof(float) * ROW_A * COL_A);
 float *B = (float*)malloc(sizeof(float) * COL_A * COL_B);
 float *C = (float*)malloc(sizeof(float) * ROW_A * COL_B);
 int i, j;

 for (i = 0; i < ROW_A; i++) {
   for (j = 0; j < COL_A; j++) {
     A[i * COL_A + j] = (float)(rand() % 1000) / 100.0f;
   }
 }
 for (i = 0; i < COL_A; i++) {
   for (j = 0; j < COL_B; j++) {
     B[i * COL_B + j] = (float)(rand() % 1000) / 100.0f;
   }
 }

 printf("Matrix Multiplication\n");
 printf("C[%d X %d] = A[%d X %d] X B[%d X %d]\n",
        ROW_A, COL_B, ROW_A, COL_A, COL_A, COL_B);

 if (option == 0) {
   printf("Sequential version...\n");
   double start_time = get_time();
   mat_mul_seq(A, B, C, ROW_A, COL_A, COL_B);
   double end_time = get_time();
   printf("Elapsed time: %f sec\n", end_time - start_time);

 } else if (option == 1) {
   printf("OpenCL version...\n");
   double start_time = get_time();
   mat_mul_opencl(A, B, C, ROW_A, COL_A, COL_B);
   double end_time = get_time();
   printf("Elapsed time (incl. initialization): %f sec\n", end_time - start_time);

 } else {
   printf("Invalid option!\n");
   exit(EXIT_FAILURE);
 }

 verify(A, B, C, ROW_A, COL_A, COL_B);

 free(A);
 free(B);
 free(C);
 return 0;
}

void verify(float *A, float *B, float *C,
           int ROW_A, int COL_A, int COL_B) {
 int i, j, k;
 float sum;

 for (i = 0; i < ROW_A; i++) {
   for (j = 0; j < COL_B; j++) {
     sum = 0.0f;
     for (k = 0; k < COL_A; k++) {
       sum += A[i * COL_A + k] * B[k * COL_B + j];
     }
     if (fabsf(C[i * COL_B + j] - sum) > 0.1) {
       printf("Verification failed! C[%d][%d]: %f vs. %f\n",
              i, j, C[i * COL_B + j], sum);
       return;
     }
   }
 }
 printf("Verification success!\n");
}


컴파일을 진행합니다.

$ make


순차 코드 실행

$ thorq --add --mode single --device gpu/1080 ./matmul 0


OpenCL 코드 실행

$ thorq --add --mode single --device gpu/1080 ./matmul 1


약 2배가 안되는 차이가 발생합니다. matrix size를 증가해보겠습니다.


엄청난 차이가 발생합니다. 232초 대비 OpenCL은 3초 정도가 차이가 발생됩니다.





'OpenCL' 카테고리의 다른 글

OpenCL [0] introduction  (0) 2018.08.27
OpenCL[2] platform, device, context, command-queue, program  (0) 2018.08.21
OpenCL [1] concept  (0) 2018.08.21
Comments