최근 CUDA와 OpenCL을 공부하고 있다. 아직 병렬 프로그래밍이 익숙하지 않아서, 예제를 다뤄보려고 한다.
여기서는 Mac을 기준, C언어로 진행한다.
_1. OpenCL 시작, deviceQuery 예제
Mac은 기본적으로 OpenCL을 내장하고 있다. 따라서 곧바로 예제를 실행해보도록하겠다.
해당 예제는 GPU device의 정보를 받아오는 예제이다.
실행코드와 Makefile의 작성은 다음과 같이 한다.
Makefile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | OPENCL=1 VPATH=./src/ EXEC=out OBJDIR=./obj/ CC=clang LDFLAGS= COMMON= CFLAGS= OPTS= CFLAGS+=$(OPTS) ifeq ($(OPENCL), 1) CFLAGS=-framework OpenCL endif OBJ=deviceQuery.o OBJS = $(addprefix $(OBJDIR), $(OBJ)) DEPS = $(wildcard src/*.h) Makefile all: obj results $(EXEC) $(EXEC): $(OBJS) $(CC) $(COMMON) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(OBJDIR)%.o: %.c $(DEPS) $(CC) $(COMMON) $(CFLAGS) -c $< -o $@ obj: mkdir -p obj results: mkdir -p results .PHONY: clean clean: rm -rf $(OBJS) $(EXEC) |
deviceQuery.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #include <stdio.h> #include <OpenCL/opencl.h> int main(int argc, char* const argv[]){ // Number of Device cl_uint num_devices, i; // getting device ids clGetDeviceIDs(NULL, CL_DEVICE_TYPE_ALL, 0, NULL, &num_devices); cl_device_id* devices = (cl_device_id*)calloc(sizeof(cl_device_id), num_devices); clGetDeviceIDs(NULL, CL_DEVICE_TYPE_ALL, num_devices, devices, NULL); char buf[128]; for (i = 0; i < num_devices; i++){ clGetDeviceInfo(devices[i], CL_DEVICE_NAME, 128, buf, NULL); fprintf(stdout, "Device %s supports", buf); clGetDeviceInfo(devices[i], CL_DEVICE_VERSION, 128, buf, NULL); // Printing device info fprintf(stdout, "%s\n", buf); } free(devices); return 0; } |
다음과 같은 디렉토리 구조에 해당 파일을 배치한다.
- Makefile
- obj
- results
- src
- deviceQuery.c
Makefile의 내용은 대략 다음과 같다.
- VPATH : 소스코드의 경로
- EXEC : make가 완료된 후에, 만들어질 실행파일의 이름
- OBJDIR : 소스코드를 이용하여 만든 Object파일을 생성할 경로
- CC : 컴파일러의 종류
- CFLAGS : 컴파일에 필요한 옵션
- OBJ : 실행파일을 만들 때, 사용할 Object파일
코드의 내용을 살펴보기전에 실행을 시켜보면, 다음과 같은 결과가 나온다.
해당 결과를 보면, 해당 노트북에 장착된 GPU에 대한 정보를 출력하는 것을 확인할 수 있다.
_2. addVector 예제
해당 예제는 vector 덧셈 예제를 GPU를 이용해서 계산하는 예제이다.
Makefile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | ifndef CC CC = gcc endif CCFLAGS=-O3 -lm LIBS = -lOpenCL -fopenmp COMMON_DIR = . # Change this variable to specify the device type # to the OpenCL device type of choice. You can also # edit the variable in the source. ifndef DEVICE DEVICE = CL_DEVICE_TYPE_DEFAULT endif # Check our platform and make sure we define the APPLE variable # and set up the right compiler flags and libraries PLATFORM = $(shell uname -s) ifeq ($(PLATFORM), Darwin) LIBS = -framework OpenCL endif CCFLAGS += -D DEVICE=$(DEVICE) vadd: vadd_c.c $(COMMON_DIR)/wtime.c $(COMMON_DIR)/device_info.c $(CC) $^ $(CCFLAGS) $(LIBS) -I $(COMMON_DIR) -o $@ clean: rm -f vadd |
device_info.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | #include <stdio.h> #include <stdlib.h> #include <string.h> #ifdef __APPLE__ #include <OpenCL/opencl.h> #else #include <CL/cl.h> #endif // // define VERBOSE if you want to print info about work groups sizes //#define VERBOSE 1 #ifdef VERBOSE extern int err_code(cl_int); #endif int output_device_info(cl_device_id device_id) { int err; // error code returned from OpenCL calls cl_device_type device_type; // Parameter defining the type of the compute device cl_uint comp_units; // the max number of compute units on a device cl_char vendor_name[1024] = {0}; // string to hold vendor name for compute device cl_char device_name[1024] = {0}; // string to hold name of compute device #ifdef VERBOSE cl_uint max_work_itm_dims; size_t max_wrkgrp_size; size_t *max_loc_size; #endif err = clGetDeviceInfo(device_id, CL_DEVICE_NAME, sizeof(device_name), &device_name, NULL); if (err != CL_SUCCESS) { printf("Error: Failed to access device name!\n"); return EXIT_FAILURE; } printf(" \n Device is %s ",device_name); err = clGetDeviceInfo(device_id, CL_DEVICE_TYPE, sizeof(device_type), &device_type, NULL); if (err != CL_SUCCESS) { printf("Error: Failed to access device type information!\n"); return EXIT_FAILURE; } if(device_type == CL_DEVICE_TYPE_GPU) printf(" GPU from "); else if (device_type == CL_DEVICE_TYPE_CPU) printf("\n CPU from "); else printf("\n non CPU or GPU processor from "); err = clGetDeviceInfo(device_id, CL_DEVICE_VENDOR, sizeof(vendor_name), &vendor_name, NULL); if (err != CL_SUCCESS) { printf("Error: Failed to access device vendor name!\n"); return EXIT_FAILURE; } printf(" %s ",vendor_name); err = clGetDeviceInfo(device_id, CL_DEVICE_MAX_COMPUTE_UNITS, sizeof(cl_uint), &comp_units, NULL); if (err != CL_SUCCESS) { printf("Error: Failed to access device number of compute units !\n"); return EXIT_FAILURE; } printf(" with a max of %d compute units \n",comp_units); #ifdef VERBOSE // // Optionally print information about work group sizes // err = clGetDeviceInfo( device_id, CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS, sizeof(cl_uint), &max_work_itm_dims, NULL); if (err != CL_SUCCESS) { printf("Error: Failed to get device Info (CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS)!\n", err_code(err)); return EXIT_FAILURE; } max_loc_size = (size_t*)malloc(max_work_itm_dims * sizeof(size_t)); if(max_loc_size == NULL){ printf(" malloc failed\n"); return EXIT_FAILURE; } err = clGetDeviceInfo( device_id, CL_DEVICE_MAX_WORK_ITEM_SIZES, max_work_itm_dims* sizeof(size_t), max_loc_size, NULL); if (err != CL_SUCCESS) { printf("Error: Failed to get device Info (CL_DEVICE_MAX_WORK_ITEM_SIZES)!\n",err_code(err)); return EXIT_FAILURE; } err = clGetDeviceInfo( device_id, CL_DEVICE_MAX_WORK_GROUP_SIZE, sizeof(size_t), &max_wrkgrp_size, NULL); if (err != CL_SUCCESS) { printf("Error: Failed to get device Info (CL_DEVICE_MAX_WORK_GROUP_SIZE)!\n",err_code(err)); return EXIT_FAILURE; } printf("work group, work item information"); printf("\n max loc dim "); for(int i=0; i< max_work_itm_dims; i++) printf(" %d ",(int)(*(max_loc_size+i))); printf("\n"); printf(" Max work group size = %d\n",(int)max_wrkgrp_size); #endif return CL_SUCCESS; } |
wtime.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #ifdef _OPENMP #include <omp.h> #else #include <sys/time.h> #endif #include <stdlib.h> double wtime() { #ifdef _OPENMP /* Use omp_get_wtime() if we can */ return omp_get_wtime(); #else /* Use a generic timer */ static int sec = -1; struct timeval tv; gettimeofday(&tv, NULL); if (sec < 0) sec = tv.tv_sec; return (tv.tv_sec - sec) + 1.0e-6*tv.tv_usec; #endif } |
vadd_c.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #ifdef __APPLE__ #include <OpenCL/opencl.h> #include <unistd.h> #else #include <CL/cl.h> #endif //pick up device type from compiler command line or from //the default type #ifndef DEVICE #define DEVICE CL_DEVICE_TYPE_DEFAULT #endif extern double wtime(); // returns time since some fixed past point (wtime.c) extern int output_device_info(cl_device_id ); //------------------------------------------------------------------------------ #define TOL (0.001) // tolerance used in floating point comparisons #define LENGTH (1024) // length of vectors a, b, and c //------------------------------------------------------------------------------ // // kernel: vadd // // Purpose: Compute the elementwise sum c = a+b // // input: a and b float vectors of length count // // output: c float vector of length count holding the sum a + b // const char *err_code (cl_int err_in) { switch (err_in) { case CL_SUCCESS: return (char*)"CL_SUCCESS"; case CL_DEVICE_NOT_FOUND: return (char*)"CL_DEVICE_NOT_FOUND"; case CL_DEVICE_NOT_AVAILABLE: return (char*)"CL_DEVICE_NOT_AVAILABLE"; case CL_COMPILER_NOT_AVAILABLE: return (char*)"CL_COMPILER_NOT_AVAILABLE"; case CL_MEM_OBJECT_ALLOCATION_FAILURE: return (char*)"CL_MEM_OBJECT_ALLOCATION_FAILURE"; case CL_OUT_OF_RESOURCES: return (char*)"CL_OUT_OF_RESOURCES"; case CL_OUT_OF_HOST_MEMORY: return (char*)"CL_OUT_OF_HOST_MEMORY"; case CL_PROFILING_INFO_NOT_AVAILABLE: return (char*)"CL_PROFILING_INFO_NOT_AVAILABLE"; case CL_MEM_COPY_OVERLAP: return (char*)"CL_MEM_COPY_OVERLAP"; case CL_IMAGE_FORMAT_MISMATCH: return (char*)"CL_IMAGE_FORMAT_MISMATCH"; case CL_IMAGE_FORMAT_NOT_SUPPORTED: return (char*)"CL_IMAGE_FORMAT_NOT_SUPPORTED"; case CL_BUILD_PROGRAM_FAILURE: return (char*)"CL_BUILD_PROGRAM_FAILURE"; case CL_MAP_FAILURE: return (char*)"CL_MAP_FAILURE"; case CL_MISALIGNED_SUB_BUFFER_OFFSET: return (char*)"CL_MISALIGNED_SUB_BUFFER_OFFSET"; case CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST: return (char*)"CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST"; case CL_INVALID_VALUE: return (char*)"CL_INVALID_VALUE"; case CL_INVALID_DEVICE_TYPE: return (char*)"CL_INVALID_DEVICE_TYPE"; case CL_INVALID_PLATFORM: return (char*)"CL_INVALID_PLATFORM"; case CL_INVALID_DEVICE: return (char*)"CL_INVALID_DEVICE"; case CL_INVALID_CONTEXT: return (char*)"CL_INVALID_CONTEXT"; case CL_INVALID_QUEUE_PROPERTIES: return (char*)"CL_INVALID_QUEUE_PROPERTIES"; case CL_INVALID_COMMAND_QUEUE: return (char*)"CL_INVALID_COMMAND_QUEUE"; case CL_INVALID_HOST_PTR: return (char*)"CL_INVALID_HOST_PTR"; case CL_INVALID_MEM_OBJECT: return (char*)"CL_INVALID_MEM_OBJECT"; case CL_INVALID_IMAGE_FORMAT_DESCRIPTOR: return (char*)"CL_INVALID_IMAGE_FORMAT_DESCRIPTOR"; case CL_INVALID_IMAGE_SIZE: return (char*)"CL_INVALID_IMAGE_SIZE"; case CL_INVALID_SAMPLER: return (char*)"CL_INVALID_SAMPLER"; case CL_INVALID_BINARY: return (char*)"CL_INVALID_BINARY"; case CL_INVALID_BUILD_OPTIONS: return (char*)"CL_INVALID_BUILD_OPTIONS"; case CL_INVALID_PROGRAM: return (char*)"CL_INVALID_PROGRAM"; case CL_INVALID_PROGRAM_EXECUTABLE: return (char*)"CL_INVALID_PROGRAM_EXECUTABLE"; case CL_INVALID_KERNEL_NAME: return (char*)"CL_INVALID_KERNEL_NAME"; case CL_INVALID_KERNEL_DEFINITION: return (char*)"CL_INVALID_KERNEL_DEFINITION"; case CL_INVALID_KERNEL: return (char*)"CL_INVALID_KERNEL"; case CL_INVALID_ARG_INDEX: return (char*)"CL_INVALID_ARG_INDEX"; case CL_INVALID_ARG_VALUE: return (char*)"CL_INVALID_ARG_VALUE"; case CL_INVALID_ARG_SIZE: return (char*)"CL_INVALID_ARG_SIZE"; case CL_INVALID_KERNEL_ARGS: return (char*)"CL_INVALID_KERNEL_ARGS"; case CL_INVALID_WORK_DIMENSION: return (char*)"CL_INVALID_WORK_DIMENSION"; case CL_INVALID_WORK_GROUP_SIZE: return (char*)"CL_INVALID_WORK_GROUP_SIZE"; case CL_INVALID_WORK_ITEM_SIZE: return (char*)"CL_INVALID_WORK_ITEM_SIZE"; case CL_INVALID_GLOBAL_OFFSET: return (char*)"CL_INVALID_GLOBAL_OFFSET"; case CL_INVALID_EVENT_WAIT_LIST: return (char*)"CL_INVALID_EVENT_WAIT_LIST"; case CL_INVALID_EVENT: return (char*)"CL_INVALID_EVENT"; case CL_INVALID_OPERATION: return (char*)"CL_INVALID_OPERATION"; case CL_INVALID_GL_OBJECT: return (char*)"CL_INVALID_GL_OBJECT"; case CL_INVALID_BUFFER_SIZE: return (char*)"CL_INVALID_BUFFER_SIZE"; case CL_INVALID_MIP_LEVEL: return (char*)"CL_INVALID_MIP_LEVEL"; case CL_INVALID_GLOBAL_WORK_SIZE: return (char*)"CL_INVALID_GLOBAL_WORK_SIZE"; case CL_INVALID_PROPERTY: return (char*)"CL_INVALID_PROPERTY"; default: return (char*)"UNKNOWN ERROR"; } } void check_error(cl_int err, const char *operation, char *filename, int line) { if (err != CL_SUCCESS) { fprintf(stderr, "Error during operation '%s', ", operation); fprintf(stderr, "in '%s' on line %d\n", filename, line); fprintf(stderr, "Error code was \"%s\" (%d)\n", err_code(err), err); exit(EXIT_FAILURE); } } #define checkError(E, S) check_error(E,S,__FILE__,__LINE__) const char *KernelSource = "\n" \ "__kernel void vadd( \n" \ " __global float* a, \n" \ " __global float* b, \n" \ " __global float* c, \n" \ " const unsigned int count) \n" \ "{ \n" \ " int i = get_global_id(0); \n" \ " if(i < count) \n" \ " c[i] = a[i] + b[i]; \n" \ "} \n" \ "\n"; //------------------------------------------------------------------------------ int main(int argc, char** argv) { int err; // error code returned from OpenCL calls float* h_a = (float*) calloc(LENGTH, sizeof(float)); // a vector float* h_b = (float*) calloc(LENGTH, sizeof(float)); // b vector float* h_c = (float*) calloc(LENGTH, sizeof(float)); // c vector (a+b) returned from the compute device unsigned int correct; // number of correct results size_t global; // global domain size cl_device_id device_id; // compute device id cl_context context; // compute context cl_command_queue commands; // compute command queue cl_program program; // compute program cl_kernel ko_vadd; // compute kernel cl_mem d_a; // device memory used for the input a vector cl_mem d_b; // device memory used for the input b vector cl_mem d_c; // device memory used for the output c vector // Fill vectors a and b with random float values int i = 0; int count = LENGTH; for(i = 0; i < count; i++){ h_a[i] = rand() / (float)RAND_MAX; h_b[i] = rand() / (float)RAND_MAX; } // Set up platform and GPU device cl_uint numPlatforms; // Find number of platforms err = clGetPlatformIDs(0, NULL, &numPlatforms); checkError(err, "Finding platforms"); if (numPlatforms == 0) { printf("Found 0 platforms!\n"); return EXIT_FAILURE; } // Get all platforms cl_platform_id Platform[numPlatforms]; err = clGetPlatformIDs(numPlatforms, Platform, NULL); checkError(err, "Getting platforms"); // Secure a GPU for (i = 0; i < numPlatforms; i++) { err = clGetDeviceIDs(Platform[i], DEVICE, 1, &device_id, NULL); if (err == CL_SUCCESS) { break; } } if (device_id == NULL) checkError(err, "Finding a device"); err = output_device_info(device_id); checkError(err, "Printing device output"); // Create a compute context context = clCreateContext(0, 1, &device_id, NULL, NULL, &err); checkError(err, "Creating context"); // Create a command queue commands = clCreateCommandQueue(context, device_id, 0, &err); checkError(err, "Creating command queue"); // Create the compute program from the source buffer program = clCreateProgramWithSource(context, 1, (const char **) & KernelSource, NULL, &err); checkError(err, "Creating program"); // Build the program err = clBuildProgram(program, 0, NULL, NULL, NULL, NULL); if (err != CL_SUCCESS) { size_t len; char buffer[2048]; printf("Error: Failed to build program executable!\n%s\n", err_code(err)); clGetProgramBuildInfo(program, device_id, CL_PROGRAM_BUILD_LOG, sizeof(buffer), buffer, &len); printf("%s\n", buffer); return EXIT_FAILURE; } // Create the compute kernel from the program ko_vadd = clCreateKernel(program, "vadd", &err); checkError(err, "Creating kernel"); // Create the input (a, b) and output (c) arrays in device memory d_a = clCreateBuffer(context, CL_MEM_READ_ONLY, sizeof(float) * count, NULL, &err); checkError(err, "Creating buffer d_a"); d_b = clCreateBuffer(context, CL_MEM_READ_ONLY, sizeof(float) * count, NULL, &err); checkError(err, "Creating buffer d_b"); d_c = clCreateBuffer(context, CL_MEM_WRITE_ONLY, sizeof(float) * count, NULL, &err); checkError(err, "Creating buffer d_c"); // Write a and b vectors into compute device memory err = clEnqueueWriteBuffer(commands, d_a, CL_TRUE, 0, sizeof(float) * count, h_a, 0, NULL, NULL); checkError(err, "Copying h_a to device at d_a"); err = clEnqueueWriteBuffer(commands, d_b, CL_TRUE, 0, sizeof(float) * count, h_b, 0, NULL, NULL); checkError(err, "Copying h_b to device at d_b"); // Set the arguments to our compute kernel err = clSetKernelArg(ko_vadd, 0, sizeof(cl_mem), &d_a); err |= clSetKernelArg(ko_vadd, 1, sizeof(cl_mem), &d_b); err |= clSetKernelArg(ko_vadd, 2, sizeof(cl_mem), &d_c); err |= clSetKernelArg(ko_vadd, 3, sizeof(unsigned int), &count); checkError(err, "Setting kernel arguments"); double rtime = wtime(); // Execute the kernel over the entire range of our 1d input data set // letting the OpenCL runtime choose the work-group size global = count; err = clEnqueueNDRangeKernel(commands, ko_vadd, 1, NULL, &global, NULL, 0, NULL, NULL); checkError(err, "Enqueueing kernel"); // Wait for the commands to complete before stopping the timer err = clFinish(commands); checkError(err, "Waiting for kernel to finish"); rtime = wtime() - rtime; printf("\nThe kernel ran in %lf seconds\n",rtime); // Read back the results from the compute device err = clEnqueueReadBuffer( commands, d_c, CL_TRUE, 0, sizeof(float) * count, h_c, 0, NULL, NULL ); if (err != CL_SUCCESS) { printf("Error: Failed to read output array!\n%s\n", err_code(err)); exit(1); } // Test the results correct = 0; float tmp; for(i = 0; i < count; i++) { tmp = h_a[i] + h_b[i]; // assign element i of a+b to tmp tmp -= h_c[i]; // compute deviation of expected and output result if(tmp*tmp < TOL*TOL) // correct if square deviation is less than tolerance squared correct++; else { printf(" tmp %f h_a %f h_b %f h_c %f \n",tmp, h_a[i], h_b[i], h_c[i]); } } // summarise results printf("C = A+B: %d out of %d results were correct.\n", correct, count); // cleanup then shutdown clReleaseMemObject(d_a); clReleaseMemObject(d_b); clReleaseMemObject(d_c); clReleaseProgram(program); clReleaseKernel(ko_vadd); clReleaseCommandQueue(commands); clReleaseContext(context); free(h_a); free(h_b); free(h_c); return 0; } | cs |
다음과 같은 디렉토리 구조에 해당 파일을 배치한다.
- Makefile
- vadd_c.c
- device_info.c
- wtime.c
이제 코드를 Make하고 실행시켜보면 다음과 같은 결과를 확인할 수 있다.
다음과 같이 출력이 된다면, 성공적으로 예제를 구동한 것이다.
이것으로 연산을 진행하는 실질적인 예제를 보았으니,
다음 포스팅부터는 OpenCL와 GPGPU에 대한 내용을 시작으로 OpenCL 프로그래밍에 대한 자세한 내용을 포스팅하도록 하겠다.
'IT > Parallel Computing' 카테고리의 다른 글
[OpenCL] Error Code (0) | 2017.04.19 |
---|---|
[OpenCL] Glossary - 용어 (0) | 2017.04.19 |