VAE + U-Net: a arquitetura por trás do Stable Diffusion
- ⬜🧮 Diffusion math: score matching e SDE/ODE(Diffusion Models & Geração Multimodal)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
Os três blocos que formam o Stable Diffusion
No módulo anterior você viu a matemática do diffusion: forward gaussian, reverse, score matching, DDIM, SDE/ODE. Mas essa matemática não roda em pixels diretos — seria caro demais. O Stable Diffusion (Rombach et al. 2022, CVPR — Latent Diffusion Models) materializa a teoria em três módulos distintos que trabalham em conjunto:
Por que rodar diffusion no latente é a sacada toda
Imagine treinar e amostrar DDPM diretamente em 512×512×3 = 786.432 dimensões. Cada passo do U-Net processa um tensor dessa magnitude, e você precisa de ~20-50 passos. Com batch size razoável e VRAM finita, é inviável fora de cluster.
A observação central de Rombach et al.: a redundância perceptual nos pixels é altíssima. Você pode comprimir 8× em cada dimensão espacial (preservando ~98% da fidelidade perceptual) com um simples autoencoder bem treinado. O latente resultante tem 64×64×4 = 16.384 dimensões — 48× menor.
| Métrica | Pixel-space diffusion (GLIDE, Imagen) | Latent diffusion (Stable Diffusion) |
|---|---|---|
| Dimensão por passo | 512×512×3 = 786k | 64×64×4 = 16k |
| VRAM em inferência (fp16) | ~16-24 GB para 1024px | ~4-8 GB para 1024px (SDXL) |
| Passos típicos para FID competitivo | 50-100 | 20-50 |
| Treino em consumer GPU | ❌ Inviável | ✅ Possível (1× 3090) |
| Custo total inferência (1 imagem) | ~10× LDM | 1× (baseline) |
| Qualidade visual no benchmark | Topo de FID em 2022 | Topo de FID em 2022, e 50× mais barato |
A democratização da geração de imagem (rodar em RTX 3060, Mac M-series, até em celular Snapdragon) é consequência direta dessa escolha arquitetural. O VAE não é apenas otimização — é o que permitiu Stable Diffusion existir como open weights.
VAE: compressão com bottleneck contínuo
O VAE (Kingma & Welling 2013) usado no SD não é o VAE original probabilístico — é uma variante mais próxima de um autoencoder regularizado. Ele tem dois objetivos no treino:
O resultado é um autoencoder convolucional com encoder E e decoder D tal que:
# Inferência simplificada do Stable Diffusion
def generate(prompt, steps=30, cfg=7.5):
# 1. Texto → embedding
c = text_encoder(prompt) # shape: [seq, dim]
c_uncond = text_encoder("") # para classifier-free guidance
# 2. Inicia no latente (não no pixel!)
z = torch.randn(1, 4, 64, 64) # latente 64×64×4
# 3. Loop de denoising no latente
for t in scheduler.timesteps: # 30 passos com DPM-Solver
eps_cond = unet(z, t, c)
eps_uncond = unet(z, t, c_uncond)
eps = eps_uncond + cfg * (eps_cond - eps_uncond)
z = scheduler.step(eps, t, z)
# 4. Decoder VAE devolve para pixel
image = vae_decoder(z) # shape: [1, 3, 512, 512]
return imageO VAE é o ponto frequentemente esquecido: latentes diferentes (SD1.5 vs SDXL vs SD3 vs Flux) não são compatíveis. Trocar de VAE quebra o modelo. Existem "VAE substitutos" como o MSE-VAE da Stability (melhora coerência de faces) — eles funcionam porque foram fine-tuned com a mesma latent distribution.
U-Net: o cérebro do denoising
A U-Net (Ronneberger, Fischer, Brox 2015 — MICCAI) tem um formato em "U" no fluxograma de dados: caminho contrativo (encoder) reduz resolução enquanto aumenta canais; caminho expansivo (decoder) faz o reverso; skip connections concatenam features simétricos para preservar detalhe espacial.
O SD 1.5 tem 859M params no U-Net. SDXL tem 2.6B. SD3 (8B) trocou U-Net por MMDiT. Flux.1 Dev (12B) usa DiT puro. A arquitetura U-Net cumpriu seu papel até ~2024, quando os transformers escalam melhor — tema do próximo módulo.
ResBlock + Cross-Attention: o tijolo fundamental
Cada bloco do U-Net combina um ResBlock (com timestep injection) e um Spatial Transformer Block (que faz self-attention + cross-attention com o prompt). Em pseudocódigo:
class ResBlock(nn.Module):
def __init__(self, ch_in, ch_out, time_dim):
self.gn1 = GroupNorm(32, ch_in)
self.conv1 = Conv2d(ch_in, ch_out, 3, padding=1)
self.time = Linear(time_dim, ch_out) # FiLM-like
self.gn2 = GroupNorm(32, ch_out)
self.conv2 = Conv2d(ch_out, ch_out, 3, padding=1)
self.skip = Conv2d(ch_in, ch_out, 1) if ch_in != ch_out else nn.Identity()
def forward(self, x, t_emb):
h = self.conv1(F.silu(self.gn1(x)))
h = h + self.time(F.silu(t_emb))[:, :, None, None] # injeta t
h = self.conv2(F.silu(self.gn2(h)))
return h + self.skip(x)
class SpatialTransformer(nn.Module):
def __init__(self, ch, n_heads, ctx_dim):
self.norm = GroupNorm(32, ch)
self.proj_in = Conv2d(ch, ch, 1)
self.attn_self = MultiHeadAttention(ch, n_heads) # tokens espaciais entre si
self.attn_cross = MultiHeadAttention(ch, n_heads, ctx_dim) # tokens espaciais ↔ texto
self.ff = FeedForward(ch)
self.proj_out = Conv2d(ch, ch, 1)
def forward(self, x, context): # context = CLIP/T5 embedding
B, C, H, W = x.shape
h = self.proj_in(self.norm(x))
h = h.reshape(B, C, H*W).transpose(1, 2) # [B, HW, C] como tokens
h = h + self.attn_self(h) # self-attn entre patches
h = h + self.attn_cross(h, context) # cross-attn com prompt
h = h + self.ff(h)
h = h.transpose(1, 2).reshape(B, C, H, W)
return self.proj_out(h) + xCross-attention: como o prompt entra na imagem
É aqui que a mágica do prompt acontece. No bloco SpatialTransformer, cada posição (x, y) do feature map vira uma query; cada token do prompt vira key e value. A atenção calcula:
Resultado: cada pixel "lê" do prompt o que é relevante para ele. Se você gerar "a red car next to a blue house", internamente os pixels da região do carro vão atender ao token "red"/"car" com peso alto, e os pixels da casa vão atender a "blue"/"house". Isso é visualizável em attention maps e é a base de técnicas como Prompt-to-Prompt (Hertz et al. 2022) e Attention-Refocusing.
Difundir num espaço latente 48 vezes menor que os pixels foi o que permitiu a geração de imagens rodar em hardware de consumo. Qual observação sustenta essa escolha?
Text encoder: CLIP, OpenCLIP e T5
| Modelo | Text encoder(s) | Dim total | Vantagem |
|---|---|---|---|
| SD 1.5 | CLIP-ViT-L/14 (OpenAI) | 768 | Padrão histórico |
| SD 2.x | OpenCLIP-ViT-H/14 | 1024 | Treino aberto (LAION) |
| SDXL | CLIP-L + OpenCLIP-bigG (concat) | 2048 + pooled | Melhor coerência semântica |
| SD 3 / 3.5 | CLIP-L + CLIP-G + T5-XXL | ~4096 | Prompts longos e detalhados via T5 |
| Flux.1 | CLIP-L + T5-XXL | ~2k + T5 | T5 melhora seguimento e texto em imagens |
| Imagen | T5-XXL puro | T5 4096 | Saari et al. mostraram que T5 supera CLIP em alinhamento |
CLIP é treinado contrastivamente com imagens; entende conceitos visuais bem mas trunca em 77 tokens. T5 é treinado em texto puro com objetivo de span corruption — entende ordem, negação, contagem, relações espaciais melhor. SD3 e Flux combinam os dois: CLIP para "look and feel", T5 para fidelidade textual.
Decisões arquiteturais que importam
📋 Você está escolhendo um modelo de base para fine-tune
SD 1.5 (2022) tem U-Net 859M, OpenAI CLIP-L só, e foi treinado em LAION 5B com filtros leves. SDXL e SD3+ têm capacidade muito maior, melhor entendimento de prompt, e licenças permissivas (SDXL: OpenRAIL; SD3.5: Stability Community; Flux.1 Dev: non-commercial).
Alt: SD 1.5
Alt: Ainda popular por LoRAs, mas qualidade base inferior; texto em imagens quase ilegível
Alt: Treinar do zero
Alt: Custo ~$1M+ em compute; só faz sentido para players com infraestrutura
Como diagnosticar quando vai mal
Perguntas que sobram
❓ Posso trocar o VAE do SDXL pelo do SD1.5?
❓ Por que algumas implementações usam `--vae-precision fp32` mesmo com U-Net em fp16?
❓ O timestep embedding usa a mesma sinusoidal do Transformer original?
❓ Por que pular do U-Net direto pra DiT funciona?
Leituras essenciais
Próximo: o que substituiu U-Net. MMDiT (SD3.5) e DiT puro (Flux). Por que transformers escalam melhor, por que SD3 usa rectified flow, e o que o paper de Peebles & Xie 2023 mostrou que mudou o jogo.
Perguntas frequentes
❓ Para que serve o autocodificador no Stable Diffusion?
❓ Por que a arquitetura em U é usada na difusão?
❓ O que mudou nas arquiteturas mais recentes?
Fixando
Como o texto do prompt influencia a imagem dentro da rede de remoção de ruído?
Por que não se pode trocar o codificador-decodificador de um modelo pelo de outra geração?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…