A language model never reads a word.
It reads numbers — integers pointing to entries in a lookup table.
The algorithm that converts your sentence into those numbers
is the tokenizer.
Change the tokenizer and you change everything:
what the model can represent, how much context it can hold,
how fairly it treats different languages.
Tokenization is where language meets math.
It deserves more attention than it gets.
01 — Why Not Just Split on Spaces?
The most obvious approach to tokenization is word-level splitting: one token per word. "The cat sat" → ["The", "cat", "sat"]. Simple, interpretable — and broken in four specific ways that matter enormously for large-scale language models.
Imagine building a dictionary for a new language assistant. If you include one entry per word, you need entries for "run", "runs", "ran", "running", "runner", "runners", "runnable" — seven entries for one root concept. Now multiply by every verb, every noun, every adjective. Your dictionary explodes to millions of entries, most of which appear rarely — so the model never learns them properly.
Subword tokenization is the insight that you can do better: keep common words whole, but split rare or complex words into meaningful pieces — "un" + "happi" + "ness". Now your dictionary stays compact and you can still represent any word, even ones you've never seen, by combining familiar pieces.
The four failure modes of word-level tokenization:
The solution, developed independently by several teams between 2015 and 2018, is subword tokenization: split text at the level of meaningful character sequences, not words. Keep common words whole. Break rare words into smaller reusable pieces. Never produce an unknown token, because at the absolute minimum you can fall back to individual characters or bytes. Three algorithms dominate: BPE, WordPiece, and SentencePiece.
02 — Byte-Pair Encoding (BPE): How GPT Tokenizes
BPE (Sennrich et al., 2016) was originally a data compression algorithm from 1994, repurposed for NLP. The key idea: start with individual characters, then iteratively merge the most frequent adjacent pair until you reach your target vocabulary size.
Imagine you're a court stenographer developing a shorthand system. You start by writing every letter individually. Then you notice "th" appears constantly — so you create a symbol for "th". Then "the" is extremely common, so you create a symbol for that. Then "ing", then "tion". You keep creating new shorthand symbols for the most frequent sequences until your system is as compact as possible while still covering everything.
BPE does exactly this: it analyses your training corpus and merges the most common character pairs into single tokens, over and over, until the vocabulary reaches your desired size (typically 32,000–100,000 tokens).
BPE Algorithm: Step by Step
Let's trace BPE on a tiny corpus: "low low low lowest newer newer" (word frequencies matter, not just words).
Initial state: split every word into characters + end-of-word marker </w>. Count all adjacent pairs.
Pair frequencies: (e,r): 2 (w,e): 3 (l,o): 4 (o,w): 4 (e,s): 1 (s,t): 1 ...
New pairs: (lo,w): 4 (w,e): 3 ...
New pairs: (low,</w>): 3 (low,e): 1 (w,e): 3 (e,r): 2 ...
After enough merges: "low", "er", "new" emerge as tokens
This continues until the vocabulary reaches the target size. The algorithm produces a merge table — an ordered list of merge rules. At inference time, you apply the same merges in the same order to tokenize new text. The result is deterministic: the same string always produces the same token sequence.
Byte-Level BPE: How GPT-2 and GPT-4 Do It
GPT-2 introduced a crucial improvement: byte-level BPE. Instead of starting with Unicode characters, it starts with the 256 possible bytes. This guarantees the tokenizer can handle any text in any language, any encoding, any emoji — because every string is ultimately a sequence of bytes. The vocabulary never needs an [UNK] token. "مرحبا" (Arabic for "hello"), 你好 (Chinese), and 🌙 are all representable without special handling.
GPT-2/3/4 and LLaMA-2 all use byte-level BPE. The vocabulary sizes:
Worked Example: Tokenizing "unhappiness"
With a GPT-style BPE tokenizer trained on English text:
3 tokens. The splits follow frequency — "happ" and "iness" are common English substrings that appeared often enough to get merged into single tokens during BPE training.
Compare the same word in different tokenizers:
| Tokenizer | "unhappiness" | Token count | Why different |
|---|---|---|---|
| GPT-4 (cl100k) | un | happ | iness | 3 | Large vocab (100k) — frequent substrings merged early |
| BERT (WordPiece) | un | ##happy | ##ness | 3 | ## prefix marks continuation; "happy" absorbed "app" |
| LLaMA-2 (BPE 32k) | un | hap | p | iness | 4 | Smaller vocab — fewer merges completed, "happ" not merged |
| LLaMA-3 (BPE 128k) | un | happiness | 2 | Much larger vocab — "happiness" frequent enough to be one token |
This directly affects cost: GPT-4 charges per token. A document tokenized into 1,000 tokens by cl100k might tokenize into 1,400 tokens with LLaMA-2's smaller vocabulary — meaning the same text consumes 40% more context window and costs 40% more to process.
03 — WordPiece: How BERT Tokenizes
WordPiece (Schuster & Nakamura, 2012; used in BERT since 2018) is superficially similar to BPE but makes a fundamentally different choice: instead of merging the most frequent pair, it merges the pair that maximises the language model likelihood of the training data.
The Core Difference from BPE
WordPiece merge criterion: merge(a, b) if count(ab) / (count(a) × count(b)) is maximum
i.e. merge pairs where co-occurrence is high relative to individual frequencies
This is the pointwise mutual information (PMI) of the pair
Why does this matter? BPE will eagerly merge very common characters like "e" and "s" because they appear together often — even if they always appear as parts of different words and the merge isn't linguistically meaningful. WordPiece is more conservative: it only merges pairs that are more likely together than apart, which tends to produce more linguistically coherent subwords.
BPE: pick the person who shows up to the office most often (raw frequency). WordPiece: pick the person whose presence in meetings correlates most strongly with yours — someone who is specifically associated with you, not just someone who happens to be around all the time.
WordPiece makes more selective merges — preferring pairs that are genuinely informationally linked over pairs that are just common.
The ## Continuation Marker
WordPiece marks subword pieces that continue a word with a ## prefix. This is a crucial design choice: it preserves the information about whether a piece starts a new word or continues one.
The ## prefix means "I am attached to the previous token — I don't start a new word." This lets the model's attention mechanism distinguish "play" at the start of a word from "##play" in the middle, even though they map to different token IDs. Without this marker, the model might confuse "play" in "playing" with "play" as a standalone word.
WordPiece Vocabulary Construction
WordPiece starts with all individual Unicode characters in the training data (or a fixed set of bytes), then greedily merges pairs using the PMI criterion. BERT-base uses a vocabulary of 30,522 tokens — smaller than GPT-2's 50,257 because BERT was designed for shorter inputs and understanding tasks rather than open-domain generation. The vocabulary includes:
- 100 unused slots (reserved for domain-specific fine-tuning)
- Special tokens:
[PAD],[UNK],[CLS],[SEP],[MASK] - All printable ASCII characters individually
- ~30,000 subword pieces learned from the training corpus
04 — SentencePiece: How LLaMA & T5 Tokenize
SentencePiece (Kudo & Richardson, 2018) is not a tokenization algorithm in itself — it's a tokenization framework that can run either BPE or Unigram as the underlying algorithm, while solving two problems neither BPE nor WordPiece address well: language independence and whitespace handling.
BPE and WordPiece are like power plugs designed for specific countries — they work great for English text but need adapters for other languages. SentencePiece is the universal travel adapter: it doesn't care about spaces, doesn't need a pre-tokenization step, and works identically on English, Arabic, Chinese, Japanese, and every other language, treating all text as a raw stream of Unicode characters.
The Two Key Innovations
Unigram Language Model — SentencePiece's Alternative Algorithm
SentencePiece can use either BPE or a second algorithm called Unigram. Instead of starting with characters and merging up, Unigram starts with a large vocabulary and prunes it down.
1. Start with a large seed vocabulary (all character n-grams up to length 6, say)
2. Train a unigram language model: P(token) = frequency(token) / total_tokens
3. For each token, compute how much the corpus likelihood drops if you remove it
4. Remove the bottom 10–20% of tokens (lowest impact on likelihood)
5. Repeat steps 2–4 until vocabulary size reaches target
Tokenization at inference:
P(tokenization) = Π P(token_i) — pick the tokenization that maximises this product
Solved efficiently with the Viterbi algorithm
The key advantage of Unigram over BPE: it produces probabilistic tokenization. A given text can be tokenized in multiple valid ways, each with a probability. During training, you can sample from this distribution — exposing the model to multiple segmentations of the same text — which improves robustness and generalisation. BPE is deterministic; Unigram is not.
Who Uses SentencePiece
05 — Worked Comparison: The Same Sentence Across Four Tokenizers
Let's tokenize one sentence through four real tokenizers and see exactly what happens:
"The LLM tokenized the word 'تعلّم' into 5 tokens."
GPT-4 (cl100k BPE, 100k vocab):
17 tokens total — Arabic characters split to individual bytes/chars since Arabic text was rare in GPT-4's training data relative to vocabulary budget
BERT (WordPiece, 30k vocab):
14 tokens — Arabic word collapsed to [UNK] (BERT's base vocabulary is English-focused); "LLM" split as "LL" + "##M"
LLaMA-2 (BPE via SentencePiece, 32k vocab):
18 tokens — Arabic is byte-level but handled without [UNK]; space encoded as ▁ prefix in each token
mT5 (Unigram via SentencePiece, 250k vocab — multilingual):
12 tokens — 250k multilingual vocab keeps Arabic word whole; "tokenized" is one token; most efficient of the four
The same sentence consumed 17, 14, 18, and 12 tokens across four tokenizers. In a model with a 4,096-token context window, this difference compounds: mT5's tokenizer lets you fit ~42% more text than LLaMA-2's. For long documents, retrieval, or any task where context length matters, the tokenizer is not a detail — it's a multiplier on your effective context budget. This is why LLaMA-3 expanded its vocabulary from 32k to 128k: the larger vocabulary tokenizes text more efficiently, especially for code and multilingual inputs.
06 — Tokenization and Fairness: The Multilingual Gap
One of the most important — and often overlooked — consequences of tokenizer design is linguistic fairness. A tokenizer trained predominantly on English text will produce very different token counts for equivalent texts in other languages.
Translating "I love machine learning" into several languages and counting tokens with GPT-3.5's tokenizer (cl100k):
| Language | Text | Tokens | Ratio vs English |
|---|---|---|---|
| English | I love machine learning | 4 | 1.0× |
| French | J'aime l'apprentissage automatique | 8 | 2.0× |
| Spanish | Amo el aprendizaje automático | 7 | 1.75× |
| Arabic | أحب التعلم الآلي | 13 | 3.25× |
| Hindi | मुझे मशीन लर्निंग पसंद है | 19 | 4.75× |
| Japanese | 機械学習が好きです | 14 | 3.5× |
An Arabic user expressing the same thought pays 3.25× more tokens — meaning 3.25× less context window and 3.25× higher API cost for the same amount of meaning. This is not a trivial inconvenience — it makes LLMs practically unusable for many languages at the same cost point as English. Multilingual models like mT5 and LLaMA-3 (with its 128k vocabulary) specifically address this by allocating more vocabulary budget to non-English scripts.
07 — Special Tokens: The Control Layer
Every tokenizer reserves a set of special tokens that carry structural meaning for the model. These are not learned from text — they are inserted by the tokenizer to mark boundaries, roles, and instructions.
| Token | Model | Meaning |
|---|---|---|
[CLS] | BERT | Start of sequence; its final embedding = sentence representation for classification |
[SEP] | BERT | Separator between two sentences in a pair (question / passage, premise / hypothesis) |
[MASK] | BERT | Replaces hidden tokens during MLM pre-training |
[PAD] | BERT / most | Pads shorter sequences to equal length in a batch; always masked in attention |
<|endoftext|> | GPT-2/3 | Marks the boundary between independent documents in training / signals generation stop |
<|im_start|> / <|im_end|> | GPT-4 (ChatML) | Marks the start/end of each message turn in chat format |
<s> / </s> | LLaMA-1/2 | Beginning / end of sequence tokens (SentencePiece convention) |
[INST] / [/INST] | LLaMA-2 chat | Wraps the user's instruction in chat fine-tuned models |
<|begin_of_text|> | LLaMA-3 | New BOS token replacing <s>; signals start of entire context |
<unk> | Most models | Unknown token — should never appear in well-designed subword tokenizers (fallback only) |
Special tokens are critical for chat models. The ChatML format used by GPT-4 structures every conversation as:
You are a helpful assistant.
<|im_end|>
<|im_start|>user
What is tokenization?
<|im_end|>
<|im_start|>assistant
← model generates from here
The model learned during fine-tuning to produce a response after seeing <|im_start|>assistant. The special tokens are literally the format the model was trained to follow.
08 — Head-to-Head: BPE vs WordPiece vs SentencePiece
| Property | BPE | WordPiece | SentencePiece |
|---|---|---|---|
| Merge criterion | Most frequent pair | Highest PMI (likelihood gain) | BPE or Unigram (configurable) |
| Starting point | Characters (or bytes for byte-BPE) | Unicode characters | Raw text → ▁ replaces space |
| OOV handling | ✓ No OOV with byte-BPE | ~ [UNK] for unseen chars | ✓ No OOV (byte fallback) |
| Deterministic? | ✓ Yes — one segmentation | ✓ Yes (greedy longest match) | ~ BPE: yes; Unigram: probabilistic |
| Multilingual support | ~ Depends on training data | ✗ Weak for non-Latin scripts | ✓ Language-agnostic by design |
| Whitespace | End-of-word marker </w> | ## prefix for continuations | ▁ prefix marks word-start |
| Typical vocab size | 32k – 100k+ | 30k (BERT-base) | 32k – 256k |
| Used by | GPT-2/3/4, LLaMA-3, Mistral, Claude | BERT, RoBERTa, DeBERTa, ALBERT | T5, LLaMA-1/2, Gemma, mT5 |
| Reversible? | ✓ Yes (with </w> or ▁) | ✓ Yes (## markers) | ✓ Yes (▁ markers) |
09 — Code: BPE From Scratch + HuggingFace Tokenizers
A minimal BPE implementation in pure Python — the same algorithm that GPT-2 uses under the hood — followed by working HuggingFace tokenizer examples.
from collections import defaultdict, Counter
import re
# ─────────────────────────────────────────────────────────────────────────
# Minimal BPE implementation from scratch
# ─────────────────────────────────────────────────────────────────────────
def get_vocab(corpus: list[str]) -> dict:
"""Convert corpus to character-level vocab with word frequencies."""
vocab = defaultdict(int)
for word in corpus:
# Represent each word as tuple of chars + end-of-word marker
vocab[' '.join(list(word)) + ' </w>'] += 1
return dict(vocab)
def get_pairs(vocab: dict) -> Counter:
"""Count all adjacent symbol pairs across the vocabulary."""
pairs = Counter()
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols) - 1):
pairs[(symbols[i], symbols[i+1])] += freq
return pairs
def merge_pair(pair: tuple, vocab: dict) -> dict:
"""Merge all occurrences of a pair in the vocabulary."""
new_vocab = {}
bigram = ' '.join(pair)
replacement = ''.join(pair)
for word in vocab:
new_word = word.replace(bigram, replacement)
new_vocab[new_word] = vocab[word]
return new_vocab
def train_bpe(corpus: list[str], num_merges: int) -> tuple[dict, list]:
"""
Train BPE on a corpus.
Returns final vocab and the ordered list of merge rules.
"""
vocab = get_vocab(corpus)
merges = []
print(f"Initial vocab size: {len(set(' '.join(vocab.keys()).split()))}")
for i in range(num_merges):
pairs = get_pairs(vocab)
if not pairs:
break
best = pairs.most_common(1)[0][0] # most frequent pair
vocab = merge_pair(best, vocab)
merges.append(best)
if i < 5: # show first 5 merges
print(f" Merge {i+1}: {best[0]!r} + {best[1]!r} → {best[0]+best[1]!r} (freq={pairs[best]})")
return vocab, merges
def tokenize_bpe(word: str, merges: list) -> list[str]:
"""Apply learned BPE merge rules to tokenize a new word."""
tokens = list(word) + ['</w>']
for merge_pair in merges:
i = 0
while i < len(tokens) - 1:
if (tokens[i], tokens[i+1]) == merge_pair:
tokens = tokens[:i] + [merge_pair[0] + merge_pair[1]] + tokens[i+2:]
else:
i += 1
return tokens
# Train on a small corpus
corpus = (["low"] * 5 + ["lowest"] * 2 + ["newer"] * 6 +
["wider"] * 3 + ["new"] * 8 + ["play"] * 4 +
["playing"] * 5 + ["played"] * 3 + ["player"] * 2)
vocab, merges = train_bpe(corpus, num_merges=15)
print(f"\nFinal vocab (top entries):")
for k, v in sorted(vocab.items(), key=lambda x: -x[1])[:5]:
print(f" {k!r}: {v}")
print(f"\nTokenize new words using learned merges:")
for word in ["newer", "playing", "lowest", "newness"]:
toks = tokenize_bpe(word, merges)
print(f" {word:12s} → {toks}")
Initial vocab size: 17 (individual chars + </w>)
Merge 1: 'n' + 'e' → 'ne' (freq=24)
Merge 2: 'ne' + 'w' → 'new' (freq=17)
Merge 3: 'p' + 'l' → 'pl' (freq=14)
Merge 4: 'pl' + 'a' → 'pla' (freq=14)
Merge 5: 'pla' + 'y' → 'play' (freq=14)
Final vocab (top entries):
'new </w>': 8 'play </w>': 4
'new er </w>': 6 'play ing </w>': 5
Tokenize new words using learned merges:
newer → ['new', 'er', '</w>']
playing → ['play', 'ing', '</w>']
lowest → ['low', 'est', '</w>']
newness → ['new', 'n', 'e', 's', 's', '</w>'] ← OOV handled via char fallback
# ─────────────────────────────────────────────────────────────────────────
# HuggingFace tokenizers — comparing BPE, WordPiece, SentencePiece
# pip install transformers
# ─────────────────────────────────────────────────────────────────────────
from transformers import AutoTokenizer
test_sentences = [
"unhappiness is a temporary state",
"def tokenize(text: str) -> list[int]:",
"مرحباً بكم في التعلم الآلي", # Arabic: "Welcome to machine learning"
]
tokenizers = {
"GPT-2 (BPE 50k)": "gpt2",
"BERT-base (WordPiece 30k)": "bert-base-uncased",
"LLaMA-2 (SentPiece 32k)": "meta-llama/Llama-2-7b-hf",
}
for name, model_id in tokenizers.items():
tok = AutoTokenizer.from_pretrained(model_id)
print(f"\n{'─'*60}")
print(f"{name}")
for sent in test_sentences:
tokens = tok.tokenize(sent)
ids = tok.encode(sent)
print(f" [{len(tokens):2d} tok] {tokens}")
────────────────────────────────────────────────────────────
GPT-2 (BPE 50k)
[6 tok] ['un', 'happiness', 'Ġis', 'Ġa', 'Ġtemporary', 'Ġstate']
[14 tok] ['def', 'Ġtoken', 'ize', '(', 'text', ':', 'Ġstr', ')', 'Ġ->', 'Ġlist', '[', 'int', ']', ':']
[29 tok] [many byte-fallback tokens for Arabic]
────────────────────────────────────────────────────────────
BERT-base (WordPiece 30k)
[8 tok] ['un', '##happy', '##ness', 'is', 'a', 'temporary', 'state'] (+ [CLS][SEP])
[16 tok] ['def', 'token', '##ize', '(', 'text', ':', 'str', ')', '-', '>', 'list', '[', 'int', ']', ':']
[5 tok] ['[UNK]', 'في', 'الت', '##علم', 'الآلي'] ← Arabic partially known, partially UNK
────────────────────────────────────────────────────────────
LLaMA-2 (SentPiece 32k)
[7 tok] ['▁un', 'happiness', '▁is', '▁a', '▁temp', 'orary', '▁state']
[15 tok] ['▁def', '▁token', 'ize', '(', 'text', ':', '▁str', ')', '▁->', '▁list', '[', 'int', ']', ':']
[12 tok] ['▁مرح', 'باً', '▁بكم', '▁في', '▁الت', 'علم', '▁الآلي'] ← no UNK, byte fallback
10 — Why This Matters in Practice
Token count = cost + context. Every LLM API charges per token. A sentence in Arabic costs 3–5× more tokens than the same sentence in English on most tokenizers. Code is also expensive: symbols like (, ), ->, [ often each get their own token, so a function signature like def f(x: int) -> str: might use 14 tokens even though it's only 22 characters — about 1.6 characters per token instead of the typical 4.
Tokenizer mismatch causes subtle bugs. If you fine-tune a model with one tokenizer and run inference with another, every token ID maps to a different piece of text. The model produces garbage. This happens more often than you'd expect when swapping base models during development. Always verify tokenizer.vocab_size and a few sample encodings before fine-tuning.
Some tasks need careful token counting. When building RAG pipelines (Day 29), you chunk documents to fit within a context window. If your chunk splitter uses character counts but your model uses token counts, you'll miscalculate and either truncate important context or waste budget. Always measure chunk size in tokens, not characters, and use the model's actual tokenizer for the measurement.
Tokenization reveals model boundaries. GPT-4 tokenizes SolidGoldMagikarp as a single token — a Reddit username that appeared frequently enough in training data to get its own vocabulary entry. There are entire vocabulary slots reserved for tokens that look like random strings but are actually subreddit names, usernames, and domain-specific jargon from the training corpus. Studying your tokenizer's vocabulary is one way to understand what's in your training data.
11 — A Thought to Sit With
Every language model is fundamentally a function over a specific tokenizer's vocabulary. Change the tokenizer — even slightly — and you have a different model, because the meanings of every token ID shift. Yet humans share no such fixed vocabulary: we constantly invent new words, blend languages, coin abbreviations. Our "tokenizer" is learned continuously and never freezes.
The fact that a model trained on byte-level BPE can discuss philosophy, write code, and translate Arabic — despite operating on sequences of integers that represent arbitrary byte chunks — is either a profound statement about the universality of language patterns, or evidence that the content of the tokens matters far less than the relationships between them. Which do you think it is?
12 — Coming Up Next
Tomorrow — Day 28: Fine-Tuning LLMs: Full, LoRA & QLoRA. We've now built the complete transformer, added positional encodings, understood BERT vs GPT, and seen how text becomes tokens. Day 28 answers the practical question every ML engineer faces: you have a pre-trained LLM and a specific task — how do you adapt it? Full fine-tuning, LoRA's low-rank decomposition, 4-bit QLoRA running on a single consumer GPU — with complete code, the math behind each approach, and when to choose which.
13 — Go Deeper
📄 Neural Machine Translation of Rare Words with Subword Units (BPE) — Sennrich et al. (2016)
The paper that brought BPE to NLP. Short (8 pages), clear, and still the canonical reference. Section 3 describes the algorithm; Table 1 shows the vocabulary size tradeoffs.
📄 SentencePiece: A simple and language independent subword tokenizer — Kudo & Richardson (2018)
The SentencePiece paper. Section 3 explains the ▁ whitespace handling; Section 4 covers the Unigram language model algorithm. Essential for understanding LLaMA and T5's tokenizers.
📄 Google's Neural Machine Translation System — Schuster & Nakamura (WordPiece)
WordPiece's first public description in the context of Google's production MT system. Section 3.1 covers the PMI-based merge criterion that distinguishes it from BPE.
🔗 HuggingFace Tokenizers Documentation
The fastest production tokenizer library (Rust-backed, Python bindings). The "Quicktour" and "Training from scratch" guides are the best practical starting points for building custom tokenizers.
🔗 Tiktokenizer — interactive GPT tokenizer visualiser
Paste any text and see exactly how GPT-3.5 / GPT-4 tokenizes it, with colour-coded tokens and counts. The fastest way to develop intuition for tokenization costs and patterns.
- Why not words: word-level tokenization fails due to vocabulary explosion, OOV tokens, multilingual breakdown, and code handling. Subword tokenization splits rare words into reusable pieces while keeping common words whole.
- BPE (GPT-2/3/4, LLaMA-3, Mistral): starts with characters, iteratively merges the most frequent adjacent pair. Byte-level BPE starts with 256 bytes — zero OOV, handles any language or encoding. Deterministic; same text always produces same tokens.
- WordPiece (BERT, RoBERTa): merges the pair with highest PMI — co-occurrence relative to individual frequencies. Produces more linguistically coherent subwords. Uses ## to mark continuation pieces. 30k vocabulary for BERT-base.
- SentencePiece (LLaMA-1/2, T5, Gemma): framework (not algorithm) that replaces spaces with ▁, enabling language-agnostic tokenization. Runs BPE or Unigram underneath. Fully reversible. Used where multilingual support matters.
- Unigram (mT5, ALBERT): starts with a large vocabulary and prunes it by removing tokens with the least impact on corpus likelihood. Enables probabilistic tokenization — multiple valid segmentations with probabilities. Better multilingual training via segmentation sampling.
- Vocabulary size matters: larger vocabulary = fewer tokens per text = more context efficiency. LLaMA-3 expanded from 32k to 128k vocabulary specifically to improve code and multilingual efficiency. GPT-4 uses 100k (cl100k).
- Multilingual fairness: Arabic, Hindi, and Japanese typically require 3–5× more tokens than English in English-centric tokenizers. This means higher API costs and less effective context window for non-English users. Multilingual tokenizers address this with larger, balanced vocabularies.
- Special tokens are structural: [CLS], [SEP], <s>, </s>, <|im_start|> are not learned from text — they're inserted by the tokenizer to mark boundaries and roles. Chat format, instruction following, and sentence-pair tasks all rely on special tokens.
- Practical rule: always measure chunk and context sizes in tokens using the model's actual tokenizer. Character counts are unreliable. Arabic and code are especially token-expensive.
- Connection forward: the token IDs produced by the tokenizer are the input to the embedding layer (Day 22 tokenization intro). The vocabulary size determines the embedding table dimensions and the LM head output size. Everything else in the model operates on these integer IDs.
- What is the difference between BPE and WordPiece, and when would you prefer one over the other?
BPE merges the most frequent adjacent pair at each step (greedy frequency). WordPiece merges the pair that maximises corpus likelihood — equivalent to choosing pairs with the highest PMI. BPE is simpler and faster to train; WordPiece tends to produce more linguistically coherent subwords because it accounts for how "expected" a pair is, not just how common. In practice: use BPE (via byte-level) for generation models where covering the full byte range and avoiding UNK is critical (GPT, LLaMA). Use WordPiece when you need a BERT-compatible tokenizer for understanding tasks and you're working primarily in English or similar scripts. - What is byte-level BPE and why is it important?
Byte-level BPE starts the algorithm with the 256 possible byte values instead of Unicode characters. This guarantees: (1) zero OOV tokens — any string in any encoding is representable as a sequence of bytes; (2) no [UNK] token needed; (3) emojis, Arabic, Chinese, code symbols, and any future Unicode additions are all handled without special cases. GPT-2 introduced this. The trade-off is that rare scripts or emojis that lack dedicated merged tokens are tokenized at the byte level, potentially using many tokens per character — but they're never lost. This is the right trade-off for general-purpose LLMs expected to handle diverse input. - Why does the same word produce a different number of tokens across different models?
Three factors: (1) Vocabulary size — a 100k vocabulary (GPT-4) has completed more merges than a 32k vocabulary (LLaMA-2), so more substrings are consolidated into single tokens. "happiness" may be one token in GPT-4 but three in LLaMA-2. (2) Training corpus — a tokenizer trained on medical text will merge medical terms like "hypertension" into one token; a general-purpose tokenizer may split it into 3–4 pieces. (3) Algorithm differences — BPE, WordPiece, and Unigram make different merge decisions for the same text. These differences have direct consequences: cost (tokens = billing), context efficiency (tokens = context window budget), and model quality (more splits = harder for the model to learn the word's meaning). - You are building an Arabic language chatbot with GPT-4. A conversation that would use 500 tokens in English uses 1,800 tokens in Arabic. What does this mean practically, and how would you address it?
Practically: (1) cost — 1,800 / 500 = 3.6× higher API cost per conversation; (2) context window — only ~2,200 tokens of Arabic conversation fit in an 8k context vs ~8,000 tokens of English, meaning conversation history truncates much earlier; (3) generation speed — more tokens to generate = slower responses. Mitigation strategies: (a) Switch to a model with a multilingual-optimised tokenizer — LLaMA-3 (128k vocab) or mT5 derivatives reduce the Arabic/English ratio to ~1.5–2×. (b) Compress conversation history aggressively — summarise older turns into shorter representations. (c) Pre-translate to English for processing, then translate back — only viable if translation quality is sufficient. (d) Fine-tune the model on Arabic data with a larger vocabulary that allocates more budget to Arabic subwords. The fundamental solution is a better tokenizer; workarounds help but don't eliminate the problem. - What are special tokens, and why does the exact format of special tokens matter for fine-tuning?
Special tokens are reserved vocabulary entries that mark structure rather than content — [CLS], [SEP], <s>, </s>, <|im_start|>, [INST], etc. They are inserted by the tokenizer, not learned from natural text, and their token IDs are fixed in the vocabulary. They matter critically for fine-tuning because: a model learns during pre-training and instruction fine-tuning to respond to specific special token sequences. If you fine-tune using the wrong chat template — for example, using LLaMA-2's [INST][/INST] format but running inference with LLaMA-3's <|begin_of_text|><|start_header_id|> format — the model sees token IDs that don't match the patterns it was trained to respond to. This is a common source of silent failure in fine-tuning: the loss goes down during training, but at inference the model produces repetitive or nonsensical outputs because the prompt format doesn't match. Always use the HuggingFace AutoTokenizer with apply_chat_template() to ensure the correct special token format for the specific model you're training or running inference with.
📄 Neural Machine Translation of Rare Words with Subword Units (BPE) — Sennrich et al. (2016)
The BPE paper. 8 pages, clear algorithm description. The baseline that everything since has improved upon. Section 3 is the key contribution.
📄 SentencePiece — Kudo & Richardson (2018)
The framework paper. Section 3.2 (subword regularisation via Unigram sampling) is the most interesting part — explains why probabilistic tokenization improves model robustness.
🔗 Tiktokenizer — Live GPT tokeniser visualiser
Paste any text, see exactly how GPT-3.5 and GPT-4 tokenize it with colours. Ten minutes here builds more intuition than an hour of reading.
▶ Let's build the GPT Tokenizer — Andrej Karpathy (2024)
YouTube · 2hr 13min · Builds a full BPE tokenizer from scratch in Python, reproducing GPT-4's cl100k tokenizer. The best hands-on treatment of tokenization available anywhere.
🔗 HuggingFace Tokenizers — Docs & Quickstart
Production tokenizer library in Rust with Python bindings. Training a custom BPE tokenizer from scratch takes under 10 lines of code. Essential for any practitioner.