- Published on
Stable Diffusion ControlNet 메모리 폭발 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
Stable Diffusion에 ControlNet을 붙이는 순간, 같은 프롬프트·같은 샘플러인데도 VRAM 사용량이 급격히 치솟아 CUDA out of memory가 터지는 경우가 많습니다. 특히 A1111(WebUI)에서 여러 ControlNet을 켜거나, ComfyUI에서 고해상도 워크플로우를 구성하거나, img2img/inpaint에 ControlNet을 얹을 때 “메모리 폭발”처럼 보이는 증상이 자주 발생합니다.
이 글은 ControlNet이 왜 메모리를 더 먹는지(구조적 원인)부터, 어떤 설정이 VRAM을 선형/비선형으로 증가시키는지, 그리고 운영 환경에서 재현 가능한 해결책(우선순위 체크리스트 + 코드 예제)까지 한 번에 정리합니다.
또한 로컬 LLM에서 OOM을 다루는 방식과 유사한 진단 프레임을 참고하면 도움이 됩니다. VRAM OOM의 공통 패턴은 아래 글에서도 잘 정리되어 있으니 함께 읽어보면 좋습니다.
ControlNet이 메모리를 폭발시키는 구조적 이유
ControlNet은 “추가 네트워크”를 하나 더 얹는 게 아니라, U-Net의 여러 블록에 조건(컨디션) 특징을 주입하기 위해 별도의 컨볼루션 경로를 타고 중간 특징맵(feature map)을 생성합니다. 이 과정에서 VRAM이 늘어나는 핵심은 다음 3가지입니다.
1) 중간 특징맵이 해상도에 비례해 커진다
U-Net은 다양한 해상도의 feature map을 들고 이동합니다. ControlNet은 입력 컨디션(예: canny, depth, openpose)을 받아서 U-Net의 블록에 합쳐질 텐서를 추가로 만듭니다. 이 텐서들의 공간 크기는 latent 해상도에 비례합니다.
- 픽셀 해상도
W x H를 키우면 latent는 대략W/8 x H/8 - 하지만 U-Net 내부는 채널 수가 커서, feature map 메모리는 단순히 latent 크기만큼만 늘지 않습니다
즉, “해상도 조금 올렸을 뿐인데” VRAM이 훅 늘어나는 이유가 여기에 있습니다.
2) ControlNet 개수만큼 경로가 늘어난다
ControlNet을 2개, 3개 켜면 각 ControlNet이 생성하는 조건 특징맵이 추가됩니다. 구현/옵션에 따라 공유되는 부분이 거의 없어서, 대체로 VRAM은 ControlNet 개수에 가깝게 증가합니다.
3) dtype과 attention 구현에 따라 피크 메모리가 달라진다
같은 모델이라도 아래 조건에서 피크 VRAM이 크게 달라집니다.
float32vsfloat16vsbfloat16- xFormers/SDPA(Scaled Dot Product Attention) 적용 여부
- VAE 디코딩을 어디서 하느냐(GPU vs CPU)
- KSampler/스케줄러가 step마다 어떤 텐서를 유지하느냐
먼저 확인할 “OOM의 종류”: VRAM 부족 vs 메모리 누수
ControlNet OOM은 대부분 “정상적인 피크 메모리 초과”입니다. 하지만 다음 증상이라면 메모리 누수/캐시 누적도 의심해야 합니다.
- 같은 설정으로 여러 번 돌릴수록 VRAM이 점점 증가
- 첫 실행은 되는데, 두 번째부터 OOM
- 작업이 끝났는데도 VRAM이 해제되지 않음
PyTorch는 캐싱 allocator를 쓰기 때문에, “해제되지 않은 것처럼 보이는” 현상이 있을 수 있습니다. 다만 진짜 누수라면 반복 실행 시 allocated가 계속 증가합니다.
아래 코드는 VRAM 상태를 로그로 남겨 누수인지 피크인지 구분하는 데 도움이 됩니다.
import torch
def vram(tag=""):
if not torch.cuda.is_available():
print("CUDA not available")
return
alloc = torch.cuda.memory_allocated() / 1024**2
reserv = torch.cuda.memory_reserved() / 1024**2
peak = torch.cuda.max_memory_allocated() / 1024**2
print(f"[{tag}] allocated={alloc:.1f}MB reserved={reserv:.1f}MB peak={peak:.1f}MB")
torch.cuda.reset_peak_memory_stats()
vram("start")
# ... inference ...
vram("after")
allocated가 스텝마다 증가하고 내려오지 않으면 누수 가능성peak만 높고allocated는 안정적이면 “피크 초과”일 확률이 큽니다
해결 우선순위 체크리스트 (효과 큰 순)
여기부터는 “VRAM이 터진다”를 빠르게 해결하는 실전 순서입니다.
1) 해상도부터 낮춰서 원인 분리
ControlNet OOM의 1순위 레버는 해상도입니다.
1024 x 1024에서 터진다면768 x 768또는832 x 832로 먼저 내리고 재현성 확인img2img/inpaint는 원본이 큰 경우가 많으니, 입력 이미지를 먼저 리사이즈하고 진행
특히 ControlNet은 컨디션 입력도 같이 처리하므로, 컨디션 맵 해상도가 불필요하게 큰 경우가 많습니다.
2) ControlNet 개수 줄이기 + 강도/가중치 재조정
ControlNet을 여러 개 쓰는 워크플로우라면, 먼저 1개만 켠 상태에서 목표 품질을 최대한 끌어올린 뒤 2개째를 추가하는 방식이 안전합니다.
- OpenPose + Depth + Canny를 동시에 켠다면, 우선 OpenPose만으로 구도 고정
- 그 다음 Depth를 추가하되 weight를 낮추고 step 범위를 제한
“몇 개를 켜느냐”가 VRAM에 거의 직결됩니다.
3) dtype을 fp16 또는 bf16로 강제
가능하면 추론(inference)은 float16 계열로 고정하세요.
- NVIDIA RTX 계열:
fp16권장 - Ampere 이후는
bf16도 안정적(환경에 따라)
Diffusers 예시:
import torch
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel
controlnet = ControlNetModel.from_pretrained(
"lllyasviel/control_v11p_sd15_canny",
torch_dtype=torch.float16,
)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
controlnet=controlnet,
torch_dtype=torch.float16,
safety_checker=None,
)
pipe = pipe.to("cuda")
만약 일부가 float32로 남아 있으면, 그 지점에서 VRAM이 튈 수 있습니다.
4) Attention 최적화: xFormers 또는 SDPA 적용
Attention 구현은 피크 VRAM을 크게 좌우합니다.
Diffusers에서는 보통 아래 중 하나를 씁니다.
# 1) xFormers (설치되어 있을 때)
pipe.enable_xformers_memory_efficient_attention()
# 2) PyTorch SDPA 경로(환경에 따라 자동/수동)
pipe.enable_attention_slicing("auto")
- xFormers는 대체로 VRAM 절감 효과가 크지만, 환경에 따라 설치/호환 이슈가 있습니다.
attention_slicing은 속도는 느려지지만 VRAM을 줄이는 데 확실히 도움됩니다.
5) VAE를 CPU로 오프로딩 또는 타일링
고해상도에서 VAE 디코딩이 의외로 VRAM을 크게 잡아먹습니다.
Diffusers에서 가능한 옵션들:
# VRAM 절약: 모델 일부를 CPU로 오프로딩
pipe.enable_model_cpu_offload()
# 또는 VAE 타일링(고해상도 디코딩 시 유용)
pipe.enable_vae_tiling()
# 메모리 더 줄이기(품질/속도 영향 가능)
pipe.enable_vae_slicing()
- GPU VRAM이 작은 경우
cpu_offload는 사실상 필수 카드가 됩니다. - 다만 속도는 느려지고, CPU RAM 및 PCIe 전송 비용이 증가합니다.
6) 배치/동시성 줄이기: batch_size=1 고정부터
A1111/ComfyUI에서 “한 번에 여러 장” 뽑는 설정은 VRAM을 직격합니다.
- batch size를 1로 고정하고, batch count로 반복 생성
- 서버로 운영한다면 동시 요청 수를 제한(큐잉)
동시성 제어는 커넥션 풀 고갈과도 비슷한 성격이 있습니다. 리소스가 제한된 환경에서 무제한 동시 요청을 받으면 결국 터집니다.
7) ControlNet 전처리 해상도/옵션 점검
ControlNet 전처리기는 입력을 특정 해상도로 리사이즈하거나, 내부적으로 큰 텐서를 만들 수 있습니다.
- 전처리 해상도를 생성 해상도와 “같게” 두는 것이 항상 최선은 아닙니다
- 구도 고정 목적이면 컨디션 맵을 더 낮은 해상도로 만들어도 충분한 경우가 많습니다
예를 들어 Canny는 디테일이 중요하지만, 포즈/세그먼트는 상대적으로 낮춰도 형태가 유지됩니다.
8) Step 수/스케줄러/CFG가 피크에 미치는 영향 이해
일반적으로 step 수 자체는 “피크 VRAM”보다는 “총 시간”에 더 영향을 줍니다. 하지만 구현에 따라 step별로 유지되는 버퍼가 다르면 메모리 패턴이 달라질 수 있습니다.
- CFG scale이 높다고 VRAM이 선형 증가하진 않지만, 일부 파이프라인은 내부적으로 복제 경로를 만들 수 있습니다
- 고급 옵션(예: refiner, hires fix, 2-pass)은 사실상 2번 돌리는 것과 비슷해져 피크가 올라갈 수 있습니다
Diffusers 기준: “안 터지는” ControlNet 파이프라인 템플릿
아래 예시는 VRAM이 작은 환경에서도 최대한 버티도록 옵션을 묶은 템플릿입니다.
import torch
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel
from diffusers.utils import load_image
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float16 if device == "cuda" else torch.float32
controlnet = ControlNetModel.from_pretrained(
"lllyasviel/control_v11p_sd15_openpose",
torch_dtype=dtype,
)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
controlnet=controlnet,
torch_dtype=dtype,
safety_checker=None,
)
# VRAM 최적화 옵션
if device == "cuda":
pipe.enable_attention_slicing("auto")
try:
pipe.enable_xformers_memory_efficient_attention()
except Exception:
pass
# VRAM이 특히 빡빡하면 CPU 오프로딩
pipe.enable_model_cpu_offload()
# VAE 메모리 최적화
pipe.enable_vae_tiling()
pipe.enable_vae_slicing()
prompt = "a portrait photo, dramatic lighting, ultra detailed"
# 컨디션 이미지(예: openpose 결과)를 준비했다고 가정
cond = load_image("pose.png")
out = pipe(
prompt=prompt,
image=cond,
num_inference_steps=25,
guidance_scale=6.5,
width=768,
height=768,
)
out.images[0].save("result.png")
포인트는 다음입니다.
- dtype을 먼저 고정
- attention 최적화 적용
- 정말 필요하면
cpu_offload - VAE 타일링/슬라이싱으로 디코딩 메모리 압박 완화
- 해상도는 보수적으로 시작
A1111(WebUI)에서 자주 먹히는 처방
A1111은 확장/옵션 조합에 따라 차이가 있지만, ControlNet OOM에서 반복적으로 효과가 큰 조합은 아래입니다.
1) --medvram 또는 --lowvram (최후의 보루)
- VRAM이 6GB 이하라면
--medvram이 현실적인 경우가 많습니다. --lowvram은 더 강하지만 속도가 크게 느려질 수 있습니다.
2) xFormers 활성화
- 설정에서 xFormers를 켜고, 드라이버/파이토치 조합이 맞는지 확인
3) Hires fix를 ControlNet과 동시에 쓰지 말고 2-pass로 분리
Hires fix는 내부적으로 업스케일 + 추가 디노이징 패스를 수행합니다. ControlNet까지 같이 얹으면 피크가 크게 증가합니다.
- 1차: ControlNet으로 구도/포즈 고정, 낮은 해상도
- 2차:
img2img업스케일(필요하면 다른 가벼운 컨디션만 사용)
ComfyUI에서의 메모리 폭발 포인트와 회피
ComfyUI는 노드 그래프 특성상 “같은 텐서를 여러 노드가 잡고 있어서 해제가 늦는” 상황이 발생할 수 있습니다.
- 불필요한 프리뷰/중간 저장 노드를 제거
- 업스케일/리파이너/후처리를 한 그래프에 모두 넣기보다, 단계별로 분리 실행
- 여러 ControlNet을 한 번에 걸기보다, 가장 중요한 1개를 먼저 확정
또한 ComfyUI는 실행 순서에 따라 피크가 달라질 수 있으니, 그래프를 단순화한 최소 재현 버전으로 먼저 안정화하는 것이 좋습니다.
“메모리 폭발”을 예방하는 운영 패턴
로컬에서야 한 번 OOM 나면 다시 실행하면 되지만, API 서버로 운영하면 OOM은 곧 장애입니다. 아래 패턴을 권장합니다.
1) 동시성 제한 + 큐잉
- GPU 1장당 동시 실행 1개를 원칙으로 두고, 요청은 큐로 대기
- 작업이 길어질수록 타임아웃/취소 처리 필요
2) 입력 파라미터에 가드레일
- 최대 해상도 제한
- ControlNet 최대 개수 제한
- batch size 강제 1
3) OOM 발생 시 프로세스 재기동 전략
PyTorch가 OOM을 겪은 뒤 allocator 상태가 꼬여 다음 요청까지 불안정해지는 경우가 있어, 운영에서는 “OOM = 워커 재기동”이 더 안전할 때가 많습니다.
자주 묻는 질문
Q1. VRAM 12GB인데도 ControlNet 2개 켜면 터집니다
가능성이 큰 순서:
- 해상도가 높음(특히
1024계열) - Hires fix 또는 2-pass가 한 번에 묶여 있음
- VAE가 GPU에 남아 디코딩 피크가 큼
- attention 최적화 미적용
위 체크리스트대로 768로 내리고, attention_slicing과 VAE 타일링부터 적용해 보세요.
Q2. 해상도는 낮은데 두 번째 실행부터 OOM입니다
- 캐시/텐서가 그래프 어딘가에서 참조를 유지하는지 확인
- 반복 실행 시
allocated가 증가하는지 로그로 확인 - 워크플로우에서 중간 결과를 저장/프리뷰하는 노드가 텐서를 붙잡고 있지 않은지 점검
결론
ControlNet의 메모리 폭발은 “ControlNet이 무겁다”라는 한 문장으로 끝낼 문제가 아니라, 해상도·ControlNet 개수·dtype·attention 구현·VAE 디코딩 위치가 합쳐져 피크 VRAM을 밀어 올리는 구조적 결과입니다.
가장 빠른 해결 루트는 다음 한 줄로 요약됩니다.
- 해상도 낮추기 → ControlNet 개수 줄이기 →
fp16고정 → xFormers/SDPA/슬라이싱 → VAE 타일링/CPU 오프로딩 → 동시성/배치 제한
위 순서대로 적용하면, “어떤 조합에서 터지는지”가 분리되어 재현 가능하게 안정화할 수 있고, 운영 환경에서도 OOM을 장애로 키우지 않게 만들 수 있습니다.