Published on

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

Authors

Stable Diffusion을 로컬 GPU에서 돌리다 보면 가장 흔하게 만나는 에러가 CUDA out of memory(VRAM OOM)입니다. 특히 txt2img는 괜찮다가 img2img, Hi-Res fix, ControlNet, AnimateDiff 같은 기능을 켜는 순간 VRAM이 급격히 튀면서 터집니다.

이 글은 “왜 OOM이 나는지”를 감으로 때우지 않고, VRAM을 실제로 줄이는 6가지 최적화를 우선순위대로 정리합니다. WebUI(AUTOMATIC1111) 기준 팁도 포함하지만, 핵심은 PyTorch/Diffusers에서도 그대로 적용됩니다.


OOM을 유발하는 대표 원인(먼저 짚고 가기)

Stable Diffusion에서 VRAM을 크게 먹는 축은 대략 다음입니다.

  • 해상도: H x W가 커질수록 U-Net의 feature map이 기하급수적으로 커짐
  • 배치 크기: 한 번에 여러 장 생성하면 그만큼 activation/latent가 증가
  • 샘플러 steps: steps 자체가 메모리를 선형으로 늘리진 않지만, 일부 구현/옵션(예: 중간 결과 저장, 고급 옵션)과 결합되면 피크가 상승
  • 부가 모델: ControlNet, LoRA 다중 적용, T2I-Adapter, IP-Adapter 등 추가 네트워크
  • 정밀도/어텐션 구현: FP32, 기본 attention은 VRAM을 많이 사용

진단의 첫 단계는 “피크 메모리가 언제 치솟는지”를 확인하는 것입니다.

import torch

def report(tag: str):
    if not torch.cuda.is_available():
        print("CUDA not available")
        return
    torch.cuda.synchronize()
    alloc = torch.cuda.memory_allocated() / 1024**2
    reserved = torch.cuda.memory_reserved() / 1024**2
    peak = torch.cuda.max_memory_allocated() / 1024**2
    print(f"[{tag}] allocated={alloc:.1f}MB reserved={reserved:.1f}MB peak={peak:.1f}MB")

# 사용 예: 파이프라인 로드 전/후, generate 전/후에 report("...") 호출

이제부터는 “OOM을 실제로 없애는” 방법을 6개로 나눠 설명합니다.


1) 해상도, 배치, Hi-Res fix를 먼저 줄여라(가장 확실)

VRAM OOM은 대부분 입력 크기와 배치에서 결정됩니다. 가장 먼저 아래를 줄이면 즉시 효과가 납니다.

  • batch size1
  • width/height를 낮추고, 필요하면 업스케일은 후처리로
  • Hi-Res fix를 쓴다면
    • 1차 생성 해상도를 낮추고
    • denoising strength를 과하게 높이지 말고
    • 업스케일 배율을 무리하지 않기

Diffusers에서도 동일합니다.

import torch
from diffusers import StableDiffusionPipeline

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

# 배치 1, 해상도 512로 시작
image = pipe(
    prompt="a cinematic portrait",
    num_inference_steps=25,
    guidance_scale=7.5,
    height=512,
    width=512,
).images[0]

실무 팁:

  • VRAM이 8GB 이하라면 768 이상은 옵션을 줄이지 않으면 자주 터집니다.
  • ControlNet을 켠 상태에서 1024, Hi-Res fix까지 얹으면 12GB에서도 OOM이 날 수 있습니다.

2) FP16/BF16로 바꾸고, 불필요한 FP32 경로를 제거

가장 “가성비” 좋은 최적화는 반정밀도(half precision) 입니다.

  • NVIDIA RTX 계열: 보통 fp16이 안정적
  • Ampere 이상에서 BF16이 더 안정적인 경우도 있으나, 파이프라인/드라이버 조합에 따라 다름

Diffusers 예시:

import torch
from diffusers import StableDiffusionPipeline

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

pipe.enable_attention_slicing()  # 메모리 절약(속도는 일부 감소)

WebUI(AUTOMATIC1111)에서는 보통 실행 옵션에서 다음 계열을 확인합니다.

  • --precision full을 쓰고 있다면 중단(가능하면)
  • --no-half를 쓰고 있다면 OOM 가능성이 커짐
  • VAE만 FP32로 강제하는 옵션이 켜져 있으면 VRAM이 올라갈 수 있음

주의:

  • 일부 커스텀 VAE나 확장 기능은 half에서 NaN이 날 수 있습니다. 그 경우에만 예외적으로 해당 컴포넌트만 정밀도를 조정하세요.

3) xFormers 또는 SDPA로 어텐션 메모리를 줄이기

OOM의 주요 범인은 U-Net의 self-attention입니다. 여기서 메모리를 크게 줄이는 방법이 두 가지입니다.

  • xFormers 메모리 효율 어텐션
  • PyTorch 2.x의 scaled_dot_product_attention(SDPA)

Diffusers에서 xFormers:

pipe.enable_xformers_memory_efficient_attention()

PyTorch SDPA(환경에 따라 자동 적용되기도 함):

import torch

# PyTorch 2.x에서 backend 힌트를 줄 수 있음(환경에 따라 다름)
torch.backends.cuda.enable_flash_sdp(True)
torch.backends.cuda.enable_mem_efficient_sdp(True)
torch.backends.cuda.enable_math_sdp(False)

체감 효과:

  • 동일 해상도에서 “아슬아슬하게 터지던” 케이스가 통과하는 일이 많습니다.
  • 대신 일부 조합에서 속도/호환성 이슈가 있을 수 있으니, 문제가 생기면 SDPA만 또는 xFormers만 남기는 식으로 단순화하세요.

4) VAE 최적화: 타일링, 경량 VAE, VAE를 CPU로 오프로딩

VAE는 샘플링 전체에서 가장 큰 비중은 아니지만, 디코딩 순간 피크 VRAM을 만들 수 있습니다. 특히 고해상도에서 그렇습니다.

VAE 타일링

Diffusers는 VAE 타일링을 지원합니다.

pipe.enable_vae_tiling()  # 디코딩을 타일로 쪼개 VRAM 피크를 낮춤

VAE 스위칭

  • 일부 VAE는 품질은 좋지만 메모리가 더 들 수 있습니다.
  • VRAM이 빡빡하면 “기본 VAE”로 돌아가거나, 경량 VAE를 고려하세요.

CPU 오프로딩

VRAM이 정말 부족하면 일부를 CPU로 내리는 전략이 있습니다.

pipe.enable_model_cpu_offload()  # accelerate 필요

오프로딩은 대개 속도를 희생합니다. 하지만 “생성이 아예 안 되는 상태”라면 가장 현실적인 타협입니다.


5) 타일 기반 생성(고해상도 필수 전략): Tiled Diffusion/Latent 타일링

1024 이상 고해상도에서 OOM이 반복된다면, 근본적으로 “한 번에 큰 텐서를 들고 있는 구조”를 바꿔야 합니다. 이때 가장 강력한 방법이 타일 기반 생성입니다.

  • 이미지를 여러 타일로 쪼개서 생성하거나
  • latent 공간에서 타일로 처리해 피크 VRAM을 제한

WebUI에서는 확장(예: 타일 기반 diffusion/업스케일 계열)을 통해 접근하는 경우가 많고, Diffusers에서도 비슷한 아이디어로 파이프라인을 구성할 수 있습니다.

타일링의 트레이드오프:

  • 경계(seam) 문제를 없애려면 overlap, blending이 필요
  • 속도는 느려질 수 있음
  • 하지만 VRAM 상한을 “타일 크기”로 고정할 수 있어, OOM 방지에 매우 효과적

6) 메모리 누수처럼 보이는 상황 정리: 캐시, 그래프, 확장 기능 정돈

“같은 설정인데 어느 순간부터만 OOM”이면, 실제로는 아래 중 하나일 가능성이 큽니다.

  • PyTorch CUDA 캐시가 예약(reserved) 상태로 남아 VRAM이 꽉 찬 것처럼 보임
  • 중간 텐서를 어딘가에 참조로 붙잡고 있어 GC가 안 됨
  • WebUI 확장 기능이 매 반복마다 텐서를 누적

Diffusers/PyTorch에서 최소한의 정리 루틴:

import gc
import torch

def cleanup_cuda():
    gc.collect()
    if torch.cuda.is_available():
        torch.cuda.empty_cache()
        torch.cuda.ipc_collect()

# 여러 번 생성 루프를 돌린다면, 작업 단위 끝날 때 cleanup_cuda() 호출

또한 torch.no_grad() 또는 torch.inference_mode()를 보장하세요.

with torch.inference_mode():
    image = pipe("a photo of a cat", num_inference_steps=20).images[0]

WebUI라면:

  • 확장 기능을 한 번에 많이 켜지 말고, OOM이 나는 시점에 켠 확장을 하나씩 꺼서 범인을 좁히기
  • ControlNet을 여러 개 동시 사용 시, 하나씩 줄여 VRAM 임계점을 찾기

OOM을 “진단 프로세스”로 접근하는 관점은 쿠버네티스에서 OOMKilled를 추적할 때와 유사합니다. 원인 분리와 재현이 핵심입니다. 같은 결로 참고할 만한 글로는 K8s CrashLoopBackOff - OOMKilled·Probe·Exit 137 진단이 있습니다.


추천 적용 순서(체크리스트)

아래 순서대로 적용하면 시행착오가 줄어듭니다.

  1. batch size=1, 해상도 512 또는 640으로 낮춰 “일단 성공” 만들기
  2. fp16 적용(가능하면), 불필요한 FP32 옵션 제거
  3. xFormers 또는 SDPA 적용
  4. VAE 타일링 적용, 필요 시 CPU 오프로딩
  5. 고해상도는 타일 기반 생성으로 전환
  6. 반복 생성/서빙이라면 캐시 정리 및 누수성 확장 제거

자주 묻는 질문

torch.cuda.empty_cache()만 하면 OOM이 해결되나요?

일시적으로는 도움이 되지만, 근본 해결은 아닙니다. 이미 필요한 피크 메모리가 VRAM을 초과한다면 캐시를 비워도 다시 터집니다. 캐시는 “예약(reserved)”을 줄여줄 뿐, 모델/텐서 자체 크기를 줄이진 않습니다.

VRAM이 12GB인데도 OOM이 납니다

가능합니다. 예를 들어 1024 해상도, Hi-Res fix, ControlNet 2개, 고 steps, 고정밀 옵션이 겹치면 12GB도 쉽게 넘습니다. 이 경우는 5번 타일링 전략이 가장 확실합니다.


마무리

Stable Diffusion VRAM OOM은 “GPU가 약해서”라기보다, 피크 메모리를 만드는 조합을 모르고 쌓아서 생기는 경우가 많습니다. 이 글의 6가지 최적화는 서로 중복되지 않게 다른 층(입력 크기, 정밀도, 어텐션, VAE, 타일링, 런타임 정리)을 공략합니다.

OOM이 나는 정확한 상황(사용 모델, VRAM 용량, 해상도, ControlNet/LoRA 개수, WebUI인지 Diffusers인지)을 알려주면, 위 6가지를 기준으로 “최소 변경으로 통과하는 설정”을 역으로 조합해 드릴 수 있습니다.