Company

Resources

Company

Resources

Technology

Vulkan Compute Shader — GPU 코드 실행의 핵심

Vulkan Compute Shader — GPU 코드 실행의 핵심

Vulkan Compute Shader — GPU 코드 실행의 핵심

이번 게시물에서는 Graphics를 위한 공개 표준인 Vulkan을 이용해 연산을 수행하는 Vulkan Compute Shader에 대해 알아보고자 합니다. 이번 글은 먼저 Vulkan을 사용하기 위해 필요한 개념들에 대해 설명을 드리고 그 다음 간략한 사용 방법을 코드로 보여드릴 것입니다. 많은 분들께 도움이 되길 기대합니다.

Jinwhan Shin

July 22, 2024

안녕하세요? 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가 가장 많이 사용됩니다.

  1. 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에서 접근 가능한지 여부에 따라 아래와 같이 구분됩니다.

  1. 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으로 구분됩니다.

  1. 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 구조체를 통해 획득할 수 있습니다.

  1. 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~ 계열의 함수를 사용하여 vkBeginCommandBuffervkEndCommandBuffer사이에 명령을 기록하는데요.

  • 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를 실행할 때의 제약 사항은 아래와 같습니다.

  1. 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() {
        // do something...
    }
  1. 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;
    
    // 필요하다면 validation layer를 추가할 수 있습니다. 이는 debugging용으로 production에서는
    // 성능저하가 있을 수 있으므로 지양해합니다.
    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);
    // end of validation layer
    
    VkInstance instance;
    vkCreateInstance(&createInfo, nulptr, &instance);


  2. 적절한 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());
    
        // find suitable device...
        // suitable device must have queue and memory heap for compute.
      // And the device may larger memory heap for compute and/or have discrete GPU for more performance.
      // It's all up to you which device to choose.


  3. 적절한 physical device를 찾았으면, logical device를 생성합니다.

    // find queue family for compute
    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());
    
    // create logical device
    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;
    
    // 필요다면 extension을 추가할 수 있습니다.
    
    deviceCreateInfo.enabledLayerCount = 0;
    
    VkDevice device;
    vkCreateDevice(physical_device, &deviceCreateInfo, nullptr, &device);


  4. Queue를 생성합니다.

    VkQueue queue;
    vkGetDeviceQueue(device, queue_family_index.value(), 0, &queue);


  5. 입출력용 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);


  6. Buffer를 위한 device memory를 할당합니다.

    // Find suitable memory heap
    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; // host에 접근 가능하고, flush가 필요 없는 memory를 할당할 때
        // 또는
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; // device에서만 사용하는 memory를 할당할 때
    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);


  7. Buffer에 device memory를 binding합니다.

    vkBindBufferMemory(device, input_buffer, input_buffer_memory, /*offset=*/0);
    vkBindBufferMemory(device, output_buffer, output_buffer_memory, /*offset=*/0);
    vkBindBufferMemory(device, uniform_buffer, uniform_buffer_memory, /*offset=*/0);


  8. 필요시 입력 buffer를 mapping하여 필요한 데이터를 입력합니다.

    void* input_ptr;
    vkMapMemory(device, input_buffer_memory, /*offset=*/0, VK_WHOLE_SIZE, 0, &input_ptr);
    
    // do something with input_ptr
    
    vkUnmapMemory(device, input_buffer_memory);
    input_ptr = nullptr;
    
    void* uniform_ptr;
    vkMapMemory(device, uniform_buffer_memory, /*offset=*/0, VK_WHOLE_SIZE, 0, &uniform_ptr);
    
    // do something with uniform_ptr
    
    vkUnmapMemory(device, uniform_buffer_memory);
    uniform_ptr = nullptr;


  9. Descriptor set을 생성합니다.

    VkDescriptorSetLayoutBinding bindings[] = {
        { /*binding=*/0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, /*descriptorCount=*/1, VK_SHADER_STAGE_COMPUTE_BIT, /*pImmutableSamplers=*/nullptr },
        { /*binding=*/1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, /*descriptorCount=*/1, VK_SHADER_STAGE_COMPUTE_BIT, /*pImmutableSamplers=*/nullptr },
        { /*binding=*/2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, /*descriptorCount=*/1, VK_SHADER_STAGE_COMPUTE_BIT, /*pImmutableSamplers=*/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; // how many sets to be allocated
    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);


  10. 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, /*dstBinding=*/0, /*dstArrayElement=*/0, /*descriptorCount=*/1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &input_desc_buffer_info, nullptr },
        { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, desc_set, /*dstBinding=*/1, /*dstArrayElement=*/0, /*descriptorCount=*/1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &output_desc_buffer_info, nullptr },
        { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, desc_set, /*dstBinding=*/2, /*dstArrayElement=*/0, /*descriptorCount=*/1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, nullptr, &uniform_desc_buffer_info, nullptr },
    };
    
    vkUpdateDescriptorSets(device, 3, write_desc_set, 0, nullptr);


  11. Shader를 로드합니다.

    std::vector<char> shader_code;
    // load 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);


  12. 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);


  13. Command pool을 생성합니다.

    VkCommandPoolCreateInfo command_pool_create_info{};
    command_pool_create_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
    
    // 만약 vkResetCommandBuffer를 호출할 계획이라면, 다음 flag가 반드시 있어야 합니다.
    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);


  14. 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);


  15. 생성한 command buffer에 command를 작성합니다.

    VkCommandBufferBeginInfo command_buffer_begin_info{};
    command_buffer_begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    // 1번만 사용하고 reset 될 경우 이 flag를 사용합니다.
    command_buffer_begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
    
    // 최초 1번만 기록하고 계속 재사용 될 경우 이 flag를 사용합니다.
    command_buffer_begin_info.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
    
    // command buffer를 재사용하는 것이라면 reset합니다.
    vkResetCommandBuffer(command_buffer, 0);
    
    vkBeginCommandBuffer(command_buffer, &command_buffer_begin_info);
    
    // recoding commands
    vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
    vkCmdBindDescriptorSets(command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_layout, /*firstSet=*/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);
    // end of recording commands
    
    vkEndCommandBuffer(command_buffer);


  16. 작성한 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, /*waitAll=*/VK_TRUE, /*timeout=*/0);


  17. 필요시 출력 buffer를 mapping하여 결과 데이터를 출력합니다.

    • 필요할 경우 8, 14, 15, 16, 17 과정 순서대로 반복하거나, command를 다시 사용한다면 14, 15과정을 생략하고 순서대로 반복합니다.

    void* output_ptr;
    vkMapMemory(device, output_buffer_memory, /*offset=*/0, VK_WHOLE_SIZE, 0, &input_ptr);
    
    // do something with output_ptr
    
    vkUnmapMemory(device, output_buffer_memory);
    output_ptr = nullptr;


  18. 할당한 모든 자원을 해제합니다.

    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);
    
    // debug callback을 생성했다면 같이 destroy 해줍니다.
    vkDestroyDebugReportCallbackEXT(instance, debug_callback, nullptr);
    
    vkDestroyInstance(instance, nullptr);

이상으로 Vulkan 사용을 위한 개념과 사용 방법을 설명드렸습니다. Vulkan을 사용하여 GPU 가속화를 하고자 하는 분들께 도움이 되었으면 좋겠습니다. 물론, 저희가 GPU 지원 기능 개발을 완료하여 위 작업을 직접 하실 필요 없이 Optimium과 Nadya을 통해 GPU 가속화를 추상화되고 편한 방법으로 수행할 수 있도록 제공하는 것이 목표입니다. 다음 게시물에서는 OpenCL을 다룰 예정이니 많은 기대 부탁드립니다!

Optimium

Optimium

Solutions

Resources

ENERZAi

Copyright ⓒ ENERZAi Inc. All Rights Reserved

Business number: 246-86-01405

Email: contact@enerzai.com

Call: +82 (2) 883 1231

Address: 06140 27, Teheran-ro 27-gil, Gangnam-gu, Seoul, Republic of Korea

Optimium

Optimium

Solutions

Resources

ENERZAi

Copyright ⓒ ENERZAi Inc. All Rights Reserved

Business number: 246-86-01405

Email: contact@enerzai.com

Call: +82 (2) 883 1231

Address: 06140 27, Teheran-ro 27-gil, Gangnam-gu, Seoul, Republic of Korea

Optimium

Optimium

Solutions

Resources

ENERZAi

Copyright ⓒ ENERZAi Inc. All Rights Reserved

Business number: 246-86-01405

Email: contact@enerzai.com

Call: +82 (2) 883 1231

Address: 06140 27, Teheran-ro 27-gil, Gangnam-gu, Seoul, Republic of Korea