서론
소상공인을 위한 AI 상세페이지 자동 생성 서비스 Ad-Lib을 개발하면서, GCP L4 GPU(22GB VRAM) 위에 SDXL 기반 이미지 생성 파이프라인을 올리게 됐다. "모델 로드하고 프롬프트 넣으면 끝 아닌가?" 싶었지만, 실전은 전혀 달랐다. 이 글에서는 SDXL을 실서비스 수준으로 운영하면서 마주친 문제들과 해결 과정을 기록한다.
1. 프로젝트 배경
Ad-Lib은 GPT와 SDXL을 조합한 멀티 에이전트 시스템이다. 사용자가 브랜드명, 제품명, 특징만 입력하면:
Brand Card → Director(기획) → Editor(HTML) → Photographer(SDXL) → Publisher(렌더링)
이 순서로 4개의 에이전트가 협업하여 완성된 상세페이지 이미지를 출력한다. 이 중 Photographer 에이전트가 SDXL로 제품 이미지를 생성하는 역할이다.
환경은 이렇다:
- GPU: GCP L4 (VRAM 22GB)
- 모델: Stable Diffusion XL Base 1.0
- 추가 모듈: IP-Adapter (제품 참조 이미지 반영)
- 프레임워크: HuggingFace Diffusers
2. 첫 번째 벽: 기본 VAE의 OOM
SDXL을 float16으로 로드하면 대략 6~7GB. 여유로워 보였다. 그런데 이미지 생성 단계에서 바로 CUDA Out of Memory.
원인은 SDXL의 기본 VAE가 디코딩 시 float32로 업캐스팅하기 때문이었다. 생성된 latent를 이미지로 변환하는 순간 VRAM 사용량이 2배로 뛴다.
해결: madebyollin/sdxl-vae-fp16-fix VAE로 교체
from diffusers import AutoencoderKL
vae = AutoencoderKL.from_pretrained(
"madebyollin/sdxl-vae-fp16-fix",
torch_dtype=torch.float16,
)
pipe = StableDiffusionXLPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
vae=vae,
torch_dtype=torch.float16,
)
이 VAE는 fp16 상태에서도 안정적으로 디코딩된다. 교체 후 단독 SDXL 생성은 OOM 없이 동작했다.
3. 두 번째 벽: CLIP 77 토큰 제한
에이전트가 GPT로 생성한 이미지 프롬프트에 mood 키워드, 스타일 지시어를 모두 붙이니 173 토큰이 됐다. SDXL의 텍스트 인코더(CLIP)는 최대 77 토큰만 받는다. 초과분은 경고 없이 잘린다.
원본: "fresh watermelon on wooden table, summer, watermelon, fresh,
green, red, juicy, sunny, warm natural sunlight, photorealistic,
DSLR, 8k, ultra detailed, commercial photography..." → 173 토큰
프롬프트의 뒷부분, 즉 품질 관련 지시어가 모두 잘려나가면서 이미지 퀄리티가 떨어졌다.
해결: mood에서 앞 3개 키워드만 사용, suffix를 최소화
mood_short = ", ".join(mood.split(",")[:3]).strip()
full_prompt = f"{prompt}, {mood_short}, photorealistic, DSLR, 8k"
"프롬프트는 길수록 좋다"는 건 토큰 제한이 없는 LLM 이야기다. Diffusion 모델에서는 핵심 키워드만 간결하게 넣는 게 결과가 더 좋았다.
4. 세 번째 벽: IP-Adapter 로드 후 일반 생성 불가
사용자가 제품 참조 이미지를 보내면 IP-Adapter로 외형을 참조하고, 없으면 프롬프트만으로 일반 생성을 한다. 문제는 IP-Adapter를 한 번이라도 로드하면, 이후 일반 생성이 깨진다는 것이었다.
RuntimeError: ...encoder_hid_dim_type is set to 'ip_image_proj'
but requires keyword argument `image_embeds`
IP-Adapter를 로드하면 UNet 내부에 cross-attention 레이어가 추가되면서, 이후 모든 추론에서 image_embeds를 요구하게 된다. 모델을 다시 로드하지 않고는 원래 상태로 돌아갈 수 없다.
해결: 일반 생성 시 scale=0 + 더미 이미지를 넣어서 IP-Adapter를 "무력화"
if self._ip_adapter_loaded:
self.pipe.set_ip_adapter_scale(0.0) # 영향력 0
dummy = Image.new("RGB", (224, 224), (0, 0, 0)) # 검은 이미지
gen_kwargs["ip_adapter_image"] = dummy
scale이 0이면 IP-Adapter의 출력이 무시되므로, 실질적으로 일반 생성과 동일한 결과가 나온다. UNet 구조를 건드리지 않으면서 우회하는 방법이다.
5. 네 번째 벽: IP-Adapter + 고해상도 = OOM
SDXL 단독은 괜찮은데, IP-Adapter까지 올리면 22GB를 초과한다. 특히 1344×768 같은 와이드 해상도에서 즉사했다.
IP-Adapter는 이미지 인코더(CLIP ViT)를 추가로 GPU에 올려야 하고, cross-attention 연산도 늘어나서 VRAM 사용량이 크게 증가한다.
해결: IP-Adapter 사용 섹션은 해상도 강제 제한
# 히어로/인트로/비주얼 섹션만 IP-Adapter 적용
IP_ADAPTER_SECTIONS = {"hero", "intro", "visual"}
# IP-Adapter 사용 시 1024x1024 고정
if use_ref:
sdxl_w, sdxl_h = 1024, 1024
추가로 매 생성 직전에 캐시를 정리하고:
if self.device == "cuda":
torch.cuda.empty_cache()
이전 실행의 파이썬 프로세스가 GPU 메모리를 잡고 있는 경우도 잦았다. GCP에서는 실행 전 nvidia-smi로 확인하고 kill -9로 정리하는 습관이 필요했다.
6. ControlNet Canny — 써보고 뺀 이유
처음에는 제품 사진의 형태를 보존하기 위해 ControlNet Canny도 함께 사용했다. Canny 에지 맵으로 제품의 윤곽을 추출하고, 그 위에 새로운 배경을 입히는 방식이었다.
결과는 기술적으로는 성공이었지만, 실용적으로는 실패였다:
- 같은 앵글, 같은 구도가 그대로 유지됨 → 4장 생성해도 전부 비슷
- 원했던 건 "다양한 구도의 제품 사진"이지, "배경만 바뀐 같은 사진"이 아니었음
ControlNet을 제거하고 IP-Adapter 단독으로 전환하니, 프롬프트로 앵글을 자유롭게 지정할 수 있게 됐다:
STYLE_PRESETS = {
"top": "overhead top-down flat lay shot...",
"closeup": "extreme close-up macro shot...",
"hero": "hero shot at 45 degree angle...",
"lifestyle": "lifestyle photo in cozy home setting...",
}
"기술적으로 동작한다"와 "제품에 맞는다"는 다른 문제라는 걸 배운 경험이다.
7. SDXL의 한계를 인정하고 타겟 좁히기
SDXL로 다양한 제품을 테스트하면서 명확한 강약점이 드러났다:
잘 되는 것 안 되는 것
| 과일, 채소 (자연물) | 패키지 제품 (로고/텍스트) |
| 생선, 해산물 (원물) | 브랜드 라벨이 있는 상품 |
| 꽃, 식물 | 전자기기, 정밀한 제품 |
SDXL은 텍스트를 렌더링하지 못한다. 상표나 로고가 있는 제품은 글자가 깨져서 사용할 수 없었다. 이 한계를 인정하고 타겟을 농산물·수산물로 집중하는 결정을 내렸다. "모든 카테고리를 지원하겠다"에서 "잘하는 걸 제대로 하자"로 전환한 것이다.
정리: 실전에서 배운 것들
문제 해결 교훈
| 기본 VAE OOM | fp16-fix VAE 교체 | 모델 기본값을 맹신하지 말 것 |
| CLIP 77토큰 잘림 | 프롬프트 간결화 | Diffusion과 LLM의 토큰 전략은 다르다 |
| IP-Adapter 후 일반 생성 불가 | dummy image + scale=0 | 모델 상태 변화를 추적해야 한다 |
| IP-Adapter + 고해상도 OOM | 해상도 1024 고정 + 캐시 정리 | GPU 메모리는 항상 부족하다 |
| ControlNet 구도 고정 | ControlNet 제거 | 기술적 성공 ≠ 제품적 성공 |
| SDXL 텍스트 렌더링 불가 | 카테고리 한정 (농산물/수산물) | 모델의 한계를 인정하고 강점에 집중 |
GPU 22GB면 넉넉해 보이지만, 실전에서 여러 모듈을 조합하면 금방 한계에 부딪힌다. 중요한 건 한계를 우회하는 엔지니어링과 한계를 인정하는 제품 판단 두 가지다.
'AI 엔지니어준비' 카테고리의 다른 글
| 자주 사용하는 Python 패턴 (0) | 2026.02.19 |
|---|---|
| (CUDA OutOfMemoryError) L4 GPU OOM 오류 대처 방법 (0) | 2026.02.14 |
| gcp git 설정 매번 아이디 패스워드 치기 귀찮을때 (0) | 2026.02.09 |
| gcloud ssh timeout 뜰때! (0) | 2026.02.09 |
| 클라우드 모델 캐시 관리 팁! GCP (1) | 2026.02.06 |