Fine-Tuning — LoRA, QLoRA, PEFT, When NOT to Fine-Tune
Fine-Tuning — LoRA, QLoRA, PEFT, When NOT to Fine-Tune: mechanisms, worked examples, assumptions and source-backed corrections with explicit experiment boundaries.
Original publication date unrecordedUpdated
Lesson preparation & details
Level: advanced
By the end, you should be able to
- Explain in one breath why LoRA can update <1% of the parameters and still work.
- Pick between LoRA, QLoRA, and full fine-tuning given a GPU budget, dataset size, and quality target.
- Recognise the five 'do NOT fine-tune, do this instead' patterns.
- Train a CPU LoRA branch and design a versioned pretrained-model fine-tuning experiment.
- Explain shared-base adapter serving, additional memory, and cache isolation.
Bring with you
Sources checked: . A source check does not change the publication or update date.
Editorial review: · What review means
In this article · 9 sections
Why this session exists
Fine-tuning changes parameters; prompting and retrieval change inputs. Choose based on observed failures, not a promise that a small adapter teaches every fact or fits any GPU. The bolt-on engine analogy captures parameter sharing, but not universal costs or quality. A base model can retain knowledge in its weights and learn new facts during fine-tuning; the problem is reliable recall, freshness, provenance and deletion, not a prohibition on learning facts.
You will derive and implement LoRA, compare LoRA/QLoRA/full fine-tuning, design a small held-out adaptation experiment, and understand adapter loading, merging and multi-adapter serving. Transfer learning (105), Transformer projections (112) and evaluation (122) are prerequisites. No “free GPU in ten minutes” or fixed quality/memory outcome is promised.
LoRA from shapes to gradients
For column input x with width i and output width o, let W0 have shape (o,i), A (r,i) and B (o,r):
The update has rank at most r, but W0 need not be low rank. Trainable parameters are r(i+o) rather than oi. For i=o=4096, r=8, the full matrix has 16,777,216 parameters and the adapter 65,536, exactly 1/256 or 0.390625%. The original diagram reversed A/B dimensions relative to its column-vector equation. Shapes, not names alone, determine the multiplication.
Figure text equivalent — labels and relationships
- Node “Input width i” (square).
- Node “Frozen W0: o by i” (square).
- Node “Trainable A: r by i” (square).
- Node “Trainable B: o by r” (square).
- Node “Multiply by alpha over r” (square).
- Node “Sum: output width o” (square).
- “Input width i” leads to “Frozen W0: o by i”. Line: normal; ends: arrow point.
- “Input width i” leads to “Trainable A: r by i”. Line: normal; ends: arrow point.
- “Trainable A: r by i” leads to “Trainable B: o by r”. Line: normal; ends: arrow point.
- “Trainable B: o by r” leads to “Multiply by alpha over r”. Line: normal; ends: arrow point.
- “Frozen W0: o by i” leads to “Sum: output width o”. Line: normal; ends: arrow point.
- “Multiply by alpha over r” leads to “Sum: output width o”. Line: normal; ends: arrow point.
Hu et al. motivate low-rank adaptation empirically. A coherent task or small-magnitude update is not by construction low rank: a tiny multiple of the identity is full rank. Pretrained representations can make a restricted update effective, but there is no proof that rank eight matches full fine-tuning on every task. Larger rank increases capacity and parameters linearly; quality and overfitting are not monotone laws.
For one example with upstream gradient g, dL/dB = s*g*(Ax)^T and dL/dA = s*B^T*g*x^T. Random A and zero B give zero initial delta, nonzero potential B gradient and zero first-step A gradient. Both zero make both gradients zero, a dead adapter. Zero initial delta preserves the base function in matched evaluation conditions; it does not prove every future training step is a refinement or cannot hurt capability.
Freeze the whole base before injecting chosen adapters. Frozen weights need no parameter gradients or optimizer moments, but operations through frozen layers may still need backpropagation to earlier adapters. Activations, gradients for adapters, temporary buffers and retained base weights remain memory costs. Do not put the entire base forward in no_grad when upstream trainable components need gradients.
Placement, scaling, QLoRA and a realistic memory budget
Q/V-only adaptation is a historical configuration, not a universal optimum or proof that K is unimportant. All attention projections and MLP projections add adaptation capacity and parameters; actual counts depend on dimensions. GQA can make V narrower than Q, so an all-square shortcut can be wrong. Embeddings/output heads may need changes for new vocabulary or objectives; inspect which parameters are trainable.
For an illustrative Llama-style layer with i=o=4096 and MLP width 11008, all four square attention adapters at r=16 use 524,288 parameters; three rectangular MLP adapters use 724,992; across 32 layers the total is 39,976,960. Q/V-only square adapters total 8,388,608. These are declared architecture assumptions, not a generic “42M at rank16” model count. A two-byte adapter payload is about 80MB decimal for the all-projection example, before metadata or optimizer state.
For P=7 billion trainable parameters, an illustrative mixed-precision Adam budget of 2-byte weights, 2-byte gradients, 4-byte master weights and two 4-byte moments is 16P=112GB decimal before activations. Implementations/precision/sharding change this. LoRA removes base gradient/optimizer state, not all P-dependent storage or compute.
QLoRA backpropagates through a frozen four-bit base into adapters and introduces NF4, double quantization and paged optimizers. Its reported 65B-on-48GB setup is a specific experiment, not “13–70B on 24GB.” Even a 70B four-bit raw payload is 35GB decimal before scales, adapters or activations. Computation uses suitable higher-precision representations; quantization error remains even though the base is frozen. Kernel/hardware/version compatibility and numerical finiteness must be tested. NF4 and BF16 are not a universal cure for NaNs.
PEFT 0.17.0 documents initialization, targets, scaling and merging. Standard alpha/r scaling differs from rank-stabilized variants. r=8–16, alpha=2r, dropout=.05, learning rate 1e-4 and a few epochs are candidate sweep points, not a “safe canonical” prescription or a proven 10× learning-rate tolerance. Prompt/prefix tuning are distinct parameter-efficient choices, not universally the weakest method.
Hands-on: train the actual low-rank branch on CPU
This toy regression trains a known rank-two residual with a frozen random base. It proves parameter/gradient/merge contracts and fits a synthetic target, not language style transfer or a pretrained-model benchmark.
import torch
from torch import nn
torch.manual_seed(123)
torch.set_num_threads(1)
torch.set_default_dtype(torch.float64)
class LoRALinear(nn.Module):
def __init__(self, base, rank=2, alpha=2):
super().__init__()
self.base = base
for p in base.parameters():
p.requires_grad_(False)
self.A = nn.Parameter(torch.randn(rank, base.in_features)*.1)
self.B = nn.Parameter(torch.zeros(base.out_features, rank))
self.scale = alpha/rank
def forward(self, x):
return self.base(x) + self.scale*(x@self.A.T)@self.B.T
def merged(self, x):
return torch.nn.functional.linear(x, self.base.weight+self.scale*self.B@self.A,
self.base.bias)
base = nn.Linear(4, 3, bias=False)
model = LoRALinear(base)
base_before = base.weight.detach().clone()
x = torch.randn(80, 4)
true_A, true_B = torch.randn(2, 4)*.3, torch.randn(3, 2)*.3
y = x@(base_before+true_B@true_A).T
assert torch.equal(model(x), base(x))
loss = ((model(x)-y)**2).mean()
loss.backward()
assert torch.count_nonzero(model.A.grad) == 0
assert torch.count_nonzero(model.B.grad) > 0
assert base.weight.grad is None
initial = loss.item()
opt = torch.optim.Adam([model.A, model.B], lr=.03)
for _ in range(600):
opt.zero_grad()
loss = ((model(x)-y)**2).mean()
loss.backward()
opt.step()
assert loss.item() < initial*1e-4
assert torch.equal(base.weight, base_before)
assert torch.allclose(model(x), model.merged(x), atol=1e-12)
assert model.A.numel()+model.B.numel() == 2*(4+3)
# Finite-difference oracle for one adapter coordinate.
opt.zero_grad(); ((model(x)-y)**2).mean().backward()
grad = model.B.grad[0,0].item(); eps = 1e-5
with torch.no_grad():
orig = model.B[0,0].item()
model.B[0,0] = orig+eps; plus = ((model(x)-y)**2).mean().item()
model.B[0,0] = orig-eps; minus = ((model(x)-y)**2).mean().item()
model.B[0,0] = orig
assert abs(grad-(plus-minus)/(2*eps)) < 1e-8
# Both-zero initialization cannot start learning this branch.
dead = LoRALinear(nn.Linear(4, 3, bias=False))
with torch.no_grad(): dead.A.zero_()
((dead(x)-y)**2).mean().backward()
assert torch.count_nonzero(dead.A.grad) == torch.count_nonzero(dead.B.grad) == 0
# Labels: padding masked, true EOS preserved even when PAD == EOS.
ids = [11, 12, 2, 2, 2]; mask = [1, 1, 1, 0, 0]
labels = [token if active else -100 for token, active in zip(ids, mask)]
assert labels == [11, 12, 2, -100, -100]
print("LoRA CPU fit/gradient/merge/masking pass", initial, loss.item())Restored language fine-tuning and adapter procedure
The original TinyLlama/pirate example was not dynamic execution; its model.eval() call is normal evaluation mode. It did need substantive repair. Repeating five rows thirty times does not create 150 independent examples, nor does pirate-like output establish generalization. Preserve that demonstration as a source-reviewed, unexecuted pretrained recipe, not a discarded objective:
- Use an authorized local base/tokenizer snapshot with pinned Transformers/PEFT versions and record the exact revision; avoid accidental downloads. Inspect its chat template and supported special tokens instead of inventing a user/assistant format.
- Split distinct examples by source/template/customer before duplication or augmentation. Keep style, factual grounding, general instruction and safety tests separate. Format user/assistant turns using the tokenizer's template.
- Tokenize with explicit truncation and attention masks. Copy labels for full-sequence causal training (the model shifts internally), then mask padding with -100 using the attention mask, not every EOS token when PAD=EOS. If assistant-only loss is intended, also mask prompt tokens using verified template boundaries. Detect samples whose entire response was truncated.
- Attach
LoraConfig(task_type=CAUSAL_LM, r=8, lora_alpha=16, target_modules=["q_proj","v_proj"], lora_dropout=.05)only after verifying those module names/shapes; inspect all trainable parameters. Choose dtype/device and gradient accumulation from a measured memory budget, not a T4-time promise. - Train with held-out evaluations, finite loss/gradient checks, logged seed/versions, an explicit learning-rate/rank/target sweep and a bounded step budget. Save adapter configuration and weights plus the base/tokenizer revision and data provenance. Adapter-only
save_pretrainedis not a complete standalone base model. - Reload on a compatible clean base, use evaluation mode and held-out prompts, and compare against the baseline. Remove all model/Trainer references before assuming GPU memory is freed; deleting one variable may not free shared references.
- For the original pirate/Shakespeare swap experiment, load one adapter with a name,
load_adapteranother compatible adapter, thenset_adapterbefore generation. Verify output and adapter identity; do not claim a serving throughput gain from sequential swapping.
Merging forms W0+sBA and removes separate low-rank linear operations in suitable precision. Keep the pristine base and adapter if switching/rollback is needed; a merged artifact alone loses separation. Quantize–merge–requantize can change rounding. Multi-LoRA engines share a base while routing adapter-specific branches; adapter memory, batch scheduling and loading still cost resources. Cache identity must include adapter/base identity, not just prefix text. Cross-tenant adapter or KV mixing is a data-isolation bug, not just a performance issue.
When not to fine-tune and the smallest useful experiment
Five stop-and-diagnose patterns are: (1) a better prompt already meets requirements; (2) failures are missing/stale/permissioned evidence; (3) labels or evals are unreliable/leaky; (4) the training/serving lifecycle is unaffordable; (5) the required guarantee is exact schema/arithmetic/authorization better enforced in code. There is no universal 500-example floor. RAG supports updateable cited evidence but needs index/cache/log deletion and permissions; retrieval does not make deletion instantaneous or all answers correct. Fine-tuning can complement retrieval, but “almost every system needs both” is unsupported.
For 2,000 support examples, split by source/time/customer into 1,400 training, 300 validation and 300 untouched test cases. Compare current prompt+RAG, improved prompt+same RAG, and LoRA+same RAG; tune on validation only. Score behavioral format, factual grounding, refusal/over-refusal, general regressions, latency and cost. Predeclare acceptable tradeoffs and confidence requirements. A 4pp domain gain plus 6pp general loss is not automatically shippable even for a narrow product; assess affected users and nonnegotiable safety/quality limits.
Catastrophic forgetting, wrong target modules, high learning rate, leakage and missing evaluation are concrete failure modes. General-data mixing is a candidate mitigation with licensing/privacy and weighting considerations, not a guaranteed 20% cure. A NaN investigation checks dtype support, kernels, finite inputs, loss masks and optimizer behavior; do not attribute it to a particular undocumented GPU bug. The original fintech cost/accuracy and multi-tenant latency anecdotes are now these design cases, without invented incident numbers.
Recall and stretch: complete answers
- Why LoRA? A low-rank update is an empirical adaptation hypothesis that reduces trainable state; task coherence does not prove low rank.
- QLoRA? Frozen quantized base plus higher-precision adapter training and associated memory techniques; select it from full measured memory/quality constraints, not parameter-count slogans.
- Avoid fine-tuning? Prompt suffices, evidence is missing/stale, or reliable data/evaluation/lifecycle support is absent. No hard sample-count law.
- Rank effects? Parameters grow as r(i+o); capacity rises, but quality and overfitting must be measured under a fixed data budget.
- General data? It can mitigate forgetting; validate mixture, licensing and both domain/general outcomes rather than imposing a magic percentage.
- Q/V versus K? The paper studies placement under a budget; it does not prove K never matters. Inspect architecture and compare placements.
- Merge versus load? Merge materializes the scaled delta in weights; separate loading retains adapter identity/switchability and extra operations. Preserve originals for rollback.
- Domain up, general down? Apply predeclared task/safety gates and uncertainty; neither universal ship nor universal reject.
- Multi-LoRA? One base, adapter-specific operations/weights per request with a suitable scheduler. No universal throughput multiplier; isolate cache/tenant identities.
- Experiment stretch: train/validation/test separation, prompt and retrieval baselines, fixed corpus, multiple quality slices and measured lifecycle cost. Choose the simplest accepted variant.
Explain the shape/count derivation, show the dead-initialization negative control, and describe a multi-adapter cache-isolation trap. This connects transfer learning, evaluation, serving (124), multimodal adapters (125) and the chat-product design (130) without promising a pretrained language result from a CPU matrix exercise.
Optional resources and onward navigation
The original optional pre-read links are retained as navigation, not fact-check evidence or promises about video length, quality or API stability. Use the versioned primary sources in the teaching for consequential claims. Original pacing was a suggested 90-minute session, not a completion guarantee.
- LoRA explained in 10 minutes
- Efficient Fine-Tuning with LoRA & QLoRA
- Fine-tune Llama with LoRA in Colab
- LoRA · Low-Rank Adaptation of Large Language Models
- Hugging Face PEFT Documentation
- QLoRA is All You Need?
Next in the original sequence: S124 · LLM Serving — KV Cache, Batching, Speculative Decoding. Publication and release navigation are adjudicated by the parent.
Hub: The 6-Month Learning Plan. The original 130-session scaffold is retained as navigation, not evidence that every destination has been reviewed.
Review and execution boundary
Reviewed against complete original and pre-edit current teaching with claim-specific primary evidence and independently replayed coverage. The executable CPU program is a bounded teaching test, not a pretrained model, serving, RAG or training benchmark. External model/service recipes remain source-supported and unexecuted. No weights, datasets, GPU, paid API or new service were used. Source publication is not end-to-end application acceptance.
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.