02 - Fine-tuning Transformer: LoRA, QLoRA e Adapters
I modelli Transformer pre-addestrati come Llama 3, Mistral, GPT-4 e Claude possiedono una conoscenza impressionante del linguaggio e del ragionamento, ma raramente sono pronti all'uso per un task specifico. Per adattarli al nostro dominio, che sia classificazione di email aziendali, generazione di codice in un framework proprietario o risposta a domande mediche, serve il fine-tuning.
Il problema e che questi modelli hanno miliardi di parametri: LLaMA 2 ne ha 7 miliardi nella versione più piccola, 70 miliardi nella più grande. Un full fine-tuning su LLaMA-7B richiede circa 28 GB di VRAM solo per i pesi in FP32, più altrettanti per gli optimizer states di Adam (momenti primo e secondo), più la memoria per i gradienti e le attivazioni. In pratica servono 4x A100 da 80 GB per un singolo run di fine-tuning.
In questo secondo articolo della serie Deep Learning Avanzato e Edge Deployment, esploreremo le tecniche di Parameter-Efficient Fine-Tuning (PEFT) che permettono di adattare modelli con miliardi di parametri usando una frazione delle risorse. Partiremo dal problema del full fine-tuning, poi analizzeremo in dettaglio LoRA, QLoRA, DoRA e gli Adapter Layers, con implementazioni complete in Python usando HuggingFace PEFT.
Panoramica della Serie
| # | Articolo | Focus |
|---|---|---|
| 1 | Attention Mechanism nei Transformer | Self-attention, multi-head, architettura completa |
| 2 | Sei qui - Fine-tuning con LoRA, QLoRA e Adapters | Parameter-efficient fine-tuning |
| 3 | Quantizzazione dei Modelli | INT8, INT4, GPTQ, AWQ |
| 4 | Pruning e Compressione | Riduzione parametri, distillazione |
| 5 | Distillazione della Conoscenza | Teacher-student, knowledge transfer |
| 6 | Ollama e LLM Locali | Inference locale, ottimizzazione |
| 7 | Vision Transformer | ViT, DINO, image classification |
| 8 | Edge Deployment | ONNX, TensorRT, dispositivi mobili |
| 9 | NAS e AutoML | Neural Architecture Search |
| 10 | Benchmark e Ottimizzazione | Profiling, metriche, tuning |
Cosa Imparerai
- perchè il full fine-tuning non e sostenibile per modelli con miliardi di parametri
- La panoramica completa delle tecniche PEFT: prefix tuning, prompt tuning, adapters, LoRA
- La matematica di LoRA: decomposizione di rango basso, scaling con alpha, moduli target
- QLoRA: come combinare quantizzazione a 4-bit con LoRA per fine-tuning su GPU consumer
- DoRA: la decomposizione peso in magnitudine e direzione che migliora LoRA
- Adapter Layers: bottleneck adapters, parallel adapters, adapter fusion
- Implementazione pratica completa con HuggingFace PEFT e SFTTrainer
- Preparazione dataset: formato Alpaca, chat template, conversational format
- Hyperparameter tuning: rank, alpha, learning rate, batch size
- Confronto LoRA vs QLoRA vs DoRA vs full fine-tuning con benchmark reali
- Hardware requirements e limiti delle GPU consumer
1. Il Problema del Full Fine-Tuning
Il full fine-tuning consiste nell'aggiornare tutti i parametri di un modello pre-addestrato durante il training su un dataset specifico. Ogni peso, da quelli dei layer di embedding a quelli delle attention heads, viene modificato attraverso la backpropagation. Questo approccio ha funzionato egregiamente per modelli di dimensioni moderate come BERT (110M parametri), ma diventa insostenibile quando si scala a miliardi di parametri.
1.1 Costi Computazionali
Per comprendere perchè il full fine-tuning e proibitivo, analizziamo i requisiti di memoria per LLaMA 2 7B:
Memory Footprint del Full Fine-Tuning (LLaMA 2 7B)
| Componente | Formula | Memoria |
|---|---|---|
| Pesi del modello (FP16) | 7B * 2 bytes | 14 GB |
| Gradienti (FP16) | 7B * 2 bytes | 14 GB |
| Optimizer states (Adam FP32) | 7B * 4 bytes * 2 (m, v) | 56 GB |
| Attivazioni (batch_size=1) | Variabile | ~8-16 GB |
| Totale | ~92-106 GB |
Nessuna GPU consumer può gestire 100 GB di VRAM. Anche la A100 da 80 GB non e sufficiente senza tecniche come gradient checkpointing e mixed precision. Per LLaMA 70B, si parla di oltre 1 TB di memoria necessaria.
1.2 Catastrophic Forgetting
Il secondo problema del full fine-tuning e il catastrophic forgetting (dimenticanza catastrofica). Quando aggiorniamo tutti i parametri su un dataset specifico, il modello tende a dimenticare le conoscenze acquisite durante il pre-training. Un modello fine-tuned su documenti legali potrebbe perdere la capacità di generare codice Python o di rispondere a domande di storia.
Questo accade perchè il full fine-tuning modifica indiscriminatamente tutti i pesi, inclusi quelli che codificano conoscenze generali del linguaggio. In pratica, il modello sovrascrive la sua conoscenza precedente con quella nuova.
1.3 Storage per Modelli Multipli
Se un'azienda ha bisogno di adattare lo stesso modello base a 10 task diversi (classificazione email, sentiment analysis, Q&A medico, generazione report, ecc.), il full fine-tuning richiede di salvare 10 copie complete del modello: per LLaMA 7B, sono 140 GB di storage. Con LoRA, come vedremo, ogni adattamento richiede solo 10-50 MB, per un totale di 100-500 MB.
perchè il Full Fine-Tuning Non Scala
| Problema | Full Fine-Tuning | PEFT (LoRA) |
|---|---|---|
| VRAM per LLaMA 7B | ~100 GB | ~16-24 GB |
| Parametri trainabili | 7.000.000.000 | ~4.000.000 (0.06%) |
| Storage per adattamento | 14 GB | 10-50 MB |
| Catastrophic forgetting | Alto rischio | Rischio minimo |
| Hardware richiesto | 4x A100 80GB | 1x RTX 3090 24GB |
2. Parameter-Efficient Fine-Tuning (PEFT): Panoramica
Le tecniche PEFT condividono un principio fondamentale: invece di aggiornare tutti i parametri del modello, congeliamo i pesi pre-addestrati e addestriamo solo un piccolo sottoinsieme di parametri aggiuntivi. Questo riduce drasticamente i requisiti di memoria, velocizza il training e preserva le conoscenze del modello base.
Tassonomia delle Tecniche PEFT
| Tecnica | Dove Agisce | Parametri Extra | Vantaggi |
|---|---|---|---|
| Prompt Tuning | Input embeddings | Soft tokens prepesi all'input | Semplicissimo, pochi parametri |
| Prefix Tuning | Ogni layer di attention | Prefissi K,V trainabili | Più espressivo del prompt tuning |
| Adapter Layers | Tra i layer del Transformer | Moduli bottleneck inseriti | Flessibile, componibile |
| LoRA | Matrici di pesi esistenti | Matrici low-rank B e A | Zero overhead a inference |
| QLoRA | Come LoRA + quantizzazione | LoRA su modello 4-bit | Fine-tuning su GPU consumer |
| DoRA | Come LoRA + decomposizione | Magnitude + direction | qualità superiore a LoRA |
2.1 Prompt Tuning
Il prompt tuning (Lester et al., 2021) aggiunge un insieme di soft tokens trainabili all'inizio dell'input. Questi token non corrispondono a parole reali del vocabolario, ma sono vettori continui che il modello impara durante il training. Il modello base resta completamente congelato: si addestrano solo gli embeddings dei soft tokens.
Input originale: [Classify this email: "Meeting at 3pm"]
Con prompt tuning: [T1][T2][T3]...[T20] [Classify this email: "Meeting at 3pm"]
^^^^^^^^^^^^^^^^^
20 soft tokens trainabili
(20 * d_model parametri = 20 * 4096 = 81.920 parametri)
Il modello base (7B parametri) e CONGELATO.
Solo 81.920 parametri vengono aggiornati.
Prompt tuning funziona sorprendentemente bene per modelli molto grandi (> 10B parametri), ma perde qualità con modelli più piccoli. Con T5-XXL (11B), prompt tuning raggiunge prestazioni quasi identiche al full fine-tuning su SuperGLUE.
2.2 Prefix Tuning
Il prefix tuning (Li & Liang, 2021) estende l'idea del prompt tuning: invece di aggiungere soft tokens solo all'input, aggiunge prefissi trainabili alle chiavi (K) e ai valori (V) di ogni layer di attention. Questo da al modello più capacità espressiva rispetto al semplice prompt tuning.
Prompt Tuning:
Layer 1 Attention: Q=[input], K=[input], V=[input]
Layer 2 Attention: Q=[input], K=[input], V=[input]
Solo soft tokens preposti all'input iniziale.
Prefix Tuning:
Layer 1 Attention: Q=[input], K=[prefix_1 | input], V=[prefix_1 | input]
Layer 2 Attention: Q=[input], K=[prefix_2 | input], V=[prefix_2 | input]
...
Layer L Attention: Q=[input], K=[prefix_L | input], V=[prefix_L | input]
Prefissi trainabili a OGNI layer di attention.
Parametri: L * prefix_len * 2 * d_model (2 per K e V)
Esempio: 32 layer * 20 prefix * 2 * 4096 = 5.242.880 parametri
3. LoRA: Low-Rank Adaptation
LoRA (Hu et al., 2021) e la tecnica PEFT più utilizzata e rappresenta un punto di svolta nel fine-tuning dei grandi modelli linguistici. L'idea chiave e elegante nella sua semplicità: invece di aggiornare direttamente le matrici di pesi del Transformer, decomponghiamo l'aggiornamento in due matrici di rango basso.
3.1 L'Intuizione: Ipotesi del Rango Intrinseco
La ricerca (Aghajanyan et al., 2020) ha dimostrato che quando facciamo fine-tuning su un task specifico, gli aggiornamenti ai pesi del modello si concentrano in un sottospazio di dimensione bassa. In altre parole, la matrice di aggiornamento dei pesi ha un rango intrinseco molto inferiore alla sua dimensione nominale.
Consideriamo una matrice di pesi W di dimensioni d x k (ad esempio, la proiezione delle query nell'attention di LLaMA 7B: 4096 x 4096). L'aggiornamento completo richiederebbe una matrice di 4096 x 4096 = 16.777.216 parametri. Ma se l'aggiornamento ha un rango intrinseco basso, possiamo approssimarlo con un rango r molto più piccolo.
3.2 La Matematica di LoRA
Data una matrice di pesi pre-addestrata W_0 di dimensioni d x k, LoRA parametrizza l'aggiornamento come il prodotto di due matrici di rango basso:
Formula Fondamentale di LoRA
W = W_0 + \Delta W = W_0 + B \cdot A
Dove:
- W_0 \in \mathbb{R}^{d \times k} e la matrice di pesi pre-addestrata (congelata)
- B \in \mathbb{R}^{d \times r} e la matrice di proiezione verso il basso
- A \in \mathbb{R}^{r \times k} e la matrice di proiezione verso l'alto
- r \ll \min(d, k) e il rango (tipicamente 4, 8, 16, 32, 64)
Con scaling: h = W_0 x + \frac{\alpha}{r} \cdot B A x
dove \alpha e un fattore di scaling che controlla l'intensità dell'adattamento.
Matrice originale W_0 (congelata):
k = 4096
+------------------+
| |
d = | W_0 | 16.777.216 parametri
4096| (CONGELATA) | Non viene aggiornata
| |
+------------------+
Aggiornamento LoRA (trainabile):
r = 16 k = 4096
+-----+ +------------------+
| | | |
d = | B | x r=16 | A |
4096| | | |
| | +------------------+
+-----+
65.536 65.536
B (d x r) A (r x k)
= 4096 * 16 = 16 * 4096
= 65.536 = 65.536
Totale LoRA: 131.072 parametri (0.78% di 16.777.216)
3.3 Inizializzazione
L'inizializzazione di A e B e cruciale. LoRA inizializza:
- A con inizializzazione Kaiming uniforme (distribuzione Gaussiana)
- B con tutti zeri
Questo garantisce che all'inizio del training il contributo di LoRA sia esattamente zero (B * A = 0), quindi il modello parte dal comportamento del modello pre-addestrato. L'addestramento modifica gradualmente B e A per adattare il modello al task specifico.
3.4 Scaling Factor Alpha
Il parametro \alpha (alpha) controlla l'intensità dell'adattamento LoRA. L'output effettivo e:
h = W_0 x + \frac{\alpha}{r} \cdot B A x
Il rapporto \alpha / r funziona come un learning rate per LoRA. Nella pratica, si tiene spesso \alpha = 2r (ad esempio \alpha = 32 con r = 16), ottenendo un fattore di scaling di 2. Questo permette di cambiare il rango senza dover riaggiustare il learning rate.
3.5 Moduli Target
LoRA non viene applicato a tutti i layer del Transformer, ma solo a specifiche matrici di pesi. La scelta dei moduli target influenza significativamente la qualità del fine-tuning:
Target Modules per Architetture Comuni
| Modulo | Tipo | Dimensioni (LLaMA 7B) | Impatto |
|---|---|---|---|
q_proj |
Attention Query | 4096 x 4096 | Alto - controlla cosa il modello "cerca" |
k_proj |
Attention Key | 4096 x 4096 | Alto - controlla come i token vengono indicizzati |
v_proj |
Attention Value | 4096 x 4096 | Alto - controlla l'informazione estratta |
o_proj |
Attention Output | 4096 x 4096 | Medio |
gate_proj |
FFN Gate | 4096 x 11008 | Alto - controlla il flusso informativo nel FFN |
up_proj |
FFN Up | 4096 x 11008 | Medio-Alto |
down_proj |
FFN Down | 11008 x 4096 | Medio |
3.6 Calcolo Esatto dei Parametri Trainabili
Calcoliamo i parametri trainabili per LLaMA 2 7B con LoRA applicato a q_proj e v_proj su tutti i 32 layer:
LLaMA 2 7B: 32 layer, d_model = 4096
Con r = 16, target_modules = ["q_proj", "v_proj"]:
Per ogni layer:
q_proj LoRA: B (4096 x 16) + A (16 x 4096) = 65.536 + 65.536 = 131.072
v_proj LoRA: B (4096 x 16) + A (16 x 4096) = 65.536 + 65.536 = 131.072
Totale per layer: 262.144
Totale: 32 layer * 262.144 = 8.388.608 parametri (8.4M)
Rapporto: 8.4M / 6.738M (totale LLaMA 7B) = 0.12% dei parametri
Con target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]:
Per ogni layer: 7 moduli * 131.072 (media) = ~917.504
Totale: 32 * 917.504 = ~29.4M parametri = 0.44%
3.7 Vantaggio a Inference: Zero Overhead
Il vantaggio chiave di LoRA rispetto agli Adapter Layers e che a inference non c'e nessun overhead computazionale. Le matrici LoRA possono essere fuse (merged) nei pesi del modello base:
W_{merged} = W_0 + \frac{\alpha}{r} \cdot B \cdot A
Dopo il merge, il modello ha esattamente le stesse dimensioni e la stessa velocità di inference del modello originale, ma con i pesi aggiornati. Questo e impossibile con adapter layers, che aggiungono parametri permanenti al modello.
4. QLoRA: Quantized LoRA
QLoRA (Dettmers et al., 2023) e un'estensione di LoRA che combina la quantizzazione a 4-bit del modello base con il fine-tuning LoRA. Questo permette di fare fine-tuning di modelli con 65 miliardi di parametri su una singola GPU da 48 GB, un risultato precedentemente impossibile.
4.1 Le Tre Innovazioni di QLoRA
QLoRA introduce tre tecniche chiave:
Le Innovazioni di QLoRA
- 4-bit NormalFloat (NF4): Un nuovo tipo di dato quantizzato ottimizzato per pesi distribuiti normalmente. Ogni peso viene mappato su uno dei 16 valori (4 bit) scelti per minimizzare l'errore di quantizzazione su una distribuzione Gaussiana. NF4 supera INT4 e FP4 perchè i pesi delle reti neurali seguono approssimativamente una distribuzione normale.
- Double Quantization: Le costanti di quantizzazione (una per ogni blocco di 64 pesi) vengono esse stesse quantizzate a 8-bit. Questo risparmia ulteriori 0.37 bit per parametro, circa 3 GB per un modello da 65B.
- Paged Optimizers: Utilizza la memoria paginata (unified memory) della GPU NVIDIA per gestire picchi di memoria durante il training, spostando automaticamente gli optimizer states tra GPU e CPU quando necessario.
4.2 Come Funziona NF4
La quantizzazione NF4 si basa sull'osservazione che i pesi di reti neurali ben addestrate seguono una distribuzione approssimativamente normale con media zero. NF4 suddivide la distribuzione normale standard in 16 intervalli di uguale probabilità, e assegna a ciascun intervallo il valore ottimale che minimizza l'errore atteso:
INT4 (equispaziato):
Livelli: [-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7]
Problema: molti livelli nelle code (pochi pesi li), pochi al centro (molti pesi li)
NF4 (quantili normali):
Livelli: [-1.0, -0.6962, -0.5251, -0.3949, -0.2844, -0.1848, -0.0911, 0.0,
0.0796, 0.1609, 0.2461, 0.3379, 0.4407, 0.5626, 0.7230, 1.0]
Vantaggio: livelli densi dove ci sono più pesi (vicino a 0)
Risultato: NF4 produce errore di quantizzazione ~2x inferiore a INT4
4.3 Memory Footprint di QLoRA
Confronto Memoria: Full FT vs LoRA vs QLoRA (LLaMA 7B)
| Componente | Full FT (FP16) | LoRA (FP16) | QLoRA (NF4) |
|---|---|---|---|
| Pesi modello | 14 GB (FP16) | 14 GB (FP16) | 3.5 GB (NF4) |
| Gradienti | 14 GB | ~16 MB | ~16 MB |
| Optimizer states | 56 GB | ~64 MB | ~64 MB |
| Attivazioni | ~12 GB | ~12 GB | ~6 GB |
| Totale | ~96 GB | ~26 GB | ~10 GB |
Con QLoRA, il fine-tuning di LLaMA 7B diventa possibile su una singola RTX 3090 (24 GB), e LLaMA 13B su una singola RTX 4090 (24 GB) con gradient checkpointing attivo.
5. DoRA: Weight-Decomposed Low-Rank Adaptation
DoRA (Liu et al., 2024) e un'evoluzione di LoRA che decompone la matrice di pesi in due componenti: magnitudine (magnitude) e direzione (direction). Questa decomposizione e ispirata dalla classica Weight Normalization di Salimans & Kingma (2016) e mira a colmare il gap di qualità tra LoRA e il full fine-tuning.
5.1 L'Intuizione dietro DoRA
L'analisi dei pattern di apprendimento mostra una differenza fondamentale tra full fine-tuning e LoRA: il full fine-tuning modifica sia la magnitudine che la direzione dei pesi in modo indipendente, mentre LoRA tende a modificarli in modo proporzionale, limitando la sua espressivita.
5.2 La Matematica di DoRA
DoRA decompone la matrice di pesi pre-addestrata in:
Formula di DoRA
W' = m \cdot \frac{W_0 + BA}{\|W_0 + BA\|_c}
Dove:
- m \in \mathbb{R}^{1 \times k} e il vettore di magnitudine (trainabile), inizializzato con le norme delle colonne di W_0
- W_0 + BA rappresenta la direzione aggiornata tramite LoRA
- \|\cdot\|_c e la norma per colonne, che normalizza ogni colonna a norma unitaria
- B e A sono le matrici LoRA standard (trainabili)
In questo modo, DoRA ha due gradi di liberta indipendenti:
- La direzione e controllata da B e A (esattamente come in LoRA)
- La magnitudine e controllata dal vettore m, che può cambiare indipendentemente dalla direzione
5.3 Vantaggi di DoRA su LoRA
Benchmark DoRA vs LoRA (commonsense reasoning, LLaMA-7B)
| Task | LoRA (r=32) | DoRA (r=32) | Full FT |
|---|---|---|---|
| BoolQ | 69.8 | 71.8 | 73.2 |
| PIQA | 82.1 | 83.2 | 83.9 |
| WinoGrande | 79.4 | 80.6 | 81.5 |
| HellaSwag | 83.6 | 84.4 | 85.1 |
| Media | 78.7 | 80.0 | 80.9 |
DoRA riduce il gap tra LoRA e full fine-tuning di circa il 60-70%, con un overhead di parametri minimo (solo il vettore m aggiuntivo).
6. Adapter Layers
Gli Adapter Layers (Houlsby et al., 2019) sono stati tra le prime tecniche PEFT proposte. L'idea e semplice: inserire piccoli moduli trainabili (adapter) tra i layer esistenti del Transformer, mantenendo congelati i pesi originali.
6.1 Bottleneck Adapters
Un adapter classico e un modulo bottleneck con tre componenti:
- Down-projection: riduce la dimensione da d_model a d_bottleneck (es. 4096 -> 64)
- Non-linearita: tipicamente ReLU o GELU
- Up-projection: ripristina la dimensione originale (es. 64 -> 4096)
- Residual connection: l'output dell'adapter viene sommato all'input
Input x (dimensione d_model = 4096)
|
+----> Down-proj: W_down (4096 x 64) --> h (dimensione 64)
| |
| Non-linearita (GELU)
| |
| Up-proj: W_up (64 x 4096) <------+
| |
+------- (+) ----+ Residual Connection
|
Output (dimensione 4096)
Parametri per adapter: (4096 * 64) + (64 * 4096) = 524.288
Con 2 adapter per layer, 32 layer: 2 * 32 * 524.288 = 33.5M parametri
6.2 Confronto: Adapters vs LoRA
Adapters vs LoRA: Differenze Chiave
| Caratteristica | Adapter Layers | LoRA |
|---|---|---|
| Overhead a inference | Si (layer aggiuntivi) | No (merge nei pesi) |
| Latenza aggiuntiva | ~5-10% aumento | 0% |
| Composabilità | Alta (Adapter Fusion) | Media (somma di LoRA) |
| Facilità di implementazione | Alta | Alta |
| Supporto librerie | AdapterHub, PEFT | PEFT, unsloth, axolotl |
7. Implementazione Pratica con HuggingFace PEFT
Passiamo alla pratica. In questa sezione implementeremo il fine-tuning di un modello Transformer utilizzando LoRA e QLoRA con la libreria HuggingFace PEFT. Useremo Mistral 7B come modello base e lo adatteremo per un task di istruzione-risposta.
7.1 Setup e Installazione
# Installazione delle librerie necessarie
# pip install torch transformers peft trl datasets accelerate bitsandbytes
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training,
TaskType,
)
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
# Verifica GPU disponibile
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"VRAM: {torch.cuda.get_device_properties(0).total_mem / 1e9:.1f} GB")
print(f"PyTorch: {torch.__version__}")
print(f"CUDA: {torch.version.cuda}")
7.2 Configurazione LoRA
# Configurazione LoRA per Mistral 7B
lora_config = LoraConfig(
# Rango della decomposizione low-rank
# Valori tipici: 4 (minimo), 8 (buono), 16 (ottimo), 32 (alto), 64 (massimo)
r=16,
# Alpha: fattore di scaling. Il peso effettivo e alpha/r.
# Regola pratica: alpha = 2 * r
lora_alpha=32,
# Dropout applicato ai layer LoRA durante il training
# Aiuta a prevenire l'overfitting, specialmente con dataset piccoli
lora_dropout=0.05,
# Moduli del Transformer a cui applicare LoRA
# Per Mistral/LLaMA: q_proj, k_proj, v_proj, o_proj (attention)
# gate_proj, up_proj, down_proj (FFN)
target_modules=[
"q_proj", # Query projection - impatto alto
"k_proj", # Key projection - impatto alto
"v_proj", # Value projection - impatto alto
"o_proj", # Output projection - impatto medio
"gate_proj", # FFN gate - impatto alto
"up_proj", # FFN up projection - impatto medio-alto
"down_proj", # FFN down projection - impatto medio
],
# Tipo di task
task_type=TaskType.CAUSAL_LM,
# Bias: "none" (consigliato), "all", "lora_only"
bias="none",
)
# Stampa riepilogo configurazione
print(f"Rank: {lora_config.r}")
print(f"Alpha: {lora_config.lora_alpha}")
print(f"Scaling: {lora_config.lora_alpha / lora_config.r}")
print(f"Target modules: {lora_config.target_modules}")
print(f"Dropout: {lora_config.lora_dropout}")
7.3 Fine-Tuning con LoRA (FP16)
model_name = "mistralai/Mistral-7B-v0.3"
# Caricamento tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# Caricamento modello in FP16
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto", # Distribuzione automatica su GPU disponibili
attn_implementation="flash_attention_2", # Flash Attention per efficienza
)
# Applica LoRA al modello
model = get_peft_model(model, lora_config)
# Mostra parametri trainabili
model.print_trainable_parameters()
# Output: trainable params: 27,262,976 || all params: 7,268,633,600 || trainable%: 0.375%
# Configurazione training
training_args = SFTConfig(
output_dir="./results/mistral-lora",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # Effective batch size: 4 * 4 = 16
gradient_checkpointing=True, # Risparmia VRAM scambiando con tempo
optim="adamw_torch",
learning_rate=2e-4, # LR più alto rispetto al full FT
lr_scheduler_type="cosine",
warmup_ratio=0.03, # 3% degli step come warmup
weight_decay=0.001,
max_grad_norm=0.3,
logging_steps=10,
save_strategy="steps",
save_steps=100,
max_seq_length=2048,
fp16=True,
report_to="wandb", # Logging su Weights & Biases
seed=42,
)
7.4 Fine-Tuning con QLoRA (4-bit)
# Configurazione quantizzazione 4-bit (QLoRA)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # Carica pesi in 4-bit
bnb_4bit_quant_type="nf4", # NormalFloat 4-bit (migliore di INT4)
bnb_4bit_compute_dtype=torch.bfloat16, # Compute in BF16 (più stabile di FP16)
bnb_4bit_use_double_quant=True, # Double quantization (risparmia ~0.4 bit/param)
)
# Caricamento modello quantizzato
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
attn_implementation="flash_attention_2",
)
# Prepara il modello per il training k-bit
# Questo congela i pesi quantizzati e prepara i layer per LoRA
model = prepare_model_for_kbit_training(
model,
use_gradient_checkpointing=True,
)
# Applica LoRA (stessa configurazione di prima)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Output: trainable params: 27,262,976 || all params: 3,778,682,880 || trainable%: 0.721%
# Nota: il modello base ora occupa ~3.8GB invece di ~14GB
# Configurazione training QLoRA
training_args = SFTConfig(
output_dir="./results/mistral-qlora",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
gradient_checkpointing=True,
optim="paged_adamw_8bit", # Paged optimizer per gestire picchi di memoria
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
weight_decay=0.001,
max_grad_norm=0.3,
logging_steps=10,
save_strategy="steps",
save_steps=100,
max_seq_length=2048,
bf16=True, # BF16 per il compute (più stabile)
report_to="wandb",
seed=42,
)
8. Preparazione del Dataset
La qualità del fine-tuning dipende enormemente dalla qualità del dataset. In questa sezione vediamo come preparare i dati nei formati standard usati per il fine-tuning di LLM.
8.1 Formato Alpaca
Il formato Alpaca (Stanford, 2023) e uno dei più diffusi per il fine-tuning di LLM su task di istruzione-risposta. Ogni esempio ha tre campi:
def format_alpaca(example):
"""Converte un esempio nel formato Alpaca per il fine-tuning."""
if example.get("input", ""):
# Con input aggiuntivo
text = (
f"### Instruction:\n{example['instruction']}\n\n"
f"### Input:\n{example['input']}\n\n"
f"### Response:\n{example['output']}"
)
else:
# Solo istruzione e risposta
text = (
f"### Instruction:\n{example['instruction']}\n\n"
f"### Response:\n{example['output']}"
)
return {"text": text}
# Caricamento dataset
dataset = load_dataset("tatsu-lab/alpaca", split="train")
dataset = dataset.map(format_alpaca)
# Esempio di output formattato
print(dataset[0]["text"])
# ### Instruction:
# Give three tips for staying healthy.
#
# ### Response:
# 1. Eat a balanced and nutritious diet...
# 2. Exercise regularly...
# 3. Get enough sleep...
8.2 Chat Template (Mistral/ChatML)
Per modelli conversazionali, il formato chat template e più appropriato. Ogni modello ha il suo template specifico:
def format_chat_template(example, tokenizer):
"""Formatta l'esempio usando il chat template del modello."""
messages = [
{"role": "system", "content": "Sei un assistente esperto e disponibile."},
{"role": "user", "content": example["instruction"]},
{"role": "assistant", "content": example["output"]},
]
# Applica il chat template del tokenizer
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=False,
)
return {"text": text}
# Formato risultante per Mistral:
# <s>[INST] Sei un assistente esperto. [/INST]
# [INST] Give three tips for staying healthy. [/INST]
# 1. Eat a balanced diet...</s>
# Per ChatML (usato da molti modelli):
# <|im_start|>system
# Sei un assistente esperto.<|im_end|>
# <|im_start|>user
# Give three tips for staying healthy.<|im_end|>
# <|im_start|>assistant
# 1. Eat a balanced diet...<|im_end|>
8.3 Training con SFTTrainer
from trl import SFTTrainer
# Dataset formattato
dataset = load_dataset("tatsu-lab/alpaca", split="train")
dataset = dataset.map(
lambda x: format_chat_template(x, tokenizer)
)
# Split train/eval
dataset = dataset.train_test_split(test_size=0.05, seed=42)
# Creazione trainer
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
processing_class=tokenizer,
)
# Avvio training
trainer.train()
# Salvataggio adattatore LoRA (solo ~50MB)
trainer.save_model("./results/mistral-qlora/final")
tokenizer.save_pretrained("./results/mistral-qlora/final")
9. Merge dei Pesi e Deployment
Dopo il training, abbiamo un modello base congelato e un piccolo adattatore LoRA. Per il deployment, possiamo mantenere i due separati (utile per switching tra adattamenti diversi) oppure fonderli in un unico modello.
9.1 Merge LoRA Weights nel Modello Base
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
# Caricamento modello base (FP16 per il merge)
base_model = AutoModelForCausalLM.from_pretrained(
"mistralai/Mistral-7B-v0.3",
torch_dtype=torch.float16,
device_map="auto",
)
# Caricamento adattatore LoRA
model = PeftModel.from_pretrained(
base_model,
"./results/mistral-qlora/final",
)
# Merge LoRA nei pesi base
# Dopo il merge: W_merged = W_0 + (alpha/r) * B * A
model = model.merge_and_unload()
# Salvataggio modello merged
model.save_pretrained("./models/mistral-7b-finetuned")
tokenizer.save_pretrained("./models/mistral-7b-finetuned")
# Upload su HuggingFace Hub
model.push_to_hub("username/mistral-7b-finetuned")
tokenizer.push_to_hub("username/mistral-7b-finetuned")
print("Modello merged e caricato su HuggingFace Hub!")
9.2 Inference con il Modello Fine-Tuned
from transformers import pipeline
# Pipeline di generazione
pipe = pipeline(
"text-generation",
model="./models/mistral-7b-finetuned",
torch_dtype=torch.float16,
device_map="auto",
)
# Generazione
messages = [
{"role": "system", "content": "Sei un assistente esperto di programmazione."},
{"role": "user", "content": "Spiega il pattern Repository in Python."},
]
output = pipe(
messages,
max_new_tokens=512,
temperature=0.7,
top_p=0.9,
do_sample=True,
)
print(output[0]["generated_text"][-1]["content"])
10. Hyperparameter Tuning
La scelta degli iperparametri e cruciale per ottenere buoni risultati. Ecco una guida pratica basata sull'esperienza della community e sui benchmark pubblicati.
Guida agli Iperparametri LoRA
| Iperparametro | Range | Consigliato | Note |
|---|---|---|---|
| rank (r) | 4-256 | 16-64 | Aumentare per task complessi; r=8 spesso sufficiente per classificazione |
| alpha | 8-128 | 2 * rank | alpha/r e lo scaling effettivo; tenere rapporto costante quando si cambia r |
| learning_rate | 1e-5 - 5e-4 | 2e-4 | Più alto del full FT (10-100x); ridurre se loss oscilla |
| batch_size | 1-32 | 4-8 | Usare gradient_accumulation per simulare batch più grandi |
| epochs | 1-5 | 2-3 | Attenzione all'overfitting; monitorare eval loss |
| warmup_ratio | 0.01-0.1 | 0.03 | Importante per stabilità; più alto con LR alto |
| dropout | 0.0-0.1 | 0.05 | 0.0 per dataset grandi, 0.1 per dataset piccoli |
| max_seq_length | 512-8192 | 2048 | Maggiore = più VRAM; adattare al dataset |
Errori Comuni nel Fine-Tuning
- Learning rate troppo alto: la loss oscilla o diverge. Soluzione: ridurre LR di 2-5x o aumentare warmup
- Rank troppo basso: il modello non impara abbastanza. Soluzione: aumentare r da 8 a 16 o 32
- Rank troppo alto: overfitting, specialmente con dataset piccoli. Soluzione: ridurre r o aumentare dropout
- Poche epoche: underfitting. Controlla se la eval loss continua a scendere
- Troppe epoche: la loss di training scende ma la eval loss risale (overfitting)
- Dataset non pulito: duplicati, errori, formattazione inconsistente degradano la qualità
- Dimenticare gradient checkpointing: OOM immediato su modelli grandi
11. Benchmark e Confronti
Come scegliere tra LoRA, QLoRA, DoRA e full fine-tuning? Ecco un confronto sistematico basato su benchmark pubblici e test della community.
Confronto Completo: LoRA vs QLoRA vs DoRA vs Full FT
| Metrica | Full FT | LoRA | QLoRA | DoRA |
|---|---|---|---|---|
| qualità (MT-Bench) | 7.8 | 7.5 | 7.3 | 7.6 |
| VRAM (7B model) | ~100 GB | ~26 GB | ~10 GB | ~26 GB |
| Training speed (rel.) | 1.0x | 1.2x | 0.8x | 1.1x |
| Parametri trainabili | 100% | 0.1-0.5% | 0.1-0.5% | 0.1-0.5% + m |
| Storage adattamento | 14 GB | 10-50 MB | 10-50 MB | 10-50 MB |
| Inference overhead | 0% | 0% (merged) | 0% (merged) | 0% (merged) |
| GPU minima (7B) | 4x A100 | 1x A100 40GB | 1x RTX 3090 | 1x A100 40GB |
12. Hardware Requirements
La scelta dell'hardware dipende dal modello che vuoi fine-tunare e dalla tecnica PEFT che utilizzi. Ecco una guida pratica per GPU consumer e professionali.
GPU Requirements per Fine-Tuning
| GPU | VRAM | LoRA (FP16) | QLoRA (4-bit) | Note |
|---|---|---|---|---|
| RTX 3060 | 12 GB | fino a 3B | fino a 7B (seq 512) | Entry-level, limitata |
| RTX 3090 | 24 GB | fino a 7B | fino a 13B | Ottima per QLoRA 7B |
| RTX 4090 | 24 GB | fino a 7B | fino a 13B | Più veloce della 3090, stessa VRAM |
| A100 40GB | 40 GB | fino a 13B | fino a 34B | Standard professionale |
| A100 80GB | 80 GB | fino a 30B | fino a 70B | Ideale per modelli grandi |
| H100 80GB | 80 GB | fino a 30B | fino a 70B | Più veloce dell'A100, supporto FP8 |
Consigli per GPU Consumer
- Budget limitato (RTX 3060 12GB): QLoRA su modelli 7B con seq_length=512, batch_size=1, gradient_accumulation=16
- Buon rapporto qualità/prezzo (RTX 3090/4090 24GB): QLoRA su modelli 7-13B con seq_length=2048, batch_size=4
- Cloud computing: Google Colab Pro ($10/mese) offre A100 40GB per sessioni limitate; RunPod e Lambda Labs per uso intensivo
- Sempre attivare: gradient_checkpointing=True, paged optimizers, Flash Attention 2
13. Casi d'Uso Pratici
Vediamo alcuni casi d'uso concreti dove il fine-tuning con LoRA e particolarmente efficace.
13.1 Classificazione di Testo
Per task di classificazione (sentiment analysis, topic classification, spam detection), LoRA e spesso preferibile perchè:
- Il task richiede pochi parametri aggiuntivi (rank r=4-8 e sufficiente)
- I dataset di classificazione sono tipicamente piccoli (1k-50k esempi)
- Il rischio di overfitting con rank alto e elevato
13.2 Generazione di Codice
Per il fine-tuning su code generation (es. adattare un modello a un linguaggio specifico o a convenzioni aziendali), si raccomanda:
- Rank più alto (r=32-64) perchè il codice ha più struttura e variabilità
- Target modules completi (tutti i 7 moduli del Transformer)
- Dataset di alta qualità: codice ben documentato, con test, stile consistente
- Sequenze lunghe (max_seq_length=4096-8192) per contesto completo
13.3 Domain-Specific Chatbot
Per creare un chatbot specializzato (medico, legale, supporto tecnico):
- Dataset conversazionale nel formato chat template del modello
- Includere esempi di rifiuto ("Non posso rispondere a questa domanda")
- Rank medio (r=16-32)
- Validazione con esperti del dominio, non solo metriche automatiche
13.4 Summarization
Per il fine-tuning su task di riassunto:
- Dataset con coppie (documento, riassunto) di alta qualità
- Sequenze lunghe per l'input (fino a 8192 token)
- Rank medio-alto (r=16-32)
- Valutazione con metriche come ROUGE e valutazione umana
14. Conclusioni e Decision Tree
Le tecniche PEFT hanno reso il fine-tuning dei grandi modelli linguistici accessibile a chiunque abbia una GPU consumer. LoRA, QLoRA e DoRA rappresentano lo stato dell'arte per il parameter-efficient fine-tuning, ognuna con i propri punti di forza.
Albero Decisionale: Quando Usare Cosa
| Situazione | Tecnica Consigliata | Motivazione |
|---|---|---|
| GPU con < 16 GB VRAM | QLoRA | Unica opzione per modelli 7B+ su hardware consumer |
| GPU con 24-40 GB VRAM | LoRA (FP16) | Migliore qualità rispetto a QLoRA, training più veloce |
| Massima qualità richiesta | DoRA | Più vicino al full FT, overhead minimo su LoRA |
| Multipli adattamenti dello stesso modello | LoRA | Adapter piccoli (~50MB), switching rapido tra task |
| Modello piccolo (< 1B parametri) | Full Fine-Tuning | Per modelli piccoli, il full FT e spesso fattibile e migliore |
| Task semplice (classificazione) | LoRA (r=4-8) | Rank basso e sufficiente; evita overfitting |
| Task complesso (code generation) | LoRA/DoRA (r=32-64) | Rank alto per catturare la complessità del task |
| Composizione di competenze | Adapter Fusion | Combina adattamenti multipli in modo strutturato |
Il campo del fine-tuning efficiente e in rapida evoluzione. Nuove tecniche come GaLore (Gradient Low-Rank Projection) e ReLoRA (training LoRA iterativo con rank crescente) promettono di ridurre ulteriormente il gap con il full fine-tuning. Tuttavia, LoRA e QLoRA rimangono oggi lo standard de facto per il fine-tuning di LLM, con un ecosistema maturo di librerie (HuggingFace PEFT, Unsloth, Axolotl) e una community attiva.
Nel prossimo articolo della serie esploreremo la Quantizzazione dei Modelli: GPTQ, AWQ, INT8, e come ridurre la dimensione dei modelli del 75% mantenendo la qualità per il deployment in produzione.
Risorse e Riferimenti
- Paper LoRA: "LoRA: Low-Rank Adaptation of Large Language Models" (Hu et al., 2021)
- Paper QLoRA: "QLoRA: Efficient Finetuning of Quantized LLMs" (Dettmers et al., 2023)
- Paper DoRA: "DoRA: Weight-Decomposed Low-Rank Adaptation" (Liu et al., 2024)
- Paper Adapters: "Parameter-Efficient Transfer Learning" (Houlsby et al., 2019)
- HuggingFace PEFT: https://github.com/huggingface/peft
- Unsloth: https://github.com/unslothai/unsloth (LoRA 2-5x più veloce)
- TRL (Transformer Reinforcement Learning): https://github.com/huggingface/trl







