hands-on labon a real NVIDIA GPU

Data Preparation for LLM Training

Build a real pretraining/instruction data pipeline: load a raw corpus, apply quality filters, deduplicate, train a BPE tokenizer, and batch-validate on GPU.

time
45 min
checked steps
5
level
Intermediate
setup
None
NCP-GENLNCA-GENLNCP-ADSPart of the AI Engineer Course

$29.99a month with Pro

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

Lab cockpit45 min · 5 stepsSession running
4 / 5 steps passingFormat for instruction tuning and validate on GPU · step 5 of 5
lab.ipynb

Step 5 — Format for instruction tuning and validate on GPU

The previous four steps prepared a pretraining corpus: raw text, cleaned. To fine-tune an instruction-following model you need paired data: an instruction and a response. The most common formats:

[ ]:
instructions = [    {'instruction': 'Summarize the benefits of unit tests.', 'response': 'They catch regressions early and document intent.'},    {'instruction': 'What is gradient descent in one line?', 'response': 'An iterative algorithm that minimizes a loss by stepping against its gradient.'},    {'instruction': 'Explain LoRA in two sentences.', 'response': 'LoRA freezes a pretrained model and trains small low-rank adapters instead. This cuts trainable parameters by 10000x while matching full fine-tune quality on most tasks.'},    {'instruction': 'Give one reason to use fp16 over fp32.', 'response': 'Half-precision halves memory and speeds up matmuls on modern GPUs with minimal quality loss.'},    {'instruction': 'What does a tokenizer do?', 'response': 'It converts text into integer token IDs the model can consume, and back to text on output.'},] def alpaca_format(ex):    return f"### Instruction: {ex['instruction']}\n### Response: {ex['response']}" prompts = [alpaca_format(ex) for ex in instructions]print(prompts[0])
[ ]:
from transformers import AutoTokenizer, AutoModelForCausalLMimport torch hf_tok = AutoTokenizer.from_pretrained('gpt2')# 🐛 If you comment the next line out, the next cell throws ValueError: Asking to pad ...hf_tok.pad_token = hf_tok.eos_token batch = hf_tok(prompts, padding=True, truncation=True, max_length=128, return_tensors='pt')batch = {k: v.to('cuda') for k, v in batch.items()} print(f'input_ids:      {tuple(batch["input_ids"].shape)}  on {batch["input_ids"].device}')print(f'attention_mask: {tuple(batch["attention_mask"].shape)}')print()print('Notice the attention_mask — 0 where we padded, 1 where there is a real token.')print('The model multiplies attention scores by this mask so padding contributes nothing to the output.')
[ ]:
model = AutoModelForCausalLM.from_pretrained('gpt2').to('cuda')model.eval()with torch.no_grad():    out = model(**batch)logits = out.logitsprint(f'logits: {tuple(logits.shape)} on {logits.device}')print(f'  batch_size={logits.shape[0]}, seq_len={logits.shape[1]}, vocab_size={logits.shape[2]}')print()print('If this cell ran without error, your batch is shaped correctly for training.')print('Next stop: Lab 1 (LoRA fine-tuning) — where these exact tensors become the training input.')
[ ]:
lab.check(5)
step 5 of 5, replayed: Format for instruction tuning and validate on GPU. The code, the output and the check are the lab's own.

5 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

    Load and explore the raw corpus

    read this step below ↓

  2. 2

    Quality filter

  3. 3

    Exact-hash deduplication

  4. 4

    Train a BPE tokenizer

  5. 5

    Format for instruction tuning and validate on GPU

Step 1 as it appears in the lab

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

Step 1: Load and explore the raw corpus

The unglamorous truth: ~90% of the work to fine-tune a good LLM is data work. Only ~10% is training. If your data is dirty, noisy, or duplicated, no amount of GPU time or clever loss functions will save you.

Two research findings to anchor everything we'll do:

  • Lee et al., 2022 — Deduplicating Training Data Makes Language Models Better. Found that existing training datasets contain many near-duplicate examples and long repetitive substrings. After deduplication, models emit memorized text 10× less frequently and reach the same accuracy in fewer steps. Meta, Google, and OpenAI now run aggressive dedup as a standard preprocessing step.
  • Zhou et al., 2023 — LIMA: Less Is More for Alignment. Took LLaMA-65B and fine-tuned on just 1,000 carefully curated instruction-response pairs. Human evaluators preferred LIMA's outputs over GPT-4's in 43% of cases. The takeaway: diversity and quality > quantity. Scaling noisy data does almost nothing for alignment.

Context for scale: Llama 3 was trained on ~15 trillion tokens, which is roughly 5× more than Llama 2. Meta reports that the data pipeline — filtering, dedup, categorization — made a larger quality difference than the model architecture changes. See the Llama 3 blog post.


What you'll build in this lab

A real preprocessing pipeline you could apply to a scraped corpus before fine-tuning:

  1. Load + explore — You can't clean what you haven't looked at.
  2. Quality filter — Cheap heuristics that remove the worst 20–40% of samples.
  3. Deduplication — Exact-hash dedup. We'll demonstrate a subtle normalization bug and let you fix it.
  4. Train a BPE tokenizer — The same algorithm GPT-2, GPT-4, and Llama 3 all use. You'll implement one piece yourself.
  5. Format for instruction tuning + validate on GPU — Assemble a batch, push to CUDA, run a real forward pass.

Throughout you'll see # 🐛 Common mistake cells. Read them — they teach the sharp edges of data prep that will absolutely bite you in a real fine-tuning project.


Golden rule of data work: look before you filter. Running filters without knowing your data's shape is how you accidentally discard 95% of your corpus because of an invisible assumption (wrong encoding, unexpected separators, language you didn't know was in there).

We'll use wikitext-2-raw-v1 as a stand-in for a collected corpus. Real-world scrapes (CommonCrawl, Dolma, RedPajama) have the same shape problems at a much larger scale. See the HuggingFace datasets hub for the public corpora everyone starts from.

Do: Load the dataset and build raw_texts — a list of non-empty strings from the text column. Then look at the length distribution before doing anything else.

from datasets import load_dataset

ds = load_dataset('Salesforce/wikitext', 'wikitext-2-raw-v1', split='train')
raw_texts = [t for t in ds['text'] if t.strip()]

print(f'Raw corpus: {len(raw_texts):,} non-empty lines')
print(f'Example line: {raw_texts[10][:200]!r}')
# Length distribution — is the data mostly headers, mostly paragraphs, or a mix?
import statistics
lengths = [len(t) for t in raw_texts]
print(f'min: {min(lengths)}  |  p50: {int(statistics.median(lengths))}  |  p95: {int(sorted(lengths)[int(len(lengths)*0.95)])}  |  max: {max(lengths)}')
print(f'Lines under 50 chars: {sum(1 for L in lengths if L < 50):,} ({100*sum(1 for L in lengths if L < 50)/len(lengths):.1f}%)')
print(f'Lines over 5000 chars: {sum(1 for L in lengths if L > 5000):,}')

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

  • Comfortable with Python lists, dicts, and basic string manipulation
  • Familiarity with Hugging Face datasets and tokenizers
  • Basic understanding of PyTorch tensors and GPU tensors

Exam domains covered

Data Analysis and VisualizationLLM Fundamentals & ArchitectureExperimentation

Part of the LLM training and fine-tuning track

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

Questions about this lab

Why not just use dataset.filter() and dataset.map() everywhere?

You can, and at production scale you should. The lab implements filter and dedup by hand so you see the rules, 100-char minimum, alphanumeric ratio, normalization before hashing, rather than burying them in a helper. Once you've written the plain-Python version, swapping to Arrow-backed datasets operations is a trivial speed optimization. Getting the rules wrong, on the other hand, silently drops the wrong samples and is invisible until your loss curve misbehaves two days into training.

Why is exact-hash deduplication 'only a floor'?

Because it catches byte-identical strings and nothing else. At web scale, near-duplicates dominate: boilerplate headers and footers, templated product descriptions, mirrored articles with one-word edits, paraphrased Wikipedia. Production pipelines layer on MinHash + LSH (for shingle-based near-dupe detection), SimHash, or semantic dedup via embedding clustering. Exact-hash is still worth running first because it's cheap and removes the obvious case, but on its own it leaves ~30-50% of effective duplicates in place on common crawl-type corpora.

Why a ByteLevel pre-tokenizer for BPE?

ByteLevel is what GPT-2 pioneered and what most modern open LLMs still use. It maps every byte to a printable Unicode character before BPE runs, which guarantees the tokenizer never sees an 'unknown' symbol at inference, any input is representable, because every input byte has a mapping. Alternatives (Whitespace pre-tokenizer, SentencePiece BPE, Unigram) handle OOV differently and change downstream behavior. ByteLevel gives you coverage + reasonable compression + compatibility with the GPT-2/Llama tokenizer family, which is why it's the default.

Why does tokenizer vocab size matter so much?

Because it's fixed for the life of the model, every later decision (embedding table size, token budget, maximum context cost, per-step compute) is downstream of it. Too small: the tokenizer splits common words into many pieces, inflating your sequence lengths and shrinking effective context. Too large: the embedding and output head balloon, wasting parameters on rare tokens. The 8,000 setting in this lab is small on purpose (fast to train, easy to inspect); real LLMs use 32k-128k depending on language coverage.

Why feed the instruction batch through GPT-2 at the end?

To prove the pipeline actually produces tensors a real model can consume without crashing. Step 5 checks that input_ids and attention_mask have matching shapes, live on CUDA, and that the forward pass returns logits with the right batch/seq/vocab dimensions. This is the smallest-possible end-to-end sanity check: a pipeline that processes ten million samples but fails to produce a valid batch tensor is worse than useless, and catching it at sample size 4 is cheap.

What's genuinely missing from this pipeline compared to Llama 3 / DBRX scale?

Language ID (keep English-only or build language-aware mixes), near-duplicate detection (MinHash/LSH), toxicity and PII scrubbing, source-weighted mixing (code, web, books at tuned ratios), per-source quality classifiers (the Llama 3 paper trained dedicated filters), and proper shuffling across shards. The reflection step pushes you to name which of these you'd build first and why, and the honest answer is usually 'near-duplicate dedup' because it's the biggest silent-quality win at scale.

What you'll build in this LLM data-prep lab

Data prep is the unglamorous work that actually determines how good an LLM turns out, the Llama 3 paper spends more pages on filtering, dedup, and source mixing than on architecture, and DBRX's post-mortem pinned most of its quality gain on data pipeline improvements rather than model changes. In about 45 minutes on a real NVIDIA GPU we provision, you'll build a working pretraining/instruction data pipeline end-to-end (load, filter, dedup, train a BPE tokenizer, batch-validate on GPU) and come out with concrete answers to the questions that actually matter in production: why is exact-hash dedup only a floor, why is the tokenizer the one decision you can never walk back, and which stage in your pipeline is silently dropping the samples you most wanted to keep.

Technically you'll stream a 1,000+ sample Hugging Face corpus (wikitext, c4, or similar), apply a quality filter on minimum length and alphanumeric ratio (with a deliberately inverted inequality you have to fix, the exact mistake that has shipped to production at real shops), normalise and md5-hash for exact dedup while keeping the original text untouched, then train a real tokenizers.Tokenizer(models.BPE()) with a ByteLevel pre-tokenizer and BpeTrainer(vocab_size=8000) via train_from_iterator. The final step batches prompt/response pairs on CUDA with padding + attention masks and runs a forward pass through GPT-2 to confirm the pipeline produces tensors a real model can consume without crashing. The pointed lesson is scale: exact-hash dedup leaves ~30-50% of effective duplicates in place on Common Crawl-type data, which is why production pipelines layer MinHash + LSH (shingle near-dupes), SimHash, or embedding-based semantic clustering on top. You'll also see why ByteLevel beats Whitespace or SentencePiece BPE for modern LLMs (every byte is representable, so OOV is impossible at inference), and why vocab size is fixed for the life of the model.

Prerequisites: comfort with Python string handling, Hugging Face datasets and tokenizers, and basic PyTorch tensors. The sandbox has HF libraries, the BPE trainer, and GPT-2 weights preinstalled. Search-intent hooks: "how to prepare data for LLM training", "deduplicate pretraining corpus", "train BPE tokenizer from scratch", "MinHash LSH dedup", "instruction tuning data format", the lab and the reflection cover each. The grader is strict where it matters, re-hashing the dedup output to prove all survivors are unique, confirming tokenisation actually compresses versus a whitespace split, and enforcing GPU-resident tensors with a realistic vocab dim before you move on.