Phase 08 - Lesson 07
Latent Diffusion & Stable Diffusion
La difusión en el espacio de píxeles en imágenes de 512×512 es un crimen de guerra computacional. Rombach et al. (2022) notaron que no se necesitan todas las 786k dimensiones para generar una imagen — se necesita lo suficiente para capturar la estructura semántica y un decodificador separado para el resto. Ejecuta la difusión dentro del espacio latente de un VAE. Esa única idea es Stable Diffusion.
Tipo: Build Lenguajes: Python Prerrequisitos: Phase 8 · 02 (VAE), Phase 8 · 06 (DDPM), Phase 7 · 09 (ViT) Tiempo: ~75 minutos
El Problema
La difusión en el espacio de píxeles a 512² significa que la U-Net se ejecuta en tensores de forma [B, 3, 512, 512]. Cada paso de muestreo es de ~100 GFLOPS para una U-Net de 500M de parámetros. Cincuenta pasos equivalen a 5 TFLOPS por imagen. Entrenar con mil millones de imágenes hace que la factura de cómputo sea absurda.
La mayoría de esos FLOPs se destinan a empujar detalles perceptualmente irrelevantes a través de la red — la textura de alta frecuencia que un VAE con pérdidas (lossy VAE) podría comprimir. La idea de Rombach: entrenar un VAE una vez (la primera etapa), congelarlo y ejecutar la difusión por completo en el espacio latente de 4 canales de 64×64 (la segunda etapa). La misma U-Net. 1/16 de los píxeles. ~64 veces menos FLOPs para una calidad comparable.
Esta es la receta de Stable Diffusion. SD 1.x / 2.x usó una U-Net de 860M sobre latentes de 64×64×4, SDXL usó una U-Net de 2.6B sobre 128×128×4, SD3 cambió la U-Net por un Diffusion Transformer (DiT) con flow matching. Flux.1-dev (Black Forest Labs, 2024) incluye un DiT-MMDiT de 12B de parámetros. Todos se ejecutan sobre el mismo sustrato de dos etapas.
El Concepto
Dos etapas, entrenadas por separado.
Etapa 1 — VAE. Codificador
E(x) → z, decodificadorD(z) → x. Compresión objetivo: submuestreo de 8× en cada eje espacial + ajuste de canales para que el tamaño latente total sea ~1/16 del conteo de píxeles. Loss = reconstrucción (L1 + LPIPS perceptual) + KL (peso pequeño para quezno sea forzado a ser demasiado gaussiano, ya que no necesitamos un muestreo exacto dez). A menudo se entrena con una loss adversarial para que las imágenes decodificadas sean nítidas.Etapa 2 — difusión en
z. Trataz = E(x_real)como los datos. Entrena una U-Net (or DiT) para eliminar el ruido dez_t. En la inferencia: muestreaz_0mediante difusión, luegox = D(z_0).
Condicionamiento de texto. Dos componentes adicionales. Un codificador de texto congelado (CLIP-L para SD 1.x, CLIP-L+OpenCLIP-G para SD 2/XL, T5-XXL para SD3 and Flux). Una inyección de atención cruzada (cross-attention): cada bloque de la U-Net toma [Q = image features, K = V = text tokens] y los mezcla. Los tokens son la única forma en que el texto influye en la imagen.
La función de pérdida es idéntica a la de la Lección 06. Mismo MSE de DDPM / flow matching sobre el ruido. Solo cambias el dominio de los datos.
Variantes de arquitectura
| Modelo | Año | Backbone | Forma 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 | mismo | muestreo de 1-4 pasos |
| 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 pasos |
La tendencia: reemplazar la U-Net con DiT (transformer sobre parches latentes), escalar el codificador de texto (T5 supera a CLIP en adherencia al prompt), aumentar los canales latentes (4 → 16 ofrece más margen para detalles).
Build It
code/main.py apila un "VAE" unidimensional de juguete (codificador + decodificador de identidad, para demostración; un VAE real sería una red convolucional) sobre el DDPM de la Lección 06 y añade condicionamiento de clase con classifier-free guidance. Muestra que la misma loss de difusión funciona ya sea que se ejecute sobre valores unidimensionales puros o sobre valores codificados — el insight fundamental.
Paso 1: codificador/decodificador
def encode(x): return x * 0.5 # toy "compression" to smaller scale
def decode(z): return z * 2.0
Un VAE real tiene pesos entrenados. Con fines pedagógicos, este mapa lineal es suficiente para mostrar que la difusión opera sobre z sin preocuparse por el espacio de datos original.
Paso 2: difusión en el espacio z
Mismo DDPM de la Lección 06. Los datos que ve la red son z = E(x). Después de muestrear z_0, decodifica con D(z_0).
Paso 3: classifier-free guidance
Durante el entrenamiento, descarta la etiqueta de clase el 10% del tiempo (reemplázala con un token nulo). En la inferencia, calcula tanto ε_cond como ε_uncond, luego:
eps_cfg = (1 + w) * eps_cond - w * eps_uncond
w = 0 = sin guía (diversidad total), w = 3 = predeterminado, w = 7+ = saturado / demasiado nítido.
Paso 4: condicionamiento de texto (concepto, no código)
Reemplaza la etiqueta de clase con la salida de un codificador de texto congelado. Alimenta el embedding de texto a la U-Net a través de cross-attention:
h = h + CrossAttention(Q=h, K=text_embed, V=text_embed)
Esta es la única diferencia sustantiva entre un modelo de difusión condicional de clase y Stable Diffusion.
Trampas
- Desajuste de escala del VAE. Los VAE de SD 1.x tienen una constante de escala (
scaling_factor ≈ 0.18215) que se aplica después de la codificación. Olvidar esto hace que la U-Net se entrene con latentes que tienen una varianza totalmente incorrecta. Cada checkpoint incluye una. - Codificador de texto silenciosamente incorrecto. SD3 necesita T5-XXL con >=128 tokens, y la alternativa de usar solo CLIP tiene pérdidas. Siempre verifica
use_t5=Trueo la fidelidad al prompt se desplomará. - Mezcla de espacios latentes. SDXL, SD3 y Flux usan VAE diferente. Un LoRA entrenado en latentes de SDXL no funcionará en SD3. Hugging Face diffusers 0.30+ se niega a cargar checkpoints que no coincidan.
- CFG demasiado alto.
w > 10produce imágenes saturadas, con aspecto aceitoso, y sobreajusta el prompt a costa de la diversidad. El punto ideal esw = 3-7. - Fuga de prompts negativos. Un prompt negativo vacío se convierte en el token nulo; un prompt negativo lleno se convierte en el
ε_uncond. No son lo mismo; algunos pipelines asumen silenciosamente el nulo por defecto.
Use It
Stacks de producción en 2026:
| Objetivo | Backbone recomendado |
|---|---|
| Dominio estrecho, datos emparejados, entrenando un modelo desde cero | Ajuste fino (fine-tune) de SDXL (LoRA / completo) — más rápido de implementar |
| Texto a imagen de dominio abierto, pesos abiertos | Flux.1-dev (12B, Apache / no comercial) o SD3.5-Large |
| Inferencia más rápida, pesos abiertos | Flux.1-schnell (1-4 pasos, Apache) o SDXL-Lightning |
| Mejor adherencia al prompt, alojado | GPT-Image / DALL-E 3 (aún), Midjourney v7, Imagen 4 |
| Workflows de edición | Flux.1-Kontext (Dic 2024) — acepta nativamente imagen + texto |
| Investigación, baseline | SD 1.5 — antiguo pero bien estudiado |
Ship It
Guarda outputs/skill-sd-prompter.md. La skill toma un prompt de texto + estilo objetivo y genera: modelo + checkpoint, escala CFG, sampler, prompt negativo, resolución, combinación opcional de ControlNet/IP-Adapter y una lista de verificación de QA paso a paso.
Ejercicios
- Fácil. Ejecuta
code/main.pycon guíaw ∈ {0, 1, 3, 7, 15}. Registra la media de las muestras por clase. ¿En quéwlas medias de las clases divergen más allá de las medias de los datos reales? - Medio. Cambia el codificador lineal de juguete por un par de codificador/decodificador tanh-MLP con una loss de reconstrucción. Vuelve a entrenar la difusión en los nuevos latentes. ¿Cambia la calidad de la muestra?
- Difícil. Configura una inferencia real de Stable Diffusion con diffusers: carga
sdxl-base, ejecuta 30 pasos de Euler con CFG=7 y toma el tiempo. Ahora cambia asdxl-turbocon 4 pasos y CFG=0. Mismo tema, diferente calidad — describe qué cambió y por qué.
Términos Clave
| Término | Lo que la gente dice | Lo que realmente significa |
|---|---|---|
| Primera etapa | "El VAE" | Par codificador/decodificador entrenado; comprime 512² a 64². |
| Segunda etapa | "La U-Net" | Modelo de difusión sobre el espacio latente. |
| CFG | "Escala de guía" | (1+w)·ε_cond - w·ε_uncond; ajusta la fuerza de condicionamiento. |
| Token nulo | "Embedding de prompt vacío" | Embedding incondicional usado para ε_uncond. |
| Cross-attention | "Cómo entra el texto" | Cada bloque de la U-Net atiende a los tokens de texto como K y V. |
| DiT | "Transformer de Difusión" | Reemplaza la U-Net por un transformer sobre parches latentes; escala mejor. |
| MMDiT | "DiT multimodal" | Arquitectura de SD3: flujos de texto e imagen con atención conjunta. |
| Factor de escala del VAE | "Número mágico" | Divide los latentes entre ~5.4 para que la difusión opere en un espacio de varianza unitaria. |
Nota de producción: ejecutar Flux-12B en una GPU de consumo de 8GB
la integración de referencia de Flux es la receta canónica para "tengo una GPU de consumo, ¿puedo implementar esto?". El truque es la misma receta de tres perillas que la literatura de inferencia en producción enumera aplicada a un DiT de difusión:
- Carga escalonada. Flux tiene tres redes que nunca necesitan coexistir en VRAM: el codificador de texto T5-XXL (~10 GB en fp32), CLIP-L (pequeño), el MMDiT de 12B y el VAE. Codifica el prompt primero, elimina los codificadores, carga el DiT, elimina el ruido, elimina el DiT, carga el VAE, decodifica. Las GPU de consumo de 8GB solo admiten una etapa a la vez.
- Cuantización de 4 bits a través de bitsandbytes.
BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16)tanto en el codificador T5 como en el DiT. Reduce la memoria en 8×, y la caída de calidad es imperceptible para texto a imagen según los benchmarks de Aritra (enlazados en el notebook). - CPU offload.
pipe.enable_model_cpu_offload()intercambia módulos automáticamente entre CPU y GPU a medida que avanza cada paso del forward pass. Añade un 10-20% de latencia pero hace que el pipeline funcione de todos modos.
La contabilidad de memoria es: 10 GB T5 / 8 = 1.25 GB cuantizado, 12 B params × 0.5 bytes = ~6 GB DiT cuantizado, más activaciones. En términos de stas00, este es el extremo de la inferencia TP=1: sin paralelismo de modelo, cuantización máxima. Para producción, ejecutarías TP=2 o TP=4 en H100; para una sola laptop de desarrollo, esta es la receta.
Lectura 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 — Familia Flux.1.
- Hugging Face Diffusers docs — implementación de referencia para cada checkpoint de arriba.