- Published on
Stable Diffusion VRAM OOM - xFormers·SDPA 최적화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Stable Diffusion을 로컬 GPU에서 돌리다 보면 가장 흔한 실패가 CUDA out of memory 입니다. 같은 프롬프트와 같은 해상도인데도 어떤 날은 되고 어떤 날은 터지는 경우가 많고, 해결책으로 xFormers를 켜라, SDPA를 써라, --medvram을 켜라 같은 조언이 뒤섞여 있습니다.
이 글에서는 “왜 OOM이 나는지”를 VRAM 사용 구조로 쪼개서 설명하고, xFormers와 PyTorch SDPA(Scaled Dot-Product Attention) 를 중심으로 가장 재현성 있게 VRAM을 줄이는 방법을 정리합니다. 운영에서 장애를 원인-가설-검증으로 푸는 방식이 익숙하다면, 트래픽/리소스 병목을 진단하는 글들과 같은 관점으로 읽히도록 구성했습니다. (예: EKS conntrack 테이블 포화로 연결 끊김 해결법, Docker 빌드가 느릴 때 BuildKit 캐시 깨짐 복구)
VRAM OOM의 진짜 원인: 가중치가 아니라 activation과 attention
Stable Diffusion에서 VRAM을 크게 잡아먹는 건 단순히 모델 가중치만이 아닙니다.
- 가중치(weights): 체크포인트 로딩 시 고정 비용. FP16, BF16, FP32에 따라 대략 선형으로 변함.
- 중간 활성값(activations): UNet을 여러 step 동안 돌면서 레이어별 중간 텐서가 생김. 배치, 해상도, 채널 수에 따라 증가.
- Attention의
Q/K/V및 attention matrix: 해상도가 커질수록 토큰 수가 증가하고, 토큰 수가 늘면 attention 비용이 급격히 커짐.
특히 SD의 UNet 내부 attention은 해상도 증가에 매우 민감합니다. 이미지 해상도를 512x512에서 768x768로 올리는 건 픽셀 기준 2.25x지만, attention에서 다루는 토큰/feature map 차원에서의 증가가 겹치면 “체감 VRAM”은 그 이상으로 튀는 경우가 많습니다.
OOM이 들쭉날쭉한 이유: 캐시/fragmentation/그래프 경로
같은 설정인데도 OOM이 랜덤하게 발생한다면 보통 아래가 섞여 있습니다.
- CUDA 메모리 단편화(fragmentation): 큰 텐서를 할당하려는데 연속 공간이 부족해 실패
- 다른 프로세스의 VRAM 점유: 브라우저, 오버레이, 다른 파이썬 세션
- attention 구현 경로 차이: xFormers, SDPA, 기본 attention 중 무엇을 타는지에 따라 피크 메모리가 달라짐
따라서 “VRAM을 줄이는 기능”은 단순 옵션이 아니라, 피크 메모리(peak) 를 낮추는 쪽이 가장 효과적입니다.
xFormers vs SDPA: 무엇이 더 낫나
결론부터 정리하면 다음과 같습니다.
- PyTorch 2.x + CUDA 최신 조합이라면: 가능하면 SDPA가 우선 선택지
- 환경이 다양하고, 특정 UI/확장과 호환성 이슈가 있다면: xFormers가 여전히 강력한 대안
둘 다 목적은 같습니다.
- attention 계산에서 불필요한 중간 텐서 생성을 줄이고
- 더 메모리 효율적인 커널을 사용해서
- VRAM 피크를 낮춤
다만 구현/커널 경로가 다르고, GPU 아키텍처(예: Ampere, Ada, Hopper)와 PyTorch 버전에 따라 성능/안정성이 달라집니다.
SDPA(Scaled Dot-Product Attention) 개요
PyTorch의 torch.nn.functional.scaled_dot_product_attention은 내부에서 조건에 따라
- Flash Attention 계열
- Memory Efficient Attention
- Math fallback
중 하나를 선택합니다. 이 중 “메모리 효율 경로”를 잘 타면 VRAM이 크게 절약됩니다.
xFormers 개요
xFormers는 다양한 attention 커널을 제공하고, Stable Diffusion 생태계에서 오래 검증된 편입니다. 다만 설치가 까다롭거나(특히 Windows), PyTorch/CUDA 버전과의 매트릭스가 맞지 않으면 빌드/런타임 이슈가 날 수 있습니다.
내 환경에서 어떤 attention 경로를 타는지 확인하기
가장 먼저 할 일은 “지금 내 파이프라인이 무엇을 쓰는지”를 확인하는 것입니다.
PyTorch/CUDA 기본 정보 확인
python -c "import torch; print('torch', torch.__version__); print('cuda', torch.version.cuda); print('gpu', torch.cuda.get_device_name(0))"
SDPA 사용 가능 여부 확인(간단 체크)
import torch
import torch.nn.functional as F
q = torch.randn(1, 8, 1024, 64, device="cuda", dtype=torch.float16)
k = torch.randn(1, 8, 1024, 64, device="cuda", dtype=torch.float16)
v = torch.randn(1, 8, 1024, 64, device="cuda", dtype=torch.float16)
with torch.no_grad():
out = F.scaled_dot_product_attention(q, k, v)
print(out.shape)
이게 돌아간다고 해서 항상 최적 커널을 탄다는 의미는 아니지만, 최소한 SDPA 호출 경로가 동작하는지 확인할 수 있습니다.
Stable Diffusion에서 VRAM을 줄이는 실전 우선순위
여기서는 “체감 효과”가 큰 순서대로 정리합니다.
1) Attention 최적화: SDPA 또는 xFormers
diffusers 파이프라인에서 SDPA 켜기 예시
import torch
from diffusers import StableDiffusionPipeline
model_id = "runwayml/stable-diffusion-v1-5"
pipe = StableDiffusionPipeline.from_pretrained(
model_id,
torch_dtype=torch.float16,
).to("cuda")
# SDPA 기반 attention 최적화
pipe.enable_attention_slicing() # 추가적인 피크 감소(속도는 다소 손해)
prompt = "a photo of a cat, ultra detailed"
image = pipe(prompt, num_inference_steps=30).images[0]
image.save("out.png")
주의할 점:
enable_attention_slicing()은 VRAM을 줄이지만 속도를 희생합니다.- diffusers 버전에 따라 더 직접적인 SDPA 설정 API가 있을 수 있고, 내부에서 자동으로 SDPA를 타는 경우도 있습니다.
xFormers 활성화 예시(diffusers)
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()
image = pipe("a futuristic city", num_inference_steps=25).images[0]
image.save("xformers.png")
실무적으로는 SDPA와 xFormers를 동시에 켜서 이득을 보는 모델/버전도 있고, 반대로 충돌/비활성화되는 조합도 있습니다. 따라서 “내 환경에서 실제로 VRAM 피크가 줄었는지”를 계측으로 확인해야 합니다.
2) 정밀도: FP16 또는 BF16로 고정
가중치와 activation 모두에 영향을 줍니다.
- FP32로 돌아가면 VRAM이 즉시 터질 가능성이 커집니다.
- Ampere 이후 GPU는 BF16도 좋은 선택입니다(환경에 따라).
diffusers에서는 보통 torch_dtype=torch.float16로 시작하는 게 안전합니다.
3) 해상도/배치/스텝: “OOM 3대 변수”를 분리해서 조정
OOM을 잡을 때 한 번에 여러 변수를 바꾸면 원인 파악이 어렵습니다.
- 해상도:
512x512로 먼저 고정 - batch:
1고정 - steps:
20정도로 먼저 고정
그 다음 하나씩 올리면서 VRAM 피크를 확인합니다.
4) VAE 최적화: VAE를 타일링하거나 CPU 오프로딩
VAE 디코딩이 마지막에 VRAM을 크게 잡아먹는 경우가 있습니다.
diffusers에서는:
enable_vae_slicing()enable_vae_tiling()
같은 옵션이 VRAM 피크를 낮추는 데 도움이 됩니다.
pipe.enable_vae_slicing()
# 또는
pipe.enable_vae_tiling()
5) CPU 오프로딩: “느려도 된다면” 가장 강력한 안전장치
VRAM이 정말 빡빡한 환경(예: 6GB, 8GB)이라면 CPU 오프로딩이 사실상 보험입니다.
pipe.enable_model_cpu_offload()
대신 속도는 눈에 띄게 느려질 수 있습니다. 하지만 “아예 실패하는 것”을 “느리지만 성공”으로 바꿔줍니다.
6) PyTorch CUDA allocator 튜닝: 단편화 완화
랜덤 OOM이 단편화 성격이 강하다면 allocator 설정이 도움이 됩니다.
예시:
export PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128,garbage_collection_threshold:0.8"
max_split_size_mb를 낮추면 큰 블록 쪼개기가 달라져 단편화가 줄 수 있습니다.- 이 값은 만능이 아니고, 워크로드에 따라 오히려 악화될 수도 있으니 A/B로 확인하세요.
VRAM 계측: “줄었다”를 숫자로 확인하기
체감이 아니라 피크를 찍어봐야 합니다.
import torch
def report(tag: str):
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}] alloc={alloc:.1f}MB reserved={reserved:.1f}MB peak={peak:.1f}MB")
torch.cuda.reset_peak_memory_stats()
report("start")
# ... inference 실행 ...
report("after")
여기서 중요 포인트는:
alloc는 실제 사용 중인 메모리reserved는 allocator가 잡아둔 캐시(단편화/캐시 때문에 크게 보일 수 있음)peak가 최적화의 핵심 지표
OOM을 막는 데는 peak를 얼마나 내리느냐가 결정적입니다.
흔한 실패 패턴과 해결 체크리스트
1) xFormers 설치는 됐는데 효과가 없다
- 파이프라인이 실제로 xFormers attention을 호출하지 않을 수 있습니다.
- UI(예: web UI)에서 별도 플래그가 필요할 수 있습니다.
- PyTorch 버전이 바뀌면서 xFormers가 fallback 경로를 타는 경우도 있습니다.
해결 접근:
- 실행 로그에서 xFormers 활성화 메시지 확인
- VRAM
peak계측으로 전후 비교
2) SDPA를 켰는데 오히려 느리거나 OOM이 난다
- SDPA가 “메모리 효율 커널”이 아니라 “math fallback”을 타면 이득이 작습니다.
- 드라이버/CUDA/PyTorch 조합이 맞지 않으면 최적 경로가 비활성화됩니다.
해결 접근:
- PyTorch를 2.x 최신 안정 버전으로 올리고
- CUDA/드라이버를 맞춘 뒤
- 동일 프롬프트로
peak비교
3) 해상도만 올리면 바로 터진다
- attention 비용이 급증하는 전형적인 케이스
해결 접근:
- 먼저 SDPA 또는 xFormers 적용
enable_attention_slicing()추가- 그래도 부족하면 VAE tiling, CPU offload
운영 관점 팁: “리소스 한계”를 기능 플래그로 관리하기
Stable Diffusion을 서비스(서버)로 붙이는 경우, 사용자 입력으로 해상도/스텝이 올라가면서 OOM이 장애로 이어집니다. 이때는 애플리케이션 튜닝과 비슷하게 가드레일이 필요합니다.
- 입력 제한: 최대 해상도, 최대 steps, batch 강제
- 큐잉/동시성 제한: GPU당 동시 요청 수 제한
- 실패 시 폴백: OOM 발생 시 자동으로 해상도 다운 또는 CPU offload 재시도
이런 접근은 “장애가 나면 수동으로 옵션 바꾸기”가 아니라, 시스템적으로 재현 가능하게 만드는 방법입니다. 대규모 트래픽에서 리소스 고갈을 다루는 방식과 유사합니다. (참고: Spring Boot HikariCP 풀 고갈·DB 타임아웃 10분 진단)
추천 조합(빠르게 적용)
환경별로 “일단 이 조합부터”를 제안하면:
VRAM 8GB 이하
- FP16 고정
- SDPA 또는 xFormers 중 하나 확실히 활성화
enable_attention_slicing()- 필요 시
enable_vae_tiling() - 최후의 보루로
enable_model_cpu_offload()
VRAM 12GB 전후
- FP16 또는 BF16
- SDPA 우선(가능하면)
- 고해상도는 VAE tiling 병행
VRAM 24GB 이상
- SDPA로 속도/메모리 균형
- slicing은 보통 불필요(속도 손해가 더 큼)
마무리: OOM은 “옵션”이 아니라 “피크 메모리” 문제다
Stable Diffusion VRAM OOM을 안정적으로 줄이려면, 감으로 옵션을 켜는 게 아니라 다음 순서가 가장 확실합니다.
- 현재 attention 경로(xFormers/SDPA/기본) 확인
peakVRAM 계측으로 전후 비교- SDPA 또는 xFormers로 attention 피크부터 낮추기
- 부족하면 slicing, VAE tiling, CPU offload로 단계적 폴백
이 과정을 한 번 템플릿화해두면, 모델이 바뀌거나 해상도가 올라가도 “왜 터지는지”를 빠르게 재현하고 해결할 수 있습니다.