이번 글에서는 CMake를 사용하여 OpenCL 기반의 응용 프로그램을 구성하고 빌드하는 방법을 알아보겠습니다. OpenCL은 이기종 시스템에서 병렬 프로그래밍을 위한 프레임워크로, CPU, GPU, FPGA 등 다양한 디바이스에서 실행할 수 있는 코드를 작성할 수 있습니다. CMake를 활용하여 OpenCL 프로젝트를 효율적으로 관리하고 빌드 시스템에 통합하는 방법을 살펴보겠습니다.
OpenCL과 CMake의 통합
OpenCL 프로젝트를 CMake로 빌드하려면 OpenCL 헤더와 라이브러리를 설정하고, CMake에서 이를 올바르게 찾고 링크해야 합니다. OpenCL은 Khronos Group에서 표준을 정의하며, 각 하드웨어 제조사에서 구현체를 제공합니다.
OpenCL 설치
- Intel CPU: Intel OpenCL SDK를 설치합니다.
- NVIDIA GPU: NVIDIA의 경우 OpenCL이 CUDA에 포함되어 있습니다. CUDA Toolkit을 설치합니다.
- AMD GPU: AMD APP SDK를 설치합니다.
- macOS: OpenCL은 시스템에 기본적으로 포함되어 있습니다.
프로젝트 구성
디렉토리 구조
my_opencl_project/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ ├── main.cpp
│ └── kernel.cl
└── include/
└── opencl_app.hpp
최상위 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyOpenCLProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(src)
src/CMakeLists.txt
# OpenCL 패키지 찾기
find_package(OpenCL REQUIRED)
# 실행 파일 생성
add_executable(MyOpenCLApp main.cpp)
# OpenCL 라이브러리 링크
target_link_libraries(MyOpenCLApp PRIVATE OpenCL::OpenCL)
# 인클루드 디렉토리 설정
target_include_directories(MyOpenCLApp PRIVATE ${OpenCL_INCLUDE_DIRS})
# 커널 파일 복사 설정
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/kernel.cl ${CMAKE_CURRENT_BINARY_DIR}/kernel.cl COPYONLY)
# 실행 파일에 커널 경로 정의
target_compile_definitions(MyOpenCLApp PRIVATE
KERNEL_FILE="${CMAKE_CURRENT_BINARY_DIR}/kernel.cl"
)
main.cpp 예제
#include <iostream>
#include <vector>
#include <CL/cl.hpp>
#include "opencl_app.hpp"
int main() {
try {
OpenCLApp app;
app.run();
} catch (const cl::Error &e) {
std::cerr << "OpenCL 오류: " << e.what() << "(" << e.err() << ")" << std::endl;
return EXIT_FAILURE;
} catch (const std::exception &e) {
std::cerr << "오류: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
opencl_app.hpp 및 opencl_app.cpp 예제
opencl_app.hpp
#pragma once
#include <CL/cl.hpp>
class OpenCLApp {
public:
void run();
private:
void initOpenCL();
void executeKernel();
void cleanup();
cl::Platform platform;
cl::Device device;
cl::Context context;
cl::Program program;
cl::CommandQueue queue;
};
opencl_app.cpp
#include "opencl_app.hpp"
#include <iostream>
#include <fstream>
#include <vector>
void OpenCLApp::run() {
initOpenCL();
executeKernel();
cleanup();
}
void OpenCLApp::initOpenCL() {
// 플랫폼 및 디바이스 선택
std::vector<cl::Platform> platforms;
cl::Platform::get(&platforms);
platform = platforms.front();
std::vector<cl::Device> devices;
platform.getDevices(CL_DEVICE_TYPE_DEFAULT, &devices);
device = devices.front();
// 컨텍스트 및 커맨드 큐 생성
context = cl::Context(device);
queue = cl::CommandQueue(context, device);
// 커널 소스 읽기
std::ifstream kernelFile(KERNEL_FILE);
std::string sourceCode(std::istreambuf_iterator<char>(kernelFile), (std::istreambuf_iterator<char>()));
cl::Program::Sources sources(1, std::make_pair(sourceCode.c_str(), sourceCode.length() + 1));
// 프로그램 생성 및 빌드
program = cl::Program(context, sources);
program.build({device});
}
void OpenCLApp::executeKernel() {
// 데이터 준비
const int arraySize = 10;
std::vector<int> A(arraySize, 1);
std::vector<int> B(arraySize, 2);
std::vector<int> C(arraySize);
// 버퍼 생성
cl::Buffer bufferA(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(int) * arraySize, A.data());
cl::Buffer bufferB(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(int) * arraySize, B.data());
cl::Buffer bufferC(context, CL_MEM_WRITE_ONLY, sizeof(int) * arraySize);
// 커널 설정
cl::Kernel kernel(program, "vector_add");
kernel.setArg(0, bufferA);
kernel.setArg(1, bufferB);
kernel.setArg(2, bufferC);
// 커널 실행
queue.enqueueNDRangeKernel(kernel, cl::NullRange, cl::NDRange(arraySize), cl::NullRange);
// 결과 읽기
queue.enqueueReadBuffer(bufferC, CL_TRUE, 0, sizeof(int) * arraySize, C.data());
// 결과 출력
std::cout << "결과: ";
for (int i = 0; i < arraySize; ++i) {
std::cout << C[i] << " ";
}
std::cout << std::endl;
}
void OpenCLApp::cleanup() {
// OpenCL 자원은 소멸자를 통해 자동으로 해제됩니다.
}
kernel.cl 예제
__kernel void vector_add(__global const int* A, __global const int* B, __global int* C) {
int i = get_global_id(0);
C[i] = A[i] + B[i];
}
OpenCL 패키지 찾기
CMake에서 OpenCL을 찾기 위해 find_package(OpenCL REQUIRED)를 사용합니다. 이때, CMake는 시스템에서 OpenCL 라이브러리와 헤더 파일을 찾습니다.
- OpenCL_FOUND: OpenCL이 발견되었는지 여부
- OpenCL_INCLUDE_DIRS: OpenCL 헤더 파일의 경로
- OpenCL_LIBRARIES: OpenCL 라이브러리
CMake 3.7 이상에서는 OpenCL 패키지를 더 쉽게 찾을 수 있도록 개선되었습니다.
전체적인 CMake 파일 구조
최종적으로 src/CMakeLists.txt는 다음과 같습니다.
# OpenCL 패키지 찾기
find_package(OpenCL REQUIRED)
# 실행 파일 생성
add_executable(MyOpenCLApp main.cpp opencl_app.cpp)
# OpenCL 라이브러리 링크
target_link_libraries(MyOpenCLApp PRIVATE OpenCL::OpenCL)
# 인클루드 디렉토리 설정
target_include_directories(MyOpenCLApp PRIVATE
${OpenCL_INCLUDE_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}
)
# 커널 파일 복사 설정
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/kernel.cl ${CMAKE_CURRENT_BINARY_DIR}/kernel.cl COPYONLY)
# 실행 파일에 커널 경로 정의
target_compile_definitions(MyOpenCLApp PRIVATE
KERNEL_FILE="${CMAKE_CURRENT_BINARY_DIR}/kernel.cl"
)
빌드 및 실행
빌드
mkdir build
cd build
cmake ..
cmake --build .
실행
./MyOpenCLApp
- OpenCL 런타임과 드라이버가 올바르게 설치되어 있어야 합니다.
- 실행 시 벡터 덧셈의 결과가 출력됩니다.
주의 사항
플랫폼별 설정
- Windows: OpenCL SDK 설치 후, 환경 변수에 OpenCL 라이브러리 경로를 추가해야 할 수 있습니다.
- Linux: 드라이버와 라이브러리가 올바르게 설치되었는지 확인합니다.
- macOS: OpenCL은 시스템에 포함되어 있으므로 추가 설정이 필요하지 않습니다.
OpenCL 헤더 및 라이브러리 경로 확인
OpenCL 헤더와 라이브러리의 경로가 올바르게 설정되지 않으면 find_package()가 실패할 수 있습니다. 이 경우 CMake 명령줄에서 경로를 수동으로 지정할 수 있습니다.
cmake -DOpenCL_INCLUDE_DIRS=/path/to/opencl/include -DOpenCL_LIBRARY=/path/to/opencl/lib ..
OpenCL C++ 바인딩 사용
OpenCL C++ 바인딩을 사용하기 위해 <CL/cl2.hpp>를 포함할 수 있습니다. 이 헤더는 OpenCL C++ API를 제공합니다.
- OpenCL 2.1 이상이 필요합니다.
- CMake 설정에서 OpenCL_INCLUDE_DIRS에 C++ 바인딩 헤더의 경로를 추가해야 할 수 있습니다.
참고 자료
- OpenCL 공식 사이트
- OpenCL SDK 다운로드
- OpenCL 튜토리얼
- CMake 공식 문서: find_package()
- CMake로 OpenCL 프로젝트 설정하기
- OpenCL Programming Guide
- OpenCL Specification
- OpenCL C++ Bindings
'개발 이야기 > CMake' 카테고리의 다른 글
[모던 CMake] SIMD 프로젝트 구성과 빌드 (1) | 2024.12.13 |
---|---|
[모던 CMake] Vulkan 프로젝트 구성과 빌드 (0) | 2024.12.13 |
[모던 CMake] 재사용 가능한 라이브러리를 위한 CMake 패키지 구성 (0) | 2024.12.12 |
[모던 CMake] Qt 프로젝트 구성과 빌드 (1) | 2024.12.11 |
[모던 CMake] CUDA 프로젝트 구성과 빌드 (0) | 2024.12.10 |