Lab 01 — LoRA / QLoRA Delta + Knowledge Distillation
Phase: 05 — PEFT: LoRA, QLoRA & Knowledge Distillation Difficulty: ⭐⭐⭐☆☆ (the linear algebra is small; the invariants are the lesson) Time: 3–4 hours
Adapting a frontier model used to mean copying all of its weights and paying for gradients and optimizer state on every one of them. PEFT broke that: freeze the giant base, learn a tiny correction. This lab builds the three mechanisms that make adaptation and serving affordable — LoRA (the low-rank delta
ΔW=(α/r)·A·B, with the all-important zero-initializedBso the adapted model starts as the pretrained one, andmerge()for zero inference overhead), QLoRA-style blockwise quantization (a 4-bit frozen base with a per-block absmax scale and a fixed symmetric grid, with a bounded reconstruction error), and knowledge distillation (training a small student on a big teacher's softened logits — the stablesoftmax_T, theT²·KLsoft term, and the blended loss).
What you build
matmul/mat_add/transpose— the three matrix helpers everything sits on; each raisesValueErroron a shape mismatch.LoRALinear(W_base, r, alpha, seed)— a frozen base(d×k), a Gaussian-initA(d×r), and a zero-initB(r×k)..forward(x)=x·W_base + (α/r)·(x·A)·B;.delta()=(α/r)·A·B;.merge()=W_base + ΔW. At init the delta is exactly the zero matrix.lora_param_count(d,k,r)=r·(d+k)andfull_param_count(d,k)=d·k— the "0.4% trainable" headline number, exactly.quantize_blockwise/dequantize_blockwise— NF4-style 4-bit storage: a per-block absmax scale + a fixed symmetric codebook; reconstruction error is bounded by the grid spacing and shrinks as you add levels.softmax_T/kl_divergence/cross_entropy/distillation_loss— the distillation kit: temperature softmax (max-subtracted), stable KL (log-space,0·log0guarded), and the Hinton lossT²·KL(teacher‖student) + α·CE(student,target).
Key concepts
| Concept | What to understand |
|---|---|
ΔW = (α/r)·A·B | adapt the update, not W; A·B is rank-r, so it trains r·(d+k) ≪ d·k params |
B is zero-init | so ΔW = 0 at step 0 ⇒ the adapted model is the pretrained model — the soul of LoRA |
α/r scaling | α sets the delta's magnitude independent of rank; doubling α doubles ΔW linearly |
merge() | fold the delta into the base ⇒ zero extra inference latency; the served model is one matrix |
| blockwise absmax | a tiny per-block scale tames outliers; a fixed grid stores the values in 4 bits |
| finer grid = less error | more levels ⇒ smaller quantization step ⇒ a strictly smaller error bound |
| softened logits | dividing by T>1 raises the non-top probabilities — the teacher's "dark knowledge" |
the T² factor | rescales the soft-term gradient (shrunk by ~1/T²) back onto the hard term's footing |
Files
| File | Purpose |
|---|---|
lab.py | skeleton with # TODO markers — your implementation |
solution.py | complete reference; python solution.py runs a worked example |
test_lab.py | the proof — run it red, make it green |
requirements.txt | pytest only (pure stdlib otherwise) |
Run
pip install -r requirements.txt
pytest test_lab.py -v # against your lab.py
LAB_MODULE=solution pytest test_lab.py -v # against the reference
python solution.py # the worked example
Success criteria
- All tests pass against your implementation (and
LAB_MODULE=solution). - You can explain why
test_lora_delta_is_zero_at_init(and..._forward_equals_base) is the soul test —Bzero-init means training starts at the pretrained model. - You can explain why
x · merge()equalsforward(x)and why that gives LoRA zero inference overhead once deployed. - You can state the LoRA trainable fraction for a
4096×4096projection atr=8(65,536 / 16,777,216 ≈ 0.39%) and why that is the entire economic case for PEFT. - You can explain why
test_quant_finer_grid_has_smaller_erroris monotonic and what the per-block absmax scale buys you over a single tensor-wide scale. - You can explain why
distillation_loss(t, t, …) == α·CE(the KL term vanishes) and why higherTincreases entropy (test_higher_temperature_softens).
How this maps to the real stack
| The miniature | The production mechanism | Where to verify it |
|---|---|---|
LoRALinear + merge() | peft's LoraConfig / get_peft_model; model.merge_and_unload() folds adapters for inference | HF peft; the adapter's adapter_config.json (r, lora_alpha, target_modules) |
A Gaussian, B zero | exactly PEFT's init: lora_A Kaiming/Gaussian, lora_B zeros, so the model is unchanged at step 0 | peft.tuners.lora source; inspect lora_B weights right after wrapping |
lora_param_count | the "trainable params: 0.4%" line print_trainable_parameters() reports | model.print_trainable_parameters() |
quantize_blockwise (+absmax) | bitsandbytes NF4: blockwise 4-bit with a per-block absmax scale, plus double quantization of the scales | bitsandbytes Linear4bit; QLoRA's bnb_4bit_quant_type="nf4" |
softmax_T / kl_divergence | the distillation objective in TinyBERT / DistilBERT / transformers-based KD trainers | a KD training loop; torch.nn.KLDivLoss(reduction="batchmean") on log-softmax |
distillation_loss (T²·KL + α·CE) | Hinton's response distillation; the T² rescale and the soft/hard blend are standard | Hinton et al. (2015); DistilBERT's loss |
Limits of the miniature (be honest in the interview): there is no autograd here —
you set A/B by hand and verify the forward algebra, not a training loop (P03 is
the autograd phase); our grid is a uniform symmetric codebook, while real NF4 uses 16
non-uniform levels fit to a normal distribution (and double-quantizes the scales);
distillation here is response-based on one example, not feature/relation distillation
over a dataset; and merge() ignores the small numeric drift real fp16 merging incurs.
Extensions (build these on real hardware)
- Add a tiny SGD loop: a target weight
W*, an MSE loss onforward(x)vsx·W*, and hand-rolled gradients forA,B; watch the rank-rdelta fit the low-rank part. - Replace the uniform grid with the real NF4 16-level codebook (quantiles of a unit
normal) and re-run
test_quant_finer_grid_has_smaller_erroragainst it. - Add double quantization: quantize the per-block
scalesthemselves and measure the extra bytes saved (QLoRA's ~0.4 bits/param trick). - Add
target_modulesselection: count params if you adapt only attentionq,vvs also the MLP, and reproduce the LoRA paper's "q,v is enough" result on param budget. - Add feature distillation: an extra MSE term between a student and teacher hidden layer, and show it helps when the soft-logit signal alone is weak.
Interview / resume
- Talking points: "Why is LoRA's
Bzero-initialized, and what breaks if it isn't?" "How does QLoRA fine-tune a 65B model on one 48GB GPU?" "Walk me through the distillation loss and the role ofTandT²." "When do you merge adapters, and what does it cost?" - Resume bullet: Implemented LoRA/QLoRA from scratch — the low-rank delta
ΔW=(α/r)·A·Bwith zero-initializedBand a mergeable adapter, NF4-style blockwise 4-bit quantization with a bounded reconstruction error, and a temperature-scaled knowledge-distillation loss (T²·KL + α·CE) — with a deterministic, dependency-free test suite proving each invariant.