Dev.to AI 🤖 Ai 👁 0 📖 3 min read

Was trainiert LoRA-Fine-Tuning eigentlich?

"LoRA trainiert etwa 1 % des Modells" ist die übliche Ein-Satz-Erklärung. Sie stimmt, verschweigt aber die Teile, die entscheiden, ob ein Lauf auf deinen Rechner passt und wie lange er dauert: welche Layer Adapter bekomm

Was trainiert LoRA-Fine-Tuning eigentlich?

"LoRA trainiert etwa 1 % des Modells" ist die übliche Ein-Satz-Erklärung. Sie stimmt, verschweigt aber die Teile, die entscheiden, ob ein Lauf auf deinen Rechner passt und wie lange er dauert: welche Layer Adapter bekommen, was der Rank an der Anzahl ändert, in welcher Präzision die eingefrorenen Gewichte liegen und wie viele Optimizer-Schritte ein kleiner Datensatz tatsächlich ergibt.

Hier sind diese Zahlen für einen konkreten Lauf durchgerechnet: Qwen2.5-1.5B-Instruct, Rank 16, auf einem Mac mit Apple Silicon und 16 GB. Die Einstellungen sind die Standardwerte des lokalen Trainers in der Desktop-App osFoundry, einem ganz normalen Skript aus transformers + PEFT + TRL. Die Rechnung lässt sich also auf jedes ähnliche Setup übertragen. (Die Seite zum LoRA-Fine-Tuning der App zeigt die Oberfläche dazu.)

Der Konfigurationsschritt: Local, LoRA, Rank 16, Alpha 32

Ein Adapter sind zwei dünne Matrizen

Für eine Gewichtsmatrix W der Form d_out × d_in friert LoRA W ein und lernt zwei Matrizen, A (r × d_in) und B (d_out × r). Die Ausgabe des Layers wird zu W·x + (α/r)·B·A·x. B startet bei null, das Training beginnt also exakt beim Basismodell.

Die Zahl der trainierbaren Parameter für diesen Layer ist r × (d_in + d_out). Sie wächst linear mit dem Rank und mit der Breite des Layers, nicht mit seiner Fläche.

Wo die Adapter sitzen

Der Trainer hängt Adapter an alle sieben linearen Projektionen jedes Transformer-Blocks:

target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                "gate_proj", "up_proj", "down_proj"]

Qwen2.5-1.5B hat 28 Blöcke, eine Hidden Size von 1536, eine MLP-Größe von 8960 und Grouped-Query Attention mit 2 KV-Heads à 128 Dimensionen, sodass k und v auf 256 herunterprojizieren. Pro Block bei r = 16:

Layer d_in → d_out Trainierbar
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

Das sind 659,456 pro Block und 18,464,768 über 28 Blöcke: 18.5M trainierbar gegenüber 1.54B insgesamt, also 1.2 %.

Zwei Dinge fallen auf. Die MLP-Projektionen enthalten drei Viertel der Adapter-Gewichte. Lässt man sie weg (wie manche ältere Rezepte, die nur q und v adaptieren), sinkt die Zahl auf etwa 2.2M, ein Achtel der Kapazität. Und weil GQA k und v schmal macht, sind deren Adapter billig.

Der meiste Speicher geht an die eingefrorenen Gewichte

Auf Apples GPU lädt der Trainer das Modell in float32:

torch_dtype = torch.float16 if device == "cuda" else torch.float32

Halbe Präzision auf MPS neigt zu NaN-Losses, deshalb ist 32 Bit dort die sichere Wahl. Damit belegt die eingefrorene Basis 1.54B × 4 Bytes ≈ 6.2 GB.

Der trainierbare Teil ist im Vergleich klein. 18.5M Gewichte in fp32 sind 74 MB, die Gradienten weitere 74 MB, und AdamW hält zwei Momentschätzungen pro Gewicht, nochmal 148 MB. Insgesamt etwa 0.3 GB. Der Rest des Budgets geht an die Aktivierungen, die mit Batchgröße und Sequenzlänge skalieren (der Trainer begrenzt Sequenzen auf 2048 Token; kurze Chat-Antworten liegen weit darunter).

Wenn die GPU von 16 GB eines Macs etwa 12 GB nutzen kann, bleibt bei 1.5B in fp32 Platz. Eine 7B-Basis in fp32 wären schon vor Trainingsbeginn etwa 30 GB. Deshalb bleibt das Basismodell auf einem Mac klein, und deshalb gibt es QLoRA (eingefrorene Gewichte in 4 Bit). Der 4-Bit-Pfad von bitsandbytes braucht allerdings eine NVIDIA-GPU, auf dem Mac bleibt es also bei normalem LoRA.

Ein kleiner Datensatz ergibt weniger Updates, als man denkt

Der Trainer nutzt Batchgröße 4 mit 4 Gradient-Accumulation-Schritten, jedes Optimizer-Update sieht also 16 Beispiele. Bei 200 Paaren:

  • 200 ÷ 4 = 50 Forward/Backward-Batches pro Epoche
  • 50 ÷ 4 ≈ 13 Optimizer-Updates pro Epoche
  • 3 Epochen ≈ 39 Updates insgesamt, die ersten 10 davon Learning-Rate-Warmup

Neununddreißig Updates bei einer Learning Rate von 2e-4 mit linearem Abfall reichen, um den Ton zu verschieben, aber nicht, um neues Wissen beizubringen. Das deckt sich mit der Praxis: Ein paar hundert Antworten ändern, wie ein Modell schreibt, nicht, was es weiß.

Ein lokaler LoRA-Lauf in Arbeit, Epoche 3 von 3

Was am Ende herauskommt

Nach dem Training wird nur der Adapter gespeichert: A und B für jeden der 196 adaptierten Layer, einige zehn Megabyte. Für die Inferenz wandelt die App ihn in eine GGUF-LoRA-Datei um, und llama.cpp wendet ihn auf eine quantisierte Kopie der Basis an, hier qwen2.5-1.5b-instruct in Q4_K_M. Die Basis wird geteilt, mehrere Adapter (einer pro Tonfall oder Aufgabe) kosten also jeweils nur ein paar Dutzend MB.

Beispiel: Basismodell und Adapter beim selben Prompt

Beachte die Präzisionslücke: Der Adapter wurde gegen fp32-Basisgewichte trainiert und läuft gegen 4-Bit-Gewichte. Bei einem kleinen Stil-Adapter fällt der Unterschied meist nicht auf, ist aber ein Grund dafür, dass ein zusammengeführtes, neu quantisiertes Modell sich leicht anders verhalten kann als derselbe Adapter zur Laufzeit.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.