[SYCL 입문 #2] NVIDIA와 Qualcomm GPU에서 SYCL 코드 실행하기

SYCL 입문 시리즈의 두 번째 글입니다. 지난 글(#1)에서는 SYCL 개념 소개, Conda 및 Windows 환경 설정, CMake 빌드, 그리고 “Hello SYCL!” 예제를 CPU나 가능한 디바이스에서 실행하는 방법을 살펴봤습니다. 이제 한 단계 더 나아가, NVIDIA GPUQualcomm GPU에서 SYCL 코드를 실행하는 전략을 다루겠습니다. 이는 SYCL의 큰 장점 중 하나인 이식성을 체험하기 위한 핵심입니다.

이번 글에서는 초보자도 따라 할 수 있도록 구현체(백엔드) 선택, 디바이스 선택 로직, CMake 빌드 옵션 등을 구체적으로 제시하겠습니다. 앞서 구축한 Conda 환경이나 Windows 환경에서도 적용 가능한 형태로 설명하니, OS나 개발자 환경에 맞게 응용해보시기 바랍니다.

목표

  • NVIDIA GPU 지원: CUDA 백엔드를 통한 SYCL 코드 실행 전략
  • Qualcomm GPU 지원: OpenCL 백엔드(또는 Level Zero 백엔드) 활용 가능 여부 확인
  • device_selector를 활용해 특정 벤더 디바이스를 우선 선택하는 방법
  • CMake 빌드 시 백엔드를 전환하는 옵션 예제
  • 이전 글의 “Hello SYCL!” 예제를 백엔드만 바꿔서 NVIDIA나 Qualcomm GPU에서 실행하는 방법

사전 준비 및 구현체 선택

앞서 설치했던 SYCL 구현체 중 하나를 사용합니다.

  1. oneAPI DPC++: Intel에서 만든 SYCL 구현체. Intel GPU나 CPU 타겟은 잘 지원하나, NVIDIA GPU 지원은 Experimental일 수 있습니다. Qualcomm GPU 지원은 OpenCL 백엔드가 제공된다면 가능할 수도 있습니다.
  2. hipSYCL: LLVM/Clang 기반 SYCL 구현으로, CUDA 백엔드를 잘 지원합니다(즉, NVIDIA GPU에 적합). OpenCL 백엔드도 지원하므로, Qualcomm GPU가 OpenCL 플랫폼으로 노출된다면 접근할 수 있습니다.

초보자는 일단 hipSYCL를 통한 CUDA 백엔드로 NVIDIA GPU 실행부터 시도하는 것이 비교적 쉬울 수 있습니다. Qualcomm GPU는 좀 더 특수한 환경일 수 있으므로 OpenCL 런타임 및 드라이버가 필요합니다.

NVIDIA GPU 지원 (hipSYCL 예시)

hipSYCL 사용 시 CUDA 백엔드를 활성화하려면 컴파일 시 다음과 같은 옵션을 줄 수 있습니다.

syclcc -O2 -fsycl-targets=nvptx64-nvidia-cuda -o hello_nvidia hello_sycl.cpp

CMake를 사용할 경우 CMakeLists.txt에 다음과 같이 백엔드를 지정하는 변수를 둘 수 있습니다.

if(BACKEND STREQUAL "cuda")
  set(BACKEND_FLAGS "-fsycl-targets=nvptx64-nvidia-cuda")
elseif(BACKEND STREQUAL "opencl")
  set(BACKEND_FLAGS "-fsycl-targets=spir64")
endif()

이렇게 하면 cmake -D BACKEND=cuda .. 명령을 통해 NVIDIA GPU용 바이너리를 빌드할 수 있습니다.

Qualcomm GPU 지원 (OpenCL 백엔드)

Qualcomm GPU를 SYCL로 활용하려면 OpenCL 런타임 및 드라이버가 설치되어야 합니다. Qualcomm 장치(예: 임베디드 디바이스나 특정 개발 보드)에서 OpenCL이 동작한다면, hipSYCL나 DPC++의 OpenCL 백엔드를 통해 접근 가능합니다.

cmake -D BACKEND=opencl ..
make
./hello_sycl

이렇게 하면 OpenCL 백엔드로 빌드한 SYCL 코드가 OpenCL 디바이스를 탐색하고, 그 중 Qualcomm GPU 디바이스를 찾을 수 있다면 선택해서 실행할 수 있습니다.

device_selector 커스터마이징

SYCL에서 특정 벤더 디바이스를 우선 선택하려면 device_selector를 오버라이드할 수 있습니다.

struct nvidia_device_selector : public sycl::device_selector {
    int operator()(const sycl::device &dev) const override {
        auto vendor = dev.get_info<sycl::info::device::vendor>();
        // NVIDIA GPU는 보통 "NVIDIA" 문자열 포함
        if (vendor.find("NVIDIA") != std::string::npos) {
            return 1;
        }
        return -1;
    }
};

struct qualcomm_device_selector : public sycl::device_selector {
    int operator()(const sycl::device &dev) const override {
        auto vendor = dev.get_info<sycl::info::device::vendor>();
        // Qualcomm GPU는 "Qualcomm" 또는 "Adreno"등의 문자열일 수 있음
        // 실제 vendor 문자열을 확인한 뒤 수정 필요
        if (vendor.find("Qualcomm") != std::string::npos || vendor.find("Adreno") != std::string::npos) {
            return 1;
        }
        return -1;
    }
};

이렇게 만든 셀렉터를 사용하면:

// NVIDIA GPU용
sycl::queue q(nvidia_device_selector());

// Qualcomm GPU용
sycl::queue q(qualcomm_device_selector());

해당 벤더 디바이스를 우선적으로 선택하려고 시도합니다(없다면 다른 디바이스로 fallback할 수 있음).

예제 코드 재활용

이전 글에서 사용한 “Hello SYCL!” 예제를 약간 수정해서 NVIDIA GPU나 Qualcomm GPU를 노리도록 합니다. main.cpp를 다음과 같이 바꿔봅시다.

#include <CL/sycl.hpp>
#include <iostream>
#include <vector>
#include <string>

// 간단히 NVIDIA 또는 Qualcomm 셀렉터 중 하나를 사용
struct nvidia_device_selector : public sycl::device_selector {
    int operator()(const sycl::device &dev) const override {
        auto vendor = dev.get_info<sycl::info::device::vendor>();
        if (vendor.find("NVIDIA") != std::string::npos) return 1;
        return -1;
    }
};

struct qualcomm_device_selector : public sycl::device_selector {
    int operator()(const sycl::device &dev) const override {
        auto vendor = dev.get_info<sycl::info::device::vendor>();
        // 실제 벤더명 확인 필요
        if (vendor.find("Qualcomm") != std::string::npos || vendor.find("Adreno") != std::string::npos)
            return 1;
        return -1;
    }
};

int main() {
    try {
        // 여기서 원하는 셀렉터 선택: 예) NVIDIA GPU
        sycl::queue q(nvidia_device_selector()); 

        std::cout << "Running on: " << q.get_device().get_info<sycl::info::device::name>() << "\n";

        size_t N = 1024;
        std::vector<float> data(N, 1.0f);

        {
            sycl::buffer<float,1> buf(data.data(), sycl::range<1>(N));
            q.submit([&](sycl::handler& cgh) {
                auto acc = buf.get_access<sycl::access::mode::read_write>(cgh);
                cgh.parallel_for<class simple_kernel>(sycl::range<1>(N), [=](sycl::id<1> i) {
                    acc[i] *= 2.0f;
                });
            });
        }

        std::cout << "data[0] = " << data[0] << ", data[100] = " << data[100] << "\n";
        std::cout << "All elements should now be 2.0\n";
    } catch (sycl::exception const &e) {
        std::cerr << "SYCL Exception: " << e.what() << "\n";
        return 1;
    }

    return 0;
}

CMake 빌드 시:

mkdir build
cd build
cmake -D SYCL_COMPILER=syclcc -D BACKEND=cuda ..
make
./hello_sycl

위와 같이 하면 CUDA 백엔드를 사용하므로 NVIDIA GPU가 인식되어 있다면 해당 디바이스에서 코드가 실행될 수 있습니다. Qualcomm GPU에서는 OpenCL 백엔드로 빌드한 뒤 qualcomm_device_selector를 사용하면 됩니다.

주의 사항

  • Qualcomm GPU 지원은 시나리오에 따라 차이가 큽니다. OpenCL 기반 런타임이 제대로 설치되어 있어야 하고, SYCL 구현체가 해당 OpenCL 디바이스를 인식해야 합니다. 임베디드 환경이나 특정 개발 보드에서는 별도의 드라이버 설정이 필요할 수 있습니다.
  • NVIDIA GPU에서 잘 동작하는 코드를 Qualcomm GPU에서도 동일하게 돌리려면, 실제로 해당 디바이스에서 OpenCL 디바이스 벤더 확인, 커널 실행 등의 테스트를 해야 합니다.
  • 디바이스 셀렉터는 단지 우선순위를 부여할 뿐, 해당 벤더 디바이스가 없으면 CPU나 다른 디바이스로 fallback할 수 있습니다. 출력 메시지를 통해 어떤 디바이스가 선택됐는지 확인하세요.

정리 및 다음 글 예고

이번 글에서는 SYCL 코드가 단지 CPU나 Intel GPU뿐 아니라, NVIDIA 및 Qualcomm GPU에서도 실행 가능하다는 점을 확인했습니다. CUDA 백엔드를 이용한 NVIDIA GPU 지원, OpenCL 백엔드를 통한 Qualcomm GPU 접근 전략, 그리고 device_selector 커스터마이징으로 특정 벤더 디바이스를 우선 선택하는 방법을 소개했습니다.

다음 글(#3)에서는 SYCL 메모리 모델과 커널 작성 패턴을 좀 더 자세히 살펴볼 예정입니다. 메모리 관리(버퍼, 액세서), C++ 람다를 이용한 커널 작성법, 워크아이템/워크그룹 개념 등을 익혀, 더 복잡한 연산을 구현하는 기반을 마련해보겠습니다.

반응형