"LoRA addestra circa l'1% del modello" è la spiegazione in una riga che si sente di solito. È vera, ma nasconde le parti che decidono se un training entra nella tua macchina e quanto dura: quali layer ricevono gli adapter, cosa fa il rank al loro numero, in che precisione stanno i pesi congelati e quanti passi di optimizer dà davvero un piccolo dataset.
Ecco quei numeri calcolati per un training concreto: Qwen2.5-1.5B-Instruct, rank 16, su un Mac Apple Silicon da 16 GB. Le impostazioni sono i valori di default del trainer locale dell'app desktop osFoundry, che è un normale script transformers + PEFT + TRL, quindi i conti valgono per qualsiasi setup simile. (La pagina sul fine-tuning LoRA dell'app mostra l'interfaccia.)
Un adapter sono due matrici sottili
Per una matrice di pesi W di forma d_out × d_in, LoRA congela W e impara due matrici, A (r × d_in) e B (d_out × r). L'output del layer diventa W·x + (α/r)·B·A·x. B parte da zero, quindi il training inizia esattamente dal modello base.
Il numero di parametri allenabili di quel layer è r × (d_in + d_out). Cresce linearmente con il rank e con la larghezza del layer, non con la sua area.
Dove vanno gli adapter
Il trainer applica gli adapter a tutte e sette le proiezioni lineari di ogni blocco transformer:
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"]
Qwen2.5-1.5B ha 28 blocchi, una hidden size di 1536, una dimensione MLP di 8960 e grouped-query attention con 2 KV head da 128 dimensioni, quindi k e v proiettano verso 256. Per blocco, con r = 16:
| Layer | d_in → d_out | Allenabili |
|---|---|---|
| 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 |
Sono 659,456 per blocco e 18,464,768 su 28 blocchi: 18.5M allenabili contro 1.54B totali, cioè l'1.2%.
Due cose saltano all'occhio. Le proiezioni MLP contengono tre quarti dei pesi dell'adapter, quindi escluderle (come fanno certe ricette più vecchie, che adattano solo q e v) porta il conteggio a circa 2.2M, un ottavo della capacità. E poiché il GQA rende k e v strette, i loro adapter costano poco.
Quasi tutta la memoria sono i pesi congelati
Sulla GPU di Apple il trainer carica il modello in float32:
torch_dtype = torch.float16 if device == "cuda" else torch.float32
La mezza precisione su MPS tende a produrre loss NaN, quindi lì i 32 bit sono la scelta sicura. La base congelata pesa quindi 1.54B × 4 byte ≈ 6.2 GB.
La parte allenabile è piccola al confronto. 18.5M di pesi in fp32 sono 74 MB, i gradienti altri 74 MB, e AdamW tiene due stime di momento per ogni peso, altri 148 MB. In tutto circa 0.3 GB. Il resto del budget va alle attivazioni, che scalano con la batch size e la lunghezza della sequenza (il trainer limita le sequenze a 2048 token; le risposte brevi di chat ne restano molto sotto).
Con circa 12 GB su 16 GB di un Mac utilizzabili dalla GPU, un 1.5B in fp32 lascia margine. Una base da 7B in fp32 sarebbe circa 30 GB prima ancora di iniziare l'addestramento: per questo su un Mac il modello base resta piccolo, e per questo esiste QLoRA (pesi congelati a 4 bit), anche se il percorso a 4 bit di bitsandbytes richiede una GPU NVIDIA, quindi su Mac si usa il LoRA semplice.
Un piccolo dataset dà meno aggiornamenti di quanto si pensi
Il trainer usa batch size 4 con 4 passi di gradient accumulation, quindi ogni aggiornamento dell'optimizer vede 16 esempi. Con 200 coppie:
- 200 ÷ 4 = 50 batch forward/backward per epoca
- 50 ÷ 4 ≈ 13 aggiornamenti dell'optimizer per epoca
- 3 epoche ≈ 39 aggiornamenti in totale, i primi 10 dei quali di warmup del learning rate
Trentanove aggiornamenti con un learning rate di 2e-4 e decadimento lineare bastano a spostare il tono, non a insegnare conoscenze nuove. Corrisponde a quello che si vede nella pratica: un paio di centinaia di risposte cambiano come scrive un modello, non cosa sa.
Cosa ne esce
Dopo il training viene salvato solo l'adapter: A e B per ciascuno dei 196 layer adattati, qualche decina di megabyte. Per l'inferenza l'app lo converte in un file LoRA GGUF e llama.cpp lo applica sopra una copia quantizzata della base, qui qwen2.5-1.5b-instruct in Q4_K_M. La base resta condivisa, quindi più adapter (uno per tono o compito) costano qualche decina di MB ciascuno.
Attenzione alla differenza di precisione: l'adapter è stato addestrato su pesi base in fp32 e gira su pesi a 4 bit. Per un piccolo adapter di stile la differenza di solito non si nota, ma è uno dei motivi per cui un modello fuso e ri-quantizzato può comportarsi in modo leggermente diverso dallo stesso adapter applicato a runtime.



Top comments (0)