av / dives /Fine-tuning: Exercises
source about me

Bonus dive

Exercises: make the learning stick

Reading code teaches you less than predicting what it will do and then checking. This file turns each section of the README into a few quick active-recall prompts.

How to use it: work the section first, then come back. Commit to an answer before you run or reveal. The prediction is where the learning happens. Answers are hidden behind ▸ toggles.

Every section here runs offline and free on the default PROVIDER=mock including the simulated fine-tune. Nothing in these exercises costs a cent.


Section 2: When to fine-tune (offline)

Recall. Three tools change three different things about a model's answer. Match each (RAG/long context, fine-tuning, tools/agents) to what it changes.

▸ Answer
  • RAG / long context → changes what's in the context (knowledge).
  • Fine-tuning → changes how the model behaves by default (format, tone, a skill).
  • Tools / agents → changes what the model can do (capability).

Most "we need to fine-tune" instincts are actually a knowledge problem (RAG) or a prompt problem (few-shot) in disguise.

Predict, then run. In examples/01_when_to_finetune.py, a scenario needs the model to answer support questions using this week's pricing, with citations. Which tool does it land on, and why not fine-tuning?

▸ Answer

RAG, not fine-tuning. The pricing changes and must be cited, and that's knowledge, and baking it into weights would be stale the moment prices change (and can't cite a source). Fine-tuning is for behavior that stays the same, not facts that move.


Section 3: The dataset is the product (offline)

Recall. What exact format does a hosted fine-tune expect, and which turn does each training example end on?

▸ Answer

JSON Lines (JSONL): one conversation per line, each a {"messages": [...]} object. Each example ends on the assistant turn: that's the behavior the model is being taught to reproduce. The system+user turns are the setup; the assistant turn is the lesson.

Do. Run examples/02_dataset_format.py. Why does it split the data into train and validation instead of training on all of it?

▸ Answer

So you have data the model didn't train on to measure against (Section 7). If you evaluate on the training rows, a model that simply memorized them looks perfect while generalizing terribly; the validation/held-out split is the only thing that catches overfitting.


Section 4: Validate before you train (offline)

Predict, then run. Before running examples/03_validate_data.py: name two problems an offline check can catch before you spend a cent training.

▸ Answer

Any of: malformed/duplicate examples, class imbalance (e.g. 90% of rows are one category, so the model just learns to guess it), a missing assistant turn, a system prompt that drifts between rows, or a token/cost estimate that's bigger than you expected. The script runs all of these on the good set and then on a deliberately broken set so you watch each check fire.


Section 5: Run a fine-tune job

Recall. List the four steps of the hosted fine-tune lifecycle in order.

▸ Answer
  1. upload the training file (files.create)
  2. create a job from it (fine_tuning.jobs.create), which is what trains
  3. poll the job until done (fine_tuning.jobs.retrieve)
  4. use the returned model id

examples/04_run_finetune.py runs this on the mock (instant, free) with nearly identical code to the real OpenAI path.

Predict. You run examples/04_run_finetune.py with PROVIDER=openai but without --real. What happens, and why is it built that way?

▸ Answer

It does not start a paid job. The real path is opt-in behind --real plus a confirmation, precisely because a fine-tune costs real money and takes real time. You have to mean it. Without --real it runs the safe simulated path.


Section 6: Use the fine-tuned model

Recall. After a job succeeds, what's different about calling the fine-tuned model versus the base model?

▸ Answer

Almost nothing. It's a normal chat call; you just pass the new model id (e.g. ft:gpt-4o-mini:...) instead of the base id. There's no special "use a fine-tune" endpoint. The behavior change lives in the weights, not the API shape.

Do. Run examples/05_use_model.py and watch base vs. tuned on the same questions. What concretely changed, and what did not?

▸ Answer

Changed: the tuned model now answers in the rigid category: ... | reply: ... house format the training demonstrated, reliably, with no reminder in the prompt. Did not change: its underlying knowledge. Fine-tuning taught a format/behavior, not new facts.


Section 7: Did it actually help?

Recall. Why is a held-out set non-negotiable when measuring a fine-tune?

▸ Answer

Because a model can score perfectly on rows it trained on simply by memorizing them, that number is meaningless. Only data the training never saw (datasets/support_eval.jsonl) tells you whether the behavior generalizes. No held-out score, no claim.

Predict, then run. examples/06_did_it_help.py reports both accuracy and a pairwise win-rate. If the tuned model loses to the base model, what's the correct move?

▸ Answer

Don't ship it. A fine-tune that doesn't beat the baseline is a regression, no matter how much effort went in. Go back to the dataset (more, cleaner, better- balanced examples) and re-measure. "We already trained it" is the sunk-cost trap.


Section 8: Hyperparameters (offline)

Recall. What does the training loss falling while validation loss rises tell you, and which knob do you turn?

▸ Answer

Overfitting: the model is memorizing the training set instead of learning the general behavior. The first knob to turn down is n_epochs (fewer passes over the data); more/cleaner data helps too. examples/07_hyperparameters.py draws this exact divergence in the simulated loss curve.


Section 9: Distillation (offline)

Recall. In distillation, who is the teacher, who is the student, and why is the dataset so cheap to build?

▸ Answer

The teacher is a big, strong, expensive model that already does the task; the student is a small, cheap, fast model you fine-tune to imitate it. The dataset is cheap because the labels write themselves: you run the teacher over your inputs and use its outputs as the assistant turns. No hand-labeling.

Do. Run examples/08_distillation.py. After it builds the distilled set, what can you immediately do with the file, and which earlier section does that prove the point of?

▸ Answer

Feed it straight into the Section 5 tune step; it's a normal training file. The example validates it (Section 4) to prove exactly that: a distillation set is just a training set whose labels came from a model instead of a human.


Section 10: Open weights & LoRA (offline, conceptual)

Recall. What's the difference between full fine-tuning and LoRA/PEFT, and what stays identical between hosted and open-weight training?

▸ Answer

Full fine-tuning updates all the model's weights (heavy: lots of GPU memory). LoRA/PEFT freezes the base weights and trains a small number of added weights and it's far cheaper, almost as good for most tasks. What's identical either way: your dataset. The JSONL you built in Sections 3–4 is exactly what an open-weight trainer consumes too.


Section 11: Preference tuning (DPO/RLHF) (offline, conceptual)

Recall. SFT (every earlier section) trains on the one right answer. When would you reach for preference tuning instead, and how is its training data shaped differently?

▸ Answer

Reach for it when there's no single correct output but you can still say which of two is better: tone, conciseness, helpfulness, how firmly to refuse. Its data isn't {prompt → ideal answer}; it's {prompt, chosen, rejected} pairs. DPO nudges the model to make the chosen response more likely than the rejected one. The cheapest source of pairs is your own 👍/👎 traffic (the Production feedback flywheel).


Section 12: Reinforcement fine-tuning (RFT) (offline, conceptual)

Recall. SFT learns from demonstrations and preference tuning from comparisons. What does RFT learn from, and when does that beat writing labeled answers or pairs?

▸ Answer

RFT learns from a grader: a function that scores an attempt (unit tests pass, JSON validates, math answer matches, or a rubric judge rates it). There's no target answer and no pair. It beats labeled data when success is easy to verify but hard to demonstrate: many correct outputs exist, so you can check one but can't enumerate them (it's how reasoning models are trained). The risk unique to it: the model hacks a weak grader, passing the check while missing the intent, so the grader needs the same scrutiny as an eval, and you still gate on a held-out set.


Capstone: finetune_run.py

Do. Run python hands_on/finetune_run.py. It chains validate → tune → eval-gate → ship-if-wins. What is the gate actually deciding, and what does a non-zero exit mean?

▸ Answer

The gate decides whether the tuned model provably beat the base model on the held-out set (optionally above a --min-winrate you set). A non-zero exit means it did not clear the bar, the same signal a CI eval gate gives, so a bad fine-tune can't silently ship.

Stretch. Run it again with --train datasets/support_distilled.jsonl (build it first via Section 9). Does the distilled set clear the gate? Then write five new training rows of your own, re-run, and watch the numbers move. The first time you change the data and the held-out score changes, the "the dataset is the product" idea has clicked.


Where to take it next

Pick a tiny behavior you want a model to do the same way every time: a fixed JSON shape, a house tone, a one-line classification. Write 30-50 honest examples, validate them, tune on the mock, and gate. If it wins on held-out data, you've done the entire real workflow; only then is it worth spending money to run it for real.