Skip to content
GKgkml.dev
← LLM mechanisms

Part 2 · Large language models · module 4 of 4 · 14 min

Instruction tuning and RLHF

How a text predictor becomes an assistant — chat templates, instruction datasets, and reinforcement learning from human feedback.

The essence

A base model completes text; it has no notion of being asked. Instruction tuning is fine-tuning on (instruction, response) pairs formatted with role markers, which teaches the model that text after 'assistant' should answer the text after 'user'. RLHF then optimises for human preference rather than for imitation, which is what produces a model that is helpful and declines rather than merely plausible.

Why it matters

This is the step that separates GPT-2 from something you can talk to, and it involves almost no new capability — it redirects what pretraining already built. It is also where alignment is implemented in practice, which makes it the concrete link between the modelling parts of this curriculum and the safety parts.

Topics covered (6)
  1. 01What instruction tuning is
  2. 02Datasets used for instruction tuning
  3. 03Training a chatbot with system, user and assistant roles
  4. 04Instruction tuning GPT-2
  5. 05Instruction tuning a larger GPT-2
  6. 06Reinforcement learning from human feedback (RLHF)

Why a base model is not an assistant

Ask a base model 'What is the capital of France?' and a very reasonable continuation is another question, because in its training data questions cluster with questions. It is not being unhelpful; it is doing exactly what it was trained to do, which is continue text.

Instruction tuning changes the distribution it continues into. Train on thousands of examples where a marked instruction is followed by a marked helpful response, and the model learns that after the assistant marker, the appropriate continuation is an answer.

The chat format, and why the loss is masked
# The template is just text with role markers. Any consistent scheme works,
# but it must be identical at training and inference or behaviour collapses.
TEMPLATE = (
    "<|system|>\n{system}<|end|>\n"
    "<|user|>\n{user}<|end|>\n"
    "<|assistant|>\n{assistant}<|end|>"
)

def build_example(tok, system, user, assistant, max_len=512):
    prompt = TEMPLATE.format(system=system, user=user, assistant="")
    full   = TEMPLATE.format(system=system, user=user, assistant=assistant)

    prompt_ids = tok(prompt)["input_ids"]
    full_ids   = tok(full, truncation=True, max_length=max_len)["input_ids"]

    # -100 is CrossEntropyLoss's ignore_index: those positions contribute
    # nothing. Masking the prompt means the model is trained to produce the
    # response, not to reproduce the question it was asked.
    labels = [-100] * len(prompt_ids) + full_ids[len(prompt_ids):]
    return {"input_ids": full_ids, "labels": labels[: len(full_ids)]}

# Modern tokenizers ship their own template — use it rather than inventing one:
# tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

Note: Add the role markers as special tokens so they are single ids and cannot be produced accidentally by ordinary text — that is part of what makes prompt injection harder.

Instruction datasets, and what each is for

DatasetOriginCharacter
AlpacaGenerated by a stronger model52k pairs; easy to start with, inherits the teacher's flaws
Dolly 15kWritten by employeesHuman-authored, permissively licensed, small
OpenAssistantCrowd-sourced conversationsMulti-turn, multilingual, quality varies
FLAN collectionExisting NLP tasks, re-templatedBroad task coverage, terse responses
ShareGPT-style logsReal conversationsRealistic; licensing and privacy need care
Instruction tuning a GPT-2
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from torch.utils.data import DataLoader

tok = AutoTokenizer.from_pretrained("gpt2-large")
tok.add_special_tokens({"additional_special_tokens":
                        ["<|system|>", "<|user|>", "<|assistant|>", "<|end|>"]})
tok.pad_token = tok.eos_token

model = AutoModelForCausalLM.from_pretrained("gpt2-large")
model.resize_token_embeddings(len(tok))    # required after adding tokens

def collate(batch):
    n = max(len(b["input_ids"]) for b in batch)
    pad = lambda seq, fill: seq + [fill] * (n - len(seq))
    return {
        "input_ids": torch.tensor([pad(b["input_ids"], tok.pad_token_id) for b in batch]),
        "labels":    torch.tensor([pad(b["labels"], -100) for b in batch]),
        "attention_mask": torch.tensor(
            [[1] * len(b["input_ids"]) + [0] * (n - len(b["input_ids"])) for b in batch]),
    }

loader = DataLoader(examples, batch_size=4, shuffle=True, collate_fn=collate)
opt = torch.optim.AdamW(model.parameters(), lr=2e-5)

model.train()
for batch in loader:
    loss = model(**batch).loss
    opt.zero_grad(set_to_none=True)
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    opt.step()

Note: Padding labels with -100 rather than the pad id is essential; otherwise the model is trained to emit padding.

Why instruction tuning is not enough

Instruction tuning imitates the responses in its dataset. That means it inherits their ceiling — the model learns to produce text that looks like a good answer, which is not the same as producing a good answer.

It also gives no way to express preferences that are easy to judge but hard to write. Nobody can author the ideal response to every ambiguous or harmful request, but people can reliably say which of two responses is better. That asymmetry is what reinforcement learning from human feedback exploits.

RLHF, in three stages

  1. Supervised fine-tuning: instruction-tune a base model on demonstrations to get a reasonable starting policy.
  2. Reward model: sample several responses per prompt, have humans rank them, and train a model to score a response so that preferred responses score higher. The loss is a pairwise comparison, so no absolute quality scale is ever needed.
  3. Policy optimisation: fine-tune the language model to maximise the reward model's score, with a KL-divergence penalty against the supervised model to stop it drifting into degenerate text that games the reward.
The reward model's loss, and the RL objective
import torch, torch.nn.functional as F

# Stage 2. Bradley-Terry pairwise loss: the chosen response should outscore
# the rejected one. Only the difference matters, so the scale is arbitrary.
def reward_loss(reward_model, prompt, chosen, rejected):
    r_c = reward_model(prompt, chosen)      # scalar per pair
    r_r = reward_model(prompt, rejected)
    return -F.logsigmoid(r_c - r_r).mean()

# Stage 3. Maximise reward, penalised for straying from the reference policy.
# Without the KL term the policy collapses onto whatever the reward model
# over-scores — reward hacking, and it happens quickly.
def ppo_objective(reward, logprobs_policy, logprobs_ref, beta=0.1):
    kl = (logprobs_policy - logprobs_ref).mean()
    return reward.mean() - beta * kl

# DPO removes the separate reward model entirely, optimising the preference
# data directly. Same data, one training stage, far simpler to run — which is
# why most open work now uses it.
def dpo_loss(pi_chosen, pi_rejected, ref_chosen, ref_rejected, beta=0.1):
    margin = beta * ((pi_chosen - ref_chosen) - (pi_rejected - ref_rejected))
    return -F.logsigmoid(margin).mean()

What this buys, and what it does not

After these stages the model answers rather than continues, follows a system prompt, keeps a consistent persona, and declines some requests. That is a large behavioural change from a small amount of training relative to pretraining.

What has not changed is what the model knows. Instruction tuning and RLHF shape the interface, not the substrate. Every capability and every gap in the base model is still there, which is exactly why the interpretability parts of this curriculum study base models — the mechanisms are the same, with less behavioural varnish on top.

Worth remembering

  • Instruction tuning is ordinary fine-tuning on formatted pairs; the format is what carries the behaviour.
  • Mask the loss so only the response tokens are trained on, not the prompt.
  • RLHF trains a reward model on human comparisons, then optimises the policy against it with a KL penalty.