Published on

SDXL ControlNet 메모리 폭발, xFormers·VAE로 잡기

Authors

SDXL에 ControlNet(캐니, 뎁스, 오픈포즈 등)을 붙이면 “같은 1024 해상도인데도” VRAM이 갑자기 2배 이상 튀면서 OOM이 나는 경우가 흔합니다. 특히 A1111(WebUI), ComfyUI, diffusers 파이프라인 모두에서 증상이 비슷하게 나타나는데, 원인은 대체로 (1) SDXL의 큰 UNet + (2) ControlNet의 추가 네트워크/특징맵 + (3) VAE 디코딩/후처리 메모리가 한 번에 겹치기 때문입니다.

이 글에서는 SDXL ControlNet에서 발생하는 메모리 폭발을 xFormers(또는 PyTorch SDPA) 적용, VAE 선택/타일링, 해상도·배치·컨트롤 설정 튜닝으로 해결하는 방법을 실전 관점에서 정리합니다.

관련해서 Stable Diffusion 전반의 VRAM OOM 최적화는 아래 글도 함께 보면 좋습니다.

왜 SDXL + ControlNet에서 VRAM이 폭발하나

1) SDXL의 기본 메모리 풋프린트가 큼

SDXL은 1.5 계열 대비 UNet이 크고, 텍스트 인코더도 2개를 쓰는 구성(파이프라인에 따라 다름)이라 기본 메모리 요구량이 높습니다. 여기에 1024x1024 기본 해상도는 latent 공간에서도 feature map이 커져 attention 연산이 무거워집니다.

2) ControlNet은 “추가 UNet”에 가까운 비용

ControlNet은 조건 이미지를 인코딩하고 UNet 중간 블록에 주입하기 위해 별도의 컨볼루션 블록과 특징맵을 유지합니다. 즉, 단순히 입력 하나 더 넣는 정도가 아니라 중간 활성화(activation) 메모리가 늘어납니다.

특히 다음 상황에서 급증합니다.

  • ControlNet을 2개 이상 동시 사용(예: depth + canny)
  • 고해상도(특히 1024 이상) + 높은 step
  • guess mode 또는 컨트롤 강도가 높아 조건 영향이 커질 때(구현별 차이는 있지만, 종종 내부 경로가 더 무거워짐)

3) VAE 디코딩이 피크 메모리를 만든다

샘플링 자체는 latent 공간에서 돌지만, 마지막에 VAE로 RGB로 디코딩할 때 큰 텐서를 만들며 피크가 발생합니다. 여기에 업스케일, 하이레즈 픽스, 후처리가 이어지면 “샘플링은 되는데 저장/미리보기에서 터지는” 형태로 나타나기도 합니다.

1순위 처방: xFormers 또는 SDPA로 attention 메모리 줄이기

SDXL의 VRAM 병목은 대부분 attention에서 터집니다. 해결의 핵심은 메모리 효율적인 attention 커널을 쓰는 것입니다.

  • xFormers: 가장 흔한 선택지. 환경에 따라 설치/호환성이 까다로울 수 있음.
  • PyTorch SDPA: PyTorch 2.x에서 기본 제공. GPU/드라이버 조합에 따라 FlashAttention 계열 경로를 타면 효과가 큼.

diffusers에서 xFormers 활성화 예시

import torch
from diffusers import StableDiffusionXLPipeline, ControlNetModel

controlnet = ControlNetModel.from_pretrained(
    "diffusers/controlnet-canny-sdxl-1.0",
    torch_dtype=torch.float16,
)

pipe = StableDiffusionXLPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    controlnet=controlnet,
    torch_dtype=torch.float16,
).to("cuda")

# xFormers attention
pipe.enable_xformers_memory_efficient_attention()

# 추가로 VRAM 절약
pipe.enable_vae_slicing()

# 필요 시 CPU 오프로딩(속도는 느려질 수 있음)
# pipe.enable_model_cpu_offload()

자주 하는 실수

  • float32로 로드: SDXL + ControlNet에서 float32는 사실상 OOM을 부르는 지름길입니다. 가능하면 float16 또는 bfloat16을 우선 고려하세요.
  • xFormers를 켰는데도 OOM: ControlNet이 2개 이상이거나, 해상도/배치/하이레즈가 너무 높으면 여전히 터질 수 있습니다. 이 경우 아래 VAE/해상도/타일링을 함께 적용해야 합니다.

PyTorch SDPA 강제(환경에 따라)

PyTorch 2.x에서는 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)

VAE로 잡는 피크 메모리: 선택, 슬라이싱, 타일링

ControlNet을 쓰면 샘플링 중에도 빡빡하지만, “마지막 디코딩”에서 터지는 케이스가 꽤 많습니다. 이때는 VAE 쪽 최적화가 즉효입니다.

1) VAE slicing / tiling

  • slicing: 채널/배치 방향으로 쪼개서 디코딩
  • tiling: 공간 방향(타일)으로 쪼개서 디코딩

diffusers 예시:

# VAE 디코딩을 쪼개서 피크 VRAM 감소
pipe.enable_vae_slicing()

# 고해상도에서 특히 효과적
pipe.enable_vae_tiling()

주의: 타일링은 경계에서 미세한 아티팩트가 생길 수 있어, 결과물 품질을 확인하면서 사용하세요.

2) VAE 교체가 도움이 되는 경우

SDXL에서 특정 VAE가 더 무겁거나, 디코딩 품질/안정성이 다를 수 있습니다. “메모리 폭발” 관점에서는 VAE 자체의 파라미터 크기보다는 디코딩 시 중간 텐서 관리 방식후처리 파이프라인이 영향을 주는 편입니다.

실무적으로는 다음 방식이 안전합니다.

  • 우선 slicing/tiling으로 피크를 낮춘다
  • 그래도 터지면 해상도/배치/ControlNet 수를 줄인다
  • 마지막으로 VAE 교체를 시도한다(품질 변동이 있을 수 있음)

해상도·배치·스텝: “곱”으로 터진다

메모리는 보통 아래 요소가 함께 커질수록 급격히 증가합니다.

  • 해상도(특히 1024 기본에서 1280, 1536로 올릴 때)
  • 배치 크기(batch_size)
  • 동시 ControlNet 개수
  • 하이레즈 픽스(2-pass) 사용 여부

권장 안전 설정(출발점)

GPU VRAM이 8GB~12GB인 환경을 기준으로, 실패 확률을 낮추는 출발점을 제안합니다.

  • 해상도: 1024 유지(처음부터 1216 이상 올리지 않기)
  • 배치: 1
  • ControlNet: 1개부터 시작
  • steps: 20~30
  • precision: fp16
  • attention: xFormers 또는 SDPA
  • VAE: slicing + (필요 시) tiling

이 상태에서 안정적으로 돌아가면 ControlNet을 추가하거나 해상도를 올리는 식으로 점진적으로 확장하는 게 좋습니다.

ControlNet 설정이 VRAM에 주는 영향 포인트

구현체마다 차이는 있지만, 아래 항목은 메모리/속도에 간접적으로 영향을 줍니다.

1) ControlNet 해상도(전처리 해상도)

Canny/Depth/OpenPose 입력을 원본 1024 그대로 넣는 대신, 전처리 해상도를 낮춰 특징맵 비용을 줄일 수 있습니다. 예를 들어 1024 결과가 목표라도 컨트롤 입력은 768 또는 512로도 충분한 경우가 많습니다.

2) 여러 ControlNet을 “약하게” 섞기

ControlNet 2개를 강하게 거는 것보다, 한 개는 강하게 다른 하나는 약하게 두는 편이 결과/안정성 측면에서 나을 때가 있습니다. 메모리 자체는 “개수”가 더 크게 좌우하지만, 강도 조절로 재시도 횟수를 줄이는 것도 실전에서는 큰 최적화입니다.

3) Guess mode, guidance scale

guidance_scale을 과도하게 올리면 품질이 좋아지기보다 발산/과포화가 생겨 재시도가 늘 수 있습니다. 재시도는 곧 총 VRAM 사용시간 증가(열/스로틀링)로 이어져 체감상 더 불안정해집니다.

ComfyUI / A1111에서 바로 적용하는 체크리스트

도구별로 UI 옵션 이름이 다르지만, 핵심은 같습니다.

A1111(WebUI)

  • xFormers 활성화(설치 및 실행 옵션)
  • --medvram 또는 --lowvram은 최후의 수단(속도 크게 하락)
  • VAE 관련 옵션에서 타일/슬라이스(확장 기능 또는 설정) 사용 가능 여부 확인
  • ControlNet 2개 이상이면 우선 1개로 줄여 안정화 후 확장

ComfyUI

  • xFormers 또는 SDPA 경로 확인
  • VAE Decode 노드에서 타일 디코딩 옵션(확장/커스텀 노드 포함) 확인
  • KSampler에서 배치 1 유지, 해상도 증가 시 단계적으로

“샘플링은 되는데 저장에서 터짐” 디버깅 방법

증상이 아래처럼 나오면 VAE/후처리 피크를 의심하세요.

  • 샘플링 진행률은 100% 근처까지 가는데 마지막에 OOM
  • 미리보기 비활성화하면 성공률이 올라감

대응 순서:

  1. pipe.enable_vae_slicing() 적용
  2. pipe.enable_vae_tiling() 적용
  3. 미리보기/중간 디코딩 빈도 줄이기(도구 옵션)
  4. 최종 저장 포맷/후처리(업스케일, 얼굴복원) 끄고 원인 분리

최후의 수단: 오프로딩과 체크포인팅

1) CPU 오프로딩

VRAM이 절대적으로 부족하면 CPU로 일부 모듈을 내리는 방식이 있습니다. 속도는 느려지지만 “돌아가게” 만드는 데는 효과적입니다.

# diffusers
pipe.enable_model_cpu_offload()

2) Gradient checkpointing은 학습용

종종 “체크포인팅으로 VRAM 줄이기”가 언급되지만, 이는 주로 학습에서 activation을 재계산하는 기법입니다. 추론(inference)에서는 효과가 제한적이거나 적용 지점이 다르니, 추론 OOM 해결책으로는 xFormers/SDPA, VAE 타일링, 오프로딩이 우선입니다.

운영 관점 팁: OOM은 ‘원인’이 아니라 ‘결과’다

실제로는 설정 하나만 바꿔도 해결되는 경우가 많지만, 재현이 어려운 OOM은 운영에서 시간을 잡아먹습니다. 아래처럼 “원인 분리” 방식으로 접근하면 빠릅니다.

  • ControlNet 제거 후 SDXL만 돌려보기
  • ControlNet 1개만 붙여보기
  • 해상도만 1024로 고정
  • VAE slicing/tiling만 켜보기
  • 마지막으로 xFormers/SDPA 경로 확인

대규모 ETL에서 스트리밍으로 메모리 폭주를 막듯, 생성 파이프라인에서도 피크가 생기는 지점을 쪼개는 발상이 유효합니다.

결론: SDXL ControlNet OOM 해결 우선순위

SDXL + ControlNet에서 메모리 폭발을 가장 빠르게 잡는 우선순위는 아래 순서가 성공률이 높습니다.

  1. fp16(또는 bf16)로 로드하고 xFormers 또는 SDPA를 확실히 적용
  2. VAE slicing, 필요 시 VAE tiling으로 디코딩 피크를 낮추기
  3. 해상도 1024, 배치 1, ControlNet 1개로 안정화 후 점진 확장
  4. 그래도 부족하면 CPU 오프로딩으로 “일단 돌아가게” 만들기

한 번 안정화된 기준 설정을 만든 뒤, ControlNet 개수/해상도/하이레즈를 하나씩 올리면서 VRAM 사용량을 측정하면 재현 가능한 튜닝 루틴을 만들 수 있습니다. 이 루틴만 확보해도 “어제는 됐는데 오늘은 터짐” 같은 시행착오를 크게 줄일 수 있습니다.