안녕하세요! 이번 포스팅부터 OpenCL(Open Computing Language)을 활용해 GPU 가속 프로그래밍을 시작하려는 입문자 분들을 위해 총 10편에 걸친 시리즈를 진행하려고 해요.
OpenCL은 GPU, CPU, FPGA 등 다양한 디바이스에서 병렬 계산을 지원하는 오픈 표준인데요. 흔히 비교되는 CUDA가 NVIDIA GPU를 염두에 둔 전용 기술이라면, OpenCL은 다양한 벤더와 디바이스에서 유연하게 활용할 수 있는 특징이 있어요. “어? 난 이미 CUDA에 좀 익숙한데?” 하는 분들도, 여기서 OpenCL을 배워두면 훨씬 넓은 하드웨어 지원 범위를 가질 수 있게 되는 셈입니다.
이번 첫 글에서는 다음과 같은 내용을 담았습니다.
- OpenCL 개발 환경 준비(Ubuntu, Windows 참조)
- 드라이버 및 의존성 패키지 설치
- CMake 빌드 환경 구성
- 간단한 "Hello OpenCL!" 예제 코드 작성 및 빌드
- 기존 C++ 스타일 코드와 C++20/23 스타일 코드 비교
- CUDA 환경과 OpenCL 환경의 차이점 간략 비교
앞으로 시리즈는 다음과 같이 진행될 예정이에요.
- 개발환경 준비 & Hello OpenCL! (이번 글)
- OpenCL 플랫폼과 디바이스 이해하기
- 커널 작성 & 빌드: 기본 문법 따라하기
- 메모리 관리 기초: 버퍼, 이미지, 파라미터
- C++20/23로 깔끔하게 감싸기 (래퍼 활용)
- 간단한 이미지 처리 예제
- 성능 최적화 맛보기
- 디버깅 & 프로파일링 기초
- 다중 디바이스 활용
- 마무리 및 추가 학습 자료 소개
중간중간 CUDA나 SYCL과 같은 다른 기술과 비교를 통해 “왜 OpenCL?”인지, 그리고 모던 C++ 문법을 어떻게 잘 활용할 수 있는지 부드럽게 짚어볼게요.
그럼 바로 시작해보겠습니다!
1. 사전 준비사항
Ubuntu 환경 (권장)
GPU 드라이버 설치:
NVIDIA GPU가 있다면 다음처럼 드라이버를 설치할 수 있어요:
sudo apt update
sudo apt install nvidia-driver-525
(드라이버 버전은 상황에 맞게 조정하세요.)
설치 후 재부팅을 추천합니다.
OpenCL 런타임 설치:
NVIDIA GPU 기준으로는 CUDA Toolkit에 OpenCL 런타임이 포함되어 있습니다.
sudo apt install nvidia-cuda-toolkit
Intel GPU의 경우 intel-opencl-icd 등, AMD의 경우 ROCm 등 상황에 맞는 패키지를 설치하면 됩니다.
CMake 설치:
Ubuntu 20.04 이상에서는 비교적 최신 CMake가 포함되어 있어요.
sudo apt install cmake
C++ 컴파일러 설치:
C++20/23을 쓰려면 GCC 11 이상이나 Clang 14 이상 버전을 권장해요.
sudo apt install build-essential
Windows 환경(참고용)
- GPU 드라이버:
NVIDIA라면 NVIDIA 홈페이지에서 최신 드라이버 설치 후, CUDA Toolkit을 받아 설치하세요. - OpenCL SDK:
Intel, AMD, NVIDIA 각 벤더별 OpenCL SDK가 있고, CMake, Visual Studio 2022 이상(최신 MSVC 툴킷 포함)도 설치해주세요.
2. CUDA와 OpenCL 환경 비교 (간단히)
CUDA 개발을 해보신 분이라면, CUDA는 NVIDIA GPU 전용 툴킷과 런타임이 깔끔하게 정리되어 있어서 편하셨을 거예요. 반면 OpenCL은 조금 더 다양한 하드웨어를 지원한다는 장점이 있지만, 기본 API는 다소 장황하고 저수준에 가까워요.
- CUDA: NVIDIA 전용, 문서나 예제가 잘 정리되어 있고, 더 최신 기능을 빠르게 반영하는 편. 드라이버 & 런타임, 컴파일러(nvcc), 라이브러리가 잘 패키징.
- OpenCL: 하드웨어에 대한 중립성 확보(AMD, Intel, NVIDIA 모두 지원), 오픈 표준 기반. 다만 디바이스 선택, 플랫폼 관리 등 직접 해줘야 할 게 많고, 초기 설정이 조금 번거로울 수 있음.
추후에 C++ 래퍼나 모던 API를 활용하면 OpenCL도 충분히 깔끔한 코드 작성이 가능해집니다. 또한 OpenCL은 GPU 뿐 아니라 CPU, FPGA 등 다양한 디바이스로 확장할 수 있어, 나중에 범용성이 필요할 때 유리해요.
3. 간단한 예제 프로젝트 구조
먼저 "Hello OpenCL!"를 출력하면서 기본적인 연산을 해보는 예제를 만들어봅시다. 디렉토리를 이렇게 잡을게요:
OpenCL-Tutorial/
├─ CMakeLists.txt
└─ src/
├─ main.cpp
└─ kernel.cl
- main.cpp: 호스트 코드 (CPU 측)
- kernel.cl: 디바이스 코드 (GPU 측에서 실행)
4. CMakeLists.txt 예제
C++20 표준을 쓰도록 하고, OpenCL 라이브러리를 찾은 뒤 빌드 타깃에 연결하는 예제입니다.
cmake_minimum_required(VERSION 3.16)
project(HelloOpenCL LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(OpenCL REQUIRED)
add_executable(hello_opencl src/main.cpp)
target_include_directories(hello_opencl PRIVATE ${OpenCL_INCLUDE_DIRS})
target_link_libraries(hello_opencl PRIVATE ${OpenCL_LIBRARIES})
CUDA에 익숙하다면, CUDA에서는 cuda_add_executable(), nvcc 컴파일러 사용 등을 했던 기억이 있으실 거예요. 여기서는 표준 C/C++ 컴파일러와 OpenCL 라이브러리를 링크하는 방식을 취합니다. 각 벤더별 런타임이 OpenCL ICD(Installable Client Driver) 형태로 제공되어, CMake가 이를 찾아 링크하도록 하는 방식이죠.
5. kernel.cl 예제
정말 간단한 커널을 작성해보죠. 이 커널은 입력 배열의 모든 값을 +1 증가시키는 일을 합니다.
__kernel void add_one(__global int* data) {
int gid = get_global_id(0);
data[gid] += 1;
}
CUDA 커널 코드(CUDA C/C++와 .cu 파일)와 비교하면, OpenCL 커널은 .cl 파일에 클라이언트 코드(host)에서 런타임에 빌드하는 점에서 차이가 있어요. CUDA는 nvcc 컴파일러로 사전에 커널을 컴파일하거나 PTX 코드로 변환하는 반면, OpenCL은 런타임 시점에 해당 디바이스에 맞춰 커널을 빌드하는 경우가 많습니다.
6. main.cpp (호스트 코드 예제)
Before (옛날 C++ 스타일):
예전 코드를 대략적으로 보면, OpenCL 함수 호출이 줄줄이 나오고 에러 체크용 if문이 계속 반복됩니다. CUDA도 마찬가지지만, CUDA는 훨씬 더 다양한 헬퍼 함수나 깔끔한 API들이 제공되어 초기 코드 작성이 좀 더 간단한 편이었습니다. 반면 OpenCL 표준 API는 이런 부분을 개발자가 직접 해야 했죠.
After (C++20 스타일로 조금 더 깔끔하게):
C++20 이상의 문법을 써서 코드를 정돈하면, 예전보다 auto, range-based for문, raw 문자열 리터럴(R"..." ) 활용으로 가독성이 좋아집니다. 물론 API 자체가 크게 바뀌진 않았지만, C++ 언어 측면에서 현대적인 스타일을 도입함으로써 전체 코드가 조금 더 깔끔해진다는 점이 핵심입니다.
#include <CL/cl.h>
#include <iostream>
#include <vector>
#include <stdexcept>
int main() {
// 플랫폼 정보 가져오기
cl_uint num_platforms = 0;
clGetPlatformIDs(0, nullptr, &num_platforms);
if (num_platforms == 0) {
std::cerr << "OpenCL 플랫폼을 찾을 수 없어요.\n";
return 1;
}
std::vector<cl_platform_id> platforms(num_platforms);
clGetPlatformIDs(num_platforms, platforms.data(), nullptr);
auto platform = platforms[0];
// GPU 디바이스 가져오기 (GPU가 없으면 CPU로 fallback)
cl_uint num_devices = 0;
clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 0, nullptr, &num_devices);
if (num_devices == 0) {
std::cerr << "GPU 디바이스를 찾지 못했어요. 그럼 CPU를 써볼게요.\n";
clGetDeviceIDs(platform, CL_DEVICE_TYPE_CPU, 0, nullptr, &num_devices);
if (num_devices == 0) {
std::cerr << "사용 가능한 디바이스가 없네요. 종료하겠습니다.\n";
return 1;
}
}
std::vector<cl_device_id> devices(num_devices);
clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, num_devices, devices.data(), nullptr);
auto device = devices[0];
// 컨텍스트 및 큐 생성
cl_int err;
cl_context context = clCreateContext(nullptr, 1, &device, nullptr, nullptr, &err);
if (err != CL_SUCCESS) throw std::runtime_error("컨텍스트 생성 실패!");
#if defined(CL_VERSION_2_0)
cl_command_queue queue = clCreateCommandQueueWithProperties(context, device, 0, &err);
#else
cl_command_queue queue = clCreateCommandQueue(context, device, 0, &err);
#endif
if (err != CL_SUCCESS) throw std::runtime_error("커맨드 큐 생성 실패!");
// 커널 코드 (런타임 빌드)
const char* kernelSource = R"CLC(
__kernel void add_one(__global int* data) {
int gid = get_global_id(0);
data[gid] += 1;
}
)CLC";
cl_program program = clCreateProgramWithSource(context, 1, &kernelSource, nullptr, &err);
if (err != CL_SUCCESS) throw std::runtime_error("프로그램 생성 실패!");
err = clBuildProgram(program, 1, &device, nullptr, nullptr, nullptr);
if (err != CL_SUCCESS) {
// 빌드 로그 출력
size_t log_size;
clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, nullptr, &log_size);
std::vector<char> build_log(log_size);
clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, build_log.data(), nullptr);
std::cerr << "빌드 에러:\n" << build_log.data() << "\n";
return 1;
}
cl_kernel kernel = clCreateKernel(program, "add_one", &err);
if (err != CL_SUCCESS) throw std::runtime_error("커널 생성 실패!");
// 테스트용 데이터
std::vector<int> data = {10, 20, 30, 40, 50};
// 버퍼 생성
cl_mem buffer = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR,
data.size() * sizeof(int), data.data(), &err);
if (err != CL_SUCCESS) throw std::runtime_error("버퍼 생성 실패!");
// 커널 인자 설정
err = clSetKernelArg(kernel, 0, sizeof(buffer), &buffer);
if (err != CL_SUCCESS) throw std::runtime_error("커널 인자 설정 실패!");
// 커널 실행
size_t global_work_size = data.size();
err = clEnqueueNDRangeKernel(queue, kernel, 1, nullptr, &global_work_size, nullptr, 0, nullptr, nullptr);
if (err != CL_SUCCESS) throw std::runtime_error("커널 실행 실패!");
// 결과 읽어오기
err = clEnqueueReadBuffer(queue, buffer, CL_TRUE, 0, data.size() * sizeof(int), data.data(), 0, nullptr, nullptr);
if (err != CL_SUCCESS) throw std::runtime_error("결과 읽기 실패!");
std::cout << "Hello OpenCL! 결과: ";
for (auto val : data) {
std::cout << val << " ";
}
std::cout << "\n";
// 자원 해제
clReleaseMemObject(buffer);
clReleaseKernel(kernel);
clReleaseProgram(program);
clReleaseCommandQueue(queue);
clReleaseContext(context);
return 0;
}
위 코드가 한 번에 이해가 안되셔도 괜찮습니다. 어차피 이번 글은 개발환경 설정과 “Hello OpenCL!”를 찍어보는 것이 목표니까요. 다음 글들에서 OpenCL의 각 요소(플랫폼, 디바이스, 커널, 메모리)를 단계별로 파헤칠 예정입니다.
CUDA에 익숙한 분이라면, CUDA에서는 cudaMalloc, cudaMemcpy, <<< >>> 연산자 등을 통해 훨씬 직관적으로 커널 호출을 했던 기억이 있을 거예요. OpenCL은 이런 부분을 함수 호출로 처리하기 때문에 조금 더 장황해 보일 수 있지만, 대신 어떤 디바이스를 선택할지, 어떤 플랫폼을 사용할지 등 세부적 제어가 유연합니다.
7. 빌드 및 실행
Ubuntu에서 빌드하는 예제:
cd OpenCL-Tutorial
mkdir build && cd build
cmake ..
make
./hello_opencl
정상적으로 실행되면
Hello OpenCL! 결과: 11 21 31 41 51
와 같이 모든 원소가 +1 증가한 값을 확인할 수 있어요.
8. 마무리
이번 글에서는 OpenCL을 시작하기 위해 필요한 환경 설정(Ubuntu & Windows), 드라이버 및 의존성 설치, CMake 기반 빌드 환경 구성, 그리고 간단한 “Hello OpenCL!” 예제를 다뤘습니다. CUDA를 사용해보신 분들은 CUDA 환경보다 약간 더 많은 초기 설정 절차를 느낄 수 있겠지만, 다음 글에서는 플랫폼과 디바이스를 더 잘 이해하고, 메모리 모델과 커널 빌드 과정을 차근차근 따라가며 OpenCL을 좀 더 친숙하게 만들어 볼게요.
다음 편에서는 플랫폼, 디바이스, 그리고 이들을 다루는 방법에 대해 좀 더 깊이 이야기하겠습니다.
유용한 링크 & 리소스
- OpenCL 공식 홈페이지 : OpenCL 표준 문서 및 정보
- NVIDIA CUDA Toolkit : CUDA 런타임 및 개발환경 (비교용)
- CMake 공식 홈페이지 : 빌드 시스템 설정 레퍼런스
- Khronos Group GitHub : OpenCL 샘플 코드 및 유틸리티
- C++ Reference : 최신 C++ 문법 및 라이브러리 참고용
'개발 이야기 > OpenCL' 카테고리의 다른 글
[OpenCL 입문 시리즈 6편] 간단한 이미지 처리 예제: 그레이스케일 변환하기 (0) | 2024.12.12 |
---|---|
[OpenCL 입문 시리즈 5편] C++20/23로 깔끔하게 래핑하기 (0) | 2024.12.11 |
[OpenCL 입문 시리즈 4편] 메모리 관리 기초: 버퍼(Buffer), 이미지(Image), 파라미터(Argument) 다루기 (0) | 2024.12.10 |
[OpenCL 입문 시리즈 3편] 커널 작성 & 빌드: 기본 문법 따라하기 (1) | 2024.12.09 |
[OpenCL 입문 시리즈 2편] 플랫폼과 디바이스 이해하기 (0) | 2024.12.08 |