Phase 08 - Lesson 07
Latent Diffusion & Stable Diffusion
A difusão no espaço de pixels em imagens de 512×512 é um crime de guerra computacional. Rombach et al. (2022) notaram que não é necessário ter todas as 786k dimensões para gerar uma imagem — você precisa do suficiente para capturar a estrutura semântica e de um decodificador separado para o resto. Execute a difusão dentro do espaço latente de um VAE. Essa única ideia é o Stable Diffusion.
Tipo: Build Linguagens: Python Pré-requisitos: Phase 8 · 02 (VAE), Phase 8 · 06 (DDPM), Phase 7 · 09 (ViT) Tempo: ~75 minutos
O Problema
A difusão no espaço de pixels a 512² significa que a U-Net roda em tensores de formato [B, 3, 512, 512]. Cada etapa de amostragem consome ~100 GFLOPS para uma U-Net de 500M de parâmetros. Cinquenta etapas totalizam 5 TFLOPS por imagem. Treinar em um bilhão de imagens resulta em uma conta de computação absurda.
A maior parte desses FLOPs é gasta processando detalhes perceptualmente sem importância na rede — a textura de alta frequência que um VAE com perdas (lossy VAE) poderia compactar. A ideia de Rombach: treinar um VAE uma vez (a primeira etapa), congelá-lo e executar a difusão inteiramente no espaço latente de 4 canais de 64×64 (a segunda etapa). Mesma U-Net. 1/16 dos pixels. ~64x menos FLOPs para uma qualidade comparável.
Esta é a receita do Stable Diffusion. O SD 1.x / 2.x utilizava uma U-Net de 860M sobre latentes de 64×64×4, o SDXL utilizou uma U-Net de 2.6B sobre 128×128×4, e o SD3 substituiu a U-Net por um Diffusion Transformer (DiT) com flow matching. O Flux.1-dev (Black Forest Labs, 2024) traz um DiT-MMDiT de 12B de parâmetros. Todos funcionam sobre o mesmo substrato de duas etapas.
O Conceito
Duas etapas, treinadas separadamente.
Etapa 1 — VAE. Codificador
E(x) → z, decodificadorD(z) → x. Compressão alvo: redução de 8× (downsample) em cada eixo espacial + ajuste de canais para que o tamanho latente total seja ~1/16 do número de pixels. Loss = reconstrução (L1 + perceptual LPIPS) + KL (peso pequeno para queznão seja forçado a ser excessivamente gaussiano, pois não precisamos de amostragem exata dez). Frequentemente treinado com uma loss adversarial para que as imagens decodificadas fiquem nítidas.Etapa 2 — difusão em
z. Trataz = E(x_real)como os dados. Treina uma U-Net (ou DiT) para remover o ruído dez_t. Na inferência: amostraz_0via difusão, depois obtémx = D(z_0).
Condicionamento de texto. Dois componentes adicionais. Um codificador de texto congelado (CLIP-L para SD 1.x, CLIP-L+OpenCLIP-G para SD 2/XL, T5-XXL para SD3 e Flux). Uma injeção de atenção cruzada (cross-attention): cada bloco da U-Net recebe [Q = image features, K = V = text tokens] e os mistura. Os tokens são a única forma pela qual o texto influencia a imagem.
A função de perda é idêntica à da Lição 06. Mesma MSE de DDPM / flow matching no ruído. Você apenas troca o domínio dos dados.
Variantes de arquitetura
| Modelo | Ano | Backbone | Formato latente | Codificador de texto | Parâmetros |
|---|---|---|---|---|---|
| SD 1.5 | 2022 | U-Net | 64×64×4 | CLIP-L (77 tokens) | 860M |
| SD 2.1 | 2022 | U-Net | 64×64×4 | OpenCLIP-H | 865M |
| SDXL | 2023 | U-Net + refinador | 128×128×4 | CLIP-L + OpenCLIP-G | 2.6B + 6.6B |
| SDXL-Turbo | 2023 | Destilado | 128×128×4 | mesmo | amostragem de 1-4 etapas |
| SD3 | 2024 | MMDiT (DiT multimodal) | 128×128×16 | T5-XXL + CLIP-L + CLIP-G | 2B / 8B |
| Flux.1-dev | 2024 | MMDiT | 128×128×16 | T5-XXL + CLIP-L | 12B |
| Flux.1-schnell | 2024 | MMDiT destilado | 128×128×16 | T5-XXL + CLIP-L | 12B, 1-4 etapas |
A tendência: substituir a U-Net por DiT (transformer sobre patches latentes), dimensionar o codificador de texto (o T5 supera o CLIP na fidelidade ao prompt), aumentar os canais latentes (4 → 16 dá mais margem para detalhes).
Build It
code/main.py empilha um "VAE" unidimensional de brinquedo (codificador + decodificador de identidade, para demonstração; um VAE real seria uma rede convolucional) sobre o DDPM da Lição 06 e adiciona condicionamento de classe com classifier-free guidance. Ele mostra que a mesma loss de difusão funciona independentemente de você rodar em valores unidimensionais puros ou em valores codificados — o insight fundamental.
Passo 1: codificador/decodificador
def encode(x): return x * 0.5 # toy "compression" to smaller scale
def decode(z): return z * 2.0
Um VAE real possui pesos treinados. Para fins pedagógicos, este mapeamento linear é suficiente para mostrar que a difusão opera em z sem se importar com o espaço de dados original.
Passo 2: difusão no espaço z
Mesmo DDPM da Lição 06. Os dados que a rede vê são z = E(x). Após a amostragem de z_0, decodifique com D(z_0).
Passo 3: classifier-free guidance
Durante o treino, descarte o rótulo de classe em 10% do tempo (substitua por um token nulo). Na inferência, calcule tanto ε_cond quanto ε_uncond e, em seguida:
eps_cfg = (1 + w) * eps_cond - w * eps_uncond
w = 0 = sem orientação (diversidade total), w = 3 = padrão, w = 7+ = saturado / excessivamente nítido.
Passo 4: condicionamento de texto (conceito, não código)
Substitua o rótulo de classe pela saída de um codificador de texto congelado. Alimente a incorporação (embedding) de texto para a U-Net via cross-attention:
h = h + CrossAttention(Q=h, K=text_embed, V=text_embed)
Esta é a única diferença substantiva entre um modelo de difusão condicional de classe e o Stable Diffusion.
Armadilhas
- Incompatibilidade de escala do VAE. Os VAEs do SD 1.x têm uma constante de escala (
scaling_factor ≈ 0.18215) aplicada após a codificação. Esquecer disso faz com que a U-Net treine em latentes com variância totalmente errada. Cada checkpoint vem com uma. - Codificador de texto silenciosamente incorreto. O SD3 precisa do T5-XXL com >=128 tokens, e o fallback para usar apenas CLIP gera perdas. Sempre verifique
use_t5=Trueou a fidelidade ao prompt despencará. - Mistura de espaços latentes. SDXL, SD3 e Flux utilizam VAEs diferentes. Um LoRA treinado em latentes do SDXL não funcionará no SD3. O Hugging Face diffusers 0.30+ recusa-se a carregar checkpoints incompatíveis.
- CFG alto demais.
w > 10produz imagens saturadas, com aspecto oleoso, e causa overfitting no prompt às custas da diversidade. O ponto ideal éw = 3-7. - Vazamento de prompts negativos. Um prompt negativo vazio torna-se o token nulo; um prompt negativo preenchido torna-se o
ε_uncond. Eles não são a mesma coisa; alguns pipelines adotam o nulo como padrão silenciosamente.
Use It
Stacks de produção em 2026:
| Objetivo | Backbone recomendado |
|---|---|
| Domínio estreito, dados pareados, treinando um modelo do zero | Ajuste fino (fine-tune) do SDXL (LoRA / completo) — mais rápido para colocar em produção |
| Texto para imagem de domínio aberto, pesos abertos | Flux.1-dev (12B, Apache / não comercial) ou SD3.5-Large |
| Inferência mais rápida, pesos abertos | Flux.1-schnell (1-4 etapas, Apache) ou SDXL-Lightning |
| Melhor aderência ao prompt, hospedado | GPT-Image / DALL-E 3 (ainda), Midjourney v7, Imagen 4 |
| Workflows de edição | Flux.1-Kontext (Dez 2024) — aceita nativamente imagem + texto |
| Pesquisa, baseline | SD 1.5 — antigo mas bem estudado |
Ship It
Salve outputs/skill-sd-prompter.md. A skill recebe um prompt de texto + estilo alvo e gera: modelo + checkpoint, escala CFG, sampler, prompt negativo, resolução, combinação opcional de ControlNet/IP-Adapter e uma lista de verificação (checklist) de QA passo a passo.
Exercícios
- Fácil. Rode
code/main.pycom orientaçãow ∈ {0, 1, 3, 7, 15}. Registre a média das amostras por classe. Em qualwas médias das classes divergem além das médias dos dados reais? - Médio. Troque o codificador linear de brinquedo por um par de codificador/decodificador tanh-MLP com uma loss de reconstrução. Treine novamente a difusão nos novos latentes. A qualidade da amostra muda?
- Difícil. Configure uma inferência real de Stable Diffusion com diffusers: carregue o
sdxl-base, execute 30 etapas de Euler com CFG=7 e meça o tempo. Agora mude para osdxl-turbocom 4 etapas e CFG=0. Mesmo assunto, qualidade diferente — descreva o que mudou e o porquê.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Primeira etapa | "O VAE" | Par codificador/decodificador treinado; compacta 512² para 64². |
| Segunda etapa | "A U-Net" | Modelo de difusão sobre o espaço latente. |
| CFG | "Escala de orientação" | (1+w)·ε_cond - w·ε_uncond; ajusta a força do condicionamento. |
| Token nulo | "Embed de prompt vazio" | Embed incondicional usado para ε_uncond. |
| Cross-attention | "Como o texto entra" | Cada bloco da U-Net atende aos tokens de texto como K e V. |
| DiT | "Transformer de Difusão" | Substitui a U-Net por um transformer sobre patches latentes; escala melhor. |
| MMDiT | "DiT multimodal" | Arquitetura do SD3: fluxos de texto e imagem com atenção conjunta. |
| Fator de escala do VAE | "Número mágico" | Divide os latentes por ~5.4 para que a difusão opere em espaço de variância unitária. |
Nota de produção: rodando Flux-12B em uma GPU comum de 8GB
a integração de referência do Flux é a receita canônica para "tenho uma GPU doméstica, posso colocar isso em produção?". O truque é a mesma receita de três botões que a literatura de inferência em produção lista aplicada a um DiT de difusão:
- Carregamento escalonado. O Flux tem três redes que nunca precisam coexistir na VRAM: o codificador de texto T5-XXL (~10 GB em fp32), o CLIP-L (pequeno), o MMDiT de 12B e o VAE. Codifique o prompt primeiro, delete os codificadores, carregue o DiT, remova o ruído, delete o DiT, carregue o VAE e decodifique. GPUs domésticas de 8GB só comportam uma etapa por vez.
- Quantização de 4 bits via bitsandbytes.
BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16)tanto no codificador T5 quanto no DiT. Reduz a memória em 8×, e a queda de qualidade é imperceptível para texto para imagem de acordo com os benchmarks de Aritra (com link no notebook). - CPU offload.
pipe.enable_model_cpu_offload()troca módulos automaticamente entre CPU e GPU conforme cada etapa do forward pass avança. Adiciona 10-20% de latência, mas faz o pipeline funcionar de qualquer maneira.
A contabilidade de memória é: 10 GB T5 / 8 = 1.25 GB quantizado, 12 B params × 0.5 bytes = ~6 GB DiT quantizado, mais ativações. Nos termos de stas00, este é o extremo de inferência TP=1 — sem paralelismo de modelo, quantização máxima. Para produção, você rodaria TP=2 ou TP=4 em H100s; para um único notebook de desenvolvimento, esta é a receita.
Leitura Adicional
- Rombach et al. (2022). High-Resolution Image Synthesis with Latent Diffusion Models — Stable Diffusion.
- Podell et al. (2023). SDXL: Improving Latent Diffusion Models for High-Resolution Image Synthesis — SDXL.
- Peebles & Xie (2023). Scalable Diffusion Models with Transformers (DiT) — DiT.
- Esser et al. (2024). Scaling Rectified Flow Transformers for High-Resolution Image Synthesis — SD3, MMDiT.
- Ho & Salimans (2022). Classifier-Free Diffusion Guidance — CFG.
- Labs (2024). Flux.1 — Black Forest Labs announcement — Família Flux.1.
- Hugging Face Diffusers docs — implementação de referência para cada checkpoint acima.