Latent Diffusion과 Stable Diffusion

512×512 이미지의 픽셀 공간(pixel space)에서 확산(diffusion)을 돌리는 것은 계산적으로 너무 비쌉니다. Rombach et al. (2022)는 이미지 하나를 생성하는 데 786k 차원(dimension) 전체가 필요한 것은 아니라고 보았습니다. 의미 구조(semantic structure)를 담을 만큼의 잠재 표현(latent)과 나머지를 복원할 별도의 디코더(decoder)만 있으면 충분합니다. 즉, VAE의 잠재 공간(latent space) 안에서 확산을 돌리는 것입니다. 이 한 가지 아이디어가 바로 Stable Diffusion입니다.

유형: Build 언어: Python 선수 지식: Phase 8 · 02 (Autoencoder와 VAE), Phase 8 · 06 (확산 모델 — DDPM 직접 구현), Phase 7 · 09 (비전 트랜스포머 (ViT)) 예상 시간: 약 75분

문제

512² 픽셀 공간에서 확산을 돌린다는 것은 U-Net이 [B, 3, 512, 512] 형태의 텐서(tensor)에서 동작해야 한다는 뜻입니다. 5억(500M) 파라미터(parameter)짜리 U-Net 기준으로 샘플링(sampling) 한 스텝(step)이 약 100 GFLOPS이고, 50 스텝이면 이미지 한 장에 5 TFLOPS가 필요합니다. 10억 장의 이미지로 학습한다고 가정하면 계산 비용은 터무니없이 커집니다.

이 연산량의 대부분은 지각적으로 중요하지 않은(perceptually unimportant) 세부, 다시 말해 손실 압축(lossy) VAE가 충분히 압축해도 무방한 고주파 텍스처(high-frequency texture)를 신경망 안에서 밀어 넣는 데 쓰입니다. Rombach의 아이디어는 VAE를 먼저 학습하고(이것이 1단계, first stage), 그 VAE를 동결(freeze)한 뒤, 4×64×64 잠재 공간(즉 second stage)에서만 확산을 돌리는 것입니다. U-Net 자체는 동일하지만 처리할 픽셀 수는 1/16에 불과하고, 비슷한 품질을 내는 데 필요한 연산량(FLOPs)은 대략 64배 줄어듭니다.

이것이 바로 Stable Diffusion 레시피(recipe)입니다. SD 1.x / 2.x는 64×64×4 잠재 위에서 860M U-Net을 사용했고, SDXL은 128×128×4 잠재 위에서 2.6B U-Net을 사용했습니다. SD3는 U-Net을 흐름 매칭(flow matching)을 사용하는 확산 트랜스포머(Diffusion Transformer; DiT)로 바꿨고, Flux.1-dev(Black Forest Labs, 2024)는 12B 파라미터 규모의 DiT-MMDiT를 배포했습니다. 이 모델들은 모두 동일한 2단계 기반(two-stage substrate) 위에 올라가 있습니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.Stable Diffusion이 512x512x3 픽셀 공간 대신 64x64x4 잠재 공간(latent space)에서 확산을 실행하는 이유는 무엇인가요?

2.Stable Diffusion에서 텍스트가 생성 이미지에 영향을 주는 방식은 무엇인가요?

0/2 답변 완료

개념

latent diffusion = VAE + diffusion, separately trained stage 1: train VAE (encoder + decoder), freeze image x 512 × 512 × 3 encoder E latent z 64 × 64 × 4 (1/16 of pixels) decoder D x̂ (reconstruction) L1 + LPIPS + GAN stage 2: train diffusion on z-space z_T ~ N(0, I) U-Net / DiT ε_θ(z_t, t, text_embed) iterate T->0 z_0 D(z_0) decoded image text encoder (CLIP / T5) -> cross-attention in each U-Net block same loss as pixel-space DDPM: L = E || ε - ε_θ(z_t, t, c) ||² ~64x fewer FLOPs than pixel diffusion for the same quality CFG: ε_cfg = (1+w) · ε_cond - w · ε_uncond (w ≈ 3-7)

두 단계를 따로 학습합니다.

  1. 1단계 — VAE. 인코더(encoder) E(x) → z와 디코더(decoder) D(z) → x로 구성합니다. 목표 압축률은 공간 축마다 8배씩 다운샘플(downsample)하고 채널(channel) 수를 조정해 전체 잠재 표현 크기를 픽셀 수의 약 1/16로 만드는 것입니다. 손실(loss)은 복원 항(reconstruction; L1 + LPIPS 지각 손실)과 KL 항을 더한 형태입니다. z를 정확한 가우시안(Gaussian)으로 만들 필요까지는 없으므로 KL 가중치는 작게 둡니다. 디코딩된 이미지가 선명(sharp)해지도록 적대적 손실(adversarial loss)을 함께 쓰는 경우가 많습니다.
  2. 2단계 — z 위의 확산. z = E(x_real)을 데이터로 취급합니다. U-Net 또는 DiT를 학습해 z_t로부터 잡음을 제거(denoise)합니다. 추론(inference) 시에는 확산으로 z_0를 표본 추출(sample)한 뒤 x = D(z_0)로 다시 디코딩합니다.

텍스트 조건화(Text conditioning). 두 가지 구성 요소가 더 추가됩니다. 하나는 동결된 텍스트 인코더(frozen text encoder)이고(SD 1.x는 CLIP-L, SD 2/XL은 CLIP-L+OpenCLIP-G, SD3와 Flux는 T5-XXL을 사용합니다), 다른 하나는 교차 어텐션 주입(cross-attention injection)입니다. U-Net 블록마다 [Q = 이미지 특징, K = V = 텍스트 토큰]으로 어텐션을 수행해 두 흐름을 섞습니다. 텍스트가 이미지에 영향을 주는 통로는 오직 이 토큰뿐입니다.

손실 함수(loss function)는 Lesson 06과 동일합니다. DDPM 또는 흐름 매칭에서 쓰는 잡음(noise)에 대한 MSE 그대로입니다. 단지 데이터 도메인이 픽셀에서 잠재로 바뀔 뿐입니다.

아키텍처 변형(Architecture variants)

모델연도백본(Backbone)잠재 형태텍스트 인코더파라미터
SD 1.52022U-Net64×64×4CLIP-L (77 토큰)860M
SD 2.12022U-Net64×64×4OpenCLIP-H865M
SDXL2023U-Net + 리파이너(refiner)128×128×4CLIP-L + OpenCLIP-G2.6B + 6.6B
SDXL-Turbo2023증류(Distilled)128×128×4동일1-4 스텝 샘플링
SD32024MMDiT (멀티모달 DiT)128×128×16T5-XXL + CLIP-L + CLIP-G2B / 8B
Flux.1-dev2024MMDiT128×128×16T5-XXL + CLIP-L12B
Flux.1-schnell2024MMDiT 증류128×128×16T5-XXL + CLIP-L12B, 1-4 스텝

전반적인 추세는 U-Net을 잠재 패치(latent patch) 위에서 동작하는 트랜스포머인 DiT로 교체하고, 텍스트 인코더를 더 크게 키우며(프롬프트 추종도에서 T5가 CLIP보다 우위입니다), 잠재 채널 수를 4에서 16으로 늘려 디테일 표현 여유(detail headroom)를 확보하는 방향입니다.

직접 만들기

code/main.py는 장난감(toy) 1차원 "VAE"(시연용 항등 인코더+디코더이며 실제 VAE는 합성곱 신경망(conv net)으로 만듭니다)를 Lesson 06의 DDPM 위에 쌓고, 분류기 없는 가이던스(classifier-free guidance; CFG)를 사용한 클래스 조건화(class conditioning)를 추가합니다. 핵심은 원본 1차원 값 위에서 돌리든 인코딩된 값 위에서 돌리든 동일한 확산 손실이 그대로 작동한다는 점을 직접 보여준다는 데 있습니다.

Step 1: 인코더와 디코더

def encode(x):    return x * 0.5          # 더 작은 스케일로 옮기는 장난감 "압축"
def decode(z):    return z * 2.0

실제 VAE에는 학습된 가중치(trained weights)가 있습니다. 여기서는 학습 목적상, 확산이 원본 데이터 공간이 아니라 z 위에서 동작한다는 사실을 보여 주기에 이 선형 사상(linear map)만으로도 충분합니다.

Step 2: z 공간에서의 확산

Lesson 06의 DDPM과 같은 흐름입니다. 신경망이 보는 데이터는 z = E(x)입니다. z_0를 표본으로 뽑은 다음 D(z_0)로 복원합니다.

Step 3: 분류기 없는 가이던스(classifier-free guidance)

학습 도중 클래스 레이블(class label)을 10% 확률로 떨어뜨리고(drop) 널 토큰(null token)으로 바꿉니다. 추론 시에는 조건부 잡음 예측 ε_cond와 무조건부 잡음 예측 ε_uncond를 모두 계산한 뒤 다음과 같이 결합합니다.

eps_cfg = (1 + w) * eps_cond - w * eps_uncond

w = 0은 가이던스가 없는 상태로 다양성(diversity)이 가장 크고, w = 3은 기본값, w = 7+는 채도가 과하거나(saturated) 과도하게 날카로운(over-sharp) 영역에 들어갑니다.

Step 4: 텍스트 조건화 (코드가 아니라 개념 수준)

클래스 레이블 자리를 동결된 텍스트 인코더의 출력으로 바꿉니다. 그 텍스트 임베딩(text embedding)을 교차 어텐션으로 U-Net에 주입합니다.

h = h + CrossAttention(Q=h, K=text_embed, V=text_embed)

클래스 조건부(class-conditional) 확산 모델과 Stable Diffusion 사이의 실질적인 차이는 결국 이 한 줄로 요약됩니다.

자주 빠지는 함정(Pitfalls)

  • VAE 스케일 불일치(VAE-scale mismatch). SD 1.x 계열 VAE는 인코딩 직후 적용하는 스케일링 상수(scaling_factor ≈ 0.18215)를 사용합니다. 이를 잊으면 U-Net이 분산(variance)이 완전히 어긋난 잠재 위에서 학습됩니다. 모든 체크포인트(checkpoint)에 이 값이 함께 들어 있습니다.
  • 텍스트 인코더가 조용히 잘못 설정됨. SD3는 T5-XXL과 128 이상의 토큰을 필요로 하고, CLIP만 쓰는 대체 경로(fallback)는 손실이 있습니다. use_t5=True인지 항상 확인하지 않으면 프롬프트 충실도(prompt fidelity)가 급격히 떨어집니다.
  • 잠재 공간 섞기. SDXL, SD3, Flux는 서로 다른 VAE를 씁니다. SDXL 잠재에서 학습한 LoRA는 SD3에서 동작하지 않습니다. Hugging Face Diffusers 0.30 이상은 호환되지 않는 체크포인트를 아예 불러오지 못하게 막아 줍니다.
  • CFG 값이 너무 큼. w > 10은 채도가 과한, 기름진(oily) 느낌의 이미지를 만들고 프롬프트에 과적합하면서 다양성을 잃습니다. 적정 구간(sweet spot)은 w = 3-7입니다.
  • 부정 프롬프트(negative prompt) 누수. 부정 프롬프트가 비어 있으면 그대로 널 토큰이 되지만, 내용이 채워진 부정 프롬프트는 ε_uncond가 됩니다. 둘은 같지 않으며, 일부 파이프라인(pipeline)은 조용히 널 토큰을 기본값으로 사용해 버립니다.

사용해보기

2026년 시점의 프로덕션 스택은 다음과 같이 정리할 수 있습니다.

목표권장 백본
좁은 도메인, 짝지어진 데이터, 처음부터 모델 학습SDXL 파인튜닝(LoRA / 전체) — 배포까지 가장 빠름
오픈 도메인 텍스트-투-이미지(text-to-image), 공개 가중치(open weights)Flux.1-dev (12B, Apache / 비상업용) 또는 SD3.5-Large
가장 빠른 추론, 공개 가중치Flux.1-schnell (1-4 스텝, Apache) 또는 SDXL-Lightning
프롬프트 추종도가 가장 좋은 호스티드(hosted) 모델GPT-Image / DALL-E 3 (여전함), Midjourney v7, Imagen 4
이미지 편집(edit) 워크플로Flux.1-Kontext (2024년 12월) — 이미지 + 텍스트를 기본으로 입력받음
연구·기준선(baseline)SD 1.5 — 오래됐지만 분석 자료가 많음

산출물 만들기

outputs/skill-sd-prompter.md로 저장합니다. 이 스킬은 텍스트 프롬프트와 타깃 스타일을 받아 모델 + 체크포인트, CFG 스케일, 샘플러(sampler), 부정 프롬프트, 해상도, 선택적인 ControlNet/IP-Adapter 조합, 그리고 스텝별 QA 체크리스트를 함께 출력합니다.

연습문제

  1. 쉬움. code/main.py를 가이던스 w ∈ {0, 1, 3, 7, 15}로 실행합니다. 클래스별 평균 표본을 기록하고, 어떤 w에서 클래스별 평균이 실제 데이터 평균을 지나치는지 확인합니다.
  2. 중간. 장난감 선형 인코더를 tanh-MLP 인코더/디코더 쌍으로 교체하고 복원 손실로 함께 학습합니다. 새 잠재 위에서 확산을 다시 학습했을 때 표본 품질이 어떻게 달라지는지 확인합니다.
  3. 어려움. Diffusers로 실제 Stable Diffusion 추론을 구성합니다. sdxl-base를 불러와 CFG=7, Euler 30 스텝으로 실행해 시간을 측정합니다. 그다음 sdxl-turbo로 바꿔 4 스텝, CFG=0으로 실행합니다. 동일한 주제에서 품질이 어떻게 달라졌는지, 왜 그런지 설명합니다.

핵심 용어

용어흔한 설명실제 의미
1단계(First stage)"VAE"학습된 인코더/디코더 쌍이며 512²를 64²로 압축한다.
2단계(Second stage)"U-Net"잠재 공간 위에서 동작하는 확산 모델이다.
CFG(Classifier-Free Guidance)"가이던스 스케일(guidance scale)"(1+w)·ε_cond - w·ε_uncond로 조건화 강도를 조절한다.
널 토큰(null token)"빈 프롬프트 임베딩(empty prompt embed)"ε_uncond에 사용되는 무조건부 임베딩이다.
교차 어텐션(cross-attention)"텍스트가 들어오는 방식"U-Net 블록이 텍스트 토큰을 K, V로 삼아 어텐션한다.
DiT(Diffusion Transformer)"확산 트랜스포머"U-Net을 잠재 패치 위에서 동작하는 트랜스포머로 대체한 구조이며 확장성이 더 좋다.
MMDiT(Multi-modal DiT)"멀티모달 DiT"SD3의 구조이며, 텍스트와 이미지 두 스트림이 결합 어텐션(joint attention)을 수행한다.
VAE 스케일링 팩터(VAE scaling factor)"마법의 숫자"잠재를 약 5.4로 나눠 확산이 단위 분산(unit-variance) 공간에서 동작하도록 한다.

프로덕션 노트(Production note): 8GB 컨슈머 GPU에서 Flux-12B 실행하기

레퍼런스 Flux 통합(integration)은 "내 컨슈머 GPU에서도 이걸 배포할 수 있나?"라는 질문에 대한 표준 레시피입니다. 핵심은 프로덕션 추론 문헌에서 자주 등장하는 세 가지 손잡이(knob)를 확산 DiT에 그대로 적용한다는 점입니다.

  1. 단계별 로딩(staggered loading). Flux에는 동시에 VRAM에 올라가 있을 필요가 없는 네 가지 신경망이 있습니다. T5-XXL 텍스트 인코더(fp32 기준 약 10 GB), CLIP-L(작음), 12B 규모의 MMDiT, 그리고 VAE입니다. 먼저 프롬프트를 인코딩한 다음 인코더를 제거하고, DiT를 올려 잡음 제거를 수행한 뒤 DiT도 제거하고, 마지막으로 VAE를 올려 디코딩합니다. 컨슈머 8GB GPU에는 한 번에 한 단계만 올라갈 수 있기 때문입니다.
  2. bitsandbytes 4-bit 양자화(quantization). T5 인코더와 DiT 모두에 BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16)를 적용합니다. 메모리는 약 8배 줄어들고, 텍스트-투-이미지의 품질 저하는 Aritra의 벤치마크(노트북에 링크)에 따르면 체감하기 어려울 정도입니다.
  3. CPU 오프로드(offload). pipe.enable_model_cpu_offload()를 호출하면 각 순방향 패스(forward pass)가 진행될 때마다 모듈을 CPU와 GPU 사이에서 자동으로 교환해 줍니다. 지연(latency)은 10-20% 늘지만, 파이프라인이 실행 자체가 가능해집니다.

메모리 회계를 정리하면 다음과 같습니다. T5 10 GB / 8 = 1.25 GB로 양자화되고, 12B 파라미터 × 0.5 바이트 = 약 6 GB로 양자화된 DiT, 여기에 활성값(activations)이 더해집니다. stas00의 표현을 빌리면 이는 모델 병렬화 없이(TP=1) 양자화를 극단까지 끌어쓴 추론입니다. 프로덕션이라면 H100 위에서 TP=2 또는 TP=4를 쓰겠지만, 1인용 개발 노트북에서는 이 레시피가 사실상의 표준입니다.

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

sd-prompter

Configure Stable Diffusion / Flux inference for a given prompt, style, and quality bar.

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.SD 1.x VAE를 불러왔지만 인코딩 후 스케일링 팩터(~0.18215)를 적용하는 것을 잊었습니다. U-Net 학습에 어떤 일이 일어나나요?

2.분류기 없는 가이던스(CFG) 스케일 w=15로 생성한 이미지가 과포화(oversaturated)되고 '기름진(oily)' 느낌입니다. 무슨 일이 일어나고 있으며 권장 범위는 어떻게 되나요?

3.엔지니어가 소비자용 8GB GPU에서 Flux.1-dev(12B 파라미터)를 실행하려 합니다. 어떤 기법 조합이 이를 가능하게 하나요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다