Fine-Tuning with TRL and PEFT
By the end of this lesson you will be able to read a supervised fine-tuning script line by line and say what every setting does, choose the handful that actually decide whether the run works, watch the two loss curves and stop at the right checkpoint, recognise overfitting from the logs rather than from the output, and say what is in the directory the run leaves behind.
Part 11 shipped the reference recipe as train-sft.py and ran it on a six-hundred-million-parameter
model to prove the toolchain worked. This lesson is that script explained, and the lab in this part
ships its own self-contained version so that the two can be read side by side.
Five objects
Section titled “Five objects”A run is a model, a tokeniser, a dataset, a training configuration and an adapter configuration, handed to one trainer.
From a JSONL file to an adapter directory
- load_dataset("json", data_files=…)The train and validation JSONL files from the dataset lesson, in conversational prompt-completion shape.
- AutoTokenizer.from_pretrained(model)The chat template travels with the tokeniser. If it is None, the model is a base checkpoint and you must supply one.
- SFTConfig(...)Learning rate, epochs, batch, sequence length, evaluation strategy, precision, where to write.
- LoraConfig(...)Rank, alpha, dropout, target modules. Passed as peft_config so the trainer wraps the model for you.
- SFTTrainer(...).train()Tokenises, applies the chat template, masks the prompt, runs the loop, evaluates each epoch, keeps the best checkpoint.
- save_model(output_dir)An adapter directory of a few tens of megabytes: adapter_config.json, adapter_model.safetensors, the tokeniser files.
The trainer takes the model as a string and loads it itself, which is why model_init_kwargs exists.
One property of TRL 1.12.0 · verified 2026-09-08 is worth knowing before it surprises you: the documentation
notes that when the model is passed as a string, “If dtype is not specified in
args.model_init_kwargs, it defaults to float32”, which differs from from_pretrained, where
since Transformers v5 the dtype is inferred from the model config. A run that is unexpectedly slow
and four times larger in memory than the arithmetic predicted is usually this.
The settings that matter
Section titled “The settings that matter”TRL’s SFTConfig has more than a hundred fields. Six decide whether your run works, and the
documentation lists four defaults that differ from the underlying TrainingArguments, which is
worth reading once so you know what you are inheriting: logging_steps defaults to 10 rather than
500, gradient_checkpointing to True rather than False, bf16 to True if fp16 is not set,
and learning_rate to 2e-5 rather than 5e-5.
Learning rate is the one that ruins runs. The 2e-5 default is a full-fine-tuning rate. Adapters
want more, and TRL says so directly: “When training adapters, you typically use a higher learning
rate (≈1e‑4) since only new parameters are being learned.” Start at 1e-4 for a LoRA run, and treat a
loss that will not move as a reason to raise it and a loss that spikes or goes to nan as a reason
to lower it. This is the same rule Part 1 gave for every training run in the course.
Epochs, not steps, for a dataset of a few hundred examples. Two to four is the useful range. One epoch often under-trains a format change; six epochs on three hundred examples is memorisation with extra steps.
Batch size and gradient accumulation together set the effective batch:
per_device_train_batch_size × gradient_accumulation_steps. Only the first costs memory, because
accumulation sums gradients across several forward passes before one optimiser step. On the 12 GB
tier a batch of 1 or 2 with accumulation of 8 or 4 gives an effective batch of 8 for the memory of
one or two, and it is the first knob to reach for when a run runs out of memory.
max_length defaults to 1024 and truncates from the start of the sequence by default
(truncation_mode="keep_start"). Set it to what your examples actually need. Part 11’s warning
applies: activation memory scales with the sequence length of the batch being processed, so a run
sized against your average example fails against your longest one.
warmup_steps ramps the learning rate from zero over the first few steps, which stops the first
gradient from moving a freshly initialised adapter too far. A handful of steps is enough on a small
dataset. lr_scheduler_type="cosine" then decays it over the run.
packing groups several examples into one fixed-length sequence to reduce padding. It defaults
to False, and for a few hundred short examples it is worth leaving off: the throughput gain is
small and it makes the relationship between an example and a step harder to reason about while you
are still learning to read the curves.
Evaluation loss and when to stop
Section titled “Evaluation loss and when to stop”The training loss tells you the model is fitting your examples. It cannot tell you whether that is learning or memorisation, which is Part 1’s distinction and the whole reason for a validation split.
Four settings turn the validation split into a stopping rule:
Fragment — not complete on its own
config = SFTConfig( eval_strategy="epoch", # compute the evaluation loss after every epoch save_strategy="epoch", # and write a checkpoint at the same points load_best_model_at_end=True, # finish holding the best one, not the last one metric_for_best_model="eval_loss", greater_is_better=False,)eval_strategy and save_strategy must agree, because ending the run on the best-scoring
checkpoint requires a checkpoint to exist at the point that score was seen. With those five lines the run ends holding
the parameters from the epoch with the lowest evaluation loss, whatever happened afterwards.
Early stopping adds the other half: stopping the run once the evaluation loss has failed to improve for a given number of evaluations. Transformers supplies a callback for it, and the lab’s script attaches it with a patience of two evaluations. On a short run its main value is not the time saved but the record: a run that stopped at epoch two out of five is telling you something about your data size that a run which used all five does not.
Overfitting, and how it shows up
Section titled “Overfitting, and how it shows up”Overfitting is the training loss continuing to fall while the evaluation loss turns and rises. On a few hundred examples it arrives early, and there are three symptoms worth recognising by name.
The curves diverge. The clearest signal and the one the stopping rule already handles. If the best epoch is consistently the first, your dataset is too small for the number of epochs, or the learning rate is too high, or both.
The model reproduces training examples verbatim. Give it an input close to one in the training set and it returns that example’s answer, including details that belonged to the original input. This is the symptom that reaches users, and it is worth checking by hand on three examples at the end of every run.
Everything becomes the training task. Ask it something unrelated and it answers in the shape of your examples. This is catastrophic forgetting and overfitting arriving together, and it is why the first lesson insisted on scoring every category of the Part 10 task set rather than the total.
The fixes, in the order to try them: fewer epochs, which costs nothing; more data, if you have it; a lower learning rate; a lower rank, since capacity you cannot fill is capacity available for memorising; and a few per cent of general instruction examples mixed in, which the forgetting study in the first lesson found helps.
Reading the logs
Section titled “Reading the logs”TRL documents exactly what it records, and four of the fields do real diagnostic work.
| Field | What it tells you |
|---|---|
loss |
“The average cross-entropy loss computed over non-masked tokens in the current logging interval.” The headline number, and the one that can fall while the model gets worse. |
eval_loss |
The same quantity on the validation split. The one that decides when to stop. |
mean_token_accuracy |
“The proportion of non-masked tokens for which the model’s top-1 prediction matches the ground truth token.” Rising towards 1 on a format task is the model learning your fixed strings; it saturates long before the interesting content does. |
entropy |
“The average entropy of the model’s predicted token distribution over non-masked tokens.” Falling entropy means a more confident model. Falling very fast is a model collapsing onto one answer shape. |
grad_norm |
“The L2 norm of the gradients, computed before gradient clipping.” Spikes here precede loss spikes, so it is the earliest warning that the learning rate is too high. |
num_tokens |
Total tokens processed. The honest measure of how much training actually happened, and the one to record rather than epochs when comparing runs with different sequence lengths. |
What the run leaves on disk
Section titled “What the run leaves on disk”With a peft_config, save_model() writes an adapter rather than a model. The directory contains
adapter_config.json, which records the base model’s name, the rank, the alpha, the dropout and the
target modules; adapter_model.safetensors, which is the two small matrices per adapted layer and
is typically tens of megabytes; and the tokeniser files if you saved them, which you should.
That is a useful artefact in itself. It is small enough to keep every one of them, it names the base
model it belongs to, and it can be attached to that base at serving time without being merged, which
llama.cpp supports through --lora and vLLM through --enable-lora. The export lesson covers both
routes.
Save adapters when you are iterating, when you want to serve several fine-tunes of one base without holding several copies of the base, or when you want to be able to fall back to the base model instantly.
Merge when you are done, when you want a single artefact to convert to GGUF or MLX, when you
want to quantise the result, or when the serving stack does not support adapters. PEFT’s guide gives
the mechanism and one warning that costs people an hour: merge_and_unload() “is not an in-place
operation”, so its return value must be assigned.
Recording the run
Section titled “Recording the run”Part 11 defined the run-log format and every training script in Level 3 appends one JSON line per
run to labbook.md: the run identifier and date, the base model, the dataset path with its SHA-256
checksum and example counts, every hyperparameter, the seed, the hardware, the package versions, the
losses and any scores. This part’s scripts ship a self-contained copy of that helper so they run
without Part 11’s files on the path, and write the same fields.
The checksum is the field people leave out and then need. A month later, the question is never “what learning rate did I use” — that is in the script — but “was this the run before or after I fixed the dataset”, and only the checksum answers it.
Prove the training configuration on a tiny subset
Section titled “Prove the training configuration on a tiny subset”Before a full run, inspect one batch and train briefly on a handful of examples. The purpose is to establish that the intended parameters receive gradients and that the loss can respond to repeated data. Deliberately fitting this tiny subset is a diagnostic; it is not evidence of generalisation.
Verify adapter target names and the trainable-parameter count after model construction. Record the chat template and loss mask. If loss is flat, check whether labels are masked out, gradients are finite and the optimiser includes the adapter parameters before increasing the learning rate. If loss falls but evaluation fails, inspect the data and serving contract.
Use a new output directory for each meaningful configuration change. Save resolved settings rather than relying only on defaults that can change between versions. At evaluation time, match the fine-tune’s exact base lineage and template. A correct training loop can produce an apparently broken model when the serving prompt differs from training. Separating these checks prevents repeated retraining from becoming the response to a deployment-format error.
A supervised fine-tuning run is five objects: a dataset, a tokeniser carrying the chat template, an
SFTConfig, a LoraConfig and the trainer that puts them together. Six settings decide the
outcome: a learning rate around 1e-4 for adapters rather than the 2e-5 default, two to four epochs,
a small batch with gradient accumulation for the effective batch you want, a max_length sized to
your data, a short warm-up with cosine decay, and packing left off while you are learning to read
the curves. Evaluation on the validation split after every epoch, with load_best_model_at_end and
early stopping, ends the run holding the best checkpoint rather than the last. Overfitting shows as
diverging curves, verbatim reproduction of training examples, and every question being answered in
the training task’s shape. The logs give loss, evaluation loss, token accuracy, entropy, gradient
norm and token count, and gradient-norm spikes are the earliest warning of a learning rate that is
too high. The run leaves an adapter directory of tens of megabytes that names its base model; keep
the tokeniser with it, and record the dataset’s checksum in the run log.
Check your understanding
Sources for this lesson
5 verified · checked 2026-09-09
- 01TRL — SFT Trainer§ Quick start; Customization; Logged metrics; SFTConfig parametershuggingface.co/docs/trl/sft_trainer2026-09-09
- 02TRL — Dataset formats and types§ Prompt-completion; conversationalhuggingface.co/docs/trl/dataset_formats2026-09-09
- 03PEFT — LoRA developer guide§ Rank and alpha; Target modules; Merging adaptershuggingface.co/docs/peft/developer_guides/lora2026-09-09
- 04PEFT — LoRA conceptual guide§ Merginghuggingface.co/docs/peft/main/en/conceptual_guides/lora2026-09-09
- 05Transformers — Chat templates§ Model traininghuggingface.co/docs/transformers/main/en/chat_templating2026-09-09
Every technical claim on this page was checked against the official documentation of the tool, vendor or model publisher on the date shown, at the version pinned for the course. Where the course disagrees with folklore, the source is how you can tell which one to trust.