youtube.nixfred.com nixfred.com

Let's build the GPT Tokenizer

Karpathy builds a byte pair encoding tokenizer from scratch and argues that tokenization is the root of a startling amount of LLM weirdness: bad spelling, failed string reversal, worse performance in non English languages, arithmetic errors, the Python indentation problem in GPT-2, and the unspeakable SolidGoldMagikarp tokens. It starts at Unicode and UTF-8, works up through the BPE merge algorithm, the GPT-2 and GPT-4 splitting patterns, special tokens, and SentencePiece, then closes on how to choose a vocabulary size.

Published Feb 20, 2024 11 min read Added Jul 30, 2026 Open on YouTube →

At a glance

Andrej Karpathy opens this one with a claim that sounds like an exaggeration and turns out to be an understatement: tokenization is at the heart of much of the strange behavior people attribute to large language models being dumb. Not the model. The step before the model.

Over two and a bit hours he builds a byte pair encoding tokenizer from an empty file, starting at Unicode code points and UTF-8, working through the merge algorithm, then through the details that production tokenizers add: the regex splitting patterns that GPT-2 and GPT-4 use to stop merges from crossing category boundaries, special tokens, and SentencePiece, the different library that Llama and much of the open ecosystem use. The companion code is minbpe.

The payoff is diagnostic. Once you can see what the tokenizer did, a long list of famous LLM failures stops being mysterious.

The deep explanation

The list of crimes

Karpathy front loads the motivation with a catalogue of failures that all trace back to this one preprocessing step:

Starting at the bottom: Unicode and UTF-8

Text in Python is a sequence of Unicode code points, of which there are around 150,000 covering every script and emoji. The naive approach would be to make each code point a token. Two problems: the vocabulary is enormous, and the standard keeps changing, so the vocabulary would not be stable.

The alternative is to encode the text into bytes. UTF-8 is the right choice, being the dominant encoding on the web and backwards compatible with ASCII, whereas UTF-16 and UTF-32 waste space with padding for the common case. Now the vocabulary is a tidy 256 symbols and every possible string can be represented.

But a byte level sequence is far too long. The context window is a fixed number of tokens, and if every character costs one or more, the model sees a very short piece of text. You want a middle ground: a vocabulary bigger than 256 that compresses common sequences into single symbols. That is precisely what byte pair encoding provides.

The byte pair encoding algorithm

The algorithm is simple enough to state in three lines and Karpathy implements it live:

  1. Count every adjacent pair of symbols in the training text.
  2. Take the most frequent pair and mint a new symbol for it, appending to the vocabulary. Replace every occurrence.
  3. Repeat until the vocabulary reaches the size you want.

Each merge shortens the sequence and grows the vocabulary by one. Run it 50,000 times starting from 256 bytes and you get a GPT-2 sized vocabulary. Encoding new text means applying the learned merges in the order they were learned. Decoding means walking the merges backwards to bytes and then decoding the bytes as UTF-8, with a note that you have to handle invalid byte sequences, usually by replacing them rather than crashing.

Karpathy is careful about a distinction people blur: the tokenizer is a completely separate object from the language model, with its own training set and its own training run. It is a translation layer between text and token sequences, and the model never sees text at all.

start: raw UTF-8 bytes, vocabulary = 256 a a b d a a b a c most frequent pair: (a, b)

merge 1: ab becomes token 256 a ab d a ab a c next most frequent: (a, ab)

merge 2: a + ab becomes token 257 aab d aab a c 9 symbols became 5, vocabulary grew by 2 Every merge trades sequence length for vocabulary size. Run it 50,000 times and you have GPT-2.

Figure 1. Byte pair encoding on a toy string. The compression is entirely statistical: the tokenizer learns whatever was frequent in its own training corpus, which is why a tokenizer trained mostly on English carves other languages into far more pieces.

What production tokenizers add

A pure BPE run over raw text produces merges nobody wants. Karpathy walks through the GPT-2 tokenizer's regex splitting pattern, published with the GPT-2 code, which pre splits the text into chunks before any merging happens. Merges are only ever allowed within a chunk. The pattern separates letters, numbers, punctuation, and whitespace, and it deliberately attaches a leading space to the following word, which is why " hello" is the common token and "hello" is the rarer one.

Two consequences worth noting. The pattern stops merges like "dog." with the period attached, which would otherwise create hundreds of near duplicate tokens for every word followed by every punctuation mark. And it caps numeric merges, though GPT-2 did this imperfectly, which is part of why arithmetic was so uneven.

GPT-4's tokenizer, shipped in tiktoken, changes the pattern in visible ways: the case handling in the regex is made explicit rather than relying on case insensitivity, numbers are limited to at most three digits per token, and whitespace is grouped rather than being one token per space. The vocabulary roughly doubles to about 100,000. Karpathy notes that the exact reasoning behind these choices was never published, so parts of it are reverse engineered from behavior.

Then there are special tokens, which are not produced by merging at all but inserted by the surrounding code: an end of text marker to separate documents in training, and in chat models a set of markers that delimit turns and roles. These get added to the vocabulary with their own ids and are handled by string matching before encoding. He points out the security dimension: if user text containing a special token string is not escaped, a user can inject a turn boundary into a conversation, which is a real class of prompt injection.

SentencePiece, and why the open models differ

SentencePiece is the other library in wide use, chosen by Llama and many open models. Karpathy is direct about it: it does the same job with a much larger and more confusing configuration surface, and its defaults come from a machine translation lineage rather than a language modeling one.

The essential difference is order of operations. tiktoken encodes text to UTF-8 bytes and then merges bytes. SentencePiece merges Unicode code points directly and has a byte fallback option for characters it has never seen, which can be turned off, in which case rare characters map to an unknown token. It also has a mess of normalization and dummy prefix options inherited from translation systems, plus a character coverage setting. His practical advice is to copy the settings that a known good model used rather than reasoning from the documentation.

Choosing a vocabulary size

The final architectural question is how big the vocabulary should be, and Karpathy treats it as a genuine trade off with pressure in both directions.

Bigger vocabulary means shorter sequences, so more text fits in the context window and each forward pass covers more material. But the output layer has one row per token, so the parameter count and the softmax cost grow with vocabulary size, and each individual token's embedding is trained on fewer examples because occurrences are spread thinner. Push it far enough and rare tokens are undertrained, which is the same failure mode as SolidGoldMagikarp on a smaller scale.

He also covers extending a vocabulary after the fact, which is what happens when you add special tokens for a fine tune or introduce gist tokens for prompt compression. You resize the embedding table and the output layer, initialize the new rows, and typically freeze everything else while training only the new parameters. This is routine, not exotic, and it is how a lot of multimodal work attaches new modalities: new token ids with new embeddings, the same transformer underneath.

SymptomTokenization causePractical fix
Cannot count lettersWords are single opaque tokensHave the model split into characters first, or use code
Bad arithmeticDigit groupings ignore place valueUse a calculator tool or a code interpreter
Weak in other languagesFewer merges learned for that scriptBudget more tokens, or pick a model with a fairer tokenizer
Poor Python in GPT-2One token per space in indentationFixed in GPT-4 by grouping whitespace
Odd output after a trailing spaceSpace belongs to the next tokenDo not end prompts with whitespace
SolidGoldMagikarp behaviorToken in the vocabulary, untrained embeddingTrain the tokenizer and the model on the same data

Key takeaways

Where this sits in the LLM Learning track

This opens the build part of the track. It comes before the GPT-2 reproduction because tokenization is the first thing in the pipeline and the easiest to get quietly wrong, and because it retroactively explains several of the model behaviors described in the deep dive at the start of the track.

Resources mentioned

About this page

This reconstruction was built from the public record of the video rather than from its caption track, because the machine that assembled it had no network route to YouTube. The build order, the algorithm, and the failure catalogue are here; the verbatim transcript, the timestamped chapter map, and pull quotes are not, and those get backfilled on the next run from a machine that can reach the source. Until then, watch it on YouTube alongside this page, with minbpe open in the other window.