Transformers and language models
Byte-pair encoding
BPE builds a vocabulary by greedily merging the most frequent adjacent pairs, starting from bytes so every Unicode string is representable.
Byte-pair encoding (BPE) builds a vocabulary by greedy merges. Start from a small alphabet, count adjacent pairs in the corpus, and repeatedly fuse the most frequent pair into a new symbol until you hit a target size. Encoding a new string applies those learned merges in order.
Concept
Byte-level BPE (GPT-2 style) starts from all 256 bytes, so any Unicode string is representable without an unknown token. Merges then build on top of those bytes.
The big idea
- Start with a base alphabet. Byte-level BPE starts from 256 bytes.
- Count adjacent symbol pairs in the training corpus (after a pre-tokenizer splits on spaces and punctuation).
- Merge the most frequent pair into a new symbol. Add it to the vocab.
- Repeat until you hit target size
V(for example 50,257 for GPT-2 including specials).
Classic BPE on Unicode characters struggles with large character sets and leftovers. GPT-2 maps raw UTF-8 bytes to 256 base tokens, then learns merges. Any emoji, CJK character or binary-ish junk still encodes. Decoding must reverse bytes carefully (the famous GPT-2 unicode escape mapping in the vocab file).
Walking through a toy corpus
Toy corpus, pretending characters are already base symbols: aaabdaaabac.
- Pair counts:
aaappears most → mergeaa→A. - Corpus becomes
AabdAabac. Next frequent merge might beab→B, and so on. - After a few merges, common chunks are single tokens. Leftovers stay as short pieces.
Real trainers run this on billions of tokens of text. The rule is identical: frequent neighbors coalesce.
After merges are learned, encoding uses an ordered list (a,b) → new_id:
- UTF-8 encode → bytes → base ids
0…255. - Repeatedly find the pair in the current id sequence that appears earliest in the merge list, merge it, until no known pair remains.
- Map special tokens in a pre-pass so BPE never splits them.
Suppose merges learned ('t','h')→256, then (256,'e')→257 so the is one token. Encoding "the": [t,h,e] → [256,e] → [257]. Encoding "then": [t,h,e,n] → [256,e,n] → [257,n]. Slight spelling changes change token counts non-linearly.
A teaching-scale trainer, same spirit as Karpathy's minbpe:
from collections import Counter
def get_stats(ids: list[int]) -> Counter:
pairs = Counter()
for a, b in zip(ids, ids[1:]):
pairs[(a, b)] += 1
return pairs
def merge(ids: list[int], pair: tuple[int, int], new_id: int) -> list[int]:
out, i = [], 0
while i < len(ids):
if i < len(ids) - 1 and (ids[i], ids[i + 1]) == pair:
out.append(new_id)
i += 2
else:
out.append(ids[i])
i += 1
return out
ids = list(b"aaabdaaabac")
stats = get_stats(ids)
pair = max(stats, key=stats.get)
print(pair, stats[pair])
print(merge(ids, pair, 256))Output:
(97, 97) 4
[256, 256, 97, 98, 100, 256, 256, 97, 98, 97, 99]97 is byte a. The most frequent pair aa merged into id 256.
Naive repeated scans are fine for teaching. Production tokenizers (tiktoken, Hugging Face tokenizers) use optimized data structures.
Common mistakes
- Reimplementing encoding with a different merge order. Priority is the merge list, not "merge whatever is frequent in this string."
- Letting BPE split special tokens. Reserve them and pre-split.
- Upgrading a tokenizer under a trained checkpoint. Embedding rows no longer match.
Check yourself
- Why start from 256 bytes instead of Unicode characters? Answer: Every UTF-8 string is then representable, with no unknown token.
- Merges learned
th→256thenthe→257. How does"then"encode? Answer:[257, n]—themerges, trailingnstays separate.
Key takeaways
- BPE greedily merges frequent adjacent pairs until it hits a vocab size.
- Byte-level BPE covers all Unicode from 256 base bytes.
- Encoding applies learned merges in priority order, not by re-counting on the new string.
- Special tokens must be reserved so BPE never splits them.
Further learning
Next: WordPiece and SentencePiece.
Karpathy builds a GPT-2-compatible tokenizer from scratch:
minbpe (Karpathy)Minimal, clean code for the Byte Pair Encoding (BPE) algorithm commonly used in LLM tokenization. - karpathy/minbpeGitHubRelated
Tokenization
Byte-pair encoding in depth: how vocabularies are built and why tokenization causes strange model behavior.
Why subword tokenization
Characters make sequences too long. Words make the vocabulary unbounded. Subwords sit in the middle, with a fixed vocabulary and a workable sequence length.
WordPiece, SentencePiece and special tokens
WordPiece and Unigram are cousins of BPE with different training criteria. Pre-tokenizers, specials and chat templates decide the actual ID sequence you train on.