[CUDA & Modern C++로 GPU 프로그래밍 시작하기] #7: CMake 활용한 현대적 빌드 설정과 Modern C++ 적용하기

이제까지 우리는 CUDA의 기본 문법, 메모리 계층, 비동기 스트림, 2D 데이터 처리 등 다양한 기초 개념을 다뤄왔습니다. 하지만 이 모든 것이 현업이나 프로젝트에서 활용되기 위해서는 체계적인 빌드 환경과 현대적 C++ 기능과의 자연스러운 결합이 중요합니다. 이번 글에서는 CMake를 통한 빌드 시스템 설정 방법C++20/23 기능(Coroutine, Concept, Constexpr, Lambda 개선 등)을 CUDA 코드와 융합하여 더 깔끔하고 유지보수하기 쉬운 코드를 만드는 방법을 살펴보겠습니다.

etc-image-0

CMake로 CUDA 빌드 환경 현대적으로 관리하기

기본 구조 복습

이전 글에서 간단히 소개했듯이, CMake는 CUDA를 별도의 언어로 인식하고, project(... LANGUAGES CUDA CXX) 형태로 프로젝트를 정의하면 NVCC와 C++ 컴파일러를 함께 다룰 수 있습니다.

예시 CMakeLists.txt:

cmake_minimum_required(VERSION 3.20 FATAL_ERROR)
project(my_cuda_project LANGUAGES CXX CUDA)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CUDA_STANDARD 20)

# 빌드 타입, 최적화 옵션 설정
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

# CUDA 아키텍처 설정 (예: sm_70)
set(CMAKE_CUDA_ARCHITECTURES 70)

add_executable(my_app main.cu utils.cu)

위와 같이 하면 CMake가 CUDA 소스(.cu) 파일을 NVCC로 처리하고, C++20 표준과 CUDA 표준을 모두 적용합니다. 또한 CMAKE_CUDA_ARCHITECTURES를 통해 특정 GPU 아키텍처에 맞춰 최적화된 코드를 생성하도록 설정할 수 있습니다.

CUDA 전용 타겟 속성 활용

CMake는 CUDA 타겟에 대해 전용 속성을 지정할 수 있습니다.

target_compile_options(my_app PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-Xptxas=-v>)

이런 식으로 CUDA 관련 전용 컴파일 옵션을 설정할 수 있습니다. 이를 통해 디버깅, 프로파일링 옵션을 손쉽게 활성화하거나, 특정 최적화 플래그를 붙일 수 있습니다.

Modern C++ 기능을 CUDA 코드에 적용하기

C++20/23에서는 다양한 언어 기능들이 추가되며, 이들은 GPU 코드에도 점진적으로 적용 가능합니다. CUDA는 C++ 표준을 점진적으로 지원해왔으며, 새로운 NVCC나 Clang CUDA 컴파일러와의 조합을 통해 람다 표현식, 콘셉트, constexpr 등을 GPU 코드에도 활용할 수 있습니다.

람다 표현식과 CUDA

람다(lambda) 표현식은 간결한 코드 작성에 유용합니다. 예를 들어, 간단한 Host 코드를 GPU 호출 사이사이에 정리하는데 람다를 써서 가독성을 높일 수 있습니다.

auto init_data = [&](std::vector<float>& v){
    for (auto& x : v) x = static_cast<float>(rand()) / RAND_MAX;
};

std::vector<float> h_a(N), h_b(N), h_c(N);
init_data(h_a);
init_data(h_b);

또한 CUDA 11부터는 GPU 내부에서도 람다 표현식을 지원하는 방향으로 발전하고 있어, 향후 커널 내 람다 활용도 점차 쉬워질 것입니다(다만 모든 C++20 기능이 완전히 지원되는 것은 아니므로 최신 문서 확인 필요).

Concepts(개념) 활용하기

Concept는 템플릿 인자를 제약하는 기능입니다. GPU 코드에서 템플릿 커널을 짤 때, 특정 타입이 GPU에서 사용할 수 있는 타입인지, 혹은 특정 연산을 지원하는지 명확히 제약할 수 있습니다.

template<typename T>
concept GpuArithmetic = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
};

__global__ void vector_add_kernel(const T* a, const T* b, T* c, int N) requires GpuArithmetic<T> {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        c[idx] = a[idx] + b[idx];
    }
}

위 예제는 개념적이며, 실제로는 CUDA 컴파일러 지원 상태를 확인해야 하지만, 이런 식으로 GPU에서 템플릿 타입을 안전하게 활용할 때 Concept가 가독성과 안정성을 높여줄 수 있습니다.

Constexpr 함수와 GPU 코드

CUDA 커널이나 디바이스 함수(__device__ 함수)에도 constexpr 함수 적용이 가능해지면서, GPU 코드 컴파일 시점에 상수로 계산할 수 있는 부분을 미리 처리할 수 있습니다.

__device__ __host__ constexpr float scale_value(float x) {
    return x * 2.0f;
}

이렇게 하면 Host와 Device 양쪽에서 활용 가능한 constexpr 함수를 정의할 수 있고, GPU 커널 호출 시 상수 표현 최적화가 가능해지는 경우도 있습니다.

Before vs After: 전통적 빌드 스크립트 vs 현대적 CMake + Modern C++

Before:

  • Makefile 수작업 및 NVCC 옵션 일일이 지정
  • C++11 수준의 코드 작성(람다/auto 정도만 활용), 제한된 표현력
  • GPU 코드와 Host 코드가 명확히 구분되지 않아 유지보수성 감소

After:

  • CMake를 통한 자동화된 빌드, GPU 아키텍처 별 코드생성 옵션 간편 관리
  • Modern C++ 기능(C++20/23)으로 제약 조건(Concept), 상수 표현(Constexpr), 람다 활용으로 가독성 향상
  • 유지보수성과 확장성이 크게 증가

이러한 변화는 팀 개발, 대규모 프로젝트, 장기 유지보수에 특히 빛을 발합니다.

다음 글 예고

다음 글(#8)에서는 디버깅과 간단한 성능 프로파일링 방법을 다룹니다. 현대적 C++와 CMake를 갖추고, 기본 최적화를 살짝 맛본 지금 시점에서, 실제로 GPU 코드가 예상대로 동작하는지 확인하고 성능 병목 구간을 찾아내는 방법을 배워보겠습니다.

유용한 링크 & 리소스

반응형