Karpathy's Zero to Hero, Lecture 8: Let's build the GPT Tokenizer — notes, code and timestamps

Updated 2026-10-07 · Video: Andrej Karpathy, published 2024-02-20 · 134 min

Watch the video (134 min)

Watch the original video on YouTube

Lecture 8 of Andrej Karpathy’s Neural Networks: Zero to Hero series steps away from the model to look at the step that feeds it. In Lecture 7 the GPT read Shakespeare one character at a time, through a vocabulary of 65 characters. Production models don’t work that way: they read chunks of text produced by a trained tokenizer, usually built with byte pair encoding. Karpathy says up front that tokenization is his least favorite part of working with LLMs, and then spends two hours showing why it can’t be ignored. The lecture builds a byte-level BPE tokenizer from scratch in a notebook, compares it with what OpenAI ships for GPT-2 and GPT-4 and with Google’s SentencePiece (used by Llama 2), and closes by tracing a list of famous LLM failures back to tokens. The code from the lecture grew into the minbpe repository. Next in the series is Lecture 9, where the GPT-2 tokenizer covered here goes into a full model reproduction.

Key takeaways

  1. A lot of LLM weirdness is tokenizer weirdness (4:40). Poor spelling, weaker non-English performance, shaky arithmetic and GPT-2’s trouble with Python all trace back, at least partly, to how text is cut into tokens.
  2. Byte-level BPE is a short algorithm (26:18). Start from the 256 possible UTF-8 bytes, merge the most frequent adjacent pair into a new token, and repeat until you hit the vocabulary size you want.
  3. The tokenizer is a separate model with its own training set (39:23). Its only parameters are the merges table and the vocabulary built from it, and the mix of languages and code in its training data decides how compactly each kind of text is encoded.
  4. Not every token sequence is valid text (47:48). A model can emit bytes that aren’t valid UTF-8, so decoding uses errors="replace".
  5. GPT tokenizers split text with a regex before BPE (1:03:56). Merges can’t cross the boundaries between letters, numbers, punctuation and whitespace; GPT-4’s pattern adds case-insensitive contractions, better whitespace handling and caps number chunks at three digits (1:13:44).
  6. Special tokens skip BPE entirely (1:21:06). Strings like <|endoftext|> are swapped for reserved IDs, and adding new ones means resizing the model’s embedding table and output layer (1:24:43).
  7. Vocabulary size is a trade-off, not a “bigger is better” knob (1:45:02). Larger vocabularies give shorter sequences but cost compute and leave rare tokens undertrained.
  8. A token the model never trained on is a landmine (2:07:20). SolidGoldMagikarp shows what happens when the tokenizer’s data and the model’s data don’t match.

Chapter notes

0:02 – 4:08 Why tokenization, and where we left off

Karpathy opens by calling tokenization “fairly hairy” and full of hidden footguns. He recaps the Shakespeare notebook from the previous lecture (0:33): 65 unique characters, a lookup table from each character to an integer, and an embedding table with one trainable row per token. That row is how the Transformer “sees” a token.

Colab notebook gpt-dev.ipynb from the previous lecture: a cell printing the 65 unique Shakespeare characters, and a cell defining stoi/itos with encode and decode lambdas, printing [46, 47, 47, 1, 58, 46, 43, 56, 43] and 'hii there'
1:19 — The starting point from Lecture 7: a character-level tokenizer with 65 symbols, where encode and decode are just lookup tables.

Real models replace characters with chunks. He points to the GPT-2 paper’s section on input representation (2:36), which settles on a vocabulary of 50,257 tokens and a context of 1,024 tokens (3:07). Everything in an LLM is counted in tokens: the Llama 2 paper, he notes, mentions training on two trillion of them.

The GPT-2 paper 'Language Models are Unsupervised Multitask Learners' open at section 2.2 Input Representation, with the word token highlighted; the right column says the vocabulary is expanded to 50,257 and the context size increased from 512 to 1024 tokens
3:06 — The GPT-2 paper's input-representation section, where the 50,257-token vocabulary and 1,024-token context are stated.

4:08 – 14:45 A tour of tokenizer weirdness

Before writing code, Karpathy lists symptoms that trace back to tokenization (4:40): trouble with spelling and simple string processing, worse results in non-English languages, bad arithmetic, GPT-2’s weakness at Python, warnings about trailing whitespace, the “SolidGoldMagikarp” meltdown, and advice to prefer YAML over JSON. He promises to come back to each one at the end.

Jupyter notebook 'Tokenization' with a heading 'Tokenization :(' and a bulleted list of questions such as 'Why can't LLM spell words?' and 'Why did GPT-2 have more than necessary trouble coding in Python?', each answered 'Tokenization.', followed by the Tiktokenizer link and an example string
5:56 — The list of tokenizer-caused problems he promises to revisit at the end of the lecture.

Then he opens the Tiktokenizer web app (5:41), which tokenizes live in the browser, and walks through examples with the GPT-2 tokenizer:

  • Spaces ride along with words. The leading space is part of the token, so “ is” and “ the” are single tokens.
  • Numbers split arbitrarily (7:21). 127 is one token, 677 is two, and four-digit numbers break up in no consistent way.
  • The same word gets many IDs (8:24). “Egg” at the start of a sentence is two tokens, “ egg” mid-sentence is one, and lowercase and uppercase versions are different tokens again. The model has to learn from data that these are all the same concept.
  • Non-English text is stretched out (9:30). A Korean passage uses many more tokens than its English equivalent, partly because the tokenizer’s own training data was mostly English. Longer sequences mean the model runs out of context sooner.
  • Python indentation is wasteful (11:03). In a FizzBuzz snippet, every indentation space is its own token (ID 220), eating context.
Tiktokenizer web app with the gpt2 encoding selected: the example text (arithmetic, Egg variants, a Korean sentence, a FizzBuzz loop) on the left, colored tokens on the right, token count 300
6:15 — Tiktokenizer with the GPT-2 encoding: the example text costs 300 tokens, and each colored chunk is one token.
Tiktokenizer gpt2 view scrolled to the FizzBuzz code: each leading space of the indentation is shaded as a separate token, and the token ID list below repeats 220 many times
11:24 — Python indentation under GPT-2: every single space is its own token (ID 220), which wastes context.

Switching the app to cl100k_base, the GPT-4 tokenizer, drops the same text from 300 tokens to 185 (12:36). The vocabulary roughly doubled, from about 50k to about 100k, and runs of spaces now merge into single tokens. Karpathy argues part of GPT-4’s better coding comes from this tokenizer design, not just the model. He also flags the cost: more tokens means a bigger embedding table and a bigger softmax at the output, so there’s a sweet spot.

Tiktokenizer with cl100k_base selected and token count 185 for the same example text; the FizzBuzz indentation is now grouped into larger whitespace tokens
12:58 — The same text under GPT-4's cl100k_base drops to 185 tokens, largely because runs of spaces merge.

14:45 – 23:39 From strings to code points to UTF-8 bytes

What is text in Python? A string is an immutable sequence of Unicode code points (15:49), integers the Unicode Consortium assigns to roughly 150,000 characters across 161 scripts. ord("h") returns 104 (16:51); an emoji returns a much bigger number.

Python documentation page 'Built-in Types', section 'Text Sequence Type — str', with the sentence 'Strings are immutable sequences of Unicode code points' and 'Unicode code' highlighted
15:48 — Python's docs: a str is an immutable sequence of Unicode code points.
Jupyter cells: a Korean greeting string with a waving-hand emoji and '(hello in Korean!)', then ord("h") with output 104
16:49 — ord returns a character's code point: 104 for "h".
Jupyter cell [ord(x) for x in "안녕하세요 👋 (hello in Korean!)"] with output starting 50504, 45397, 54616, 49464, 50836, 32, 128075, followed by small ASCII values
17:42 — Code points for the whole string: Korean syllables are in the tens of thousands and the emoji is 128075, while ASCII stays below 128.

Why not use code points directly as tokens? The vocabulary would be about 150,000 entries, and the standard keeps changing, so it isn’t a stable base (17:54). Encodings turn code points into bytes, and UTF-8 is the one to use (18:27): each code point becomes 1 to 4 bytes, and it’s backward compatible with ASCII. Encoding a string to UTF-16 and UTF-32 shows the waste: English text turns into rows of zero bytes (20:01).

Wikipedia UTF-8 article, Encoding section, showing the 'Code point ↔ UTF-8 conversion' table with 1 to 4 bytes per code point range
18:55 — UTF-8's variable-length scheme: one byte for ASCII, up to four bytes for higher code points.
Nathan Reed's blog page 'A Programmer's Introduction to Unicode', dated March 3, 2017, with its opening paragraph and table of contents
19:24 — The Unicode blog post he recommends; its first paragraph later becomes the BPE training text.
Jupyter cell list("안녕하세요 👋 (hello in Korean!)".encode("utf-32")) with output 255, 254, 0, 0, 72, 197, 0, 0, 85, 177, 0, 0 and so on
21:04 — UTF-32 bytes for the test string: a byte-order mark and then rows padded with zeros, which is why UTF-8 is preferred.

Raw UTF-8 bytes alone give a vocabulary of only 256 (21:04). The embedding table would be tiny, but sequences would be very long, and attention over long sequences is expensive. Karpathy says he’d love to feed raw bytes straight into models, and shows a paper that proposes a hierarchical Transformer to do exactly that (22:37), but he doesn’t think it has been proven at scale yet. So the plan is to keep UTF-8 and compress the byte stream with BPE.

arXiv paper 'MEGABYTE: Predicting Million-byte Sequences with Multiscale Transformers' by Lili Yu, Dániel Simig, Colin Flaherty, Armen Aghajanyan, Luke Zettlemoyer and Mike Lewis, dated 19 May 2023, with the abstract and a diagram of local models over patches feeding a global model
23:11 — The MEGABYTE paper's hierarchical Transformer, an attempt to model raw bytes without a tokenizer.

Our note: the byte-level paper on screen is MEGABYTE (Yu et al., 2023), whose abstract claims the viability of “tokenization-free autoregressive sequence modeling at scale”. The blog post Karpathy borrows text from throughout the notebook is Nathan Reed’s “A Programmer’s Introduction to Unicode”.

23:39 – 34:08 The BPE idea, then counting and merging pairs

The Wikipedia example (23:39) uses a four-letter alphabet. In the sequence aaabdaaabac, the pair aa is most common, so it becomes a new symbol Z. Then ab becomes Y, and finally ZY becomes X (24:41). Eleven symbols with a vocabulary of four turn into five symbols with a vocabulary of seven. Each round trades a slightly bigger vocabulary for a shorter sequence. With bytes, the same idea starts at 256 and mints token 256, 257, and so on (26:18).

Wikipedia 'Byte pair encoding' article, Example section: aaabdaaabac becomes ZabdZabac with Z=aa, then ZYdZYac with Y=ab, then XdXac with X=ZY
25:30 — Wikipedia's toy BPE run: three merges shrink an 11-symbol string to 5 symbols while the vocabulary grows from 4 to 7.

The implementation starts on a paragraph of the Unicode blog post (26:48): 533 code points become 616 UTF-8 bytes, because non-ASCII characters take several bytes each. The first helper, get_stats, counts every adjacent pair (28:24). The top pair is (101, 32), which chr reveals to be “e” followed by a space, occurring 20 times: lots of words end in e (29:58).

Jupyter cell taking text from the reedbeta.com Unicode post, encoding it to UTF-8 and converting to a list of ints; output shows the paragraph, length: 533, and the start of the byte list
27:42 — The training paragraph: 533 code points, which become 616 UTF-8 bytes.
Jupyter cell defining get_stats and printing sorted((v,k) for k,v in stats.items()), reverse=True); output begins [(20, (101, 32)), (15, (240, 159)), (12, (226, 128)), ...
29:48 — get_stats sorted by count: the pair (101, 32), "e" followed by a space, is top with 20 occurrences.

max(stats, key=stats.get) picks the top pair (30:59), and a merge function walks the list left to right, replacing every occurrence with the new ID (31:30). The one trap is the bounds check at the last element. After merging, the list shrinks from 616 to 596, exactly 20 shorter, and no (101, 32) pair is left (33:37).

Jupyter cells: top_pair = max(stats, key=stats.get) giving (101, 32); def merge(ids, pair, idx) with the bounds check i < len(ids) - 1 highlighted; print(merge([5, 6, 6, 7, 9, 1], (6, 7), 99)) giving [5, 6, 99, 9, 1]
32:28 — merge tested on a toy list: (6, 7) becomes 99, guarded by the last-position bounds check.
Jupyter output of merging the top pair into token 256 over the training bytes, ending with length: 596
33:45 — After one merge the sequence is 596 tokens, exactly 20 shorter than 616.

34:08 – 39:23 The training loop: a forest of merges

Repeating that step in a loop is the whole training procedure (34:08). How many times to repeat is a hyperparameter: more merges, bigger vocabulary, shorter sequences. Karpathy switches to the full blog post for better statistics and picks a final vocabulary of 276, which means exactly 20 merges (35:44).

Jupyter cell with get_stats, merge, and the training loop: vocab_size = 276, num_merges = vocab_size - 256, ids = list(tokens), merges = {}, and a for loop that picks the max pair, prints 'merging {pair} into a new token {idx}', merges and records it
35:43 — The whole training procedure: 20 rounds of count, pick the top pair, merge, and record it in merges.

The merges dictionary records each (child, child) → new token mapping. He describes the result as a forest rather than a tree (36:46): the 256 bytes are the leaves, and newly minted tokens can themselves be merged again, as the 20th merge shows when it combines two earlier merged tokens (38:19). With only 20 merges, about 24,000 bytes shrink to about 19,000 tokens, a compression ratio of roughly 1.27 (38:51).

Training loop output: merging (101, 32) into a new token 256, (105, 110) into 257, through (259, 256) into a new token 275
37:57 — The 20 merges in order; the last one, (259, 256) into 275, combines two tokens that were themselves merged earlier.
Jupyter cell printing tokens length: 24597, ids length: 19438, compression ratio: 1.27X
38:46 — Twenty merges on the full blog post compress 24,597 bytes into 19,438 tokens, a ratio of 1.27.

39:23 – 42:32 The tokenizer is its own model

This is the conceptual point of the lecture (39:23). The tokenizer is a preprocessing stage, trained once, on its own training set, with BPE. Once trained it’s a translation layer between raw text and token sequences, in both directions. The LLM is trained afterwards, often on data that has been tokenized once and stored on disk as a giant token stream, with the raw text thrown away (40:57).

Notebook note saying the Tokenizer is a completely separate, independent module from the LLM with its own training dataset, above a diagram: raw text (Unicode code point sequence) to Tokenizer to token sequence to LLM
40:26 — The diagram behind the lecture's main point: the tokenizer translates between raw text and tokens, and the LLM only ever sees tokens.

Because the tokenizer has its own data, its data mix matters (41:30). Lots of Japanese in the tokenizer’s training set means more Japanese merges, shorter Japanese sequences, and more Japanese text per context window for the LLM.

42:32 – 48:21 Decoding, and invalid UTF-8

Decoding goes from IDs back to a Python string (42:32). Build a vocab dictionary from token ID to bytes: the first 256 entries are single bytes, and each merged token is the concatenation of its two children’s bytes (43:33). This only works if the merges are visited in insertion order, which Python dicts guarantee from version 3.7 (44:06). Then join the bytes and call .decode("utf-8").

Notebook 'decoding' section: vocab = {idx: bytes([idx]) for idx in range(256)}, a loop over merges.items() concatenating vocab[p0] + vocab[p1], and def decode(ids) joining bytes and calling tokens.decode("utf-8")
45:16 — First version of decode: build each token's bytes from its two children, join, and decode as UTF-8.

There’s a bug waiting (45:07). Decoding token 97 gives “a”, but token 128 alone raises a UnicodeDecodeError: 128 is 10000000 in binary, which isn’t a legal start byte in UTF-8’s multi-byte scheme. The fix is errors="replace" (46:44), which substitutes the replacement character (�). OpenAI’s released code does the same. If you see that character in model output, the model produced a token sequence that isn’t valid UTF-8 (47:48).

Jupyter traceback for print(decode([128])): UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte
45:58 — Decoding token 128 alone fails: 0x80 is not a valid UTF-8 start byte.
Python codecs documentation, Error Handlers table listing 'strict', 'ignore', 'replace', 'backslashreplace' and 'surrogateescape' with their meanings
47:23 — The error handlers Python offers; replace substitutes U+FFFD, the official replacement character.
The decode cell now using tokens.decode("utf-8", errors="replace"), with print(decode([128])) outputting the replacement character �
48:17 — With errors="replace", invalid byte sequences decode to � instead of crashing.

48:21 – 57:08 Encoding by replaying merges in order

Encoding goes the other way (48:21). Turn the text into UTF-8 bytes, then apply merges. Order matters: merges learned later can depend on tokens created by earlier ones, so earlier merges have to happen first (49:23).

Jupyter cell showing the merges dictionary from (101, 32): 256 through (259, 256): 275, and below it a stub def encode(text) that converts text to a list of UTF-8 bytes
49:27 — The merges dict in insertion order, next to the start of encode: earlier merges must be applied first.

Karpathy’s loop reuses get_stats, but only for its keys, the set of adjacent pairs currently present. Among those, it picks the pair with the lowest index in merges, using min with merges.get(p, float("inf")) as the key (50:57). Pairs that were never learned get infinity and can’t win. When nothing is mergeable, every pair scores infinity and min returns an arbitrary pair, so the loop checks whether that pair is in merges and breaks if not (53:01).

encode in progress: tokens = list(text.encode("utf-8")), a while True loop with stats = get_stats(tokens) and pair = min(stats, key=lambda p: merges.get(p, float("inf")))
51:45 — The min trick: among the pairs present, pick the one with the lowest merge index; unknown pairs score infinity.
Completed encode function with the 'if pair not in merges: break # nothing else can be merged' check, and print(encode("hello world!")) outputting [104, 101, 108, 108, 111, 32, 119, 266, 108, 100, 33]
55:12 — Working encode: "hello world!" comes out as bytes plus one merged token (266).

An edge case: a one-character or empty string has no pairs, which crashes min, so the loop runs only while there are at least two tokens (55:04). Tests close the section (56:07): encode-then-decode returns the original text, both on the training text and on unseen validation text. The reverse, decode-then-encode, isn’t guaranteed, since not every token sequence is valid UTF-8.

Jupyter cells: print(encode("")) giving [], print(decode(encode("hello world"))) giving hello world, and two round-trip checks text2 == text and valtext2 == valtext, both printing True
56:55 — Round-trip tests pass on both the training text and unseen validation text, and the empty string encodes to [].

57:08 – 1:07:32 GPT-2’s regex pre-split

Real tokenizers add rules on top of plain BPE. The GPT-2 paper explains why (58:09): “dog” appears next to every kind of punctuation, and naive BPE would mint tokens like “dog.” and “dog!”, mixing meaning with punctuation. OpenAI’s fix lives in encoder.py in the GPT-2 repo (59:12), a regex pattern compiled with the regex package, a more powerful extension of Python’s built-in re (1:00:14).

GPT-2 paper section on Byte Pair Encoding, describing how BPE would include many versions of common words like dog. dog! dog? and how OpenAI prevents merging across character categories
58:24 — The GPT-2 paper's motivation for the pre-split: without it BPE learns "dog.", "dog!" and "dog?" as separate tokens.
GitHub openai/gpt-2, file src/encoder.py, top of the file showing imports including 'import regex as re' and the bytes_to_unicode function
1:00:17 — GPT-2's encoder.py imports the third-party regex package as re.

re.findall with that pattern chops the text into chunks first (1:00:47). Each chunk is tokenized on its own and the results are concatenated, so a merge can never cross a chunk boundary (1:03:24). In practice the “e” and the space after it can no longer merge, because the space now starts the next word’s chunk (1:03:56).

Notebook section 'Forced splits using regex patterns (GPT series)': import regex as re, gpt2pat = re.compile(...) with the GPT-2 pattern, and print(re.findall(gpt2pat, "Hello world how are you")) giving ['Hello', ' world', ' how', ' are', ' you']
1:03:57 — The GPT-2 pattern splits a sentence into words that each carry their leading space.

Reading the pattern alternative by alternative:

Piece of the pattern What it captures
's, 't, 're, 've, 'm, 'll, 'd common English contractions (1:05:28)
?\p{L}+ an optional space plus a run of letters from any script (1:01:50)
?\p{N}+ an optional space plus a run of numeric characters (1:04:58)
?[^\s\p{L}\p{N}]+ an optional space plus punctuation and other symbols
\s+(?!\S) and \s+ runs of whitespace
regular-expressions.info Unicode tutorial listing Unicode categories, with \p{L} or \p{Letter}: any kind of letter from any language, and its subcategories highlighted
1:02:09 — Reference for \p{L}: letters from any script; \p{N} further down covers numeric characters.

The contraction rule is brittle. A curly Unicode apostrophe isn’t matched, and because OpenAI didn’t make the match case-insensitive, “HOW’S” splits differently from “how’s”, something their own code comment admits (1:06:29). Karpathy adds that hard-coding English contractions is probably unfair to other languages.

GitHub gpt-2 encoder.py around line 52: the comment '# Should haved added re.IGNORECASE so BPE merges can happen for capitalized versions of contractions' above self.pat = re.compile(...)
1:06:36 — OpenAI's own comment admitting the contraction rule should have been case-insensitive.

1:07:32 – 1:11:38 Whitespace, and rules nobody published

The punctuation alternative catches anything that isn’t a letter, number or space (1:07:32). The whitespace rule is the subtle one (1:08:02). Its negative lookahead takes a run of spaces up to, but not including, the last one, so that last space can attach to the following word. That keeps “ you” as the familiar single token even after extra spaces.

Running the pattern on a snippet of Python shows it splitting every time the character category changes (1:09:33). But that isn’t the whole story: in the real GPT-2 tokenizer, runs of spaces never merge at all, each staying token 220 (1:10:35). So OpenAI applied some extra rule during training. Nobody outside knows what it was, because only the inference code was released, never the training code (1:11:06).

Jupyter cells: findall on "Hello've world123 how's are you!!!?" giving ['Hello', "'ve", ' world', '123', ' how', "'s", ' are', ' you', '!!!?'], then findall on a FizzBuzz code string giving chunks such as '\n', 'for', ' i', ' in', ' range' and runs of spaces
1:09:59 — The pre-split on mixed text and on Python code: chunks break wherever the character category changes.
Tiktokenizer with gpt2 and 'Show whitespace' checked on the FizzBuzz code; token count 108 and the ID list full of repeated 220 tokens
1:10:53 — In the real GPT-2 tokenizer, every indentation space stays a separate 220 token, a hint of an unpublished training rule.

1:11:38 – 1:18:28 tiktoken, the GPT-4 pattern and OpenAI’s encoder.py

OpenAI’s official tokenization library is tiktoken, again inference only (1:11:38). Encoding the same string with the gpt2 and cl100k_base encodings shows GPT-4 merging the whitespace that GPT-2 leaves alone. The patterns themselves are published in tiktoken’s openai_public.py (1:12:41), because inference needs them. The GPT-2 one there is a faster but equivalent form of the pattern above.

Jupyter cell: import tiktoken; GPT-2 (does not merge spaces) enc.encode("    hello world!!!") giving [220, 220, 220, 23748, 995, 10185]; GPT-4 (merges spaces) with cl100k_base giving [262, 24748, 1917, 12340]
1:12:02 — tiktoken on the same string: GPT-2 leaves the spaces as three 220s, GPT-4 merges them into one token.
GitHub tiktoken, tiktoken_ext/openai_public.py, def gpt2() with explicit_n_vocab 50257, a comment giving the original GPT-2 pattern, an equivalent faster pat_str, and special_tokens {ENDOFTEXT: 50256}
1:12:51 — tiktoken's openai_public.py publishes the GPT-2 pattern (in a faster equivalent form) and its single special token.

The GPT-4 pattern changes three things that matter (1:13:44): contractions now match case-insensitively, whitespace is handled differently, and numbers are matched in runs of one to three digits, so no token is ever longer than three digits (1:14:16). Why those choices? Undocumented. Karpathy suggests stepping through the regex with ChatGPT and the docs rather than doing it on camera.

openai_public.py def cl100k_base(): special_tokens ENDOFTEXT 100257, FIM_PREFIX 100258, FIM_MIDDLE 100259, FIM_SUFFIX 100260, ENDOFPROMPT 100276, and a pat_str beginning (?i:[sdmt]|ll|ve|re) with the case-insensitive group highlighted
1:13:52 — The GPT-4 (cl100k_base) definition: case-insensitive contractions, number runs capped by \p{N}{1,3}, and its special tokens.

Back in GPT-2’s encoder.py (1:14:49), two files make up the saved tokenizer. encoder.json is the equivalent of the lecture’s vocab (inverted, mapping string to integer), and vocab.bpe is, confusingly, the equivalent of merges (1:15:51). OpenAI also wraps the tokenizer in an extra byte_encoder / byte_decoder layer that Karpathy dismisses as an unimportant implementation detail (1:16:24). Strip that away and the bpe function is the same find-the-bigram, merge-it-everywhere loop built earlier (1:17:25).

Jupyter cell downloading vocab.bpe and encoder.json for GPT-2, loading encoder with json.load and building bpe_merges from the lines of vocab.bpe
1:15:37 — Loading OpenAI's two tokenizer files: encoder.json plays the role of vocab, vocab.bpe the role of merges.
GitHub gpt-2 encoder.py, the bpe method: while True, bigram = min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float('inf'))), break if not in bpe_ranks, then merge occurrences of first and second
1:17:37 — OpenAI's bpe function is the same loop as the lecture's encode: pick the lowest-ranked pair and merge it everywhere.

1:18:28 – 1:25:13 Special tokens

GPT-2’s vocabulary has 50,257 entries (1:19:01): 256 bytes, 50,000 merges, and one more. That last one is <|endoftext|>, token 50256, inserted between documents in the training stream (1:19:32). The model has to learn from data that it means “what follows is unrelated”.

Jupyter cells under 'special tokens': len(encoder) # 256 raw byte tokens. 50,000 merges. +1 special token gives 50257; encoder['<|endoftext|>'] gives 50256
1:19:50 — GPT-2's vocabulary: 256 bytes plus 50,000 merges plus <|endoftext|> at ID 50256.

In Tiktokenizer, typing the string piece by piece produces ordinary tokens until it’s complete, then it snaps into the single ID 50256 (1:21:06). That’s because special tokens never go through BPE; tiktoken’s Rust core looks for them and swaps them in directly (1:21:36). Chat models lean on them heavily: the GPT-3.5-turbo format wraps every message in tokens such as <|im_start|> and <|im_end|> (1:22:38). tiktoken lets you fork cl100k_base and register your own (1:23:10), and GPT-4’s encoding adds fill-in-the-middle tokens (<|fim_prefix|>, <|fim_middle|>, <|fim_suffix|>) plus one more (1:23:41).

GitHub tiktoken src/lib.rs, function _encode_native with an allowed_special parameter; code finds the next allowed special token with special_regex before running the ordinary BPE path
1:21:37 — tiktoken's Rust core searches for allowed special tokens first and swaps them in, outside the BPE merges.
Tiktokenizer with gpt-3.5-turbo selected, a system message 'You are a helpful assistant' and an empty user message rendered as <|im_start|>system ... <|im_end|> tokens, token count 18
1:22:26 — Chat formatting in GPT-3.5-turbo: each message is wrapped in <|im_start|> and <|im_end|> special tokens.
tiktoken README section 'Extending tiktoken': creating a tiktoken.Encoding named cl100k_im from cl100k_base's pattern and ranks, with extra special tokens <|im_start|> 100264 and <|im_end|> 100265
1:23:12 — tiktoken's README shows how to fork cl100k_base and register your own special tokens.
arXiv paper 'Efficient Training of Language Models to Fill in the Middle' by Mohammad Bavarian, Heewoo Jun, Nikolas Tezak, John Schulman, Christine McLeavey, Jerry Tworek and Mark Chen, OpenAI, dated 28 Jul 2022
1:24:16 — The OpenAI fill-in-the-middle paper behind GPT-4's FIM special tokens.

Adding a special token to a trained model takes “model surgery” (1:24:43): one new row in the embedding table, usually initialized small and random, and one more output in the final projection. It’s routine when turning a base model into a chat model.

Our note: the extra cl100k_base token is <|endofprompt|>. Fill-in-the-middle comes from OpenAI’s 2022 paper “Efficient Training of Language Models to Fill in the Middle”.

1:25:13 – 1:28:51 minbpe and the exercise

Karpathy introduces minbpe, the repository that came out of preparing this lecture (1:25:13), and its exercise.md, which breaks building a GPT-4-style tokenizer into four steps (1:25:45). The target is to reproduce tiktoken’s GPT-4 encodings exactly, and also to write the training function tiktoken doesn’t have (1:26:48).

Notebook section 'minbpe exercise' pointing to the exercise progression, and a cell using tiktoken cl100k_base to encode and decode the Korean test string, printing a list of token IDs and True
1:25:35 — The exercise target: reproduce tiktoken's GPT-4 encoding of this string exactly, and add a train function.

He compares GPT-4’s merge order with a minbpe tokenizer trained on the Taylor Swift Wikipedia article, chosen because it’s one of the longest (1:27:19). GPT-4’s very first merge was two spaces into one token, which hints at a lot of code in its tokenizer data. Otherwise the two lists look alike, as you’d expect from the same algorithm on different data.

VS Code with the minbpe repo open, showing gpt4.vocab and regex.vocab side by side: byte tokens up to 255, then merges such as [ ][ ] -> [  ] 256 on the GPT-4 side and [e][r] -> [er] 256 on the Taylor Swift-trained side
1:27:02 — GPT-4's first merges (left) next to those of a minbpe tokenizer trained on the Taylor Swift article (right).

1:28:51 – 1:35:34 SentencePiece: BPE on code points

SentencePiece, used by Llama and Mistral, can both train and run tokenizers (1:28:51). The big difference is the order of operations (1:29:22). tiktoken encodes to UTF-8 and runs BPE on bytes. SentencePiece runs BPE on code points directly, and handles rare code points (as defined by the character_coverage setting) either by mapping them to an unknown token or, with byte_fallback on, by encoding them as UTF-8 bytes that have their own byte tokens (1:29:53). Karpathy finds the tiktoken way cleaner.

Notebook 'sentencepiece' section explaining that sentencepiece runs BPE on Unicode code points with character_coverage and byte_fallback options, a TLDR comparing it with tiktoken, and cells importing sentencepiece and writing toy.txt
1:29:11 — The key difference in one note: tiktoken runs BPE on UTF-8 bytes, SentencePiece on code points with optional byte fallback.

The demo trains on a toy text file (1:30:54), and the first surprise is the sheer number of options, many irrelevant to BPE, which Karpathy puts down to accumulated history (1:31:25). He copied the settings Meta used for Llama 2 by opening Meta’s released tokenizer model file with protobuf (1:32:27). Two of his complaints:

  • Normalization (1:32:58). Older NLP lowercased text and collapsed whitespace. For LLMs he prefers to leave the raw data untouched, so these rules are switched off.
  • Sentences (1:33:28). SentencePiece treats sentences as training examples, with limits on their count and length. Karpathy argues “sentence” is ill-defined in raw internet text and would rather treat a file as one stream.

There are also options for splitting digits and whitespace, roughly SentencePiece’s version of the regex pre-split, and hard-coded unknown, beginning and end of sentence, and padding tokens.

GitHub sentencepiece doc/options.md: a long list of spm_train flags such as --input, --model_type, --vocab_size, --character_coverage, --input_sentence_size, --split_digits, --byte_fallback and --normalization_rule_name
1:31:56 — SentencePiece's options page; many of these flags have nothing to do with BPE.
Jupyter options dict for training: model_prefix tok400, model_type bpe, vocab_size 400, normalization_rule_name identity, remove_extra_whitespaces False, character_coverage 0.99995, byte_fallback True, split_digits True, add_dummy_prefix True, unk_id 0, bos_id 1, eos_id 2, pad_id -1, then spm.SentencePieceTrainer.train(**options)
1:34:41 — The Llama 2-style training settings used in the demo, copied from Meta's released tokenizer.

1:35:34 – 1:43:29 Inside a Llama 2–style SentencePiece model

Training with a vocabulary of 400 produces a model whose vocabulary is laid out in a fixed order (1:35:34): special tokens first (<unk> is 0, beginning and end of sequence are 1 and 2, padding disabled), then the 256 byte tokens, then the merged tokens, then the individual code-point tokens seen in training (1:36:07).

Jupyter cell loading tok400.model and listing the vocabulary: ['<unk>', 0], ['<s>', 1], ['</s>', 2], then byte tokens ['<0x00>', 3], ['<0x01>', 4] and onward
1:36:01 — The trained vocabulary's layout: special tokens first, then the 256 byte tokens.

Encoding “hello” followed by Korean text shows byte fallback at work (1:38:14). The Korean characters weren’t in the toy training data, so they come out as UTF-8 byte tokens, offset by 3 because of the special tokens at the front. Turning byte_fallback off (1:38:45) frees vocabulary space for more merges, but the whole Korean string collapses into <unk>, ID 0. Feeding a model one token for “everything I don’t recognise” is a bad idea, so Llama was right to turn fallback on (1:39:48).

Jupyter cells: sp.encode("hello 안녕하세요") giving [362, 378, 361, 372, 358, 362, 239, 152, 139, 238, ...] and the pieces ['▁', 'h', 'e', 'l', 'lo', '▁', '<0xEC>', '<0x95>', ...]
1:37:48 — Byte fallback in action: the unseen Korean characters come out as UTF-8 byte tokens.
After retraining without byte fallback: sp.encode("hello 안녕하세요") gives [362, 378, 252, 102, 362, 0] and the pieces ['▁', 'h', 'el', 'lo', '▁', '<unk>']
1:39:29 — With byte_fallback off, the whole Korean string collapses into a single <unk> (ID 0).

Two more quirks. Spaces are displayed as a bold underscore character (▁). And there’s a space before “hello” that wasn’t in the input, added by add_dummy_prefix (1:40:21). In tiktoken, “world” at the start of text and “ world” mid-sentence are different tokens; the dummy prefix makes both “ world” so the model doesn’t have to learn they’re the same (1:41:54). Llama 2 uses it.

GitHub sentencepiece_model.proto, NormalizerSpec: the comment 'Adds dummy whitespace at the beginning of text in order to treat "world" in "world" and "hello world" in the same way' above optional bool add_dummy_prefix = 3 [default = true]
1:40:51 — The add_dummy_prefix option, documented in SentencePiece's protobuf definition.
Tiktokenizer with cl100k_base on two lines 'world' and 'hello world': token count 4, IDs [14957, 198, 15339, 1917]
1:40:59 — Without a dummy prefix, "world" at the start and " world" after "hello" get different IDs (14957 vs 1917).

Karpathy’s verdict (1:42:27): widely used and efficient, but loaded with historical baggage, confusing concepts and thin documentation.

1:43:29 – 1:51:48 Choosing vocab size, adding tokens, and beyond text

Back in the GPT from Lecture 7, vocab_size appears in only two places (1:43:29): the rows of the token embedding table and the outputs of the final lm_head linear layer that produces the logits. Why not make it huge (1:45:02)?

GitHub ng-video-lecture gpt.py with vocab_size highlighted in the GPTLanguageModel constructor: token_embedding_table = nn.Embedding(vocab_size, n_embd) and lm_head = nn.Linear(n_embd, vocab_size)
1:44:08 — Where vocab_size enters the model: the rows of the token embedding table and the outputs of lm_head.
Bigger vocabulary helps Bigger vocabulary hurts
Shorter sequences, so more text fits in context (1:46:04) Embedding table and lm_head grow, and so does compute
Each token appears more rarely, so its vector may be undertrained (1:45:33)
Too much text squeezed into one token for a single forward pass to process

In Karpathy’s telling, current models land in the high tens of thousands to around 100,000 (1:46:36). Extending a trained model’s vocabulary, for chat formatting or tool use, is the same mild surgery as before. You can even freeze the base model and train only the new rows (1:47:39).

He then points at the wider design space (1:48:10). The gist tokens paper compresses a long prompt into a few new tokens by training only their embeddings, distilling the model’s behavior with the full prompt. And for images, video and audio, the trend he sees is to keep the Transformer and tokenize the new modality instead (1:50:14), with hard (integer) or soft tokens, as in OpenAI’s Sora, which describes its “visual patches” as the counterpart of text tokens (1:50:45).

OpenReview PDF 'Learning to Compress Prompts with Gist Tokens' by Jesse Mu, Xiang Lisa Li and Noah Goodman of Stanford University, with the abstract
1:48:25 — The gist tokens paper: a long prompt is distilled into a few new token embeddings.
Project page 'Taming Transformers for High-Resolution Image Synthesis (a.k.a #VQGAN)' with a diagram of a CNN encoder, codebook quantization, a Transformer over codebook indices, and a CNN decoder
1:50:33 — VQGAN, an example of turning images into discrete tokens a Transformer can model.
OpenAI Sora page, section 'Turning visual data into patches', with the sentence 'Whereas LLMs have text tokens, Sora has visual patches' highlighted and a diagram of video frames compressed into spacetime patches
1:51:16 — OpenAI's Sora description: visual patches play the role that text tokens play for LLMs.

Our note: the paper is “Learning to Compress Prompts with Gist Tokens” (Mu, Li and Goodman, 2023).

1:51:48 – 1:56:58 Revisiting the quirks: spelling, languages, arithmetic, Python

Now the opening list gets its explanations (1:51:48):

  • Spelling. .DefaultCellStyle is one GPT-4 token. Asked how many l’s it contains, GPT-4 says three; the answer is four (1:52:19). Asked to reverse it, GPT-4 produces a jumble (1:52:51). Asked to first list the characters separated by spaces and then reverse the list, it succeeds, because each character is now its own token (1:53:21).
  • Non-English. “Hello how are you” is 5 tokens; its Korean translation is 15, and even the everyday Korean greeting takes 3 tokens where “hello” takes 1 (1:54:24).
  • Arithmetic. Addition is a digit-by-digit algorithm, but digits are grouped arbitrarily (1:54:55). A blog post titled “Integer tokenization is insane” maps how four-digit numbers split in GPT-2 (1:55:25). Llama 2 splits all digits individually, partly for this reason.
  • Python. GPT-2’s one-token-per-space indentation was close to a tokenizer bug, fixed in GPT-4 (1:56:27).
Tiktokenizer with cl100k_base on the string .DefaultCellStyle: token count 1, ID [98518]
1:52:34 — .DefaultCellStyle is a single GPT-4 token.
ChatGPT 4 conversation: 'How many letters "l" are there in the word ".DefaultCellStyle"' answered 'The word ".DefaultCellStyle" contains three "l" letters.'
1:52:38 — GPT-4 counts three l's in a word that has four.
ChatGPT 4 conversation asking to reverse ".DefaultCellStyle" by first printing every character separated by spaces and then printing them backwards; after 'No toolsies, just do it!' the model prints the spaced characters and then the reversed sequence
1:53:43 — Spelling the word out first, one character per token, lets GPT-4 reverse it.
Tiktokenizer with cl100k_base on 'Hello how are you?': token count 5, IDs [9906, 1268, 527, 499, 5380]
1:54:40 — "Hello how are you?" is 5 tokens in English; the Korean translation he compares it with takes 15.
Image titled 'Addition Using Standard Algorithm' adding 1,296 and 3,457 column by column with carry digits, with steps Add the ones, Add the tens, Add the hundreds
1:55:25 — Addition works digit by digit, but tokens group digits arbitrarily.
Beren's Blog post on integer tokenization: a 'Number composition' plot coloring how four-digit numbers are tokenized in GPT-2 (unique, 1-3, 2-2, 3-1 splits)
1:55:54 — The "Integer tokenization is insane" post: four-digit numbers split in inconsistent ways across GPT-2's vocabulary.

1:56:58 – 2:04:44 Special-token injection, trailing spaces and partial tokens

Ask GPT-4 to print the string <|endoftext|> and it can’t seem to see it (1:56:58). Karpathy doesn’t know what OpenAI does internally, but suspects user text is being parsed with special tokens allowed. User input is attacker-controlled, so special-token handling becomes an attack surface (1:58:01).

ChatGPT 4 conversation: asked to print the string <|endoftext|>, the model asks which string to print, claims it received no string, and finally echoes an empty string
1:57:26 — GPT-4 behaves as if <|endoftext|> in the user's message isn't there.

The trailing whitespace warning (1:58:31) appears in the OpenAI playground with the GPT-3.5-turbo-instruct completion model. Because GPT tokens carry their leading space (” o” is one token), a prompt ending in a bare space puts token 220 where the model expects the space to be part of the next word (2:00:05). That’s rare in training data, so the completion is out of distribution.

OpenAI Playground in Complete mode with model gpt-3.5-turbo-instruct; prompt 'Here is a tag line for an ice cream shop:' and the completion '"Scoops of happiness in every cone!"'
1:59:17 — The completion model working normally when the prompt has no trailing space.
Tiktokenizer with cl100k_base on 'Here is a tag line for an ice cream shop: ' with a trailing space: token count 12 and the ID list ending in 25, 220
1:59:43 — A trailing space becomes its own token, 220, a position the model rarely saw in training.

The same goes for cutting a token in half. Prompting with .DefaultCellSta (without the “yle”) makes the model end its completion immediately, and a retry gets flagged as a possible policy violation (2:01:40). Karpathy calls these “partial token” problems. tiktoken’s Rust code has a lot of undocumented handling for “unstable tokens” (2:03:44); ideally a completion API would search over continuations at the character level rather than append whole tokens.

OpenAI Playground with gpt-3.5-turbo-instruct and the prompt .DefaultCellSty; a warning reads 'The model predicted a completion that begins with a stop sequence, resulting in no output.'
2:02:21 — Prompting with a partial token makes the model end the completion immediately.
OpenAI Playground with a completion continuing .DefaultCellStyle.PaddingBottom = 2f; and a notice 'This request may violate our usage policies.'
2:03:15 — On a retry the request gets flagged as a possible policy violation.

2:04:44 – 2:09:26 SolidGoldMagikarp

Karpathy’s favorite (2:04:44). A blog post clustered GPT token embeddings and found a cluster of strange tokens: “SolidGoldMagikarp”, “StreamerBot” and other odd strings (2:05:16). Asking the model to simply repeat one of them produces evasion, hallucination, or insults (2:06:20).

LessWrong post 'SolidGoldMagikarp (plus, prompt generation)' by Jessica Rumbelow and mwatkins, 5th Feb 2023, noting work done at SERI-MATS by Jessica Rumbelow and Matthew Watkins
2:05:04 — The SolidGoldMagikarp post on LessWrong.
LessWrong post figure of token clusters in embedding space, with one cluster containing tokens such as ' attRot', 'EStreamFrame', ' SolidGoldMagikarp' and 'PsyNetMessage', labeled 'Wait, what?'
2:05:29 — The k-means cluster of strange tokens that started the investigation.
LessWrong post table of anomalous behaviors: evasion replies such as 'I can't hear you', hallucinatory completions like ' SolidGoldMagikarp' > 'distribute', and inter-referential hallucinations
2:06:05 — What the models said when asked to repeat these tokens: evasion, hallucinated substitutes and insults.

The likely explanation (2:07:20): SolidGoldMagikarp is a Reddit user who posted a lot. The tokenizer’s training data included plenty of Reddit, so the username earned its own token. The language model’s training data apparently didn’t, so that token never appeared during training and its embedding row stayed at its random initialization (2:08:24). Karpathy compares it to unallocated memory in a C program: invoking the token plucks out an untrained vector and the model’s behavior is undefined.

Our note: the original post is “SolidGoldMagikarp (plus, prompt generation)” by Jessica Rumbelow and Matthew Watkins on LessWrong (2023). It’s also a direct, real-world case of takeaway 3: the tokenizer and the model were trained on different data.

2:09:26 – end Formats, final advice, and outtakes

Token efficiency depends on format (2:09:26). The same data is 116 tokens as JSON and 99 as YAML. Since you pay per token, in both context and money, it’s worth measuring formats in Tiktokenizer.

Tiktokenizer with cl100k_base on a YAML version of a product record with reviews: token count 99
2:09:46 — The YAML version of the record is 99 tokens; the same data as JSON is 116.

Closing advice (2:10:28): don’t brush tokenization off; it carries security and AI safety issues. And “eternal glory goes to anyone who can get rid of it”. His practical recommendations (2:10:59):

  1. If you can, reuse the GPT-4 vocabulary with tiktoken.
  2. If you must train your own vocabulary today, use BPE in SentencePiece, but copy Meta’s settings carefully, since misconfiguring it is easy (2:11:30).
  3. What he really wants is tiktoken with training code. minbpe is a Python implementation of that idea, not yet efficient (2:12:02).
Notebook 'Final recommendations' cell: don't brush off tokenization; eternal glory to anyone who can delete it; maybe re-use GPT-4 tokens and tiktoken; if training a vocab, use BPE with sentencepiece and be careful with the million settings; switch to minbpe once it is as efficient as sentencepiece
2:10:29 — Karpathy's closing recommendations, as written in the notebook.

The final half-minute after the sign-off is outtakes.

Code you’ll write

The lecture builds everything in a notebook; minbpe packages the same logic into BasicTokenizer, RegexTokenizer and GPT4Tokenizer classes. These are short excerpts.

Count pairs and merge one (28:24, 31:30). get_stats tallies adjacent pairs; merge replaces every occurrence of one pair with a new ID.

def get_stats(ids):
    counts = {}
    for pair in zip(ids, ids[1:]):  # consecutive elements
        counts[pair] = counts.get(pair, 0) + 1
    return counts

def merge(ids, pair, idx):
    newids = []
    i = 0
    while i < len(ids):
        if i < len(ids) - 1 and ids[i] == pair[0] and ids[i+1] == pair[1]:
            newids.append(idx)
            i += 2
        else:
            newids.append(ids[i])
            i += 1
    return newids

Train: repeat the merge (35:44). Twenty merges take the vocabulary from 256 to 276, and merges records the order.

vocab_size = 276  # 256 raw bytes + 20 merges
num_merges = vocab_size - 256
ids = list(tokens)  # tokens = list(text.encode("utf-8"))
merges = {}  # (int, int) -> int
for i in range(num_merges):
    stats = get_stats(ids)
    pair = max(stats, key=stats.get)
    idx = 256 + i
    print(f"merging {pair} into a new token {idx}")
    ids = merge(ids, pair, idx)
    merges[pair] = idx

Decode (43:33). Build token-to-bytes from the merges, join, and decode leniently.

vocab = {idx: bytes([idx]) for idx in range(256)}
for (p0, p1), idx in merges.items():  # relies on insertion order
    vocab[idx] = vocab[p0] + vocab[p1]

def decode(ids):
    tokens = b"".join(vocab[idx] for idx in ids)
    text = tokens.decode("utf-8", errors="replace")
    return text

Encode (50:57). Always apply the earliest-learned merge available, and stop when none is left.

def encode(text):
    tokens = list(text.encode("utf-8"))
    while len(tokens) >= 2:
        stats = get_stats(tokens)
        pair = min(stats, key=lambda p: merges.get(p, float("inf")))
        if pair not in merges:
            break  # nothing else can be merged
        idx = merges[pair]
        tokens = merge(tokens, pair, idx)
    return tokens

GPT-2’s pre-split, and tiktoken for comparison (1:00:47, 1:11:38). The regex chunks text so merges never cross categories; tiktoken runs the real GPT-2 and GPT-4 tokenizers.

import regex as re
gpt2pat = re.compile(r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""")
print(re.findall(gpt2pat, "Hello've world123 how's are you!!!?"))

import tiktoken
enc = tiktoken.get_encoding("gpt2")
print(enc.encode("    hello world!!!"))  # spaces stay separate
enc = tiktoken.get_encoding("cl100k_base")  # GPT-4
print(enc.encode("    hello world!!!"))  # spaces merge

A Llama 2–style SentencePiece run (1:32:27). A trimmed set of the options Karpathy copied from Meta’s tokenizer; the notebook sets several more.

import sentencepiece as spm
spm.SentencePieceTrainer.train(
    input="toy.txt", model_prefix="tok400",
    model_type="bpe", vocab_size=400,
    normalization_rule_name="identity", remove_extra_whitespaces=False,
    split_digits=True, byte_fallback=True,
    add_dummy_prefix=True, character_coverage=0.99995,
    unk_id=0, bos_id=1, eos_id=2, pad_id=-1,
)
sp = spm.SentencePieceProcessor()
sp.load("tok400.model")
ids = sp.encode("hello 안녕하세요")
print([sp.id_to_piece(i) for i in ids])

Full code: karpathy/minbpe (MIT license), including the lecture notebook, exercise.md and tests that check the GPT-4 tokenizer against tiktoken.

Skip list

  • 0:02 – 2:36 Recap of the character-level tokenizer. Skip if you’ve just watched Lecture 7.
  • 18:58 – 20:01 Pros and cons of UTF-16 versus UTF-32 and the reading list. The demo right after makes the point faster.
  • 1:16:24 – 1:17:25 OpenAI’s byte_encoder / byte_decoder layer. Karpathy himself calls it an uninteresting detail and skips it.
  • 1:31:25 – 1:35:02 The tour of SentencePiece configuration and the critique of “sentences”. Skip unless you plan to train with SentencePiece.
  • 1:48:10 – 1:51:48 Gist tokens and multimodal tokenization. These are quick pointers to papers, with no code.
  • 2:12:33 – end Outtakes after the sign-off.

Glossary

  • Token — the unit an LLM actually reads and predicts: an integer ID that stands for a chunk of text, anywhere from one byte to a long word.
  • Tokenizer — a separately trained component that translates text into token IDs and back; for BPE, it’s fully described by its merges and vocabulary.
  • Unicode code point — the integer Unicode assigns to a character; what ord() returns for a one-character Python string.
  • UTF-8 — a variable-length encoding that turns each code point into 1 to 4 bytes; ASCII-compatible and the basis of byte-level BPE.
  • Byte pair encoding (BPE) — a compression algorithm that repeatedly replaces the most frequent adjacent pair of symbols with a new symbol; used to build LLM vocabularies.
  • Merges — the ordered table of (token, token) → new token rules learned in training; encoding replays them earliest first.
  • Vocab — the map from each token ID to the bytes it represents, built from the 256 single bytes plus the merges.
  • Compression ratio — bytes in divided by tokens out; about 1.27 after 20 merges in the lecture.
  • Regex pre-split (pre-tokenization) — chopping text into chunks of letters, numbers, punctuation and whitespace before BPE, so merges never cross those boundaries.
  • Special token — a reserved ID such as <|endoftext|> or <|im_start|> that is inserted directly rather than produced by merges, used to mark documents or chat structure.
  • Model surgery — resizing a trained model’s embedding table and output layer to add new tokens.
  • tiktoken — OpenAI’s fast, inference-only BPE library containing the GPT-2 (gpt2) and GPT-4 (cl100k_base) encodings.
  • SentencePiece — Google’s library for training and running tokenizers; runs BPE on code points and is used by Llama and Mistral.
  • Byte fallback — a SentencePiece option that encodes code points missing from the vocabulary as UTF-8 byte tokens instead of <unk>.
  • add_dummy_prefix — a SentencePiece option that puts a space in front of the text so a word at the start looks the same as mid-sentence.
  • Partial (unstable) token — a prompt that ends partway through what would normally be one token, such as a trailing space, leaving the model out of distribution.
  • Glitch token — a token present in the vocabulary but absent from the model’s training data, so its embedding is untrained; SolidGoldMagikarp is the classic example.

FAQ

What is byte pair encoding in Karpathy's tokenizer video?

Byte pair encoding (BPE) starts from the UTF-8 bytes of the training text, so the vocabulary begins at 256 tokens. It then repeatedly finds the most frequent pair of adjacent tokens, gives that pair a new token ID (256, 257, and so on) and replaces every occurrence. The ordered list of merges is the trained tokenizer. Encoding replays those merges on new text; decoding looks each token up as a byte string and joins them.

Why can't LLMs spell words or count letters, according to Karpathy?

Because the model never sees letters, only tokens, and some tokens are long chunks. In the lecture, '.DefaultCellStyle' is a single GPT-4 token, and the model miscounts the letter l in it and fails to reverse it. When Karpathy first asks it to spell the string out character by character, so that each character becomes its own token, the reversal comes out right.

What is the difference between tiktoken and SentencePiece?

tiktoken is OpenAI's inference-only library: it encodes text to UTF-8 bytes and applies byte-level BPE merges, but it cannot train a vocabulary. SentencePiece, used by Llama and Mistral, can both train and encode. It runs BPE on Unicode code points and only falls back to bytes for rare characters when byte_fallback is on. Karpathy finds tiktoken's approach cleaner and SentencePiece full of easy-to-miss settings.

What is SolidGoldMagikarp and why did it break GPT models?

SolidGoldMagikarp is a Reddit username that became a single token in the GPT-2/GPT-3 vocabulary, probably because the tokenizer's training data contained a lot of Reddit text. The language model's own training data apparently lacked it, so that token's embedding row was never updated. Asking the model about it feeds in an untrained vector, which Karpathy compares to reading unallocated memory, and the model responds with evasion, hallucinations or insults.

How big should a tokenizer's vocabulary be?

It's an empirical hyperparameter. A bigger vocabulary shortens sequences, so the model sees more text per context window, but it grows the embedding table and the final output layer, leaves rare tokens undertrained, and may pack too much meaning into one token. Karpathy says state-of-the-art models sit in the high tens of thousands to around 100,000; GPT-2 used 50,257 and GPT-4 about 100,000.

Studying another course? Paste its YouTube or Bilibili link on the home page to request a handout — or make your own with the desktop app (coming soon).

Updated 2026-10-07 · By Yao

Not affiliated with the creators, universities or companies whose videos are covered here. Videos belong to their creators; this site only adds commentary.

Sources & credits

How these notes were made

A human watched the full video and wrote these notes with AI assistance for drafting. Stills are limited to a handful of frames used for commentary, each linked to its timestamp; no transcript is reproduced. Spot an error? Let us know via the contact page.