Published on

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

Authors
Binance registration banner

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가지를 기준으로 “최소 변경으로 통과하는 설정”을 역으로 조합해 드릴 수 있습니다.