Published on

Stable Diffusion VRAM OOM 없애는 7가지 최적화

Authors

Stable Diffusion을 돌리다 보면 가장 흔한 실패가 CUDA out of memory 입니다. 특히 1024x1024, 큰 배치, 고해상도 업스케일, ControlNet 다중 사용, LoRA 여러 개를 동시에 얹는 순간 VRAM은 순식간에 바닥납니다.

OOM을 없애려면 단순히 해상도를 낮추는 것만으로는 부족합니다. 어떤 단계가 VRAM을 먹는지(UNet forward, attention, VAE decode, ControlNet, sampler step)와 어떤 최적화가 “피크 메모리”를 깎는지를 이해해야 같은 카드로 더 큰 작업을 안정적으로 돌릴 수 있습니다.

아래 7가지는 diffusers 기반 파이프라인, AUTOMATIC1111(WebUI), ComfyUI 등에서 공통으로 통하는 실전 최적화입니다.

참고: OOM은 본질적으로 “리소스 부족으로 인한 크래시”라서, 서버 운영 관점에서는 크래시 루프 디버깅과도 닮았습니다. 비슷한 사고방식(원인 분해, 재현, 관측 지표)이 필요하면 Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅도 함께 보시면 도움이 됩니다.

1) FP16 또는 BF16로 바꾸기: 가장 먼저 할 일

VRAM 사용량은 텐서 dtype에 크게 좌우됩니다. 기본이 FP32면 절반 수준으로 줄일 수 있는 여지가 큽니다.

  • NVIDIA RTX 계열: 보통 FP16이 무난
  • Ampere 이후(예: A100, RTX 30/40 일부 환경): BF16도 안정적

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")

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

체크 포인트

  • FP16에서 NaN이 나오면 sampler, VAE, 혹은 특정 LoRA 조합이 원인일 수 있습니다. 이 경우 VAE만 FP32로 올리는 방법(아래 6번)도 유효합니다.

2) xFormers / SDPA로 attention 메모리 최적화

Stable Diffusion에서 VRAM 피크는 attention에서 크게 튑니다. 특히 고해상도일수록 attention의 메모리 비용이 급증합니다.

  • xFormers 메모리 효율 attention
  • PyTorch 2.x의 scaled_dot_product_attention(SDPA) 기반 최적화

diffusers: xFormers 활성화

pipe.enable_xformers_memory_efficient_attention()

diffusers: PyTorch SDPA 사용(환경에 따라)

import torch
torch.backends.cuda.enable_flash_sdp(True)
torch.backends.cuda.enable_mem_efficient_sdp(True)
torch.backends.cuda.enable_math_sdp(False)

실전 팁

  • xFormers는 설치/버전 조합에 민감합니다. OOM은 줄어도 속도나 품질이 약간 달라질 수 있습니다.
  • SDPA는 PyTorch, CUDA, 드라이버 조합에 따라 fallback이 일어나며, 그 경우 기대한 만큼 절감이 안 될 수 있습니다.

3) UNet/VAE CPU 오프로딩: VRAM을 “비우는” 전략

VRAM이 절대적으로 부족한 카드(예: 6GB, 8GB)에서는 계산이 끝난 모듈을 CPU로 내리는 오프로딩이 강력합니다. 속도는 느려질 수 있지만, OOM을 끊는 데는 효과가 확실합니다.

diffusers: sequential CPU offload

from diffusers import StableDiffusionPipeline
import torch

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

image = pipe(
    "cinematic lighting, ultra detailed",
    num_inference_steps=35,
).images[0]

diffusers: model CPU offload(좀 더 단순)

pipe.enable_model_cpu_offload()

주의

  • CPU RAM과 PCIe 전송이 병목이 됩니다. 대신 “돌아가게” 만드는 옵션입니다.

4) 타일링(특히 VAE): 고해상도에서 피크 메모리 급감

고해상도에서 자주 터지는 구간이 VAE 디코드/인코드입니다. VAE는 이미지 공간으로 변환할 때 큰 텐서를 만들기 때문에, 1장 생성에서도 피크가 튈 수 있습니다.

해결책은 타일 단위로 VAE를 돌리는 것입니다.

diffusers: VAE slicing / tiling

pipe.enable_vae_slicing()   # 메모리 절약, 속도 약간 감소
pipe.enable_vae_tiling()    # 고해상도에서 특히 효과적

언제 쓰나

  • 1024x1024 이상
  • Hires fix, 업스케일 파이프라인
  • 여러 장을 한 번에 뽑는 배치 작업

5) 해상도·배치·스텝을 “피크 기준”으로 재설계하기

OOM은 평균 사용량이 아니라 피크 사용량에서 발생합니다. 따라서 다음 세 변수를 감으로 조절하면 비효율이 큽니다.

  • height, width: attention과 feature map 크기에 직결
  • batch_size 또는 num_images_per_prompt: 한 번에 잡는 텐서 수가 늘어남
  • num_inference_steps: 총 연산량은 늘지만, 피크 VRAM은 주로 해상도/배치가 좌우

권장 접근

  1. 해상도는 목표의 70% 정도로 먼저 안정화
  2. num_images_per_prompt=1로 고정
  3. 스텝은 늘려도 OOM이 나지 않는지 확인
  4. 마지막에만 배치를 늘리거나, 배치 대신 반복 실행으로 분산

diffusers 예시: 배치 대신 반복 실행

prompts = ["portrait photo", "landscape photo", "product photo"]

for p in prompts:
    img = pipe(p, num_inference_steps=30, height=768, width=512).images[0]

배치를 올리면 VRAM이 한 번에 터질 수 있지만, 반복 실행은 VRAM 피크를 일정하게 유지합니다.

6) VAE만 FP32로, 나머지는 FP16: 품질/안정성 균형

FP16으로 내리면 VRAM은 줄지만, 특정 환경에서 VAE나 일부 연산이 불안정해질 수 있습니다. 이때는 UNet은 FP16 유지, VAE만 FP32로 올려서 안정성을 확보합니다.

diffusers 예시

import torch
from diffusers import StableDiffusionPipeline, AutoencoderKL

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

pipe.vae = AutoencoderKL.from_pretrained(
    "stabilityai/sd-vae-ft-mse",
    torch_dtype=torch.float32,
).to("cuda")

포인트

  • VRAM은 조금 늘지만, NaN/밴딩/이상 색감 같은 문제를 줄이는 경우가 있습니다.
  • 반대로 VRAM이 너무 빡빡하면 4번의 VAE 타일링과 같이 쓰는 게 안전합니다.

7) 메모리 누수처럼 보이는 “캐시/그래프” 정리: 반복 실행 안정화

같은 설정인데도 여러 번 돌리다 보면 OOM이 나는 경우가 있습니다. 원인은 크게 두 가지입니다.

  • 계산 그래프가 해제되지 않음: torch.no_grad() 미사용, 텐서를 리스트에 쌓아둠
  • 캐시/메모리 단편화: 긴 세션에서 allocator 단편화로 피크가 증가

기본 수칙

  • 추론은 반드시 torch.inference_mode() 또는 torch.no_grad()
  • 생성된 텐서를 GPU에 쌓아두지 말고 즉시 CPU로 옮기거나 파일로 저장

예시: 안전한 반복 추론 루프

import torch

torch.set_grad_enabled(False)

with torch.inference_mode():
    for i in range(20):
        out = pipe(
            "a realistic photo",
            num_inference_steps=25,
            height=768,
            width=512,
        )
        img = out.images[0]
        img.save(f"out_{i}.png")

        # 긴 세션에서 단편화가 의심되면 주기적으로 정리
        if i % 5 == 0:
            torch.cuda.empty_cache()

관측 팁

  • nvidia-smi에서 프로세스 VRAM이 계속 우상향이면, 코드가 텐서를 잡고 있을 확률이 큽니다.
  • 단편화는 empty_cache()가 만능은 아니지만, 장시간 배치 작업에서는 체감 효과가 있는 편입니다.

OOM 진단 체크리스트: 어디서 터지는지 먼저 고정하자

최적화를 적용하기 전에 “터지는 지점”을 고정하면 시행착오가 줄어듭니다.

  • 해상도만 올릴 때 터지나, ControlNet 추가에서 터지나
  • num_images_per_prompt2로 바꾸는 순간 터지나
  • 업스케일(또는 VAE decode) 단계에서만 터지나
  • 여러 번 반복 실행 시점에만 터지나

이런 식의 분해는 웹/앱에서 리소스 문제를 쪼개는 방식과 동일합니다. 브라우저/그래픽 컨텍스트도 리소스가 부족하면 갑자기 죽는데, 그때도 “어느 순간 컨텍스트가 날아갔는지”를 좁히는 게 핵심입니다. 관련해서는 Safari iOS WebGL 컨텍스트 손실 해결법처럼 관측 지점을 잡는 접근이 참고가 됩니다.

조합 추천: VRAM 용량별 현실적인 프리셋

아래는 “가장 자주 성공하는 조합” 위주로 묶은 가이드입니다.

6GB~8GB

  • torch_dtype=torch.float16
  • xFormers 또는 SDPA
  • enable_sequential_cpu_offload()
  • enable_vae_tiling()
  • 해상도는 512x512 또는 768x512부터

10GB~12GB

  • FP16 + xFormers
  • VAE tiling은 고해상도에서만
  • 배치는 1 유지, 대신 반복 실행

16GB 이상

  • FP16/BF16 + SDPA
  • 1024x1024도 가능하지만 ControlNet 다중, 큰 배치, 업스케일을 동시에 하면 여전히 OOM 가능

마무리

Stable Diffusion의 VRAM OOM은 “GPU가 약해서”라기보다, 피크 메모리를 만드는 원인을 모르고 옵션을 쌓아 올릴 때 자주 발생합니다.

정리하면 다음 순서가 가장 효율적입니다.

  1. FP16 또는 BF16로 내리기
  2. attention 최적화(xFormers/SDPA) 적용
  3. VAE slicing/tiling로 고해상도 피크 제거
  4. 그래도 부족하면 CPU offload로 생존 확보
  5. 마지막에 해상도·배치·ControlNet·LoRA를 하나씩 올리며 피크를 관리

이 흐름대로 적용하면, 같은 GPU에서도 “이전엔 불가능했던” 해상도와 워크로드를 안정적으로 소화할 수 있습니다.