Phase 08 - Lesson 08

ControlNet, LoRA y Condicionamiento

El texto por sí solo es una señal de control imprecisa. ControlNet te permite clonar un modelo de difusión preentrenado y guiarlo con un mapa de profundidad, un esqueleto de pose, un boceto (scribble) o una imagen de bordes. LoRA te permite ajustar un modelo de 2,000 millones de parámetros entrenando solo 10 millones de parámetros. Juntos, transformaron Stable Diffusion de un juguete al pipeline de imágenes de 2026 que se entrega en todas las agencias.

Tipo: Build Lenguajes: Python Prerrequisitos: Fase 8 · 07 (Difusión Latente), Fase 10 (LLMs desde Cero — para la base de LoRA) Tiempo: ~75 minutos

El Problema

Un prompt como "a woman in a red dress walking a dog on a busy street" no le da información al modelo sobre dónde está el perro, qué pose tiene la mujer o la perspectiva de la calle. El texto define aproximadamente el 10% de lo que necesitas para especificar una imagen. El resto es visual y no se puede describir de manera eficiente con palabras.

Entrenar un nuevo modelo condicional desde cero para cada señal (pose, profundidad, canny, segmentación) es prohibitivo. Lo ideal es mantener congelado el backbone de SDXL de 2.6B de parámetros, conectar una pequeña red secundaria (side-network) que lea el condicionamiento y hacer que esta modifique ligeramente las características intermedias del backbone. Eso es ControlNet.

También quieres enseñarle nuevos conceptos al modelo (tu rostro, tu producto, tu estilo) sin volver a entrenar el modelo completo. Quieres un delta 100 veces menor. Eso es LoRA: adaptadores de bajo rango (low-rank adapters) que se acoplan a los pesos de atención existentes.

ControlNet + LoRA + texto = el conjunto de herramientas del profesional en 2026. La mayoría de los pipelines de imágenes en producción superponen de 2 a 5 LoRAs, de 1 a 3 ControlNets y un IP-Adapter sobre una base SDXL / SD3 / Flux.

El Concepto

ControlNet clona el codificador; LoRA añade deltas de bajo rango

ControlNet (Zhang et al., 2023)

Toma un SD preentrenado. Clona la mitad del codificador (encoder) de la U-Net. Congela el original. Entrena el clon para aceptar una entrada de condicionamiento adicional (bordas, profundidad, pose). Conecta el clon de vuelta a la mitad del decodificador (decoder) del original con conexiones de atajo de convolución cero (zero-convolution) (convoluções 1×1 inicializadas en cero: comienzan como una operación nula o no-op y aprenden un delta).

SD U-Net decoder:   ... ← orig_enc_features + zero_conv(controlnet_enc(condition))

La inicialización de convolución cero (zero-conv) significa que ControlNet comienza como una identidad: no causa daños incluso antes del entrenamiento. Entrénalo con 1 millón de triples (prompt, condición, imagen) con la pérdida de difusión estándar.

Los ControlNets por modalidad se distribuyen como modelos secundarios pequeños (~360M para SDXL, ~70M para SD 1.5). Puedes componerlos en la inferencia:

features += weight_a * control_a(depth) + weight_b * control_b(pose)

LoRA (Hu et al., 2021)

Para cualquier capa lineal W ∈ R^{d×d} en el modelo, congela W y añade un delta de bajo rango (low-rank):

W' = W + ΔW,  ΔW = B @ A,  A ∈ R^{r×d},  B ∈ R^{d×r}

con r << d. Un rango (rank) de 4 a 16 es el estándar para la atención, y de 64 a 128 para ajustes finos intensos. Cantidad de nuevos parámetros: 2 · d · r en lugar de . Para la atención de SDXL con d=640, r=16: 20k parámetros por adaptador en lugar de 410k, una reducción de 20 veces. En todo el modelo, un LoRA suele pesar entre 20 y 200 MB, frente a los 5 GB de la base.

En la inferencia puedes escalar el LoRA: W' = W + α · B @ A. Lo normal es usar α = 0.5-1.5. Múltiples LoRAs se apilan de forma aditiva (con la advertencia habitual de que interactúan de formas no lineales).

IP-Adapter (Ye et al., 2023)

Un adaptador diminuto que acepta una imagen como condicionamiento (junto con el texto). Utiliza el codificador de imágenes de CLIP para generar tokens de imagen y los inyecta en la atención cruzada (cross-attention) junto con los tokens de texto. Pesa alrededor de ~20 MB por modelo base. Te permite "generar una imagen con el estilo de esta referencia" sin necesidad de un LoRA.

Matriz de composibilidad

Herramienta Qué controla Tamaño Cuándo usarlo
ControlNet Estructura espacial (pose, profundidad, bordes) 70-360MB Distribución exacta, composición
LoRA Estilo, sujeto, concepto 20-200MB Personalización, estilo
IP-Adapter Estilo o sujeto a partir de una imagen de referencia 20MB Ningún texto puede describir la apariencia
Textual Inversion Concepto único como un nuevo token 10KB Legado, reemplazado mayoritariamente por LoRA
DreamBooth Ajuste fino completo en un sujeto 2-5GB Identidad fuerte, alto costo computacional
T2I-Adapter Alternativa más ligera a ControlNet 70MB Dispositivos de borde, presupuesto de inferencia limitado

ControlNet ≈ espacial. LoRA ≈ semántico. Usa ambos.

Implementación

code/main.py simula los dos mecanismos en 1-D:

  1. LoRA. Una capa lineal preentrenada W. Congélala. Entrena un B @ A de bajo rango para que W + BA coincida con una capa lineal objetivo. Demuestra que r = 1 es suficiente para aprender perfectamente una corrección de rango 1 (rank-1).

  2. ControlNet-lite. Un predictor de "base congelada" y una "red secundaria" (side network) que lee una señal adicional. La salida de la red secundaria está controlada por una compuerta (gate) escalar entrenable inicializada en cero (nuestra versión de zero-conv). Entrénala y observa cómo se incrementa la compuerta.

Paso 1: Matemática de LoRA

def lora(W, A, B, x, alpha=1.0):
    # W is frozen; A, B are the trainable low-rank factors.
    return [W[i][j] * x[j] for i, j in ...] + alpha * (B @ (A @ x))

Paso 2: Red secundaria con inicialización en cero

side_out = control_net(x, condition)
gated = gate * side_out  # gate initialized to 0
h = base(x) + gated

En el paso 0, la salida es idéntica a la base. El entrenamiento inicial actualiza la compuerta gate lentamente: sin desvíos catastróficos.

Errores Comunes

  • Sobreescalado de LoRAs (Over-scaling). Utilizar α = 2 o α = 3 es un truque común para "hacerlo más fuerte" que genera salidas demasiado estilizadas o corruptas. Mantén α ≤ 1.5.
  • Conflicto de pesos de ControlNet. Usar un ControlNet de Pose con peso 1.0 y un ControlNet de Profundidade con peso 1.0 generalmente causa distorciones (overshoot). Un valor seguro por defecto es que la suma de los pesos sea ≈ 1.0.
  • LoRA en la base incorrecta. Los LoRAs de SDXL no tienen ningún efecto (no-op) de forma silenciosa en SD 1.5 porque las dimensiones de atención no coinciden. Diffusers mostrará una advertencia a partir de la versión 0.30+.
  • Desviación de Textual Inversion. Los tokens entrenados en un checkpoint se desvían de forma severa en otro. LoRA es mucho más portable.
  • Fusión de pesos y almacenamiento de LoRA. Puedes incorporar (bake) un LoRA en los pesos del modelo base para una inferencia más rápida (sin adición en tiempo de ejecución), pero perderás la capacidad de escalar α en tiempo de ejecución. Conserva ambas versiones.

Cómo Usarlo

Objetivo Pipeline de 2026
Reproducir el estilo artístico de una marca LoRA entrenado en ~30 imágenes seleccionadas con rango (rank) 32
Poner mi rostro en una imagen generada DreamBooth o LoRA + IP-Adapter-FaceID
Pose específica + prompt ControlNet-Openpose + SDXL + texto
Composición basada en profundidad ControlNet-Depth + SD3
Referencia + prompt IP-Adapter + texto
Distribución exacta ControlNet-Scribble o ControlNet-Canny
Reemplazar el fondo ControlNet-Seg + Inpainting (Lección 09)
Estilo rápido en 1 paso LCM-LoRA en SDXL-Turbo

Prática

Guarda outputs/skill-sd-toolkit-composer.md. La habilidad (skill) recibe una tarea (recursos de entrada: prompt, imagen de referencia opcional, pose opcional, profundidad opcional, boceto opcional) y genera la pila de herramientas, los pesos y un protocolo de semilla (seed) reproducible.

Ejercicios

  1. Fácil. En code/main.py, varía el rango r de LoRA de 1 a 4. ¿En qué rango coincide exactamente el LoRA con un delta objetivo de rango 2?
  2. Medio. Entrena dos LoRAs diferentes en dos transformaciones objetivo. Cárgalos juntos y muestra su interacción aditiva. ¿En qué punto la interacción rompe la linealidad?
  3. Difícil. Utiliza diffusers para apilar: SDXL-base + Canny-ControlNet (peso 0.8) + un LoRA de estilo (α 0.8) + IP-Adapter (peso 0.6). Mide el compromiso (trade-off) entre FID y adherencia al prompt a medida que varían los pesos de la pila.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
ControlNet "Control espacial" Codificador clonado + atajos zero-conv; lee una imagen de condicionamiento.
Zero convolution "Comienza como identidad" Convolución 1×1 inicializada en cero; ControlNet comienza como una operación nula (no-op).
LoRA "Adaptador de bajo rango" W + B @ A, r << d; 100 veces menos parámetros que un ajuste fino completo.
rank r "La perilla de ajuste" Compresión de LoRA; típicamente de 4 a 16, 64+ para personalizaciones intensas.
α "Fuerza del LoRA" Escala en tiempo de ejecución del delta de LoRA.
IP-Adapter "Imagen de referencia" Adaptador pequeño de condicionamiento de imagen mediante tokens de imagen CLIP.
DreamBooth "Ajuste fino completo del sujeto" Entrena el modelo completo con ~30 imágenes de un sujeto.
Textual Inversion "Nuevo token" Aprende solo un nuevo embedding de palabra; legado, mayoritariamente reemplazado.

Nota de producción: Intercambio de LoRAs, carriles de ControlNet y servicio multi-tenant

Un SaaS real de texto a imagen sirve cientos de LoRAs y una docena de ControlNets sobre el mismo checkpoint base. El problema de servicio se parece mucho a la multi-tenencia (multi-tenancy) en LLMs (la literatura de producción aborda el caso de los LLMs bajo procesamiento por lotes continuo — continuous batching — y LoRAX / S-LoRA):

  • Intercambia LoRAs en tiempo real (hot-swap), no los fusiones. Fusionar W' = W + α·B·A en la base ofrece una inferencia por paso ~3-5% más rápida, pero congela α y la base. Mantén los LoRAs activos en VRAM como deltas de rango r; diffusers expone pipe.load_lora_weights() + pipe.set_adapters([...], adapter_weights=[...]) para la activación por solicitud. El costo de intercambio son los pesos de tamaño 2 · d · r · num_layers, en escala de MB y en menos de un segundo.
  • ControlNet como un segundo carril de atención. El codificador clonado se ejecuta en paralelo con la base. Dos ControlNets con peso 1.0 cada uno = dos pasadas hacia adelante (forward passes) adicionales por paso, no una sola pasada unificada. El margen del tamaño del lote disminuye de forma cuadrática. Presupuesta aproximadamente ~1.5 veces el costo del paso por cada ControlNet activo.
  • LoRAs cuantizados también. Si cuantizaste la base (ver Lección 07, Flux en 8GB), el delta de LoRA también se cuantiza limpiamente a 8 o 4 bits. La carga estilo QLoRA te permite apilar de 5 a 10 LoRAs sobre una base Flux de 4 bits sin agotar la memoria.

Específico de Flux: el notebook Flux-on-8GB de Niels cuantiza la base a 4 bits; apilar un LoRA de estilo (pipe.load_lora_weights("user/style-lora")) sobre esa base cuantizada en weight_name="pytorch_lora_weights.safetensors" sigue funcionando. Esta es la receta que la mayoría de las agencias SaaS entregan en 2026.

Lectura Adicional

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