본문으로 건너뛰기
뒤로가기

[Vulkan] MoltenVK 종료 시 메모리 할당 메시지 분석

이 글은 Vulkan을 학습하는 개발자의 관점에서, MoltenVK 사용 시 프로그램 종료 시점에 나타나는 ‘GPU memory still allocated’ 로그의 원인을 사실 위주로 정리한 기록입니다.


문제 상황

MoltenVK를 사용하는 Vulkan 애플리케이션을 종료할 때 아래와 같은 메시지가 출력되는 현상을 발견했습니다. 이것이 실제 메모리 누수인지 확인해볼 필요가 있었습니다.

[Vulkan] Destroyed VkPhysicalDevice for GPU Apple M4 Pro with 29 MB of GPU memory still allocated.

가설 1: Vulkan 리소스 정리 순서 문제

첫 번째로, Vulkan 명세를 위반한 리소스 파괴 순서 때문에 메모리 누수가 발생했을 수 있다고 가정했습니다.

검증: Vulkan 명세 확인

Vulkan 명세에 따르면, 안전한 종료를 위해서는 특정 순서에 따라 객체를 파괴해야 합니다.

단계객체 계층파괴 순서근거
0동기화vkDeviceWaitIdle모든 GPU 작업의 종료를 보장합니다.
1프레임Framebuffer → ImageView → Swapchain부모-자식 규칙을 따릅니다.
2파이프라인Pipeline → PipelineLayout → RenderPass/Shader의존성 관계에 따라 파괴합니다.
3디스크립터Set → Pool → Layout풀을 파괴하면 셋이 암묵적으로 해제됩니다.
4커맨드CommandPool풀을 파괴하면 커맨드 버퍼가 암묵적으로 해제됩니다.
5리소스Buffer/Image → Memory자식 객체를 먼저 파괴합니다.
6메모리VMA Allocator모든 리소스가 해제된 후 파괴합니다.
7동기화 객체Fence, SemaphoreVkDevice 파괴 전에 파괴해야 합니다.
8상위 객체Device → Surface → Instance모든 자식 객체가 파괴된 후 파괴합니다.

제 프로젝트의 shutdown 함수는 vkDeviceWaitIdle 호출을 시작으로, Vulkan 명세가 권장하는 순서(자식 객체 파괴 후 부모 객체 파괴)를 잘 따르고 있음을 확인했습니다. Validation Layer에서도 관련 오류가 발생하지 않았습니다.

가설 1 결론: 기각

리소스 정리 순서는 문제가 아니라고 결론 내렸습니다.

가설 2: CommandBuffer 명시적 해제 누락

Vulkan 명세상 vkDestroyCommandPool이 풀에서 할당된 모든 커맨드 버퍼를 자동으로 해제하지만, MoltenVK에서는 vkFreeCommandBuffers를 명시적으로 호출하는 것을 권장하는 경우가 있습니다. 이것이 원인일 수 있다고 생각했습니다.

검증: 명시적 해제 구현

destroyCommandPool 함수를 수정하여 vkFreeCommandBuffers를 명시적으로 호출하도록 변경했습니다.

void Application::destroyCommandPool() {
    if (commandPool_ != VK_NULL_HANDLE) {
        // MoltenVK 권장: 명시적 해제
        if (!commandBuffers_.empty()) {
            vkFreeCommandBuffers(device_.device(), commandPool_,
                                 static_cast<uint32_t>(commandBuffers_.size()),
                                 commandBuffers_.data());
            commandBuffers_.clear();
        }
        vkDestroyCommandPool(device_.device(), commandPool_, nullptr);
        commandPool_ = VK_NULL_HANDLE;
    }
}

하지만 코드를 수정하고 실행해도 “GPU memory still allocated” 메시지는 사라지지 않았습니다.

가설 2 결론: 기각

CommandBuffer의 명시적 해제 여부는 이 문제와 관련이 없었습니다.

가설 3: Objective-C Autorelease Pool 문제

macOS 환경에서는 Metal 객체들이 @autoreleasepool에 의해 관리되는데, 이 풀이 제때 해제되지 않아 메모리가 남아있을 수 있다는 가설을 세웠습니다.

검증: Autorelease Pool 적용

메인 루프의 각 프레임 시작과 끝에 @autoreleasepool을 생성하고 해제하는 코드를 추가했습니다.

// main loop
while (!window_.shouldClose()) {
#ifdef __APPLE__
    void* pool = objc_autoreleasePoolPush();
#endif
    window_.pollEvents();
    drawFrame();
#ifdef __APPLE__
    objc_autoreleasePoolPop(pool);
#endif
}

이 변경 후에도 종료 시점의 메시지는 동일하게 발생했습니다.

가설 3 결론: 기각

Objective-C의 Autorelease Pool 문제도 아니었습니다.

가설 4: CAMetalLayer의 Drawable 보유 문제 (최종 가설)

남은 가능성은 MoltenVK의 버그가 아니라, MoltenVK가 의존하는 macOS 그래픽 스택(Core Animation)이 마지막 프레임을 표시하기 위해 드로어블을 잠시 보유하는 구조적 특성이라고 판단했습니다.

이론적 배경

  1. VkImageCAMetalDrawable의 관계
    MoltenVK에서 스왑체인을 생성하면 각 VkImageCAMetalLayer가 반환한 CAMetalDrawable.texture를 래핑합니다. 즉, Vulkan 스왑체인 이미지는 Metal 드로어블을 직접 참조할 뿐, 실제 리소스의 소유자는 아닙니다.

  2. 드로어블 풀과 표시 타이밍
    Apple의 Metal Best Practices Guide – Drawables에 따르면,
    CAMetalLayer제한된 수의 드로어블을 재사용 가능한 풀(limited and reusable resource pool) 로 관리합니다.
    시스템이 아직 드로어블을 재활용하지 못한 경우, 새 드로어블을 요청하는 시점에 일시적으로 대기(block) 가 발생할 수 있습니다.
    또한 드로어블의 실제 표시(present)는 커맨드 버퍼 실행이 완료된 후 이루어집니다.
    이러한 구조는 macOS가 트리플 버퍼링(triple buffering) 을 구현하는 일반적인 방식으로, Core Animation이 몇 개의 프레임을 미리 보유하여 부드러운 화면 전환을 보장합니다.
    즉, macOS는 프레임 전환의 일관성을 위해 마지막 몇 개의 드로어블을 잠시 유지(retain) 하므로, 애플리케이션이 리소스를 모두 해제하더라도 일부 드로어블이 GPU 메모리를 계속 점유할 수 있습니다.

MoltenVK 관점

MoltenVK은 CAMetalDrawable.texture를 단순히 감싼 VkImage를 노출합니다. Core Animation이 드로어블을 retain하는 동안 Metal 드라이버는 해당 텍스처를 “사용 중”으로 간주하고, MoltenVK은 디바이스 파괴 시점에 그 메모리를 아직 할당된 것으로 보고합니다. 실제로 MoltenVK 로그에는 다음과 같은 정보 메시지가 남습니다.

[mvk-info] Destroyed VkPhysicalDevice for GPU Apple M4 Pro with 29 MB of GPU memory still allocated.

MoltenVK 이슈 #2547에서도 비슷한 현상이 논의되며, 메모리 누수의 가능성보다는 타이밍의 문제라고 보고 있습니다. Instruments에서 Metal 객체 누수가 관찰되지 않고 있다는 점이 이러한 주장을 더 강화합니다.

검증

1. 수학적 검증
해상도 2560 × 1440, 포맷 RGBA8(4 bytes/pixel)일 때 한 프레임은 약 14.06 MiB이고 두 프레임은 약 28.1 MiB입니다. 로그의 29 MB와 거의 일치합니다.

2. 포맷 변경 실험

VkSurfaceFormatKHR chooseSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& formats) {
    for (const auto& f : formats) {
        if ((f.format == VK_FORMAT_R16G16B16A16_SFLOAT) &&
            f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
            return f;
        }
    }
    return formats.empty()
               ? VkSurfaceFormatKHR{VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}
               : formats[0];
}
[Vulkan] Created 3 swapchain images with size (2560, 1440) and contents scale 2.0
[Swapchain] Created: format=97, extent=2560x1440, images=3
[Vulkan] Destroyed VkDevice on GPU Apple M4 Pro with 2 Vulkan extensions enabled.
[Vulkan] Destroyed VkPhysicalDevice for GPU Apple M4 Pro with 57 MB of GPU memory still allocated.

스왑체인 포맷을 RGBA16F(8 bytes/pixel)로 바꾸면 예상 크기는 약 56.2 MiB입니다. 실제 로그에서도 57 MB가 남았다고 보고되어 예측과 정확히 맞아떨어졌습니다.

가설 4 결론: 채택

남아 있는 메모리는 Core Animation이 retain 중인 스왑체인 드로어블이며, macOS의 정상적인 컴포지션 타이밍 차이에서 비롯된 정보성 메시지라고 결론 내렸습니다.

최종 결론

MoltenVK에서 출력되는 “GPU memory still allocated” 메시지는 다음과 같이 정리할 수 있습니다.

  1. 실제 메모리 누수가 아닙니다. 이것은 MoltenVK가 제공하는 정보성 로그이며, macOS의 디스플레이 컴포지터(CAMetalLayer)가 정상적으로 동작하면서 마지막 프레임들을 보유하고 있음을 보여주는 것입니다. 프로세스가 종료되면 이 메모리는 운영체제에 의해 완전히 회수됩니다. 실제 프로젝트에서 MVK 로그를 출력하도록 설정하고 확인을 해보면 [mvk-info] Destroyed VkPhysicalDevice for GPU Apple M4 Pro with 29 MB of GPU memory still allocated.와 같은 메시지가 나오는데, 이 메시지는 위에서 나온 Vulkan의 로그와 일치하는 것을 알 수 있습니다. 즉, Vulkan의 로그는 MoltenVK에서 제공하는 정보성 로그와 동일합니다.

  2. 애플리케이션의 리소스 정리는 올바릅니다. Vulkan Validation Layer에서 오류가 없었고, 리소스 파괴 순서도 명세를 준수하고 있었습니다.

  3. 보고된 메모리의 정체는 스왑체인 이미지 2장입니다. 실험을 통해 보고된 메모리 크기가 스왑체인 이미지 2장의 크기와 정확히 일치함을 입증했습니다.

따라서 이 메시지는 실질적인 문제가 아니며, 무시하거나 로그 레벨을 조정하여 숨겨도 괜찮습니다.

권장 조치 (선택사항)

이 정보성 로그를 보지 않으려면, 환경 변수를 설정하여 MoltenVK의 로그 레벨을 조정할 수 있습니다.

# .zshrc 또는 .bash_profile 에 추가
export MVK_CONFIG_LOG_LEVEL=1  # 에러만 표시

또는 코드 내에서 직접 설정할 수도 있습니다.

#ifdef __APPLE__
    setenv("MVK_CONFIG_LOG_LEVEL", "1", 1);
#endif

참고 자료


이 게시물은 학습한 내용을 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 내용을 검수하고 다듬어 완성되었습니다.


공유하기:

이전 글
[Vulkan] 커맨드 풀 설계: 스왑체인 기반과 프레임 기반 구조 비교 분석
다음 글
[Vulkan] 좌표계의 Y축은 왜 아래를 향할까?