LoRA (Low-Rank Adaptation) ist eine der wichtigsten Innovationen der letzten Jahre. Sie erlaubt es, große Modelle mit Consumer-Hardware zu trainieren.
Die Kernidee: Rank Decomposition
Das Problem (nochmal)
Ein Llama 3 70B Modell hat 70 Milliarden Gewichte. Um alle zu trainieren:
Forward Pass: Berechne Predictions
Layer 1: [4096 × 4096] Matrix × [Batch × 4096] Input
Layer 2: [4096 × 4096] Matrix × [Output von Layer 1]
...
Total: Milliarden Multiplikationen
Backward Pass: Berechne Gradienten
Brauche Ableitungen für JEDES Gewicht
Memory: 3-4× die Modellgröße selbst
140 GB Modell → 420-560 GB RAM braucht
Die LoRA Lösung: Math
Bei Standard Matrix-Multiplikation:
Y = W × X (W ist 4096 × 4096, das ist "full rank")
LoRA-Idee: W ist wahrscheinlich nicht wirklich full-rank nötig.
Die "echte" Information könnte in niedriger Rank leben.
Also trainiere zwei kleinere Matrizen statt einer großen:
Y_original = W × X
Y_lora = (A @ B) × X (A ist 4096 × r, B ist r × 4096)
r = "rank" (z.B. 16)
A @ B produziert 4096 × 4096 Matrix, aber mit nur r × (4096 + 4096) = 32K Parameter
vs Full W = 16M Parameter → 500× weniger!
Final Output = W × X + α × (A @ B) × X
└─ Original, frozen ─┘ └─ Trainable Delta ─┘
Warum funktioniert das?
Empirische Beobachtung (Hu et al. 2021, "LoRA" paper):
Hypothesis: Die Änderungen nötig für Task-Adaptation
liegen in einem niedrig-dimensionalen Subspace.
Beispiel: Llama 3 70B trainiert auf Englisch.
Um auf Deutsch anzupassen, brauchst du nicht
alle 70B Parameter zu ändern.
Vielleicht nur die "Deutsch-Features" in Rank-16 Subspace.
Empirisch bestätigt: LoRA Rank-16 Training erreicht
>95% der Qualität von Full Fine-Tuning
mit 250× weniger Parametern!
Die Mathematik vereinfacht
Tensor-Contraction (vereinfacht)
Input Tensor: x ∈ ℝ^(batch × seq × hidden)
Beispiel: [32, 128, 4096]
Original Gewicht: W ∈ ℝ^(hidden × hidden)
Beispiel: [4096, 4096]
LoRA Gewichte:
A ∈ ℝ^(hidden × rank) [4096, 16]
B ∈ ℝ^(rank × hidden) [16, 4096]
Forward Pass:
y_orig = x @ W^T (shape: [32, 128, 4096])
y_delta = x @ A @ B^T (shape: [32, 128, 4096])
y_final = y_orig + α × y_delta
α (typically 16 für rank=16): Skalierungs-Faktor
α = 2 × lora_alpha / rank
Warum "Rank"?
In Linear Algebra:
Matrix Rank = Anzahl linear unabhängiger Reihen/Spalten
Beispiel Matrix:
[1 2 3]
[2 4 6] ← Diese Reihe ist 2× erste Reihe (nicht linear unabhängig)
[3 6 9] ← Diese Reihe ist 3× erste Reihe
Rank dieser Matrix = 1 (nur 1 linear unabhängige Reihe)
Bei LoRA Rank-16:
A @ B Matrix hat maximal Rank-16
(auch wenn A @ B resultat 4096 × 4096 ist)
Das bedeutet: Max 16 "Richtungen" der Änderung
→ Stark begrenzt, aber ausreichend für Finetuning-Tasks!
Rank-Auswahl (praktisch)
Kleine Rank (r=4-8)
Parameter: 4 × 4096 × 2 = 32K (+ Layer Overhead)
Training Speed: Sehr schnell (1-2 Stunden)
Quality: ~85% von Full Fine-Tuning
Use-Case:
- Kleine Stil-Anpassungen
- Kleine Datasets (<100 Beispiele)
- Budget sehr begrenzt
Problem: Kann komplexe Tasks nicht beschreiben
Standard Rank (r=16)
Parameter: 16 × 4096 × 2 = 128K (industry standard)
Training Speed: 2-4 Stunden
Quality: ~95% von Full Fine-Tuning
Use-Case:
- Standard Domain Anpassung
- 500-5000 Beispiele
- Budget moderat
Warum 16?
- Empirisch: Diminishing returns nach Rank-16 für viele Tasks
- Paper-Standard seit LoRA 2021
- Gut Balance zwischen Qualität und Speicher
Große Rank (r=32-64)
Parameter: 32 × 4096 × 2 = 256K
Training Speed: 4-8 Stunden
Quality: ~98% von Full Fine-Tuning
Use-Case:
- Komplexe Task-Anpassung
- Großes Dataset (>5000 Beispiele)
- Speicher verfügbar
Problem: Nach r=32 sehr geringe zusätzliche Verbesserung
aber 2× mehr Training-Zeit
→ diminishing returns
Empirischer Graph (Hypothetisch)
Quality vs Rank (typisch für Chat-Fine-Tuning):
99% ┌─────────────────── Full Fine-Tuning (Baseline)
│
98% │ ╱─────── r=32 (98%)
│ ╱────────╱
95% │ ╱───╱────── r=16 (95%)
│ ╱─╱
90% │╱────────── r=8 (90%)
│────────────────────────────
0 4 8 16 32 64 128
LoRA Rank
nach r=16: jede Verdoppelung Rank ~1-2% Verbesserung
aber 2× längeres Training
→ Meist nicht wert
Welche Layer sollten trainiert werden?
Standard: target_modules = ["q_proj", "v_proj"]
# In jedem Transformer Block:
Input
├─ Self-Attention
│ ├─ Q (Query) Projection ← Trainiere (q_proj)
│ ├─ K (Key) Projection ← Trainiere? (k_proj)
│ ├─ V (Value) Projection ← Trainiere (v_proj)
│ └─ Output Projection
│
├─ Feed-Forward
│ ├─ Linear 1 ← Trainiere? (up_proj)
│ └─ Linear 2 ← Trainiere? (down_proj)
│
Output
Strategien
Option 1: Nur Q und V (STANDARD)
target_modules = ["q_proj", "v_proj"]
Begründung:
- Q und V sind wo Semantik gelesen wird
- K ist weniger wichtig (nur für Ähnlichkeit)
- Feed-Forward ist teuer zu trainieren
Parameter: ~64K pro Layer
Empirisch: 95% von "trainiere alles"
Kosten: Standard
Option 2: Trainiere alles (Aggressive)
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "up_proj", "down_proj"]
Vorteile:
- Beste Qualität (~99% von Full)
- Komplexere Adaptation möglich
Nachteile:
- 6× mehr Parameter
- 2-3× längeres Training
- Risiko von Overfitting steigt
Besser für: Große Datasets (>5000), wenn Zeit/Budget unbegrenzt
Option 3: Nur Feed-Forward (Nisch)
target_modules = ["up_proj", "down_proj"]
Warum:
- Feed-Forward ist wo "Wissen" gespeichert ist (laut Mechanistic Interpretability)
- Für Knowledge-basierte Tasks besser
Empirisch: 90% Qualität bei 50% der Parameter
Kompromiss: OK wenn dein Task wirklich "Knowledge-Heavy"
QLoRA vs DoRA: Variationen
QLoRA (Quantized LoRA)
Zusätzlich zu LoRA: Quantisiere das Base-Modell auf INT4
Standard LoRA:
Model (FP32): 140 GB
LoRA (A, B): 50 MB
Optimizer State: 35 GB
Total: ~175 GB
QLoRA:
Model (INT4): 35 GB ← Komprimiert + nur read
LoRA (A, B): 50 MB ← Float16 (trainierbar)
Optimizer State: 35 GB
Total: ~70 GB
→ 2.5× weniger RAM!
Trade-off:
Qualität:
- Standard LoRA: 95%
- QLoRA: 93-94% (Quantisierung-Rauschen)
RAM:
- Standard: 175 GB
- QLoRA: 70 GB (2.5× Ersparnis)
Real-world: QLoRA ist fast nicht schlechter, aber 70× günstiger
DoRA (Decomposed Rank Adaptation)
Standard LoRA:
Δ = α/rank × (A @ B)
DoRA:
Δ = m × (A @ B) / ||A @ B||
└─ magnitude vector (trainierbar, pro output-dim)
└─ Normalisierte Richtung
Intuitiv: Trainiere nicht nur "Richtung der Änderung"
sondern auch "wie stark" unabhängig
Empirisch:
- Standard LoRA: 95% Quality
- DoRA: 96.5% Quality (+1.5%)
- Similar RAM/Speed
Nachteil: Etwas komplexer zu implementieren
Vorteil: Konsistent bessere Qualität
DoRA im Code:
# Noch nicht in allen Tools standard
# Axolotl unterstützt's:
config.use_dora = True
LoRA Mergen (Praktisch)
Szenario: Du trainierst LoRA, jetzt deployen
Nach Training hast du:
- Original Model: model.safetensors (140 GB)
- LoRA Weights: adapter_model.safetensors (50 MB)
Problem bei Deployment:
- Muss beide Dateien laden
- Mergen kostet Zeit beim Startup
- Speicher größer als nötig
Lösung: Merge LoRA in Original
from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer
# Lade LoRA-adaptiertes Modell
model_path = "./path/to/lora-model"
model = AutoPeftModelForCausalLM.from_pretrained(
model_path,
device_map="cpu", # Muss auf CPU sein für Merge!
torch_dtype=torch.float16
)
# Merge (trainiert Gewichte + LoRA zusammen)
merged_model = model.merge_and_unload()
# Speichern
merged_model.save_pretrained("./merged-model")
tokenizer = AutoTokenizer.from_pretrained(model_path)
tokenizer.save_pretrained("./merged-model")
Was passiert beim Merge?
Original W [4096 × 4096]
LoRA Delta (A @ B) [4096 × 4096, rank=16]
Merged W_new = W + α × (A @ B)
Math:
W_new = W + (2 × A @ B / 16)
= W + 0.125 × (A @ B)
Result: Neue Matrix mit "learned deltas eingebacken"
Keine LoRA-Info mehr nötig → simpleres Deployment
Tradeoffs beim Mergen
| Aspekt | Merged | Separate |
|---|---|---|
| Speicher | 140 GB | 140GB + 50MB |
| Load-Zeit | Normal | Normal (beide geladen) |
| Inference | Schnell | Schnell (kein Unterschied) |
| Reversibel | Nein | Ja (original noch da) |
| Multiple LoRA | Nein | Ja (mehrere auf einmal möglich) |
Best Practice:
- Development: Separate (reversibel, kombinierbar)
- Production: Merged (einfacher, deterministic)
Praktisches Beispiel: 30-Minuten LoRA Training
Dataset vorbereiten
[
{
"instruction": "Übersetze zu Englisch",
"input": "Der Hund springt schnell",
"output": "The dog jumps fast"
},
{
"instruction": "Beantworte die Frage",
"input": "Was ist ein Transformer?",
"output": "Ein Transformer ist eine Architektur..."
}
]
Code (mit Unsloth)
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset
# 1. Load Model mit LoRA
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/llama-2-7b",
max_seq_length=1024,
load_in_4bit=True, # QLoRA
)
# 2. Attach LoRA
model = FastLanguageModel.get_peft_model(
model,
r=16, # Rank
lora_alpha=16, # Scaling
target_modules=["q_proj", "v_proj"], # Welche
lora_dropout=0.05,
bias="none",
use_gradient_checkpointing=True,
)
# 3. Load Dataset
dataset = load_dataset("json", data_files="data.json")
# 4. Train
trainer = SFTTrainer(
model=model,
train_dataset=dataset["train"],
args=TrainingArguments(
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
warmup_steps=100,
num_train_epochs=1,
learning_rate=2e-4,
output_dir="lora-output",
optim="adamw_8bit", # 8-bit Optimizer für RAM-Effizienz
max_steps=500, # ~30 Min
),
packing=True,
max_seq_length=1024,
dataset_text_field="text",
)
trainer.train()
# 5. Save & Merge
model.save_pretrained("lora-weights")
# Later: Merge für Deployment
from peft import AutoPeftModelForCausalLM
merged = AutoPeftModelForCausalLM.from_pretrained(
"lora-weights",
device_map="cpu",
torch_dtype=torch.float16,
).merge_and_unload()
merged.save_pretrained("merged-model")
Timing auf RTX 4090:
- Load Model: 2 min
- Training 500 steps: 20-25 min
- Save: 1 min
- Total: ~30 min
Häufige LoRA-Fehler
Fehler 1: Target Modules falsch
Problem:
target_modules = ["all"] # ← Nicht existent!
Lösung:
target_modules = ["q_proj", "v_proj"] # Für Llama
target_modules = ["query", "value"] # Für andere Modelle
target_modules = ["lin_in", "lin_out"] # Für spezifische
Check model structure:
for name, module in model.named_modules():
print(name) # Sieh welche Layer existieren
Fehler 2: Rank zu klein für komplexe Task
Problem:
Task = "Lerne ganzes neues Fachgebiet (Medizin) mit 10K Beispiele"
Solution: r=4 # ← Zu klein!
Result: Model gibt Halluzinationen
Lösung:
r=32 oder r=64 für komplexe Tasks
Fehler 3: Learning Rate falsch
Problem:
learning_rate = 1e-4 # ← Zu klein für LoRA!
Result: Kaum Training, Loss sinkt nicht
Learning Rate Guideline:
Full Fine-Tuning: 5e-5 (alles trainiert, klein)
LoRA (Rank 16): 2e-4 (nur kleine Delta, größer)
QLoRA: 2e-4 bis 1e-3 (noch größer möglich)
Fehler 4: Zu viele Targets bei kleinem Rank
Problem:
r = 8
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "up_proj", "down_proj"]
Dann braucht man 6 × 8 × 4096 × 2 = 400K Parameter mit Rank 8
Das ist Overkill! Rank verdoppeln zu r=16
LoRA ist eine der besten Innovationen im LLM-Space. Mit Rank-16 auf Q und V Projections trainierst du 95% Qualität mit 250× weniger Parametern. Praktisch auf Consumer-Hardware machbar in <2 Stunden.
