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

Latent diffusion: VAE compression + diffusion in latent space

Duas etapas, treinadas separadamente.

  1. Etapa 1 — VAE. Codificador E(x) → z, decodificador D(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 que z não seja forçado a ser excessivamente gaussiano, pois não precisamos de amostragem exata de z). Frequentemente treinado com uma loss adversarial para que as imagens decodificadas fiquem nítidas.

  2. Etapa 2 — difusão em z. Trata z = E(x_real) como os dados. Treina uma U-Net (ou DiT) para remover o ruído de z_t. Na inferência: amostra z_0 via difusão, depois obtém x = 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=True ou 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 > 10 produz 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

  1. Fácil. Rode code/main.py com orientação w ∈ {0, 1, 3, 7, 15}. Registre a média das amostras por classe. Em qual w as médias das classes divergem além das médias dos dados reais?
  2. 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?
  3. 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 o sdxl-turbo com 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:

  1. 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.
  2. 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).
  3. 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

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).