Phase 04 - Lesson 11
Stable Diffusion — Arquitectura y Fine-Tuning
Stable Diffusion es un DDPM que se ejecuta en el espacio latente de un VAE preentrenado, condicionado a texto mediante cross-attention, muestreado con un solver de EDO determinista y rápido, y guiado por classifier-free guidance.
Tipo: Aprender + Usar Lenguajes: Python Prerrequisitos: Fase 4 Lección 10 (Diffusion), Fase 7 Lección 02 (Self-Attention) Tiempo: ~75 minutos
Objetivos de aprendizaje
- Recorrer las cinco piezas de un pipeline de Stable Diffusion: VAE, codificador de texto, U-Net, scheduler, safety checker — y lo que cada una de ellas realmente hace
- Explicar la difusión latente y por qué entrenar en un espacio latente de 4x64x64 (en lugar de una imagen 3x512x512) reduce el cómputo en 48x sin pérdida de calidad
- Usar
diffuserspara generar imágenes, ejecutar image-to-image, inpainting y generación guiada por ControlNet - Hacer fine-tuning de Stable Diffusion con LoRA en un pequeño dataset personalizado y cargar el adaptador LoRA en la inferencia
El Problema
Entrenar un DDPM directamente sobre imágenes RGB 512x512 es costoso. Cada paso de entrenamiento hace backprop a través de una U-Net que ve 3x512x512 = 786.432 valores de entrada, y el muestreo toma 50+ forward passes por esa misma U-Net. Al nivel de calidad de Stable Diffusion 1.5 (lanzado en 2022), la difusión en el espacio de píxeles requeriría aproximadamente 256 GPU-meses de entrenamiento y de 10 a 30 segundos por imagen en una GPU de consumo.
El truco que hizo viable el text-to-image de pesos abiertos fue la difusión latente (Rombach et al., CVPR 2022). Entrena un VAE que mapea una imagen 3x512x512 a un tensor latente 4x64x64 y de vuelta, y luego haz la difusión en ese espacio latente. El cómputo cae en (3*512*512)/(4*64*64) = 48x. El muestreo cae de decenas de segundos a menos de dos segundos en la misma GPU.
Casi todo modelo moderno de generación de imágenes — SDXL, SD3, FLUX, HunyuanDiT, Wan-Video — es un modelo de difusión latente con variaciones en el autoencoder, el denoiser (U-Net o DiT) y el condicionamiento de texto. Aprende Stable Diffusion y habrás aprendido la plantilla.
El Concepto
El pipeline
flowchart LR
TXT["Prompt de texto"] --> TE["Codificador de texto<br/>(CLIP-L o T5)"]
TE --> CT["Embedding<br/>de texto"]
NOISE["Ruido<br/>4x64x64"] --> UNET["UNet<br/>(denoiser con<br/>cross-attention<br/>al texto)"]
CT --> UNET
UNET --> SCHED["Scheduler<br/>(DPM-Solver++,<br/>Euler)"]
SCHED --> LATENT["Latente limpio<br/>4x64x64"]
LATENT --> VAE["Decodificador del VAE"]
VAE --> IMG["Imagen 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. El codificador convierte la imagen en latentes (usado en img2img y entrenamiento). El decodificador convierte los latentes de vuelta en una imagen.
- Codificador de texto — codificador de texto CLIP (SD 1.x/2.x), CLIP-L + CLIP-G (SDXL), o T5-XXL (SD3/FLUX). Produce una secuencia de embeddings de tokens.
- U-Net — el denoiser. Tiene capas de cross-attention que atienden desde los latentes al embedding de texto en cada nivel de resolución.
- Scheduler — el algoritmo de muestreo (DDIM, Euler, DPM-Solver++). Elige los sigmas y mezcla el ruido predicho de vuelta en el latente.
- Safety checker — filtro opcional de contenido NSFW / ilegal en la imagen de salida.
Classifier-free guidance (CFG)
El condicionamiento de texto puro aprende epsilon_theta(x_t, t, c) para cada prompt c. El CFG entrena la misma red con c descartado el 10% de las veces (reemplazado por un embedding vacío), produciendo un único modelo que predice tanto el ruido condicional como el incondicional. En la inferencia:
eps = eps_uncond + w * (eps_cond - eps_uncond)
w es la escala de guidance. w=0 es incondicional, w=1 es condicional puro, w>1 empuja la salida hacia ser "más condicionada al prompt" al costo de la diversidad. El valor por defecto de SD es w=7.5.
El CFG es la razón por la que el text-to-image funciona con calidad de producción. Sin él, los prompts sesgan la salida débilmente; con él, los prompts dominan.
Geometría del espacio latente
El latente de 4 canales del VAE no es solo una imagen comprimida. Es una variedad (manifold) donde la aritmética corresponde aproximadamente a ediciones semánticas (prompt engineering + interpolación viven ambos aquí), y donde la U-Net de difusión ha sido entrenada para gastar todo su presupuesto de modelado. Decodificar un latente aleatorio 4x64x64 no produce una imagen de apariencia aleatoria — produce basura, porque solo una submanifold específica de latentes decodifica a imágenes válidas.
Dos consecuencias:
- Img2img = codificar la imagen a latente, agregar ruido parcial, ejecutar el denoiser, decodificar. La estructura de la imagen sobrevive porque la codificación es casi invertible; el contenido cambia según el prompt.
- Inpainting = igual que img2img pero el denoiser solo actualiza las regiones enmascaradas; las regiones no enmascaradas se mantienen en el latente codificado.
La arquitectura de la U-Net
La U-Net de SD es una versión grande de la TinyUNet de la Lección 10 con tres adiciones:
- Bloques de transformer en cada resolución espacial, conteniendo self-attention + cross-attention al embedding de texto.
- Time embedding mediante MLP sobre una codificación sinusoidal.
- Skip connections entre el codificador y el decodificador en resoluciones coincidentes.
Total de parámetros en SD 1.5: ~860M. SDXL: ~2.6B. FLUX: ~12B. El salto en parámetros está principalmente en las capas de atención.
Fine-tuning con LoRA
El fine-tuning completo de Stable Diffusion necesita 20+ GB de VRAM y actualiza 860M de parámetros. LoRA (Low-Rank Adaptation) mantiene el modelo base congelado e inyecta pequeñas matrices de descomposición de bajo rango en las capas de atención. Un adaptador LoRA para SD pesa típicamente de 10 a 50 MB, se entrena en 10 a 60 minutos en una sola GPU de consumo, y se carga en el momento de la inferencia como una modificación 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.
LoRA es la forma en que casi todo fine-tune de la comunidad se distribuye. CivitAI y Hugging Face alojan millones de ellos.
Schedulers que verás
- DDIM — determinista, ~50 pasos, simple.
- Euler ancestral — estocástico, 30-50 pasos, muestras ligeramente más creativas.
- DPM-Solver++ 2M Karras — determinista, 20-30 pasos, valor por defecto de producción.
- LCM / TCD / Turbo — consistency models y variantes destiladas; 1-4 pasos al costo de algo de calidad.
Cambiar de scheduler es un cambio de una línea en diffusers y a veces corrige problemas de muestreo sin ningún reentrenamiento.
Constrúyelo
Esta lección usa diffusers de extremo a extremo en lugar de reconstruir Stable Diffusion desde cero. Las piezas que necesitarías reconstruir (VAE, codificador de texto, U-Net, scheduler) son temas de sus propias lecciones; aquí el objetivo es fluidez con la API de producción.
Paso 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 reduce la VRAM a la mitad sin pérdida de calidad visible. num_inference_steps=25 con el DPM-Solver++ por defecto equivale a num_inference_steps=50 con DDIM.
Paso 2: Cambiar el scheduler
from diffusers import DPMSolverMultistepScheduler, EulerAncestralDiscreteScheduler
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
El estado del scheduler está desacoplado de los pesos de la U-Net. Puedes entrenar con DDPM y muestrear con cualquier scheduler.
Paso 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 es cuánto ruido agregar antes del denoising (0.0 = sin cambios, 1.0 = regeneración completa). 0.5-0.7 es el rango estándar para transferencia de estilo.
Paso 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]
Los píxeles blancos en la máscara son el área a regenerar. Los píxeles negros se preservan.
Paso 5: Carga 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 la fuerza; 0.0 = ningún efecto, 1.0 = efecto total. fuse_lora integra el adaptador en los pesos in situ para ganar velocidad, pero impide el intercambio. Llama a pipe.unfuse_lora() antes de cargar un adaptador diferente.
Paso 6: Entrenamiento de LoRA (esbozo)
El entrenamiento real de LoRA vive en peft o diffusers.training. El esquema:
# 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()
Solo las matrices LoRA reciben gradiente; la U-Net base, el VAE y el codificador de texto quedan congelados. Con un batch size de 1 y gradient checkpointing esto cabe en 8 GB de VRAM.
Úsalo
En producción, las decisiones que realmente tomas:
- Familia de modelo: SD 1.5 para fine-tunes open-source de la comunidad, SDXL para mayor fidelidad, SD3 / FLUX para el estado del arte y requisitos estrictos de licenciamiento.
- Scheduler: DPM-Solver++ 2M Karras para 20-30 pasos, LCM-LoRA cuando la latencia está por debajo de 1s.
- Precisión:
float16en 4080/4090,bfloat16en A100 y más nuevas,int8(mediantebitsandbytesocompel) cuando la VRAM está ajustada. - Condicionamiento: el texto puro funciona; para un control más fuerte, agrega ControlNet (canny, depth, pose) sobre el pipeline base.
Para generación por lotes, AUTO1111 / ComfyUI son las herramientas de la comunidad; para APIs de producción, diffusers + accelerate u optimum-nvidia con compilación TensorRT.
Entrégalo
Esta lección produce:
outputs/prompt-sd-pipeline-planner.md— un prompt que elige SD 1.5 / SDXL / SD3 / FLUX más scheduler y precisión dados un presupuesto de latencia, un objetivo de fidelidad y una restricción de licenciamiento.outputs/skill-lora-training-setup.md— una skill que escribe una config completa de entrenamiento de LoRA para un dataset personalizado, incluyendo captions, rank, batch size y learning rate.
Ejercicios
- (Fácil) Genera el mismo prompt con
guidance_scaleen[1, 3, 5, 7.5, 10, 15]. Describe cómo cambia la imagen. ¿A qué valor de guidance aparecen los artefactos? - (Medio) Toma cualquier fotografía real, pásala por
StableDiffusionImg2ImgPipelineconstrengthen[0.2, 0.4, 0.6, 0.8, 1.0]. ¿Qué strength preserva la composición mientras cambia el estilo? ¿Por qué 1.0 ignora la entrada por completo? - (Difícil) Entrena un LoRA con 10-20 imágenes de un único sujeto (una mascota, un logo, un personaje) y genera escenas inéditas con ese sujeto en ellas. Reporta el rank del LoRA y los pasos de entrenamiento que produjeron la mejor preservación de identidad sin overfitting en las imágenes de entrada.
Términos clave
| Término | Lo que dice la gente | Lo que realmente significa |
|---|---|---|
| Difusión latente | "Difundir en latentes" | Ejecutar todo el DDPM en el espacio latente del VAE (4x64x64) en lugar del espacio de píxeles (3x512x512); ahorro de 48x en cómputo |
| Factor de escala del VAE | "0.18215" | Constante que reescala el latente crudo del VAE a varianza aproximadamente unitaria; hardcoded en todo pipeline SD |
| Classifier-free guidance | "CFG" | Mezclar predicciones de ruido condicional e incondicional; la perilla de inferencia más impactante de todas |
| Scheduler | "Sampler" | El algoritmo que convierte ruido + predicciones del modelo en una trayectoria de latente denoised |
| LoRA | "Adaptador de bajo rango" | Pequeñas matrices de descomposición de bajo rango que hacen fine-tune a las capas de atención sin tocar los pesos base |
| Cross-attention | "Atención texto-imagen" | Atención de los tokens latentes a los tokens de texto; inyecta información del prompt en cada nivel de la U-Net |
| ControlNet | "Condicionamiento de estructura" | Un adaptador entrenado por separado que guía a SD con una entrada extra (canny, depth, pose, segmentación) |
| DPM-Solver++ | "El scheduler por defecto" | Solver de EDO determinista de segundo orden; mejor calidad con pocos pasos (20-30) en 2026 |
Lectura adicional
- High-Resolution Image Synthesis with Latent Diffusion (Rombach et al., 2022) — el paper de Stable Diffusion; incluye cada ablación que justifica el diseño
- Classifier-Free Diffusion Guidance (Ho & Salimans, 2022) — el paper del CFG
- LoRA: Low-Rank Adaptation of Large Language Models (Hu et al., 2021) — LoRA nació en NLP; se transfirió a SD con casi ningún cambio
- diffusers documentation — la referencia para todo pipeline SD / SDXL / SD3 / FLUX