Fine-tuning vs RAG vs Prompting
"Should we fine-tune?" is the most common — and most misunderstood — question in applied LLMs. The answer is usually no, not yet. This part gives you the decision framework: what each technique actually changes, when each wins, how LoRA makes fine-tuning cheap, and how to prepare data — so you pick deliberately instead of reaching for the most expensive option first.
Learning objectives
- State the fine-tune vs RAG vs prompt decision clearly.
- Recognize the cases where each wins.
- Estimate the cost/effort of fine-tuning honestly.
- Make and defend the build decision as a lead.
code/xt4-finetuning/ in the course, with a README on how to run it. Read here, run there — no need to copy-paste.1 · The most misunderstood question essential
Picture the meeting. A stakeholder watches the demo, sees the model get one answer slightly wrong, and says the words that have burned more LLM budgets than any other: "Can we just fine-tune it on our data?" It sounds decisive and technical, so it usually wins the room. Three weeks and a five-figure GPU bill later, the team has a tuned model that is worse at the actual problem — because the problem was never a behavior problem in the first place. Almost everyone reaches for fine-tuning too early, and almost everyone regrets it.
Here's the analogy that untangles it. Think of a talented new hire on their first day. There are three completely different ways to make them more useful, and they are not interchangeable. You can give clearer instructions ("format every report like this, always cite your source") — that's prompting, and it's instant and free. You can hand them the company binder to look things up while they work ("here are our current prices, policies, and docs") — that's RAG, and it's how you supply facts they couldn't possibly have memorized. Or you can send them to a months-long training course to permanently rewire how they operate — that's fine-tuning, expensive and slow, and worth it only for a durable change in how they work, never for information that changes weekly.
The precise version: prompting and RAG change what the model sees at request time (its context), while fine-tuning changes the model's weights — its ingrained behavior. That distinction is the entire lesson. Fine-tuning is genuinely good at teaching a consistent style, a strict output format, or a narrow skill. It is genuinely bad at teaching facts — bake yesterday's price list into the weights and it's stale tomorrow, un-updatable without retraining, and prone to being confidently recited even when wrong. The common mistake, stated plainly: treating "the model doesn't know X" as a fine-tuning problem. Missing knowledge is a retrieval problem. The framework below is a staircase you climb one rung at a time, stopping the moment the output is good enough — and most real problems are solved on the bottom two rungs, long before you ever touch a GPU.
"Should we fine-tune?" is usually answered no, not yet. Prompting and RAG solve most problems faster and cheaper. Fine-tuning changes behavior (style, format, a narrow skill) — it's a poor way to add knowledge (that's RAG). Match the tool to the problem.
This is the whole lesson in one row of boxes: when your LLM output isn't good enough, you climb these rungs left to right, cheapest first, and you stop as soon as the output is good. Most real problems never reach the last box.
- Need better output (leftmost) is the starting point — you have a task and the model's answers aren't right yet. The label cheapest is a reminder: always begin at the cheap end.
- Prompt engineering is the first thing to try: rewrite the instructions, add examples, set the format. It's free, instant to change, and fixes a surprising amount. The arrow means "if that wasn't enough, move right."
- + RAG (knowledge) is next: when the model is missing facts (your docs, product details, fresh data), you fetch the right text and paste it into the prompt. RAG adds knowledge, not new behavior.
- Fine-tune (behavior) is the last, most expensive rung — labeled last resort. You only reach it when you need a consistent style, a strict format, or a narrow skill that prompting and RAG couldn't deliver. Note it changes behavior, not knowledge.
In short: Read the boxes as a staircase you climb only when forced. The single most common mistake in applied LLMs is jumping straight to the far-right box (fine-tuning) before trying the two cheap ones on its left.
2 · What each technique changes essential
Here's a failure you can feel. A team wanted their support bot to answer in the company's warm, on-brand voice and quote the current refund policy. They fine-tuned on a few hundred old support transcripts, hoping to get both at once. The result was uncanny: the bot sounded perfectly on-brand — and cheerfully quoted the refund window from last year, because that was the policy baked into the transcripts it trained on. They'd used one tool to solve two problems that live in different places, and got the voice right while getting the facts wrong. The fix wasn't a better tune; it was realizing style and knowledge are separate levers.
The clean way to hold this in your head is to ask where each technique makes its change. Prompting and RAG both operate in the context window — the text the model reads fresh on every single request. Prompting puts instructions there ("be concise, answer in JSON"); RAG puts knowledge there (retrieved passages from your docs). Because both live in the prompt, they're editable in seconds, they update the instant your data changes, and nothing is permanent. Fine-tuning is the odd one out: it changes the weights themselves, so the change is baked in and travels with the model on every call without costing you any context tokens.
That "where" tells you almost everything about the trade-offs, and it's why the three map to non-overlapping jobs:
- Prompting → the default. Instructions in context. Free, instant to change, no infrastructure. Try this first for every problem; it fixes a startling amount on its own.
- RAG → for facts and freshness. Knowledge in context, fetched per query. This is the only one of the three that keeps up with data that changes, and the only one that can cite its source — which is why it, not fine-tuning, is the answer to "the model doesn't know our stuff."
- Fine-tuning → for durable behavior. A change to the model's weights: a reliable voice, a strict format it won't drift from, or a small specialized model that's cheaper and faster than prompting a big one. It's the expensive rung, and it changes behavior — not what the model knows.
The common mistake this table exists to prevent: assuming the three compete. They don't — a serious production system usually uses all three at once (a fine-tuned small model, fed retrieved facts, steered by a careful prompt). The skill is matching each job to the lever that actually moves it, and never paying for fine-tuning to do a job prompting or RAG already does for free.
Read it as two containers side by side: the left box is the context window (rewritten every request), the right box is the model's weights (changed only by training).
- The left blue box holds the two cheap levers. The green band is prompting (instructions you type in) and the amber band is RAG (facts fetched into the prompt). Both live in the text the model reads.
- The right purple box is fine-tuning — a change to the weights themselves. It sits outside the context window because it costs no prompt tokens and rides along on every call.
- The arrows and labels underneath are the payoff: the left side is "editable in seconds," the right side needs a full re-train. That single difference — where the change happens — is what drives every trade-off in the table below.
In short: Prompting and RAG are edits to what the model reads; fine-tuning is an edit to what the model is — instant-and-temporary versus baked-in-and-slow.
| Technique | Changes | Best for |
|---|---|---|
| Prompting | instructions in-context | most tasks; instant iteration |
| RAG | knowledge in-context | facts, docs, freshness, citations |
| Fine-tuning | the model's behavior | consistent style/format, narrow skill, latency |
3 · Intermediate — the four cases fine-tuning wins intermediate
By now the drumbeat has been "don't tune." So it's fair to ask the honest question: is fine-tuning ever the right answer, or is this whole chapter just talking you out of it? It is absolutely the right answer — but only in a short, specific list of situations. The trap isn't fine-tuning itself; it's fine-tuning for a problem that isn't on the list. So instead of a vague "it depends," here are the four concrete cases where tuning genuinely earns its cost, each with the tell-tale sign you're actually in it.
- Consistent style or voice. You've written the prompt three different ways and the model still drifts — too formal here, too chatty there, never quite your brand. When the desired tone is subtle and pervasive enough that no instruction pins it down reliably, tuning on examples of the voice bakes it in. The tell: you can show good outputs far more easily than you can describe the rule.
- Strict output format. The model must emit valid JSON (or a specific schema) on every single call, and even a 1% rate of malformed output breaks a downstream system. Prompting gets you to "usually"; a small tune plus a few hundred examples gets you to "essentially always." The tell: format failures are causing real incidents, not just annoyance.
- A small model replacing a big one. A large prompted model does the job, but it's expensive and slow at your volume. Tuning a small model on that narrow task can match the big one's quality on that task while cutting per-call cost and latency dramatically. The tell: you're at high, sustained volume where the per-request savings actually add up to real money.
- A narrow, specialized task. A single well-defined job (a specific classification, a domain-specific transformation) where a focused model beats a generalist. The tell: the task is stable, repetitive, and you can gather many clean labeled examples of it.
The unifying pattern — and the mistake it guards against: every winning case is about behavior (how the model acts) or economics (cost/latency at scale), never about knowledge (what it knows). The moment you catch yourself justifying a tune with "so it knows our...", stop — that's the RAG problem wearing a fine-tuning costume. And notice all four also assume you've already hit a wall with prompting and RAG. If none of these four cases describes your situation, the disciplined answer is simply: don't tune. The little decision helper below turns exactly this reasoning into code you can run.
Tune when: a consistent style/voice is needed, a strict output format must be reliable, a small tuned model can replace a big prompted one (latency/cost), or a narrow task where a specialized model beats a general one. If none apply, don't tune.
Scan the four green boxes first — those are the situations where a tune earns its cost — then look right to the red box, which is the one reason that is never a tuning job.
- The four green boxes are the winning cases: a consistent voice, a strict format, a small model replacing a big one, and a narrow specialized task. Each carries its tell-tale sign in the smaller line beneath it.
- The grey band underneath names what all four share — they are about behavior (how the model acts) or economics (cost and latency), never about knowledge.
- The red box on the right, with the arrow pointing into it, is the trap: "so it knows our facts" feels like a tuning reason but is really a RAG problem. If none of the four green cases fits your situation, the answer is simply don't tune.
In short: Match your task to one of the four green boxes or don't tune — and if your justification is about facts, that red box says it belongs to RAG.
decision.pydef should_finetune(needs_facts, needs_style, needs_strict_format,
latency_critical, prompt_rag_tried):
if not prompt_rag_tried:
return "NO — try prompting + RAG first"
if needs_facts and not (needs_style or needs_strict_format):
return "NO — that's a RAG problem, not fine-tuning"
if needs_style or needs_strict_format or latency_critical:
return "MAYBE — a real fine-tuning case; measure vs prompt+RAG first"
return "NO — no clear behavior change needed"
print(should_finetune(True, False, False, False, True)) # facts -> RAG
print(should_finetune(False, True, True, True, True)) # style+format+latency -> maybe
print(should_finetune(False, False, False, False, False)) # haven't tried basics
NO — that's a RAG problem, not fine-tuning
MAYBE — a real fine-tuning case; measure vs prompt+RAG first
NO — try prompting + RAG first
This little function turns the whole "should we fine-tune?" debate into a set of plain if-checks. You feed it yes/no answers about your task, and it tells you NO, MAYBE, or points you at the cheaper tool instead. Read the checks top to bottom — the first one that matches wins and returns.
- The function takes five true/false inputs describing your task: does it need facts, a consistent style, a strict format, low latency, and — crucially — whether you've already tried prompting + RAG.
- The first check is the gatekeeper:
if not prompt_rag_triedmeans "you haven't even tried the cheap options yet" — so it immediately returns NO, try prompting + RAG first. This alone stops most premature fine-tuning. - The second check catches the classic mistake: if you only need facts (and not style or format), that's a knowledge problem — the comment
# facts -> RAGflags it. Fine-tuning is the wrong tool; use RAG. - The third check is the one real green light: if you genuinely need style, a strict format, or speed, it returns MAYBE — a real fine-tuning case, and even then tells you to measure against prompt+RAG first. If nothing matched, the final line returns NO — no clear behavior change needed.
What the output means: The three print calls run three example tasks. You see NO — that's a RAG problem… (facts only), then MAYBE — a real fine-tuning case… (style + format + latency), then NO — try prompting + RAG first (basics not tried). Notice the order of outputs matches the order the checks fire, not the order of the calls.
Try this: Change the first call's last argument from True to False (pretend you skipped prompt+RAG) and predict the answer before running — the gatekeeper check should flip it to NO, try prompting + RAG first, no matter what the other flags say.
4 · Advanced — the true cost of fine-tuning advanced
Here's the estimate that quietly kills projects. Someone prices fine-tuning by the one number that's easy to look up — the GPU rental — sees "$12 of compute for a training run," and greenlights it. What they didn't price is the part that doesn't show up on any invoice: the days of human effort spent building the dataset. So the project starts, the "quick" tune stalls at 200 hand-labeled examples out of the 1,000 it actually needs, everyone loses momentum, and it's abandoned half-done. The compute was never the cost. The data was the cost, and nobody budgeted for it.
The analogy: fine-tuning is like publishing a book. The printing (the GPU run) is fast and cheap and the only visible line item. The writing and editing — the manuscript itself — is where all the real months go. With a tune, that manuscript is your dataset: hundreds or thousands of clean, correctly-labeled, deduplicated examples of exactly the behavior you want. Each one is largely handcrafted or carefully curated, and quality matters more than quantity because the model will faithfully imitate whatever you feed it, including your mistakes.
Precisely, a fine-tune has four cost buckets, and they are wildly lopsided:
- Dataset creation — the dominant cost. Sourcing, labeling, cleaning, and deduplicating examples. Measured in days-to-weeks of skilled human time, and it grows with the number of examples you need.
- Training compute. The GPU hours for the actual run. Real, but often the smallest bucket — hours, not days, especially with LoRA (which you'll meet in the fine-tuning track).
- Evaluation. Building an honest eval set and measuring the tuned model against the prompt+RAG baseline. Skipping this means shipping on faith — the professional section is entirely about why you can't.
- Maintenance — the recurring cost. The one everybody forgets. When the base model is upgraded, your requirements shift, or the data drifts, you re-tune. A fine-tune is a liability you keep paying for, not a one-time purchase.
The mistake this section prevents: pricing only the compute. The lab below makes the lopsidedness undeniable — plug in realistic numbers and watch data prep come out around 70% of the total effort, with the GPU run a rounding error beside it. Internalize that ratio and a lot of premature-tuning decisions answer themselves: if you can't commit to producing quality data, you can't fine-tune well, so the honest move is prompt + RAG.
Fine-tuning isn't just a training run. The real cost: dataset creation (the dominant effort), training compute, evaluation, and ongoing maintenance (re-tune when the base model or requirements change). Estimate it before committing.
Read it as a bar chart, top to bottom: each row is one cost bucket, and the length of the bar is how much of the total effort it eats.
- The top red bar — dataset creation — is by far the longest: sourcing, labeling, cleaning, and deduplicating examples, measured in days-to-weeks. It is the ~71% the cost lab computes.
- The amber evaluation bar and the short blue GPU bar come next. Notice the training compute — the one number that's easy to look up and the one people price the project by — is the smallest bar.
- The purple maintenance bar is the one everyone forgets: it's a recurring cost, redrawn every time the base model upgrades or the data drifts, so a tune is a liability you keep paying for.
In short: The bar you can invoice for (GPU) is a sliver; the bar that decides whether you should tune at all (data) is the whole chart.
cost.pydef finetune_effort(examples_needed, hours_per_100_examples, gpu_hours, eval_hours):
data_hours = examples_needed / 100 * hours_per_100_examples
total = data_hours + gpu_hours + eval_hours
return {"data_prep_hours": round(data_hours), "total_hours": round(total),
"data_is_pct": round(100*data_hours/total)}
print(finetune_effort(examples_needed=1000, hours_per_100_examples=3,
gpu_hours=4, eval_hours=8))
{'data_prep_hours': 30, 'total_hours': 42, 'data_is_pct': 71}
Fine-tuning feels like "just run a training command," but the real work is building the dataset. This function adds up where your hours actually go so you can see that truth in numbers before you commit.
- It takes four honest estimates: how many training examples you need, how many hours it takes to hand-build 100 examples, the GPU hours for the training run, and the eval hours to check the result.
data_hoursis the big one:examples_needed / 100 * hours_per_100_examplesscales your per-100 labeling rate up to the full dataset. This is usually the dominant cost.totalsums data + GPU + eval. Thenround(100*data_hours/total)computes what percent of the whole effort is just data prep — the number that makes the point.- It returns a small dictionary of three figures so the caller can print them clearly, rather than one buried number.
What the output means: For 1000 examples at 3 hours per 100, the result is {'data_prep_hours': 30, 'total_hours': 42, 'data_is_pct': 71}. The headline: 71% of the effort is building the dataset — the training run (4 GPU hours) is the small part. That's why the warning below says data prep is most of the work.
Try this: Bump examples_needed to 3000 and re-run. Watch data_is_pct climb even higher — more scale means data prep dominates even more, which is exactly why teams underestimate fine-tuning.
5 · Professional — measure tuned vs baseline professional
You spent the two weeks. You built the dataset, ran the tune, and the model "feels" better in a handful of hand-picked examples. The temptation now is enormous: ship it, because you've paid for it and it looks good. This is exactly where careful teams and sloppy teams part ways. "Feels better" on a few cherry-picked prompts is not evidence — it's the sunk-cost fallacy dressed up as a demo. The professional discipline is brutally simple: a tune ships only if it beats the cheaper prompt+RAG baseline on a real, held-out eval set, by a margin big enough to be worth its ongoing maintenance.
Think of it like a drug trial. Nobody approves a new medication because a few patients said they felt great; you run it against a control group and demand a statistically real improvement, because the new drug also carries new costs and risks. Your fine-tune is the new drug; prompt+RAG is the control group; your eval set is the trial. And there are two numbers to watch, not one — because a tune can help your target task while quietly hurting everything else:
- The target metric must go up. On the specific task you tuned for, accuracy (or whatever you measure) has to clear the baseline by a real margin — not by a point or two of noise, but by enough that the win survives the eval set's natural variation.
- General capability must not regress. Fine-tuning narrowly can cause "catastrophic forgetting" — the model gets better at your task and worse at everything adjacent. So you also check the tune didn't degrade the broader behaviors you still rely on.
The mistake this guards against is shipping on faith and discovering the regression in production, where it's expensive and embarrassing. Gate the decision on measured evidence before the model reaches users. If the tuned model doesn't clear the margin, the correct — and senior — outcome is to keep the cheaper baseline and treat the tune as a useful experiment that answered "no." The eval-gated comparison from Ch 5 is the decision-maker here; the tune's good feeling in a demo is not.
A fine-tune is only justified if it beats prompt+RAG on your evals — target metric up, general capability not regressed. Never ship a tune on faith; the eval-gated comparison (Ch 5) is the decision-maker.
Follow it left to right like a drug trial: two candidates on the left feed into one eval set in the middle, which produces two pass/fail checks on the right.
- The two left boxes are the contenders — prompt+RAG (the control group) and the tuned model (the new drug). Both are pushed through the same held-out eval so the comparison is fair.
- The middle box is that eval set, and the two arrows out of it are the two numbers you must watch, not one.
- The two green boxes are the gates: the target metric must rise past a real margin, and general capability must not regress (catastrophic forgetting). Only when both are true does the amber box below say ship; if either fails, you keep the cheaper baseline.
In short: Treat the tune as a drug on trial against a control — it ships only on measured evidence that it wins on the target task without breaking everything else.
6 · Tech-lead — the build decision tech-lead
Everything so far has been analysis. This is the part where someone has to own the answer — and put their name on it in front of a room that already wants to fine-tune. That's the lead's job, and it's harder than the engineering, because the pressure runs the wrong way. A stakeholder has heard "fine-tuning" is what serious AI teams do; a keen engineer wants the resume line; a vendor is happy to sell you the GPUs. The path of least resistance is to say yes. The lead's real value is being the person who can say "no, not yet" and defend it with the framework, not with a gut feeling.
The analogy is any senior technical decision: choosing to not build the microservice, not adopt the shiny database, not rewrite the legacy system. Juniors demonstrate competence by building things; seniors demonstrate judgment by knowing which things not to build, and being able to explain why so clearly that the room agrees. Talking a team out of a premature fine-tune — and being right — is precisely that senior move.
Concretely, owning this call means running a short, defensible checklist rather than a vibe:
- Run the decision framework out loud. Which of the four winning cases does this actually hit? If it's really a knowledge gap, name it as a RAG problem and redirect the energy there.
- Estimate the true cost honestly — data prep included, not just compute — so the room sees the real price (the days of labeling, not the $12 of GPU) before committing.
- Require a measured win. Make "beats prompt+RAG on the eval set by a real margin, with no capability regression" a precondition for shipping, not an afterthought.
- Account for maintenance. Whoever approves the tune is signing up to re-tune it when the base model or requirements change. Name that recurring cost up front.
The mistake a lead exists to prevent is the organization spending weeks and real money to fine-tune its way backwards — a staler, costlier, harder-to-maintain system than the prompt+RAG setup it replaced. Most of the time, disciplined analysis lands on "prompt + RAG," and saying so — clearly, early, with the numbers to back it — is not the timid choice. It's the expert one.
A lead owns the fine-tune vs RAG vs prompt call: run the decision framework, estimate cost honestly, require a measured win over the cheaper alternatives, and account for maintenance. Most of the time the disciplined answer is "prompt + RAG" — and saying no to a premature fine-tune is a senior move.
Follow the flow left to right: the pressure to fine-tune enters on the left, passes through the lead's four checks in the middle, and lands on one of two outcomes on the right.
- The amber box on the left is the room's default push — "let's fine-tune." That's the input the lead has to filter, not accept.
- The four blue boxes are the checklist run out loud: name the winning case (or redirect to RAG), price the true cost including data prep, demand a measured win, and own the recurring maintenance.
- The two right boxes are the outcomes. The green prompt+RAG box is where most calls exit — the thick, common path; the purple fine-tune box is the rare exception you only reach when all four gates say yes.
In short: The lead's value is running the four gates so the loud "let's fine-tune" usually and correctly resolves to the cheaper prompt+RAG.
Exercise XT4.1 — Make the call
Context: The whole lesson is a decision, not a training run. Applying the decision and effort helpers to a real task you're tempted to tune is where it becomes concrete.
Your task: Take a real task you're tempted to fine-tune, run it through the decision helper, estimate the effort, and state what you'd try with prompt engineering plus RAG first.
Requirements:
- Describe a genuine task and which of the four cases (facts/style/format/volume) it hits
- Run the decision helper and record its recommendation
- Estimate the effort with the cost helper for your example count
- Write down a prompt + RAG plan you'd try before tuning
- If none of the four cases apply, state plainly why you would not tune it — that's the lesson
💡 Hint: "No, not yet" is a valid and common outcome — the exercise is as much about ruling tuning out as choosing it.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Fine-tuning is usually the wrong first move. It teaches behavior — style, format, lower per-call cost at scale — but it does not add fresh facts; that's what RAG is for.
Your task: Reproduce the lesson's decision helper should_finetune(...) that returns a recommendation based on whether you need facts, style, strict format, or high volume.
Requirements:
- If the need is fresh facts, recommend RAG, not fine-tuning
- If the need is style, strict format, or high volume, recommend trying prompt engineering first and tuning only if it plateaus
- Otherwise recommend prompt engineering as likely sufficient
- Stdlib only; take the four needs as boolean parameters
- Show a facts case returning "use RAG" and a style/format/volume case returning "maybe"
💡 Hint: Order the checks so the facts case short-circuits first — it's the one hard "no" regardless of the other flags.
Show solution
Runnable, stdlib only:
def should_finetune(needs_facts, needs_style, needs_strict_format, high_volume):
if needs_facts:
return "No -- use RAG. Fine-tuning teaches behavior, not fresh facts."
if needs_style or needs_strict_format or high_volume:
return "Maybe -- try prompt engineering first; fine-tune if it plateaus."
return "No -- prompt engineering is likely enough."
print(should_finetune(needs_facts=True, needs_style=False,
needs_strict_format=False, high_volume=False))
print(should_finetune(needs_facts=False, needs_style=True,
needs_strict_format=True, high_volume=True))
The usual answer is "no, not yet": prompt engineering is free and instant; reach for tuning only when it plateaus on style/format/cost — never to inject knowledge.
Context: Teams underestimate tuning because they picture the GPU run, not the labeling. In reality data prep dominates the effort — the training run is the small part.
Your task: Reproduce finetune_effort(...) and report the total hours and the percentage that is data prep for 1000 examples at 3 hours per 100.
Requirements:
- Compute data-prep hours as examples/100 × hours-per-100
- Total = data-prep + GPU hours + eval hours
- Return data-prep hours, total hours, and data-prep as a percentage of total
- Stdlib only; round the reported numbers
- Show that for the given inputs data prep is the large majority of the effort
💡 Hint: The interesting output is the percentage — it makes the point that labeling, not the GPU run, is where the time goes.
Show solution
Runnable, stdlib only:
def finetune_effort(examples_needed, hours_per_100_examples, gpu_hours, eval_hours):
data_hours = examples_needed / 100 * hours_per_100_examples
total = data_hours + gpu_hours + eval_hours
return {"data_prep_hours": round(data_hours),
"total_hours": round(total),
"data_is_pct": round(data_hours / total * 100)}
print(finetune_effort(examples_needed=1000, hours_per_100_examples=3,
gpu_hours=4, eval_hours=8))
# {'data_prep_hours': 30, 'total_hours': 42, 'data_is_pct': 71}
71% of the effort is building the dataset; the GPU run is the small part. Teams underestimate tuning because they picture the training run, not the labeling.
Context: LoRA is cheap because it freezes the base weights and trains small low-rank adapters instead of the full matrix. Counting the trainable parameters shows just how small that is.
Your task: Write lora_params(d_out, d_in, r) comparing full fine-tuning (d_out×d_in) against LoRA (r×(d_out+d_in)) and compute the ratio for a 4096×4096 layer at r=8.
Requirements:
- Full fine-tuning parameter count is
d_out × d_in - LoRA parameter count is
r × (d_out + d_in)(the two adapter matrices) - Return full count, LoRA count, and LoRA as a percentage of full
- Stdlib only; format the counts readably
- Show LoRA is well under 1% of the layer's parameters at r=8
- Connect the tiny count to fitting on modest GPUs and producing swappable adapter files
💡 Hint: The ratio collapses to roughly r×(d_out+d_in) over d_out×d_in — a fraction of a percent for a square layer at small rank.
Show solution
Runnable, stdlib only:
def lora_params(d_out, d_in, r):
full = d_out * d_in
lora = r * (d_out + d_in) # A: (r x d_in) + B: (d_out x r)
return full, lora, round(lora / full * 100, 3)
full, lora, pct = lora_params(4096, 4096, r=8)
print(f"full={full:,} lora={lora:,} lora is {pct}% of full")
# full=16,777,216 lora=65,536 lora is 0.391% of full
LoRA trains <0.5% of the parameters of that layer, which is why it fits on modest GPUs and produces tiny, swappable adapter files instead of a full model copy.
Context: Never ship a tuned model on vibes. A tuned model must beat the prompted baseline by a real margin to justify its ongoing retraining, maintenance, and serving cost.
Your task: Given a labeled eval set and predictions from a baseline and a tuned model, compute each model's accuracy and only recommend the tuned model if it clears the baseline by a set margin. Runnable offline.
Requirements:
- Write an
accuracy(preds, gold)over paired predictions and labels - Compute accuracy for both the baseline and the tuned predictions
- Compare against a required margin (e.g. 5 points) before recommending the tuned model
- Stdlib only; print both accuracies and the decision
- Recommend keeping the cheaper baseline when the gain doesn't clear the margin
💡 Hint: Gate the recommendation on tuned - baseline >= MARGIN, not on the tuned model merely being higher.
Show solution
Runnable, stdlib only:
def accuracy(preds, gold):
return sum(p == g for p, g in zip(preds, gold)) / len(gold)
gold = ["refund", "refund", "cancel", "cancel", "other", "refund"]
baseline = ["refund", "cancel", "cancel", "cancel", "refund", "refund"]
tuned = ["refund", "refund", "cancel", "cancel", "other", "refund"]
a_base = accuracy(baseline, gold) # 0.666...
a_tuned = accuracy(tuned, gold) # 1.0
MARGIN = 0.05
print(round(a_base, 3), round(a_tuned, 3))
if a_tuned - a_base >= MARGIN:
print("ship tuned: +{:.0%} over baseline".format(a_tuned - a_base))
else:
print("keep baseline: gain not worth the maintenance cost")
A tuned model must clear a real margin (here 5 points) to justify its ongoing maintenance, retraining, and serving cost — otherwise the cheaper prompted baseline wins.
Context: Most of fine-tuning is the data. Trainers consume clean chat-formatted JSONL, and duplicate or empty examples quietly leak into eval and inflate accuracy.
Your task: Write a validator to_sft_records(pairs) that turns raw (prompt, completion) pairs into chat-style JSONL records, rejecting empty examples and dropping duplicates. Runnable offline with stdlib json.
Requirements:
- Raise on any pair with an empty prompt or completion
- Deduplicate on the (stripped prompt, stripped completion) key
- Emit each record as a chat message list with a user and an assistant turn
- Strip whitespace on the stored content
- Show a duplicate being dropped (three pairs in, two records out) and print one record as a JSONL line
- Explain why dedupe and non-empty checks matter for honest eval metrics
💡 Hint: Track a seen set of the stripped pair so the second copy is skipped before it becomes a record.
Show solution
Runnable, stdlib only:
import json
def to_sft_records(pairs):
records, seen = [], set()
for prompt, completion in pairs:
if not prompt.strip() or not completion.strip():
raise ValueError("empty prompt or completion")
key = (prompt.strip(), completion.strip())
if key in seen: # dedupe: leaked dupes inflate metrics
continue
seen.add(key)
records.append({"messages": [
{"role": "user", "content": prompt.strip()},
{"role": "assistant", "content": completion.strip()},
]})
return records
pairs = [("Classify: love it", "positive"),
("Classify: love it", "positive"), # duplicate -> dropped
("Classify: hate it", "negative")]
recs = to_sft_records(pairs)
print(len(recs)) # 2
print(json.dumps(recs[0])) # one JSONL line
Deduping and non-empty checks matter: duplicate examples leak into eval and inflate accuracy; clean chat-formatted JSONL is exactly what SFT/LoRA trainers consume.
Context: Once you've decided tuning is worth it and prepared the data, the actual LoRA run is a standard PEFT skeleton. It needs a GPU and the libraries; the decision, data, and eval rungs above are the runnable offline part.
Your task: Write the training setup with transformers + peft to LoRA-tune a base model. Needs a GPU and the libraries (transformers, peft, datasets).
Requirements:
- Load a base causal-LM model and tokenizer
- Build a
LoraConfig(rank, alpha, dropout, target modules, causal task type) and wrap the model withget_peft_model - Print trainable parameters to confirm well under 1% — matching the LoRA math
- Load the prepared JSONL dataset and train with an SFT trainer and standard
TrainingArguments - Save only the adapter (megabytes), not a full model copy
- Frame it as: decide with the offline rungs first, then spend the GPU hours
💡 Hint: The saved artifact is just the small adapter, which is exactly why LoRA is cheap to store, swap, and serve.
Show solution
Needs a GPU and libraries (transformers, peft,
trl/datasets). Standard PEFT-LoRA skeleton — no invented APIs:
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
from datasets import load_dataset
base = "meta-llama/Llama-3.2-1B"
tok = AutoTokenizer.from_pretrained(base)
model = AutoModelForCausalLM.from_pretrained(base, device_map="auto")
lora = LoraConfig(r=8, lora_alpha=16, lora_dropout=0.05,
target_modules=["q_proj", "v_proj"], task_type="CAUSAL_LM")
model = get_peft_model(model, lora)
model.print_trainable_parameters() # <1% trainable -- matches the LoRA math above
ds = load_dataset("json", data_files="train.jsonl", split="train")
trainer = SFTTrainer(
model=model, tokenizer=tok, train_dataset=ds,
args=TrainingArguments(output_dir="out", num_train_epochs=1,
per_device_train_batch_size=4, learning_rate=2e-4),
)
trainer.train()
model.save_pretrained("adapter") # tiny adapter, not a full model copy
The saved artifact is just the LoRA adapter (megabytes), which is why it's cheap to store, swap, and serve. Decide with the offline rungs above; only then spend the GPU hours here.
✓ Checkpoint — you can move on when you can…
- State the fine-tune vs RAG vs prompt decision.
- Recognize the four cases tuning wins.
- Estimate fine-tuning's true cost (data dominates).
- Require a measured win; own the build decision.
Knowledge check check yourself
The lesson's decision framework climbs prompting → RAG → fine-tuning. Why is fine-tuning usually the wrong answer when you need to add knowledge?
Show answer
The cost lab shows data prep as ~71% of fine-tuning effort. Why does the lesson stress this, and what does it imply about when you can fine-tune well?