75 minutes · the first module of Pillar 2 — where you do your first real fine-tune
Pillar 2 — Parameter-Efficient Fine-Tuning
LoRA (Hu et al. 2021) freezes the pretrained W₀ and adds a low-rank update B·A.
Frozen
W₀ — pretrained weight (d×k). No gradient. The base, untouched.
Trainable (the adapter)
A (r×k) — random-init · B (d×r) — ZERO-init
Dimensional collapse: a 4096×4096 ΔW is 16.7M numbers; the LoRA update at r=8 is 65K — a 256× reduction. Across every layer: adapters under 1% of the model.
This is the FT00 thesis made rigorous. If fine-tuning were injecting knowledge, you would need high rank. But it is redirecting probability mass the base already has — and that redirection is low-rank.
| Knob | What it does | Modern default |
|---|---|---|
| r (rank) | # directions of change | 16 (8 narrow, 64 complex) |
| α (alpha) | scaling: applied as α/r | α ≈ 2×r (r=16→α=32) |
| target_modules | which weights get adapters | ALL linear — q,k,v,o + gate,up,down |
| lora_dropout | regularization | 0.05 (0.1 small data/high r) |
from peft import LoraConfig
lora_config = LoraConfig(
r=16,
lora_alpha=32, # convention: alpha ≈ 2×r
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj", # attention
"gate_proj", "up_proj", "down_proj", # MLP (modern default)
],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
get_peft_model(model, lora_config) then print_trainable_parameters() → expect <1% trainable.
LoRA made adapters cheap. QLoRA freezes the base at 4-bit so the whole job fits a consumer GPU.
| # | Innovation | What it does |
|---|---|---|
| 1 | NF4 (NormalFloat 4-bit) | Quantile bins matched to the normal distribution. Info-theoretically optimal for Gaussian weights. |
| 2 | Double quantization | Quantize the quantization constants to 8-bit. Saves ~0.37 bits/param (~0.3 GB on 7B). |
| 3 | Paged optimizers | NVIDIA Unified Memory pages optimizer state to CPU — absorbs the OOM spikes during checkpointing. |
| Consumer | Rule | 7B |
|---|---|---|
| Base weights (4-bit NF4) | 0.5 B/param | ~3.5 GB |
| Quant constants (double-quant'd) | ~0.18 bits/param | ~0.16 GB |
| LoRA adapter (r=16, all-linear) | ~40M × ~10 bytes | ~0.4 GB |
| Activations (4K, batch 1, ckpt, FA2) | rule of thumb | ~4–7 GB |
| CUDA context + fragmentation | fixed overhead | ~1–2 GB |
| Total | ~10–14 GB → fits RTX 4090 (24GB) |
BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=bf16)prepare_model_for_kbit_training(model) — the step everyone forgetsget_peft_model(model, lora_config) → print_trainable_parameters() → expect <1%gradient_checkpointing=True, attn_implementation="flash_attention_2", optim="paged_adamw_8bit", decouple batch via gradient_accumulation_steps.
merge_and_unload() — deploy
Bakes the adapter into the base: W₀ := W₀ + (α/r)BA. One self-contained model, no PEFT at serve time. Re-quantize (GGUF/AWQ) afterward.
keep separate — hot-swap
Save adapter_model.safetensors (<100 MB). Load base + PeftModel.from_pretrained. One base, many adapters, swap at runtime.
Two teams, same trainable-param count. Who generalizes better?
| Team | Config | Result |
|---|---|---|
| A | r=16, all-linear (7 modules) | generalizes better on unseen prompts |
| B | r=56, attention-only (2 modules) | worse — missed the MLP pathway |
QLoRA-fine-tune a 1–1.5B base on 500 style-steering examples. Merge. Run inference. <30 min on a consumer GPU or Colab T4.
The steer
Make the model a terse, contrarian pirate. Pure steering — the base already knows the vocabulary; you make it reliable and in-format.
The loop
Load 4-bit → prepare → attach → train 3 epochs → merge → infer on an unseen question. Compare to the un-steered base. The gap is your adapter.
Qwen/Qwen2.5-1.5B-Instruct (open, ChatML). Fallbacks: MiniCPM5-1B, Llama-3.2-1B. Expect ~0.6% trainable, ~6–10 GB peak VRAM, loss 1.6 → ~0.9.
Next: FT09 — DoRA, rsLoRA & Modern PEFT
W = W₀ + BA — and tie it to the intrinsic dimension hypothesis & the FT00 thesis.The steering wheel is built. The tokens are verified. Now you have built the steer itself.