LLMModule 08 · Stage 04

Synthetic data & curation pipelines

Use a strong model to write training data, use a checker to throw away the bad part, and train a smaller model on what survives. The generator is the easy bit. The checker, the diversity and the bookkeeping decide whether the result is any good.

PrerequisitesFine-tuning a small model (Module 04), SFT basics (Module 07), Python scripting
Time15–25 h (reading 5–6 h, laptop pipeline 8–12 h, write-up 2–3 h)
HardwareLaptop for the pipeline logic · Colab GPU (T4 or better) to generate with and fine-tune a ~0.5–1.5B model · API budget or a multi-GPU node for the full 50,000-trace version
Key papersWang et al. 2022 (Self-Instruct) · Xu et al. 2023 (Evol-Instruct) · Zelikman et al. 2022 (STaR) · Yuan et al. 2023 (RFT) · Gunasekar et al. 2023 (phi-1) · Shumailov et al. 2023 (model collapse)

Source: Suraj Sharma, X post, stage 4

Start here (no maths needed)

Instead of collecting more writing from people, you ask a clever AI to write practice examples, check them, keep only the good ones, and use those to teach a smaller AI.

Think of it like this

Imagine a teacher making a revision book. She writes lots of maths questions and asks a strong student to solve each one, showing the working. Then she checks every answer with the answer key. Wrong ones go in the bin. Right ones go in the book. A younger student then studies from the book. The book is only as good as the checking. If the teacher's answer key has mistakes, wrong working ends up in the book, and the younger student learns those mistakes too.

How it works

  1. Make questions: start with a few examples and ask a big AI to invent many more, harder and more varied ones.
  2. Make answers: the big AI answers each question several times, showing its working.
  3. Check: a checker (a calculator, a test, or another AI) marks each answer right or wrong.
  4. Clean up: throw out wrong answers, near-copies, and anything that leaks the test questions.
  5. Teach: train a smaller AI on the answers that survived, then test it on questions it has never seen.

Why robots care

Robots have the same problem with even less data: nobody has millions of hours of robot videos. So engineers make "synthetic" robot data in computer simulations, or let a robot try a task many times and keep only the attempts that worked. It's the same recipe: make lots, check, keep the good ones.

Try it

Words to know

Synthetic data
Training examples written by a computer, not collected from people.
Verifier
A checker that says whether an answer is right.
Rejection sampling
Make many attempts, throw away the ones that fail the check.
Trace
The step-by-step working that leads to an answer.
Model collapse
When models trained on other models' output slowly lose variety and rare knowledge.

Going deeper: everything below is the full version. On a first read, skim the headings, try the demo, and read the reality check.

1. What this stage covers

TL;DR

A synthetic-data pipeline has four parts: prompts (where the questions come from and how diverse they are), generation (a stronger model writes answers or reasoning traces, usually several per prompt), filtering (a verifier, a judge model, dedup and decontamination) and training (SFT on the survivors, then honest evaluation). The core maths is simple base-rate arithmetic. The fraction of wrong examples in your kept data depends on the generator's pass rate and the verifier's false-pass rate, and it gets worse fast when the generator is weak. Where answers can be checked exactly (maths, code, logic), this works very well. Where they can't, quality depends on judge models with their own biases.

2. Concepts, explained

Self-Instruct: bootstrapping instructions from a seed set

Self-Instruct (Wang et al., 2022) starts from a small pool of human-written seed tasks (175 in the paper). Each round, it samples a few tasks from the pool as in-context examples and asks the model for new instructions, then for inputs and outputs. Before a new task joins the pool, filters remove invalid outputs and near-duplicates: a new instruction is rejected if its ROUGE-L overlap with any existing one is too high. The pool grows, and later rounds draw on generated tasks too. Fine-tuning vanilla GPT-3 on the result gave a 33% absolute gain on Super-NaturalInstructions, close to InstructGPT-001.

pool = seed_tasks
repeat:
    examples  = sample(pool, k)                       # mix of seed and generated
    new_tasks = LM("write new tasks like these", examples)
    new_tasks = [t for t in new_tasks
                 if valid(t) and max_rougeL(t, pool) < threshold]
    pool += new_tasks

The lesson that carries over: diversity filtering is part of generation. Without the similarity check, the pool fills with paraphrases of the seeds.

Evolutionary prompting: Evol-Instruct

Self-Instruct's tasks tend to be easy. Evol-Instruct (Xu et al., 2023, the WizardLM paper) "evolves" each instruction by asking an LLM to rewrite it. In-depth evolution makes it harder: add a constraint, require more reasoning steps, make the input more complex. In-breadth evolution writes a new instruction on a related but different topic. An elimination step discards evolutions that failed (for example, the rewrite didn't add anything, or the model couldn't answer it). Repeating this for several rounds produces a mix of difficulty levels. The authors fine-tuned LLaMA on the mix and, in their human evaluations, preferred its outputs on the hardest instructions over ChatGPT's.

The general idea is a genetic algorithm over prompts: mutate, test, keep the fit ones. "Fit" here means the instruction is still well-posed and answerable, not just longer.

Rejection sampling: STaR and RFT

For tasks with a checkable answer, sample several solutions per problem and keep only the correct ones.

The arithmetic of a filter. Let p be the generator's pass rate, TPR the chance the verifier accepts a correct trace, and FPR the chance it accepts a wrong one:

yield      = p·TPR + (1 − p)·FPR                  # fraction of generations you keep
precision  = p·TPR / yield                       # fraction of kept data that is correct
generations needed for N survivors  =  N / yield

example: p = 0.10, TPR = 0.90, FPR = 0.05
  yield = 0.09 + 0.045 = 0.135     precision = 0.09 / 0.135 ≈ 0.67   → one kept trace in three is wrong

That's the base-rate trap. A verifier that looks "95% accurate" still lets lots of garbage through when most generations are wrong. Two fixes: make the verifier stricter (exact checks, unit tests), or make the generator better on the problems you pick (don't feed it problems far above its level).

Interactive: rejection-sampling yield vs. verifier quality

x axis: generator pass rate p. Solid (accent): precision, the share of kept traces that are actually correct. Dashed (muted): yield, the share of generations kept. The dot marks your chosen p. Target: 50,000 survivors, as in the post.

Yield
–
Wrong among kept
–
Generations for 50k
–
Problems with ≥1 correct kept
–

Try this: p = 0.10, TPR = 0.90, FPR = 0.05 gives the worked example above, about a third of kept traces wrong. Set FPR to 0 (an exact checker) and precision goes to 100%. The last stat assumes every problem has the same pass rate. Real problems vary, so hard ones are covered far less often than this number suggests.

Using frontier models to generate and filter

The most common pipeline in 2026 is distillation through data: a large "teacher" writes answers or reasoning traces, a filter keeps the good ones, a smaller "student" is fine-tuned on them. Three levers matter:

The risk: model collapse. Shumailov et al. (2023) showed that training each generation of model on the previous generation's output makes the tails of the original distribution disappear: rare facts, unusual styles, minority cases. The practical defences are to keep real data in the mix, filter for diversity, and avoid training repeatedly on your own model's unfiltered output.

3. The practice project

(a) Laptop / Colab version: verified logic traces for a 0.5B student

Pick a task where the verifier is exact and free, so you can study the pipeline without arguing about judges. Programmatically generated puzzles are ideal: you know the answer by construction.

  1. Problems: write a generator for multi-step puzzles with a known answer, with a difficulty knob (e.g. chained arithmetic with 3–8 steps, or "who sits where" constraint puzzles solved by brute force). Make 3,000 train and 500 held-out test problems, with different random seeds.
  2. Teacher: a 1.5–7B instruct model on Colab (with a fast inference library), or a modest API budget for a few thousand calls to a stronger model. Sample k = 4 traces per problem at temperature ~0.8, asking for working then a final line ANSWER: ….
  3. Verify and curate: parse the final answer, check it exactly, drop traces with no parsable answer, deduplicate identical traces, cap at 2 kept traces per problem.
  4. Student: fine-tune a ~0.5B model (LoRA is fine) on the survivors. Train a second student on unfiltered traces with the same token budget as the control.
  5. Measure: yield per difficulty level; held-out accuracy of the base student, the unfiltered student and the filtered student; accuracy by difficulty level. Report all three numbers, not just the best one.
import random, re

def make_problem(rng, steps):
    x = rng.randint(1, 20); text = f"Start with {x}."
    for _ in range(steps):
        op, n = rng.choice("+-*"), rng.randint(2, 9)
        text += f" Then {'add' if op == '+' else 'subtract' if op == '-' else 'multiply by'} {n}."
        x = x + n if op == "+" else x - n if op == "-" else x * n
    return {"q": text + " What is the result?", "a": x, "steps": steps}

def verify(trace, answer):
    m = re.search(r"ANSWER:\s*(-?\d+)", trace)
    return m is not None and int(m.group(1)) == answer

def curate(problems, sample, k=4, keep=2):
    out, stats = [], {"gen": 0, "kept": 0}
    for p in problems:
        traces = {sample(p["q"]) for _ in range(k)}   # set = exact dedup
        stats["gen"] += k
        good = [t for t in traces if verify(t, p["a"])][:keep]
        stats["kept"] += len(good)
        out += [{"prompt": p["q"], "completion": t, "steps": p["steps"]} for t in good]
    return out, stats

rng = random.Random(0)
train = [make_problem(rng, rng.randint(3, 8)) for _ in range(3000)]

What to expect: the yield will fall as the number of steps grows, so the kept data will be skewed toward easy problems. That skew is the most useful thing to notice. If the filtered student improves mostly on easy problems, try oversampling hard problems (larger k) or the STaR-style "rationalise from the correct answer" trick for the ones that never pass.

(b) The full version from the post

50,000 complex logic traces from a heavy reasoning model, verified, then used to train a smaller model. Points to plan for:

(c) Pitfalls

4. Reality check

The post's claim: human text is running out, so curating synthetic data is "the ultimate moat". Directionally right, overstated.

5. In this course

6. Why it matters for robotics

7. Self-check

Why does Self-Instruct reject new instructions with high ROUGE-L overlap with the pool?

To keep diversity. Without it the pool fills with paraphrases of the seed tasks, and the fine-tuned model sees many copies of the same few skills.

Generator pass rate 0.1, verifier TPR 0.9, FPR 0.05. What fraction of kept traces is wrong?

Yield = 0.1·0.9 + 0.9·0.05 = 0.135. Correct kept = 0.09. Precision ≈ 0.67, so about one in three kept traces is wrong.

What does STaR do with questions the model gets wrong?

It gives the model the correct answer as a hint and asks for a rationale that reaches it ("rationalisation"), then trains on rationales that lead to the right answer, so hard questions aren't simply dropped.

Why is rejection sampling biased toward easy problems, and what can you do about it?

Easy problems pass more often, so they contribute more survivors. Track yield by difficulty, sample more attempts for hard problems, cap survivors per problem, or use hints/rationalisation for problems that never pass.

What is model collapse, and how do you guard against it?

Repeatedly training on generated data loses the tails of the original distribution. Keep real data in the mix, filter for diversity, and avoid recursive training on unfiltered self-generated output.

Why is an exact verifier worth more than a bigger teacher?

An exact verifier drives the false-pass rate to zero, so kept data is clean whatever the teacher's pass rate. A bigger teacher only raises the pass rate, and a leaky verifier still lets errors through.

8. Courses & content

ResourceTypeUse it for
Hugging Face smol-course · Synthetic Data unitCourse (free)Generating synthetic datasets for a domain
Cosmopedia: how to create large-scale synthetic dataCase studyPrompt design, diversity and dedup for a large synthetic pre-training set
distilabelLibrary + docsPipelines for generating, judging and filtering synthetic data
Karpathy · nanochatCode (free)How synthetic and curated data feed the later training stages

Start with the smol-course synthetic data unit: it's short, hands-on and sized for small models, which matches the laptop project. Read Cosmopedia next for how diversity is handled at scale.

Full course map for all 12 stages →

9. Papers & docs