LECCIÓN 9 · BLOQUE 3

Por qué fine-tuning completo es caro

En la Lección 5 viste que LLaMA-7B necesita ~60 GB solo para los pesos. ¿Por qué? ¿Por qué fine-tuning necesita todavía más? Esta lección lo calcula con precisión y presenta las tres técnicas para reducir ese coste — motivando la LoRA de la próxima lección.

Recuerda de las lecciones anteriores: en las Lecciones 5-8 aprendiste fine-tuning completo sobre DistilBERT (66M parámetros). Funcionó porque DistilBERT es pequeño — cabe cómodamente en cualquier GPU. Hoy entendemos por qué con modelos más grandes (BERT-large 340M, LLaMA-7B, Mistral 7B) el fine-tuning completo se convierte en un problema de ingeniería serio.

1. Los cuatro ocupantes de memoria en una GPU

Cuando entrenas un modelo, la GPU necesita guardar en memoria cuatro cosas distintas. Mucha gente solo piensa en los pesos del modelo, pero eso es solo una cuarta parte del total — y no siempre la parte más grande.

MEMORIA EN GPU DURANTE FINE-TUNING — LOS CUATRO COMPONENTES
Diagrama de barras apiladas mostrando los cuatro componentes de memoria en GPU durante fine-tuning Cuatro barras de distintos tamaños representando pesos del modelo, gradientes, estado del optimizador Adam y activaciones intermedias. Memoria relativa por componente (fine-tuning con Adam en FP32) PESOS 1× el modelo GRADIENTES 1× el modelo ESTADO ADAM 2× el modelo (momentum + variance) ACTIVACIONES INTERMEDIAS depende del batch_size y la longitud del texto — puede ser muy grande TOTAL ≈ (pesos × 4) + activaciones Para un modelo de 7B parámetros en FP32: ≈ 112 GB solo de pesos+gradientes+Adam Pesos Gradientes Adam state Activaciones

El estado del optimizador Adam es el componente más costoso: guarda dos valores por cada peso del modelo.

2. Cálculo numérico preciso para LLaMA-7B

Vamos a calcular exactamente cuántos gigabytes necesita fine-tuning completo de LLaMA-7B en condiciones estándar (FP32, optimizador Adam). No dejamos ningún paso sin mostrar.

Primero: un parámetro es un número (un peso) en el modelo. LLaMA-7B tiene 7.000.000.000 parámetros (siete mil millones). En el formato FP32 (float de 32 bits — el formato estándar en computación), cada número ocupa 32 bits = 4 bytes.

✍️ Cálculo completo de memoria para LLaMA-7B (fine-tuning completo, FP32)
LLaMA-7B tiene exactamente 6.738.415.616 parámetros
(≈ 6.7 mil millones — "7B" es el redondeo de marketing)
Usaremos 7.000.000.000 para simplificar el cálculo.

───────────────────────────────────────────────────────────

COMPONENTE 1: PESOS DEL MODELO
  7.000.000.000 params × 4 bytes/param
  = 28.000.000.000 bytes
  = 28.000.000.000 / (1024 × 1024 × 1024) GB
  = 28.000.000.000 / 1.073.741.824
  ≈ 26.1 GB

───────────────────────────────────────────────────────────

COMPONENTE 2: GRADIENTES
  (exactamente el mismo tamaño que los pesos)
  7.000.000.000 params × 4 bytes/param ≈ 26.1 GB

  ¿Por qué el mismo tamaño?
  Cada peso tiene exactamente un gradiente.
  El gradiente es un número del mismo tipo (float32 = 4 bytes).

───────────────────────────────────────────────────────────

COMPONENTE 3: ESTADO DEL OPTIMIZADOR ADAM
  Adam guarda DOS valores por cada peso:
    · m_t (momento de primer orden — media exponencial de gradientes)
    · v_t (momento de segundo orden — media de gradientes al cuadrado)

  Costo: 7.000.000.000 × 2 × 4 bytes = 52.2 GB

  ¡El estado de Adam pesa el DOBLE del modelo!

───────────────────────────────────────────────────────────

COMPONENTE 4: ACTIVACIONES INTERMEDIAS (batch_size=4, seq_len=512)
  Las activaciones son los resultados intermedios de cada capa
  que hay que guardar para el backward pass.

  Para LLaMA-7B con batch_size=4, seq_len=512:
  ≈ 4 × 512 × 4096 × 32_capas × 4_bytes ≈ ~8.6 GB
  (esto es una estimación — varía según la implementación)

───────────────────────────────────────────────────────────

TOTAL APROXIMADO:
  Pesos:       26.1 GB
  Gradientes:  26.1 GB
  Adam state:  52.2 GB
  Activaciones: 8.6 GB
  ─────────────────────
  TOTAL:      ≈113.0 GB

La GPU A100 más grande de NVIDIA tiene 80 GB.
LLaMA-7B en fine-tuning completo NO CABE en una sola A100.
Necesitarías 2 A100 de 80 GB (costo: ~$20.000 cada una en 2024).
⚠️ Y eso es con un modelo "pequeño" de 7B. LLaMA-70B necesita ~1.1 TB solo para pesos + gradientes + Adam. GPT-4 se estima en varios cientos de miles de millones de parámetros. ¿Ves el problema?

3. Gradient accumulation — procesar menos a la vez sin perder calidad

El gradient accumulation (acumulación de gradientes) es una técnica para simular un batch grande cuando la GPU no tiene suficiente memoria para procesarlo de una vez.

La idea es simple: en vez de procesar 32 ejemplos a la vez y actualizar los pesos, procesas 4 ejemplos a la vez durante 8 pasos, acumulas (sumas) los gradientes de cada paso, y solo al final de los 8 pasos actualizas los pesos. El resultado matemático es idéntico.

✍️ Sin gradient accumulation vs con gradient accumulation
SIN gradient accumulation (batch real = 32):
  Paso 1: procesa 32 ejemplos → calcula gradientes → actualiza pesos
  Paso 2: procesa 32 ejemplos → calcula gradientes → actualiza pesos
  ...
  Memoria necesaria: suficiente para 32 ejemplos simultáneos

CON gradient accumulation (steps=8, batch por paso=4):
  Paso 1: procesa 4 ejemplos → calcula gradientes → NO actualiza (acumula)
  Paso 2: procesa 4 ejemplos → calcula gradientes → NO actualiza (acumula)
  ...
  Paso 8: procesa 4 ejemplos → calcula gradientes → SÍ actualiza (borra acumulado)
  Memoria necesaria: suficiente para 4 ejemplos simultáneos

Resultado matemático: equivalente a batch_size=32
(las sumas de gradientes de 4×8 = suma de gradientes de 32)

¿Cuándo usar gradient_accumulation_steps=8?
  · Cuando quieres batch efectivo de 32 pero solo caben 4 en GPU
  · Coste: es 8 veces más lento (8 pasos en vez de 1)
  · Beneficio: reduce memoria de activaciones por factor de 8
# Configurar gradient accumulation en TrainingArguments
args = TrainingArguments(
    output_dir="./modelo",
    per_device_train_batch_size=4,       # solo 4 en GPU a la vez (menos memoria)
    gradient_accumulation_steps=8,     # batch efectivo = 4 × 8 = 32
    # ... resto de argumentos
)

# El Trainer lo maneja automáticamente
# Al hacer trainer.train(), internamente:
#   for step, batch in enumerate(dataloader):
#       loss = model(batch)
#       loss = loss / 8  ← normalizar
#       loss.backward()  ← acumular, no borrar gradientes
#       if (step + 1) % 8 == 0:
#           optimizer.step()  ← actualizar solo cada 8 pasos
#           optimizer.zero_grad()
print("gradient_accumulation_steps=8 configurado")
print("Batch efectivo = 4 × 8 = 32 (equivalente a batch_size=32)")
salida realgradient_accumulation_steps=8 configurado Batch efectivo = 4 × 8 = 32 (equivalente a batch_size=32)

4. Mixed precision training — FP16/BF16 en lugar de FP32

Hasta ahora hemos asumido que todos los números se guardan en FP32 (32 bits = 4 bytes). Pero los pesos de un modelo no necesitan tanta precisión para funcionar bien. En vez de usar 32 bits por número, podemos usar 16 bits — reduciendo la memoria a la mitad.

Analogía: para medir la distancia entre tu casa y el trabajo, no necesitas precisión de milímetros — con metros es suficiente. Los pesos del modelo son similares: no necesitan 7 dígitos decimales de precisión — con 3-4 ya funciona bien.
✍️ FP32 vs FP16 vs BF16 — diferencias y usos
FP32 (float de 32 bits):
  · 4 bytes por número
  · Rango: ±3.4 × 10^38
  · Precisión: ~7 dígitos decimales
  · Uso: cálculo estándar, gradientes (¡siempre en FP32!)

FP16 (float de 16 bits):
  · 2 bytes por número — MITAD de memoria
  · Rango: ±65.504 (mucho menor que FP32)
  · Precisión: ~3-4 dígitos decimales
  · Problema: overflow fácil (números > 65.504 se convierten en Inf o NaN)
  · Uso: activaciones y pesos durante forward/backward

BF16 (bfloat16, Brain Float):
  · 2 bytes por número — también MITAD de memoria
  · Rango: ±3.4 × 10^38 (igual rango que FP32 ← esto es crucial)
  · Precisión: ~2-3 dígitos decimales (menor que FP16)
  · No tiene el problema de overflow
  · Uso: el preferido actualmente para LLMs (si la GPU lo soporta)
  · Soportado en: A100, H100, RTX 3090+, Apple M1+

"Mixed" = los pesos se guardan en FP16/BF16 pero los gradientes
           y las actualizaciones se calculan en FP32 para estabilidad
# Activar mixed precision en TrainingArguments
args = TrainingArguments(
    output_dir="./modelo",
    fp16=True,   # usar FP16 (para GPUs NVIDIA más viejas: V100, T4)
    # bf16=True,  # usar BF16 (para NVIDIA A100, H100, RTX 3090+)
    # ... resto de argumentos
)

# Verificar soporte de BF16 en la GPU actual
import torch
soporta_bf16 = torch.cuda.is_bf16_supported() if torch.cuda.is_available() else False
print(f"GPU disponible: {torch.cuda.is_available()}")
print(f"BF16 soportado: {soporta_bf16}")
print(f"Recomendado: {'bf16=True' if soporta_bf16 else 'fp16=True'}")
salida realGPU disponible: True BF16 soportado: True Recomendado: bf16=True
✍️ Impacto de mixed precision en LLaMA-7B
SIN mixed precision (todo FP32):
  Pesos:       26.1 GB
  Gradientes:  26.1 GB
  Adam state:  52.2 GB  ← Adam SIEMPRE en FP32
  SUBTOTAL:   104.4 GB

CON mixed precision BF16:
  Pesos (BF16):      13.1 GB  ← MITAD
  Gradientes (BF16): 13.1 GB  ← MITAD
  Adam state (FP32): 52.2 GB  ← igual (Adam necesita FP32 para estabilidad)
  SUBTOTAL:          78.4 GB

Ahorro: 26 GB (25% de reducción)
¿Por qué Adam sigue en FP32?
  Los valores de momentum y variance de Adam son muy pequeños
  (del orden de 1e-8 a 1e-4). En FP16/BF16 estos valores se redondearían
  a cero y el optimizador dejaría de funcionar. FP32 es obligatorio aquí.

5. Gradient checkpointing — cambiar velocidad por memoria

Las activaciones intermedias (componente 4) son el problema más variable. Aquí está el porqué de que existan: para hacer el backward pass y calcular los gradientes, PyTorch necesita saber cuáles fueron los valores en cada capa durante el forward pass. Normalmente guarda todo esto en memoria durante el forward — de ahí el enorme coste.

El gradient checkpointing (también llamado activation checkpointing) propone un trueque: en vez de guardar todas las activaciones, guarda solo algunas (los "checkpoints") y recalcula las demás durante el backward pass cuando se necesitan.

Analogía: imagina que tienes que seguir el camino de regreso de una caminata. En vez de marcar cada árbol que pasaste (guardar todas las activaciones), solo marcas los cruces principales (checkpoints). Si necesitas saber qué hay entre dos cruces, simplemente caminas ese trecho otra vez (recalculas). Pagas en tiempo, no en papel.
# Activar gradient checkpointing en el modelo
model.gradient_checkpointing_enable()

# En TrainingArguments también hay un flag
args = TrainingArguments(
    output_dir="./modelo",
    gradient_checkpointing=True,
    # ... resto de argumentos
)

# Verificar que está activo
print("Gradient checkpointing habilitado")
print("Coste: entrenamiento ~20-30% más lento")
print("Beneficio: reduce activaciones hasta 10× menos memoria")
salida realGradient checkpointing habilitado Coste: entrenamiento ~20-30% más lento Beneficio: reduce activaciones hasta 10× menos memoria
✍️ Impacto de las tres técnicas combinadas — LLaMA-7B
Configuración base (FP32, sin optimizaciones):
  Pesos:              26.1 GB
  Gradientes:         26.1 GB
  Adam state:         52.2 GB
  Activaciones:        8.6 GB  (batch=4, seq=512)
  ─────────────────────────────
  TOTAL:            ≈ 113.0 GB  (necesitas 2× A100 de 80 GB)

Después de todas las optimizaciones:
  +BF16 pesos/gradientes → pesos: 13.1, gradientes: 13.1
  +Adam en FP32          → Adam:  52.2 (sin cambio)
  +Gradient checkpointing → activaciones: ~0.9 GB (10× reducción)
  ─────────────────────────────
  TOTAL optimizado:       ≈ 79.3 GB  (barely cabría en 1× A100 de 80 GB)

La conclusión honesta:
  Incluso con TODAS las optimizaciones disponibles,
  fine-tuning completo de LLaMA-7B requiere una A100 de 80 GB ($15.000+).
  Eso es exactamente el problema que resuelven LoRA y QLoRA en la siguiente lección.

6. La motivación para PEFT: cambiar solo lo necesario

Aquí viene la pregunta clave: ¿es realmente necesario modificar los 7 billones de parámetros de LLaMA para que clasifique reseñas de restaurantes?

La respuesta, que investigadores de Microsoft demostraron en 2021, es no. Se comprobó experimentalmente que cuando adaptas un modelo grande a una tarea específica, el cambio real en los pesos es de muy bajo rango — matemáticamente, la diferencia entre el modelo original y el adaptado tiene un rango intrínseco muy pequeño.

Analogía: imagina que tienes un chef de cocina francesa con 20 años de experiencia. Para enseñarle a cocinar sushi, no necesitas borrar todo lo que sabe y empezar desde cero — solo añades un módulo pequeño de técnicas japonesas encima de todo su conocimiento previo. La "diferencia" entre el chef original y el adaptado es pequeña, aunque el chef completo es enorme.

PEFT (Parameter-Efficient Fine-Tuning) es el nombre genérico para todas las técnicas que adaptan el modelo cambiando solo una fracción pequeña de los parámetros. Las más importantes son:

Técnica % parámetros modificados Memoria necesaria Calidad vs full FT
Fine-tuning completo 100% 113 GB (LLaMA-7B) Referencia 100%
LoRA (r=8) ~0.5-1% ~18 GB (LLaMA-7B) ~95-99%
QLoRA (r=8, 4-bit) ~0.5-1% ~6-8 GB (LLaMA-7B) ~90-95%
Prompt tuning <0.1% Mínima ~80-90%

LoRA sobre LLaMA-7B necesita ~18 GB — que cabe cómodamente en una RTX 3090 de consumidor ($800 al momento de escribir esto). QLoRA reduce eso a 6-8 GB — que cabe en una RTX 3080 o incluso en Google Colab gratuito.

La intuición matemática de LoRA (que desarrollaremos en la Lección 10): si el cambio necesario ΔW se puede expresar como el producto de dos matrices pequeñas (A × B), no necesitas guardar ni los gradientes ni el estado de Adam de los 7B parámetros originales — solo de las matrices A y B que son miles de veces más pequeñas. Eso es exactamente lo que hace LoRA.

🎮 Calculadora de memoria para fine-tuning

Elige el tamaño del modelo, el tipo de datos y las optimizaciones activadas. La calculadora muestra exactamente cuánta VRAM necesitas.

125M
4
512

8. Lo que aprendiste

Lo que aprendiste hoy: el fine-tuning completo necesita cuatro tipos de memoria en GPU: pesos del modelo (1×), gradientes (1×), estado del optimizador Adam (2× — es el mayor), y activaciones intermedias (variable). Para LLaMA-7B en FP32 sin optimizaciones, el total es ~113 GB — imposible en una sola GPU de consumidor. Tres técnicas alivian esto: gradient accumulation (simula batch grande sin más memoria), mixed precision BF16 (reduce pesos y gradientes a la mitad), y gradient checkpointing (recalcula activaciones en vez de guardarlas, 20% más lento pero 10× menos memoria). Con todas ellas, LLaMA-7B todavía necesita ~79 GB. Eso motiva LoRA: cambiar solo el 0.5-1% de los parámetros para obtener el 95-99% del rendimiento del fine-tuning completo, en una GPU que cuesta 50 veces menos.

En la próxima lección: LoRA — cómo funciona matemáticamente la descomposición de bajo rango, qué matrices se adaptan, el parámetro de escala α, y el código real con la librería PEFT.