Lab 03 — PEFT/LoRA Adapters & the Hub: Fine-Tune Small, Ship Versioned
Phase 30 · Lab 03 · Phase README · Warmup
The problem
Full fine-tuning of a 7B-parameter model updates every weight: you need optimizer state for
all of them (roughly 4x the model in memory for Adam), and you get a full multi-gigabyte
checkpoint per task. LoRA (Low-Rank Adaptation) observes that fine-tuning updates have low
intrinsic rank, freezes the base weight matrix W, and learns a factored update instead:
$$ W' = W + \frac{\alpha}{r} , A B \qquad A \in \mathbb{R}^{d_{out} \times r},; B \in \mathbb{R}^{r \times d_{in}},; r \ll \min(d_{out}, d_{in}) $$
Only A and B train — typically well under 1% of the parameters — and at deploy time you
choose: merge the product into W (zero inference overhead, the adapter vanishes) or keep it
separate and swap adapters per task (one shared base model, many megabyte-sized adapters).
The arithmetic identity that makes merging safe — merged forward output equals unmerged forward
output, exactly — is something you should be able to prove, and this lab's tests do.
The second half is where fine-tuned artifacts live: the Hub. from_pretrained("org/model", revision=...) is a query against a versioned artifact store — every repo state is a commit,
tags name revisions, main floats, and pinning a revision is the difference between a
reproducible deployment and "the model changed under us on Tuesday." The Hub is also where the
safetensors vs pickle decision bites: pickle deserialization can execute arbitrary code, so a
safe loader refuses it unless you explicitly opt in — the trust_remote_code=True supply-chain
decision, made mechanical.
What you build
| Piece | What it does | The lesson |
|---|---|---|
mat_vec / mat_mul / mat_add / mat_scale | list-of-lists linear algebra | the LoRA formula is four lines of math, not a framework |
LoRAConfig | r, lora_alpha, target_modules, scaling = alpha/r | the real peft.LoraConfig knobs and what each means |
LoRALinear.forward | W@x + (alpha/r)·(A@(B@x)), with B zero-initialized | a fresh adapter is a no-op — training starts from the base behavior |
trainable_parameters / apply_gradient_step | only A/B may change; the base never does | parameter-efficiency is an enforced invariant, not a convention |
merge() | W ← W + scaling·(A@B); forward identical before/after | merge trades swappability for zero serving overhead |
state_dict / load_state_dict | save/load/swap adapters (rank & shape checked) | one base, many task adapters — the multi-tenant serving pattern |
Hub.push / tag / resolve_revision / from_pretrained | commit history, full-snapshot commits, tags, main | from_pretrained is a versioned-store query; pin revisions |
| the safety gate | non-safetensors artifacts refused without trust_remote_code=True | safe-by-default loading; opt-in is a supply-chain decision |
push_adapter / load_adapter | the PEFT publish/consume loop over the Hub | ship megabytes per task, record the exact serving sha |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference implementation + worked example (python solution.py) |
test_lab.py | 31 tests: the LoRA formula, freeze invariant, merge identity, adapter swap, hub round-trip, revision pinning, unsafe-load gate |
requirements.txt | pytest only |
Run it
pytest test_lab.py -v # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v # the reference (green)
python solution.py # worked example
Success criteria
-
A fresh
LoRALinearis an exact no-op (Bstarts at zeros): forward equalsW@x. -
After training,
forward(x)matches the hand-computedW@x + (alpha/r)·(A@(B@x))— the formula, verified numerically. -
trainable_parameters()exposes onlylora_A/lora_B, and repeated gradient steps leavebase_weightbyte-identical. -
merge()changesWby exactlydelta_weight()and the merged forward equals the unmerged forward; double-merge and train-after-merge are rejected. -
Loading a different adapter into the same layer changes the output; a rank/shape mismatch
raises
AdapterError. -
push→from_pretrainedround-trips files, card, and sha; commits are full snapshots; shas are deterministic. -
revision="v1.0"keeps returning the pinned commit after new pushes tomain. -
A pickle-format artifact is refused without
trust_remote_code=Trueand loads with it; safetensors loads without any flag. -
All 31 tests pass under both
labandsolution.
How this maps to the real stack
peft.LoraConfig(r=8, lora_alpha=16, target_modules=["q_proj","v_proj"])andget_peft_model(model, config)are the real API: PEFT walks the model, finds the named target modules (attention projections are the classic choice, per the LoRA paper), and wraps each in alora.Linearthat carries frozen base weights plus trainablelora_A/lora_B— structurally ourLoRALinear. Real PEFT initializesAwith a small random Gaussian andBwith zeros; either way the product starts at zero, which is the invariant our zero-init keeps.model.merge_and_unload()is ourmerge(): foldscaling·(A@B)into the base weight and drop the adapter path. Real PEFT also supportsunmerge(), multiple named adapters on one model withset_adapter()switching between them — ourload_state_dictswap is that mechanism's core.- QLoRA is this exact machinery with one change: the frozen base weights are stored 4-bit
(NF4 quantization via
bitsandbytes) whileA/Bstay in higher precision — which is why a QLoRA adapter is just a LoRA adapter; the quantization lives in the base. The Warmup covers the NF4/double-quantization details. huggingface_hubis the real client:push_to_hub/upload_filecommit to a git-backed repo,from_pretrained(repo_id, revision=...)accepts a branch, tag, or commit sha, andhf_hub_downloadcaches by revision. Our full-snapshot commits andsha-Ncounters are the deterministic miniature of the same content-addressed history.- Model cards really are
README.mdfiles with YAML front-matter (license, tags, datasets, metrics) — ourModelCarddataclass is that front-matter. License and gated-access checks are a real pre-adoption step, not paperwork (Warmup §12). - safetensors vs pickle is a real security boundary: PyTorch
.bincheckpoints are pickle and can execute code on load;safetensorsis a pure tensor format that cannot. The Hub scans for malicious pickles,transformersdefaults to safetensors when available, andtrust_remote_code=True(needed for repos that ship custom modeling code) is the explicit opt-in our flag models. Realtrust_remote_codegates custom code execution specifically; our miniature folds the pickle risk and the custom-code risk into one gate to keep the lesson (safe-by-default, explicit opt-in) sharp. load_adapterreturning the sha mirrors the production discipline of recording exactly which artifact revision is serving — the same pin-your-dependencies rule as a container digest.
Limits of the miniature. Real LoRA wraps many modules across many transformer layers and
trains with an actual optimizer over batched activations; ours is one linear layer with an
injected "gradient step," because the invariants (what may change, what the merge preserves)
are the lesson — the training loop itself is Phase 30's Warmup §9 (Trainer) and the transformer
internals are the sibling Senior AI Engineer track. The real Hub is git + LFS with
content-addressed blobs, resumable downloads, and a global CDN; ours is a dict with counters.
Real trust_remote_code executes repo-provided Python classes; nothing here executes anything —
the flag models the decision, not the mechanism.
Extensions (your own machine)
- Add
unmerge(): subtractdelta_weight()back out and prove forward returns to the pre-merge adapter path (floating-point tolerance applies — say why). - Add multiple named adapters (
add_adapter(name)/set_adapter(name)) on one layer, the way PEFT hosts several tasks on one base model. - Simulate QLoRA storage: quantize the base weights to 4-bit integers with a per-row scale,
dequantize in
forward, and measure the output error against the float base. - With real libraries:
peft.get_peft_modelover a smalltransformersmodel, printmodel.print_trainable_parameters(), and confirm the ratio matches yournum_trainablemath.
Interview / resume signal
"Implemented LoRA from the formula: a frozen base weight wrapped with rank-r A·B factors scaled by alpha/r, with tests proving the freeze invariant (only A/B ever change), the merge identity (merged forward == unmerged forward, exactly), and adapter save/load/swap — plus a versioned Hub client with full-snapshot commits, tag/sha revision pinning, and safe-by-default loading that refuses pickle artifacts without an explicit trust_remote_code opt-in."