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 diffusers para 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:

  1. 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.
  2. 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: float16 em 4080/4090, bfloat16 em A100 e mais novas, int8 (via bitsandbytes ou compel) 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

  1. (Fácil) Gere o mesmo prompt com guidance_scale em [1, 3, 5, 7.5, 10, 15]. Descreva como a imagem muda. Em qual valor de guidance os artefatos aparecem?
  2. (Médio) Pegue qualquer fotografia real, passe-a por StableDiffusionImg2ImgPipeline com strength em [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?
  3. (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

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