Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
The previous lesson used Reinforcement Learning from Verifiable Rewards (RLVR): train a policy against checked outcomes where a verifier exists. Distillation starts after that useful teacher exists. At 09:00, a 7B student language model, a smaller large language model (LLM), passes staging's familiar restore-window cases. At 09:15, it answers a new expired-admin case with 7 days; trusted policy says not allowed. The student is fast enough for production, but copying that regression isn't a deployment win.
Knowledge distillation trains a smaller student to imitate useful behavior from a larger or otherwise more capable teacher. In this example, a 70B-class teacher answers "How many days can a disabled production API key stay restorable?" well enough to pass your checks, but you can't afford that model on every request. A 7B-class student is viable only if it keeps enough of that behavior under a latency and serving-cost budget.
That transfer signal depends on what the teacher exposes. In white-box settings, the student can match softened token probabilities or internal features. In black-box settings, it fine-tunes on selected teacher-written answers, solution traces, or synthetic corpora. The student doesn't inherit the teacher's judgment. It copies the behavior you can measure, then earns release through the same restore-window checks, latency target, and cost budget.
Before computing a token-level loss, compare teacher and student on the same token vocabulary, prompt formatting, and response positions. Two arrays can share a shape while assigning column 17 to different tokens. The loss still runs, but it trains the student toward the wrong words.
Why soft labels teach more than hard labels
The staging regression points at a weakness in hard labels. Hinton et al. popularized a practical formulation in 2015: train a student to mimic a teacher's behavior rather than relying only on ground-truth labels. A teacher's full probability distribution can carry more information than a one-hot label.[1]
Start with one response token. The trusted answer is 7d, so a hard label puts mass 1.00 on 7d and zero on every alternative. Predict what gets lost: the teacher's ranking among plausible wrong answers. Its output also contains dark knowledge. Here 3d is the strongest near-miss, with smaller mass on 30d and never. That extra signal is useful only if the teacher's ranking is itself useful. It isn't proof that the student learned a general access-policy rule.

Those four bars are one simplified prediction. For a causal LLM, logit distillation applies the same idea at each predicted token position. Temperature divides the logits before softmax. is the model's ordinary distribution. flattens it so second-place tokens aren't rounded into zero. Push far enough and the ranking disappears.
Predict the top gap before running this check: it should shrink as rises, then nearly vanish. The code uses the same logits as the figure, , and prints the four probabilities plus at , , and .
1import math
2
3logits = {"7d": 4.0, "3d": 2.0, "30d": 1.0, "never": 0.7}
4
5def softened_probabilities(temperature: float) -> dict[str, float]:
6 scaled = {token: math.exp(logit / temperature) for token, logit in logits.items()}
7 total = sum(scaled.values())
8 return {token: value / total for token, value in scaled.items()}
9
10for temperature in (1.0, 4.0, 40.0):
11 probs = softened_probabilities(temperature)
12 rounded = {token: round(probability, 3) for token, probability in probs.items()}
13 top_gap = probs["7d"] - probs["3d"]
14 print(f"T={temperature:g} probabilities:", rounded, "top_gap:", round(top_gap, 3))1T=1 probabilities: {'7d': 0.818, '3d': 0.111, '30d': 0.041, 'never': 0.03} top_gap: 0.708
2T=4 probabilities: {'7d': 0.397, '3d': 0.241, '30d': 0.188, 'never': 0.174} top_gap: 0.156
3T=40 probabilities: {'7d': 0.263, '3d': 0.25, '30d': 0.244, 'never': 0.242} top_gap: 0.013Once alternatives carry signal, decide how much to trust that signal. A common training objective blends the teacher-matching loss with a ground-truth task loss:
matches teacher behavior, matches trusted labels, and sets the mix. Raising copies more of the teacher, including its mistakes. That makes a behavior-and-risk choice, not a knob to tune from training loss alone.
What happens when moves closer to 1?
Answer
The student listens more to the teacher distribution and less to the hard task labels. That can transfer richer teacher behavior, but it also makes teacher mistakes more influential.
That trade-off is why distillation isn't automatically better than training from scratch. It's most useful when you already have a strong teacher, legal access to its signal, and a clear smaller deployment target. The Gemma 2 report gives a controlled example: the authors train the 2B and 9B models with token-probability distillation, and a 2B ablation trained for 500B tokens scores 67.7 when distilled from a 7B teacher versus 60.3 from scratch on their three-benchmark average.[2] That 7B teacher is an ablation sized to mimic their 27B-to-9B gap, not a claim about the production teacher.
Gemma 2 writes the distillation objective as a token-level negative log-likelihood under the teacher distribution, which matches forward KL up to the teacher's entropy, a constant with respect to the student. Different recipes still expose different channels: Orca trains on explanation traces, phi-1.5 uses curated synthetic textbook-like data, and Gemma 2 uses teacher token probabilities for small models.[3][4][2] These examples motivate careful data and signal selection. They don't establish one universally best recipe.
Soft labels explain why a teacher distribution can beat a one-hot target. Next, put that target on the causal language-model path and see what can go wrong.
Matching the teacher's probabilities: logit distillation
Logit KD needs the teacher's logits: raw, unnormalized scores from the final layer, before softmax. Training minimizes KL divergence (Kullback-Leibler divergence) so the student's token distribution tracks the teacher's.
Send the same restore-window prompt through both models. Predict the update: the teacher's output is a target, while only the student's weights move.

Start by applying temperature scaling, dividing both models' logits by before softmax:
are the teacher's logits, are the student's logits, and is temperature. The KL term is then
Reading the formula
- is the teacher's softened probability for token (the target)
- is the student's softened probability for token (the current prediction)
- is temperature (higher is softer and closer to uniform)
- The factor compensates for gradient scaling. Hinton et al. note that gradients from soft targets scale as about , so multiplying by keeps the soft term's relative weight stable while you tune temperature.[1]
- The hard task loss, when you use one, is ordinary cross-entropy at
- KL measures extra surprisal from coding samples from with a code built for
Why use temperature during logit distillation?
Answer
Higher temperature softens the teacher distribution, exposing near-miss classes instead of only the top answer. The student learns the teacher's ranking over alternatives rather than a hard label alone.
Two implementation details can turn a plausible loss into bad supervision. First, next-token training needs a one-token shift: logits at position train against the label at position . Second, direct logit KD assumes teacher and student use the same token-to-id output mapping. Equal vocabulary sizes aren't enough: token id 42 must denote the same token in both models. If the output spaces differ, plain token-level KL no longer lines up and you usually fall back to response distillation or design an explicit mapping.
⚠️ Common mistake: Running logit distillation without verifying tokenizer alignment. Two models can share a vocabulary size and still disagree on token-id mappings. Compare the complete output mapping, not
vocab_sizealone, before training.
Use this stdlib toy loss to check both mechanics without a neural net. It shifts next-token labels, skips ignored positions, computes KL plus hard cross-entropy, and fails fast on a reordered vocabulary.
1import math
2
3VOCAB = ("7d", "3d", "30d", "never")
4
5def softmax(logits: list[float], temperature: float) -> list[float]:
6 scaled = [logit / temperature for logit in logits]
7 offset = max(scaled)
8 exps = [math.exp(logit - offset) for logit in scaled]
9 total = sum(exps)
10 return [value / total for value in exps]
11
12def log_softmax(logits: list[float], temperature: float) -> list[float]:
13 return [math.log(prob) for prob in softmax(logits, temperature)]
14
15def kl_divergence(teacher_probs: list[float], student_log_probs: list[float]) -> float:
16 return sum(
17 teacher_prob * (math.log(teacher_prob) - student_log_prob)
18 for teacher_prob, student_log_prob in zip(teacher_probs, student_log_probs)
19 )
20
21def cross_entropy(logits: list[float], label: int) -> float:
22 return -log_softmax(logits, 1.0)[label]
23
24def distillation_loss(
25 student_logits: list[list[float]],
26 teacher_logits: list[list[float]],
27 labels: list[int],
28 student_vocabulary: tuple[str, ...],
29 teacher_vocabulary: tuple[str, ...],
30 temperature: float = 3.0,
31 alpha: float = 0.5,
32 ignore_index: int = -100,
33) -> float:
34 if student_vocabulary != teacher_vocabulary:
35 raise ValueError(
36 "Logit KD requires identical token-to-id mappings. "
37 "Use response KD or design an explicit mapping when output spaces differ."
38 )
39 width = len(student_vocabulary)
40 if any(len(row) != width for row in student_logits + teacher_logits):
41 raise ValueError("Logit rows and vocabulary dimensions must agree.")
42
43 # Causal LMs predict token t+1 from positions up to t.
44 shift_student = student_logits[:-1]
45 shift_teacher = teacher_logits[:-1]
46 shift_labels = labels[1:]
47
48 soft_terms: list[float] = []
49 hard_terms: list[float] = []
50 for student_row, teacher_row, label in zip(shift_student, shift_teacher, shift_labels):
51 if label == ignore_index:
52 continue
53 teacher_probs = softmax(teacher_row, temperature)
54 student_log_probs = log_softmax(student_row, temperature)
55 soft_terms.append(kl_divergence(teacher_probs, student_log_probs) * temperature**2)
56 hard_terms.append(cross_entropy(student_row, label))
57
58 if not soft_terms:
59 return 0.0
60 soft_loss = sum(soft_terms) / len(soft_terms)
61 hard_loss = sum(hard_terms) / len(hard_terms)
62 return alpha * soft_loss + (1 - alpha) * hard_loss
63
64student_logits = [
65 [0.20, 0.10, 0.00, -0.10],
66 [1.50, 0.40, 0.10, -0.20],
67 [0.30, 2.00, 0.20, 0.00],
68 [0.10, 0.20, 0.30, 0.40],
69]
70teacher_logits = [
71 [2.00, 0.50, 0.10, 0.00],
72 [3.00, 1.00, 0.20, 0.10],
73 [0.20, 2.50, 0.40, 0.10],
74 [0.00, 0.10, 0.20, 1.50],
75]
76labels = [0, 0, 1, -100]
77
78loss = distillation_loss(
79 student_logits,
80 teacher_logits,
81 labels,
82 VOCAB,
83 VOCAB,
84 temperature=3.0,
85 alpha=0.6,
86)
87mismatch_failed = False
88try:
89 distillation_loss(
90 student_logits,
91 teacher_logits,
92 labels,
93 VOCAB,
94 tuple(reversed(VOCAB)),
95 )
96except ValueError as exc:
97 mismatch_failed = "token-to-id mappings" in str(exc)
98
99print("loss:", round(loss, 4))
100print("loss_is_finite:", math.isfinite(loss))
101print("mismatch failed:", mismatch_failed)1loss: 0.72
2loss_is_finite: True
3mismatch failed: TrueWhite-box logits are the richest token-level channel. Many teachers expose text only, so the supervision contract changes.
When you only have text: response distillation
Without weights or logits, response distillation is the available KD channel: the teacher writes answers, and the student fine-tunes on selected (prompt, response) pairs.
This is Supervised Fine-Tuning (SFT) on teacher-generated targets, not ground truth. An API commonly provides text rather than full token probabilities, so the channel is thinner than direct logit access. A teacher can provide worked solutions, decomposed subproblems, critiques, or multiple candidates, but those outputs should pass task-specific checks where you have them. For the restore-window assistant, ask which row you would train on before looking at the teacher's prose: only an answer that matches a trusted rule belongs in the target set. That is the same verifier boundary used in the RLVR chapter.
| Student / recipe | Teacher | Signal transferred | Why it matters |
|---|---|---|---|
| Alpaca 7B[5] | text-davinci-003 | 52K generated instruction-response examples | The repository reports preliminary instruction-following evaluation and clear non-commercial dataset terms. |
| Orca 13B[3] | GPT-4 + ChatGPT | Explanation traces and task instructions | Evaluates a richer generated-trace recipe, rather than logit KD. |
| phi-1.5[4] | Existing LLMs + curated synthetic data | Textbook-like synthetic corpora | Adjacent synthetic-data recipe, not a teacher-distribution KD comparison. |
| Distilling Step-by-Step[6] | Generated rationales plus labels | Rationales as an extra supervised target | Tests whether smaller students improve on the studied tasks with rationale supervision. |
| Gemma 2 2B / 9B[2] | Larger Gemma teachers | Token-probability distillation during pretraining | Reports a controlled 2B distilled-versus-from-scratch ablation. |
| DeepSeek-R1-Distill[7] | DeepSeek-R1 | About 800K selected SFT examples | Fine-tunes Qwen2.5 and Llama bases (1.5B, 7B, 8B, 14B, 32B, 70B) with SFT only, no RL stage on those distilled checkpoints. The paper reports 55.5% on AIME 2024 for Distill-Qwen-7B and 72.6% for Distill-Qwen-32B. |
Response distillation and synthetic-data training overlap when a stronger model generates the selected targets. The label "distillation" doesn't remove the need for evaluation. Generated traces can be wrong, stylistically misleading, contaminated, or out of scope for the intended student.
Use the restore-window gate as a small prediction test. The teacher is trusted on disabled prod key restore and wrong on expired admin restore, so predict which prompt the filter rejects.
1generated = [
2 {"prompt": "disabled prod key restore", "teacher": "7 days", "verified": "7 days"},
3 {"prompt": "expired admin restore", "teacher": "7 days", "verified": "not allowed"},
4 {"prompt": "rotate staging key window", "teacher": "not restorable", "verified": "not restorable"},
5]
6
7accepted = [
8 example for example in generated
9 if example["teacher"] == example["verified"]
10]
11rejected = [
12 example["prompt"] for example in generated
13 if example["teacher"] != example["verified"]
14]
15
16print("generated:", len(generated))
17print("accepted:", len(accepted))
18print("rejected prompts:", rejected)
19print("teacher text is trusted label:", len(rejected) == 0)1generated: 3
2accepted: 2
3rejected prompts: ['expired admin restore']
4teacher text is trusted label: FalseText and logits supervise the output. With white-box access you can also ask hidden states to match, which introduces a projection problem that output losses don't have.
Aligning internal layers: feature distillation
Logit distillation matches output distributions. White-box access also lets a training objective match selected student hidden states to selected teacher hidden states through a learned projection.
and are hidden states at layer , and projects student features into the teacher feature space before comparison.
Why does feature distillation need the projection ?
Answer
The student and teacher may have different hidden sizes. The projection maps student features into the teacher's feature space so the layer representations can be compared.
Feature matching adds three decisions before it adds value: which layers correspond, how to train the projection, and whether its extra compute improves held-out outcomes. Hidden-state access is a richer interface, not a guarantee of a better student.
| Method | Teacher signal | Main advantage | Main constraint |
|---|---|---|---|
| Response KD | Selected text outputs | Works without white-box access | Teacher errors become SFT targets unless filtered |
| Logit KD | Token probabilities | Preserves distribution information | Requires aligned output space or an explicit mapping |
| Feature KD | Selected hidden states | Exposes intermediate representations | Needs layer/projection design and more storage or compute |
| On-policy KD | Teacher scores on student samples | Visits prefixes the student produces | Requires online sampling and teacher evaluation |
So far KL was written as , with the teacher on the left. For generation, that direction is a choice, not a law.
Forward versus reverse KL
When minimizing KL for language generation, direction matters. Classical distillation commonly minimizes forward KL (teacher student), which penalizes a student for missing probability mass that the teacher assigns to continuations. When a small student can't model the teacher distribution well, that coverage pressure can be costly. Predict the consequence: a capacity-limited student may spread probability across many teacher modes instead of committing to a usable answer.
Reverse KL (student teacher) places more pressure on probability mass the student assigns where the teacher assigns little. MiniLLM reports improvements over its studied standard-KD baselines using reverse KL with an on-policy optimization algorithm in instruction-following experiments.[8] GKD evaluates multiple divergences and reports that the best divergence depends on task and the diversity-performance tradeoff.[9]
| Direction | Formula | Behavior | Common fit |
|---|---|---|---|
| Forward KL | Mean-seeking, covers more of the teacher distribution | Classic KD when broad coverage matters | |
| Reverse KL | Penalizes student mass in teacher-low-probability regions | Candidate objective to evaluate for generation |
Stay inside the restore-window story. At a later token the teacher still prefers 7d (0.58) over 3d (0.40) and almost never predicts never (0.02). Predict which student should trigger the larger reverse-KL penalty: one covers the ranking but leaks 0.10 onto never; another dumps 0.25 onto never. The latter is the stronger out-of-support mistake, and reverse KL more than doubles while forward KL rises less.

No divergence wins every task. Measure task quality, diversity, calibration, and failure rates under the decoding setup you will ship.
1import math
2
3teacher = {"7d": 0.58, "3d": 0.40, "never": 0.02}
4students = {
5 "covers_teacher": {"7d": 0.54, "3d": 0.36, "never": 0.10},
6 "dumps_on_never": {"7d": 0.40, "3d": 0.35, "never": 0.25},
7}
8
9def kl(left: dict[str, float], right: dict[str, float]) -> float:
10 return sum(prob * math.log(prob / right[token]) for token, prob in left.items())
11
12for name, student in students.items():
13 forward = kl(teacher, student)
14 reverse = kl(student, teacher)
15 print(name, "forward:", round(forward, 3), "reverse:", round(reverse, 3))
16
17print("choose objective from evaluation, not slogan")1covers_teacher forward: 0.051 reverse: 0.084
2dumps_on_never forward: 0.218 reverse: 0.436
3choose objective from evaluation, not sloganKL direction decides how the student fits a distribution on a given prefix. It can't help with prefixes the distillation set never contains.
Off-policy versus on-policy distillation
Off-policy (standard) distillation trains the student on a fixed set of prefixes, which may come from ground-truth sequences or teacher-generated outputs. At inference the student generates its own tokens, so its prefix distribution shifts. Predict the failure: once one early token changes, later prefixes can leave the fixed training set, and errors compound. That's exposure bias.
On-policy methods such as Generalized Knowledge Distillation (GKD) sample sequences from the student, then compare student and teacher token distributions on the prefixes the student produced. GKD can mix fixed outputs and student-generated outputs through a student-data fraction ; it doesn't require a natural-language critique.[9] The cost is computational: both student sampling and teacher scoring run during training. This is useful when fixed teacher data misses prefixes that the deployed student commonly enters, but the benefit has to be measured per task.
Gemma 2's post-training recipe applies the same idea after SFT on predominantly synthetic teacher responses: the authors also distill from the teacher on the student's distribution, citing GKD and MiniLLM.[2][9][8]
1fixed_teacher_prefixes = {
2 "restore disabled prod key",
3 "rotate staging key",
4}
5student_generated_prefixes = {
6 "restore disabled prod key",
7 "restore expired admin key",
8 "restore key without approval",
9}
10
11unseen_in_fixed_data = student_generated_prefixes - fixed_teacher_prefixes
12teacher_scored_prefixes = fixed_teacher_prefixes | student_generated_prefixes
13
14print("fixed prefixes:", len(fixed_teacher_prefixes))
15print("student prefixes needing new teacher scores:", sorted(unseen_in_fixed_data))
16print("scored after on-policy collection:", len(teacher_scored_prefixes))1fixed prefixes: 2
2student prefixes needing new teacher scores: ['restore expired admin key', 'restore key without approval']
3scored after on-policy collection: 4The losses and sampling rules matter only if a training step updates the student and not the teacher. The next loop makes that invariant visible.
A practical distillation training loop
A typical loop freezes the teacher, sends the same input through both models, and updates only the student with the combined loss. The local example uses tiny positionwise PyTorch stand-ins rather than Transformers, so you can test the mechanics without downloading a real teacher.
1import torch
2from torch import nn
3import torch.nn.functional as F
4
5class TinyLM(nn.Module):
6 def __init__(self, vocab_size: int, hidden_size: int):
7 super().__init__()
8 self.embedding = nn.Embedding(vocab_size, hidden_size)
9 self.output = nn.Linear(hidden_size, vocab_size)
10
11 def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
12 return self.output(self.embedding(input_ids))
13
14def kd_loss(student_logits: torch.Tensor, teacher_logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
15 temperature = 3.0
16 alpha = 0.7
17
18 shift_student = student_logits[:, :-1, :]
19 shift_teacher = teacher_logits[:, :-1, :].detach()
20 shift_labels = labels[:, 1:]
21
22 student_flat = shift_student.reshape(-1, shift_student.size(-1))
23 teacher_flat = shift_teacher.reshape(-1, shift_teacher.size(-1))
24 labels_flat = shift_labels.reshape(-1)
25
26 soft_teacher = F.softmax(teacher_flat / temperature, dim=-1)
27 soft_student = F.log_softmax(student_flat / temperature, dim=-1)
28 soft_loss = F.kl_div(soft_student, soft_teacher, reduction="batchmean") * temperature**2
29 hard_loss = F.cross_entropy(student_flat, labels_flat)
30 return alpha * soft_loss + (1 - alpha) * hard_loss
31
32torch.manual_seed(0)
33vocab_size = 12
34teacher = TinyLM(vocab_size=vocab_size, hidden_size=16)
35student = TinyLM(vocab_size=vocab_size, hidden_size=6)
36teacher.requires_grad_(False)
37teacher.eval()
38
39input_ids = torch.tensor([
40 [1, 2, 3, 4, 5],
41 [1, 3, 5, 7, 9],
42])
43labels = input_ids.clone()
44optimizer = torch.optim.AdamW(student.parameters(), lr=0.05)
45
46with torch.no_grad():
47 teacher_logits = teacher(input_ids)
48
49before = kd_loss(student(input_ids), teacher_logits, labels)
50optimizer.zero_grad()
51before.backward()
52has_grad = any(parameter.grad is not None for parameter in student.parameters())
53optimizer.step()
54
55after = kd_loss(student(input_ids), teacher_logits, labels)
56
57print("before:", round(float(before), 4))
58print("after:", round(float(after), 4))
59print("has_grad:", has_grad)
60print("after_is_finite:", bool(torch.isfinite(after)))
61print("improved:", bool(after < before))1before: 1.1513
2after: 0.9793
3has_grad: True
4after_is_finite: True
5improved: TrueA deployed pipeline usually puts this pattern inside a framework trainer, with aligned-vocabulary models such as a larger Gemma teacher and smaller Gemma student when you need direct token-probability distillation. Teams may pre-compute some teacher signal offline to avoid running the teacher inside every student update. Dense next-token logits across a long corpus are costly to store, so a design may consider top-k logits, teacher responses, or online scoring, then measure the quality effect of compression. The payload-only estimate below ignores metadata and storage-format overhead, so treat it as a lower-bound sizing exercise.
1tokens = 50_000_000
2vocab_size = 32_000
3bytes_per_logit = 2 # bf16
4top_k = 64
5bytes_per_topk_item = 2 + 4 # bf16 value plus int32 token id
6
7dense_bytes = tokens * vocab_size * bytes_per_logit
8topk_bytes = tokens * top_k * bytes_per_topk_item
9gib = 1024 ** 3
10
11print("dense cache GiB:", round(dense_bytes / gib, 1))
12print("top-k cache GiB:", round(topk_bytes / gib, 1))
13print("storage reduction:", round(dense_bytes / topk_bytes, 1), "x")
14print("quality must still be evaluated:", True)1dense cache GiB: 2980.2
2top-k cache GiB: 17.9
3storage reduction: 166.7 x
4quality must still be evaluated: TrueIf an online-distillation pilot is feasible, compare it with an offline baseline before committing to large-scale data generation. That comparison can show whether fresh teacher scoring earns enough held-out quality to justify its compute cost for this task.
The training step assumes a legal, filtered corpus. For response KD, corpus design is the main lever you control, alongside student capacity and the evaluation split.
Building a distillation dataset
When using response-based distillation, selected data, student capacity, and training budget set the ceiling. Build a generation and selection pipeline that can reject incorrect, duplicate, contaminated, or irrelevant examples before they become targets.
Seed-Expand-Filter pipeline
A Seed-Expand-Filter pipeline gives teacher-data generation a sequence of decisions. It doesn't prove quality by itself. Each filter needs a measurable contract and a separate evaluation split.
- Seed: Start with a small set of high-quality, human-written prompts (for example, 100 restore-window and key-rotation questions).
- Expand: Ask the teacher to generate new, diverse variations of those prompts.
- Generate: Have the teacher answer the new prompts, often with rationales or decomposed steps when richer supervision helps.
- Filter: Use checks, deduplication, safety screening, or reviewed scoring rules to reject unsuitable generations.
Use the three-row fixture from the teacher-output gate to see those filters fire:
| Prompt | Teacher text | Gate | Decision |
|---|---|---|---|
disabled prod key restore | 7 days | matches the trusted answer | keep |
Disabled prod key restore | (not generated) | same normalized prompt | skip as duplicate |
expired admin restore | 7 days | trusted answer is not allowed | reject |
This follows the broad shape of Alpaca's Self-Instruct-style pipeline and Orca's richer explanation-trace generation, while real systems add deduplication, safety filters, and task balancing.[5][3]
Two checks are easy to skip and expensive to recover from: deduplicate prompts before paying for generation, and verify teacher answers before they become student targets. The wrapper encodes both.
1from collections.abc import Callable
2
3class DistillationDataGenerator:
4 def __init__(
5 self,
6 teacher_generate: Callable[[str], str],
7 verify_response: Callable[[str, str], bool],
8 ):
9 self.teacher_generate = teacher_generate
10 self.verify_response = verify_response
11
12 def generate_dataset(self, prompts: list[str]) -> list[dict[str, str]]:
13 selected: list[dict[str, str]] = []
14 seen: set[str] = set()
15 for prompt in prompts:
16 normalized = " ".join(prompt.lower().split())
17 if normalized in seen:
18 continue
19 seen.add(normalized)
20 response = self.teacher_generate(normalized).strip()
21 if self.verify_response(normalized, response):
22 selected.append({"prompt": normalized, "response": response})
23 return selected
24
25def fake_teacher(prompt: str) -> str:
26 return "7 days"
27
28trusted_answers = {
29 "disabled prod key restore": "7 days",
30 "expired admin restore": "not allowed",
31}
32
33def verify_response(prompt: str, response: str) -> bool:
34 return trusted_answers[prompt] == response
35
36generator = DistillationDataGenerator(fake_teacher, verify_response)
37examples = generator.generate_dataset([
38 "disabled prod key restore",
39 " Disabled prod key restore ",
40 "expired admin restore",
41])
42
43print("selected prompts:", [example["prompt"] for example in examples])
44print("selected count:", len(examples))
45print("bad response retained:", any("expired admin" in example["prompt"] for example in examples))1selected prompts: ['disabled prod key restore']
2selected count: 1
3bad response retained: FalseKeep training data out of evaluation
Teacher generation can quietly contaminate a benchmark when prompts, reference solutions, or close rewrites enter the student training set. Block exact normalized overlap before training. For real releases, extend the gate with near-duplicate and reference-solution checks.
1def normalize(prompt: str) -> str:
2 return " ".join(prompt.lower().replace("?", "").split())
3
4candidate_training_prompts = [
5 "Compute retry budget for a failed restore",
6 "Summarize failed restore logs",
7 "How many days can a disabled production API key stay restorable?",
8]
9held_out_prompts = [
10 "how many days can a disabled production API key stay restorable",
11 "Estimate GPU queue delay for a remote region",
12]
13
14held_out_keys = {normalize(prompt) for prompt in held_out_prompts}
15accepted = [
16 prompt for prompt in candidate_training_prompts
17 if normalize(prompt) not in held_out_keys
18]
19blocked = [
20 prompt for prompt in candidate_training_prompts
21 if normalize(prompt) in held_out_keys
22]
23
24print("accepted training prompts:", len(accepted))
25print("blocked overlap:", blocked)
26print("held-out exact overlap after gate:", any(normalize(p) in held_out_keys for p in accepted))1accepted training prompts: 2
2blocked overlap: ['How many days can a disabled production API key stay restorable?']
3held-out exact overlap after gate: FalseA clean corpus still can't make a small model remember behavior beyond its capacity or context window, and it can't settle license constraints.
Limitations and when not to distill
Distillation doesn't erase context-window or data-coverage limits. A student may beat its teacher on a narrow checked metric after filtering or task-specific training, while regressing on other behavior. Treat teacher and student as separate artifacts to evaluate.
Before investing in a distillation pipeline, write the behavior contract and its tests. If the student passes common restore cases but fails expired-admin or long-context cases, capacity, context length, prompt coverage, and teacher-target quality are competing explanations. Split those hypotheses with targeted slices instead of reading one aggregate score.
| Behavior | Regression risk to test | Useful held-out gate |
|---|---|---|
| Domain answers | Generated targets can repeat teacher errors | Checked answer accuracy and abstention rate |
| Instruction following | Narrow traces can miss new constraints | Fresh constraint-following prompts |
| Multi-step solutions | Final answers can hide invalid steps | Step checks where available plus final-answer accuracy |
| Long-context use | Student architecture or context limit may differ | Retrieval and long-context slices at deployment length |
| Safety and policy behavior | Filtered corpus may omit refusals or edge cases | Safety-policy evaluation separate from task benchmark |
Legal and ethical considerations
Because the student can mirror teacher outputs closely, the origins of that training data matter.
- Provider terms matter: the Stanford Alpaca release was research-only and non-commercial, and the repo points to both the underlying LLaMA restrictions and the dataset's CC BY-NC 4.0 terms.[5]
- Restrictions must be reviewed: before generating a corpus or shipping a student, review the teacher access terms, base-student license, generated-data license, and permitted use of outputs. Don't infer permission from technical access.
- Imitation isn't capability proof: a student may reproduce style or familiar output patterns while failing new checked tasks. Held-out evaluation, not resemblance, establishes value.
Cost-quality tradeoff
Choose a distillation method from measured quality and economics. Richer teacher access enables different losses; it doesn't rank final models without evaluation.
| Approach | Required access | Main training cost | Release gate |
|---|---|---|---|
| Use teacher directly | Teacher inference | No student training | Baseline quality, latency, and cost |
| Response KD | Generated outputs and permitted use | Generation plus SFT | Output filtering and held-out task quality |
| Logit KD | Aligned teacher token probabilities | Teacher scoring or cache storage | Task quality plus cache/online cost |
| Feature KD | Hidden states and layer mapping | Extra projections and state transfer | Ablation against simpler KD baseline |
Distillation loss is an optimization signal, not a release decision. A student can match teacher probabilities on training batches and still regress on held-out generation quality, long-context behavior, or latency targets. Measure task metrics, pairwise win rate, and real serving cost together.
1teacher = {"checked_accuracy": 0.94, "policy_error_rate": 0.01, "latency_ms": 180, "monthly_cost": 42000}
2student = {"checked_accuracy": 0.92, "policy_error_rate": 0.04, "latency_ms": 42, "monthly_cost": 18000}
3requirements = {
4 "checked_accuracy": 0.90,
5 "max_policy_error_rate": 0.02,
6 "max_latency_ms": 60,
7 "max_monthly_cost": 20000,
8}
9
10checks = {
11 "quality": student["checked_accuracy"] >= requirements["checked_accuracy"],
12 "policy": student["policy_error_rate"] <= requirements["max_policy_error_rate"],
13 "latency": student["latency_ms"] <= requirements["max_latency_ms"],
14 "cost": student["monthly_cost"] <= requirements["max_monthly_cost"],
15}
16
17print("student faster:", student["latency_ms"] < teacher["latency_ms"])
18print("student cheaper:", student["monthly_cost"] < teacher["monthly_cost"])
19print("release checks:", checks)
20print("deploy student:", all(checks.values()))1student faster: True
2student cheaper: True
3release checks: {'quality': True, 'policy': False, 'latency': True, 'cost': True}
4deploy student: FalseThe gate makes that trade-off concrete: this student is faster and cheaper, and its checked accuracy clears the threshold, but its policy error rate doesn't. Keep the teacher or revise the student until every release check passes.
When distillation breaks
| Symptom | Cause | Fix |
|---|---|---|
| Validation loss barely changes as you raise temperature. | The softened teacher distribution may be too flat or the soft-loss weight may be ineffective. | Inspect teacher probabilities and tune temperature and loss weight on held-out tasks. |
| Student looks strong on training prompts but weak on held-out tasks. | Distillation corpus is too narrow, repetitive, or too close to evaluation data. | Broaden prompt coverage, filter duplicates, and keep a separate held-out evaluation slice. |
| Student predicts current token instead of next token during logit KD. | Causal LM loss forgot the one-token shift. | Shift logits at position against labels at position before KL or cross-entropy. |
| Student copies teacher hallucinations and policy mistakes. | Distillation blindly transferred bad teacher outputs. | Filter teacher generations, add task loss, and evaluate against trusted labels or reward checks. |
| KL loss runs but student quality stays random. | Teacher and student token-to-id mappings don't align, even if sizes match. | Compare mappings exactly, use response distillation, or design an explicit output-space mapping. |
| Tiny student misses required checked behaviors. | Student capacity, context, or data coverage is insufficient for this release target. | Narrow task scope, increase student size, or revise training and evaluation design. |
| Offline metrics look great but production quality collapses. | Distillation and evaluation data leaked into each other. | Split generation, tuning, and evaluation sets cleanly before training starts. |