BXUP Track progress & test yourself — free
Data Science & ML/AI · Module · ~16 min read
DATA SCIENCE & ML/AI · MODULE

Model Adaptation — Fine-Tuning That Earns Its Keep

This module installs the judgment fine-tuning roles actually screen for: read the eval to tell a form gap from a knowledge gap, walk the adaptation ladder, decontaminate training data against the eval set, and let a frozen harness decide whether the adapter ships.

  1. KARTHIK WANTS TO TUNE

    The incident-summary feature — which condenses a resolved support thread into two lines for the exec channel — keeps producing summaries in the wrong register, too chatty where terse wins. He has a Kaggle GPU and a weekend.

  2. NEHA'S FIRST QUESTION

    Not rank or learning rate: "what does the eval say the failure actually is?"

  3. THE HARNESS ANSWERS

    The M5 harness says the summaries are stylistically off, not factually wrong — a form failure, which is the one thing fine-tuning is genuinely for.

  4. CLIMB THE LADDER

    A clearer prompt spec closes half the gap, and the residue is exactly the kind of consistent-form problem weights fix better than context.

  5. CURATION

    Karthik curates eight hundred pairs from accepted summaries.

  6. PRIYA'S FLAG

    The question that saves them from a lie: "Did you check the training set against the eval set?" He hadn't.

  7. CONTAMINATION

    An embedding-similarity pass finds forty near-duplicates of eval items sitting in the training data — the exact contamination that would have reported a spectacular, meaningless lift and shipped a model no better than the base.

  8. THEN, HONESTLY

    He removes them, trains, and lets the frozen harness decide whether the adapter earns production.

That sequence — measure the gap, walk the ladder, decontaminate, prove the lift honestly — is the whole skill. Fine-tuning is the most over-prescribed intervention in applied LLM work.

It appears in job postings — LoRA and QLoRA are named verbatim in DS/ML listings from Bengaluru to Boston — precisely because employers have been burned by both failure modes: teams that fine-tune when a better prompt would have done, and teams that prompt-engineer for months around a problem a weekend of adaptation would have solved. The skill being screened is not "can you run a training loop." It's "can you tell when a training loop is justified, run it under real constraints, and prove the lift honestly."

You arrive at this module with an asset most fine-tuning tutorials assume away: the eval harness you built in M5–M6. Every claim in this module gets measured with it. That inverts the usual tutorial flow — instead of "train, then eyeball some generations," the discipline here is "freeze the eval set first, train second, and let the harness decide whether the adapter ships." Everything below is engineered to fit the free-tier GPU reality most learners have: a single 16GB T4 or P100 on Kaggle, roughly 30 GPU-hours a week.

Key Takeaway:

Fine-tuning is for form, not facts. It excels at teaching a model how to respond — style, format, domain vocabulary, task-specific behavior — and is the wrong tool for injecting knowledge that changes, which is retrieval's job. Most "we need to fine-tune" conversations end at a cheaper rung of the ladder, and the eval harness is how you find out which rung.

What You'll Learn

  • The adaptation ladderprompt → RAG → fine-tune → distill as a decision framework, with honest cost accounting at each rung
  • SFT dataset engineeringcurating a small, high-quality instruction set from logs and domain documents, with dedup and eval-set decontamination
  • LoRA mechanics and QLoRA's 4-bit trickrank, alpha and target modules, with a worked TRL config that trains a 7–8B model on one 16GB GPU
  • Preference tuning with DPOwhen pairwise preferences beat more SFT data
  • Honest before/after evaluationtask lift on your M5 harness plus a general-capability regression check
  • Fine-tuning the embedding model for domain retrievaland quantifying the recall lift against your M3 baseline

The Adaptation Ladder

Every adaptation decision walks the same four rungs, and each rung should be exhausted — measured, not vibed — before you climb to the next.

RungWhat it changesTypical cost to tryIteration speedWhen it wins
Prompt & context engineeringInstructions, examples, structureHours; API spend onlyMinutes per iterationBehavior gaps a clearer spec or few-shot examples can close — most gaps
RAGWhat the model knows at inferenceDays; retrieval infraHoursKnowledge that is large, changing, or needs provenance/citations
Fine-tuningWhat the model is — weightsDays–weeks; GPU time, dataset curation, eval cyclesDaysConsistent form: output style, domain dialect, structured tasks where few-shot plateaus, latency/cost pressure from long prompts
DistillationWhich model you runWeeks; teacher labeling + student trainingDaysA frontier model is provably good at the task and you need it at 1/20th the serving cost

The economics deserve honesty because the training run is the cheap part. A QLoRA run on Kaggle costs a few GPU-hours — effectively free. What isn't free: the 20–40 hours of dataset curation done properly, the eval cycles before and after, the second training run when the first one regresses something, and the permanent operational tax of owning a model artifact — you now re-validate on every base-model upgrade, maintain a training pipeline, and lose the frictionless model-swapping a prompt-based system enjoys. Price the whole lifecycle, not the trainer.train() call.

Three signals that fine-tuning is genuinely the right rung:

  1. Few-shot has plateaued on a measurable gap. Your M5 harness shows the task metric stuck below target across serious prompt iterations, and error analysis says the failures are form failures — wrong register, malformed structure, non-native use of domain vocabulary — not missing knowledge.
  2. The prompt is doing a fine-tune's job expensively. Ten exemplars and three pages of format rules in every request is adaptation-by-context; at volume, baking that behavior into weights cuts both latency and cost.
  3. You need an open-weight model to reach frontier behavior on one narrow task. Data residency, cost, or latency forces a small self-hosted model, and stock quality isn't there. This is the classic LoRA case, and it composes with M9's serving story.

And the anti-signal: "the model doesn't know about our products" is a RAG problem. Fine-tuning on Tuesday's product catalog produces a model that is confidently wrong by Friday, with no citation trail to catch it.

Exam Tip

Scenario questions on adaptation choice hinge on one distinction: knowledge gaps (changing facts, provenance needed) point to RAG; behavior gaps (style, format, domain dialect, few-shot plateau at volume) point to fine-tuning. An answer that reaches for fine-tuning to fix factual staleness is the planted wrong option.

SFT Dataset Engineering: Quality Over Quantity

Supervised fine-tuning is only as good as its dataset, and the practitioner consensus is blunt: a small, ruthlessly curated instruction set beats a large noisy one. For a narrow task on a 7–8B model, several hundred to a few thousand excellent pairs is the working range. Your job is curation, and your background in data quality is the edge here — this is dataset construction with a labeling budget, a task you've done before under other names.

The pipeline:

Source from reality. The best training pairs come from the same place your golden dataset came from in M5: real traces. Mine your logs for inputs paired with outputs that were accepted, corrected, or highly rated. Pair domain documents with the artifacts experts actually produced from them. Synthetic augmentation — using a frontier model to draft additional pairs — is legitimate only with human review; unreviewed synthetic data teaches the teacher's tics along with the task.

Deduplicate semantically. Exact-dedup first, then embed everything and cluster (you built this muscle in M2): near-duplicate clusters get collapsed to their best exemplar. Fifty paraphrases of the same billing question teach the model one thing fifty times and starve everything else.

Decontaminate against your eval set. This is the step that separates honest fine-tunes from accidental fraud. Embed both your training candidates and your M5 eval set; flag any training example above a high similarity threshold to any eval item, and remove it. A model trained on near-copies of its test set will show a spectacular, meaningless lift.

Format for TRL. The trainer accepts conversational data directly and applies the model's chat template for you:

# One line of train.jsonl — conversational prompt-completion format
{"prompt": [{"role": "user", "content": "Summarize this incident report for the exec channel:\n<report>...</report>"}],
 "completion": [{"role": "assistant", "content": "**Sev-2, resolved.** Payment webhooks delayed 41 min..."}]}

With prompt-completion data, TRL computes loss on the completion only by default — you're teaching the response, not the prompt. Hold out 10–15% as a validation split for the trainer (this is separate from your M5 eval set, which stays untouched in its vault until the final before/after comparison).

Warning

Eval-set decontamination is not optional housekeeping. Skipping it is the single most common way teams fool themselves — the harness reports a huge win, the model ships, and production quality is unchanged because the "win" was memorization of the test set.

LoRA and QLoRA on a 16GB GPU

Full fine-tuning of an 8B model is out of reach on free GPUs — the weights, gradients, and optimizer states of 8B parameters in mixed precision want well over 100GB. LoRA (Low-Rank Adaptation) sidesteps this: freeze the base weights, and learn a pair of small low-rank matrices A (d×r) and B (r×d) whose product is added to selected weight matrices. With rank r=16 on an 8B model you're training tens of millions of parameters instead of eight billion — under 1% — and the artifact you ship is an adapter of a few hundred megabytes.

The knobs that matter:

  • r (rank): capacity of the adaptation. 8–16 is the sweet spot for style/format tasks; 32–64 for harder behavioral shifts. Doubling r doubles adapter size and rarely doubles quality — treat it as a hyperparameter your validation loss arbitrates.
  • lora_alpha: scaling factor; the effective update is scaled by alpha/r. The common convention is alpha = 2r, which keeps effective scale constant as you sweep r.
  • target_modules: which weight matrices get adapters. Attention-only (q_proj, v_proj) is the minimal classic; modern practice targets all attention and MLP projections, which measurably helps at small extra cost.

QLoRA adds the trick that makes 7–8B training fit in 16GB: load the frozen base model quantized to 4-bit NF4 (a data type shaped for normally-distributed weights), with double quantization shaving the quantization constants themselves, while the LoRA adapters and their gradients stay in 16-bit. The arithmetic on a T4: an 8B base at 4-bit is roughly 5GB; adapters, gradients, and optimizer state add well under 1GB; the rest of the 16GB absorbs activations — which gradient checkpointing (on by default in TRL) keeps manageable at 1K-token sequences with a small batch.

The worked configuration, using TRL's modern API:

import torch
from datasets import load_dataset
from peft import LoraConfig
from transformers import BitsAndBytesConfig
from trl import SFTConfig, SFTTrainer

bnb = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.float16,   # T4 has no bfloat16 support
)

lora = LoraConfig(
    r=16,
    lora_alpha=32,                          # alpha = 2r convention
    lora_dropout=0.05,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    task_type="CAUSAL_LM",
)

args = SFTConfig(
    output_dir="qlora-incident-summarizer",
    max_length=1024,
    packing=True,                           # pack short examples into full sequences
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,          # effective batch size 16
    learning_rate=2e-4,                     # adapters train hot; ~10x full-FT rates
    num_train_epochs=2,
    fp16=True,                              # not bf16 — Turing GPU
    eval_strategy="steps",
    eval_steps=50,
    logging_steps=10,
)

trainer = SFTTrainer(
    model="Qwen/Qwen2.5-7B-Instruct",
    args=args,
    train_dataset=load_dataset("json", data_files="train.jsonl", split="train"),
    eval_dataset=load_dataset("json", data_files="val.jsonl", split="train"),
    quantization_config=bnb,
    peft_config=lora,
)
trainer.train()
trainer.save_model("adapter-v1")            # saves the adapter, not 7B of weights

Practical notes from the trenches: watch validation loss, not training loss — with a small dataset an 8B model can memorize your training set inside two epochs, and the validation curve turning upward is your stop signal. On Kaggle, save adapter checkpoints to the working directory regularly; sessions cap at nine hours and this run should finish in a fraction of that. If you hit CUDA OOM, the levers in order: shorter max_length, batch size 1 with more accumulation, then smaller r. Unsloth offers the same workflow with substantially faster kernels and lower memory if the stock stack is tight.

Pro Tip

The alpha = 2r convention exists so that when you sweep rank, you're testing capacity — not silently changing the update scale at the same time. Change one variable per run; GPU-hours on a free tier are a budget like any other.

Remember:

The adapter is the deliverable, and it composes with serving: vLLM can load LoRA adapters on top of a shared base model, so one deployed base can serve several task-specific adapters. That story completes in M9.

Preference Tuning: A DPO Introduction

SFT teaches the model what good looks like; it never shows what bad looks like. When your error analysis says outputs are "correct but worse" — verbose where terse wins, hedging where directness wins, plausible-but-shallow where rigor wins — you have a preference gap, and pairs of (chosen, rejected) responses encode it better than more gold examples can.

Direct Preference Optimization (DPO) trains on triples of {"prompt": ..., "chosen": ..., "rejected": ...} with a loss that pushes the policy's relative log-probability of chosen-over-rejected above a frozen reference model's, temperature-controlled by beta (lower = stronger preference pressure, higher = stay closer to the reference). Unlike RLHF it needs no reward model and no sampling loop — it's a supervised-shaped training run you can do with TRL on the same Kaggle GPU, typically as a short second stage on top of your SFT adapter:

from trl import DPOConfig, DPOTrainer

args = DPOConfig(
    output_dir="dpo-stage",
    beta=0.1,
    learning_rate=5e-6,        # far gentler than SFT
    num_train_epochs=1,
)
trainer = DPOTrainer(
    model=sft_model,           # your SFT-tuned PeftModel
    args=args,
    train_dataset=pref_ds,     # prompt / chosen / rejected
    processing_class=tokenizer,
)
trainer.train()

With a PEFT model, TRL derives the frozen reference by disabling the adapter — no second model copy in memory, which is what keeps this on-budget. Preference pairs come from the annotation ops you built in M6: reviewer corrections (original = rejected, corrected = chosen) are the cheapest honest source. A few hundred clean pairs is a real experiment. GRPO and other RL-based methods — now appearing by name in Indian job postings — sit beyond this module; DPO is the entry point that gets you conversant in the family.

Evaluating the Fine-Tune Honestly

This section is the module's reason for existing. The evaluation protocol, in order:

  1. Same harness, both models, blind. Run base and tuned models through the identical M5/M6 harness — same eval set, same judge with the same calibrated rubric, same code-based checks. If your judge is an LLM, it must not know which output came from which model; you audited position bias in M6, so randomize order per item as you did there.
  2. Report deltas with uncertainty. On a 100-item eval set, a two-point gain is noise. Bootstrap the metric difference or run a paired test across items — your statistical training is precisely the differentiator here, and "the tuned model is better, p≈0.6" is a result worth knowing before you ship.
  3. Slice before celebrating. Aggregate lift can hide slice regressions. Re-use your M5 failure taxonomy as slices: a summarizer that improved on incidents but broke on multi-service outages is not an improvement, it's a trade you need to make knowingly.
  4. Check general-capability regression. Narrow SFT can cause catastrophic forgetting — the model gets better at your task and quietly worse at everything adjacent: instruction following, reasoning, safety behavior, tasks the same endpoint serves tomorrow. Assemble a small out-of-domain suite and require near-parity: a handful of standard benchmark subsets via EleutherAI's lm-evaluation-harness (a few hundred items keeps it inside free-GPU time), plus 30–50 held-out general prompts scored by your judge. Define the regression budget before training — e.g., ship only if task metric improves by your target and every general probe stays within a small tolerance of base.
  5. Re-check contamination from the artifact side. Spot-check the tuned model on eval items: verbatim or near-verbatim reproduction of eval-set completions is contamination evidence, whatever your earlier filtering said.

The output of all this is the one-page honest eval report — base vs. tuned on task metrics with confidence intervals, slice table, regression suite results, and a ship/no-ship recommendation. That artifact, more than the adapter, is what distinguishes you in interviews: "benchmarking model performance" appears in fine-tuning job specs because most candidates can train and few can prove.

Note

The same protocol covers distillation, where the question becomes parity rather than lift: label a large input set with a frontier teacher — batch APIs cut labeling cost roughly in half — train a small student (an SLM via this module's SFT recipe, or even a classical GBM on extracted features for classification tasks), and require the student to hold a defined fraction of teacher quality on your harness at its fraction of the cost.

For teacher labeling at scale, schema-enforced outputs keep the training data clean:

import anthropic
from pydantic import BaseModel
from typing import Literal

class TriageLabel(BaseModel):
    category: Literal["billing", "bug", "how_to", "churn_risk", "other"]
    severity: Literal["low", "medium", "high"]

client = anthropic.Anthropic()
resp = client.messages.parse(
    model="claude-opus-4-8",
    max_tokens=256,
    messages=[{"role": "user", "content": f"<ticket>{ticket_text}</ticket>\nLabel this ticket."}],
    output_format=TriageLabel,
)
label = resp.parsed_output    # validated TriageLabel instance

Fine-Tuning the Embedding Model

The other model worth adapting is not the generator — it's the retriever. Off-the-shelf embedding models were trained on general text; your domain's vocabulary ("rollover" in pensions vs. mobile data vs. dog training) is exactly where they blur distinctions your users rely on. If your M3/M4 retrieval benchmark shows recall stuck despite hybrid search and reranking, embedding fine-tuning is the next lever, and it's dramatically cheaper than generator fine-tuning — base models are a few hundred million parameters and train on free GPUs in minutes to an hour.

The recipe is contrastive: mine (query, positive) pairs from your own telemetry — queries paired with the chunk users actually clicked or the passage a correct answer cited — and train with a loss that pulls queries toward their positives and pushes them from in-batch negatives:

from datasets import load_dataset
from sentence_transformers import (SentenceTransformer, SentenceTransformerTrainer,
                                   SentenceTransformerTrainingArguments)
from sentence_transformers.losses import MultipleNegativesRankingLoss

model = SentenceTransformer("BAAI/bge-base-en-v1.5")
train_ds = load_dataset("json", data_files="pairs.jsonl", split="train")  # columns: anchor, positive
loss = MultipleNegativesRankingLoss(model)   # in-batch negatives: bigger batch = harder task

trainer = SentenceTransformerTrainer(
    model=model,
    args=SentenceTransformerTrainingArguments(
        output_dir="domain-embedder", num_train_epochs=1,
        per_device_train_batch_size=64, learning_rate=2e-5, fp16=True,
    ),
    train_dataset=train_ds,
    loss=loss,
)
trainer.train()

Add mined hard negatives — retrieved-but-wrong chunks from your logs — as a third column when in-batch negatives get too easy. Evaluation reuses the labeled retrieval benchmark you built for embedding selection in M3: recall@k and MRR, off-the-shelf vs. tuned, on held-out queries. The same honesty rules apply — decontaminate training pairs against benchmark queries, and note the operational cost you're accepting: a custom embedder means re-embedding the corpus on every model update, and a frozen dependency you must now version and re-validate.

Key Takeaway:

A few points of recall@k from a fine-tuned embedder often moves end-to-end answer quality more than any generator intervention — the generator can't cite what retrieval never surfaced. Check the retriever before assuming the generator is the model that needs adapting.

Pro Tip

Your first 15 minutes Monday: before anyone says "let's fine-tune," write the one-line ladder verdict — is the gap a knowledge gap (changing facts, needs provenance → RAG) or a form gap (style, format, domain dialect, few-shot plateau at volume → fine-tune)? If it's knowledge, stop; fine-tuning on Tuesday's catalog is confidently wrong by Friday. That single distinction settles most "we need to fine-tune" conversations before a GPU is booked.

Check yourself: A teammate says "the model keeps getting our product names and specs wrong — let's fine-tune it on our catalog." Why is that the planted wrong answer, and what does the eval evidence need to show before fine-tuning is justified?

▸Show answeroptional deep dive — click to expand

"The model doesn't know about our products" is a knowledge gap, and fine-tuning is for form, not facts. Baking a product catalog into weights produces a model that is confidently wrong the moment the catalog changes — with no citation trail to catch it — which is retrieval's job (RAG), not adaptation's. Fine-tuning is justified only when the eval shows a form gap: your M5 harness reports the task metric stuck below target across serious prompt iterations, and error analysis says the failures are wrong register, malformed structure, or non-native domain dialect — not missing knowledge — or when a prompt is doing a fine-tune's job expensively (pages of format rules and exemplars in every request at volume). And even then you price the whole lifecycle (curation hours, eval cycles, the re-validation tax on every base-model upgrade), decontaminate training data against the eval set, and let the frozen harness prove the lift with uncertainty before shipping.

Exercises

  1. Adaptation decision memo. Take a real task from your org (or the provided support-bot scenario). Walk the four-rung ladder explicitly: what was tried at each rung, what the eval evidence showed, and full lifecycle cost of the rung you recommend — curation hours, GPU spend, eval cycles, and ongoing ownership tax. One page; the memo is the deliverable, whichever rung wins.
  2. Curate and decontaminate. Build an 800-pair SFT dataset from a raw log dump: semantic dedup, quality filter, TRL conversational format, and an embedding-similarity decontamination pass against a provided 60-item eval set. Deliverable: the dataset plus a curation log stating how many candidates each step removed and why.
  3. QLoRA on Kaggle. Run the worked config against your dataset on a Kaggle T4. Produce the training/validation loss curves, note peak VRAM (torch.cuda.max_memory_allocated()), and save the adapter. Then rerun with r=8: compare validation loss, adapter size, and wall-clock, and state which you'd ship.
  4. The honest eval report. Run base vs. tuned through your M5/M6 harness blind, with bootstrap confidence intervals and slice breakdowns, plus a general-capability probe (an lm-evaluation-harness subset or 30 held-out general prompts). Deliverable: the one-page ship/no-ship report.
  5. Embedding lift. Mine 500+ (query, positive) pairs from retrieval logs, fine-tune bge-base, and measure recall@5 and MRR against your M3 benchmark, off-the-shelf vs. tuned, on decontaminated held-out queries. State whether the lift justifies owning a custom embedder.

Your playbooks & labs

This module anchors Lab L11 — QLoRA fine-tune on a free GPU, the track's proof that you can run the full adaptation lifecycle under real constraints: curate ~800 instruction pairs with documented dedup and decontamination, QLoRA-tune a 3–8B model inside Kaggle's 16GB budget, and evaluate base-vs-tuned on a held-out harness including a general-capability regression check. The artifact — adapter weights plus the honest eval report — is exactly the evidence "LoRA/QLoRA + benchmarking" job postings are probing for. The playbooks provide the adaptation-decision worksheet (ladder + lifecycle cost model), the SFT curation checklist with the decontamination procedure, the QLoRA memory-budget calculator for 16GB GPUs, and the before/after evaluation protocol template. Do this at work this week → pb-27 Choose the Adaptation Path: Prompt, RAG, Fine-Tune, or Distill (Neha's ladder verdict with honest cost accounting), pb-28 Engineer an SFT Dataset: 1,000 Curated Pairs Beat 100,000 Noisy Ones (curation with the decontamination pass Priya demanded), and pb-29 LoRA and QLoRA Fine-Tuning on a Free 16GB GPU (the worked Kaggle-budget training run). Every path is free-tier: Kaggle/Colab GPUs, open-weight 7–8B bases, judge calls on cheap-tier models within the track's standard per-lab API budget.

Further Reading

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in