Build1 publisher3 min readPublished
tiktoken keeps its only byte-pair trainer in a file named _educational.py
Encoding replays an ordered merge list that was learned once, before training. Swap the list and the weights still load, but the model you tested is gone.
The Engineer · Build desk

What happened
- The function that turns text into ids is frozen the day a model is trained and ships as part of the artifact, so swapping it leaves the weights in place and the model gone.
- tiktoken's Rust core ships no trainer, which leaves bpe_train at _educational.py lines 119 to 185 as the only copy of the training loop in the package.
- Training starts at 256 tokens, one per byte, then repeatedly takes the most frequent adjacent pair, gives it the next id and replaces it through the corpus until the vocabulary hits the requested size.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint The vocabulary is the one component you cannot patch after shipping: the embedding rows were trained against those exact ids, so changing the merge list means training again.
- exposure A mismatched tokenizer has no error path. Bytes always encode, so a wrong vocabulary in a pipeline returns worse output instead of an exception a test can catch.
- cost Per-language serving cost is set by which byte pairs happened to merge during training, and users writing scripts that never merged pay more positions for the same sentence.
- decision Teams serving non-Latin scripts have to weigh the vocabulary alongside the weights when picking a model, because the token count per character is fixed for the artifact's life.
Encoding replays the merge list. Split the input into bytes, find the adjacent pair with the lowest merge rank, merge it, then repeat until nothing merges [11]. The educational encoder that does this is thirty-four lines and never iterates the merge list [11]. Training takes the most frequent pair and encoding takes the lowest-ranked one, which the post says comes to the same thing [12]. So the vocabulary is the merge list in order, and the post calls that order the whole model of the language [10].
Subwords exist because both ends of the range fail: characters give a tiny vocabulary and sequences far too long, while whole words give short sequences, an unbounded vocabulary and a hole for every word missing from it [4][5]. Sennrich, Haddow and Birch pointed byte-pair encoding, a compression algorithm from 1994, at that problem in 2016 [6]. The author read tiktoken's `_educational.py` and checked its encoder against the Rust core by running both over every sample in the post and comparing ids [8].
Order makes ties matter. The post's interactive widget runs the loop on a toy corpus, and twelve of its eighteen steps are ties, settled by the rule that the first pair seen wins [13]. Two thirds of that demonstration's merges were decided by iteration order [1]. Whether the ratio survives on a real corpus depends on how flat the pair counts are, and the post measures ties only on the toy corpus [13].
Because the merges run on bytes, any input encodes, and a character the merges never saw costs up to one token per UTF-8 byte instead of raising [14]. Under GPT-2 that puts the character ss-ligature `ß` at one token, `漢` at three, and a Gothic letter at four [15]. `o200k_base` has `漢` whole; the Gothic letter still costs four everywhere [16]. A lexer that met bytes it did not recognise would throw, and this one just bills you.
Attention costs the square of the sequence length [4]. Text made entirely of characters that encode at three tokens each occupies three times the positions of the same text under a vocabulary that holds them whole, so roughly nine times the attention work [2]. For that factor to transfer to your traffic, the documents have to be almost all such characters. Mixed text pays the penalty only on the share that never merged.
There is a second stage before any of this. A regex splits the text into chunks and merges never cross chunk boundaries, and GPT-2's pattern keeps a leading space attached to the word after it, so " token" and "token" are different tokens [17]. Every id has its own row in the embedding matrix [3]. Two strings differing only in a leading space therefore reach the first matrix multiply as different vectors [3].
That is why the vocabulary is fixed before training: the embedding rows were trained against those ids [3][1]. The author, who came to machine learning from compilers, wrote: "I came to this from the compiler side, where the parser is something you own and can change on a Tuesday. Here you own it exactly once, before training, and never again." [2]
What to watch
- Whether tiktoken's Rust core ever ships a trainer, which would move vocabulary training out of a file named _educational.py.
- A tie count measured on a production-scale corpus, which would show whether the toy widget's two-thirds ratio is an artifact of small data.
- Vocabularies after o200k_base, and whether scripts still encoded one token per byte, such as Gothic, get whole-character ids.