In plain English
Synthetic training data is training data written by a model instead of a human. You give a strong LLM a prompt that describes the task — "write a customer support question about billing, then write a perfect answer" — and it produces an example. Do that thousands of times and you have a dataset you can use to fine-tune a smaller model on that exact task.
Think of it like hiring a tireless contractor. A senior employee knows exactly what a great support reply looks like. Instead of asking them to manually label 10,000 tickets, you ask them to write 10,000 example tickets and ideal replies from scratch. The contractor never gets bored, never calls in sick, and can produce consistent output at whatever volume you need. The catch: you need to spot-check their work, because they can make up facts or repeat the same pattern until the whole dataset feels monotonous.
This article is a practical guide to doing that well: how to write the generation prompts, how to push for variety so your model learns robustly, how to filter out low-quality examples, and when synthetic data is actually the better choice over paying human annotators.
Why it matters
The biggest reason most fine-tuning projects fail is the dataset, not the training run. Real labeled data is expensive, slow to collect, legally restricted, or simply nonexistent for the exact task you need. Synthetic generation sidesteps those problems.
- Speed. A strong LLM can generate thousands of examples in minutes. Human labeling of the same volume can take weeks or months.
- Cost. API calls to generate data cost a fraction of what a professional annotation firm charges per example.
- Privacy. You never expose sensitive user data to labelers — the model generates fictional but representative inputs from scratch.
- Coverage of rare cases. You can explicitly prompt for edge cases, uncommon dialects, or low-frequency topics that would be badly underrepresented in any data you could collect naturally.
- Consistency. A well-prompted model applies the same rubric to every example. Human labelers introduce subjective disagreements that can add noise to the training signal.
The technique is now mainstream. The Magpie paper (ICLR 2025) showed that a single aligned model like Llama-3-Instruct could generate 4 million instruction–response pairs autonomously, and after filtering to 300,000 high-quality examples, the resulting fine-tuned model outperformed datasets built from human curation (ShareGPT, WildChat, UltraChat) on several benchmarks. Synthetic data is no longer a fallback — for many tasks it is the default starting point.
How it works
The full pipeline has four stages: write a generation prompt, call the model to produce examples, filter and score the output, then format it for training. The diagram below shows the flow.
Stage 1: the generation prompt
The generation prompt is the most important part. A weak prompt produces a narrow, repetitive dataset; a good prompt produces varied, realistic examples. The prompt should specify: the task type (question answering, classification, summarization…), the format you want the output in (usually a JSON object with input and output fields), and crucially, a diversity instruction — an explicit request to vary the topic, difficulty, style, or domain with each call.
import json, random
from openai import OpenAI
client = OpenAI()
TEACHER = "gpt-4o" # or any strong model you have access to
# A seed list forces variety — each call picks a different domain.
DOMAINS = [
"e-commerce returns", "SaaS billing", "travel booking",
"food delivery", "streaming subscriptions", "online banking",
]
GEN_PROMPT = """
You are generating training data for a customer support classifier.
Task: produce ONE realistic support ticket (the 'input') and its
correct intent label (the 'output').
Domain: {domain}
Difficulty: {difficulty}
IMPORTANT: vary the customer's tone, vocabulary, and specific
problem — avoid repeating patterns from previous examples.
Respond with ONLY a JSON object:
{{"input": "<customer message>", "output": "<intent label>"}}
"""
examples = []
for _ in range(1000):
domain = random.choice(DOMAINS)
difficulty = random.choice(["easy", "medium", "hard"])
resp = client.chat.completions.create(
model=TEACHER,
messages=[{
"role": "user",
"content": GEN_PROMPT.format(domain=domain, difficulty=difficulty)
}],
temperature=0.9, # higher temp = more diversity in outputs
response_format={"type": "json_object"},
)
try:
obj = json.loads(resp.choices[0].message.content)
if "input" in obj and "output" in obj:
examples.append(obj)
except json.JSONDecodeError:
pass # discard malformed outputs
with open("raw_dataset.jsonl", "w") as f:
for ex in examples:
f.write(json.dumps(ex) + "\n")Stage 2: diversity tricks
A dataset where every example feels like a slight rephrasing of the same thing is called distribution collapse — and it is the most common failure mode of naive synthetic generation. The model fine-tuned on it learns to produce monotone, formulaic outputs. There are several techniques to prevent this.
- Seed rotation. Maintain a list of topics, domains, personas, or difficulty levels and sample from them on each call, as shown above. This guarantees that the model must exercise different vocabulary and reasoning for each example.
- High temperature. Set
temperatureto 0.8–1.0 during generation. This increases lexical variety. Do not use temperature 0 — you will get near-identical outputs. - Multiple teacher models. Generating half your data with one model and half with another prevents any single model's stylistic habits from dominating the dataset. Research confirms that synthetic data from diverse sources measurably reduces distribution collapse and preserves output breadth.
- Self-Instruct expansion. Start with 20–50 human-written seed examples, then prompt the teacher to generate new, different examples that are not similar to any in the seed set. This bootstraps coverage far beyond what humans drafted.
- Adversarial inputs. Explicitly prompt for inputs that should trigger edge-case or failure behaviors — ambiguous phrasing, conflicting constraints, or topics outside the main distribution. These are the examples most fine-tunes miss.
Stage 3: quality filtering
Raw generated examples contain noise: the model sometimes ignores the format, hallucinates incorrect labels, produces near-duplicate outputs, or generates off-topic content. Filtering before training is not optional — even a small fraction of bad examples can degrade model quality significantly. Quality filtering delivers more value than raw data volume.
import json
from openai import OpenAI
client = OpenAI()
JUDGE = "gpt-4o-mini" # a cheaper model is fine for judging
JUDGE_PROMPT = """
You are a data quality judge. Rate this training example on a 1-5 scale.
Input: {input}
Output: {output}
Task: intent classification for customer support
Score on:
- Correctness (output is the right label for the input)
- Clarity (input is natural, realistic customer language)
- Uniqueness (not a generic, repetitive phrasing)
Respond with ONLY a JSON object: {{"score": <1-5>, "reason": "..."}}
"""
def score_example(ex: dict) -> float:
resp = client.chat.completions.create(
model=JUDGE,
messages=[{
"role": "user",
"content": JUDGE_PROMPT.format(**ex)
}],
response_format={"type": "json_object"},
temperature=0,
)
result = json.loads(resp.choices[0].message.content)
return float(result.get("score", 0))
with open("raw_dataset.jsonl") as f:
raw = [json.loads(line) for line in f]
# Keep only examples with a quality score >= 4
filtered = [ex for ex in raw if score_example(ex) >= 4]
with open("filtered_dataset.jsonl", "w") as f:
for ex in filtered:
f.write(json.dumps(ex) + "\n")
print(f"Kept {len(filtered)}/{len(raw)} examples ({100*len(filtered)/len(raw):.0f}%)")When synthetic beats human data
Synthetic data is not always better — but there are clear conditions where it reliably outperforms human-labeled examples.
- You need >1,000 examples fast
- Task is well-defined with clear right/wrong answers
- Data is sensitive or legally restricted
- Edge cases are rare in natural data
- You need consistent labeling rubrics
- Budget for human annotation is tight
- Task requires genuine lived experience
- Cultural nuance matters deeply
- High-stakes outputs (medical, legal)
- Catching model blind spots and biases
- Final eval and red-teaming data
- Open-ended creative or preference tasks
Research comparing the two reveals an interesting pattern: for small datasets (under ~100 examples) performance is similar. As dataset size grows, human-labeled data tends to improve more steadily, while purely synthetic data plateaus sooner for subjective or nuanced tasks. The best practical approach is a hybrid: generate a large synthetic corpus for coverage, then sprinkle in a smaller set of human-verified examples to anchor quality and catch model blind spots.
There is one domain where synthetic data consistently wins outright: code. Correct code can be verified mechanically by running it. This means synthetic code examples can be filtered for correctness automatically — no human judgment needed. Models like DeepSeek-Coder and StarCoder2 were trained heavily on synthetically generated code problems and verified solutions, and they match or exceed models trained on far larger unfiltered human corpora.
Putting it all together
Below is the complete minimal pipeline: generate with diversity, filter with an LLM judge, format for supervised fine-tuning, then run the training step. This pattern works with any chat-format model that accepts a JSONL fine-tuning file.
import json
def to_chat_format(ex: dict) -> dict:
"""
Convert a {input, output} example to the JSONL chat format
expected by most fine-tuning APIs (OpenAI, Together, etc.).
"""
return {
"messages": [
{"role": "system", "content": "Classify the intent of the customer message."},
{"role": "user", "content": ex["input"]},
{"role": "assistant", "content": ex["output"]},
]
}
with open("filtered_dataset.jsonl") as f:
filtered = [json.loads(line) for line in f]
# Shuffle and split 90/10 train/validation
import random
random.shuffle(filtered)
split = int(len(filtered) * 0.9)
train, val = filtered[:split], filtered[split:]
for name, data in [("train.jsonl", train), ("val.jsonl", val)]:
with open(name, "w") as f:
for ex in data:
f.write(json.dumps(to_chat_format(ex)) + "\n")
print(f"Train: {len(train)} examples | Val: {len(val)} examples")With train.jsonl ready you can call the fine-tuning endpoint of any provider that supports supervised fine-tuning (OpenAI, Together AI, Fireworks, or a local Hugging Face training loop with LoRA). The validation split is critical: it lets you watch the loss on real-distribution examples and stop training before the model overfits to the synthetic patterns.
Going deeper
The pipeline above covers supervised fine-tuning. Here is what the frontier looks like and where synthetic data goes from here.
Self-Instruct and Magpie
Self-Instruct (Wang et al., 2023) is the formalization of the bootstrap approach: start with a small set of seed tasks, prompt the model to generate new tasks that are different from the seeds, filter for diversity and quality, then add surviving examples back to the seed pool and repeat. This compound loop produced the original Alpaca and Vicuna training sets and remains the foundation of most open fine-tuning datasets.
Magpie (ICLR 2025) pushed further: it discovered that instruction-tuned models can generate training data spontaneously when you supply only the pre-query template and let the model complete it. No seed tasks required. A single Llama-3-Instruct model produced 4 million diverse instruction–response pairs this way; 300,000 filtered examples from that set outperformed every human-curated open dataset on standard benchmarks.
Active learning: not all synthetic examples are equal
The most efficient fine-tuning pipelines do not generate a large static dataset and filter it once. They use active learning: after an initial training pass, run the current model on a diverse validation set, identify the examples it gets wrong, generate more synthetic data specifically targeting those failure modes, and retrain. This iterative loop dramatically reduces the number of total examples needed to reach a quality target.
Synthetic reasoning traces
For tasks that benefit from chain-of-thought prompting, you can generate synthetic reasoning traces, not just final answers. A strong reasoning model works through the problem step by step; the full trace (with the final answer) becomes the training target. The student model learns to reason through problems the same way — this is why small open models have become surprisingly competent at math, science, and code: they were trained on the worked solutions of much larger reasoning models.
Verifiable rewards: the code special case
The cleanest form of synthetic data quality filtering applies to code and math: generated outputs can be automatically verified by running a test suite or checking an equation. This removes the need for an LLM judge entirely and eliminates the risk of the judge being fooled by plausible-sounding wrong answers. If your task has a computable ground truth — SQL queries with expected outputs, regex patterns, numerical answers — exploit that and use execution-based filtering instead of model-based scoring.
FAQ
Can I use synthetic data to fine-tune a model without any human-labeled examples?
Yes, but you should still use real human-written inputs for your evaluation set, even if the training data is fully synthetic. This is essential — a model that looks good on synthetic evals often has an unexpected gap when it meets real user traffic. Human examples in your val split catch that gap before it reaches production.
How many synthetic examples do I need to fine-tune a model?
For a narrow, well-defined task (e.g., intent classification, format conversion, structured extraction), 1,000–5,000 high-quality filtered examples are often enough to produce a strong fine-tune. More is not always better — a smaller, carefully filtered set consistently outperforms a larger unfiltered one. Start with 1,000, evaluate, and generate more only if the model still struggles on specific failure modes.
What is the difference between synthetic data generation and model distillation?
They overlap but serve different goals. Synthetic data generation is about building a labeled training dataset using an LLM. Model distillation is a specific strategy for compressing a large model into a smaller one, which often uses synthetic data as its training set. Distillation is one downstream use case for synthetic data; other use cases include supervised fine-tuning, RLHF preference data, and evaluation set augmentation.
How do I avoid repetition and distribution collapse in synthetic datasets?
Use a high sampling temperature (0.8–1.0), rotate through a diverse seed list of domains or topics, prompt explicitly for variety ("generate an example that is different from all previous ones"), and use multiple teacher models if possible. After generation, run a semantic deduplication step — embedding the examples and removing near-duplicates with cosine similarity — to clean up any remaining repetition.
Is it legal to use commercial LLM outputs as training data?
It depends on the provider's terms of service. Several major commercial providers explicitly forbid using their model outputs to train competing models. Always check the terms before running a generation pipeline at scale. Using a permissively licensed open model (e.g., Llama 3, Mistral, Qwen) as the teacher sidesteps the legal issue entirely and is the safe default for experimentation.
What is an LLM judge and how is it used in quality filtering?
An LLM judge is a second model (often a cheaper one) that rates each generated example on dimensions like correctness, clarity, and uniqueness, returning a numeric score. You discard examples below a threshold (typically 3 or 4 out of 5) before training. The judge does not need to be the same model that generated the data — a smaller, cheaper model can score examples faster and at lower cost, provided the task rubric is well-specified in the judging prompt.