Full fine-tune vs PEFT
PEFT is the default; full fine-tuning is for big shifts with data and hardware. The memory math explains why — and why QLoRA exists. Plus merging and multi-adapter serving.
Learning objectives
- Compare full fine-tuning and PEFT on memory, cost, and when each is right.
- Do the memory math for full fine-tuning (why it's so heavy).
- Explain adapter merging and multi-adapter serving.
- Choose an approach for a given constraint.
30-second recap
- When fine-tuning wins: Prompting and RAG change what the model sees; fine-tuning changes the model itself — worth it for consistent style, format or narrow skills, not for adding facts.
code/ft4-full-vs-peft/ in the course, with a README. Run the scripts or copy the configs directly.When you'd go full intermediate
Here's a decision that has quietly burned real budgets. A team decides their model needs to "learn their domain," so they reach for the biggest hammer they know — full fine-tuning — spin up a multi-GPU cluster, and three days and several thousand dollars later they have a model that's marginally better than what a $40 LoRA run would have produced in an afternoon on a single card. The mistake wasn't the tuning; it was skipping the question this whole section exists to answer: do I actually need to move every weight, or just nudge a few?
The plain way to hold the two options in your head is a renovation analogy. PEFT (LoRA/QLoRA) is like adding a smart-home overlay to a house: you leave the walls, wiring, and plumbing exactly as they are and bolt on a small module that changes how the place behaves. Full fine-tuning is knocking the house down to the studs and rebuilding — enormously more powerful if you genuinely need a different structure, but wildly overkill if all you wanted was to dim the lights differently. Because the base model already knows grammar, world facts, reasoning, and instruction-following from its original training, most tasks are a "behavior overlay," not a "rebuild." That is why PEFT is the default and full FT is the exception, not a matter of taste.
The common mistake is treating "full fine-tuning" as the serious, professional choice and PEFT as the budget shortcut — the intuition is exactly backwards. On nearly every practical axis PEFT is the better engineering decision: it trains under 1% of the parameters, fits on hardware you can actually rent, produces a small swappable artifact instead of a fresh multi-gigabyte model, and — as you'll see two sections down — is far less prone to catastrophic forgetting, the failure mode where a model gets so absorbed in your narrow task that it forgets how to do everything else it used to be good at. Full fine-tuning touches every weight, so it can overwrite that general competence; PEFT freezes the original weights, so the model literally cannot forget what it knew. The honest cases for going full are narrow and specific: you need a large behavior shift (a new language, a new modality, a genuinely different style of reasoning — not just a tone tweak), you have enough clean data to justify moving billions of parameters without overfitting, and you have the multi-GPU hardware to pay the memory bill the next section computes. Miss any one of those and PEFT is not the compromise — it's the correct answer. Read the comparison table below as a scorecard where every column but one favors PEFT; that lopsidedness is the point.
Start at the single box on top — the same pretrained base model — and follow the two blue arrows down into the two paths you can take with it.
- The left red column is full fine-tuning: it touches every weight, so it must hold weights, gradients, and optimizer state (the red inner bar → 100+ GB) and its output is a whole new model that you can't swap out.
- The right green column is PEFT: the base stays frozen and you train under 1% of the parameters (the adapter), so it fits on one GPU, resists catastrophic forgetting, and its output is a small swappable adapter, not a new model.
- The payoff is the bottom row of each column — compare "a whole new model" against "a small swappable adapter." That artifact difference is what decides your serving story later.
In short: both paths start from the same base, but full FT rewrites everything at huge memory cost while PEFT bolts on a cheap, swappable overlay — which is why PEFT is the default.
| Full fine-tune | PEFT (LoRA/QLoRA) | |
|---|---|---|
| Params trained | all (billions) | <1% |
| Memory | very high (weights+grads+optimizer) | low (QLoRA: one GPU) |
| Artifacts | a whole new model | a small adapter |
| Swappable | no | yes — many adapters, one base |
| Use when | big shift + data + hardware | almost always |
The memory math intermediate
The single most useful thing you can do before any fine-tuning project is a back-of-the-envelope memory calculation — because "will this even fit on the GPU I have?" is the question that decides everything else, and the answer surprises almost everyone the first time. People assume a 7-billion-parameter model needs "about 7B worth of memory" to train. It doesn't. It needs roughly six times that, and understanding why turns fine-tuning from guesswork into arithmetic you can do in your head.
Here's the intuition, and it's worth internalizing because it explains the entire existence of PEFT. Think of each parameter not as a single number but as a small entourage that travels together during training. The parameter's actual value (the weight) is just one member. To learn, the optimizer also has to remember, for that same parameter, which direction to nudge it (the gradient) and — because modern training uses the Adam optimizer — two running averages of past gradients (Adam's "moments," usually called m and v, which smooth the updates so training doesn't thrash). So one parameter you can see drags along three invisible companions you have to store. In mixed precision the rough tally is: weight ≈ 2 bytes, gradient ≈ 2 bytes, and Adam's two moments ≈ 4 + 4 bytes — about 12 bytes per parameter, of which the weight itself is a mere sixth. The gradient and optimizer state, not the weights, are what blow up your budget.
Multiply that out and the picture is stark: a 7B model at ~12 bytes/param is roughly 78 GB just for the training state, before you count activations or the batch — comfortably past any single card, which is precisely why full fine-tuning means renting a multi-GPU box. The common misconception to kill here is that quantization or a smaller batch will rescue a full fine-tune on one GPU; it won't, because the optimizer entourage scales with the number of trainable parameters, and in full FT that number is all of them. This is the exact hinge on which PEFT turns: if you freeze the base weights, they need no gradient and no optimizer state — they become read-only ballast you can even store in 4-bit (that's the "Q" in QLoRA). You then pay the full 12-byte entourage cost only for the tiny adapter — under 1% of the parameters — so the training state collapses from ~78 GB to a few GB that fits on one consumer card. The formula below is the whole argument made numeric; the takeaway is that PEFT wins on memory not by being clever about the same work, but by refusing to do most of the work at all.
Read the top row left to right: it breaks a single trainable parameter into the four things training must store for it.
- The four top boxes are the entourage of one parameter — the weight (2 bytes), its gradient (2 bytes), and Adam's two optimizer moments m and v (4 + 4 bytes) — summing to the "~12 bytes/param" label on the right.
- The red bar multiplies that by all 7B parameters: ~78 GB, which is why full fine-tuning spills past a single card onto multi-GPU.
- The arrow down is the PEFT trick: freeze the base so it needs no gradient or optimizer state and store it in 4-bit (the blue box, ~0.5 byte/param), then pay the full 12-byte cost only on the tiny adapter (the green box). The bottom green box is the payoff — the total collapses to ~3.5 GB, one GPU.
In short: the gradient and optimizer moments — not the weight — dominate memory, so freezing 99%+ of the model is what shrinks 78 GB down to a single-card 3.5 GB.
Merging and serving adapters advanced
You've trained an adapter — now comes the question that trips people up at deploy time: do you glue it onto the base model permanently, or keep it as a detachable part you snap on at request time? This isn't a trivial packaging detail; it quietly decides how many models you have to store, how fast each request is, and how painful it is to ship an update. Getting it wrong is the difference between a serving stack that scales to a dozen tuned behaviors on one GPU and one that needs a dozen separate model copies.
The analogy that makes it click is camera lenses. A LoRA adapter is a lens; the base model is the camera body. Merging (merge_and_unload()) is like welding one lens permanently onto the body — you get a single, purpose-built instrument that's dead simple to hand someone and has zero attach-time overhead, but it's now one camera that does one thing. Keeping the adapter separate is the normal interchangeable-lens setup: one body, a bag of lenses, and you click on whichever one this shot needs. Mechanically, merging works because a LoRA adapter is just a small low-rank update to the weights — mathematically it's a matrix you can add back into the original weight matrices, after which the adapter has no independent existence; the numbers have been folded in. Once merged, the model loads and runs exactly like any ordinary model, with no adapter code in the serving path at all.
So when do you weld versus keep-detachable? Merge when you're serving a single tuned behavior, or when latency is critical — a merged model has no per-request adapter-loading step, so it's both the simplest artifact to ship (one folder, drop it onto vLLM or TGI) and the fastest to run. The common mistake is to merge reflexively because it "feels cleaner," and then regret it the moment you have a second behavior to serve. Keep adapters separate when one base model must serve many behaviors — a support-tone adapter, a code-style adapter, a summarizer — because you can load the base once and hot-swap the right small adapter per request, which saves enormous memory (one 14 GB base instead of five) and, just as importantly, lets you retrain and redeploy one behavior without rebuilding or re-testing the others. That decoupling — update one lens without touching the camera or the other lenses — is often the deciding factor in a real platform, well beyond the raw memory savings. The lab below shows the merge step; the callout after it shows when to resist merging.
merge_and_unload() bakes the adapter into the base → one standalone model, no runtime load, fastest; keeping adapters separate lets one frozen base hot-swap the right small adapter per request and update one without touching the rest.
Start at the top box — your trained base plus adapter — and follow the two arrows to the two serving choices, labelled on each arrow.
- The left green path is merge:
merge_and_unload()folds the adapter into the weights, giving one standalone model with no runtime adapter load — the fastest, simplest artifact, ideal for a single tuned behavior. - The right amber path is hot-swap: one frozen base plus a shelf of small adapters (tone, code, summary). You load the right one per request, so one base serves many behaviors and you can update one adapter without touching the others.
- The deciding element is the bottom row of each side: "no runtime load · fastest · 1 behavior" versus "load per request · update one alone." Count your behaviors and check your latency budget to pick a side.
In short: merge when you serve one behavior and want speed and simplicity; keep adapters separate when one base must serve many and you value swap-and-update flexibility.
merge.pyfrom peft import AutoPeftModelForCausalLM
# Load base + adapter, merge into one set of weights, save a standalone model.
model = AutoPeftModelForCausalLM.from_pretrained("out/adapter", device_map="auto")
merged = model.merge_and_unload()
merged.save_pretrained("out/merged-model") # now serve it like any HF model (IC6/LM3)
After training you have a base model plus a separate adapter. This tiny script bakes the adapter into the base to produce one standalone model that's simplest to serve.
AutoPeftModelForCausalLM.from_pretrained("out/adapter", device_map="auto")loads the base model and your trained adapter together, in one step.device_map="auto"places it on whatever hardware you have.model.merge_and_unload()is the key line: it merges the adapter's changes into the base weights and unloads the separate adapter, leaving a single ordinary model.merged.save_pretrained("out/merged-model")writes that combined model to disk — from here it behaves like any normal Hugging Face model and can be served directly (FT6, IC6/LM3).
What the output means: A new folder out/merged-model appears containing a full standalone model — no adapter needed at serving time. It's bigger than the adapter alone but the simplest thing to deploy.
Try this: Merge when you serve one tuned behavior. If you have several adapters (support tone, code style, …), skip merging and keep them separate so one base model can hot-swap between them per request.
Exercise FT4.1 — Decide and defend
Context: The PEFT-vs-full decision is one you should be able to defend with numbers, and merging your adapter is the step that turns a tune into something servable in FT6.
Your task: For your task, decide PEFT vs full and justify it with the memory math and your data volume, then merge your FT3 adapter into a standalone model and confirm it loads and generates.
Requirements:
- State the PEFT-vs-full decision for your task
- Justify it with the per-param memory math and your data volume
- Merge the FT3 adapter into a standalone model
- Confirm the merged model loads and generates
- Note it's now ready to serve in FT6
💡 Hint: Reuse the merge step from the Professional rung; the defence is just the memory numbers you computed earlier applied to your own case.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: PEFT beats full fine-tuning on nearly every practical axis — params trained, memory, artifact size, and swappability — so full FT is the justified exception, not the default.
Your task: Encode the lesson's full-vs-PEFT comparison table and print the row you'd reach for by default.
Requirements:
- Rows compare params trained (all vs <1%), memory, artifact, and swappability
- Print the table in aligned form
- Declare PEFT (LoRA/QLoRA) as the default
- Make the case that PEFT wins on every practical axis
💡 Hint: Every column favours PEFT except raw capacity — that lopsidedness is the whole point of the row you pick.
Show solution
The comparison table as data, with the default called out:
rows = {
"params trained": ("all (billions)", "<1%"),
"memory": ("very high", "low (QLoRA: one GPU)"),
"artifact": ("whole new model","small adapter"),
"swappable": ("no", "yes — many adapters, one base"),
}
print(f"{'dimension':16}{'full':18}PEFT")
for k,(full,peft) in rows.items():
print(f"{k:16}{full:18}{peft}")
print("\nDEFAULT: PEFT (LoRA/QLoRA) — cheaper, faster, swappable")
PEFT wins on every practical axis for applied work; full fine-tuning is the exception you justify, not the default you assume.
Context: Full fine-tuning holds four things per parameter — the weight, its gradient, and Adam's two moments — and it's the last three, not the weight, that force multi-GPU.
Your task: Compute full-FT memory for a 7B model in mixed precision and show why it needs more than one card.
Requirements:
- Account weight 2 + grad 2 + Adam 4 + 4 ≈ 12 bytes/param
- Print totals for 1B, 7B, and 13B models
- Show 7B (≈78 GB) exceeds a single card
- Note weights are only ~1/6 of the cost; grads + optimizer dominate
- Frame this as the motivation for PEFT
💡 Hint: The weight itself is the cheap part — it's the gradient plus Adam's m and v that multiply the footprint by six.
Show solution
Sum the per-parameter memory the way the lesson describes it:
def full_ft_gb(params_b, w=2, grad=2, adam_m=4, adam_v=4):
bytes_per = w + grad + adam_m + adam_v # ~12 bytes/param, mixed precision
return params_b * bytes_per / 1024**3
for p in (1e9, 7e9, 13e9):
print(f"{p/1e9:.0f}B params -> {full_ft_gb(p):.0f} GB")
# 1B -> 11 GB 7B -> 78 GB 13B -> 145 GB
Weights are only a sixth of the cost — gradients and Adam's two moments dominate. A 7B model needs ~78 GB, well past one card, which is the entire motivation for PEFT.
Context: QLoRA fits one GPU because freezing + quantizing removes the weight, gradient, and optimizer cost for 99.6% of the model.
Your task: Model full FT (all params at 12 bytes) against QLoRA (4-bit frozen base + <1% trained at full optimizer cost) for a 7B model and print the ratio.
Requirements:
- Full =
params × 12 bytes - QLoRA = frozen base at 0.5 bytes/param + adapter at 12 bytes/param on ~0.4% of params
- Print both totals and the ratio (~20× lighter)
- Attribute the win to freezing + quantizing the base
💡 Hint: Only the tiny trained fraction still pays the full 12-byte optimizer tax; the frozen 99.6% drops to half a byte each.
Show solution
Put both footprints in one function:
def compare(params_b, trained_frac=0.004):
full = params_b * 12 / 1024**3
base_4bit = params_b * 0.5 / 1024**3 # frozen, quantized
adapter = params_b * trained_frac * 12 / 1024**3 # only the adapter pays optimizer
qlora = base_4bit + adapter
return round(full,1), round(qlora,1), round(full/qlora,1)
full, qlora, ratio = compare(7e9)
print(f"full FT : {full} GB") # 78.2
print(f"QLoRA : {qlora} GB") # ~3.5
print(f"QLoRA is ~{ratio}x lighter") # ~22x
Freezing + quantizing the base removes the weight/grad/optimizer cost for 99.6% of the model. QLoRA is roughly 20x lighter, which is the difference between a datacenter and a single consumer GPU.
Context: A trained adapter can be merged into the base (one standalone model, simplest and fastest to serve) or kept separate and loaded at runtime (one base serves many adapters).
Your task: Write a chooser that picks merged vs hot-swapped serving from the serving need.
Requirements:
- Choose HOT-SWAP for many adapters when latency isn't critical (one base, load per request)
- Choose MERGE for ops simplicity or latency-critical paths (one standalone model)
- Default a single-task deployment to MERGE
- Explain the tradeoff: one-model-simplicity vs many-adapters-on-one-base
💡 Hint: Count the adapters and check the latency bar: many + relaxed latency leans hot-swap, one-or-latency-critical leans merge.
Show solution
Decide the serving mode from how many tasks share the base:
def serve_mode(num_task_adapters, latency_critical, ops_simplicity):
if num_task_adapters >= 2 and not latency_critical:
return "HOT-SWAP adapters — one base serves many tasks, load per request"
if ops_simplicity or latency_critical:
return "MERGE — bake adapter into base -> one standalone model, simplest & fastest"
return "MERGE — default for a single-task deployment"
print(serve_mode(1, True, True)) # MERGE
print(serve_mode(5, False, False)) # HOT-SWAP
Merge when you serve one task and want the simplest, fastest artifact; keep adapters separate when one base must serve many tasks and you can pay a small per-request load cost to hot-swap them.
Context: Merging folds an adapter into the base weights, producing a single standalone model with no runtime adapter load — the simplest path onto vLLM/TGI.
Your task: Show the lesson's real merge step so a teammate can produce a standalone model, using only documented PEFT APIs and labelling the required libraries.
Requirements:
- Load base + adapter together with
AutoPeftModelForCausalLM.from_pretrained - Fold with
merge_and_unload() - Write one standalone model with
save_pretrained - Result has no runtime adapter load
- Note the libs:
peft transformers
💡 Hint: merge_and_unload() is the whole step — it bakes the adapter into the weights so what you save is just a normal model.
Show solution
The real merge — needs pip install peft transformers:
from peft import AutoPeftModelForCausalLM
# Load base + trained adapter together, fold the adapter into the weights, save.
model = AutoPeftModelForCausalLM.from_pretrained("out/adapter", device_map="auto")
merged = model.merge_and_unload() # adapter baked into base weights
merged.save_pretrained("out/merged-model") # now serve like any HF model (IC6)
merge_and_unload() collapses the adapter into the base so there is a single set of weights to serve — no runtime adapter loading, simplest path onto vLLM/TGI.
Context: PEFT is the default, but occasionally a project genuinely needs a full fine-tune; before spending the hardware, several conditions must all hold.
Your task: Write the checklist gate that must all be true before a full fine-tune is approved and print GO/NO-GO with the missing conditions named.
Requirements:
- Require a large behaviour shift, ample clean data, AND multi-GPU hardware
- Collect any unmet conditions into a list
- Return GO only when all are present
- Return NO-GO with the specific missing items otherwise
- Fall back to PEFT when the gate fails
💡 Hint: Treat it as an AND of three conditions — any single missing one flips the verdict to NO-GO and names itself.
Show solution
Gate full FT on all three conditions the lesson requires:
def full_ft_gate(big_behavior_shift, lots_of_quality_data, have_multigpu):
need = {"large behavior shift": big_behavior_shift,
"lots of quality data": lots_of_quality_data,
"multi-GPU hardware": have_multigpu}
missing = [k for k,v in need.items() if not v]
return ("GO — full FT justified" if not missing
else "NO-GO — PEFT instead; missing: " + ", ".join(missing))
print(full_ft_gate(True, True, True)) # GO
print(full_ft_gate(True, False, True)) # NO-GO ... missing: lots of quality data
Full fine-tuning is worth its cost only when a large behavior shift, ample clean data, and the hardware are all present. Miss any one and PEFT is the right call — the gate makes that explicit before the spend.
✓ Checkpoint — you can move on when you can…
- Compare full and PEFT on memory, cost, and fit.
- Do the full-fine-tune memory math.
- Merge an adapter and explain multi-adapter serving.
- Choose an approach for a stated constraint.
Knowledge check check yourself
Do the memory math: why does full fine-tuning of a 7B model need 100+ GB while QLoRA fits on one GPU?
Show answer
When should you merge a LoRA adapter into the base weights versus keep it separate, and why?