Published on

Stable Diffusion VRAM OOM 해결 - xformers·VAE 타일링

Authors

Stable Diffusion을 돌리다 보면 가장 흔하게 마주치는 에러가 VRAM OOM(out of memory)입니다. 특히 1024x1024 이상 고해상도, 배치 증가, ControlNet/LoRA 다중 적용, Hires.fix 같은 “한 번 더” 단계가 추가될 때 OOM은 거의 필연처럼 등장합니다.

이 글은 단순히 “해상도 낮추세요” 같은 처방이 아니라, 왜 OOM이 생기는지를 메모리 관점에서 정리하고, 실전에서 효과가 큰 xformers(메모리 효율 어텐션), VAE 타일링(디코드/인코드 타일 분할), 그리고 함께 쓰면 좋은 주변 최적화(half, attention slicing, CPU offload 등)를 코드와 함께 정리합니다.


OOM이 터지는 지점: UNet vs VAE vs 후처리

Stable Diffusion 파이프라인에서 VRAM을 많이 쓰는 구간은 크게 3곳입니다.

  1. UNet(확산 단계)

    • 매 스텝마다 어텐션 맵과 중간 feature를 들고 있어야 하며, 해상도와 배치에 비례해 메모리가 급격히 증가합니다.
    • 특히 Self-Attention은 토큰 수가 늘면 비용이 커집니다.
  2. VAE(최종 디코딩/인코딩)

    • 샘플링이 끝난 latent를 이미지로 디코딩할 때 한 번에 큰 텐서를 만들며, 고해상도에서 OOM이 자주 납니다.
    • “샘플링은 되는데 저장/미리보기에서 죽는” 케이스가 대표적입니다.
  3. 부가 기능(ControlNet, IP-Adapter, 다중 LoRA, Hires.fix)

    • 추가 네트워크가 붙거나 2차 패스가 생기면 VRAM 피크가 더 높아집니다.

결론적으로, UNet 메모리 최적화는 xformers가 가장 체감이 크고, VAE OOM은 타일링이 가장 확실한 해결책이 됩니다.


1) xformers: 어텐션 메모리를 확 줄이는 핵심 카드

xformers는 “메모리 효율 어텐션” 커널을 제공해, 어텐션 연산에서 중간 행렬을 덜 들고 계산하도록 도와줍니다. Stable Diffusion에서는 UNet 어텐션이 병목이 되는 경우가 많아서, xformers 적용만으로도 고해상도에서 생존 확률이 크게 올라갑니다.

설치(환경별)

가장 흔한 조합은 diffusers + torch + xformers입니다. CUDA/torch 버전에 맞춰 설치해야 하며, 미스매치면 빌드가 실패하거나 런타임에서 비활성화됩니다.

# 예시: pip로 설치 (환경에 따라 호환 버전 필요)
pip install -U xformers

설치가 꼬이면, torch/CUDA 조합을 먼저 고정한 뒤 xformers를 맞추는 방식이 안전합니다. 운영 환경에서 의존성 꼬임은 장애로 이어지기 쉬우니, CI에서 버전을 고정하고 재현 가능하게 만드는 습관이 중요합니다. (비슷한 맥락의 “재현 가능한 디버깅” 관점은 Jenkins 에이전트 Offline 원인과 해결 체크리스트 같은 글의 접근과도 통합니다.)

diffusers에서 활성화 코드

import torch
from diffusers import StableDiffusionPipeline

model_id = "runwayml/stable-diffusion-v1-5"
pipe = StableDiffusionPipeline.from_pretrained(
    model_id,
    torch_dtype=torch.float16,
)
pipe = pipe.to("cuda")

# xformers 메모리 효율 어텐션 활성화
pipe.enable_xformers_memory_efficient_attention()

image = pipe(
    prompt="a photo of a cat",
    num_inference_steps=30,
    guidance_scale=7.5,
).images[0]

image.save("out.png")

xformers가 안 먹는 경우 체크리스트

  • GPU/드라이버/CUDA/torch/xformers 버전 불일치
  • torch_dtypefloat32로 강제되어 메모리 이득이 상쇄
  • 일부 환경에서 xformers가 설치되어도 런타임에서 fallback

런타임에서 실제로 적용됐는지 확인하려면, 실행 로그/경고를 확인하거나, 메모리 사용량이 유의미하게 줄었는지 관찰하는 것이 현실적입니다.


2) VAE 타일링: “마지막 디코딩에서 죽는” OOM을 잡는다

샘플링은 UNet이 담당하지만, 최종적으로 사람이 보는 이미지를 만들려면 VAE 디코딩이 필요합니다. 고해상도에서 OOM이 자주 발생하는 이유는, VAE가 한 번에 큰 텐서를 만들기 때문입니다.

VAE 타일링은 디코딩(또는 인코딩)을 타일 단위로 쪼개서 처리해, 피크 VRAM을 낮추는 방식입니다. 속도는 느려질 수 있지만, “일단 살아남게” 만드는 데 매우 강력합니다.

diffusers에서 VAE 타일링 활성화

import torch
from diffusers import StableDiffusionPipeline

pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16,
).to("cuda")

pipe.enable_xformers_memory_efficient_attention()

# VAE 디코딩을 타일 단위로 처리
pipe.enable_vae_tiling()

image = pipe(
    prompt="ultra detailed landscape, 8k",
    height=1024,
    width=1024,
    num_inference_steps=30,
).images[0]

image.save("tiled.png")

타일링을 언제 켜야 하나

  • 1024x1024 이상에서 디코딩 시점 OOM이 난다
  • 배치 2 이상에서 마지막에 터진다
  • ControlNet/후처리를 붙였더니 “마지막 한 방”이 부족해졌다

반대로, 512x512에서 여유가 충분하면 타일링은 속도만 손해일 수 있습니다.


3) xformers + VAE 타일링 조합 전략

둘 다 켜면 대부분의 “고해상도 OOM”이 해결되는 편이지만, 목표는 “무조건 켜기”가 아니라 품질·속도·안정성의 균형입니다.

권장 접근은 다음 순서입니다.

  1. float16 또는 bfloat16로 기본 VRAM 절감
  2. xformers로 UNet 어텐션 메모리 최적화
  3. 그래도 마지막에 죽으면 VAE 타일링
  4. 그래도 부족하면 attention slicing, CPU offload, 해상도/배치 조정

4) 추가로 잘 먹히는 메모리 절감 옵션들

(1) attention slicing

어텐션을 슬라이스 단위로 계산해 피크 메모리를 줄입니다. 속도는 느려질 수 있습니다.

pipe.enable_attention_slicing("auto")

xformers가 잘 동작하면 slicing은 굳이 필요 없을 때도 많지만, VRAM이 매우 타이트한 카드에서는 “보험”이 됩니다.

(2) CPU offload

GPU VRAM이 부족할 때 일부 모듈을 CPU로 옮겨 VRAM을 확보합니다. 속도는 확실히 느려집니다.

pipe.enable_model_cpu_offload()

(3) 배치, 해상도, 스텝의 현실적인 타협

  • 배치 1로 고정하고, 필요하면 여러 번 돌려서 큐잉
  • 해상도는 768 또는 832 같은 중간 타협도 고려
  • 스텝을 무작정 늘리기보다, 샘플러/스케줄러 조정으로 효율을 챙기기

5) OOM 디버깅: “어디서” 터지는지부터 분리

OOM을 빨리 잡으려면, 다음을 분리해 관찰하는 게 좋습니다.

  1. 샘플링 중 OOM: UNet/어텐션 이슈일 가능성이 큼
    • xformers, attention slicing, 해상도/배치 조정
  2. 샘플링 후 디코딩에서 OOM: VAE 이슈일 가능성이 큼
    • VAE 타일링, VAE를 half로, 디코딩 파이프라인 점검
  3. 특정 기능 추가 후 OOM: ControlNet/어댑터/LoRA 누적
    • 하나씩 끄며 피크 지점 확인

이 과정은 API 장애에서 병목을 쪼개 추적하는 방식과 유사합니다. 예를 들어 “원인 분리” 관점은 Spring Boot 3 간헐적 500? Netty 메모리릭 추적 같은 글에서의 접근과도 닮아 있습니다.


6) 실전 레시피: 8GB VRAM 기준 생존 설정

8GB 카드에서 많이 쓰는 현실적인 조합 예시는 아래와 같습니다.

  • 해상도: 768x768 또는 832x832
  • 배치: 1
  • dtype: float16
  • xformers: on
  • VAE 타일링: 고해상도 또는 디코딩 OOM 시 on
  • attention slicing: 그래도 부족하면 on

통합 예제 코드

import torch
from diffusers import StableDiffusionPipeline, EulerAncestralDiscreteScheduler

pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16,
)

pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
pipe = pipe.to("cuda")

# 1) UNet 메모리 최적화
pipe.enable_xformers_memory_efficient_attention()

# 2) 디코딩 OOM 방지
pipe.enable_vae_tiling()

# 3) 여전히 부족하면 추가
# pipe.enable_attention_slicing("auto")

prompt = "cinematic portrait photo, 35mm, high detail"

out = pipe(
    prompt=prompt,
    height=832,
    width=832,
    num_inference_steps=28,
    guidance_scale=6.5,
)

out.images[0].save("result.png")

7) 자주 묻는 함정: “VRAM은 남아 보이는데 왜 OOM?”

모니터링 툴에서 VRAM이 남아 보이는데도 OOM이 나는 이유는 보통 다음 중 하나입니다.

  • 메모리 단편화: 큰 연속 블록을 못 잡아 실패
  • 피크 사용량이 짧게 발생: 샘플링 특정 스텝이나 디코딩 순간에 스파이크
  • 다른 프로세스 점유: 브라우저, 다른 모델 서빙, 데스크톱 캡처 등

이럴 때는 “평균 사용량”이 아니라 “피크 순간”을 줄이는 옵션(xformers, 타일링)이 특히 효과적입니다.


정리

  • UNet 쪽 OOM은 xformers가 가장 즉효인 경우가 많습니다.
  • “다 돌고 마지막에 죽는” OOM은 VAE 타일링으로 해결되는 경우가 많습니다.
  • 그래도 부족하면 attention slicing, CPU offload로 생존성을 올리고, 배치/해상도/스텝을 현실적으로 조정합니다.

Stable Diffusion OOM은 결국 “피크 VRAM을 누가 만들어내는가”를 찾는 게임입니다. xformers로 어텐션 피크를 낮추고, VAE 타일링으로 디코딩 피크를 낮추면, 같은 GPU에서도 훨씬 공격적인 해상도와 파이프라인 구성이 가능해집니다.