TAI BUI
Back to curriculum
BuildPython

Data Pipelines for Pre-Training

The model is a mirror. It reflects whatever data you feed it. Feed it garbage and it reflects garbage, fluently.

TIME: ~90 minutesPREREQUISITES: Phase 10, Lessons 01-02 (Tokenizers, Building a Tokenizer)

Learning Objectives

  • Build a streaming data pipeline that tokenizes, chunks, shuffles, and batches terabytes of text without loading it all into memory
  • Implement data quality filters (deduplication, language detection, content filtering) used in real pre-training pipelines
  • Create fixed-length training sequences with proper attention masks and document boundary handling
  • Profile pipeline throughput to ensure the dataloader keeps up with GPU training speed

The Problem

You have a tokenizer. Now you need data.

Not a dataset. Not a CSV file. Terabytes of text, cleaned, deduplicated, filtered for quality, tokenized into fixed-length sequences, and served in randomized batches fast enough that your 8-GPU cluster isn't left waiting on the next one.

Most people think training an LLM is about the model architecture. Mostly it isn't. Llama 3 used 15.6 trillion tokens. GPT-3 used 300 billion. DeepSeek-V2 used 8.1 trillion. The architecture across all three is roughly the same: stacked transformer blocks with attention and feedforward layers. The difference in output quality comes largely from the data.

The Chinchilla paper from DeepMind made this precise. For a given compute budget, there is an optimal ratio of model parameters to training tokens. Chinchilla showed that most models in 2022 were badly undertrained: they had too many parameters for the amount of data they saw. A 70B parameter model trained on 1.4 trillion tokens (Chinchilla-optimal) outperformed a 280B model trained on 300 billion tokens (Gopher).

Your data pipeline determines whether your model learns language or learns noise.

Warm-up0/3
1. What does a tokenizer produce when you give it a string of text?Warm-up
A string of text feeding into a box labelled tokenizer, which produces an unlabeled output marked with a question mark.
Schematic setup for the question.
2. A tokenizer splits "unbelievable" into three tokens. What does that say about how its vocabulary is built?Warm-up
The word unbelievable above an arrow into three unlabeled piece boxes, with a prompt asking what needing three pieces says about the tokenizer's vocabulary.
The three-way split is the question's premise.
3. A program must process a log file far larger than the machine's RAM. What is the usual approach?Warm-up
Two bars, a tall one for a log file and a short one for RAM, with a prompt asking how to process a file that does not fit in memory.
Bar heights are schematic, not measured sizes.
Check: The Problem0/2
1. Why can't a pre-training pipeline simply load the whole corpus into memory?Explain
Two bars, a very tall one labelled terabytes of text and a short one labelled gigabytes of RAM, with a prompt asking whether the corpus can fit in memory at once.
Bar heights are schematic; no specific sizes are claimed.
2. What happens when the data pipeline is slower than GPU training speed?Explain
Three GPU training step boxes with dashed gaps between them, labelled as the dataloader not having delivered the next batch yet, and a prompt asking what the GPU is doing in the gap.
Schematic timeline.

The Concept

Where the Data Comes From

Large language models are trained on a mix of sources. Most labs guard the exact composition, but we know enough to understand the categories.

SourceSizeQualityUsed By
Common Crawl~250 TB rawLow (needs heavy filtering)GPT-3, Llama, most open models
Wikipedia~20 GBHighEvery major LLM
GitHub code~1 TB+Medium (lots of duplicates, dead code)StarCoder, CodeLlama, DeepSeek-Coder
Books (BookCorpus, Pile)~100 GBHighGPT-2, GPT-3, early models
Academic papers (arXiv, S2ORC)~100 GBHigh for STEMLlama, Galactica
StackOverflow, Reddit~100 GBMediumLlama, Falcon
Curated web (C4, RefinedWeb)~5 TBMedium-High (pre-filtered)T5, Falcon

Llama 3 disclosed its data mix: roughly 50% web data, 25% code, 13% books and academic papers, 8% math data, and 4% multilingual web data. The total was 15.6 trillion tokens from sources exceeding 5 TB of raw text.

The ratio matters as much as the total size. Too much web data and the model becomes a Reddit parrot. Too little code and it cannot program. Too little math and it fails at reasoning. Getting this mix right is one of the hardest parts of training an LLM, and there is no formula, so you find it by experimenting and evaluating.

Data Cleaning

Raw web data is filthy. A typical Common Crawl dump contains:

  • HTML tags and JavaScript
  • Boilerplate headers, footers, navigation menus
  • Duplicate pages (exact and near-duplicate)
  • Machine-generated spam
  • Personally identifiable information (PII)
  • Low-quality text (lists of keywords, SEO spam)
  • Non-text content encoded as text

How well you clean it decides whether you get a model that generates coherent paragraphs or one that outputs HTML tags mixed with product listings.

Mermaid diagram

Each step eliminates a category of noise:

HTML stripping: Remove all markup. Keep only the visible text content. Libraries like trafilatura and readability extract article content while discarding navigation, ads, and boilerplate.

Language detection: Use fastText's language identification model (lid.176.bin) to classify each document. Filter to your target languages. A document classified as English with less than 0.8 confidence probably is not clean English.

Quality filtering: This is where it gets interesting. RefinedWeb (the dataset behind Falcon) uses a perplexity-based filter: train a small language model on Wikipedia, then score each document. High perplexity means the document is unlike Wikipedia, which usually means spam, keyword lists, or machine-generated content. Documents with perplexity above a threshold get removed.

Deduplication: The most impactful cleaning step. Common Crawl contains enormous numbers of duplicated pages: legal disclaimers, cookie notices, terms of service. Training on duplicates wastes compute and can cause the model to memorize and regurgitate specific passages verbatim.

PII removal: Names, email addresses, phone numbers, social security numbers. Regex-based detection for structured PII, NER models for names in context.

Deduplication with MinHash

Exact deduplication is easy: hash each document, remove duplicates. But near-duplicates are the real problem. Two copies of the same news article with slightly different ads around it are near-duplicates. The content is 95% identical, but byte-for-byte they differ.

MinHash + Locality-Sensitive Hashing (LSH) solves this efficiently.

Mermaid diagram

The idea:

  1. Shingling: Convert each document into a set of n-grams (e.g., 5-grams of words or characters). "the quick brown fox" with 3-word shingles becomes {"the quick brown", "quick brown fox"}.

  2. MinHash: For each document's shingle set, compute k hash values. Each hash value is the minimum hash across all shingles under a different hash function. This creates a fixed-size "signature" that approximates the Jaccard similarity between any two documents.

  3. LSH: Group documents into buckets based on bands of their MinHash signature. Documents in the same bucket are candidate near-duplicates. This avoids comparing every pair, so you only compare candidates.

  4. Verify: For each candidate pair, compute exact Jaccard similarity. Remove one copy if similarity exceeds a threshold (typically 0.8).

The Llama team reported removing approximately 38% of their web data through deduplication. That is a lot. More than a third of Common Crawl is duplicate or near-duplicate content.

Sequence Packing

Your model expects fixed-length input sequences. Your documents are variable length. Some are 50 tokens. Some are 50,000 tokens.

Naive approach: pad every document to the maximum sequence length. This wastes a large share of compute on padding tokens that contribute nothing to learning.

Better approach: pack multiple documents into a single sequence, separated by end-of-sequence tokens. A 2048-token sequence might hold three short documents with [EOS] tokens between them.

Mermaid diagram

The attention mask must be set correctly. Tokens from Document A should not attend to tokens from Document B within the same packed sequence. This requires a block-diagonal attention mask.

Long documents get truncated or split into chunks at sequence boundaries. The split point matters: splitting mid-sentence forces the model to see incomplete thoughts. Some pipelines align splits to paragraph or sentence boundaries when possible.

The Chinchilla Scaling Law

For a fixed compute budget C (measured in FLOPs), the optimal model size N and dataset size D follow:

N_opt ~ C^0.5
D_opt ~ C^0.5

In practice, this means you should scale model size and dataset size roughly equally. A model with 10x more parameters needs roughly 10x more training tokens to reach the same loss.

ModelParametersTraining TokensChinchilla-Optimal?
GPT-3175B300BNo (undertrained 3-4x)
Chinchilla70B1.4TYes (by design)
Llama 270B2TOvertrained (intentionally)
Llama 370B15THeavily overtrained

Llama 3 deliberately violates the Chinchilla law. Meta found that training on far more data than the compute-optimal ratio calls for produces a better model to run at inference. The extra training cost is paid once, and the smaller model is cheaper to serve for as long as you run it. This is sometimes called the "inference-optimal" scaling approach, and it has become the industry standard since 2024.

Check: The Concept0/20
1. What is Common Crawl?Define
A mockup of one raw crawled web page, full of navigation markup, an ad, boilerplate footer text and one real paragraph, with a prompt asking what Common Crawl is.
Page content is a mockup matching the lesson's list of what raw web data contains.
2. What is data mixing in pre-training?Define
A card titled Data mixing, showing the common shorthand Choosing training data, with a blank, dashed box in place of the actual meaning.
The shorthand phrase is from the lesson's Key Terms table.
3. What is a perplexity filter?Define
A card titled Perplexity filter, showing the common shorthand Quality scoring, with a blank, dashed box in place of the actual meaning.
The shorthand phrase is from the lesson's Key Terms table.
4. What does deduplication remove from a raw web corpus?Define
A card titled Deduplication, showing the common shorthand Removing copies, with a blank, dashed box in place of the actual meaning.
The shorthand phrase is from the lesson's Key Terms table.
5. What does a MinHash signature let you do?Define
A card titled MinHash, showing the common shorthand Some hashing trick, with a blank, dashed box in place of the actual meaning.
The shorthand phrase is from the lesson's Key Terms table.
6. What is locality-sensitive hashing used for in deduplication?Define
A card titled LSH, showing the common shorthand Locality-Sensitive Hashing, with a blank, dashed box in place of its actual function.
The shorthand phrase is from the lesson's Key Terms table.
7. What is sequence packing?Define
Three document boxes of different lengths feeding a highlighted box labelled sequence packing with a question mark, which feeds an unlabeled output box.
Document names are placeholders; lengths are illustrative.
8. What does the attention mask do in a packed sequence?Define
A row of seven token chips labelled a1 through a4 and b1 through b3, representing two documents packed into one sequence, with a prompt asking what an attention mask should do at the boundary between them.
Labels are generic stand-ins for two packed documents.
9. What does Chinchilla scaling say about a fixed compute budget?Define
A card titled Chinchilla scaling, showing the common shorthand Train on more data, with a blank, dashed box in place of the actual rule.
The shorthand phrase is from the lesson's Key Terms table.
10. Why does Data Cleaning treat deduplication as such an important step?Explain
Two document boxes, doc 1 and doc 11, both the same Deep learning paragraph from the lesson's sample corpus, with a prompt asking what keeping both copies costs.
Doc 1 and doc 11 are the exact-duplicate pair generate_sample_corpus() actually contains.
11. In Deduplication with MinHash, what happens after two documents land in the same LSH bucket?Explain
Two generic documents, X and Y, sharing the same bucket hash, feeding an arrow into a highlighted box marked with a question mark.
Document names and the hash value are placeholders for the mechanism being asked about.
12. Why does a perplexity filter built on Wikipedia text catch spam and keyword lists?Explain
A reference model trained on Wikipedia text, with a prompt asking why scoring a new document's surprise would catch spam or keyword lists.
Setup matches the lesson's Data Cleaning section on quality filtering.
13. Why does Llama 3 deliberately train far past the Chinchilla-optimal token count?Explain
The lesson's table of GPT-3, Chinchilla, Llama 2 and Llama 3 with their parameters, training tokens and Chinchilla-optimal status, with the Llama 3 row highlighted and a prompt asking why it deliberately overtrains.
Numbers copied from the lesson's Concept section table.
14. Why is padding every document to the maximum sequence length a poor way to build training sequences?Explain
A 50-token document next to a box showing it padded out to 2,048, with a prompt asking what happens to the remaining 1,998 positions.
Document and sequence lengths are the lesson's own Sequence Packing example (50 to 50,000 tokens; a 2,048-token sequence).
15. Why do some pipelines align chunk splits to sentence or paragraph boundaries when possible?Explain
A paragraph of illustrative text with a dashed line marking a chunk boundary partway through a sentence, splitting it before the sentence finishes.
Sentence text is illustrative, written for this picture.
16. Why are documents packed or chunked into fixed-length sequences before training?Explain
Three document boxes of visibly different widths, short, medium and long, with a note that the model expects one fixed input width and a prompt asking how that gap gets resolved.
Bar widths are schematic, not measured token counts.
17. A team plans 100 times more compute and follows the Chinchilla rule. By what factor should model size and token count each grow?Apply
Two boxes, model size and training tokens, each marked with an unknown multiplying factor, above the formulas N_opt proportional to C to the 0.5 and D_opt proportional to C to the 0.5.
Formulas are quoted from the lesson's Chinchilla Scaling Law section.
18. A crawl holds thousands of copies of the same terms-of-service page, each wrapped in different ads. Exact-hash deduplication removes almost none. What should you do?Analyze
Two pages carrying the same legal text but different ad banners, with different exact hashes, and a prompt asking what to do when exact-hash deduplication fails to catch them.
Hash values are placeholders for the general problem described.
19. You have a fixed training budget and the model will answer millions of queries for years. Which plan fits the lesson's reasoning best?Design
A fixed compute budget box next to a deployment box noting millions of queries over years, with a prompt asking how the budget should be spent.
Generic scenario, not naming any specific model.
20. Your code-focused model reasons poorly about math. With a fixed token budget, which step fits the lesson's advice on data mixing?Design
A bar chart of Llama 3's published data mix, 50% web, 25% code, 13% books, 8% math, 4% multilingual, with a note about a code-strong, math-weak model on a fixed budget, and a prompt asking the next step.
Percentages are Llama 3's disclosed mix from the lesson's Concept section, shown for scale.

Build It

Step 1: Text Cleaning

Strip HTML, normalize whitespace, remove non-text content. We will run these steps on a small sample corpus.

python
import re
 
def clean_text(text):
    text = re.sub(r"<[^>]+>", "", text)
    text = re.sub(r"http\S+", "", text)
    text = re.sub(r"[^\x20-\x7E\n]", "", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    text = re.sub(r" {2,}", " ", text)
    return text.strip()
 
def quality_filter(text, min_words=50, max_ratio_caps=0.3, max_ratio_special=0.1):
    words = text.split()
    if len(words) < min_words:
        return False
    caps_ratio = sum(1 for w in words if w.isupper()) / len(words)
    if caps_ratio > max_ratio_caps:
        return False
    special_chars = sum(1 for c in text if not c.isalnum() and not c.isspace())
    if special_chars / max(len(text), 1) > max_ratio_special:
        return False
    return True

The quality filter catches SEO spam (ALL CAPS), machine-generated noise (high special character ratio), and stub pages (too short). These three checks alone remove a surprising amount of garbage from web crawls.

Step 2: MinHash Deduplication

Implement MinHash from scratch with nothing but hashlib.

python
import hashlib
from collections import defaultdict
 
def get_shingles(text, k=5):
    words = text.lower().split()
    if len(words) < k:
        return set()
    return {" ".join(words[i:i+k]) for i in range(len(words) - k + 1)}
 
def minhash_signature(shingles, num_hashes=128):
    signature = []
    for i in range(num_hashes):
        min_hash = float("inf")
        for shingle in shingles:
            h = int(hashlib.sha256(f"{i}:{shingle}".encode()).hexdigest(), 16)
            min_hash = min(min_hash, h)
        signature.append(min_hash)
    return signature
 
def lsh_buckets(signature, bands=16):
    rows_per_band = len(signature) // bands
    buckets = []
    for b in range(bands):
        start = b * rows_per_band
        band_data = tuple(signature[start:start + rows_per_band])
        bucket_hash = hashlib.md5(str(band_data).encode()).hexdigest()
        buckets.append((b, bucket_hash))
    return buckets
 
def deduplicate(documents, threshold=0.8, num_hashes=128, bands=16):
    signatures = []
    shingle_sets = []
    for doc in documents:
        shingles = get_shingles(doc)
        shingle_sets.append(shingles)
        signatures.append(minhash_signature(shingles, num_hashes))
 
    bucket_map = defaultdict(list)
    for doc_idx, sig in enumerate(signatures):
        for band_id, bucket_hash in lsh_buckets(sig, bands):
            bucket_map[(band_id, bucket_hash)].append(doc_idx)
 
    duplicate_pairs = set()
    for bucket_docs in bucket_map.values():
        if len(bucket_docs) < 2:
            continue
        for i in range(len(bucket_docs)):
            for j in range(i + 1, len(bucket_docs)):
                duplicate_pairs.add((bucket_docs[i], bucket_docs[j]))
 
    removed = set()
    for i, j in duplicate_pairs:
        if i in removed or j in removed:
            continue
        s1, s2 = shingle_sets[i], shingle_sets[j]
        if not s1 or not s2:
            continue
        jaccard = len(s1 & s2) / len(s1 | s2)
        if jaccard >= threshold:
            removed.add(j)
 
    return [doc for idx, doc in enumerate(documents) if idx not in removed], len(removed)

The num_hashes=128 and bands=16 parameters control the precision-recall tradeoff. More hashes give more accurate similarity estimates. More bands increase recall (catch more duplicates) at the cost of more false positives. These values work well for typical web text.

Step 3: Tokenize and Pack Sequences

Take the clean, deduplicated text, tokenize it, and pack into fixed-length sequences for training.

python
def tokenize_corpus(documents, tokenizer):
    all_tokens = []
    for doc in documents:
        tokens = tokenizer.encode(doc)
        all_tokens.extend(tokens)
        all_tokens.append(tokenizer.eos_id)
    return all_tokens
 
def pack_sequences(token_ids, seq_length, pad_id=0):
    sequences = []
    attention_masks = []
    for i in range(0, len(token_ids), seq_length):
        seq = token_ids[i:i + seq_length]
        mask = [1] * len(seq)
        if len(seq) < seq_length:
            pad_count = seq_length - len(seq)
            seq = seq + [pad_id] * pad_count
            mask = mask + [0] * pad_count
        sequences.append(seq)
        attention_masks.append(mask)
    return sequences, attention_masks

Step 4: DataLoader for Training

Yield randomized batches of packed sequences. This is what the training loop consumes.

python
import random
 
class PreTrainingDataLoader:
    def __init__(self, sequences, attention_masks, batch_size, shuffle=True):
        self.sequences = sequences
        self.attention_masks = attention_masks
        self.batch_size = batch_size
        self.shuffle = shuffle
 
    def __len__(self):
        return (len(self.sequences) + self.batch_size - 1) // self.batch_size
 
    def __iter__(self):
        indices = list(range(len(self.sequences)))
        if self.shuffle:
            random.shuffle(indices)
        for start in range(0, len(indices), self.batch_size):
            batch_idx = indices[start:start + self.batch_size]
            batch_seqs = [self.sequences[i] for i in batch_idx]
            batch_masks = [self.attention_masks[i] for i in batch_idx]
            yield batch_seqs, batch_masks

Step 5: Dataset Statistics

Compute the numbers that matter: total tokens, unique tokens, compression ratio, document length distribution.

python
from collections import Counter
 
def compute_statistics(documents, token_ids, sequences, tokenizer_vocab_size):
    total_chars = sum(len(d) for d in documents)
    total_tokens = len(token_ids)
    unique_tokens = len(set(token_ids))
    compression_ratio = total_chars / total_tokens
 
    doc_lengths = [len(d.split()) for d in documents]
    avg_doc_length = sum(doc_lengths) / max(len(doc_lengths), 1)
    max_doc_length = max(doc_lengths) if doc_lengths else 0
    min_doc_length = min(doc_lengths) if doc_lengths else 0
 
    token_counts = Counter(token_ids)
    top_tokens = token_counts.most_common(10)
 
    non_pad_tokens = sum(sum(1 for t in seq if t != 0) for seq in sequences)
    total_positions = sum(len(seq) for seq in sequences)
    utilization = non_pad_tokens / max(total_positions, 1)
 
    stats = {
        "total_documents": len(documents),
        "total_characters": total_chars,
        "total_tokens": total_tokens,
        "unique_tokens": unique_tokens,
        "vocab_utilization": unique_tokens / tokenizer_vocab_size,
        "compression_ratio": compression_ratio,
        "avg_doc_length_words": avg_doc_length,
        "max_doc_length_words": max_doc_length,
        "min_doc_length_words": min_doc_length,
        "num_sequences": len(sequences),
        "sequence_utilization": utilization,
        "top_10_tokens": top_tokens,
    }
    return stats

Compression ratio tells you how efficient the tokenizer is on this corpus. English text typically compresses to about 3-4 characters per token. If you see 1.5 characters per token, your tokenizer is splitting too aggressively. If you see 8+, it has learned very domain-specific merges.

Sequence utilization tells you how much of your packed sequences is real data versus padding. Below 90% means your packing is inefficient and you are spending compute on padding tokens.

Check: Build It0/13
1. What is the fertility of a tokenizer on a corpus?Define
Two boxes, total tokens and total words, both marked unknown, above the formula fertility equals total tokens over total words equals a question mark.
The formula shape is the one being asked about; the question distinguishes it from compression ratio.
2. In the MinHash dedup code, what does raising num_hashes do, and what does raising bands do?Explain
Two settings from the lesson's MinHash deduplication code, num_hashes set to 128 and bands set to 16, each with a question mark over the effect of raising it.
Values are the lesson's own defaults in deduplicate().
3. get_shingles("One two three four five") is called with k=3. What does it return?Apply
Five word chips, One, two, three, four, five, above the call get_shingles with k equals 3 and an unknown result.
Capitalization matches the question's input string exactly.
4. A 3-word document goes through deduplicate with the default k=5. What happens to it?Apply
The three-word document This is short feeding get_shingles, with an unresolved box for the resulting shingle set, and a prompt asking what happens to the document inside deduplicate.
Text matches the question; k defaults to 5 in get_shingles.
5. pack_sequences gets 10 token ids with seq_length=4 and pad_id=0. What does it return?Apply
Ten numbered token chips, 0 through 9, above the call pack_sequences with seq_length 4 and pad_id 0, result unknown.
Ten ids match the question's input exactly.
6. PreTrainingDataLoader holds 1000 sequences with batch_size=64. What does len(loader) return?Apply
A loader holding 1,000 sequences with batch size 64, with its length marked unknown.
Numbers match the question exactly.
7. minhash_signature has 128 hashes and lsh_buckets uses bands=16. How many signature values fall in each band?Apply
A signature of length 128 and a bands setting of 16, above the formula rows per band equals 128 integer-divided by 16, result unknown.
Numbers match the question exactly.
8. What does clean_text return for "<b>Buy now</b> http://x.io today"?Apply
A terminal showing the raw input string with HTML tags, a URL and extra spaces, and an unresolved output marked with a question mark.
Input string matches the question exactly; the four operations are named without showing intermediate states.
9. quality_filter gets a 100-word document with 35 all-caps words and few special characters, using the defaults. What does it return?Apply
A document with 100 words and 35 all-caps words feeding quality_filter, with an unresolved True or False result.
Counts match the question exactly.
10. You run the Build It pipeline and Step 5 reports a sequence utilization of 71%. What is the most likely cause?Analyze
A stats readout showing sequence utilization at 71 percent, with a note that the lesson's own sample corpus run reaches about 99 percent, and a prompt asking the likely cause of the lower figure.
71% is the question's own hypothetical figure, not a number this script computed; it differs from the real run's 99.0% utilization.
11. After training on packed sequences from pack_sequences, the model sometimes continues a document using content from the one before it. What explains this?Analyze
The real pack_sequences code: the mask starts as all ones for the real tokens, then gets zeros appended only for padding, with a prompt asking whether this mask tracks document boundaries.
Code copied from the lesson's own main.py, unchanged.
12. Step 5 on a new corpus reports a compression ratio of 1.5 characters per token, where English usually gives 3-4. What is the likely problem?Analyze
A stats readout showing a compression ratio of 1.5 characters per token next to the typical English range of 3 to 4, with a prompt asking what a ratio this low usually means.
1.5 is the question's own hypothetical figure for this scenario.
13. You are turning the Build It pipeline into a real pre-training pipeline. Which set of changes does the lesson point to?Design
Three facts about the toy pipeline from the Author's Note: everything lives in Python lists in memory, the mask marks only padding, and MinHash hashes every shingle with SHA-256, with a prompt asking which changes the lesson points to for real pre-training.
Facts are quoted from the lesson's own Author's Note section.

Use It

Compare With HuggingFace Datasets

Load the same corpus through HuggingFace's datasets library and compare the pipeline speed.

python
from datasets import load_dataset
from transformers import AutoTokenizer
 
ds = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")
 
import time
 
start = time.time()
tokenized = ds.map(
    lambda x: tokenizer(x["text"], truncation=True, max_length=2048),
    batched=True,
    num_proc=4,
)
hf_time = time.time() - start
total_tokens = sum(len(t) for t in tokenized["input_ids"])
print(f"HuggingFace: {total_tokens:,} tokens in {hf_time:.2f}s ({total_tokens/hf_time:,.0f} tokens/sec)")

The HuggingFace pipeline uses Rust tokenizers under the hood and parallel processing across 4 cores. Your pure Python pipeline will be 10-50x slower. That gap is why production teams use compiled tokenizers. The algorithm is the same; the implementation language is what changes.

Ship It

This lesson produces a prompt for validating and debugging data quality in LLM training pipelines. See outputs/prompt-data-quality-checker.md.

Author's Note

I'd run main.py before reading any of the stages closely. It prints a count after each one (cleaning, quality filter, MinHash dedup, tokenization, packing, batching), so you can see where documents and tokens drop out. The corpus comes from generate_sample_corpus(), so the numbers tell you whether each filter fires, not what a real crawl would give you.

The part I'd reread is pack_sequences. It cuts one flat stream of token ids into seq_length chunks, with an EOS id after each document and padding only on the last chunk. The attention mask it returns marks padding, nothing else. That means the block-diagonal mask described in Sequence Packing is not what the code builds: tokens from neighboring documents in the same sequence can attend to each other here. For a toy that's fine. For real pre-training I'd build the document-boundary mask.

I'd also keep the scale in proportion. Everything lives in Python lists, the shuffle is random.shuffle over indices, and the MinHash loop hashes every shingle once per hash function with SHA-256. None of that streams, and none of it is fast. Treat it as the shape of the pipeline, then reach for the HuggingFace version in Use It when throughput matters.

Exercises

  1. Easy: Add language detection to the cleaning pipeline using a simple heuristic (character set analysis). Filter to only English documents and measure how many documents get removed.
  2. Medium: Implement exact deduplication using SHA-256 hashes alongside the MinHash near-deduplication. Compare the number of duplicates caught by each method on a web-scraped corpus.
  3. Hard: Build a perplexity-based quality filter. Train a small bigram language model on Wikipedia text, score each document by perplexity, and remove the bottom 20%. Compare model output quality when training on filtered vs unfiltered data.

Key Terms

TermWhat people sayWhat it actually means
Common Crawl"The internet"A non-profit that crawls the web monthly -- ~250TB raw, the starting point for most LLM training data
MinHash"Some hashing trick"A technique to estimate Jaccard similarity between sets using fixed-size signatures -- enables near-duplicate detection at scale
LSH"Locality-Sensitive Hashing"A method to group similar items into the same bucket -- reduces pairwise comparisons from O(n^2) to near-linear
Sequence packing"Concatenating documents"Fitting multiple documents into fixed-length sequences with proper attention masks -- eliminates padding waste
Chinchilla scaling"Train on more data"For a fixed compute budget, optimal performance requires scaling model size and training tokens roughly equally
Fertility"Tokens per word"Average number of tokens per word -- 1.3 for English in GPT-4, higher for non-Latin scripts
Data mixing"Choosing training data"The ratio of code vs text vs math vs multilingual data -- no formula, requires experimentation
Perplexity filter"Quality scoring"Use a small language model to score documents -- high perplexity means the text is unlike clean reference data
Deduplication"Removing copies"Eliminating exact and near-duplicate documents -- typically removes 30-40% of raw web data
Attention mask"Which tokens to look at"A binary mask that prevents attention across document boundaries in packed sequences

Further Reading

Code

Ships With