Transformers from the Weights Up/Part 7 of 20
Transformers from the Weights Up, Part 7: Tokenization (BPE) and embeddings
A tokenizer turns text into integers, and embed_tokens turns each integer into a vector by row lookup. The vocabulary quietly sets sequence length, a slice of the parameter budget, and some of a model's blind spots.
5 Nov 2026
- 1
Pre-tokenize
A regex splits text into chunks (words with their leading space, runs of up to 3 digits, punctuation runs, whitespace). Merges never cross chunk boundaries.
- 2
Bytes
Each chunk becomes UTF-8 bytes. With 256 byte tokens as the base vocabulary, any string at all can be encoded: there is no unknown token.
- 3
Merge
Apply the learned merge list in rank order until no learned pair remains. Llama 3 has 128,000 regular tokens, built from its byte base by merges.
- 4
Ids → vectors
Add special tokens such as <|begin_of_text|> (id 128000), then gather rows of embed_tokens: [T] ints → [T, 4096] bf16. That tensor is the residual stream's starting point (image 7).
Byte-pair encoding: learning the merges
BPE began as a 1994 compression trick and was adapted to neural machine translation by Sennrich, Haddow and Birch (2015). Training is greedy and purely statistical: start with a base alphabet, count every adjacent pair of symbols in the corpus (weighted by how often each pre-tokenized chunk occurs), merge the most frequent pair into a new symbol, record that merge, and repeat. The output is two artifacts: a vocabulary and an ordered merge list. The order matters, because encoding replays it.
Step it to the end and look at what happened. The suffix "est_" was built first, because it is shared by two frequent words; "low" came next; "newest" became a single token, as did "low_". The rare word "widest" stays in pieces. That is the general shape at scale: common words and common subwords get their own ids, and rare words are spelled out of reusable fragments. Ten merges took the toy corpus from 95 symbol occurrences to 28.
Encoding: applying merges in rank order
Encoding a new string does not search for the "best" segmentation. Within each pre-tokenized chunk it finds the adjacent pair with the lowest merge rank, merges it everywhere in the chunk, and repeats until no learned pair is left. It is deterministic and fast (tiktoken and Hugging Face tokenizers do it in Rust), and it means a word's tokens depend on merge history, not on meaning.
- Tokens
- 13
- Characters (incl. word ends)
- 24
- Characters per token
- 1.85
Llama 3's pre-tokenizer is the regex below, from Meta's reference tokenizer.py. It attaches a word's leading space to the word (so " the" and "the" are different tokens), keeps English contractions as their own chunks, and caps digit runs at three, so "2026" is always split as "202" + "6" before any merge can see it.
(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+| Behavior you have seen | Mechanism in the tokenizer |
|---|---|
| Counting letters in a word is surprisingly hard | The model receives one id for a whole word or a few chunks; the spelling is never in its input and must be learned indirectly. |
| Arithmetic on long numbers is brittle | Digits arrive in chunks of up to 3, and the chunk boundaries come from the left, so the same place value lands in different positions within its token. |
| Leading spaces and capitalization change outputs | " Paris", "Paris" and " paris" are different ids with separately learned embeddings. |
| Non-English text costs more per word | Fewer merges were learned for it, so the same content takes more tokens: more compute, more KV cache, less fits in context. Llama 3's extra 28K tokens target exactly this. |
| Glitch tokens | A token that won a merge in the tokenizer's corpus but barely appears in the model's training data keeps a near-random embedding (next section). Land and Bartolo (2024) built detectors for these under-trained tokens across many open models. |
The embedding table is a [V, d] lookup
embed_tokens.weight in Llama-3-8B is a [128256, 4096] bf16 tensor: 525,336,576 parameters, 1.05 GB. Row i is token i's learned vector. Conceptually the input is a one-hot vector eᵢ ∈ ℝ^V multiplied by the table, but nobody does that matmul: it selects one row, so the implementation is a gather.
The original Transformer multiplied embeddings by √d_model; Llama does not scale them, and every block reads an RMSNorm-ed copy of the stream anyway (Lesson 11). Token embeddings carry no information about position: "the" at position 1 and position 400 start as the identical vector. Position enters inside attention, through RoPE (Lesson 10).
Vocab size trade-offs, tied and untied
A bigger vocabulary compresses text into fewer tokens. Every downstream cost that scales with sequence length (attention's T² work, KV-cache bytes, the context window, and the number of decode steps for a given answer) gets cheaper. The price is paid in the two [V, d] tables, in the lm_head matmul and softmax on every position, and in rarer tokens getting fewer gradient updates each.
| Tokenizer | V | English chars per token | Tokens for 10,000 chars |
|---|---|---|---|
| Llama 2 (SentencePiece BPE) | 32,000 | 3.17 | ≈ 3,155 |
| Llama 3 (tiktoken-based BPE) | 128,256 | 3.94 | ≈ 2,538 (19.5% fewer) |
Compression figures are from the Llama 3 paper, measured on "a sample of English data"; the 10,000-character column is just arithmetic on them.
- One [V, d] table
- 128,256 × 4096 = 525,336,576 params, 1.05 GB
- Vocabulary share of all params
- 13.1% of 8.03B
- lm_head FLOPs per token
- 2·V·d = 1.051 GFLOP, 6.5% of the ≈ 2N forward
- fp32 logits per position
- 4·V = 513 KB
Tying means using one matrix for both ends: logits = h Eᵀ. Press and Wolf (2016) showed that sharing the input and output embeddings lowers perplexity in language models while removing a whole table, and GPT-2 does it. The trade is pure budget: in GPT-2 small, the shared 50,257 × 768 table is 38.6M of 124.4M parameters (31%). Llama-3.2-1B ties its 128,256 × 2048 table (262.7M of 1.24B, 21.3%); with a separate lm_head it would carry another 262.7M. Llama-3-8B unties: its two tables total 1,050,673,152 parameters, 13.1% of the model, and the output side gets to learn "what predicts token i" separately from "what token i means as input."
Lesson 7: key numbers
- 128,256
- Llama 3 vocabulary128,000 BPE tokens + 256 reserved special ids
- 525.3M
- params per [128256, 4096] table1.05 GB in bf16
- 13.1%
- of Llama-3-8B is vocabulary tablesembed_tokens + untied lm_head = 1,050,673,152
- 8 KB
- embedding bytes read per tokenone bf16 row of 4096
- 3.17 → 3.94
- English chars per token, Llama 2 → 3≈ 19.5% fewer tokens for the same text
- 50,257
- GPT-2 vocabulary256 bytes + 50,000 merges + 1 special
- 1.05 GFLOP
- lm_head per token, Llama-3-8B2·V·d, ≈ 6.5% of the forward pass
- 21.3%
- Llama-3.2-1B's tied table share262.7M of 1.24B params
Lesson 7: self-check
1. In BPE training, a pair (a, b) has the highest count. What exactly gets added to the tokenizer, and why does the order of additions matter?
2. Why can byte-level BPE encode any string, and what is the cost?
3. Llama-3-8B processes the id 9906 at one position. What tensor slice does that touch, and how many bytes does it read at bf16?
4. A batch contains the token ' the' 300 times. What gradient does its embedding row receive?
5. Why does Lesson 4's streamed-parameter count for decode exclude embed_tokens but include lm_head?
6. You double the vocabulary of a Llama-3-8B-shaped model to 256,512 and keep everything else. What grows, and roughly by how much?
7. Why might a model produce garbage when it sees a particular rare token?
Lesson 7: resources
- Let's build the GPT TokenizerYouTube · Andrej Karpathy
- Neural Machine Translation of Rare Words with Subword UnitsSennrich, Haddow & Birch, 2015 (arXiv)
- Byte-Pair Encoding tokenizationHugging Face LLM Course, chapter 6
- Using the Output Embedding to Improve Language ModelsPress & Wolf, 2016 (arXiv)
- Fishing for Magikarp: Automatically Detecting Under-trained Tokens in LLMsLand & Bartolo, 2024 (arXiv)