Phase 04 - Lesson 11
Stable Diffusion — Arquitetura e Fine-Tuning
O Stable Diffusion é um DDPM que roda no espaço latente de um VAE pré-treinado, condicionado a texto via cross-attention, amostrado com um solver de EDO determinístico e rápido, e guiado por classifier-free guidance.
Tipo: Aprender + Usar Linguagens: Python Pré-requisitos: Fase 4 Lição 10 (Diffusion), Fase 7 Lição 02 (Self-Attention) Tempo: ~75 minutos
Objetivos de aprendizagem
- Percorrer as cinco peças de um pipeline de Stable Diffusion: VAE, encoder de texto, U-Net, scheduler, safety checker — e o que cada uma delas realmente faz
- Explicar a difusão latente e por que treinar em um espaço latente de 4x64x64 (em vez de uma imagem 3x512x512) reduz o custo computacional em 48x sem perda de qualidade
- Usar
diffuserspara gerar imagens, executar image-to-image, inpainting e geração guiada por ControlNet - Fazer fine-tuning do Stable Diffusion com LoRA em um pequeno dataset customizado e carregar o adaptador LoRA na inferência
O Problema
Treinar um DDPM diretamente em imagens RGB 512x512 é caro. Cada passo de treinamento faz backprop através de uma U-Net que vê 3x512x512 = 786.432 valores de entrada, e a amostragem leva 50+ forward passes por essa mesma U-Net. No nível de qualidade do Stable Diffusion 1.5 (lançado em 2022), a difusão no espaço de pixels exigiria aproximadamente 256 GPU-meses de treinamento e de 10 a 30 segundos por imagem em uma GPU de consumo.
O truque que tornou o text-to-image de pesos abertos viável foi a difusão latente (Rombach et al., CVPR 2022). Treine um VAE que mapeia uma imagem 3x512x512 para um tensor latente 4x64x64 e de volta, e então faça a difusão nesse espaço latente. O custo computacional cai em (3*512*512)/(4*64*64) = 48x. A amostragem cai de dezenas de segundos para menos de dois segundos na mesma GPU.
Quase todo modelo moderno de geração de imagens — SDXL, SD3, FLUX, HunyuanDiT, Wan-Video — é um modelo de difusão latente com variações no autoencoder, no denoiser (U-Net ou DiT) e no condicionamento de texto. Aprenda Stable Diffusion e você terá aprendido o template.
O Conceito
O pipeline
flowchart LR
TXT["Prompt de texto"] --> TE["Encoder de texto<br/>(CLIP-L ou T5)"]
TE --> CT["Embedding<br/>de texto"]
NOISE["Ruído<br/>4x64x64"] --> UNET["UNet<br/>(denoiser com<br/>cross-attention<br/>ao texto)"]
CT --> UNET
UNET --> SCHED["Scheduler<br/>(DPM-Solver++,<br/>Euler)"]
SCHED --> LATENT["Latente limpo<br/>4x64x64"]
LATENT --> VAE["Decoder do VAE"]
VAE --> IMG["Imagem RGB<br/>512x512"]
style TE fill:#dbeafe,stroke:#2563eb
style UNET fill:#fef3c7,stroke:#d97706
style SCHED fill:#fecaca,stroke:#dc2626
style IMG fill:#dcfce7,stroke:#16a34a
- VAE — autoencoder congelado. O encoder transforma a imagem em latentes (usado em img2img e treinamento). O decoder transforma os latentes de volta em uma imagem.
- Encoder de texto — encoder de texto CLIP (SD 1.x/2.x), CLIP-L + CLIP-G (SDXL), ou T5-XXL (SD3/FLUX). Produz uma sequência de embeddings de tokens.
- U-Net — o denoiser. Tem camadas de cross-attention que atendem dos latentes ao embedding de texto em cada nível de resolução.
- Scheduler — o algoritmo de amostragem (DDIM, Euler, DPM-Solver++). Escolhe os sigmas e mistura o ruído previsto de volta no latente.
- Safety checker — filtro opcional de conteúdo NSFW / ilegal na imagem de saída.
Classifier-free guidance (CFG)
O condicionamento de texto puro aprende epsilon_theta(x_t, t, c) para cada prompt c. O CFG treina a mesma rede com c descartado 10% das vezes (substituído por um embedding vazio), gerando um único modelo que prevê tanto o ruído condicional quanto o incondicional. Na inferência:
eps = eps_uncond + w * (eps_cond - eps_uncond)
w é a escala de guidance. w=0 é incondicional, w=1 é condicional puro, w>1 empurra a saída para ser "mais condicionada ao prompt" ao custo de diversidade. O padrão do SD é w=7.5.
O CFG é a razão pela qual text-to-image funciona em qualidade de produção. Sem ele, os prompts enviesam a saída fracamente; com ele, os prompts dominam.
Geometria do espaço latente
O latente de 4 canais do VAE não é apenas uma imagem comprimida. É uma variedade (manifold) onde a aritmética corresponde aproximadamente a edições semânticas (prompt engineering + interpolação ambos vivem aqui), e onde a U-Net de difusão foi treinada para gastar todo o seu orçamento de modelagem. Decodificar um latente aleatório 4x64x64 não produz uma imagem de aparência aleatória — produz lixo, porque apenas uma submanifold específica de latentes decodifica para imagens válidas.
Duas consequências:
- Img2img = codificar a imagem em latente, adicionar ruído parcial, executar o denoiser, decodificar. A estrutura da imagem sobrevive porque a codificação é quase invertível; o conteúdo muda com base no prompt.
- Inpainting = igual ao img2img, mas o denoiser só atualiza regiões mascaradas; regiões não mascaradas são mantidas no latente codificado.
A arquitetura da U-Net
A U-Net do SD é uma versão grande da TinyUNet da Lição 10 com três adições:
- Blocos de transformer em cada resolução espacial, contendo self-attention + cross-attention ao embedding de texto.
- Time embedding via MLP sobre uma codificação senoidal.
- Skip connections entre encoder e decoder em resoluções correspondentes.
Total de parâmetros no SD 1.5: ~860M. SDXL: ~2.6B. FLUX: ~12B. O salto em parâmetros está principalmente nas camadas de atenção.
Fine-tuning com LoRA
O fine-tuning completo do Stable Diffusion precisa de 20+ GB de VRAM e atualiza 860M de parâmetros. O LoRA (Low-Rank Adaptation) mantém o modelo base congelado e injeta pequenas matrizes de decomposição de baixo posto nas camadas de atenção. Um adaptador LoRA para SD tem tipicamente de 10 a 50 MB, treina em 10 a 60 minutos em uma única GPU de consumo, e carrega no momento da inferência como uma modificação plug-in.
Original: W_q : (d_in, d_out) frozen
LoRA: W_q + alpha * (A @ B) where A : (d_in, r), B : (r, d_out)
r is typically 4-32.
O LoRA é a forma como quase todo fine-tune da comunidade é distribuído. CivitAI e Hugging Face hospedam milhões deles.
Schedulers que você verá
- DDIM — determinístico, ~50 passos, simples.
- Euler ancestral — estocástico, 30-50 passos, amostras ligeiramente mais criativas.
- DPM-Solver++ 2M Karras — determinístico, 20-30 passos, padrão de produção.
- LCM / TCD / Turbo — consistency models e variantes destiladas; 1-4 passos ao custo de alguma qualidade.
Trocar de scheduler é uma mudança de uma linha no diffusers e às vezes corrige problemas de amostragem sem nenhum retreino.
Construa
Esta lição usa diffusers de ponta a ponta em vez de reconstruir o Stable Diffusion do zero. As peças que você precisaria reconstruir (VAE, encoder de texto, U-Net, scheduler) são temas de lições próprias; aqui o objetivo é fluência com a API de produção.
Passo 1: Text-to-image
import torch
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16,
).to("cuda")
image = pipe(
prompt="a dog riding a skateboard in tokyo, studio ghibli style",
guidance_scale=7.5,
num_inference_steps=25,
generator=torch.Generator("cuda").manual_seed(42),
).images[0]
image.save("dog.png")
float16 reduz a VRAM pela metade sem perda de qualidade visível. num_inference_steps=25 com o DPM-Solver++ padrão equivale a num_inference_steps=50 com DDIM.
Passo 2: Trocar o scheduler
from diffusers import DPMSolverMultistepScheduler, EulerAncestralDiscreteScheduler
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
O estado do scheduler é desacoplado dos pesos da U-Net. Você pode treinar com DDPM e amostrar com qualquer scheduler.
Passo 3: Image-to-image
from diffusers import StableDiffusionImg2ImgPipeline
from PIL import Image
img2img = StableDiffusionImg2ImgPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16,
).to("cuda")
init_image = Image.open("dog.png").convert("RGB").resize((512, 512))
out = img2img(
prompt="a dog riding a skateboard, oil painting",
image=init_image,
strength=0.6,
guidance_scale=7.5,
).images[0]
strength é quanto ruído adicionar antes de denoising (0.0 = inalterado, 1.0 = regeneração completa). 0.5-0.7 é a faixa padrão para transferência de estilo.
Passo 4: Inpainting
from diffusers import StableDiffusionInpaintPipeline
inpaint = StableDiffusionInpaintPipeline.from_pretrained(
"runwayml/stable-diffusion-inpainting",
torch_dtype=torch.float16,
).to("cuda")
image = Image.open("dog.png").convert("RGB").resize((512, 512))
mask = Image.open("dog_mask.png").convert("L").resize((512, 512))
out = inpaint(
prompt="a cat",
image=image,
mask_image=mask,
guidance_scale=7.5,
).images[0]
Pixels brancos na máscara são a área a regenerar. Pixels pretos são preservados.
Passo 5: Carregamento de LoRA
pipe.load_lora_weights("sayakpaul/sd-lora-ghibli")
pipe.fuse_lora(lora_scale=0.8)
image = pipe(prompt="a village square in ghibli style").images[0]
lora_scale controla a força; 0.0 = nenhum efeito, 1.0 = efeito total. fuse_lora integra o adaptador nos pesos no local para ganhar velocidade, mas impede a troca. Chame pipe.unfuse_lora() antes de carregar um adaptador diferente.
Passo 6: Treinamento de LoRA (esboço)
O treinamento real de LoRA vive em peft ou diffusers.training. O esboço:
# Pseudocode
for step, batch in enumerate(dataloader):
images, prompts = batch
latents = vae.encode(images).latent_dist.sample() * 0.18215
t = torch.randint(0, num_train_timesteps, (batch_size,))
noise = torch.randn_like(latents)
noisy_latents = scheduler.add_noise(latents, noise, t)
text_emb = text_encoder(tokenizer(prompts))
pred_noise = unet(noisy_latents, t, text_emb) # LoRA weights injected here
loss = F.mse_loss(pred_noise, noise)
loss.backward()
optimizer.step()
Apenas as matrizes LoRA recebem gradiente; a U-Net base, o VAE e o encoder de texto ficam congelados. Com um batch size de 1 e gradient checkpointing isso cabe em 8 GB de VRAM.
Use
Em produção, as decisões que você realmente toma:
- Família de modelo: SD 1.5 para fine-tunes open-source da comunidade, SDXL para maior fidelidade, SD3 / FLUX para o estado da arte e requisitos rígidos de licenciamento.
- Scheduler: DPM-Solver++ 2M Karras para 20-30 passos, LCM-LoRA quando a latência está abaixo de 1s.
- Precisão:
float16em 4080/4090,bfloat16em A100 e mais novas,int8(viabitsandbytesoucompel) quando a VRAM está apertada. - Condicionamento: texto puro funciona; para controle mais forte, adicione ControlNet (canny, depth, pose) sobre o pipeline base.
Para geração em lote, AUTO1111 / ComfyUI são as ferramentas da comunidade; para APIs de produção, diffusers + accelerate ou optimum-nvidia com compilação TensorRT.
Entregue
Esta lição produz:
outputs/prompt-sd-pipeline-planner.md— um prompt que escolhe SD 1.5 / SDXL / SD3 / FLUX mais scheduler e precisão dados um orçamento de latência, alvo de fidelidade e restrição de licenciamento.outputs/skill-lora-training-setup.md— uma skill que escreve uma config completa de treinamento de LoRA para um dataset customizado, incluindo captions, rank, batch size e learning rate.
Exercícios
- (Fácil) Gere o mesmo prompt com
guidance_scaleem[1, 3, 5, 7.5, 10, 15]. Descreva como a imagem muda. Em qual valor de guidance os artefatos aparecem? - (Médio) Pegue qualquer fotografia real, passe-a por
StableDiffusionImg2ImgPipelinecomstrengthem[0.2, 0.4, 0.6, 0.8, 1.0]. Qual strength preserva a composição enquanto muda o estilo? Por que 1.0 ignora a entrada completamente? - (Difícil) Treine um LoRA com 10-20 imagens de um único sujeito (um pet, um logo, um personagem) e gere cenas inéditas com esse sujeito nelas. Reporte o rank do LoRA e os passos de treinamento que produziram a melhor preservação de identidade sem overfitting nas imagens de entrada.
Termos-chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Difusão latente | "Difundir em latentes" | Rodar todo o DDPM no espaço latente do VAE (4x64x64) em vez do espaço de pixels (3x512x512); economia de 48x em computação |
| Fator de escala do VAE | "0.18215" | Constante que reescala o latente bruto do VAE para variância aproximadamente unitária; hardcoded em todo pipeline SD |
| Classifier-free guidance | "CFG" | Misturar previsões de ruído condicional e incondicional; o botão de inferência mais impactante de todos |
| Scheduler | "Sampler" | O algoritmo que transforma ruído + previsões do modelo em uma trajetória de latente denoised |
| LoRA | "Adaptador de baixo posto" | Pequenas matrizes de decomposição de baixo posto que fazem fine-tune nas camadas de atenção sem tocar nos pesos base |
| Cross-attention | "Atenção texto-imagem" | Atenção dos tokens latentes para os tokens de texto; injeta informação do prompt em cada nível da U-Net |
| ControlNet | "Condicionamento de estrutura" | Um adaptador treinado separadamente que guia o SD com uma entrada extra (canny, depth, pose, segmentação) |
| DPM-Solver++ | "O scheduler padrão" | Solver de EDO determinístico de segunda ordem; melhor qualidade com poucos passos (20-30) em 2026 |
Leitura adicional
- High-Resolution Image Synthesis with Latent Diffusion (Rombach et al., 2022) — o paper do Stable Diffusion; inclui cada ablação que justifica o design
- Classifier-Free Diffusion Guidance (Ho & Salimans, 2022) — o paper do CFG
- LoRA: Low-Rank Adaptation of Large Language Models (Hu et al., 2021) — o LoRA nasceu no NLP; transferiu-se para o SD com quase nenhuma mudança
- diffusers documentation — a referência para todo pipeline SD / SDXL / SD3 / FLUX