[모던 CMake] OpenCL 프로젝트 구성과 빌드

이번 글에서는 CMake를 사용하여 OpenCL 기반의 응용 프로그램을 구성하고 빌드하는 방법을 알아보겠습니다. OpenCL은 이기종 시스템에서 병렬 프로그래밍을 위한 프레임워크로, CPU, GPU, FPGA 등 다양한 디바이스에서 실행할 수 있는 코드를 작성할 수 있습니다. CMake를 활용하여 OpenCL 프로젝트를 효율적으로 관리하고 빌드 시스템에 통합하는 방법을 살펴보겠습니다.

etc-image-0

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++ 바인딩 헤더의 경로를 추가해야 할 수 있습니다.

참고 자료

반응형