Qué entrena realmente el fine-tuning con LoRA
"LoRA entrena alrededor del 1 % del modelo" es la explicación de una línea que se suele dar. Es cierta, pero esconde las partes que deciden si un entrenamiento cabe en tu máquina y cuánto tarda: qué capas reciben adaptad
"LoRA entrena alrededor del 1 % del modelo" es la explicación de una línea que se suele dar. Es cierta, pero esconde las partes que deciden si un entrenamiento cabe en tu máquina y cuánto tarda: qué capas reciben adaptadores, qué hace el rank con su número, en qué precisión están los pesos congelados y cuántos pasos de optimizador da realmente un dataset pequeño.
Aquí están esas cuentas para un entrenamiento concreto: Qwen2.5-1.5B-Instruct, rank 16, en un Mac con Apple Silicon y 16 GB. Los ajustes son los valores por defecto del entrenador local de la aplicación de escritorio osFoundry, que es un script normal de transformers + PEFT + TRL, así que la aritmética sirve para cualquier configuración parecida. (La página de fine-tuning con LoRA de la app muestra la interfaz.)
Un adaptador son dos matrices delgadas
Para una matriz de pesos W de forma d_out × d_in, LoRA congela W y aprende dos matrices, A (r × d_in) y B (d_out × r). La salida de la capa pasa a ser W·x + (α/r)·B·A·x. B empieza en cero, así que el entrenamiento arranca exactamente desde el modelo base.
El número de parámetros entrenables de esa capa es r × (d_in + d_out). Crece de forma lineal con el rank y con el ancho de la capa, no con su área.
Dónde van los adaptadores
El entrenador pone adaptadores en las siete proyecciones lineales de cada bloque transformer:
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"]
Qwen2.5-1.5B tiene 28 bloques, un tamaño oculto de 1536, un tamaño de MLP de 8960 y grouped-query attention con 2 cabezas KV de 128 dimensiones, así que k y v proyectan hacia 256. Por bloque, con r = 16:
| Capa | d_in → d_out | Entrenables |
|---|---|---|
| q_proj | 1536 → 1536 | 49,152 |
| k_proj | 1536 → 256 | 28,672 |
| v_proj | 1536 → 256 | 28,672 |
| o_proj | 1536 → 1536 | 49,152 |
| gate_proj | 1536 → 8960 | 167,936 |
| up_proj | 1536 → 8960 | 167,936 |
| down_proj | 8960 → 1536 | 167,936 |
Son 659,456 por bloque y 18,464,768 en los 28 bloques: 18.5M entrenables frente a 1.54B en total, un 1.2 %.
Dos cosas llaman la atención. Las proyecciones del MLP concentran tres cuartas partes de los pesos del adaptador, así que dejarlas fuera (como hacían algunas recetas antiguas, que adaptaban solo q y v) baja la cifra a unos 2.2M, una octava parte de la capacidad. Y como GQA hace que k y v sean estrechas, sus adaptadores salen baratos.
Casi toda la memoria son los pesos congelados
En la GPU de Apple, el entrenador carga el modelo en float32:
torch_dtype = torch.float16 if device == "cuda" else torch.float32
La media precisión en MPS tiende a producir pérdidas NaN, así que 32 bits es lo seguro ahí. Eso hace que la base congelada ocupe 1.54B × 4 bytes ≈ 6.2 GB.
La parte entrenable es pequeña en comparación. 18.5M de pesos en fp32 son 74 MB, sus gradientes otros 74 MB, y AdamW guarda dos estimaciones de momento por peso, otros 148 MB. En total, unos 0.3 GB. El resto del presupuesto se va en activaciones, que escalan con el tamaño de batch y la longitud de secuencia (el entrenador limita las secuencias a 2048 tokens; las respuestas cortas de chat quedan muy por debajo).
Con unos 12 GB de un Mac de 16 GB utilizables por la GPU, un modelo de 1.5B en fp32 deja margen. Una base de 7B en fp32 serían unos 30 GB antes de empezar a entrenar, y por eso en un Mac el modelo base se mantiene pequeño y por eso existe QLoRA (pesos congelados en 4 bits), aunque la ruta de 4 bits de bitsandbytes necesita una GPU NVIDIA, así que en un Mac se usa LoRA a secas.
Un dataset pequeño da menos actualizaciones de las que crees
El entrenador usa batch size 4 con 4 pasos de acumulación de gradiente, de modo que cada actualización del optimizador ve 16 ejemplos. Con 200 pares:
- 200 ÷ 4 = 50 batches de forward/backward por época
- 50 ÷ 4 ≈ 13 actualizaciones del optimizador por época
- 3 épocas ≈ 39 actualizaciones en total, las 10 primeras de warmup del learning rate
Treinta y nueve actualizaciones con un learning rate de 2e-4 y decaimiento lineal bastan para cambiar el tono, pero no para enseñar conocimiento nuevo. Coincide con lo que se ve en la práctica: un par de cientos de respuestas cambian cómo escribe un modelo, no lo que sabe.
Qué sale al final
Tras el entrenamiento solo se guarda el adaptador: A y B de cada una de las 196 capas adaptadas, decenas de megabytes. Para la inferencia, la app lo convierte en un archivo LoRA GGUF y llama.cpp lo aplica sobre una copia cuantizada de la base, aquí qwen2.5-1.5b-instruct en Q4_K_M. La base se comparte, así que varios adaptadores (uno por tono o tarea) cuestan unas pocas decenas de MB cada uno.
Ojo con la diferencia de precisión: el adaptador se entrenó contra pesos base en fp32 y se ejecuta contra pesos de 4 bits. Para un adaptador de estilo pequeño, la diferencia suele no notarse, pero es una de las razones por las que un modelo fusionado y recuantizado puede comportarse algo distinto del mismo adaptador aplicado en tiempo de ejecución.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.

