π¨ Fine-Tuning LLMs: A Visual GuideΒΆ
A beginner-friendly tour of what fine-tuning is, why PEFT exists, and every parameter strategy shipped in slick-tune: Full FT, LoRA, DoRA, AdaLoRA, and QLoRA.
No prior ML background required β diagrams first, math second, then how it maps to slick-tune. π
π Table of contentsΒΆ
1. πΊοΈ The big pictureΒΆ
A large language model (LLM) is a giant function: text in β next-token probabilities out. Those probabilities come from billions of numbers called weights (or parameters).
flowchart LR
subgraph world [How models get useful]
PT[Pre-training<br/>learn language from the internet]
FT[Fine-tuning<br/>specialize on your data]
USE[Inference<br/>answer users]
end
PT --> FT --> USE
Fine-tuning means: take an already-trained model and continue training on a smaller, task-specific dataset so it behaves the way you want β for example, answering questions about your product, your company, or (in slick-tuneβs demo) Amirhessam / SlickML.
slick-tune treats fine-tuning as five orthogonal axes:
model Γ strategy Γ objective Γ data Γ metrics
Axis |
Question it answers |
|---|---|
π§ model |
Which base checkpoint? ( |
π§© strategy |
How do weights change? (LoRA, QLoRA, full, β¦) |
π objective |
What loss / data contract? (SFT, DPO, ORPO, KTO) |
π data |
Which examples? (train JSONL + optional holdout) |
π metrics |
Did it learn? (loss, PPL, probes, judges) |
You can swap one axis without rewriting the others β that is the whole point of the library.
2. π₯ Pre-training vs prompting vs fine-tuningΒΆ
flowchart TB
subgraph pt [Pre-training]
Huge[Huge public text]
Base[Base / instruct model]
Huge --> Base
end
subgraph prompt [Prompting only]
Base2[Frozen model]
Prompt[Your prompt / RAG]
Out1[Answer]
Base2 --> Prompt --> Out1
end
subgraph ft [Fine-tuning]
Base3[Starting checkpoint]
Data[Your labeled examples]
New[Adapted model / adapter]
Out2[Answer that knows your facts]
Base3 --> Data --> New --> Out2
end
pt --> prompt
pt --> ft
Approach |
Weights change? |
Good for |
Limit |
|---|---|---|---|
Prompting / RAG |
No |
Quick demos, docs lookup |
Context length; model may still hallucinate your facts |
Fine-tuning |
Yes (all or adapters) |
Teaching stable facts/style/format |
Needs data + compute; can overfit |
Pre-training |
Yes, from scratch / continued |
New domains at web scale |
Extremely expensive |
π Rule of thumb: if you need the model to reliably know something small and personal (names, emails, product APIs), fine-tuning + probes beats hoping the prompt sticks.
3. βοΈ What actually changes when you fine-tune?ΒΆ
Inside a Transformer, most compute is linear layers: matrices that mix features.
flowchart LR
x[Input vector x] --> W["Weight matrix W"]
W --> y[Output y = Wx]
During fine-tuning we compute a loss (how wrong the next-token predictions were), then backpropagation produces a gradient for each trainable weight: βnudge this number up or down.β
An optimizer (usually AdamW) applies those nudges for many steps. After enough steps, the modelβs distribution shifts toward your data.
sequenceDiagram
participant D as Your dataset
participant M as Model
participant L as Loss
participant O as Optimizer
D->>M: batch of tokens
M->>L: predictions vs labels
L->>M: gradients
M->>O: trainable params + grads
O->>M: updated weights / adapters
4. π‘ Why not always update every weight?ΒΆ
Updating all weights (full fine-tuning) works, but:
Needs a lot of GPU memory (weights + gradients + optimizer states β severalΓ model size).
Produces a full copy of the model per run (hard to share many variants).
Easy to catastrophically forget general skills if data is tiny.
Parameter-Efficient Fine-Tuning (PEFT) freezes the base model and trains a small add-on (adapter). The most popular adapter family is LoRA.
flowchart TB
subgraph full [Full fine-tuning]
W1[All weights trainable]
end
subgraph peft [PEFT e.g. LoRA]
WF[Base weights frozen]
A[Tiny adapters trainable]
WF --- A
end
Full FT |
PEFT (LoRA-like) |
|
|---|---|---|
Trainable params |
~100% |
Often <1β5% |
Checkpoint size |
Entire model |
Small adapter files |
Multi-task serving |
Heavy |
Swap adapters on one base |
Quality ceiling |
Highest in theory |
Usually close for many tasks |
5. π§ Strategy overviewΒΆ
slick-tune strategies answer: how do we change weights?
flowchart TB
Start[Need to fine-tune?] --> Mem{GPU memory tight?}
Mem -->|Yes, CUDA| Q[QLoRA]
Mem -->|Yes, Mac/CPU| L[LoRA / DoRA]
Mem -->|Plenty| Choice{Want adaptive ranks?}
Choice -->|No| Fixed{Want DoRA decomposition?}
Fixed -->|No| L2[LoRA]
Fixed -->|Yes| D[DoRA]
Choice -->|Yes| AD[AdaLoRA]
Mem -->|Research / max quality| F[Full FT]
Strategy |
Idea in one sentence |
slick-tune class |
|---|---|---|
ποΈ Full |
Train every parameter |
|
π§© LoRA |
Freeze (W); train low-rank (A,B) |
|
β¨ DoRA |
LoRA + separate magnitude / direction |
|
π― AdaLoRA |
Start higher rank; prune toward a budget |
|
π¦ QLoRA |
4-bit frozen base + LoRA on top |
|
6. ποΈ Full fine-tuningΒΆ
π‘ Idea: every weight that can learn, learns.
flowchart LR
subgraph layer [One linear layer]
W["W β all entries trainable"]
end
Batch[Training batch] --> W
W --> Grad[Gradients for all of W]
Grad --> W
β When to use
Small base models where memory allows (slick-tuneβs SmolLM demos).
You need maximum capacity and will keep one specialized checkpoint.
βοΈ Trade-offs
Highest memory and storage cost.
One run β one full model directory (not a tiny adapter).
π§βπ» In slick-tune:
from slicktune import FullStrategy, SFTObjective, Tuner
Tuner(
model_id="HuggingFaceTB/SmolLM2-135M-Instruct",
strategy=FullStrategy(),
objective=SFTObjective(),
output_dir="outputs/sft_full",
).fit("examples/data/about_amir.jsonl")
7. π§© LoRA (Low-Rank Adaptation)ΒΆ
π IntuitionΒΆ
Instead of updating a huge matrix (W), keep (W) frozen and learn a small correction:
[ Wβ = W + \Delta W, \quad \Delta W = \frac{\alpha}{r}, B A ]
(A) is (r \times d_{\text{in}}) (often started random / Gaussian).
(B) is (d_{\text{out}} \times r) (often started at zero, so training begins as βno changeβ).
(r) (rank) is tiny (e.g. 8 or 16) vs thousands of hidden dims.
(\alpha) (alpha) scales the update; people often set (\alpha \approx 2r).
flowchart TB
x[Input x] --> W["Frozen W"]
x --> A["Trainable A<br/>shape r Γ din"]
A --> B["Trainable B<br/>shape dout Γ r"]
W --> sum((+))
B -->|"Ξ±/r Β· BAx"| sum
sum --> y[Output y]
Why βlow-rankβ? Empirically, the useful change (\Delta W) often lives in a low-dimensional subspace β so a thin (BA) is enough for many adaptations.
π Where adapters attachΒΆ
LoRA is usually injected into attention / MLP linear projections (q_proj, v_proj, β¦). slick-tune defaults to target_modules="all-linear" so PEFT discovers linear layers for you.
flowchart LR
subgraph transformer [Transformer block]
Attn[Attention linears + LoRA]
MLP[MLP linears + LoRA]
end
Tok[Tokens] --> Attn --> MLP --> Next[Next block]
π ServingΒΆ
After training you typically have:
the base model (unchanged), and
a small adapter folder (
adapter_model.safetensors,adapter_config.json).
At inference: load base + adapter, or merge adapter into (W) for engines that want a single set of weights.
ποΈ Knobs that matterΒΆ
Knob |
Meaning |
Typical start |
|---|---|---|
|
Rank / capacity of (\Delta W) |
8β64 |
|
Strength of update |
(2r) |
|
Regularize adapters |
0.05 |
|
Which layers get LoRA |
|
from slicktune import LoRAStrategy
LoRAStrategy(r=16, alpha=32, dropout=0.05)
8. β¨ DoRA (Weight-Decomposed LoRA)ΒΆ
π IntuitionΒΆ
Full fine-tuning changes both how large a weight row is (magnitude) and which direction it points. Plain LoRA mostly learns a directional update on top of frozen (W).
DoRA decomposes the adapted weight into:
a magnitude vector (m), and
a direction component updated with a LoRA-style low-rank term.
flowchart TB
W[Frozen pretrained W] --> Dir[Direction path<br/>LoRA-style BA]
W --> Mag[Magnitude m<br/>trainable scale]
Dir --> Norm[Normalize direction]
Norm --> Mix["m Β· direction"]
Mag --> Mix
Mix --> Y[Layer output]
Same knobs as LoRA (r, alpha, β¦) plus use_dora=True under the hood in PEFT.
β When to try DoRA
You like LoRAβs cost but want a bit more quality headroom.
Slightly more compute than LoRA; still PEFT-cheap vs full FT.
from slicktune import DoRAStrategy
DoRAStrategy(r=16, alpha=32)
9. π― AdaLoRA (Adaptive-rank LoRA)ΒΆ
π IntuitionΒΆ
Not every layer needs the same rank. AdaLoRA:
Starts with a higher initial rank (
init_r).Scores parameter importance during training.
Prunes toward an average target rank (
target_r).Uses a schedule: warmup (
tinit) β allocate/prune (deltaT) β final fine-tune (tfinal).
flowchart LR
subgraph timeline [Training steps]
T0[t = 0 β¦ tinit<br/>warmup, little/no prune]
T1[tinit β¦ totalβtfinal<br/>importance + rank budget]
T2[last tfinal steps<br/>fixed ranks, fine-tune]
end
T0 --> T1 --> T2
flowchart TB
Layers[Many LoRA layers] --> Score[Importance scores from grads]
Score --> Budget[Global rank budget β target_r]
Budget --> Prune[Mask / shrink unimportant singular directions]
Prune --> Layers
β οΈ Critical detail: PEFTβs update_and_allocate must run after optimizer.step() and before zero_grad() (gradients must still exist). slick-tuneβs AdaLoRACallback hooks Hugging Face Trainerβs on_optimizer_step for this.
β When to try AdaLoRA
Longer runs where adaptive capacity might help.
Tiny memorization demos often need warmup (
tinit) + slightly higher LR than LoRA β seeexamples/run_sft_adalora.py.
from slicktune import AdaLoRAStrategy
AdaLoRAStrategy(init_r=16, target_r=12, tinit=60, tfinal=30, deltaT=5)
10. π¦ QLoRA (Quantized LoRA)ΒΆ
π IntuitionΒΆ
QLoRA keeps a 4-bit quantized copy of the base weights in memory (NF4 + double quant in the common setup), computes in a higher precision (e.g. bfloat16), and still trains LoRA adapters in higher precision.
flowchart TB
Disk[16/32-bit checkpoint on disk] --> Q["Load as 4-bit base<br/>frozen"]
Q --> Lora[Trainable LoRA A,B in bf16/fp16]
Batch[Batch] --> Q
Batch --> Lora
Q --> Out[Forward]
Lora --> Out
π€ Why it exists: fine-tune larger models on smaller GPUs by shrinking the memory footprint of the frozen base.
π Requirements in slick-tune
CUDA GPU (bitsandbytes 4-bit path).
Extra install:
uv sync --extra qlora.On Apple Silicon / CPU β use LoRA, not QLoRA.
from slicktune import QLoRAStrategy
QLoRAStrategy(r=16, alpha=32)
11. π³οΈ Choosing a strategyΒΆ
flowchart TB
A[Start] --> B{Hardware}
B -->|CUDA + big model + tight VRAM| Q[QLoRA]
B -->|Mac MPS / CPU| L[LoRA]
B -->|CUDA plenty of VRAM| C{Goal}
C -->|Simple default| L2[LoRA]
C -->|LoRA-quality+| D[DoRA]
C -->|Adaptive ranks| AD[AdaLoRA]
C -->|Max quality / tiny model| F[Full]
Situation |
Prefer |
|---|---|
Laptop smoke test (Mac) |
LoRA (or DoRA) |
First serious PEFT run |
LoRA |
Want LoRA-like cost, try better quality |
DoRA |
Long run, explore rank budgets |
AdaLoRA |
7B+ on a single consumer GPU |
QLoRA |
Small model, memory OK, one final specialist |
Full |
π Orthogonal reminder: strategy β objective. You can run LoRA + SFT today and later LoRA + DPO without changing the adapter idea.
12. π Objectives: what the model learnsΒΆ
Objective |
Data shape |
Loss idea |
Phase |
|---|---|---|---|
SFT |
instruction β response (chat |
Next-token NLL on the answer |
Phase 1β2 |
DPO / ORPO / KTO |
preferred vs rejected (or unpaired KTO labels) |
Preference optimization |
Phase 3 (now) |
GRPO / RL |
prompts + rewards |
Policy improvement |
Phase 4+ |
flowchart LR
subgraph sft [SFT]
U[User turn] --> A[Assistant turn]
A --> NLL[Maximize likelihood of assistant tokens]
end
slick-tuneβs SFTObjective expects a dataset with a messages column (JSONL loaders also accept prompt/response and instruction/output).
13. π§ͺ Did it work? Probes, holdout PPL, judgesΒΆ
Fine-tuning can lower training loss while still failing your real goal. Measure explicitly: π
flowchart TB
Train[Train JSONL] --> Fit[Tuner.fit]
Fit --> Adapter[Adapter / checkpoint]
Hold[Holdout JSONL] --> PPL[Holdout perplexity]
Adapter --> PPL
Probes[Probe JSONL] --> Gen[Generate answers]
Adapter --> Gen
Gen --> Sub[SubstringJudge]
Gen --> LLM[LLMJudge]
Signal |
Asks |
Good when |
|---|---|---|
Train loss |
Did optimization move? |
Downward trend |
Holdout perplexity |
How surprising is unseen text? |
Lower PPL on |
Probes + substring judge |
Does the answer contain your fact? |
High pass rate |
LLM judge |
Rubric score 0β10 |
Stronger judge model; weak on tiny self-judges |
Perplexity (= e^{\text{mean NLL}}). Intuition: effective branching factor for next tokens β lower is better.
π Shipped demo files:
File |
Role |
|---|---|
|
ποΈ Train |
|
π Holdout PPL |
|
β Fact checks |
14. π How slick-tune wires this togetherΒΆ
flowchart TB
subgraph inputs [You provide]
mid[model_id]
strat[Strategy]
obj[Objective]
data[train JSONL]
eval[optional eval JSONL]
probes[optional probes]
end
subgraph tuner [Tuner.fit]
load[load tokenizer + model]
apply[strategy.apply β PEFT or full]
train[TRL SFTTrainer]
metrics[MetricsTracker]
end
subgraph out [Artifacts]
ckpt[adapter or full weights]
mj[metrics.json]
end
mid --> load
strat --> apply
load --> apply --> train
obj --> train
data --> train
train --> metrics
eval --> metrics
probes --> metrics
train --> ckpt
metrics --> mj
β¨οΈ CLI shortcuts:
# ποΈ Train
uv run slicktune train --strategy lora \
--data examples/data/about_amir.jsonl \
--eval-data examples/data/about_amir.eval.jsonl \
--output outputs/sft_lora
# π§ͺ Probes
uv run slicktune probe \
--model-dir outputs/sft_lora \
--probes examples/data/about_amir.probes.jsonl
# π Holdout PPL + judges
uv run slicktune eval \
--model-dir outputs/sft_lora \
--eval-data examples/data/about_amir.eval.jsonl \
--probes examples/data/about_amir.probes.jsonl \
--judge substring
15. π GlossaryΒΆ
Term |
Meaning |
|---|---|
Weight / parameter |
A learned number inside the model |
Gradient |
Direction to change a weight to reduce loss |
Adapter / PEFT |
Small trainable module; base frozen |
Rank (r) |
Inner dimension of LoRAβs (A,B) |
Alpha (\alpha) |
Scales LoRA update |
Quantization |
Store weights in fewer bits (e.g. 4-bit) |
SFT |
Supervised fine-tuning on inputβoutput demos |
Holdout |
Eval data not used for training steps |
Perplexity (PPL) |
(\exp(\text{mean token NLL})); lower = better fit |
Probe |
Question + required substring to verify learning |
Merge |
Bake LoRA (\Delta W) into (W) for single-file serving |
16. π Further readingΒΆ
π LoRA paper: Low-Rank Adaptation of Large Language Models
π QLoRA: QLoRA: Efficient Finetuning of Quantized LLMs
π DoRA: Weight-Decomposed Low-Rank Adaptation
π AdaLoRA: Adaptive Budget Allocation for Parameter-Efficient Fine-Tuning
π€ Hugging Face PEFT docs: https://huggingface.co/docs/peft
π§ slick-tune README: https://github.com/slickml/slick-tune
π§ Maintained with slick-tune β swap strategy, keep the rest of the stack.