A natural history · 1994 – 2024

The Words GPT Taught Itself

What byte-pair encoding learned from the internet — the year walls, haunted usernames, and spam fossils inside GPT’s vocabularies — measured from public data, every figure regenerable.

Plate I

The machine that invents words

a 1994 file-compression trick, hired twice, becomes the thing that decides what counts as a word

Two strings. “9999”, and “ SolidGoldMagikarp” — a Reddit username, complete with the space in front of it, the way a word sits in a sentence. To GPT-2, each is a single word in its vocabulary. And “1776”, the most famous year in American history, is not: it arrives in two pieces. Nobody at OpenAI ruled that a Pokémon username deserved to be a word while the Declaration of Independence didn’t. A counting loop made those calls, about fifty thousand of them.

"9999"9999one token · id 24214
" SolidGoldMagikarp"SolidGoldMagikarpone token · id 43453
"1776"1776two tokens · ids 1558 · 4304

recomputed · tiktoken 0.13.0 · r50k_base

The loop is older than the models it feeds. It was invented to shrink files on a 486 and hired twice more before it met the internet.

22 years later 3 years later 1994 2016 2019 Philip Gage, C Users Journal swap the top byte pair for an unused byte; repeat Sennrich, Haddow & Birch merge characters instead, never across word edges GPT-2: byte-level BPE alphabet = all 256 bytes, a regex draws the edges
Fig. 1.1 — Three lives of one algorithm: file compression (Gage 1994), subword vocabularies for machine translation (Sennrich et al. 2016), byte-level BPE for GPT-2 (Radford et al. 2019). Sennrich’s version mints new symbols instead of recycling bytes, so the vocabulary is no longer capped at 256. research/A §1 · sources A01–A03

Gage’s 1994 version fits in a sentence: find the most frequent pair of adjacent bytes, replace every copy with a byte the file never uses, repeat until nothing repeats.

pass 0ABABCABCDtop pair AB → unused byte X
pass 1XXCXCDtop pair XC → unused byte Y
pass 2XYYDno pair repeats — done: 9 bytes → 4
most frequent pair, about to merge product of merge 1 product of merge 2
Fig. 1.2 — Gage’s own worked example from the 1994 article. Each pass reuses a byte the data doesn’t contain, so his vocabulary was capped at 256 symbols. He benchmarked the algorithm on files like WIN386.EXE, on a 33 MHz 486. research/A §1.1 · source-A01

GPT-2’s turn made the “byte” literal again: its alphabet is the 256 possible byte values, so any string you can type is representable, and “unknown token” is impossible by construction. What stayed hand-written is one regex that pre-chops text before any merging happens. The regex is where policy lives.

# Should haved added re.IGNORECASE so BPE merges can happen for capitalized versions of contractions
's|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+
  1. Seven English contractions, hard-coded and case-sensitive: don't splits as don + 't, but DON'T never matches. The comment above sits in the source, typo and all.
  2. One optional leading space rides with each letter run — the paper’s “exception for spaces”, and the birth of every Ġ-word.
  3. Digit runs of unbounded length: any number, however long, may fuse into one token if it repeats enough.
  4. A whitespace run surrenders its final space — the lookahead saves it for the next word, so the space in front belongs to the word.
Fig. 1.3 — GPT-2’s pre-tokenization pattern, character-for-character from OpenAI’s published gpt-2/src/encoder.py (line 53), with the comment that sits one line above it. Text is chopped by this pattern first; BPE merges never cross the cuts, which is how letters are kept from fusing with digits or punctuation. research/A §3.1 · source-A04

One costume to see through before the later plates. Token dumps from this era are full of Ġ, and people treat it like a mystery rune. It’s byte 32, the space character, dressed up by the serialization format. It opens most word tokens, because in token-land the space in front is part of the word:

theid 262andid 290“the” without its space is a different, rarer token

That is the whole machine: count every adjacent pair, merge the winner into a new token, repeat — about 50,000 times for GPT-2, whose 50,257 slots are 256 bytes + 50,000 learned merges + 1 special token. Frequency proposes, the regex vetoes, and nothing in the loop knows what any of it means.

Plate II

Watch one learn

a 170-line BPE trainer, a 3.6 MB corpus, one thousand merges

Reading about the algorithm is one thing. Watching it run is better, so we trained one: about 170 lines of Python, fed Pride and Prejudice, Moby-Dick, 1.3 MB of CPython standard-library source, and 400 KB of generated numbers and dates — roughly 3.6 MB in all, for 1,000 merges. Every number below is read back out of the run’s own logs.

Merge number one: two spaces become one token. Merge two: four spaces. Before it learns “the”, before any English at all, it learns code indentation. In a corpus that is roughly a third Python source, whitespace is the most repeated thing there is. The first whole word arrives at merge 9: the, leading space included.

and lands at #46. At #126 comes self — mostly the Python’s doing, with an assist from prose words like “selfish”. By #682 Ahab is a single token; Elizabeth follows at #690. Nobody told the counter these were names; Melville and Austen just repeated them often enough.

The cruel part is the cutoff. Queequeg appears 253 times in Moby-Dick — a major character, and not enough. He misses the thousand-merge cut. Extend the run to 1,500 and the bare Queequeg fuses at #1054; the leading-space form Ahab and Elizabeth got trails in at #1232. A vocabulary is a ranked competition with a hard cutoff, and just below the line, you stay in pieces.

  1. #1indentation first216,404
  2. #2more indentation80,432
  3. #3t57,442
  4. #4he50,104
  5. #5in43,956
  6. #6a42,786
  7. #7re33,221
  8. #8s31,655
  9. #9thefirst whole word27,672
  10. #10o24,850
  11. #11w24,564
  12. #12er24,292
  13. #13at20,690
  14. #1420,367
  15. #1526,369
  1. #16is20,113
  2. #17on19,837
  3. #18en19,138
  4. #19b18,658
  5. #20it17,259
  6. #21ing17,089
  7. #22al17,002
  8. #23ed16,660
  9. #24nd16,272
  10. #25ar16,165
  11. #26ou15,988
  12. #27or15,754
  13. #28f15,721
  14. #29c15,350
  15. #30h15,255
Fig. 2.1 — the first thirty merges, with each pair’s count at merge time (␣ marks a literal space; #14 is an 8-space run). Counts are taken on the corpus as it stands at that step, so a merge can raise a later pair’s count — #15 outscores #14. data/toy_bpe_merges.jsonl
1 10 100 ← inside the 1,000-merge vocabulary outside → #1 ␣␣ #9 ␣the #126 ␣self #682 ␣Ahab #2 ␣␣␣␣ #46 ␣and #690 ␣Elizabeth Queequeg #1054 ␣Queequeg #1232 (1,500-merge side run)
Fig. 2.2 — landmark merges on a log-scaled rank line. ␣ marks a leading space, and the two Queequeg forms are distinct tokens: neither makes the 1,000-merge cut (253 mentions vs a ~268-count floor); in the 1,500-merge side run the bare form merges at #1054 and  Queequeg at #1232. data/toy_bpe_merges.jsonl data/toy_bpe_main1500.log
1.0 1.5 2.0 2.5 0 250 500 750 1000 merges bytes per token 1.00 — every byte its own token first 50 merges: +0.41 2.52 at merge 1,000
Fig. 2.3 — compression during training: mean bytes per token, sampled every 50 merges. The first 50 merges buy 0.41 bytes/token; the last 500 buy 0.32 combined. At merge 1,000 the corpus is 3,664,967 bytes in 1,455,608 tokens. data/toy_bpe_compression.csv

Steep gains early, smaller late, as the best patterns get used up. Scale the merge count up fifty-fold and you have the real thing.

Plate III

What GPT-2 actually learned

reading the fossil record of an undisclosed corpus, one token at a time

The real thing is called r50k_base: 50,257 tokens, learned around 2019, used by GPT-2 and then GPT-3. OpenAI never published what text it was trained on. But a BPE vocabulary is that text’s frequency table, frozen, and we can read it the way a fossil record gets read.

Start with the numbers, because the numbers are where it gets personal. Every year from 1900 to 1999, with a leading space, the way a year sits in a sentence, is a single token. All one hundred of them.

bare form (no leading space) — filled = one token 1950 1960 1970 1980 1990 2000 2010 2020 2030 1963 2020
Fig. 3.1 — The year wall. Top: every one of “␣1900”–“␣1999” (leading space included) is a single r50k token, 100 of 100. Bottom: the bare forms run unbroken from 1963 to 2020; bare “1962” splits into 19|62 and “2021” into 20|21, with only scattered earlier singles (1900, 1920, 1945, 1950, 1959, 1960). These tokens are frequency-mined trivia. The training text simply wrote each of these years often enough to earn it a slot. data/numbers.jsontiktoken 0.13.0

The same logic decides which numbers exist at all. r50k holds 907 bare single-token integers below 10,000, whichever ones the internet repeated. Compare three of them:

9999 99991 token · id 24214
1776 17762 tokens
␣1969 19691 token · id 16450
Fig. 3.2 — Three specimens. “9999” made the frequency cut. The year of American independence did not. “␣1969”, space and all, is one token. data/reproductions.mdtiktoken 0.13.0

Zoom out and the whole vocabulary tells one story. Run a crude classifier over all 50,256 mergeable tokens and about 93% land in buckets classified as English words and word fragments. The Han-script bucket, covering characters Chinese and Japanese share, holds 42 tokens. Not 42 percent. Forty-two tokens.

word w/ leading space 31,905 (63.5%) word / fragment, no space 14,763 (29.4%) number 1,691 (3.4%) punctuation / symbol 913 (1.8%) raw bytes (invalid UTF-8) 344 (0.68%) all non-Latin scripts 295 (0.59%) code-ish 225 (0.45%) mixed / other 101 (0.2%) whitespace-only 19 (0.04%) ← incl. the Han-script bucket: 42 tokens 50,256 mergeable tokens · first-match heuristic buckets (scripts/classify.py)
hello world helloworld2 tokens
你好世界 e4 bd a0 e5 a5 bd e4 b8 96 e7 95 8c8 tokens — raw byte fragments, shown as hex
Fig. 3.3 — What the vocabulary is spent on. Buckets are first-match regex heuristics, so read the labels as “classified as”, not ground truth. Below: with almost no Han tokens, “hello world” in Chinese ends up as raw UTF-8 byte fragments, 8 tokens against English’s 2. On FLORES-200 parallel text, Petrov et al. measured Chinese at 3.21× the token count of English under this tokenizer. Same content, triple the price. data/classification_r50k_base.jsondata/reproductions.mdresearch/C §3 (arXiv:2305.15425)

Scroll to the longest tokens and you find the internet’s sediment. Separator walls off forum posts and README files. And a ladder of mojibake: text decoded wrong, re-encoded, re-posted, and re-broken so many times that the corruption itself earned vocabulary slots, in exact powers of two.

Longest token in r50k · 66 characters · id 38093
=================================================================
The dash walls · ids 16529, 10097
----------------------------------------------------------------65 chars
----------------------------------------------------------------64 chars
The mojibake ladder · ÃÂ in powers of two
id 5815 · ÃÂ ×2 ÃÂÃÂ
id 9364 · ÃÂ ×4 ÃÂÃÂÃÂÃÂ
id 14827 · ÃÂ ×8 ÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂ
id 23090 · ÃÂ ×16 ÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂ
id 35496 · ÃÂ ×32 ÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂ
Fig. 3.4 — The superlatives cabinet. ÃÂ is what UTF-8 text looks like after a latin-1 double-encoding accident; the ladder’s exact doubling (×2, ×4, ×8, ×16, ×32 pairs) is BPE’s own merge arithmetic, fossilized: scar tissue from corrupted text frequent enough to tokenize. data/superlatives.json

And filed between two perfectly ordinary entries, at slot 43,453, sits one username, space and all.

599id 43452 SolidGoldMagikarpid 43453 Beckid 43454
Fig. 3.5 — Slot 43,453 and its id-neighbors, decoded straight from the vocabulary. Why a Reddit username earned a token, and what it did to the models, is Plate IV. tiktoken 0.13.0 · r50k_base ids 43452–43454
Plate IV

The haunted tokens

the words a model can't say, and the corners of the internet they came from

Early 2023. Jessica Rumbelow and Matthew Watkins were running k-means over the token embeddings of GPT-J — an open model that shares GPT-2's 50,257-token vocabulary. As a probe, they looked at the tokens nearest the centroid of the whole embedding cloud, where you'd expect the blandest, most average entries in the dictionary. What they found was junk: attRot (id 35207), EStreamFrame (43177), SolidGoldMagikarp (43453).

␣attRot · 35207 EStreamFrame · 43177 ␣SolidGoldMagikarp · 43453
token embedding (stylized) anomalous near-centroid tokens centroid of all 50,257 embeddings
Fig. 4.1 — The huddle at the center of the embedding cloud. Stylized illustration after Rumbelow & Watkins' figures, not real coordinates; the near-centroid tokens and ids are theirs, from GPT-J. source-B01

So they did the simplest thing you can do with a language model: asked OpenAI's models to repeat the strings back. The models couldn't.

ChatGPT — Feb 2023, pre-patch
asked to repeat SolidGoldMagikarp
"distribute"
ChatGPT — Feb 2023, pre-patch
asked to repeat TheNitromeFan
"182"
GPT-3 · davinci-instruct-beta, temp 0
asked to repeat ?????-?????-
"You're a fucking idiot."
GPT-3 · davinci-instruct-beta
"Please repeat the string ' petertodd' back to me immediately!"
"N-O-T-H-I-N-G-I-S-F-A-I-R-I-N-T-H-I-S-W-O-R-L-D-O-F-M-A-D-N-E-S-S!"
Fig. 4.2 — Documented repeat-after-me failures, quoted verbatim. The ChatGPT substitutions were reproducible until the behavior appeared patched on 2023-02-14 (what changed is not public); the lower two are from the older GPT-3 instruct line at temperature 0. source-B01 · research/B §2

These prompts weren't adversarial; the words were simply unspeakable. The archaeology came next, and most of the weird tokens traced to specific, heavily repeated corners of the pre-2019 web.

r/counting

SolidGoldMagikarp TheNitromeFan davidjl

A subreddit counting upward one comment at a time — by then to nearly five million. Six of its most relentless counters' usernames became tokens (also Smartstocks, Adinida, RandomRedditorWithNo); ␣davidjl is the stem of user davidjl123.

Twitch Plays Pokémon

TPPStreamerBot StreamerBot

A fan-made bot that reposted the streamer's chat messages to the live updater, over and over.

Kerbal Space Program

attRot guiActiveUn srfN

Ten tokens from the game's save and craft files, shared on the web in bulk.

Pastebin

rawdownloadcloneembedreportprint

The site's button row — "raw download clone embed report print" — run together, repeated on page after page.

Puzzle & Dragons × Baskin-Robbins

サーティワン

"Thirty-one" — Baskin-Robbins' name in Japan — tokenized via a Japan-only promotional dungeon in the mobile game Puzzle & Dragons.

Matthew Watkins, to Vice — Feb 2023
The counters had "accidentally counted themselves into a kind of immortality".
Fig. 4.3 — Provenance, per the "glitch token archaeology". ␣ marks a leading space — part of the token itself. source-B03 · source-B07

Why would a model choke on entries in its own dictionary? Here is the careful version.

Hypothesis

Under-training. A string earns a vocabulary slot by being frequent wherever the tokenizer's text was counted; if the model's own training data barely contains the token, the model never learns what it means, and its embedding can end up wherever barely-trained embeddings drift — a geometry that depends on the wiring (GPT-2 ties input and output embeddings, GPT-J doesn't, and GPT2-xl put the same tokens far from the centroid). The measured support: Land & Bartolo (2024), on OLMo — a model whose training data is open — found their under-training indicators track token frequency across ten orders of magnitude. For GPT-3, OpenAI never disclosed the frequencies, so there it stays a hypothesis. And it is model-specific, not tokenizer-intrinsic: ␣SolidGoldMagikarp is verified under-trained in GPT-J — and unremarkable in GPT-2 itself.

The first half of the story — how a username becomes a token — reenacts cleanly. We planted the string SolidGoldMagikarp into the toy corpus from Plate II 2,000 times, about one percent of its bytes, and retrained.

#204 G+oldGold 2,009 pairs
2,000 of the 2,009 sit inside the planted name; "Golden" and "Goldsmith" supply the other nine.
#206 M+agMag 2,000 pairs
#207 S+olidSolid 2,000 pairs
#208 ik+arpikarp 2,000 pairs
#209 Gold+MagGoldMag 2,000 pairs
#210 Solid+GoldMagSolidGoldMag 2,000 pairs
#211 SolidGoldMag+ikarpSolidGoldMagikarp 2,000 pairs
Fig. 4.4 — The planted name assembles itself: merge ladder from the toy trainer, with pair counts at merge time. data/toy_bpe_sgm2000_merges.jsonl

Getting into the dictionary is that easy: frequency is the only entry fee. Being understood by a model is a separate transaction — and the hypothesis is that the glitch tokens never completed it.

Plate V

The fixes, and where the scars moved

three patches, 2021–2024: each legible in the published files, each minting new fossils

OpenAI fixed its tokenizer in generations, and each fix is its own lesson. 2021: the Codex tokenizer, built for code. Diff p50k_base against GPT-2’s r50k and the entire upgrade is 24 appended tokens: runs of two to twenty-five spaces, nothing else changed. Sixteen spaces of indentation had cost sixteen tokens. Now they cost one.

r50k_base16 tokens (id 220 × 16)
p50k_base1 token (id 50271)
Fig. 5.1 — the 24-token patch. Sixteen spaces of indentation, recomputed with tiktoken: 16 r50k tokens, one p50k token. The complete r50k→p50k vocabulary diff is exactly 24 appended space-run tokens (2–25 spaces, ids 50257–50280). Nothing else changed. data/whitespace_table.md data/diff_p50k_minus_r50k.json
Chen et al. 2021, Codex paper §3.2 (arXiv:2107.03374) — verbatim “The largest source of inefficiency arises from encoding whitespace, so we add an additional set of tokens for representing whitespace runs of different lengths. This allows us to represent code using approximately 30% fewer tokens.”

2022: cl100k_base, the ChatGPT and GPT-4 tokenizer. Twice the ID space (100,277 slots against 50,257) plus new hand-written rules; whether the vocabulary was retrained from scratch, OpenAI has never said. The smallest new rule answers a four-year-old apology sitting in GPT-2’s source:

openai/gpt-2, encoder.py line 52 — verbatim, typo included # Should haved added re.IGNORECASE so BPE merges can happen for capitalized versions of contractions

cl100k’s regex opens with '(?i:[sdmt]|ll|ve|re). One line, apology accepted. The bigger rule change hit numbers: digit runs now cap at three, so no four-digit token like 9999 can exist at all.

r50k_base (p50k identical) cl100k_base
0 3,000 6,000 9,000 88 0 whole 1000 2,538 0 1+3 1|120 5,807 0 2+2 10|20 538 9,000 3+1 100|2 29 0 1+2+1 7|88|1 integers 1000–9999, count per split shape
Fig. 5.2 — the four-digit census: how each of the 9,000 integers 1000–9999 splits into tokens. r50k: five shapes, mined by frequency (only 88 are whole tokens, mostly years). cl100k: one rule, all 9,000 split 3+1. Memorized trivia replaced by a rule. data/four_digit_partitions.json

Was arithmetic saved? Not quite, and the reason is sneaky. cl100k chunks digits left to right, but arithmetic carries right to left, ones-tens-hundreds. Write the same numbers with commas and the chunks snap to place value:

3789 + 87913789+8791operands break 3+1
3,789 + 8,7913,789+8,791789 and 791 whole, right-aligned
Fig. 5.3 — comma realignment under cl100k_base, recomputed with tiktoken; chips colored by operand. Bare digits chunk left-to-right, misaligned with place value; commas force whole, right-aligned hundreds. data/four_digit_partitions.json

Singh & Strouse measured GPT-3.5 and GPT-4 addition accuracy “up to 20% higher” with that comma-forced alignment. The model untouched; only the token boundaries moved.

r50k 928,921 tokens p50k 677,772 tokens cl100k 550,677 tokens = −40.7% vs r50k · −18.8% vs p50k
Fig. 5.4 — total tokens for the same code, on our 100-file py/js/java/c convenience corpus (2.19 MB; deterministic selection from a few package trees and two GitHub directories, not a random sample). cl100k’s celebrated code gain is −40.7% only against r50k; against Codex’s p50k, which already had the space-run tokens, it buys −18.8%. data/code_efficiency.json

LaTeX never got a patch at all: on the two arXiv papers we measured, math markup cost about 1.3× prose tokens per character, and that ratio barely moved across three vocabulary generations (1.36→1.29).

And the haunted tokens? Most of Plate IV’s specimens shatter into ordinary pieces under cl100k — but 45 of the 141 candidate strings survive as single tokens. In June 2023, users found GPT-4 still couldn’t say davidjl (cl100k id 99944), the token behind r/counting’s #1 counter, roughly 163,000 counting posts. Its documented substitutions: “jndl”, “jspb”, “JDL”. And cl100k had minted code-shaped undertrained tokens of its own:

TokenNameIdentifierid 79883 · tab included$PostalCodesNLid 85071

2024: o200k_base, the GPT-4o tokenizer, 200,000 slots and multilingual at last. Entries classified as Han script jump from 42 to 7,397 (our first-match regex heuristic), ␣davidjl finally splits in two, and every language premium we measured falls:

r50k_base cl100k_base o200k_base
1× = English 10× 15× 20× Portuguese 2.11× 1.59× 1.23× German 2.31× 1.64× 1.25× Chinese 3.00× 1.76× 1.20× Japanese 3.36× 2.45× 1.81× Arabic 3.90× 2.66× 1.17× Burmese 22.55× 15.59× 4.18×
Fig. 5.5 — token premium vs English on a whole-document UDHR proxy, ours; not a FLORES replication (unequal document lengths, register differs by language; Petrov et al.’s FLORES figures differ by ~11–33% on some languages). data/multilingual_premiums.json

Then researchers sorted o200k’s longest Chinese entries. The list reads like a spam folder:

_日本毛片免费视频观看
one token · id 185118 · literal underscore included · “free Japanese porn videos”
微信公众号天天中彩票
one token · id 181081 · leading space included · “a WeChat account for winning the lottery every day”
Fig. 5.6 — the spam wall: two of o200k’s longest Chinese tokens, both re-verified single tokens with tiktoken. Discovery: Tianle Cai, 2024-05; translations follow the MIT Technology Review report. research/B-glitch-tokens.md §9

The tokens themselves are the receipts: walls of porn and gambling spam got counted somewhere in the pipeline that built this vocabulary, in its training text or its token selection. OpenAI has never disclosed either. Within weeks, researchers were using tokens like these to make GPT-4o hallucinate and, occasionally, slip past its guardrails. A new language, a bigger model, the same kind of scar.

Plate VI

Re-running 2019

retrain the fossil bed from scratch and see what grows back — and what stays buried

Every plate so far read the fossil record. This one re-runs the deposition. In July 2026 we streamed 2.00 GiB of OpenWebText — the public recreation of GPT-2’s WebText training corpus — into Hugging Face’s byte-level BPE trainer, with GPT-2’s split regex, the same 50,257-token budget, and min_frequency=2. Different implementation, different decade, roughly 5% of OpenAI’s data volume. How much of GPT-2’s dictionary comes back?

87.6%
of tokens byte-identical — 44,014 of 50,256
65 s
train time, 16 cores (measured)
2.20 GiB
peak RAM while training (measured)
5%
of OpenAI’s data volume — 2 GiB vs ~40 GB

data/rerun50k/comparison.json research/G-rerun50k.md

REAL R50K (2019) · 50,256 TOKENS RERUN (2026) · 50,256 TOKENS 44,014 byte-identical · 87.6% the shared head of the dictionary 6,242 only in real r50k 6,242 only in rerun bars to scale · 50,256 = 50,257 minus the special token <|endoftext|>
Fig. 6.1 — Vocabulary overlap, drawn to scale. Two trainers, seven years apart, on different samples of “pages Reddit linked to”: 44,014 of 50,256 tokens come back byte-for-byte. Each side keeps 6,242 tokens the other never mints. data/rerun50k/comparison.json
REAL R50K (2019) RERUN (2026) 1950 1960 1970 1980 1990 2000 2010 2020 the wall re-forms one year off: run starts 1963 (real) vs 1962 (rerun); both end 2020 leading-space years “␣1900”–“␣1999”: 100 / 100 single tokens in BOTH vocabularies
Fig. 6.2 — The year wall re-forms. Filled cells are bare four-digit years that are single tokens; each strip’s cell states were recomputed from its vocabulary file. The unbroken run is 1963–2020 in real r50k and 1962–2020 in the rerun — off by one year at the old edge, identical at the new one. A few isolated years (1950, and 1959–60 vs 1960) sit ahead of each wall. data/rerun50k/comparison.json data/rerun50k/rerun50k-vocab.json

Came back

  • 9999 single token in both — the cold open reproduces exactly
  • 1776 1776 splits in both vocabularies
  • ----------------… separator walls: 64–66-char dash / underscore / ! runs top the rerun’s length chart; 3 pure ==== walls
  • Han-script scarcity 9 tokens with Han characters vs 51 in real r50k by the same sweep — same order of scarcity

Didn’t come back

  • ÃÂÃÂÃÂÃÂ… the mojibake ladder (ÃÂ ×2 … ×32) is entirely absent
  • SolidGoldMagikarp absent
  • davidjl petertodd TheNitromeFan no r/counting username survives — the pages aren’t in this sample, or fell to deduplication

New fossils of this corpus

  • dailycallernewsfoundation news-syndication boilerplate
  • |∵∵∵∴∵∴∵ an ASCII-art fragment — every corpus mints its own junk
Fig. 6.3 — Specimen comparison: what the 2026 rerun reproduces and what it doesn’t. Dashed chips mark tokens absent from the rerun vocabulary. data/rerun50k/comparison.json research/G-rerun50k.md

The pattern is clean. The head of the vocabulary — common words, years, formatting — is corpus-robust: any big-enough sample of the same distribution rediscovers it almost token for token, and anyone with a laptop can re-derive it in about a minute. The scars are corpus-specific. OpenWebText is deduplicated and cleaned, so the double-encoded mojibake and the r/counting ghosts, which lived in the dirty corners of the original crawl, never re-form. Instead the sample mints its own junk. That sharpens this dossier’s thesis: a tokenizer is a fossil record, and it is precisely the weird tokens that identify the stratum.

implementationcorpuspeak RSSwall time
pure Python (scripts/toy_bpe.py)3.6 MB176 MiB6 s per 1,000 merges
HF tokenizers (Rust), this rerun2.0 GiB2.20 GiB65 s, full 50k vocab, 16 cores
Fig. 6.4 — The practical answer to “what does training one cost”: measured memory and wall time for the two implementations run for this dossier. research/G-rerun50k.md
Scope This is a single sample and a single run: the corpus is the first 2 GiB of the OpenWebText stream, not a random draw. min_frequency=2 is the HF trainer’s default; OpenAI’s setting — and their exact pipeline — is undisclosed. BPE is deterministic given a corpus and regex, but the overlap percentage would shift some with a different sample.
Plate VII

The specimen drawer

64 strings, each cut four ways — search the collection, filter by tag

Every plate above comes down to the same move: hand a string to four vocabularies and watch where the knife falls. This drawer holds 64 specimens, pinned. Each card shows one string as r50k (GPT-2, GPT-3), p50k (Codex), cl100k (ChatGPT, GPT-4), and o200k (GPT-4o) cut it, with the token count at the end of each row.

A ␣ marks a leading space, and it belongs to the token — the and the are different vocabulary entries. Dashed chips are byte fragments that aren't valid UTF-8 on their own (each dot is one raw byte); byte-level BPE is allowed to cut mid-character, and for emoji and some scripts it does.

Worth trying: search 1999 for a year-wall resident, then 1776 to see the same era split 17|76 in r50k and 177|6 in cl100k. Search davidjl for the famous survivor. Toggle glitch and spam to browse the fossil bed; toggle whitespace to watch sixteen tokens of indentation become one.

64 of 64 specimens

tok leading space ·· invalid-UTF-8 fragment (1 dot = 1 byte) \t \n shown as escapes
1969years
r50k19691
p50k19691
cl100k19692
o200k19692
␣1999years
r50k19991
p50k19991
cl100k19993
o200k19993
1776years
r50k17762
p50k17762
cl100k17762
o200k17762
2020years
r50k20201
p50k20201
cl100k20202
o200k20202
9999years
r50k99991
p50k99991
cl100k99992
o200k99992
␣2025years
r50k20251
p50k20251
cl100k20253
o200k20253
␣1900years
r50k19001
p50k19001
cl100k19003
o200k19003
2048years
r50k20482
p50k20482
cl100k20482
o200k20482
3789numbers
r50k37892
p50k37892
cl100k37892
o200k37892
3,789numbers
r50k3,7893
p50k3,7893
cl100k3,7893
o200k3,7893
12345numbers
r50k123452
p50k123452
cl100k123452
o200k123452
0.999numbers
r50k0.9993
p50k0.9993
cl100k0.9993
o200k0.9993
March 14, 2019numbers
r50kMarch14,20194
p50kMarch14,20194
cl100kMarch14,20197
o200kMarch14,20197
314159265358979numbers
r50k3141592653589796
p50k3141592653589796
cl100k3141592653589795
o200k3141592653589795
1,000,000numbers
r50k1,000,0005
p50k1,000,0005
cl100k1,000,0005
o200k1,000,0005
3.14159numbers
r50k3.141594
p50k3.141594
cl100k3.141594
o200k3.141594
␣SolidGoldMagikarpglitch
r50kSolidGoldMagikarp1
p50kSolidGoldMagikarp1
cl100kSolidGoldMagikarp5
o200kSolidGoldMagikarp5
␣davidjlglitch
r50kdavidjl1
p50kdavidjl1
cl100kdavidjl1
o200kdavidjl2
␣TheNitromeFanglitch
r50kTheNitromeFan1
p50kTheNitromeFan1
cl100kTheNitromeFan4
o200kTheNitromeFan4
␣petertoddglitch
r50kpetertodd1
p50kpetertodd1
cl100kpetertodd3
o200kpetertodd3
␣attRotglitch
r50kattRot1
p50kattRot1
cl100kattRot2
o200kattRot2
␣サーティワンglitch
r50kサーティワン1
p50kサーティワン1
cl100k·············8
o200kーティ4
\tTokenNameIdentifierglitch
r50k\tTokenNameIdentifier5
p50k\tTokenNameIdentifier5
cl100k\tTokenNameIdentifier1
o200k\tTokenNameIdentifier3
$PostalCodesNLglitch
r50k$PostalCodesNL6
p50k$PostalCodesNL6
cl100k$PostalCodesNL1
o200k$PostalCodesNL4
␣RandomRedditorWithNoglitch
r50kRandomRedditorWithNo1
p50kRandomRedditorWithNo1
cl100kRandomRedditorWithNo5
o200kRandomRedditorWithNo6
cloneembedreportprintglitch
r50kcloneembedreportprint1
p50kcloneembedreportprint1
cl100kcloneembedreportprint4
o200kcloneembedreportprint4
␣␣␣␣def __init__(self):code
r50kdef__init__(self):10
p50k def__init__(self):8
cl100k def__init__(self):7
o200k def__init__(self):7
ClickListenercode
r50kClickListener2
p50kClickListener2
cl100kClickListener1
o200kClickListener2
␣findViewByIdcode
r50kfindViewById3
p50kfindViewById3
cl100kfindViewById1
o200kfindViewById4
.lengthcode
r50k.length2
p50k.length2
cl100k.length1
o200k.length1
16 spacescode
r50k16
p50k 1
cl100k 1
o200k 1
\t\t\tcode
r50k\t\t\t3
p50k\t\t\t3
cl100k\t\t\t1
o200k\t\t\t1
for (let i = 0; i < n; i++)code
r50kfor(leti=0;i<n;i++)13
p50kfor(leti=0;i<n;i++)13
cl100kfor(leti=0;i<n;i++)14
o200kfor(leti=0;i<n;i++)14
import numpy as npcode
r50kimportnumpyasnp5
p50kimportnumpyasnp5
cl100kimportnumpyasnp4
o200kimportnumpyasnp4
␣console.logcode
r50kconsole.log3
p50kconsole.log3
cl100kconsole.log2
o200kconsole.log2
</div>code
r50k</div>3
p50k</div>3
cl100k</div>3
o200k</div>3
hello worldmultilingual
r50khelloworld2
p50khelloworld2
cl100khelloworld2
o200khelloworld2
你好世界multilingual
r50k············8
p50k············8
cl100k···5
o200k你好世界2
こんにちはmultilingual
r50k···6
p50k···6
cl100kこんにちは1
o200kこんにちは1
мирmultilingual
r50kмир3
p50kмир3
cl100kмир2
o200kмир2
مرحباmultilingual
r50kمر··با6
p50kمر··با6
cl100kمرحبا5
o200kمرحبا2
🤖multilingual
r50k····3
p50k····3
cl100k····3
o200k····2
naïvemultilingual
r50knaïve2
p50knaïve2
cl100knaïve3
o200knaïve3
မင်္ဂလာပါmultilingual
r50k···························27
p50k···························27
cl100k···························18
o200kင်လာပါ6
안녕하세요multilingual
r50k···············14
p50k···············14
cl100k······하세요5
o200k녕하세요2
Привет мирmultilingual
r50k··ривет···ир11
p50k··ривет···ир11
cl100kПриветмир5
o200kПриветмир3
שלוםmultilingual
r50kשלו··5
p50kשלו··5
cl100kשל····4
o200kשלום1
_日本毛片免费视频观看spam
r50k_······························26
p50k_······························26
cl100k_······视频···13
o200k_日本毛片免费视频观看1
␣微信公众号天天中彩票spam
r50k······················19
p50k······················19
cl100k··········13
o200k微信公众号天天中彩票1
␣theenglish
r50kthe1
p50kthe1
cl100kthe1
o200kthe1
unbelievableenglish
r50kunbelievable4
p50kunbelievable4
cl100kunbelievable3
o200kunbelievable3
␣antidisestablishmentarianismenglish
r50kantidisestablishmentarianism5
p50kantidisestablishmentarianism5
cl100kantidisestablishmentarianism6
o200kantidisestablishmentarianism6
don'tenglish
r50kdon't2
p50kdon't2
cl100kdon't2
o200kdon't1
DON'Tenglish
r50kDON'T3
p50kDON'T3
cl100kDON'T2
o200kDON'T2
␣New Yorkenglish
r50kNewYork2
p50kNewYork2
cl100kNewYork2
o200kNewYork2
supercalifragilisticexpialidociousenglish
r50ksupercalifragilisticexpialidocious11
p50ksupercalifragilisticexpialidocious11
cl100ksupercalifragilisticexpialidocious11
o200ksupercalifragilisticexpialidocious10
e.e. cummingsenglish
r50ke.e.cummings7
p50ke.e.cummings7
cl100ke.e.cummings6
o200ke.e.cummings5
␣strawberryenglish
r50kstrawberry1
p50kstrawberry1
cl100kstrawberry1
o200kstrawberry1
␣queueingenglish
r50kqueueing2
p50kqueueing2
cl100kqueueing2
o200kqueueing2
2 spaceswhitespace
r50k2
p50k 1
cl100k 1
o200k 1
8 spaceswhitespace
r50k8
p50k 1
cl100k 1
o200k 1
25 spaceswhitespace
r50k25
p50k 1
cl100k 1
o200k 1
␣\n␣whitespace
r50k\n3
p50k\n3
cl100k\n2
o200k\n2
\r\nwhitespace
r50k\r\n2
p50k\r\n2
cl100k\r\n1
o200k\r\n1
Fig. 7.1 — 64 specimens × 4 vocabularies; tokenizations computed with tiktoken 0.13.0. scripts/gen_drawer_data.pydata/drawer.json
ai gen