Cmake

Настройка CMake для проектов с CUDA

обновлено 47 просмотров

Введение

CUDA позволяет переносить тяжёлые вычисления на видеокарту NVIDIA: обработку изображений, физические расчёты, машинное обучение. Но собрать проект, где обычный C++ код соседствует с файлами .cu, бывает непросто. Современный CMake решает эту задачу без сторонних скриптов. В статье разберём настройку CMake для проектов с CUDA: минимальный проект, выбор архитектуры GPU, отдельную библиотеку для GPU-кода, флаги компилятора NVCC, отладку, связку с Qt и сборку в CI.

Поддержка языка: CMake для проектов с CUDA

CUDA поддерживается в CMake как полноценный язык программирования, наравне с C и C++. Достаточно указать его в списке языков проекта:

project(gpu LANGUAGES CXX CUDA)

После этого CMake сам найдёт компилятор nvcc, а файлы с расширением .cu будут компилироваться им автоматически. Язык CUDA поддерживается с версии 3.8, но многие удобные возможности появились позже: свойство CUDA_ARCHITECTURES — в 3.18, модуль FindCUDAToolkit — в 3.17, значение native для архитектуры — в 3.24. Поэтому для новых проектов лучше требовать CMake 3.24 или выше.

Старый модуль FindCUDA и функция cuda_add_executable() устарели. Если они встречаются в вашем проекте, стоит перейти на новый подход.

Минимальный CUDA-проект

Пусть у нас есть ядро в файле kernel.cu и обычная функция main() в main.cpp:

// kernel.cu
#include <cuda_runtime.h>

__global__ void addKernel(const float *a, const float *b, float *c, int n)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n)
        c[i] = a[i] + b[i];
}

void addOnGpu(const float *a, const float *b, float *c, int n)
{
    int threads = 256;
    int blocks = (n + threads - 1) / threads;
    addKernel<<<blocks, threads>>>(a, b, c, n);
    cudaDeviceSynchronize();
}

Файл CMakeLists.txt для такого проекта:

cmake_minimum_required(VERSION 3.24)

project(cuda_app LANGUAGES CXX CUDA)

find_package(CUDAToolkit REQUIRED)

add_executable(cuda_app main.cpp kernel.cu)

set_target_properties(cuda_app PROPERTIES
    CUDA_SEPARABLE_COMPILATION ON
)

target_compile_features(cuda_app PUBLIC cxx_std_20 cuda_std_17)
target_link_libraries(cuda_app PRIVATE CUDA::cudart)

Команда find_package(CUDAToolkit) создаёт импортированные цели вроде CUDA::cudart, CUDA::cublas, CUDA::cufft. Свойство CUDA_SEPARABLE_COMPILATION нужно, если device-функции вызываются из разных .cu-файлов. Если весь GPU-код находится в одном файле, его можно не включать: сборка будет немного быстрее.

Compute Capability: выбор архитектуры GPU

Каждое поколение видеокарт NVIDIA имеет свою вычислительную архитектуру (Compute Capability). Например, 75 — это Turing (RTX 20xx), 86 — Ampere (RTX 30xx), 89 — Ada (RTX 40xx). Архитектуры задаются свойством цели:

set_property(TARGET cuda_app PROPERTY CUDA_ARCHITECTURES 75 86)

Или сразу для всего проекта при конфигурации:

cmake -DCMAKE_CUDA_ARCHITECTURES=86 ..

Удобные специальные значения: native — собрать под видеокарту текущего компьютера, all-major — под все основные архитектуры. Чем больше архитектур указано, тем дольше сборка и больше размер бинарника.

Статическая CUDA-библиотека

В реальных проектах GPU-код лучше выделить в отдельную библиотеку. Остальная часть программы будет собираться обычным компилятором C++ и не будет зависеть от nvcc напрямую:

add_library(cuda_core STATIC kernel.cu)

set_target_properties(cuda_core PROPERTIES
    CUDA_SEPARABLE_COMPILATION ON
    CUDA_RESOLVE_DEVICE_SYMBOLS ON
)

target_link_libraries(cuda_core PUBLIC CUDA::cudart)

Свойство CUDA_RESOLVE_DEVICE_SYMBOLS выполняет компоновку device-кода внутри самой библиотеки. Это важно, когда статическая библиотека подключается к цели, в которой нет .cu-файлов.

CUDA + Qt

Типичная архитектура приложения с графическим интерфейсом на Qt и вычислениями на GPU выглядит так:

gui → gpu_backend → cuda_core

Интерфейс ничего не знает о CUDA. Он общается с промежуточным слоем gpu_backend, который принимает данные в типах Qt и вызывает функции из cuda_core:

add_library(gpu_backend STATIC gpu.cpp)
target_link_libraries(gpu_backend PUBLIC cuda_core Qt6::Core)

target_link_libraries(app PRIVATE gpu_backend)

Не включайте заголовки Qt в .cu-файлы. Компилятор nvcc плохо справляется со сложными шаблонами и макросами, а moc не обрабатывает такие файлы. Передавайте в GPU-код простые типы и указатели на данные.

Флаги NVCC

Флаги, которые нужны только компилятору CUDA, задаются через генераторное выражение:

target_compile_options(cuda_core PRIVATE
    $<$<COMPILE_LANGUAGE:CUDA>:--use_fast_math>
)

Флаг --use_fast_math ускоряет математические функции за счёт небольшой потери точности. Генераторное выражение гарантирует, что флаг не попадёт к обычному компилятору C++.

Отладка CUDA

Для отладки device-кода в cuda-gdb или Nsight нужен флаг -G. Для профилирования в релизной сборке достаточно -lineinfo:

target_compile_options(cuda_core PRIVATE
    $<$<AND:$<COMPILE_LANGUAGE:CUDA>,$<CONFIG:Debug>>:-G>
    $<$<AND:$<COMPILE_LANGUAGE:CUDA>,$<CONFIG:Release>>:-lineinfo>
)

Флаг -G отключает оптимизации device-кода, поэтому программа работает заметно медленнее. Используйте его только в отладочной сборке.

CUDA + Tests

enable_testing()

add_executable(cuda_tests test_gpu.cpp)
target_link_libraries(cuda_tests PRIVATE cuda_core)

add_test(NAME gpu COMMAND cuda_tests)

Тесты GPU-кода запускаются только на машине с видеокартой NVIDIA. Если её нет, тест лучше пропускать, а не считать проваленным. Проверить наличие устройства можно через cudaGetDeviceCount().

Кроссплатформенный GPU-бэкенд

Если приложение должно работать и на компьютерах без NVIDIA, выделите интерфейс бэкенда:

class IGpuBackend {
public:
    virtual ~IGpuBackend() = default;
    virtual void process() = 0;
};

Реализацию на CUDA можно оформить как плагин и загружать через QPluginLoader. Если плагин не загрузился, используется реализация на CPU. Сделать CUDA необязательной на этапе сборки поможет модуль CheckLanguage:

include(CheckLanguage)
check_language(CUDA)
if(CMAKE_CUDA_COMPILER)
    enable_language(CUDA)
    add_subdirectory(cuda_backend)
endif()

CI для CUDA

В github-actions-%d0%bf%d0%b5%d1%80%d0%b2%d1%8b%d0%b9-workflow-%d0%b4%d0%bb%d1%8f-%d1%81%d0%b1%d0%be%d1%80%d0%ba%d0%b8-qt-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b0/" class="qth-seo-autolink">GitHub Actions можно собирать CUDA-проект, установив CUDA Toolkit на runner:

- run: cmake -S . -B build -DCMAKE_CUDA_ARCHITECTURES=86
- run: cmake --build build

Учтите, что стандартные runner-машины не имеют видеокарт. Собрать проект на них можно, а запустить GPU-тесты — нет. Для тестов нужен собственный runner с видеокартой NVIDIA. Архитектуру в CI задавайте явно: значение native там не сработает.

Заключение

Настройка CMake для проектов с CUDA сводится к нескольким шагам: добавить CUDA в языки проекта, подключить CUDAToolkit, указать архитектуры GPU и вынести device-код в отдельную библиотеку. Такая структура позволяет спокойно сочетать CUDA с Qt, писать тесты и собирать проект в CI.