안녕하세요? ENERZAi에서 runtime을 개발하고 있는 신진환이라고 합니다. 이전 게시물들에서 소개 드렸듯이 저희가 자체적으로 개발한 프로그래밍 언어 Nadya는 현재 CPU만을 지원하고 있으며, GPU까지 지원 범위를 확장하기 위한 연구 개발을 진행 중입니다. 코드를 컴파일 하면 바로 실행할 수 있는 CPU와는 달리 GPU는 코드 실행을 지원하는 별도의 Library가 필요한데요. 많이 들어보셨을 DirectX, Metal, Vulkan 등이 이러한 Library에 해당합니다.
위에서 언급된 DirectX, Vulkan 등의 Graphics Library 외에도 Computing을 위한 CUDA, OpenCL 같은 GPGPU Library도 존재하는데요. GPU를 일반적인 연산에도 많이 활용하게 되면서 Graphics Library들도 DirectX Compute Shader(구 DirectCompute), Metal Performance Shader, Vulkan Compute Shader 등의 기능을 추가하여 GPGPU를 지원하고 있습니다.
그 중 이번 게시물에서는 Graphics를 위한 공개 표준인 Vulkan을 이용해 연산을 수행하는 Vulkan Compute Shader에 대해 알아보고자 합니다. 이번 글은 먼저 Vulkan을 사용하기 위해 필요한 개념들에 대해 설명을 드리고 그 다음 간략한 사용 방법을 코드로 보여드릴 것입니다. 많은 분들께 도움이 되길 기대합니다.
Vulkan Compute Shader
Vulkan runtime은 위와 같은 flow를 통해 컴파일된 코드가 GPU에서 실행될 수 있도록 지원하는데요. 세부적인 기능들을 아래에 중요 키워드별로 정리하였으니 참고 부탁 드립니다.
Instance
이제 Vulkan은 OpenGL과 같이 global state 대신 application별 state 저장을 지원합니다. 그에 따라 application별 state를 저장하는 객체가 vkInstance
객체입니다.
이 객체와 vkCreateInstance
함수를 통해 하나의 application에서 여러 instance를 생성할 수 있다고알려져 있지만, 실제로는 제대로 지원하지 않는 구현체도 많은 것으로 확인되었습니다.
Physical Device & Logical Device
Vulkan에서 device는 physical device(vkPhysicalDevice
)와 logical device(vkDevice
)으로 구분되는데요.
Physical device는 하나의 Vulkan 구현체(하나의 GPU라고 보셔도 무방하며, OpenCL의 Platform과 비슷한 개념입니다)를 나타내고, Logical device는 physical device를 instantiate 한 객체로 각각 고유한 자원과 상태를 가지고 있습니다. vkEnumeratePhysicalDevice
함수로 physical device를 순회할 수 있고, vkGetPhysicalDeviceProperties
함수를 통해 physical device의 정보를 가져 올 수 있습니다.
Queue는 명령을 처리하는 창구입니다. 이 queue는 여러 개가 존재할 수 있으며, 비슷한 역할을 수행하는 queue끼리 queue family로 묶이게 됩니다. 하나의 queue family가 여러 종류의 명령을 처리할 수도 있고, 여러 개의 queue families가 한 종류의 명령을 처리할 수도 있는데요. 목표 명령의 종류는 다음과 같습니다.
Video Decode
Video Encode
Graphics
Compute
Transfer
Memory management
Physical device의 queue families 정보는 vkGetPhysicalDeviceQueueFamilyProperties
을 통해 가져올 수 있습니다.
위의 함수들을 통해 적절한 physical device를 찾으면 그 physical device로 logical device를 생성할 수 있으며, 이 작업은 vkCreateDevice
함수를 통해 수행됩니다.
Buffer & Memory
Buffer
Vulkan에서는 buffer와 memory를 별도로 관리합니다. Buffer는 memory의 view일 뿐이며, 실제 메모리는 따로 관리되는데요. 따라서 Vulkan에서는 buffer와 memory를 별도로 할당해주고 buffer에 memory를 binding해주는 과정이 필요합니다.
vkBuffer
객체는 vkCreateBuffer
함수로 생성할 수 있습니다.
vkBindBufferMemory
로 buffer 객체에 memory를 binding할 수 있습니다.
Shader에서 사용할 수 있는 buffer에는 다양한 종류가 있지만, compute shader에서는 그 중 아래와 같은 3가지 buffer가 가장 많이 사용됩니다.
Storage Buffer
대량의 데이터를 읽고 쓸 수 있는 buffer입니다. Tensor나 weight, bias와 같은 정보를 저장하기에 적합합니다.
2. Uniform Buffer
소량의 데이터를 읽기에 적합한 buffer입니다. Kernel에 인자를 전달할 때 많이 사용합니다.
3. Push Constant Buffer
Uniform Buffer와 동일하지만 사용법이 약간 다릅니다.
Memory
OpenCL이나 OpenGL과는 다르게, Vulkan에서는 device memory할당도 application에서 처리해야 합니다. OpenCL과 OpenGL의 경우 원하는 메모리의 크기와 접근 방법만 지정하면 자동으로 할당되지만, Vulkan에서는 device의 memory heap을 enumerate하고, 사용 가능한 memory area를 찾은 뒤, 그 area에 할당하는 복잡한 작업을 수행해야 합니다.
Device memory는 device 및 host에서 접근 가능한지 여부에 따라 아래와 같이 구분됩니다.
Device-local
2. Device-local, Host-visible
디바이스 메모리 이지만 호스트에서도 접근 가능한 메모리입니다.
3. Host-local, Host-visible
Host 메모리 이지만 Device에서도 접근 가능한 메모리 입니다.
Device에 따라 memory들이 분리되어 있을 수도 있지만, 하나의 memory가 모든 역할을 수행할 수도 있는데요. 생성하는 buffer의 목적에 따라 적절한 area에 메모리를 할당한다면 더 높은 성능을 달성할 수 있습니다.
vkGetPhysicalDeviceMemoryProperties
함수를 통해 Physical device에 대한 memory 정보를 가져올 수 있으며, 이 함수를 통해 얻은 memory type index를 이용해 vkAllocateMemory
함수로 device memory를 할당할 수 있습니다.
또한, Vulkan에서는 최대 memory allocation 횟수에 제한이 있으므로, VkPhysicalDeviceLimits::maxMemoryAllocationCount
횟수 이하로만 메모리를 할당할 수 있는데요. 필요하다면 하나의 bulk memory를 할당한 다음 buffer를 생성할 때 memory의 일부분을 buffer로 binding하는 등의 요구가 존재할 수도 있습니다.
Pipeline
Pipeline은 장치에서 일련의 과정을 수행하는 과정을 나열한 것 입니다. 작업 지시서와 유사한 개념으로, Pipeline에는 어떠한 resource을 사용할 것인지, 어떤 stage가 존재하고 그 stage에서는 어떤 shader가 사용되는지 등 다양한 정보가 기술됩니다.
Compute pipeline은 graphics pipeline과 다르게 여러 과정이 생략됩니다. Graphics pipeline에서는 vertex shader, geometry shader, pixel shader, ray tracing 등 많은 stage가 등장하지만, compute pipeline에서는 입력 buffer의 값으로 연산을 수행한 뒤 출력 buffer에 쓰기만 하면 되므로 compute shader 하나의 stage만 존재하게 됩니다.
따라서, compute shader만 결정되면 vkCreateComputePipelines
함수를 통해 compute pipeline을 만들 수 있습니다.
Descriptor
Descriptor는 shader에서 사용하는 resource를 나타내는 객체입니다. Descriptor를 통해 shader에서 사용하는 resource를 binding 할 수 있습니다.
Descriptor는 아래와 같이 Descriptor Pool, Descriptor Set Layout, Descriptor Set으로 구분됩니다.
Descriptor Pool
Descriptor Set을 할당하기 위한 pool입니다. Memory allocator 역할을 수행합니다.
2. Descriptor Set Layout
Descriptor Set이 어떠한 구조로 되어있는지 설명합니다. 같은 layout를 사용하는 여러 shader가 있다면 공유 가능합니다.
3. Descriptor Set
실제 정보를 담고 있는 객체입니다. Set 내부의 buffer가 어떠한 실제 buffer와 mapping되는 지, offset은 얼마인지 등의 정보를 담고 있습니다.
또한, Descriptor Set을 생성할 때 사용 가능한 resource에 제한이 있는데요. 관련 정보는 VkPhysicalDeviceLimits
구조체를 통해 획득할 수 있습니다.
VkPhysicalDeviceLimits::maxPushConstantSize
Push Constant Buffer의 최대 크기를 나타냅니다.
2. VkPhysicalDeviceLimits::maxPerStageDescriptorUniformBuffers
하나의 stage에서 최대로 사용 가능한 uniform buffer의 수를 나타냅니다.
3. VkPhysicalDeviceLimits::maxPerStageDescriptorStorageBuffers
하나의 stage에서 최대로 사용 가능한 storage buffer의 수를 나타냅니다.
4. VkPhysicalDeviceLimits::maxPerStageResources
하나의 stage에서 최대로 사용 가능한 모든 resource의 수를 나타냅니다.
5. VkPhysicalDeviceLimits::maxDescriptorSetUniformBuffers
하나의 descriptor set에서 최대로 사용 가능한 uniform buffer의 수를 나타냅니다.
6. VkPhysicalDeviceLimits::maxDescriptorSetStorageBuffers
하나의 descriptor set에서 최대로 사용 가능한 storage buffer의 수를 나타냅니다.
Command Pool & Command Buffer
Command Buffer는 GPU가 수행할 명령을 기록한 buffer로, 해당 buffer에 실행할 명령을 작성한 뒤 queue에 한번에 입력하게 됩니다. Command Pool은 command buffer를 memory에 할당하기 위한 memory allocator인데요. Command buffer에 명령을 한 번만 기록한 뒤(Command pool을 통해 할당된 memory에 기록), 기록한 command를 계속 submit 하여 효율적으로 명령을 실행할 수 있습니다.
Compute shader에서 command Buffer는 아래와 같은 vkCmd~
계열의 함수를 사용하여 vkBeginCommandBuffer
와 vkEndCommandBuffer
사이에 명령을 기록하는데요.
vkCmdBindDescriptorSets
: 사용할 descriptor set을 binding합니다. 이 이후부터 binding된 buffer를 사용합니다.
vkCmdDispatch
: compute shader를 실행합니다.
vkCmdCopyBuffer
: buffer를 복사합니다.
vkCmdPipelineBarrier
: pipeline간 barrier를 삽입합니다. Memory barrier도 이 함수를 통해 삽입할 수 있습니다.
vkCmdPushConstants
: Push Constant Buffer에 값을 write합니다.
vkCmdSetEvent
: event를 raise합니다.
vkCmdResetEvent
: event를 reset합니다.
vkCmdWaitEvents
: event가 발생할때까지 대기합니다.
만약 command buffer의 명령을 지우고 재활용하고자 한다면 vkBeginCommandBuffer
이전에 vkResetCommandBuffer
함수로 초기화하여 다시 사용할 수도 있습니다.
Compute shader를 실행할 때의 제약 사항은 아래와 같습니다.
VkPhysicalDeviceLimits::maxComputeSharedMemorySize
최대로 사용 가능한 shared memory (OpenCL 에서는 local memory)의 크기를 나타냅니다.
2. VkPhysicalDeviceLimits::maxComputeWorkGroupCount
최대 global workgroup size를 나타냅니다.
3. VkPhysicalDeviceLimits::maxComputeWorkGroupSize
최대 local workgroup size를 나타냅니다.
4. VkPhysicalDeviceLimits::maxComputeWorkGroupInvocations
한 local workgroup에서 호출할 수 있는 최대 크기를 나타냅니다. local workgroup size의 모든 차원 값의 곱이 이 값을 초과하지 않아야 합니다.
Fences, Semaphores, Events and Barriers
Fence
Fence는 host와 command queue 사이의 동기화를 목적으로 하는 객체입니다. 이 fence를 이용해 enqueue한 command가 종료될 때 까지 기다리는 등의 작업을 수행할 수 있습니다. Command buffer에 기록된 명령을 vkQueueSubmit
함수를 통해 queue에 제출할 때 fence를 함께 제출하면, 명령이 끝날 때까지 wait할 수 있습니다.
Fence는 vkCreateFence
를 통해 생성되며, vkWaitForFences
를 통해 하나 이상의 fence가 신호 상태(signaled)가 될 때까지 대기할 것을 지시할 수 있습니다.
Semaphore
Semaphore는 command queue 간의 의존성을 삽입할 때 사용하는 동기화 객체인데요. Semaphore를 통해 특정 queue가 종료될 때 까지 기다리거나, 특정 queue가 종료된 후 queue의 명령을 실행하는 등의 작업을 수행할 수 있습니다.
Semaphore는 vkCreateSemaphore
를 통해 생성되며, vkQueueSubmit
을 통해 command를 submit할 때 의존성을 설정하는데 사용됩니다.
Event
Event는 command 간의 의존성을 삽입할 때 사용하는 동기화 객체 입니다. 하나의 queue 안에 있는 command 사이에서만 사용 가능합니다. 이 event를 통해 특정 command가 종료될 때 까지 기다리거나, 특정 command가 실행된 후 command를 실행하는 등의 작업을 수행할 수 있습니다.
Event는 vkCreateEvent
를 통해 생성되며, 다른 동기화 객체와는 달리 device 내 동기화 뿐만 아니라 device ↔ host 간 동기화도 지원합니다. Host에서는 vkSetEvent
, vkResetEvent
를 통해 signal할 수 있으며, device에서는 command buffer에 명령을 기록할 때 vkCmdSetEvent
, vkCmdResetEvent
를 통해 signal할 수 있습니다.
Barrier
Barrier는 command 간의 의존성을 삽입할 때 사용하는 동기화 객체입니다. Event와 비슷한 역할을 수행하지만 device에서만 사용 가능하고, queue 사이에서도 동작한다는 차이점이 있습니다. 뿐만 아니라, memory 동기화 목적으로도 사용되며, vkCmdPipelineBarrier
를 통해 command buffer에 명령을 기록합니다.
위에서 Vulkan으로 GPU를 실행하는데 필요한 개념을 알아보았습니다. 이제 실제 코드를 작성하여 실행해보고 싶은 분들을 위해 위 내용들을 코드를 통해 한 번 더 설명 드립니다. 아래 순서대로 실행하시면 됩니다.
Execute Kernel with Vulkan
Template compute shader code
layout(set=0, binding=0) readonly buffer Input {
float input_tensor[];
} input;
layout(set=0, binding=1) writeonly buffer Output {
float output_tensor[];
} output;
layout(set=0, binding=2) uniform UniformArgs {
} uniform_args;
layout(push_constant) uniform PushArgs {
} push_args;
void main() {
}
Instance를 생성합니다.
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLIATION_INFO;
appInfo.pApplicationName = "Hello World App";
appInfo.applicationVersion = VK_MAKE_VERSION(0, 0, 1);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(0, 0, 1);
appInfo.apiVersion = VK_API_VERSION_1_2;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
const char* layers[1] = {
"VK_LAYERS_KHRONOS_validation"
};
VkValidationFeatureEnableEXT enable_features[] = {
VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT,
VK_VALIDATION_FEATURE_ENABLE_BEST_PRACTICE_EXT
};
VkValidationFeaturesEXT validation_features{};
validation_features.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT;
validation_features.enabledValidationFeaturesCount = 2;
validation_features.pEnabledValidationFeatures = enable_features;
createInfo.enabledLayerCount = 1;
createInfo.ppEnabledLayerNames = layers;
createInfo.pNext = &validation_features;
VkDebugReportCallbackCreateInfo debug_callback_create_info{};
debug_callback_create_info.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT;
debug_callback_create_info.flags = VK_DEBUG_REPORT_INFORMATION_BIT_EXT|VK_DEBUG_REPORT_WARNING_BIT_EXT|VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT|VK_DEBUG_REPORT_ERROR_BIT_EXT|VK_DEBUG_REPORT_DEBUG_BIT_EXT;
debug_callback_create_info.pfnCallback = DebugCallback;
VkDebugReportCallbackEXT debug_callback;
vkCreateDebugReportCallbackEXT(instance, &debug_callback_create_info, nullptr, &debug_callback);
VkInstance instance;
vkCreateInstance(&createInfo, nulptr, &instance);
적절한 physical device를 검색합니다.
std::vector<VkPhysicalDevice> physical_devices;
uint32_t device_count = 0;
vkEnumeratePhysicalDevices(instance, &device_count, nullptr);
physical_devices.resize(device_count);
vkEnumeratePhysicalDevices(instance, &device_count, physical_devices.data());
VkPhysicalDevice selected_physical_device = VK_NULL_HANDLE;
for (auto physical_device : physical_devices) {
VkPhysicalDeviceProperties device_properties;
VkPhysicalDeviceFeatures device_features;
VkPhysicalDeviceMemoryProperties device_memory_properties;
std::vector<VkQueueFamilyProperties> queue_family_properties;
uint32_t queue_family_property_count;
vkGetPhysicalDeviceProperties(physical_device, &device_properties);
vkGetPhysicalDeviceFeatures(physical_device, &device_features);
vkGetPhysicalDeviceMemoryProperties(physical_device, &device_memory_properties);
vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_property_count, nullptr);
queue_family_properties.resize(queue_family_property_count);
vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_property_count, queue_family_properties.data());
적절한 physical device를 찾았으면, logical device를 생성합니다.
std::optional<uint32_t> queue_family_index;
std::vector<VkQueueFamilyProperties> queue_family_properties;
uint32_t queue_family_property_count;
vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_property_count, nullptr);
queue_family_properties.resize(queue_family_property_count);
vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_property_count, queue_family_properties.data());
for (auto i = 0; i < queue_family_property_count; ++i) {
auto& properties = queue_family_properties[i];
if (properties.queueFlags & VK_QUEUE_COMPUTE_BIT) {
queue_family_index = i;
break;
}
}
assert(queue_family_index.has_value());
float queuePriority = 1.0f;
VkDeviceQueueCreateInfo queueCreateInfo{};
queueCreateInfo.sType = VK_STRUCTURE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = queue_family_index.value();
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &queuePriority;
VkDeviceCreateInfo deviceCreateInfo{};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
deviceCreateInfo.queueCreateInfoCount = 1;
deviceCreateInfo.enabledExtensionCount = 0;
deviceCreateInfo.enabledLayerCount = 0;
VkDevice device;
vkCreateDevice(physical_device, &deviceCreateInfo, nullptr, &device);
Queue를 생성합니다.
VkQueue queue;
vkGetDeviceQueue(device, queue_family_index.value(), 0, &queue);
입출력용 buffer를 생성합니다.
VkBufferCreateInfo input_buffer_create_info{};
VkBufferCreateInfo output_buffer_create_info{};
VkBufferCreateInfo uniform_buffer_create_info{};
input_buffer_create_info.sType = VK_SCRUCTURE_TYPE_BUFFER_CREATE_INFO;
input_buffer_create_info.size = INPUT_SIZE;
input_buffer_create_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
input_buffer_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
output_buffer_create_info.sType = VK_SCRUCTURE_TYPE_BUFFER_CREATE_INFO;
output_buffer_create_info.size = OUTPUT_SIZE;
output_buffer_create_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
output_buffer_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
uniform_buffer_create_info.sType = VK_SCRUCTURE_TYPE_BUFFER_CREATE_INFO;
uniform_buffer_create_info.size = sizeof(UniformArgs);
uniform_buffer_create_info.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
uniform_buffer_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VkBuffer input_buffer;
VkBuffer output_buffer;
VkBuffer uniform_buffer;
vkCreateBuffer(device, &input_buffer_create_info, nullptr, &input_buffer);
vkCreateBuffer(device, &output_buffer_create_info, nullptr, &output_buffer);
vkCreateBuffer(device, &uniform_buffer_create_info, nullptr, &uniform_buffer);
Buffer를 위한 device memory를 할당합니다.
VkPhysicalDeviceMemoryProperties device_memory_properties;
vkGetPhysicalDeviceMemoryProperties(physical_device, &device_memory_properties);
constexpr VkMemoryPropertyFlagBits required_properties =
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
VkMemoryRequirements input_buffer_requirements;
VkMemoryRequirements output_buffer_requirements;
VkMemoryRequirements uniform_buffer_requirements;
vkGetBufferMemoryRequirements(device, input_buffer, &input_buffer_requirements);
vkGetBufferMemoryRequirements(device, output_buffer, &output_buffer_requirements);
vkGetBufferMemoryRequirements(device, uniform_buffer, &uniform_buffer_requirements);
auto input_memory_type_index = FindOptimalHeap(input_buffer_requirements.memoryTypeBits, required_properties);
auto output_memory_type_index = FindOptimalHeap(output_buffer_requirements.memoryTypeBits, required_properties);
auto uniform_memory_type_index = FindOptimalHeap(uniform_buffer_requirements.memoryTypeBits, required_properties);
assert(input_memory_type_index.has_value());
assert(output_memory_type_index.has_value());
assert(uniform_memory_type_index.has_value());
VkDeviceMemory input_buffer_memory;
VkDeviceMemory output_buffer_memory;
VkDeviceMemory uniform_buffer_memory;
VkMemoryAllocateInfo input_memory_allocate_info{};
VkMemoryAllocateInfo output_memory_allocate_info{};
VkMemoryAllocateInfo uniform_memory_allocate_info{};
input_memory_allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
input_memory_allocate_info.allocationSize = input_buffer_requirements.size;
input_memory_allocate_info.memoryTypeIndex = input_memory_type_index.value();
output_memory_allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
output_memory_allocate_info.allocationSize = output_buffer_requirements.size;
output_memory_allocate_info.memoryTypeIndex = output_memory_type_index.value();
uniform_memory_allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
uniform_memory_allocate_info.allocationSize = uniform_buffer_requirements.size;
uniform_memory_allocate_info.memoryTypeIndex = uniform_memory_type_index.value();
vkAllocateMemory(device, &input_memory_allocate_info, &input_buffer_memory);
vkAllocateMemory(device, &output_memory_allocate_info, &output_buffer_memory);
vkAllocateMemory(device, &uniform_memory_allocate_info, &uniform_buffer_memory);
Buffer에 device memory를 binding합니다.
vkBindBufferMemory(device, input_buffer, input_buffer_memory, 0);
vkBindBufferMemory(device, output_buffer, output_buffer_memory, 0);
vkBindBufferMemory(device, uniform_buffer, uniform_buffer_memory, 0);
필요시 입력 buffer를 mapping하여 필요한 데이터를 입력합니다.
void* input_ptr;
vkMapMemory(device, input_buffer_memory, 0, VK_WHOLE_SIZE, 0, &input_ptr);
vkUnmapMemory(device, input_buffer_memory);
input_ptr = nullptr;
void* uniform_ptr;
vkMapMemory(device, uniform_buffer_memory, 0, VK_WHOLE_SIZE, 0, &uniform_ptr);
vkUnmapMemory(device, uniform_buffer_memory);
uniform_ptr = nullptr;
Descriptor set을 생성합니다.
VkDescriptorSetLayoutBinding bindings[] = {
{ 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr },
{ 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr },
{ 2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr }
};
VkDescriptorSetLayoutCreateInfo desc_set_layout_create_info{};
desc_set_layout_create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
desc_set_layout_create_info.bindingCount = 3;
desc_set_layout_create_info.pBindings = bindings;
VkDescriptorSetLayout desc_set_layout;
vkCreateDescriptorSetLayout(device, &desc_set_layout_create_info, nullptr, &desc_set_layout);
VkDescriptorPoolSize desc_pool_size[] = {
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2 },
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1 }
};
VkDescriptorPoolCreateInfo desc_pool_create_info{};
desc_pool_create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
desc_pool_create_info.maxSets = 1;
desc_pool_create_info.poolSizeCount = 2;
desc_pool_create_info.pPoolSizes = &desc_pool_size;
VkDescriptorPool desc_pool;
vkCreateDescriptorPool(device, &desc_pool_create_info, nullptr, &desc_pool);
VkDescriptorSetAllocateInfo desc_set_allocate_info{};
desc_set_allocate_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
desc_set_allocate_info.descriptorPool = desc_pool;
desc_set_allocate_info.descriptorSetCount = 1;
desc_set_allocate_info.pSetLayouts = &desc_set_layout;
VkDescriptorSet desc_set;
vkAllocateDescriptorSets(device, &desc_set_allocate_info, &desc_set);
Descriptor set과 buffer를 연결합니다.
VkDescriptorBufferInfo input_desc_buffer_info{};
VkDescriptorBufferInfo output_desc_buffer_info{};
VkDescriptorBufferInfo uniform_desc_buffer_info{};
input_desc_buffer_info.buffer = input_buffer;
input_desc_buffer_info.offset = 0;
input_desc_buffer_info.range = WK_WHOLE_SIZE;
output_desc_buffer_info.buffer = output_buffer;
output_desc_buffer_info.offset = 0;
output_desc_buffer_info.range = WK_WHOLE_SIZE;
uniform_desc_buffer_info.buffer = uniform_buffer;
uniform_desc_buffer_info.offset = 0;
uniform_desc_buffer_info.range = WK_WHOLE_SIZE;
VkWriteDescriptorSet write_desc_set[] = {
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, desc_set, 0, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &input_desc_buffer_info, nullptr },
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, desc_set, 1, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &output_desc_buffer_info, nullptr },
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, desc_set, 2, 0, 1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, nullptr, &uniform_desc_buffer_info, nullptr },
};
vkUpdateDescriptorSets(device, 3, write_desc_set, 0, nullptr);
Shader를 로드합니다.
std::vector<char> shader_code;
VkShaderModuleCreateInfo shader_module_create_info{};
shader_module_create_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
shader_module_create_info.pCode = reinterpret_cast<uint32_t*>(shader_code.data());
shader_module_create_info.codeSize = shader_code.size();
VkShaderModule shader_module;
vkCreateShaderModule(device, &shader_module_create_info, nullptr, &shader_module);
Pipeline을 생성합니다.
VkPushConstantRange push_constant_range{};
push_constant_range.offset = 0;
push_constant_range.size = sizeof(PushArgs);
push_constant_range.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
VkPipelineLayoutCreateInfo pipeline_layout_create_info{};
pipeline_layout_create_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipeline_layout_create_info.setLayoutCount = 1;
pipeline_layout_create_info.pSetLayouts = &desc_set_layout;
pipeline_layout_create_info.pushConstantRangeCount = 1;
pipeline_layout_create_info.pPushConstantRanges = &push_constant_range;
VkPipelineLayout pipeline_layout;
vkCreatePipelineLayout(device, &pipeline_layout_create_info, nullptr, &pipeline_layout);
VkComputePipelineCreateInfo compute_pipeline_create_info{};
compute_pipeline_create_info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
compute_pipeline_create_info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
compute_pipeline_create_info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
compute_pipeline_create_info.stage.module = shader_module;
compute_pipeline_create_info.stage.pName = "main";
compute_pipeline_create_info.layout = pipeline_layout;
VkPipeline pipeline;
vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &compute_pipeline_create_info, nullptr, &pipeline);
Command pool을 생성합니다.
VkCommandPoolCreateInfo command_pool_create_info{};
command_pool_create_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
command_pool_create_info.flags |= VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
command_pool_create_info.queueFamilyIndex = queue_family_index.value();
VkCommandPool command_pool;
vkCreateCommandPool(device, &command_pool_create_info, nullptr, &command_pool);
Command buffer를 생성합니다.
VkCommandBufferAllocateInfo command_buffer_allocate_info{};
command_buffer_allocate_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
command_buffer_allocate_info.commandPool = command_pool;
command_buffer_allocate_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
command_buffer_allocate_info.commandBufferCount = 1;
VkCommandBuffer command_buffer;
vkAllocateCommandBuffers(device, &command_buffer_allocate_info, &command_buffer);
생성한 command buffer에 command를 작성합니다.
VkCommandBufferBeginInfo command_buffer_begin_info{};
command_buffer_begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
command_buffer_begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
command_buffer_begin_info.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
vkResetCommandBuffer(command_buffer, 0);
vkBeginCommandBuffer(command_buffer, &command_buffer_begin_info);
vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdBindDescriptorSets(command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_layout, 0, 1, &desc_set, 0, nullptr);
vkCmdPushConstants(command_buffer, pipeline_layout, VK_PIPELINE_BIND_POINT_COMPUTE, 0, sizeof(PushArgs), &push_args);
vkCmdDispatch(command_buffer, WORKGROUP_X, WORKGROUP_Y, WORKGROUP_Z);
vkEndCommandBuffer(command_buffer);
작성한 command를 queue에 submit합니다.
VkFenceCreateInfo fence_create_info{};
fence_create_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
VkFence fence;
vkCreateFence(device, &fence_create_info, nullptr, &fence);
VkSubmitInfo submit_info{};
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &command_buffer;
vkQueueSubmit(queue, 1, &submit_info, fence);
vkWaitForFences(device, 1, &fence, VK_TRUE, 0);
필요시 출력 buffer를 mapping하여 결과 데이터를 출력합니다.
필요할 경우 8, 14, 15, 16, 17 과정 순서대로 반복하거나, command를 다시 사용한다면 14, 15과정을 생략하고 순서대로 반복합니다.
void* output_ptr;
vkMapMemory(device, output_buffer_memory, 0, VK_WHOLE_SIZE, 0, &input_ptr);
vkUnmapMemory(device, output_buffer_memory);
output_ptr = nullptr;
할당한 모든 자원을 해제합니다.
vkFreeCommandBuffers(device, command_pool, 1, &command_buffer);
vkDestroyFence(device, fence, nullptr);
vkDestroyCommandPool(device, command_pool, nullptr);
vkDestroyPipeline(device, pipeline, nullptr);
vkDestroyPipelineLayout(device, pipeline_layout, nullptr);
vkDestroyShaderModule(device, shader_module, nullptr);
vkFreeDescriptorSets(device, desc_pool, 1, &desc_set);
vkDestroyDescriptorPool(device, desc_pool, nullptr);
vkDestroyDescriptorSetLayout(device, desc_set_layout, nullptr);
vkFreeMemory(device, uniform_buffer_memory, nullptr);
vkFreeMemory(device, output_buffer_memory, nullptr);
vkFreeMemory(device, input_buffer_memory, nullptr);
vkDestroyBuffer(device, uniform_buffer, nullptr);
vkDestroyBuffer(device, output_buffer, nullptr);
vkDestroyBuffer(device, input_buffer, nullptr);
vkDestroyDevice(device, nullptr);
vkDestroyDebugReportCallbackEXT(instance, debug_callback, nullptr);
vkDestroyInstance(instance, nullptr);
이상으로 Vulkan 사용을 위한 개념과 사용 방법을 설명드렸습니다. Vulkan을 사용하여 GPU 가속화를 하고자 하는 분들께 도움이 되었으면 좋겠습니다. 물론, 저희가 GPU 지원 기능 개발을 완료하여 위 작업을 직접 하실 필요 없이 Optimium과 Nadya을 통해 GPU 가속화를 추상화되고 편한 방법으로 수행할 수 있도록 제공하는 것이 목표입니다. 다음 게시물에서는 OpenCL을 다룰 예정이니 많은 기대 부탁드립니다!