Probado con PyTorch 2.14.1 · Transformers 5.21.0 · PEFT 0.21.2 · TRL 1.16.0 · bitsandbytes 0.50.2 · Docker en arm64 sin GPU · verificado

Actualizado: 2026-10-27

LoRA (Low-Rank Adaptation) entrena dos matrices pequeñas por capa en lugar de todos los pesos del modelo, y QLoRA (Quantized LoRA) hace lo mismo sobre un modelo base cuantizado a 4 bits. Las dos atacan el problema que hacía prohibitivo el fine-tuning: la memoria que ocupan gradientes y estado del optimizador. Para comprobarlo con números propios, entrené el mismo modelo con LoRA y con ajuste completo en un contenedor Docker sin GPU: LoRA usó el 3,5 % de los parámetros, un pico de 2,35 GB de RAM frente a 3,88 GB, y dejó un adaptador de 19,6 MB.

QLoRA lleva la idea más lejos: los pesos congelados se guardan en NF4, un formato de 4 bits, y solo los adaptadores se entrenan en 16 bits. Así cabe en una GPU lo que en 16 bits no cabría. En la misma prueba, bitsandbytes 0.50.2 cargó el modelo en 4 bits también en CPU: 109,8 MB frente a 269 MB en BF16.

Puntos clave

  • LoRA congela el modelo base y entrena un par de matrices de rango bajo por capa elegida; con rango 8 sobre una matriz de 4096 × 4096, los parámetros entrenables bajan de 16,8 millones a 65.536.

  • El ajuste completo con Adam en FP32 necesita unos 16 bytes por parámetro entre pesos, gradientes y optimizador: 128 GB para un modelo de 8.000 millones de parámetros, antes de contar las activaciones.

  • En la prueba en CPU con SmolLM2-135M, LoRA de rango 16 sobre todas las capas lineales entrenó 4,88 millones de parámetros y su pico de memoria fue de 2,35 GB, frente a 3,88 GB del ajuste completo.

  • El paper de QLoRA afinó un modelo de 65.000 millones de parámetros en una GPU de 48 GB conservando el rendimiento del ajuste completo en 16 bits.

  • Sin datos suficientes no hay técnica que salve el entrenamiento: Soup, la herramienta de línea de comandos que probé, se niega por debajo de 50 ejemplos y propone cambiar de vía a partir de unas 200 filas limpias.

El problema que resuelven

El fine-tuning completo guarda en memoria, para cada parámetro, el peso, su gradiente y dos momentos del optimizador Adam. En FP32 son 4 + 4 + 8 = 16 bytes por parámetro. Para un modelo de 8.000 millones de parámetros, como Llama 3 8B, la cuenta da:

  • Pesos: 8.000 millones × 4 bytes = 32 GB.

  • Gradientes: otros 32 GB.

  • Estado del optimizador Adam: 64 GB (dos momentos por parámetro).

  • Total: 128 GB, sin contar las activaciones, que crecen con el tamaño del lote y la longitud de las secuencias.

A escala de 70.000 millones de parámetros, la misma cuenta se multiplica por 8,75 y pasa de 1 TB. Entrenar en BF16 con precisión mixta reduce algo los pesos y los gradientes, pero el optimizador sigue siendo la parte más pesada.

LoRA resuelve el problema con una observación del paper original de Hu y otros[1]: los cambios que el fine-tuning induce en los pesos tienen rango intrínseco bajo. No necesitas actualizar una matriz densa de millones de parámetros; puedes aproximar ese cambio con el producto de dos matrices mucho más pequeñas:

ΔW ≈ B × A
donde B ∈ ℝ^(d×r) y A ∈ ℝ^(r×k), con r << d y r << k

Si el rango r es 8 y la matriz original es de 4096 × 4096, los parámetros entrenables bajan de 16,8 millones a 2 × 4096 × 8 = 65.536, 256 veces menos. Gradientes y estado del optimizador solo existen para esos parámetros, así que bajan en la misma proporción.

QLoRA: cuantización más adaptadores

QLoRA añade un paso más: cuantiza el modelo base a NF4 (Normal Float de 4 bits), un formato pensado para la distribución de los pesos de los LLM. Sobre ese modelo congelado, los adaptadores LoRA se entrenan en BF16.

El paper de Dettmers y otros[2] introdujo la técnica en mayo de 2023. Afinó un modelo de 65.000 millones de parámetros en una sola GPU de 48 GB conservando el rendimiento del ajuste completo en 16 bits. Para hacerlo añadió la doble cuantización de las constantes y los optimizadores paginados, que absorben los picos de memoria.

La cuenta de los pesos es directa. En NF4 cada parámetro ocupa medio byte más una pequeña parte de constantes de cuantización. Un modelo de 8.000 millones de parámetros pasa de 16 GB en BF16 a unos 4 GB, y uno de 70.000 millones, de 140 a unos 35 GB. A eso hay que sumar las activaciones, los adaptadores y su optimizador, que dependen del lote y del contexto: mide antes de comprar hardware.

Un ejemplo reproducible: LoRA en CPU con Docker

Para tener cifras propias, entrené el mismo modelo con LoRA y con ajuste completo el 27 de octubre de 2026. Usé un contenedor Docker en una máquina arm64 con 18 núcleos y 62 GB de RAM, sin GPU.

El modelo es SmolLM2-135M-Instruct[3], que no necesita aceptar licencia. Los datos son los 320 primeros ejemplos de trl-lib/Capybara[4], un conjunto de conversaciones. La imagen instala PyTorch para CPU y la pila de Hugging Face:

FROM python:3.12-slim
RUN pip install --no-cache-dir torch \
      --index-url https://download.pytorch.org/whl/cpu \
 && pip install --no-cache-dir transformers peft trl datasets accelerate
WORKDIR /work

Las versiones que resolvió pip fueron PyTorch 2.14.1, Transformers 5.21.0, PEFT 0.21.2, TRL 1.16.0 y Datasets 5.1.0. El script de entrenamiento usa SFTTrainer de TRL, y la única diferencia entre los dos modos es si recibe una configuración de LoRA:

ds = load_dataset("trl-lib/Capybara", split="train[:320]")
tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.float32)
peft_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05,
                         target_modules="all-linear", task_type="CAUSAL_LM")
args = SFTConfig(output_dir="/out/lora", max_steps=30,
                 per_device_train_batch_size=4, learning_rate=2e-4,
                 max_length=256, logging_steps=5, save_strategy="no",
                 report_to=[], use_cpu=True, seed=0)
tr = SFTTrainer(model=model, args=args, train_dataset=ds,
                processing_class=tok, peft_config=peft_config)
tr.train()
tr.save_model("/out/lora/final")

Para el ajuste completo, el mismo script pasa peft_config=None y una tasa de aprendizaje de 2e-5. El pico de memoria sale de resource.getrusage al terminar, y cada modo se lanza en su propio contenedor:

docker build -t lora-cpu .
docker run --rm -v "$PWD:/work" -v "$PWD/out:/out" \
  -e MODEL=HuggingFaceTB/SmolLM2-135M-Instruct lora-cpu python train.py lora

print_trainable_parameters() devolvió 4.884.480 parámetros entrenables de 139.399.488, el 3,5 %. Es más que el ejemplo clásico de solo q_proj y v_proj, porque all-linear adapta también las proyecciones de clave, salida y la capa MLP. El ajuste completo usó 1,65 veces más memoria que LoRA:

Pico de memoria al entrenar SmolLM2-135M en CPU GB · menos es mejor
LoRA r=16, todas las capas lineales
2,35 GB
QLoRA r=16, base en NF4
2,01 GB
Ajuste completo
3,88 GB
Ver tabla de valores
ModoGB
LoRA r=16, todas las capas lineales2,35 GB
QLoRA r=16, base en NF42,01 GB
Ajuste completo3,88 GB

Pico de RSS del proceso, 30 pasos, lote de 4 y secuencias de 256 tokens, FP32, Docker en arm64 sin GPU, 27 de octubre de 2026.

La diferencia, 1,53 GB, es casi exactamente lo que la cuenta predice: gradientes y estado de Adam para los 134,5 millones de parámetros del modelo base suman 1,61 GB. El resto del pico, los pesos y las activaciones, lo pagan los dos modos. En un modelo de 8.000 millones de parámetros esa misma parte evitada son 96 GB.

Los 30 pasos llevaron 11,7 minutos con LoRA y 9,8 con ajuste completo. LoRA no fue más rápido: su ventaja es la memoria, no el cómputo, porque el paso hacia atrás sigue recorriendo todo el modelo. La CPU compartía la máquina con otros procesos, así que los tiempos solo sirven como referencia aproximada.

La pérdida de entrenamiento bajó de 2,36 a 1,59 con LoRA y de 2,37 a 1,59 con ajuste completo. El adaptador guardado ocupa 19,6 MB, que son esos 4,88 millones de parámetros en FP32; el modelo base, en cambio, no se toca y no hay que volver a distribuirlo.

QLoRA también se ejecutó en esta CPU. Con bitsandbytes 0.50.2 y el modelo base en NF4, los mismos 30 pasos dieron un pico de 2,01 GB y llevaron 10,5 minutos. La pérdida bajó de 2,58 a 1,71.

La pérdida parte más alta que con LoRA, probablemente por el error que introduce cuantizar el modelo base, y en 30 pasos no llegó a igualarla. En un modelo de 135 millones de parámetros, el ahorro frente a LoRA es pequeño: 0,34 GB, porque los pesos pesan poco frente a las activaciones. En uno de 8.000 millones, los pesos pasan de 16 a unos 4 GB.

Un detalle práctico: la primera prueba, con Qwen2.5-0.5B y secuencias de 512 tokens, tardaba unos 187 s por paso en esta CPU. En CPU, el límite real es el tiempo, no la memoria.

El mismo script con QLoRA

El script de arriba pasa a QLoRA cambiando solo la carga del modelo. BitsAndBytesConfig cuantiza el modelo base a NF4 y el resto, LoraConfig y SFTTrainer, se queda igual. Es el fragmento que usé en la prueba; en una GPU, cambia device_map="cpu" por device_map="auto":

from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
    MODEL, quantization_config=bnb_config, device_map="cpu")

La librería PEFT[5] (Parameter-Efficient Fine-Tuning) gestiona los adaptadores, transformers carga el modelo base y TRL[6] añade SFTTrainer para el ajuste supervisado.

Arquitectura Transformer completa con sus bloques de codificador y decodificador, capas de atención multi-cabezal y capas feed-forward, que es donde LoRA inserta las matrices de adaptación de bajo rango durante el fine-tuningArquitectura Transformer completa con sus bloques de codificador y decodificador, capas de atención multi-cabezal y capas feed-forward. Ahí es donde LoRA inserta las matrices de adaptación de bajo rango durante el fine-tuning (Imagen: dvgodoy, CC BY 4.0, vía Wikimedia Commons)

Elección de hiperparámetros

Estos son los puntos de partida habituales para los hiperparámetros de LoRA; ajústalos midiendo contra un conjunto de validación:

  • Rango (r): valores bajos (4-8) para tareas de estilo o formato; medios (16-32) para adaptación de dominio; altos (64-128) para cambios de comportamiento más profundos. A mayor rango, más parámetros entrenables y más capacidad, pero también más riesgo de sobreajuste.

  • lora_alpha: el doble del rango como valor de partida. Controla el factor de escala de los adaptadores.

  • target_modules: q_proj y v_proj son el mínimo del paper original; all-linear adapta todas las capas lineales y, en la prueba, multiplicó los parámetros entrenables hasta el 3,5 % del modelo.

  • Learning rate: alrededor de 2e-4, diez veces lo que usarías en ajuste completo (2e-5 en la prueba).

Para modelos que luego servirás con vLLM en producción, asegúrate de que el formato de los adaptadores LoRA es compatible con la versión de vLLM que usas. El soporte de Multi-LoRA permite servir más de un adaptador sobre el mismo modelo base.

LoRA/QLoRA frente a prompt engineering

La decisión entre fine-tuning y prompt engineering depende de los datos que tengas y de lo que el modelo base ya resuelve:

Cuándo el fine-tuning compensa:

  • Tienes cientos de ejemplos limpios y revisados de la tarea, no unas decenas.

  • El dominio tiene vocabulario, formato o razonamiento específicos que el modelo base no conoce.

  • La tarea requiere un formato de salida constante que el prompt no consigue fijar.

  • El coste de tokens en producción pesa; un modelo afinado necesita prompts más cortos.

Cuándo el prompt engineering es suficiente:

  • Los datos de entrenamiento son escasos o difíciles de obtener.

  • La tarea es razonablemente general y el modelo base la resuelve bien con instrucciones.

  • Necesitas iterar sin ciclos de entrenamiento.

  • La alucinación del modelo base es aceptable para el caso de uso.

Con pocos datos, el modelo aprende el formato pero pierde generalización. Soup, la herramienta de fine-tuning que te dice cuándo no entrenar, pone números a esa frontera. Con 20 ejemplos recomendó no entrenar, porque su mínimo son 50 filas. Propone pasar al fine-tuning a partir de unas 200 filas limpias. Con LoRA/QLoRA hay poca fricción técnica para experimentar, pero eso no elimina el requisito de tener datos de calidad.

Despliegue de adaptadores LoRA

Una vez entrenado, el adaptador LoRA se guarda separado del modelo base: en la prueba, 19,6 MB frente a los 538 MB de los pesos del modelo base en FP32. Su tamaño es el número de parámetros entrenables por los bytes de cada uno, así que crece con el rango y con las capas que adaptas. Las opciones de despliegue son tres:

  • Fusión: merge_and_unload() suma los pesos del adaptador al modelo base y genera un modelo estándar que cualquier runtime puede servir. En la prueba, el modelo fusionado ocupó 538 MB y generó texto sin problemas, aunque a la pregunta de qué es LoRA respondió describiendo la radio LoRa. Treinta pasos sobre conversaciones genéricas no enseñan conocimiento nuevo: por eso hace falta evaluar con ejemplos de tu tarea.

  • Multi-LoRA con vLLM: servir más de un adaptador sobre el mismo modelo base, conmutando por petición. Eficiente cuando tienes más de un fine-tune especializado; la documentación de vLLM sobre adaptadores LoRA[7] detalla cómo cargarlos y descargarlos en caliente.

  • Adaptador separado con transformers: carga el modelo base más el adaptador con PeftModel.from_pretrained; útil para experimentar.

Para observabilidad del modelo fine-tuned en producción, el reto específico de los adaptadores LoRA es saber qué adaptador se usó en cada inferencia. Ese campo no sale de la instrumentación por defecto: lo añades tú como atributo de la traza.

Conclusión

LoRA y QLoRA eliminaron la barrera de hardware que hacía prohibitivo el fine-tuning para quien no tiene un clúster de GPU. La cuenta lo explica: el ajuste completo paga 16 bytes por parámetro y LoRA solo los paga por el pequeño porcentaje que entrena, como confirmó la prueba en CPU. El verdadero cuello de botella ya no es el hardware ni el framework. Son los datos de entrenamiento de calidad y la evaluación rigurosa de si el fine-tuning mejora lo que importa para tu caso de uso.

Este artículo también está disponible en inglés: LoRA and QLoRA: Efficient Fine-Tuning on a Single Laptop.

Fuentes:

  1. Hu et al., LoRA: Low-Rank Adaptation of Large Language Models (arXiv:2106.09685)[1]
  2. Dettmers et al., QLoRA: Efficient Finetuning of Quantized LLMs (arXiv:2305.14314)[2]
  3. Hugging Face, documentación de PEFT[5]
  4. Hugging Face, documentación de TRL[6]
  5. Hugging Face, SmolLM2-135M-Instruct[3]
  6. Hugging Face, conjunto trl-lib/Capybara[4]
  7. vLLM, documentación de adaptadores LoRA[7]

Preguntas frecuentes

¿Cuánta memoria necesito para hacer fine-tuning de un Llama 3 8B con QLoRA?

Los pesos del modelo en NF4 ocupan unos 4 GB, frente a 16 GB en BF16. A eso se suman las activaciones, los adaptadores y su optimizador, que dependen del lote y de la longitud de las secuencias, así que mide con tu configuración antes de comprar hardware. Como referencia, el ajuste completo del mismo modelo con Adam en FP32 necesita 128 GB solo para pesos, gradientes y optimizador. El paper de QLoRA afinó un modelo de 65.000 millones de parámetros en una GPU de 48 GB.

¿Se puede hacer fine-tuning con LoRA sin GPU?

Sí, con modelos pequeños. Sin GPU, en Docker, LoRA de rango 16 sobre SmolLM2-135M entrenó 30 pasos con un pico de 2,35 GB de RAM. El adaptador ocupó 19,6 MB. Usé PyTorch 2.14.1, PEFT 0.21.2 y TRL 1.16.0.

El límite en CPU es el tiempo: unos 23 s por paso con ese modelo, y unos 187 s con uno de 500 millones de parámetros y secuencias de 512 tokens. QLoRA también funcionó en CPU con bitsandbytes 0.50.2, pero donde ahorra memoria de verdad es en una GPU con modelos grandes.

¿Cuántos ejemplos necesito antes de que el fine-tuning con LoRA compense frente al prompt engineering?

Cientos de ejemplos limpios, no decenas. Soup, una herramienta de línea de comandos de código abierto para fine-tuning, se niega a entrenar por debajo de 50 filas y propone pasar al fine-tuning a partir de unas 200 limpias. Además, el dominio debe tener vocabulario, formato o razonamiento que el modelo base no conoce, o la tarea debe exigir un formato constante. Con pocos datos el modelo aprende el formato pero pierde generalización; si los datos son escasos, empieza por el prompt engineering.

Fuentes

  1. paper original de Hu y otros
  2. paper de Dettmers y otros
  3. SmolLM2-135M-Instruct
  4. trl-lib/Capybara
  5. librería PEFT
  6. TRL
  7. documentación de vLLM sobre adaptadores LoRA