왜 이렇게 복잡할까?
CUDA에 익숙한 분이라면 cudaSetDevice() 하나로 GPU를 선택하고, 바로 커널을 실행했던 기억이 있을 겁니다. 하지만 Vulkan은 조금 다릅니다. Vulkan은 다소 “하드코어”한 API라 할 수 있습니다. CUDA가 “GPU는 NVIDIA 것이고, 나머지는 다 내가 알아서 할게”라고 말해주는 친절한 셰프라면, Vulkan은 “냉장고는 저쪽, 칼은 여기, 조리대는 저기 있으니 필요한 걸 직접 꺼내서 써”라고 말하는 요리학원 선생님 같은 느낌이에요. 초반에 해야 할 준비가 많지만, 그만큼 다양한 하드웨어와 상황에 대처할 수 있는 큰 자유를 줍니다.
물리 디바이스(Physical Device) 열람하기
인스턴스를 만든 상태라면, 이제 시스템에 장착된 GPU 목록(물리 디바이스)들을 얻을 수 있습니다. 이런 식으로 물리 디바이스를 나열한 뒤, 우리가 원하는 조건(Compute 기능 지원, 특정 확장 지원, 성능 특성 등)에 맞는 디바이스를 선택할 수 있습니다.
// 인스턴스가 이미 만들어졌다고 가정 (이전 글 참조)
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, NULL);
if (deviceCount == 0) {
fprintf(stderr, "No GPU with Vulkan support found!\n");
return 1;
}
VkPhysicalDevice* physicalDevices = malloc(sizeof(VkPhysicalDevice)*deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, physicalDevices);
// 여기서 physicalDevices 배열을 순회하며 원하는 디바이스 조건을 체크
// 예: Compute 큐가 있는지 확인
CUDA에서는 NVIDIA GPU만 가정하므로 이런 과정이 매우 단순합니다. 하지만 Vulkan은 AMD, Intel, NVIDIA 등 다양한 벤더 GPU를 지원하므로, 이 과정에서 특정 GPU를 선택하는 전략을 세울 수 있습니다.
Compute 큐를 지원하는 디바이스 골라내기
우리는 GPGPU 연산에 초점을 맞추고 있으니, Compute 기능을 지원하는 큐 패밀리가 있는 디바이스를 선택해야 합니다.
VkPhysicalDevice chosenDevice = VK_NULL_HANDLE;
int computeQueueFamilyIndex = -1;
for (uint32_t i = 0; i < deviceCount; i++) {
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevices[i], &queueFamilyCount, NULL);
VkQueueFamilyProperties* queueFamilies = malloc(sizeof(VkQueueFamilyProperties)*queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevices[i], &queueFamilyCount, queueFamilies);
for (uint32_t j = 0; j < queueFamilyCount; j++) {
if (queueFamilies[j].queueFlags & VK_QUEUE_COMPUTE_BIT) {
chosenDevice = physicalDevices[i];
computeQueueFamilyIndex = j;
break;
}
}
free(queueFamilies);
if (chosenDevice != VK_NULL_HANDLE) {
break;
}
}
if (chosenDevice == VK_NULL_HANDLE) {
fprintf(stderr, "No suitable GPU with compute capability found!\n");
return 1;
}
여기서 Compute 큐를 지원하는 디바이스를 하나 찾았다면, chosenDevice에 그 디바이스 핸들을 저장합니다. 나중에 최적화나 확장 단계에서는 여러 디바이스 중 성능이 좋은 것을 고르거나, 특정 확장 지원 여부를 따져 선택할 수도 있습니다.
로지컬 디바이스(Logical Device)와 큐(Queue) 만들기
물리 디바이스가 결정되었다면, 실제로 우리가 사용할 GPU 명령 실행 창구인 “로지컬 디바이스”를 만들어야 합니다. 로지컬 디바이스를 만들 때, 어떤 큐를 사용할지도 함께 명시해야 합니다.
float queuePriority = 1.0f;
VkDeviceQueueCreateInfo queueCreateInfo = {};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = computeQueueFamilyIndex;
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &queuePriority;
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.queueCreateInfoCount = 1;
deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
VkDevice device;
if (vkCreateDevice(chosenDevice, &deviceCreateInfo, NULL, &device) != VK_SUCCESS) {
fprintf(stderr, "Failed to create logical device!\n");
return 1;
}
이렇게 로지컬 디바이스가 생기면, GPU에 명령을 제출할 때 사용할 큐를 얻을 수 있습니다.
VkQueue computeQueue;
vkGetDeviceQueue(device, computeQueueFamilyIndex, 0, &computeQueue);
이제 computeQueue를 통해 GPU에 Compute 명령을 제출할 수 있게 되었습니다. CUDA에서 큐(스트림)는 주로 cudaStreamCreate() 같은 명령으로 만들 수 있지만, Vulkan은 큐 생성 과정이 로지컬 디바이스 생성 과정과 함께 이루어집니다. 초기 설정은 복잡하지만, 이는 나중에 다양한 큐를 조합하거나 특정 우선순위를 조정하는 등의 고급 패턴을 가능하게 합니다.
간단한 샘플 코드 종합하기
아래 예제는 이전 글에서 만든 인스턴스 기반 위에, 물리 디바이스를 선택하고 로지컬 디바이스와 큐를 확보하는 최소한의 예를 보여줍니다. 실제 실행 가능한 코드로 만들려면 에러 처리를 더 꼼꼼하게 하고, free나 vkDestroy* 호출로 리소스를 정리하는 과정도 필요하지만, 여기서는 흐름 이해를 우선합니다.
// 인스턴스 생성 후 코드라고 가정 (instance 존재)
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, NULL);
VkPhysicalDevice* physicalDevices = malloc(sizeof(VkPhysicalDevice)*deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, physicalDevices);
VkPhysicalDevice chosenDevice = VK_NULL_HANDLE;
int computeQueueFamilyIndex = -1;
for (uint32_t i = 0; i < deviceCount; i++) {
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevices[i], &queueFamilyCount, NULL);
VkQueueFamilyProperties* queueFamilies = malloc(sizeof(VkQueueFamilyProperties)*queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevices[i], &queueFamilyCount, queueFamilies);
for (uint32_t j = 0; j < queueFamilyCount; j++) {
if (queueFamilies[j].queueFlags & VK_QUEUE_COMPUTE_BIT) {
chosenDevice = physicalDevices[i];
computeQueueFamilyIndex = j;
break;
}
}
free(queueFamilies);
if (chosenDevice != VK_NULL_HANDLE) {
break;
}
}
if (chosenDevice == VK_NULL_HANDLE) {
fprintf(stderr, "No suitable GPU found!\n");
// 리소스 정리 후 return
}
float queuePriority = 1.0f;
VkDeviceQueueCreateInfo queueCreateInfo = {};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = computeQueueFamilyIndex;
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &queuePriority;
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.queueCreateInfoCount = 1;
deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
VkDevice device;
if (vkCreateDevice(chosenDevice, &deviceCreateInfo, NULL, &device) != VK_SUCCESS) {
fprintf(stderr, "Failed to create device!\n");
// 리소스 정리 후 return
}
VkQueue computeQueue;
vkGetDeviceQueue(device, computeQueueFamilyIndex, 0, &computeQueue);
printf("Device and compute queue successfully created!\n");
// 여기서 vkDestroyDevice, free(physicalDevices), vkDestroyInstance 등 필요한 정리를 해줘야 함
실행 시 “Device and compute queue successfully created!”가 뜬다면, GPU에 접근할 수 있는 기반을 만든 것입니다. 이제 커맨드 버퍼를 만들고(다음 글에서 다룰 예정), 실제 Compute 파이프라인을 구성해 연산을 수행할 수 있습니다.
다음 글 예고
다음 글(#4)에서는 큐와 커맨드 버퍼(Command Buffer)를 본격적으로 파고듭니다. CUDA 스트림과는 또 다른 개념인 커맨드 버퍼를 이해하면, Vulkan이 GPU 명령을 어떻게 관리하고 제출하는지 감을 잡을 수 있을 겁니다.
유용한 링크 & 리소스
- Khronos Vulkan 공식 문서
- Vulkan Tutorial (Device Creation 섹션)
- CUDA 공식 문서 - CUDA에서 디바이스 선택과 스트림 개념 비교
'개발 이야기 > Vulkan' 카테고리의 다른 글
[Vulkan으로 GPGPU 시작하기] #6: Compute 셰이더, 파이프라인 구성, 디스크립터 사용법 (0) | 2024.12.09 |
---|---|
[Vulkan으로 GPGPU 시작하기] #5: 메모리 관리와 버퍼/이미지 객체 기초 (1) | 2024.12.09 |
[Vulkan으로 GPGPU 시작하기] #4: 큐와 커맨드 버퍼로 명령 관리하기 (0) | 2024.12.09 |
[Vulkan으로 GPGPU 시작하기] #2: 환경 설정, Hello Vulkan 예제, 그리고 CMake 세팅 (0) | 2024.12.09 |
[Vulkan으로 GPGPU 시작하기] #1: Vulkan 소개와 시리즈 개요 (0) | 2024.12.09 |