48GB Mac에서 스왑 없이 Qwen-Image-2.1 실행하는 방법
요약
48GB Mac 환경에서 Qwen-Image-2.1과 같은 대형 이미지 생성 모델을 스왑 없이 구동하는 방법을 안내합니다. 일반적인 로딩 방식이나 CPU 오프로드는 메모리 부족 문제를 해결하지 못했으며, 두 개의 대형 모델 중 하나만 메모리에 로드하는 방식으로 성공적으로 실행했습니다.
핵심 포인트
- 대형 AI 모델은 48GB Mac에서도 메모리 관리가 필수적입니다.
- 일반적인 파이프라인 로딩 및 CPU 오프로드는 스왑을 막지 못합니다.
- 성공적인 구동을 위해서는 대규모 모델 중 일부만 메모리에 로드하는 전략이 필요합니다.
Qwen-Image-2.1은 diffusers를 통해 QwenImage21Pipeline으로 구동됩니다. bfloat16 형식에서 텍스트 인코더는 16.3 GB, 트랜스포머(transformer)는 13.3 GB, VAE는 1.3 GB를 차지하여 로드된 파이프라인은 48 GB Mac에서 총 31.4 GB가 되며, 1024 × 1024 이미지 디코딩 과정에서 추가로 11 GB가 필요했습니다. 이 가이드에서는 일반적인 설정으로 해당 Mac에서 어떤 일이 발생했는지, 왜 모델 CPU 오프로드(model CPU offload)가 도움이 되지 않았는지, 그리고 최종적으로 성공한 설정에 대해 설명합니다. 즉, 두 개의 대형 모델 중 하나만 메모리에 로드하는 방식이었습니다.
실험은 48 GB의 Apple M5 Pro, torch 2.14.0, 그리고 main 브랜치의 diffusers 0.41.0.dev0을 사용하여 진행되었습니다. QwenImage21Pipeline 역시 diffusers 0.41.0 릴리스에 포함되어 있지만, 저는 이 버전을 사용하지 않았습니다. 모든 실행은 시드(seed) 7로 20단계의 1024 × 1024 이미지를 생성했으며, 스왑이 8 GB 증가하면 중단되는 메모리 보호 장치 하에서 진행되었습니다.
디코딩 시 메모리가 스왑으로 넘어감 (Eager loading goes into swap in the decode)
일반적인 코드는 세 모델을 모두 GPU에 로드하고 그곳에 유지합니다:
import torch
from diffusers import QwenImage21Pipeline
...
이 방식은 인코딩과 20단계의 모든 노이즈 제거(denoising) 단계를 거치면서 메모리 사용량이 31 GB에서 33 GB까지 증가했습니다. 이후 VAE 디코드 과정에서 메모리 사용량은 32.5 GB에서 43.6 GB로 늘어났고, 스왑 공간은 8 GB가 증가하여 보호 장치가 이미지를 쓰기 전에 실행을 중단시켰습니다. PyTorch는 그 이전에 메모리가 부족하다는 오류(out-of-memory error)를 발생시키지 않았으며, 대신 Mac이 스왑으로 전환되었습니다.
왜 모델 CPU 오프로드가 Mac에서 도움이 되지 않는가 (Why model CPU offload does not help on a Mac)
파이프라인에 맞지 않아 메모리가 부족할 경우 diffusers가 내장한 해결책은 모델 CPU 오프로드(model CPU offload)입니다. 이 방식은 각 모델을 CPU에 대기시키고, 실제로 실행될 때만 GPU로 이동시키는 것입니다.
pipe = QwenImage21Pipeline.from_pretrained(
Mac에서는 CPU와 GPU가 하나의 메모리 풀을 공유하기 때문에, 'CPU에서' 대기하는 모델도 같은 RAM에 머무릅니다. 한 번의 오프로드(offload) 실행 동안 프로세스 풋프린트(process footprint)는 18.5 GB를 넘지 않았는데, 이는 적절해 보입니다. 그럼에도 불구하고 스왑(Swap)은 증가했습니다. 디노이징(denoising) 과정에서 3.5 GB가 늘었고 VAE 디코드(VAE decode)에서는 9.6 GB까지 늘었으며, 가드(guard)는 이전에 중단되었던 지점에서 실행을 막았습니다. 프로세스가 차지하는 메모리 양인 풋프린트는 이미 스왑된 페이지를 계산하지 않기 때문에 이를 보여줄 수 없습니다.
## 스테이지별로 모델 로드하기
이 파이프라인은 필요한 순서대로 모델을 사용합니다. 즉, 텍스트 인코더(text encoder)는 프롬프트 인코딩에만 사용하고, 트랜스포머(transformer)는 디노이징 루프에서만 사용하며, VAE 디코드에서는 어느 쪽도 사용하지 않습니다. 따라서 두 개의 큰 모델이 메모리에 동시에 존재할 필요가 없습니다. [stageload](https://github.com/nefayran/stageload)은 이를 추적합니다: `StagedModels`는 스테이지가 처음으로 요청할 때 모델을 로드하고, 파이프라인이 해당 모델을 목록에 포함하지 않는 다른 스테이지로 진입하면 해제합니다.
from stageload import StagedModels, on_call
STAGES = {"encode": ["text_encoder"], "denoise": ["transformer"], "decode": []}
...
`text_encoder`와 `transformer`는 각각 `from_pretrained(..., subfolder=...)`를 사용하여 모델을 로드하고 이를 `mps`로 이동시키는 함수입니다. 파이프라인 자체는 `text_encoder=None, transformer=None`으로 구성되며 VAE만 계속 로드된 상태를 유지합니다. `on_call`은 각 스테이지가 시작되기 직전에 `models.enter(stage)`를 실행하여, 파이프라인의 코드를 변경할 필요 없이 작동하게 합니다. 추가로 필요한 부분은 다음과 같습니다: 파이프라인이 디노이징을 시작하기 전에 트랜스포머의 설정을 읽기 때문에, `pipe.text_encoder`와 `pipe.transformer`는 체크포인트에서 `.config`에 응답하고 다른 사용 시 모델을 로드하는 임시 객체(stand-ins)입니다. 전체 스크립트에는 이러한 임시 객체들과 메모리 추적 기록이 포함되어 있으며, [bench/qwen_image.py](https://github.com/nefayran/stageload/blob/main/bench/qwen_image.py)에서 확인할 수 있고, 라이브러리는 `pip install stageload`로 설치할 수 있습니다.
두 단계 실행(staged runs)은 각각 94초와 124초 만에 완료되었습니다. 인코딩 시 피크 메모리 사용량은 19.0 GB였고, 두 실행의 디노이징(denoising) 과정에서는 각각 16.2 GB와 18.6 GB, 디코드(decode) 과정에서는 14.4 GB였습니다. 어느 쪽에서도 스왑(Swap) 메모리가 증가하지 않았으며, 사용 가능한 메모리(free memory)는 항상 51% 미만으로 떨어지지 않았습니다. 두 모델을 로드하는 데는 실행당 약 13초가 걸렸고, 디코드 전에 트랜스포머를 해제하는 데는 추가로 4~5초가 소요되었습니다. 트랜스포머가 메모리에 있는 시점부터 디코드가 시작될 때까지 20단계(steps)는 한 단계 실행에서 70초, 다른 단계 실행에서 99초, 그리고 eager 방식에서는 77.5초가 걸렸습니다. 이 추적 기록들로는 왜 두 단계 실행의 시간이 다른지 알 수 없습니다. 하지만 둘 다 비트 단위로 동일한 이미지를 생성했습니다.
## 스왑을 확인하며 실행 테스트하기
어떤 실행이 성공적인지는, 메모리 사용량(footprint)이 무엇이라고 하든 간에 실행하는 동안 스왑이 증가하지 않는지를 통해 판단합니다. 다음 두 명령어가 이를 보여줍니다:
sysctl vm.swapusage # 실행 전후의 "사용된" 양을 비교
sysctl kern.memorystatus_level # 시스템 전체 사용 가능한 메모리 비율(%)```
stageload의 MemoryMeter는 매 0.5초마다 메모리 사용량, 스왑, 사용 가능한 메모리를 JSONL 추적 파일로 기록하며, stageload guard는 자리가 생길 때 명령을 시작하고, 스왑이 예산(budget)을 초과하여 증가하면 중지합니다:
stageload guard --wait-free 40 --swap-budget 8G -- python generate.py
제가 측정하지 않은 것들
- VAE 타일링(tiling).
pipe.vae.enable_tiling()은 이미지를 타일로 디코드하며, eager 방식과 offload 방식 모두 스왑을 사용하게 되는 부분이 바로 이 디코드 과정입니다. 저는 이를 시도해보지 않았기 때문에, 이것이 eager 방식으로 이 Mac에서 완료될 수 있는지 여부는 말씀드릴 수 없습니다. - 순차적 CPU 오프로드(Sequential CPU offload), 양자화된 가중치(quantized weights), 그리고 48 GB보다 적거나 많은 메모리를 가진 Mac에서의 테스트.
- 단계별 로딩이 eager 방식과 동일한 이미지를 생성하는지 여부. 이 Mac에서는 eager 방식으로 이미지가 한 번도 기록되지 않았습니다.
더 자세한 측정 내용은 Qwen-Image-2.1 one stage at a time와 Model CPU offload on a 48 GB Mac에 있으며, 추적 기록은 the stageload repository에서 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기