Salta al contenuto principale

02 - Fine-tuning Transformer: LoRA, QLoRA e Adapters

Guida completa a Fine-tuning Transformer: LoRA, QLoRA e Adapters: architettura, implementazione pratica e best practices per developer e team tecnici.

Cosa ne pensi di questo articolo?

02 - Fine-tuning Transformer: LoRA, QLoRA e Adapters
Deep Learning AvanzatoFoto:Brett SaylesPexels

In Breve

Guida completa a Fine-tuning Transformer: LoRA, QLoRA e Adapters: architettura, implementazione pratica e best practices per developer e team tecnici.

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
1Attention Mechanism nei TransformerSelf-attention, multi-head, architettura completa
2Sei qui - Fine-tuning con LoRA, QLoRA e AdaptersParameter-efficient fine-tuning
3Quantizzazione dei ModelliINT8, INT4, GPTQ, AWQ
4Pruning e CompressioneRiduzione parametri, distillazione
5Distillazione della ConoscenzaTeacher-student, knowledge transfer
6Ollama e LLM LocaliInference locale, ottimizzazione
7Vision TransformerViT, DINO, image classification
8Edge DeploymentONNX, TensorRT, dispositivi mobili
9NAS e AutoMLNeural Architecture Search
10Benchmark e OttimizzazioneProfiling, 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.

Prompt Tuning: Schema Concettuale

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.

Prefix Tuning vs 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.

LoRA: Visualizzazione della Decomposizione

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:

Calcolo Parametri LoRA

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

  1. 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.
  2. 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.
  3. 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:

NF4 vs INT4: Distribuzione dei Livelli di Quantizzazione

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:

  1. Down-projection: riduce la dimensione da d_model a d_bottleneck (es. 4096 -> 64)
  2. Non-linearita: tipicamente ReLU o GELU
  3. Up-projection: ripristina la dimensione originale (es. 64 -> 4096)
  4. Residual connection: l'output dell'adapter viene sommato all'input
Bottleneck Adapter: Flusso

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 dipendenze
# 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

lora_config.py - Configurazione LoRA dettagliata
# 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)

lora_finetuning.py - Fine-tuning LoRA completo
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)

qlora_finetuning.py - QLoRA con bitsandbytes
# 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:

dataset_preparation.py - Formattazione dataset
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:

chat_template.py - Formato conversazionale
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

sft_training.py - Training completo 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

merge_and_push.py - Merge e upload su HuggingFace Hub
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

inference.py - Generazione 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

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti