Tokenization at Scale
Every large language model faces a problem before it sees a single sentence of grammar. Text arrives as raw Unicode bytes. But neural networks operate on fixed-size vectors from a finite vocabulary. Somewhere between a string like "unbelievably" and a matrix of embeddings, a decision must be made. What are the atomic units of meaning — whole words, characters, or something in between? Get this wrong and the model faces trouble. Either its vocabulary table explodes into millions of rarely-seen word forms. Or it shreds every sentence into so many characters. A 2,000-word document then no longer fits the Full Transformer Architecture's context window. Tokenization is the answer. It is a preprocessing algorithm that learns, from a large text corpus, a fixed vocabulary of subword units. It also defines a deterministic procedure for splitting any new string into a sequence of those units. This sounds like a minor engineering detail. But it is one of the highest-leverage design choices in an LLM pipeline. It fixes the effective sequence length of every document the model will ever see. It determines how gracefully the model handles typos, code, emoji, and rare languages. And it quietly shapes tokens-per-dollar economics at inference time. This article covers the three subword algorithms that power essentially every modern large language model. These are Byte-Pair Encoding (BPE), WordPiece, and SentencePiece. SentencePiece is technically a framework wrapping BPE/unigram variants, not a fourth algorithm. All three solve the same underlying tension. A vocabulary of whole words is too large and has a fatal out-of-vocabulary problem. A vocabulary of characters is too small and produces impractically long sequences. Subword tokenization threads the needle. Common words become single tokens, and rare words decompose into meaningful pieces. Unseen strings can still always be represented as some sequence of known pieces. By the end you will be able to trace exactly how a BPE merge table is built and applied. You will understand the greedy longest-match logic of WordPiece. You will see why SentencePiece treats whitespace as a first-class symbol. This makes tokenization fully reversible. You will also be able to reason about the complexity and failure modes of each. This groundwork feeds directly into how BERT and Masked Language Modeling and GPT and Autoregressive Language Modeling represent their inputs. It is also one of the hidden variables behind Scaling Laws. Compute-optimal scaling is measured in tokens, and what counts as a token is exactly what this article defines.
- Subword tokenization balances vocabulary size against sequence length by splitting text into frequent, reusable pieces rather than whole words or single characters.
- Byte-Pair Encoding (BPE) greedily merges the most frequent adjacent symbol pair, repeated a fixed number of times, to build a merge-ordered vocabulary.
- WordPiece is similar to BPE but scores candidate merges by a likelihood ratio, not raw frequency, and tokenizes greedily via longest-prefix matching at inference.
- SentencePiece is a language-agnostic framework that treats input as a raw Unicode/byte stream (no pre-tokenization on whitespace) and implements both BPE and a unigram language-model tokenizer.
- Tokenization is fully deterministic and reversible in production systems, which is essential for both training-serving consistency and lossless detokenization.
- The choice of vocabulary size and algorithm directly affects sequence length, training compute, and the token-based accounting used in Scaling Laws.
Subword tokenization learns a fixed vocabulary of frequent character sequences so that any string — common word, rare word, typo, or foreign script — can be losslessly encoded as a short sequence of reusable, statistically efficient units.
The idea
A tokenizer maps a string of raw text into a sequence of integers, each indexing an entry in a fixed vocabulary learned from training data. The naive choices sit at two extremes. A word-level vocabulary treats each whitespace-delimited word as one token; this keeps sequences short but forces an enormous vocabulary (English alone has hundreds of thousands of inflected forms), and any word not seen during vocabulary construction becomes an unrecoverable out-of-vocabulary (OOV) token, typically mapped to a generic `` symbol with total loss of information. A character-level vocabulary solves OOV entirely — every string is composed of a small, closed set of characters — but produces sequences several times longer than word-level ones, which is expensive because the Full Transformer Architecture's self-attention costs grow quadratically in sequence length.
Subword tokenization is the compromise that all modern LLMs use. The vocabulary contains a mixture of common whole words (`the`, `ing`), frequent word fragments (`##ing`, `un`, `▁the`), and, critically, every individual byte or character as a fallback. This guarantees two properties simultaneously: no OOV tokens (any string can be decomposed down to guaranteed-present atomic symbols) and short sequences in practice (because frequent strings are represented by single large tokens learned from the data distribution). The vocabulary is not hand-designed; it is learned by an algorithm that scans a large corpus and decides which character sequences are common enough to deserve their own token. Three families dominate practice. Byte-Pair Encoding (BPE), adapted from a 1994 data-compression algorithm, iteratively merges the most frequent adjacent pair of symbols. WordPiece, developed at Google for machine translation and later used in BERT, is algorithmically similar to BPE but chooses merges using a likelihood-based score rather than raw frequency. SentencePiece is not a separate merge algorithm but an implementation framework: it treats the input as a raw stream of Unicode code points (or bytes), explicitly encodes whitespace as a visible symbol (conventionally `▁`, U+2581), and offers both a BPE backend and a probabilistic unigram language model tokenizer as pluggable segmentation strategies. This design makes tokenization fully reversible and language-agnostic, since it never assumes whitespace-delimited words exist (a property essential for Chinese, Japanese, Thai, and other languages without word-boundary spaces).
How it works
Byte-Pair Encoding (BPE)
BPE training operates on a corpus that has first been pre-tokenized into words (via whitespace/punctuation splitting), each word represented as a sequence of characters with a special end-of-word marker. Training proceeds greedily: at each step, count every adjacent symbol pair across the entire corpus, merge the single most frequent pair into a new symbol, and repeat for a fixed number of merges. The sequence of merges is the model — it defines a deterministic, ordered vocabulary. ``` ALGORITHM: BPE-Train INPUT: corpus (list of words with frequencies), num_merges K OUTPUT: ordered list of merge rules, final vocabulary 1. vocab_word ← map each word w to a sequence of symbols: the characters of w, each followed by "" appended only to the LAST character (marks word boundary) 2. merges ← empty ordered list 3. symbol_set ← set of all individual characters appearing (initial vocab) 4. for step in 1..K: 5. pair_counts ← empty map (pair -> count) 6. for each (word, freq) in vocab_word: 7. symbols ← current symbol sequence for word 8. for i in 0..len(symbols)-2: 9. pair ← (symbols[i], symbols[i+1]) 10. pair_counts[pair] += freq 11. if pair_counts is empty: 12. break // no more pairs to merge 13. best_pair ← argmax over pair_counts by count 14. new_symbol ← concat(best_pair[0], best_pair[1]) 15. merges.append(best_pair -> new_symbol) 16. symbol_set.add(new_symbol) 17. // apply this merge everywhere it occurs, left to right 18. for each (word, freq) in vocab_word: 19. vocab_word[word] ← replace all occurrences of best_pair in symbols with new_symbol 20. return merges, symbol_set ``` ``` ALGORITHM: BPE-Encode (apply learned merges to new text) INPUT: word (string), ordered merges (from training) OUTPUT: list of subword tokens 1. symbols ← characters of word, with "" on the last one 2. loop: 3. candidate_pairs ← all adjacent pairs currently in symbols 4. if candidate_pairs is empty: break 5. // find the pair that appears EARLIEST in the merge order 6. applicable ← {p in candidate_pairs : p in merges} 7. if applicable is empty: break 8. best ← the pair in applicable with the SMALLEST merge rank (i.e., learned earliest during training) 9. merge best into a single symbol, everywhere it occurs 10. return symbols ``` The encoder never recomputes frequencies at inference time — it replays the training merge order on new text, always applying whichever learnable merge occurs earliest in that order. This is what makes tokenization deterministic and identical between training and serving.
WordPiece
WordPiece (used in BERT and its descendants) trains almost identically to BPE, but changes the scoring function for choosing which pair to merge. Instead of raw co-occurrence count, it scores a candidate merge of symbols `x` and `y` by an approximation of the mutual-information gain from treating `xy` as one unit:
This down-weights merges where `x` or `y` is individually extremely common (like a single vowel), preferring merges that are surprising given the base rates — closer to a pointwise mutual information criterion than a raw frequency count. Everything else about training (iterative merges, fixed target vocabulary size) mirrors BPE. WordPiece's encoding procedure at inference time is also distinct: rather than replaying merges, it uses greedy longest-match-first segmentation over the fixed learned vocabulary. ``` ALGORITHM: WordPiece-Encode INPUT: word (string), vocabulary V (set of known tokens, continuation tokens prefixed with "##") OUTPUT: list of tokens, or [UNK] if unsegmentable 1. tokens ← empty list 2. start ← 0 3. n ← length(word) 4. while start < n: 5. end ← n 6. cur_token ← null 7. while end > start: 8. substr ← word[start:end] 9. if start > 0: substr ← "##" + substr 10. if substr in V: 11. cur_token ← substr 12. break 13. end ← end - 1 14. if cur_token is null: 15. return [UNK] // word cannot be tokenized 16. tokens.append(cur_token) 17. start ← end 18. return tokens ``` This is a greedy longest-prefix match, re-run from each successive starting position, with the `##` prefix marking "this piece continues the previous one" (so `unbelievable` becomes `un`, `##believ`, `##able` rather than three ambiguous free-standing pieces).
SentencePiece
SentencePiece is best understood as a preprocessing and interface layer, not a competing scoring rule. Its key design decisions: 1. No language-specific pre-tokenization. Raw input (including whitespace) is treated as one long Unicode sequence. Spaces are replaced with the visible meta-symbol `▁` before training, so tokenization and detokenization become a pure, lossless string operation: `decode(encode(x)) == x` exactly, byte-for-byte. 2. Pluggable model: SentencePiece can run BPE (as above) or a unigram language model tokenizer. The unigram model works in the opposite direction from BPE/WordPiece: it starts from a large candidate vocabulary (e.g., all substrings up to some length, or the output of a BPE pass) and iteratively prunes it down to the target size. ``` ALGORITHM: Unigram-Train (SentencePiece unigram mode) INPUT: corpus, initial large seed vocabulary, target size T OUTPUT: vocabulary of size T with unigram probabilities 1. V ← seed vocabulary (each piece v with probability p(v), initialized e.g. proportional to frequency) 2. repeat: 3. // E-step: for each word, find its most likely (or 4. // marginalize over all) segmentations under current V 5. // using the Viterbi algorithm over a lattice of substrings 6. run EM to re-estimate p(v) for all v in V, maximizing corpus log-likelihood: L = sum over words w of log( sum over segmentations s of w of prod_{v in s} p(v) ) 7. // pruning: for each piece v, compute how much L would 8. // DROP if v were removed from V (its "loss contribution") 9. remove the bottom fraction (e.g. 20%) of pieces by loss contribution, always keeping single characters 10. if |V| <= T: break 11. return V, p ``` At inference, unigram-mode SentencePiece finds the segmentation of a string that maximizes total sequence probability (product of piece probabilities) using the Viterbi algorithm over the directed acyclic graph of all substrings that are valid vocabulary pieces — an exact dynamic program, not a greedy heuristic like WordPiece's longest-match.
Complexity & correctness
Training complexity. A naive implementation of BPE-Train recomputes all pair counts from scratch at every merge step. With corpus vocabulary of distinct words (total length symbols) and target merges, this costs in the worst case, since each merge step rescans the whole corpus. Production implementations avoid this by maintaining a priority queue of pair counts and only updating the counts of pairs adjacent to a merge site after each step, since a single merge can only change counts local to occurrences of that pair — this reduces the practical cost to roughly over the whole training run, dominated by priority-queue operations, since only a bounded number of pair-count entries change per merge and each change is an heap update. WordPiece training has the same asymptotic profile, since it differs from BPE only in the score computed per candidate pair (still per pair per step given running counts). Unigram (SentencePiece) training is more expensive per iteration: each EM step must run a Viterbi decode over every word in the corpus to estimate expected piece usage. For a word of length and a vocabulary where any substring might be a piece, exact Viterbi over the substring lattice costs per word, where bounds the maximum piece length considered — summed over the corpus this is per EM iteration, repeated for a handful of iterations at each pruning round, and pruning rounds themselves repeat times to shrink from seed size to target . This is why unigram training is noticeably slower than BPE/WordPiece training in practice, though still entirely feasible offline on large corpora.
Encoding complexity (the cost that matters at serving time). This is the number that dominates production latency, since encoding runs on every request:
- BPE encode: naively per word of length (rescan all adjacent pairs after every merge), but with a priority queue over applicable merges this is .
- WordPiece encode: the greedy longest-match scan is in the worst case per word (for each of start positions, scan backward up to characters), though a trie-based vocabulary lookup makes the common case close to linear, where is average matched token length.
- Unigram encode (SentencePiece): exact Viterbi over the lattice costs where is the maximum piece length — a clean dynamic program, not a heuristic.
Correctness argument. The key invariant across all three algorithms is: every character (or byte) individually is guaranteed to be in the final vocabulary, because training always seeds the vocabulary with the full base alphabet before any merges are considered, and merges only ever combine existing symbols rather than deleting them. This one invariant is what eliminates the OOV problem entirely: in the worst case, a novel string just falls back to its raw characters/bytes, never to an information-destroying ``. (Some implementations, including certain vocabularies, do retain an explicit `` for pieces outside a byte-level alphabet — but byte-level BPE, used in GPT-2 and later models, guarantees full coverage of all 256 byte values, making `` provably unreachable.) A second correctness property is determinism: given a fixed vocabulary/merge table, encoding a string always produces the same token sequence, both in training and in serving. This matters enormously in practice — a model trained on one tokenization of a string and served with a different tokenization of the same string will see out-of-distribution input embeddings it was never trained on, silently degrading quality. Reproducing the exact training-time merge order (BPE) or the exact greedy scan (WordPiece) or the exact Viterbi optimum (unigram) is what guarantees this consistency.
Tradeoffs, honestly. BPE's greedy, purely frequency-driven merges can produce linguistically awkward splits (frequent character pairs that cross morpheme boundaries), and its encode is a heuristic replay, not a global optimum. WordPiece's likelihood-ratio score better respects statistical independence, but its greedy longest-match encoder can make a locally optimal choice that is not globally optimal — a longer initial match can sometimes force an ugly `[UNK]`-adjacent tail, whereas a shorter first match would have allowed clean coverage. Unigram's Viterbi encoding is provably optimal given the fixed piece probabilities, and its probabilistic framing additionally supports subword regularization (sampling alternate valid segmentations during training as data augmentation) — but training cost is higher, and the pruning heuristic (which pieces to drop each round) is itself an approximation, not exact marginal-likelihood maximization.
Worked example
Consider training a tiny BPE tokenizer on a corpus with these word frequencies (a toy example, chosen to expose real merge-order subtleties, not a trivial single-word case):
| word | frequency |
|---|---|
| `low` | 5 |
| `lower` | 2 |
| `newest` | 6 |
| `widest` | 3 |
Initial symbol sequences (character-level, `` marks end of word): ``` l o w (freq 5) l o w e r (freq 2) n e w e s t (freq 6) w i d e s t (freq 3) ```
Step 1 — count all adjacent pairs (weighted by word frequency):
| pair | count |
|---|---|
| (e, s) | 6+3 = 9 |
| (s, t) | 6+3 = 9 |
| (l, o) | 5+2 = 7 |
| (o, w) | 5+2 = 7 |
| (w, e) | 2+6 = 8 |
| (n, e) | 6 |
| (t, ``) | 9 |
| (w, i) | 3 |
| (i, d) | 3 |
| (d, e) | 3 |
| (r, ``) | 2 |
| (w, ``) | 5 |
The three-way tie between `(e,s)`, `(s,t)`, `(t, )` at 9 is broken by a fixed rule (e.g., leftmost-first in a canonical ordering); take `(e, s)` → merge into `es`.
Step 2 — apply merge `e s → es` everywhere: ``` l o w l o w e r n e w es t w i d es t ``` Recount: now `(es, t)` has count 6+3 = 9 — still the maximum. Merge `es t → est`. ``` l o w l o w e r n e w est w i d est ``` Next highest: `(est, )` at 9. Merge `est → est`. ``` l o w l o w e r n e w est w i d est ``` Next: `(l, o)` and `(o, w)` are now tied at 7 (5+2 each, present in `low` and `lower`). Take `(l,o)` first (leftmost rule) → merge `l o → lo`. ``` lo w lo w e r n e w est w i d est ``` Next: `(lo, w)` now at 7. Merge → `low`. ``` low low e r n e w est w i d est ``` After six merges the vocabulary contains: the original characters, plus `es`, `est`, `est`, `lo`, `low`, and so on. Note the emergent structure: `est` becomes a single reusable token that fires on both `newest` and `widest`, despite those words sharing no prefix — the algorithm discovered a shared suffix purely from co-occurrence statistics, exactly the kind of subword generalization that lets an unseen word like `"lowest"` (never in the training corpus) tokenize cleanly as `low` + `est` at inference time, rather than falling back to six separate characters or an ``.
Encoding `"lowest"` with the learned merges (replaying merge order): start `l o w e s t ` → apply `e s → es` (learned early) → `l o w es t ` → apply `es t → est` → `l o w est ` → apply `est → est` → `l o w est` → apply `l o → lo` → `lo w est` → apply `lo w → low` → `low est`. Final tokenization: `low`, `est</w>` — two tokens, both drawn from a vocabulary built without ever seeing the word `lowest`, demonstrating the OOV-free generalization property directly.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Mismatched tokenizer between training and serving | Different library version, different merge file, or re-training vocabulary without updating the model | Silent distribution shift — inputs the model was never trained on, degraded quality with no error thrown |
| Ignoring case/Unicode normalization inconsistency | Corpus normalized (NFKC) during training but raw user input isn't at inference | Same word tokenizes differently, hurting rare-word coverage |
| Assuming whitespace = word boundary for all languages | Baked into naive pre-tokenization before BPE runs | Breaks badly on Chinese, Japanese, Thai and other non-whitespace-segmented scripts |
| Treating token count ≈ word count in cost estimates | Subword tokenizers split rare words, code, and non-English text into many more pieces than words | Underestimates context usage and inference cost, especially for multilingual or code-heavy input |
| Off-by-one in longest-match scanning (WordPiece) | Loop bound errors when shrinking the candidate substring window | Missed valid matches, spurious `[UNK]` tokens |
| Forgetting the `` / `▁` boundary marker | Treating "cat" mid-sentence and "cat" as a whole word as identical strings during merge counting | Loses information about whether a piece starts a word, hurting downstream detokenization and model quality |
Pioneers & history
Byte-Pair Encoding began life not as an NLP idea at all, but as a general-purpose data compression trick, later repurposed for subword vocabulary construction and popularized in neural machine translation before becoming the default tokenizer family for GPT-style models.
Origins · 1 minPhilip Gage and how this was discovered
Gage described BPE in 1994 as a simple, general byte-level compression method: repeatedly replace the most frequent adjacent pair of bytes with a single unused byte value. The algorithm was designed purely for compression ratio, with no notion of language or vocabulary — its repurposing for subword tokenization, over two decades later, is a striking example of an idea migrating across fields.
Where this matters
Nearly every production LLM depends on one of these three families. GPT-2 and its successors use a byte-level BPE variant, operating on raw UTF-8 bytes rather than Unicode characters, which guarantees full coverage of any input string (including malformed text, emoji, and arbitrary binary-looking sequences) without ever needing an `` token. BERT and many of its derivatives use WordPiece, with the `##` continuation-prefix convention still visible in tokenizer outputs today. Multilingual and instruction-tuned models — including T5, XLNet, ALBERT, and most Llama-family models — rely on SentencePiece, precisely because its whitespace-as-symbol design avoids privileging any one language's word-segmentation conventions, which matters enormously for fair multilingual coverage. Beyond individual models, tokenization choices ripple into system-level engineering: API pricing for commercial LLMs is billed per token, so the tokenizer's compression efficiency on a given language directly determines cost; context-window budgeting (how much text fits in, say, 128K tokens) depends on the average tokens-per-word ratio of the tokenizer for the language and domain in question; and the Scaling Laws literature measures model and dataset size in tokens, making the tokenizer an implicit unit-of-account that must be held fixed for fair comparisons across models. Code-focused models additionally require tokenizers tuned to preserve meaningful substructure in identifiers, indentation, and punctuation — a domain where naive whitespace-based pre-tokenization performs especially poorly, motivating custom vocabularies trained on code corpora.
Ask the tutor: How would changing a model's tokenizer vocabulary size affect its position on a compute-optimal scaling curve?
Why not just use whole words as tokens, given that word embeddings work well in principle?
Whole-word vocabularies suffer from an unavoidable out-of-vocabulary problem: any word not seen during vocabulary construction — a typo, a new product name, a rare technical term, a word in an unseen language — has no representation and must be mapped to a generic `` token, destroying information. Word-level vocabularies also grow enormous for morphologically rich languages (Finnish, Turkish, German compounds), since every inflected form counts as a separate token. Subword tokenization keeps common words as single tokens for efficiency while guaranteeing, via a base alphabet fallback, that literally any string can be represented without loss, at the cost of occasionally splitting a rare word into multiple pieces.
Is SentencePiece a different algorithm from BPE and WordPiece, or something else entirely?
SentencePiece is a software framework and encoding convention, not a fourth scoring algorithm. It implements BPE as one of its backend segmentation modes and additionally offers a probabilistic unigram language-model tokenizer as an alternative backend. Its distinguishing contribution is treating raw text — including whitespace — as an undifferentiated stream of Unicode code points or bytes, with no language-specific pre-tokenization step, which makes the whole pipeline reversible and language-agnostic. In practice, "SentencePiece" in a model's documentation usually specifies this framework plus a choice of BPE or unigram mode underneath it.
How does byte-level BPE differ from character-level BPE, and why do models like GPT-2 use it?
Character-level BPE operates over a corpus's observed Unicode characters, which means characters never seen in training (rare scripts, unusual symbols) still cannot be represented and require a fallback ``. Byte-level BPE instead operates over the 256 possible raw byte values that any UTF-8-encoded string decomposes into, so the base alphabet is always exactly 256 symbols regardless of language or script. This guarantees complete coverage of any input string, including malformed or mixed-encoding text, with zero possibility of an unencodable character, at the modest cost of sometimes needing more tokens to represent multi-byte Unicode characters (such as CJK ideographs or emoji) that occupy several bytes each.
Why does the merge order matter for BPE encoding, and what happens if it's ignored?
BPE's merge table is an ordered list, and applying merges in a different order than the one learned during training can produce a different, invalid segmentation that the model was never trained on. Because merges progressively build larger symbols out of smaller ones, an out-of-order application might attempt a later merge before an earlier one has created the symbol it depends on, or might combine symbols in a way inconsistent with how the training corpus statistics were computed. Correct implementations always replay merges by rank — earliest-learned merge applied first among all currently applicable pairs — which is exactly what guarantees train/serve consistency.
Does vocabulary size (e.g., 32K vs 128K tokens) matter for model quality and cost?
Yes, substantially. A larger vocabulary means more distinct pieces are single tokens, shortening average sequence length for a given amount of text — which reduces the quadratic attention cost and can fit more content in a fixed context window. But a larger vocabulary also means a larger embedding table and output softmax layer, increasing parameter count and per-step compute for the input/output projections. Vocabulary size is therefore a real engineering tradeoff, tuned empirically per model family and typically scaled somewhat with overall model size and the breadth of languages/domains the model must serve well.
How does subword tokenization interact with multilingual and code models?
Any single tokenizer's vocabulary reflects the frequency statistics of its training corpus, so a vocabulary trained mostly on English text will tokenize other languages far less efficiently — often falling back to many small pieces or raw bytes/characters — inflating token counts and thus both cost and effective context usage for those languages. Multilingual models address this by training the tokenizer itself on a balanced, multilingual (or multilingual-plus-code) corpus so that common words and subwords across many languages and programming languages earn their own efficient token, which is precisely why SentencePiece's language-agnostic, whitespace-free design is favored for multilingual and code-focused LLMs.
How does tokenization connect to Scaling Laws and reported model sizes?
Scaling laws describe how model loss improves as a (roughly power-law) function of model parameters, dataset size, and compute — and dataset size in this literature is measured in tokens, not words or characters. Since different tokenizers compress the same raw text into different numbers of tokens, comparing "token counts" across models trained with different tokenizers is only meaningful if the tokenizer (and its typical bytes-per-token ratio) is held fixed or accounted for. This makes the tokenizer an often-overlooked hidden variable in reproducing or comparing published scaling-law results.