hands-on labon a real NVIDIA GPU

Synthetic Data Generation for Model Training

Build a Self-Instruct style synthetic dataset end-to-end: seed instructions, LLM-driven generation, robust parsing, quality filtering, and dedup + diversity scoring.

time
40 min
checked steps
4
level
Intermediate
setup
None
NCP-GENLNCP-ADSNCA-GENLPart of the AI Engineer Course

$29.99a month with Pro

Pro opens this lab and every other one. Cancel any time.

Lab cockpit40 min · 4 stepsSession running
3 / 4 steps passingDedup + diversity + final dataset · step 4 of 4
lab.ipynb

Step 4 — Dedup + diversity + final dataset

Even after quality filtering, small generator models love to regurgitate a handful of topics. You'll see five slightly-different versions of "Explain X in simple terms" crowding out everything else. Two tools fix this:

[ ]:
# Normalize + dedup instructionsdef normalize(s: str) -> str:    return re.sub(r'\s+', ' ', s.strip().lower()) seen_norm = set()final_dataset = []for p in quality_filtered:    n = normalize(p['instruction'])    if n in seen_norm:        continue    seen_norm.add(n)    final_dataset.append(p) print(f'Dedup: {len(quality_filtered)} -> {len(final_dataset)} unique instructions')
[ ]:
# Cheap diversity metric: average Jaccard similarity of instruction word-sets.# Lower = more topically diverse. Published datasets usually report mean pairwise ROUGE-L.import itertools def jaccard(a: set, b: set) -> float:    if not a and not b:        return 0.0    return len(a & b) / len(a | b) word_sets = [set(re.findall(r'\w+', p['instruction'].lower())) for p in final_dataset]if len(word_sets) >= 2:    sims = [jaccard(a, b) for a, b in itertools.combinations(word_sets, 2)]    diversity_score = 1.0 - (sum(sims) / len(sims))else:    diversity_score = 1.0 print(f'Diversity score (higher = more diverse): {diversity_score:.3f}')print()print(f'Final synthetic dataset ({len(final_dataset)} items):')for i, p in enumerate(final_dataset[:5]):    print(f"  [{i+1}] Q: {p['instruction']}")    print(f"      A: {p['output'][:120]}{'...' if len(p['output']) > 120 else ''}")
[ ]:
lab.check(4)
step 4 of 4, replayed: Dedup + diversity + final dataset. The code, the output and the check are the lab's own.

4 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Seed instructions + load the generator

    read this step below ↓

  2. 2

    Generate & parse

  3. 3

    Quality filter

  4. 4

    Dedup + diversity + final dataset

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Seed instructions + load the generator

Hiring a team of human annotators to write 52,000 instruction-response pairs would cost ~$500,000 and take months. In March 2023, Stanford's Alpaca team did it for $500 in OpenAI API credits, in about 3 hours, by having text-davinci-003 generate the data itself. That moment is when instruction-tuning stopped being a moat.

The technique — Self-Instruct (Wang et al., 2022) — has since become the default recipe. Every open instruction dataset you've heard of (Alpaca, Dolly, WizardLM, UltraChat, OpenOrca) is built on it. Microsoft's Phi-3 technical report goes further: Phi-3 matches GPT-3.5's benchmarks with 3.8B parameters largely because ~70% of its training data is LLM-generated, curated, "textbook-quality" synthetic text.

The core insight: a capable LLM already knows enough to write training data for a smaller/weaker model. You just have to:

  1. Write a handful of diverse seed instructions yourself
  2. Prompt the LLM to generate more in the same style
  3. Parse, filter, deduplicate — then train on the result

What you'll build

A miniature Self-Instruct pipeline end-to-end on your GPU:

  1. Seeds + generator — load TinyLlama-1.1B-Chat, pick 5 seed instructions
  2. Generate + parse — produce raw completions, parse structured instructions out of them
  3. Quality filter — length gates and format checks
  4. Dedup + diversity — remove near-dupes, measure diversity, produce the final dataset

We'll use TinyLlama-1.1B-Chat as the generator — 1.1B params, Apache-2.0 license, no gating, ~2 GB VRAM. Alpaca used GPT-3.5 to generate training data for a 7B Llama; we use a 1.1B to generate data for illustration. The pipeline is identical.

Two references to read while this runs


Self-Instruct starts with a tiny human-written seed set. The original paper used 175 carefully diverse seeds; Alpaca cut it to a smaller manually-curated set. The seeds define the shape and breadth of what the generator will imitate.

Seed quality matters enormously — if your seeds are all coding questions, you'll get coding questions back. Common practice: vary across:

  • Topic (math, writing, summarization, reasoning, creative, factual)
  • Style (command, question, roleplay, constraint)
  • Length (one-liner, multi-sentence, with examples)
seed_instructions = [
    {'instruction': 'Explain Bayes theorem in two sentences.',
     'output': 'Bayes theorem tells you how to update a belief when you see new evidence. The updated probability is proportional to how likely the evidence was under that belief times your prior estimate of the belief.'},
    {'instruction': 'Write a SQL query that returns the top 3 salaries from a table called employees.',
     'output': 'SELECT salary FROM employees ORDER BY salary DESC LIMIT 3;'},
    {'instruction': 'Summarize the plot of Pride and Prejudice in one paragraph.',
     'output': 'Elizabeth Bennet, the sharp-witted second of five sisters, initially despises the aloof Mr. Darcy, but as she uncovers his hidden generosity and reevaluates her own prejudices, the two overcome the social gap between them and marry.'},
    {'instruction': 'Give me three reasons to use type hints in Python.',
     'output': '1. Catch bugs earlier via static checking with mypy. 2. Make function contracts self-documenting. 3. Enable better IDE autocomplete and refactoring.'},
    {'instruction': 'What is a closure in programming?',
     'output': 'A closure is a function that captures variables from its enclosing scope and carries them with it, letting the function reference those variables even after the enclosing scope has exited.'},
]

print(f'{len(seed_instructions)} diverse seed instructions ready')
for s in seed_instructions:
    print(f"  - {s['instruction']}")
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

MODEL_ID = 'TinyLlama/TinyLlama-1.1B-Chat-v1.0'

gen_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
gen_model = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=torch.float16).to('cuda')
gen_model.eval()

print(f'Loaded {MODEL_ID}')
print(f'  Parameters: {gen_model.num_parameters() / 1e9:.2f}B')
print(f'  VRAM used by weights: ~{gen_model.num_parameters() * 2 / 1e9:.2f} GB (fp16)')
print(f'  Free VRAM: {torch.cuda.mem_get_info()[0] / 1e9:.2f} GB')

what you get

  • A JupyterLab notebook on a real NVIDIA GPU
  • A check at the end of every step
  • Hints and the full solution
  • Nothing to install or configure

before you start

  • Familiar with Hugging Face transformers and tokenizers
  • Comfortable with Python list/dict data wrangling
  • Basic understanding of instruction tuning

Exam domains covered

Data Analysis and VisualizationExperimentationLLM Integration and Development

Part of the LLM training and fine-tuning track

Lab 10 of 14. LoRA and QLoRA fine-tuning, DPO alignment, and a tokenizer and tiny GPT built from scratch.

Questions about this lab

Why use TinyLlama specifically, won't a stronger generator produce better data?

Absolutely it would, and that's one of the reflection lessons. TinyLlama is used here because it fits on a single small GPU and runs fast enough that you can iterate on prompts and filters in seconds. In production you'd swap in Llama 3 70B, Mixtral, or even a frontier API model, the pipeline code doesn't change, only the generator handle does. The point of the lab is the scaffolding, not the generator choice.

Why expect parse-success well below 100%?

Because LLM output is fundamentally free-form and your regex/format parser is strict. A sampling temperature of 0.7-1.0 is what gives you instruction diversity, but it also makes the model occasionally skip delimiters, hallucinate extra fields, or emit markdown where you wanted plain text. Production Self-Instruct pipelines hit 40-70% parse success on first pass and compensate by generating more, tightening the parser below that level drops diversity faster than it drops noise.

What's wrong with exact-match deduplication at scale?

It only catches byte-identical strings after normalization. A 50k dataset generated from similar prompts fills up with templated rewrites, 'Write a haiku about the sea', 'Compose a haiku about the ocean', 'Write me a haiku about a sea', that lexically differ but teach the model the same lesson ten thousand times. Embedding-based dedup (compute BGE vectors, cluster, keep one per cluster) is what catches those, and it's the first upgrade the reflection step asks you to defend.

How should I pick a diversity score, token ratio, edit distance, or verb coverage?

All three are valid for this lab as long as they land in [0, 1]. Unique-token ratio (unique tokens / total tokens across all instructions) is cheapest and catches degenerate repetition well. Average pairwise edit distance catches near-duplicates that token ratio misses. Verb coverage (count of distinct lead verbs, 'write', 'classify', 'summarize', etc.) is what the original Self-Instruct paper used because it approximates task-type diversity. In practice you'd track all three and alert when any one collapses.

Why do seed examples matter so much when the generator produces most of the samples?

Because the few-shot prompt is what defines the task distribution the model generates into. Three seeds that are all 'write a haiku about X' collapse the output to haikus. Three seeds covering classification, extraction, and open-ended writing produce a much broader distribution. This is why the original Self-Instruct used 175 hand-written seeds and Alpaca used 175 as well, the seeds are the implicit task ontology, and they're worth hand-crafting.

When should I use LLM-as-judge filtering vs regex rules?

Length and format rules catch obvious junk for almost no cost, run them first. LLM-as-judge catches what rules can't see: factual errors, failure to follow the instruction, subtle toxicity, low-effort completions. The cost is real, though, every kept sample means an extra inference call, and the judge has its own biases (favoring verbose answers, penalizing correct-but-brief ones). The right pattern is regex rules as a cheap first pass, LLM-as-judge on the survivors, and human review on a calibration set that tunes both.

What you'll build in this synthetic-data lab

Synthetic data is the only reason frontier fine-tuning is tractable in 2026, Alpaca, WizardLM, Orca, every domain instruction corpus that isn't scraped off the public web, and most of what powers open-weight instruction tunes came out of a Self-Instruct-style pipeline. In about 40 minutes on a real NVIDIA GPU we provision, you'll implement that pipeline end-to-end: seed examples, LLM-driven generation with sampling, robust parsing of free-form LM output, length + repetition filters, exact-hash dedup, and a diversity score you can watch collapse if your seeds are too homogeneous. You'll walk away with a working mental model of synthetic data as a funnel (not a fountain) and concrete numbers for how many samples die at each stage, parse failures alone typically kill 30-60% on first pass.

The substance is the scaffolding you'll end up rewriting whenever you generate training data for a real fine-tune. You'll build a few-shot prompt from hand-crafted seeds, call model.generate(do_sample=True, temperature=0.7-1.0), high enough to get instruction diversity, high enough that your regex parser will fail on some outputs, and watch parse-success rates in the 40-70% band that production Self-Instruct pipelines actually hit. You'll apply length gates ([10, 500] chars for instructions, [10, 2000] for outputs), strip degenerate repetition, then normalise and exact-hash dedup with a set. The critical lesson is that exact-hash dedup is a floor: at 50k samples you'll have thousands of templated paraphrases ("Write a haiku about the sea" vs "Compose a haiku about the ocean") that lex-differ but teach the model the same thing, which is why production pipelines layer embedding-based semantic dedup on top. TinyLlama is the generator here because it iterates in seconds, swap in Llama 3 70B, Mixtral, or a frontier API model for production and the pipeline code doesn't change.

Prerequisites: comfort with Hugging Face transformers, Python list/dict wrangling, and the basic shape of instruction tuning. TinyLlama weights and the tokenizer are cached in the sandbox. The grader is deliberately realistic about stochastic generation, if a run produces zero quality-passing samples, it surfaces the diagnostic rather than failing silently, because that outcome is itself a teaching moment about generator choice. Search-intent hooks: "generate synthetic training data for LLM", "Self-Instruct pipeline tutorial", "Alpaca-style data generation", "deduplicate instruction tuning dataset".