주의 : 이 문서는 초심자 튜토리얼이 아닙니다. 기본 개념 정도는 안다고 가정합니다. 초심자는 [ Vulkan Tutorial ] 이나 [ Vulkan Samples Tutorial ] 을 보면서 같이 보시기 바랍니다.

주의 : 완전히 이해하고 작성한 글이 아니므로 잘못된 내용이 포함되어 있을 수 있습니다.

주의 : 이상하면 참고자료를 확인하세요.



VkInstance 를 생성할 때 VkApplicationInfo 라는 것이 필요합니다. 거기에는 ApiVersion 이라는 필드가 있습니다.



명세에서의 설명을 보면 다음과 같습니다.


apiVersion must be the highest version of Vulkan that the application is designed to use, encoded as described in the API Version Numbers and Semantics section. The patch version number specified in apiVersion is ignored when creating an instance object. Only the major and minor version of the instance must match those requested in apiVersion.


- Vulkan specification 1.1.96


이 필드에는 VK_API_VERSION_1_0 이나 VK_API_VERSION_1_1 을 넣게 됩니다. 이것은 다음과 같이 정의되죠.



그리고 VK_MAKE_VERSION 은 다음과 같이 정의되어 있습니다.



여기에서 문제가 하나 발생합니다. 제가 지금 제 로컬 머신에 받아 놓은 LUNARG Vulkan SDK 의 버전은 1.1.85.0 입니다. 그리고 LUNAR XCHANGE 에 가 보면 여러 가지 버전들이 있는 것을 알 수 있죠.



여기에는 버전 번호가 하나 더 있어 헷갈립니다. VK_MAKE_VERSION 은 3 개 항목을 입력으로 받는데요, 여기에는 네 번째 버전이 하나 더 들어 가 있습니다.


대체 이 버전들의 의미는 무엇이고, 왜 패치 버전은 사용하지 않는 것일까요?


Api Version



Vulkan 명세에 의하면, Vulkan API 는 세 개의 버전 정보로 정의되며 32 비트 정수 안에 패킹( packing )됩니다; Major, Minor, Patch.



이것은 명세의 버전과 동일합니다. Vulkan 명세에서는 각 버전에서의 차이점을 다음과 같이 설명하고 있습니다.


  • Major 버전에서는 API 셋들이 변경됩니다. 보통 새로운 기능과 인터페이스가 추가되며, 기능이 변경되기도 합니다. 그리고 기능이 없어지거나 수정되기도 하죠. 즉 하위 버전과의 호환성이 유지되지 않을 수 있습니다. 아직 1.1 밖에 안 된 상황이라 미래에도 이런 일이 벌어질 지는 미지수네요.

  • Minor 버전에서는 새로운 기능이 추가되고, 일부 기능들은 변경되거나 없어질 수 있습니다. 하지만 없어진 기능들을 위한 인터페이스는 그대로 유지됩니다.

  • Patch 버전에서는 명세나 헤더가 조금만 변경됩니다. 보통 버그때문에 변경되는 것으로서 하위 호환성이 유지되어야만 합니다.


그러면 LUNARG SDK 의 버전은 무엇일까요? SDK 버전의 처음 세 개는 Vulkan Api 버전과 동일합니다. 단지 같은 버전에 대해서 다시 패치할 때 마지막 버전을 넣습니다.


$(Major).$(Minor).($Patch).($LUNARG_Patch)


뭐 사람이 하는 일이다보니 실수가 없을 수는 없겠죠. Api 버전은 명세일 뿐이고 실제 구현은 다르기 때문에 이런 버전을 추가한 것으로 보입니다.


Comparing patch versions


그런데 왜 ApiVersion 을 넣을 때는 patch 버전이 무시되는 것일까요?


안타깝게도 Vulkan SDK 에서 배포하고 있는 라이브러리들은 이름에 버전정보를 담고 있지 않습니다. 그리고 헤더에다가도 버전 정보를 담고 있지 않죠; 예를 들어 "vulkan-1-1-85.lib" 같은 라이브러리나 "VkCreateDevice_1_1_85()" 같은 함수는 없습니다. 아마도 Dll Hell 을 피하기 위해 항상 가장 높은 버전을 사용하도록 하고 있는 것으로 보입니다. 심지어는 라이브러리 이름조차 major 버전만을 사용합니다; "vulkan-1.lib".


그러므로 VK_MAKE_VERSION 에서는 패치 번호가 무시됩니다. 


제가 처음에 Api Version 을 입력할 때는 이걸 모르고 있었습니다. Api Version 에 patch 버전을 넣을 수 있길래 버전별로 그 결과를 비교할 수 있겠다 싶어서, 다음과 같이 만들었는데 쓸데 없는 짓이었군요.



만약 LUNARG SDK 의 1.1.85.0 버전에서 발생하던 버그가 1.1.92.1 버전에서 해결되었는지 알고자 한다면, 두 개의 SDK 를 모두 깔아 놓고 "VULKAN_SDK", "VK_SDK_PATH" 등의 환경변수를 변경해 가면서 테스트하는 수밖에 없습니다.


환경변수 설정을 batch 파일 등을 통해서 동적으로 제어할 수 있으면, 결과를 비교하는 더 나은 개발 환경을 만들 수는 있을 것 같습니다.

주의 : 이 문서는 초심자 튜토리얼이 아닙니다. 기본 개념 정도는 안다고 가정합니다. 초심자는 [ Vulkan Tutorial ] 이나 [ Vulkan Samples Tutorial ] 을 보면서 같이 보시기 바랍니다.

주의 : 완전히 이해하고 작성한 글이 아니므로 잘못된 내용이 포함되어 있을 수 있습니다.

주의 : 이상하면 참고자료를 확인하세요.

정보 : 본문의 소스 코드는 Vulkan C++ exmaples and demos 를 기반으로 하고 있습니다. 


 

Memory 관련해서 정리하다가 의문을 하나 가지게 되었습니다.

 

DEVICE_LOCAL 을 지정하면 디바이스 램에 메모리가 할당됩니다. 하지만 allocator 를 사용해서 메모리를 할당하면 어떻게 될까요?

 

먼저 간단하게 malloc 을 사용하는 allocator 용 콜백 함수들을 만들었습니다.

 

 

그리고 나서 allocator 에 할당합니다.

 

 

그리고 1000 개의 이미지를 생성해서 메모리를 할당해 줬습니다. 이 때 앞에서 만든 allocator 를 사용했습니다.

 

 

여러분은 어떤 결과가 나올 거라 예상하시나요? 저는 호스트 램의 메모리가 올라갈 것이라 예상했습니다. 왜냐하면 malloc 은 최종적으로 VirtualAlloc 과 연결되어 있고 호스트 램을 위한 메모리를 할당할 것이라 생각했기 때문이죠. 또한 호스트 메모리 중에서 공유 메모리를 사용할 것이라 생각했습니다.

 

하지만 결과는 충격적( ? )이었습니다. texture 프로세스는 1.6 GB 의 디바이스 메모리를 할당합니다. 제가 캡쳐하는 시점에 다른 응용프로그램이 디바이스 메모리를 사용했는지 수치가 좀 높네요. 실제 수치는 크게 신경쓰지 않으셔도 됩니다. 어디에서 메모리가 늘어나고 있는지가 중요하죠.

 

 

 

리소스 모니터를 보면 좀 다른 결과이긴 하더군요.

 

 

이러한 결과를 볼 때 다음과 같은 가설을 세울 수 있습니다.

 

  • malloc 은 호출되는 문맥에 따라서 호스트 램에 메모리를 할당하기도 하고 디바이스 램에 메모리를 할당하기도 합니다. CUDA 버전 malloc 함수가 따로 있는 이유가 여기에 기반하는 것 같네요.
  • "커밋 크기"는 램의 종류에 상관없이 프로세스가 사용한 메모리의 총량을 보여줍니다.

 

이건 제가 의도한 환경이 아닙니다. 그래서 이미 할당된 메모리를 반환하는 allocator 를 만들어 보기로 했습니다. Reallocation  Free 는 별 의미가 없으므로 대충 넘겼습니다. 메모리가 어디에 할당되는지 확인하는 용도로 만든 것이므로 그냥 Allocation 만 구현했습니다.

 

그래서 처음에 2 GB 를 할당하고 요청이 올 때마다 메모리를 증가시켜 그 주소를 반환할 겁니다.

 

 

실행해 보니 역시나 충격적인 결과를 줍니다. CPU 와 GPU 의 메모리 usage 는 동일한데, 리소스 모니터의 "커밋" 만 다른 결과를 줍니다.

 

 

제가 할당한 2 GB 만 고스란히 늘어났습니다. 즉 DEVICE_LOCAL 을 지정한 메모리를 할당할 때 호스트 메모리를 반환하면, 그것이 무시되고 자체적으로 메모리를 할당한다고 추측해 볼 수 있습니다. Allocator 를 사용하는 의미가 없는거죠.

 

정리

 

DEVICE_LOCAL 을 위한 메모리에 대해서 이미 할당된 메모리를 관리하는 allocator 를 사용하면 메모리가 늘어납니다. 그러므로 HOST_VISIBLE 이나 HOST 전용 메모리( 아무 속성도 지정되지 않은 것 )에 대해서만 allocator 를 사용하시기 바랍니다.

 

이런 관리를 하는 건 매우 귀찮은 일이기 때문에, allocator 를 사용하고 싶으시다면 GPU Open 에서 제공하는 "Vulkan Memory Allocator" 를 사용하는 것을 추천합니다. 헤더 파일 하나만 include 해서 사용할 수 있는 편리한 라이브러리입니다.

주의 : 이 문서는 초심자 튜토리얼이 아닙니다. 기본 개념 정도는 안다고 가정합니다. 초심자는 [ Vulkan Tutorial ] 이나 [ Vulkan Samples Tutorial ] 을 보면서 같이 보시기 바랍니다.

주의 : 완전히 이해하고 작성한 글이 아니므로 잘못된 내용이 포함되어 있을 수 있습니다.

주의 : 이상하면 참고자료를 확인하세요.

정보 : 본문의 소스 코드는 Vulkan C++ exmaples and demos 를 기반으로 하고 있습니다. 



독립적인 Video Card 를 장착한 일반적인 PC 에서의 메모리는 다음과 같이 구성되어 있습니다.



Windows 10 의 작업관리자의 성능탭을 보면 다음과 같이 그 정보가 나와 있습니다.



여기에서 공유 메모리는 호스트 램에 할당될 수 있는 메모리를 의미합니다. 워낙 디바이스 램의 크기가 작다보니 나온 방식이죠. 어쨌든 메모리 속성은 다음과 같이 매핑될 수 있습니다.



VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 를 지정하면 GPU 전용 메모리에 할당되고, VK_PROEPRTY_HOST_VISIBLE_BIT 를 지정하면 공유 메모리에 할당됩니다. 아무 것도 지정하지 않는다면 CPU 전용 메모리가 되겠죠. 물론 거의 사용되지 않긴 합니다. HOST_VISIBLE 인 경우에는 항상 HOST_COHERENT 이거나 HOST_CACHED 여야만 합니다. 


일반적으로 이미지는 DEVICE_LOCAL 로 생성되고, 버퍼는 HOST_VISIBLE 로 생성되는 것 같습니다. 그리고 버퍼의 경우에는 HOST_VISIBLE 과 함께 HOST_COHERENT 를 사용하고 있는 것 같습니다. 아마도 버퍼에 대한 쓰기 연산이 많기 때문인 것으로 보입니다.


샘플의 "texture" 프로젝트에서는 텍스쳐를 생성해서 메모리를 바인딩합니다.



DEVICE_LOCAL 을 지정했으므로 이것은 GPU 전용 메모리에 할당될 것이라 예측할 수 있습니다. 실제로도 그렇죠.



그럼 이 텍스쳐를 공유 메모리에 할당하는 것도 가능할까요? 속성을 HOST_VISIBLE | HOST_COHERENT 로 변경해 봤습니다.



하지만 이렇게 하면 일치하는 메모리 타입을 찾는데 실패해서 크래시가 납니다( 위의 코드에서 getMemoryType() 호출 ). 


하지만 아직까지 메모리 타입과 힙에 대해서 제대로 이해하지 못했기 때문에 에러가 나도 뭘 봐야할지 갑갑하기만 할 것입니다. 그래서 이 부분에 대해서 좀 더 이해할 필요가 있습니다.


vkGetImageMemoryRequirements() 는 이미지를 위한 메모리 요구사항을 반환합니다.



Memory Management Basics ] 에서 언급했듯이, Vulkan 은 오브젝트 생성과 메모리 할당을 분리하고 있습니다. 그래서 두 번째 인자인 image 에는 이미지의 해상도나 포맷 정보가 포함되어 있죠. 그러면 vkGetImageMemoryRequirements() 는 이를 가져가서 세 번째 인자에다가 적절한 요구사항을 채워 줍니다.



  • size 는 이미지를 생성하는데 필요한 바이트 단위 메모리 크기입니다.
  • alignment 는 이미지를 생성하는 데 필요한 메모리가 몇 바이트 단위에서 정렬되어 있어야 하는지를 의미합니다.
  • memoryTypeBits 는 이미지를 위해 지원되는 메모리 타입들에 대한 비트마스크입니다. i 번째 비트는 VkPhysicalDeviceMemoryProperties 의 i 번째 메모리 타입을 의미합니다.


제가 vkGetImageMemoryRequirements() 를 호출했을 때 다음과 같은 결과를 획득했습니다.



이 이미지를 지원하는 메모리 타입은 1 번과 7 번이라는 의미가 됩니다( 1 번과 7 번 비트가 1 이기 때문입니다 ). 


CapsViewer 를 열어 보도록 하겠습니다. CapsViewer 의 Memory 탭의 내용은 메모리 타입과 힙에 대한 정보를 담고 있습니다. 


스크롤을 마지막까지 내리면 힙 정보가 있죠. 이는 VkPhysicalDeviceMemoryProperties 의 memoryHeaps 에 정의되어 있는 정보를 보여줍니다.



두 개의 힙이 존재하는 것을 보실 수 있습니다. 0 번 힙에는 HEAP_DEVICE_LOCAL 로 지정되어 있고, 1 번 힙에는 HEAP_DEVICE_LOCAL 이 지정되어 있지 않습니다. 이제 스크롤을 위로 올려 보면, 각 메모리 타입에 대한 정의를 볼 수 있습니다. 이는 VkPhysicalDeviceMemoryProperties 의 memoryTypes 에 정의되어 있는 정보를 보여줍니다.


예상하고 계시겠지만 HeapIndex 가 0 이라면 DEVICE_LOCAL 만 활성화될 수 있고, 1 이라면 DEVICE_LOCAL 은 절대 활성화될 수 없겠죠. 



아까 위에서 이미지를 위해서 1 번과 7 번 메모리 타입이 지원된다고 했었죠( 10000010 )? 1 번 메모리 타입은 순수하게 호스트를 위해서만 사용될 수 있습니다. 그러므로 HOST_VISIBLE | HOST_COHERENT 를 지정할 수가 없죠. 


7 번은 아래와 같이 DEVICE_LOCAL 로만 사용할 수 있는 메모리 타입입니다.



그러므로 이 이미지를 위한 메모리는 순수하게 GPU-Only 이거나 CPU-Only 여야 한다는 것을 알 수 있습니다.


어떤 리소스가 어떤 메모리 타입을 사용할 수 있느냐는 리소스를 생성할 때 사용한 정보에 의존합니다. 이 경우에는 VkImageCreateInfo 의 내용에 의존하겠죠. 아직까지 정확하게 어떤 항목들이 영향을 주는지는 잘 모르겠지만, 아마도 usage 가 가장 큰 영향을 주지 않을까 생각하고 있습니다.


정리


리소스의 메모리는 힙에 할당됩니다. 그 힙은 크게 HEAP_DEVICE_LOCAL 플래그를 포함하는 힙과 그렇지 않은 힙으로 나뉩니다. 어떤 힙을 사용하느냐에 따라서 메모리 타입이 결정이 되는데요, 특정 리소스는 그것이 사용할 수 있는 메모리 타입의 종류가 정해져 있습니다.


힙과 메모리 타입의 종류는 VkPhysicalDeviceMemoryProperties 에 저장되어 있으며, CapsViewer 에서 일목요연하게 확인하실 수 있습니다.

+ Recent posts