LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 158 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/8
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned Classifier
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/15
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning & Test-Time Compute
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
Back to Topics
LearnCore LLM FoundationsBPE, WordPiece, and SentencePiece
📝MediumNLP Fundamentals

BPE, WordPiece, and SentencePiece

Build a small subword tokenizer, compare BPE, WordPiece, and SentencePiece, then audit token cost and Unicode behavior.

20 min read
Learning path
Step 49 of 158 in the full curriculum
The Bitter Lesson & ComputeStatic to Contextual Embeddings

A developer writes const tokenCount = encodePrompt(prompt).length;. A model can't read that code or any other text directly. It receives integer IDs, and a tokenizer decides which pieces of the string get those IDs.

In the previous lesson, you saw why learning systems need representations that can improve with data and compute. Tokenization is the first such representation for text: a fixed contract that turns new wording, languages, and symbols into model input. You'll compare Byte Pair Encoding (BPE), WordPiece, and SentencePiece as you build that contract, break it, and learn what to measure before serving it.

Tokenization pipeline for prompt cache failed unexpectedly: common words remain whole, unexpectedly splits into un, expect, ed, and ly, seven pieces map to tokenizer-local integer IDs, and those IDs select seven rows from an embedding table. Tokenization pipeline for prompt cache failed unexpectedly: common words remain whole, unexpectedly splits into un, expect, ed, and ly, seven pieces map to tokenizer-local integer IDs, and those IDs select seven rows from an embedding table.
The tokenizer fixes both sequence length and lookup addresses. Here three common words stay whole, one rare word becomes four reusable pieces, and the resulting seven tokenizer-local IDs select seven embedding rows.
Diagram showing Raw prompt or code, Normalize and segment, Map pieces to IDs, and Look up embeddings. Diagram showing Raw prompt or code, Normalize and segment, Map pieces to IDs, and Look up embeddings.
Raw prompt or code, Normalize and segment, Map pieces to IDs, and Look up embeddings.

An embedding is a vector associated with a token ID. The next lesson teaches those vectors. For now, keep one rule in mind: ID 421 has no universal meaning. It only means something when paired with the exact tokenizer artifact that produced it.

Choose pieces between characters and words

A word-level tokenizer could keep tokenizer as one item, but it needs a policy for every rare identifier, version string, and prompt marker. A character-level tokenizer never runs out of pieces, but it turns short strings into long sequences. Subword tokenization keeps frequent fragments whole while retaining small fallback pieces for rare text.

Consider this prompt fixture:

prompt.txt
1tokenizer failed for prompt128k
Token unitExample piecesWhat it buys youWhat it costs
Charactert o k e n i z e r <space> ...Every character, including whitespace, is representableLong input sequence
Wordtokenizer, failed, for, prompt128kShort common stringsRare words and IDs need fallbacks
Subwordtoken, izer, <space>fail, ed, <space>for, <space>prompt, 128, kCompact common patterns with fallback partsRequires learned vocabulary
Token granularity frontier for tokenizer failed for prompt128k: character tokenization uses 28 positions and a tiny vocabulary, subwords use eight positions with medium vocabulary and open coverage, and whole words use four positions but require many rows and carry higher unknown-token risk. Token granularity frontier for tokenizer failed for prompt128k: character tokenization uses 28 positions and a tiny vocabulary, subwords use eight positions with medium vocabulary and open coverage, and whole words use four positions but require many rows and carry higher unknown-token risk.
For this fixture, larger pieces reduce sequence length from 31 characters, including whitespace, to eight whitespace-preserving subwords to four words. Vocabulary cost moves in the opposite direction, so subwords often occupy the practical region with short sequences and reusable fallback pieces.

The first lab makes that tradeoff concrete. The subword split is written by hand because you haven't trained a tokenizer yet. Use its token count as a budget to compare against character and whitespace-word baselines.

01-count-token-units.py
1message = "tokenizer failed for prompt128k" 2 3characters = list(message) 4words = message.split() 5subwords = ["token", "izer", "<space>fail", "ed", "<space>for", "<space>prompt", "128", "k"] 6 7print("characters:", len(characters)) 8print("words:", len(words)) 9print("candidate subwords:", len(subwords), subwords) 10assert "".join(subwords).replace("<space>", " ") == message
Output
1characters: 31 2words: 4 3candidate subwords: 8 ['token', 'izer', '<space>fail', 'ed', '<space>for', '<space>prompt', '128', 'k']

The word count looks smallest, but it hides the hard question: what happens when prompt128k was never in the vocabulary? Subwords answer that question by learning common fragments and retaining smaller pieces for the rest.

Train byte pair encoding from counts

Byte Pair Encoding (BPE) builds a vocabulary from repeated adjacent pieces. Sennrich, Haddow, and Birch applied BPE subword units to open-vocabulary neural machine translation: start from small symbols, merge frequent adjacent pairs, and reuse the resulting pieces for rare words.[1]Reference 1Neural Machine Translation of Rare Words with Subword Units.https://arxiv.org/abs/1508.07909

For teaching, start with characters inside pre-separated code and text terms. A production tokenizer has extra decisions about whitespace, bytes, and normalization, but the merge loop is the important first mechanism.

Three-round BPE recount on weighted terms code, coder, and cope: c plus o wins with count seven, co plus d ties d plus e at five and wins by deterministic first-seen order, then cod plus e wins with count five to form code. Three-round BPE recount on weighted terms code, coder, and cope: c plus o wins with count seven, co plus d ties d plus e at five and wins by deterministic first-seen order, then cod plus e wins with count five to form code.
Each column recounts adjacent pairs after the previous merge. Round two contains a 5-5 tie: this classroom implementation picks the pair encountered first, while a production trainer must define a stable tie-break so the learned vocabulary is reproducible.

Suppose a code-text corpus contains these term counts:

TermCount
code3
coder2
cope2
token2
prompt1

At the start, c + o appears seven times: three in code, two in coder, and two in cope. Merge it into co. On the updated corpus, co + d and d + e are tied at five. The toy trainer encounters co + d first, so it becomes cod; a production trainer needs an explicit deterministic tie-break. Recounting again makes cod + e the next winner at five, producing code.

This miniature trainer stores words as tuples of current pieces, counts adjacent pairs, merges the winner everywhere, and prints the first three learned rules.

02-train-mini-bpe.py
1from collections import Counter 2 3frequencies = { 4 "code": 3, 5 "coder": 2, 6 "cope": 2, 7 "token": 2, 8 "prompt": 1, 9} 10state = {tuple(word): count for word, count in frequencies.items()} 11 12def count_pairs(words: dict[tuple[str, ...], int]) -> Counter[tuple[str, str]]: 13 pairs: Counter[tuple[str, str]] = Counter() 14 for pieces, count in words.items(): 15 for pair in zip(pieces, pieces[1:]): 16 pairs[pair] += count 17 return pairs 18 19def merge_pair( 20 pieces: tuple[str, ...], pair: tuple[str, str] 21) -> tuple[str, ...]: 22 merged: list[str] = [] 23 i = 0 24 while i < len(pieces): 25 if i + 1 < len(pieces) and pieces[i : i + 2] == pair: 26 merged.append("".join(pair)) 27 i += 2 28 else: 29 merged.append(pieces[i]) 30 i += 1 31 return tuple(merged) 32 33merges: list[tuple[str, str]] = [] 34for step in range(3): 35 pair, count = count_pairs(state).most_common(1)[0] 36 state = {merge_pair(pieces, pair): freq for pieces, freq in state.items()} 37 merges.append(pair) 38 print(step + 1, pair, "->", "".join(pair), "count", count) 39 40print("learned merges:", merges)
Output
11 ('c', 'o') -> co count 7 22 ('co', 'd') -> cod count 5 33 ('cod', 'e') -> code count 5 4learned merges: [('c', 'o'), ('co', 'd'), ('cod', 'e')]

The result isn't a linguistic analysis. BPE doesn't know that code relates to programming. It knows only that a boundary occurs often enough to compress.

Replay merges on a new term

Training chooses an ordered merge list once. Encoding a new input doesn't recount a new corpus; it replays those learned rules. That distinction matters in production because the tokenizer must stay fixed with the model.

The next lab takes the three rules learned above and applies them to new terms. coder benefits from the common stem, while coper gets only the co merge because the corpus never earned cope as one piece in the first three steps.

03-replay-bpe-merges.py
1def apply_rule(pieces: list[str], pair: tuple[str, str]) -> list[str]: 2 result: list[str] = [] 3 i = 0 4 while i < len(pieces): 5 if i + 1 < len(pieces) and tuple(pieces[i : i + 2]) == pair: 6 result.append("".join(pair)) 7 i += 2 8 else: 9 result.append(pieces[i]) 10 i += 1 11 return result 12 13rules = [("c", "o"), ("co", "d"), ("cod", "e")] 14 15for term in ["coder", "codec", "coper"]: 16 pieces = list(term) 17 for rule in rules: 18 pieces = apply_rule(pieces, rule) 19 print(term, "->", pieces)
Output
1coder -> ['code', 'r'] 2codec -> ['code', 'c'] 3coper -> ['co', 'p', 'e', 'r']

Use bytes as a complete base alphabet

Character-starting BPE still needs an unknown-token policy for characters absent from its base vocabulary. GPT-2 used a byte-level BPE variant: its base alphabet represents bytes, then learned merges build larger pieces over that base. Because any Unicode string has a UTF-8 byte representation, every input remains representable without an unknown character token.[2]Reference 2Language Models are Unsupervised Multitask Learners.https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf

Some tokenizers use an explicit byte fallback only when ordinary pieces can't encode an input. That has the same coverage goal but isn't the same mechanism as GPT-2's byte-level starting alphabet. This lab doesn't train merges. It isolates the common foundation: a code comment containing Japanese characters and an emoji is reversible through raw UTF-8 byte values.

04-utf8-byte-round-trip.py
1message = "関数✨" 2byte_ids = list(message.encode("utf-8")) 3reconstructed = bytes(byte_ids).decode("utf-8") 4 5print("byte count:", len(byte_ids)) 6print("first byte ids:", byte_ids[:8]) 7print("round trip:", reconstructed) 8assert reconstructed == message 9assert all(0 <= value <= 255 for value in byte_ids)
Output
1byte count: 9 2first byte ids: [233, 150, 162, 230, 149, 176, 226, 156] 3round trip: 関数✨

Byte coverage prevents an unrepresentable character. It doesn't promise compact tokenization: if the training data rarely covers a script or emoji sequence, several bytes may remain separate pieces.

WordPiece chooses vocabulary differently

WordPiece appeared in Google's Japanese and Korean voice-search work and later became familiar through BERT's tokenizer.[3]Reference 3Japanese and Korean Voice Search.https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37842.pdf[4]Reference 4BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding.https://arxiv.org/abs/1810.04805 Like BPE, it creates reusable pieces. Unlike simple frequency-based BPE, the original WordPiece description selects vocabulary additions to improve a language-model likelihood objective.

Exact training recipes aren't fully specified by the short original paper, and library trainers can differ. A useful classroom proxy is an association score:

score⁡(a,b)=count⁡(ab)count⁡(a)count⁡(b)\operatorname{score}(a,b) = \frac{\operatorname{count}(ab)} {\operatorname{count}(a)\operatorname{count}(b)}score(a,b)=count(a)count(b)count(ab)​

Here, count(ab) is how often two neighboring pieces occur together; the denominator penalizes pieces that occur frequently in many other contexts. This formula teaches why a less frequent but tightly associated pair could be attractive. Treat it as intuition for WordPiece's likelihood motivation, not as the original implementation specification.

Candidate pairPair countIndividual countsProxy scoreLesson
code + base4250 and 440.0191Often occurs together
the + model90900 and 3000.0003Frequent pieces aren't necessarily exclusive

At encoding time, BERT-style WordPiece uses continuation pieces such as ##ing and a greedy longest-match lookup. A piece beginning with ## continues the current word rather than beginning a new word.

The next lab implements that lookup for tokenizer vocabulary. It always tries the longest valid piece from the current cursor position.

05-wordpiece-longest-match.py
1def wordpiece_tokenize(word: str, vocabulary: set[str]) -> list[str]: 2 result: list[str] = [] 3 start = 0 4 while start < len(word): 5 chosen = None 6 for end in range(len(word), start, -1): 7 candidate = word[start:end] 8 if start > 0: 9 candidate = "##" + candidate 10 if candidate in vocabulary: 11 chosen = candidate 12 start = end 13 break 14 if chosen is None: 15 return ["[UNK]"] 16 result.append(chosen) 17 return result 18 19vocabulary = {"code", "##base", "token", "##ized"} 20for term in ["code", "codebase", "tokenized"]: 21 print(term, "->", wordpiece_tokenize(term, vocabulary))
Output
1code -> ['code'] 2codebase -> ['code', '##base'] 3tokenized -> ['token', '##ized']

Expose the unknown-token failure

Standard WordPiece can't necessarily spell a word from arbitrary bytes. If no valid segmentation reaches the end of a word, BERT-style tokenization emits [UNK] for that word. That loses distinctions between two different unseen strings.

This failure test uses the same algorithm with a missing ##bot continuation. The fix isn't to silently map new strings to a known ID. You must use the model's tokenizer contract, or explicitly change the vocabulary and corresponding model parameters as a training decision.

06-wordpiece-unknown-token.py
1def encode_word(word: str, vocabulary: set[str]) -> list[str]: 2 pieces: list[str] = [] 3 cursor = 0 4 while cursor < len(word): 5 match = None 6 for end in range(len(word), cursor, -1): 7 candidate = word[cursor:end] 8 if cursor: 9 candidate = "##" + candidate 10 if candidate in vocabulary: 11 match = candidate 12 cursor = end 13 break 14 if match is None: 15 return ["[UNK]"] 16 pieces.append(match) 17 return pieces 18 19vocabulary = {"code", "##base", "token"} 20known = encode_word("codebase", vocabulary) 21missing = encode_word("codebot", vocabulary) 22 23print("known term:", known) 24print("missing continuation:", missing) 25assert known == ["code", "##base"] 26assert missing == ["[UNK]"]
Output
1known term: ['code', '##base'] 2missing continuation: ['[UNK]']

SentencePiece treats boundaries as part of the artifact

Many subword pipelines first split a sentence into word-like units, then segment inside each unit. That assumption is awkward for text where spaces don't mark each word. SentencePiece is a tokenizer and detokenizer framework that trains directly from raw sentences instead of requiring pre-tokenized word sequences.[5]Reference 5SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing.https://arxiv.org/abs/1808.06226

SentencePiece commonly makes spaces visible as ▁, so Token cache warm becomes a stream like ▁Token▁cache▁warm before final pieces are chosen. Decoding targets the normalized input string, not necessarily the original raw byte sequence, because normalization can fold equivalent or compatibility forms.

SentencePiece artifact flow for token file delayed: normalization folds the fi ligature, visible boundary markers enter the exact Unigram piece sequence from the lab, decoding restores spaces, and lower diagrams contrast BPE adding merges with Unigram pruning candidate paths. SentencePiece artifact flow for token file delayed: normalization folds the fi ligature, visible boundary markers enter the exact Unigram piece sequence from the lab, decoding restores spaces, and lower diagrams contrast BPE adding merges with Unigram pruning candidate paths.
The upper path uses the lab's exact normalization and piece sequence: the artifact folds fi, makes boundaries visible, segments, and decodes. The lower diagrams separate its two model choices: BPE adds frequent merges, while Unigram prunes a candidate pool and can sample legal paths during training.

SentencePiece supports BPE, and it also supports the Unigram language model algorithm proposed with subword regularization. Unigram starts with many candidate pieces, assigns probabilities, removes pieces that contribute least to corpus likelihood, and can sample multiple valid segmentations during model training.[6]Reference 6Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates.https://arxiv.org/abs/1804.10959

That sampling option matters because a word such as tokenized can have several legal piece boundaries. Training on more than one segmentation can make the downstream model less dependent on a single boundary choice. For deterministic serving, the tokenizer can still select its highest-probability segmentation.

The official library makes the artifact visible. This lab trains a small Unigram SentencePiece model on raw tokenizer-related text, encodes an unseen request, and proves that its decoded form matches the tokenizer's normalized text.

07-train-sentencepiece-unigram.py
1from pathlib import Path 2from tempfile import TemporaryDirectory 3 4import sentencepiece as spm 5 6spm.set_min_log_level(2) 7 8with TemporaryDirectory() as tmp: 9 corpus = Path(tmp) / "text.txt" 10 corpus.write_text( 11 "token budget pending\n" 12 "token budget approved\n" 13 "prompt cache created\n" 14 "context window delayed\n" 15 "unicode file requested\n", 16 encoding="utf-8", 17 ) 18 prefix = str(Path(tmp) / "text_tokenizer") 19 spm.SentencePieceTrainer.train( 20 input=str(corpus), 21 model_prefix=prefix, 22 model_type="unigram", 23 vocab_size=40, 24 character_coverage=1.0, 25 hard_vocab_limit=False, 26 bos_id=-1, 27 eos_id=-1, 28 pad_id=-1, 29 ) 30 tokenizer = spm.SentencePieceProcessor(model_file=prefix + ".model") 31 raw_message = "token file delayed" 32 normalized_with_marker = tokenizer.normalize(raw_message) 33 pieces = tokenizer.encode(raw_message, out_type=str) 34 decoded = tokenizer.decode(pieces) 35 36print("raw:", raw_message) 37print("normalized with marker:", normalized_with_marker) 38print("pieces:", pieces) 39print("decoded:", decoded) 40assert raw_message != decoded 41assert normalized_with_marker == "▁token▁file▁delayed" 42assert decoded == "token file delayed" 43assert any(piece.startswith("▁") for piece in pieces)
Output
1raw: token file delayed 2normalized with marker: ▁token▁file▁delayed 3pieces: ['▁token', '▁', 'f', 'i', 'l', 'e', '▁', 'de', 'l', 'a', 'y', 'ed'] 4decoded: token file delayed

The default SentencePiece normalization folds the compatibility ligature fi into fi. The visible ▁ marker participates in segmentation, then decoding restores spaces. That's why the decoded output matches normalized text rather than the original raw code-point sequence.

Vocabulary size spends parameters to save positions

Every added vocabulary entry can compress a recurring string into fewer tokens. It also adds an embedding row. If the output projection isn't tied to the input embedding matrix, it adds another row there too.

For a vocabulary of size VVV and hidden dimension ddd, an input embedding matrix contains VdVdVd parameters. With float16 weights, each parameter takes two bytes. A second untied output matrix doubles that vocabulary-dependent memory.

Exact vocabulary memory budget for a 4096-wide float16 model: 8,000 entries use 62.5 MiB for input embeddings or 125 MiB with an untied output matrix, 32,000 use 250 or 500 MiB, and 128,000 use 1,000 or 2,000 MiB; compression and quality remain measurements across traffic, locales, and code. Exact vocabulary memory budget for a 4096-wide float16 model: 8,000 entries use 62.5 MiB for input embeddings or 125 MiB with an untied output matrix, 32,000 use 250 or 500 MiB, and 128,000 use 1,000 or 2,000 MiB; compression and quality remain measurements across traffic, locales, and code.
Memory grows exactly with V × d × bytes; the bars use the lab's calculated outputs. Compression has no universal curve, so extra rows are justified only by measured token savings and downstream quality across the workloads you serve.

Use numbers before making a design argument. This lab compares hypothetical tokenizer vocabularies for a model with hidden dimension 4096; it calculates embedding memory rather than assuming a larger vocabulary is free.

08-vocabulary-memory-budget.py
1def embedding_memory_mib( 2 vocabulary_size: int, hidden_size: int, bytes_per_weight: int = 2 3) -> float: 4 return vocabulary_size * hidden_size * bytes_per_weight / (1024**2) 5 6hidden_size = 4096 7for vocabulary_size in [8_000, 32_000, 128_000]: 8 input_mib = embedding_memory_mib(vocabulary_size, hidden_size) 9 untied_mib = 2 * input_mib 10 print( 11 f"{vocabulary_size:>6,} tokens:", 12 f"input={input_mib:>7.1f} MiB", 13 f"input+untied-output={untied_mib:>7.1f} MiB", 14 )
Output
18,000 tokens: input= 62.5 MiB input+untied-output= 125.0 MiB 232,000 tokens: input= 250.0 MiB input+untied-output= 500.0 MiB 3128,000 tokens: input= 1000.0 MiB input+untied-output= 2000.0 MiB

Sequence compression must be measured too. A vocabulary can shorten common English tokenizer prompts and still fragment another script or your TypeScript repository badly. Tokenizer design is an evaluation problem, not a race to the largest V.

Audit language and code token budgets

Fertility is a token-length measure. At word level, fertility is the average number of tokenizer pieces needed per word. For parallel-message audits, you can also compare total token count or a locale-to-baseline ratio for equivalent text. For a product with international users, compare parallel messages across target languages instead of using English traffic alone. Petrov et al. measured translated text and found tokenizer-length disparities as large as 15 times for some language and tokenizer combinations, with implications for cost, latency, and available context.[7]Reference 7Language Model Tokenizers Introduce Unfairness Between Languages.https://arxiv.org/abs/2305.15425

Exact cl100k_base fixture audit comparing English, Portuguese, and Japanese token counts, followed by a safer locale-audit path. Exact cl100k_base fixture audit comparing English, Portuguese, and Japanese token counts, followed by a safer locale-audit path.
The bars reproduce the lab's exact three-message output, not a language ranking. A production audit pins the tokenizer version, expands to reviewed parallel messages, summarizes distributions rather than one count, and checks downstream quality.

Code belongs in the same audit. Identifiers, indentation, and operators are all model input. A tokenizer trained with limited code coverage may split familiar repository patterns into many pieces, leaving less room for files, tests, and error logs. Human-readable splits are useful debugging clues, not proof of model quality. Measure token totals and validate downstream code tasks.

Exact cl100k_base tokenization of const tokenCount equals encodePrompt prompt length: 10 tokenizer pieces and IDs expose identifier fragmentation, leading-space pieces, and syntax-sensitive spans, followed by round-trip and downstream code-quality checks. Exact cl100k_base tokenization of const tokenCount equals encodePrompt prompt length: 10 tokenizer pieces and IDs expose identifier fragmentation, leading-space pieces, and syntax-sensitive spans, followed by round-trip and downstream code-quality checks.
The strip uses the lab's exact 10 cl100k_base pieces and IDs, with · marking a leading space. It exposes identifier and syntax fragmentation without treating readable pieces or token count as proof of code quality.

The next lab uses one named tokenizer artifact to measure several fixtures. The sample isn't a language-quality study; its job is to give you a repeatable audit shape. Replace the fixture messages with reviewed parallel translations and repository files for a real decision.

09-audit-token-lengths.py
1import tiktoken 2 3encoding = tiktoken.get_encoding("cl100k_base") 4fixtures = { 5 "english": "How do I tokenize this prompt?", 6 "portuguese": "Como tokenizo este prompt?", 7 "japanese": "このプロンプトをトークン化するには?", 8 "typescript": "const tokenCount = encodePrompt(prompt).length;", 9} 10 11english_tokens = len(encoding.encode(fixtures["english"])) 12for name, text in fixtures.items(): 13 ids = encoding.encode(text) 14 print(f"{name:>10}: {len(ids):>2} tokens {len(ids) / english_tokens:>4.1f}x english") 15 assert encoding.decode(ids) == text
Output
1english: 7 tokens 1.0x english 2portuguese: 6 tokens 0.9x english 3 japanese: 16 tokens 2.3x english 4typescript: 10 tokens 1.4x english

Don't read a single sample as a ranking of languages. Build a locale-aware test set, record tokenizer version, compare distribution summaries, and then check downstream task quality.

Make Unicode policy explicit

Two strings can look identical while holding different Unicode code points. For example, café may contain one composed é or the sequence e plus a combining accent. Without a stable policy, cache keys, token counts, and filter behavior can disagree across clients.

Normalization Form C (NFC) composes canonically equivalent forms without folding broad compatibility distinctions. Normalization Form Compatibility Composition (NFKC) also folds compatibility characters, such as the fi ligature into fi. Python exposes both through unicodedata.normalize; choosing between them is a product policy decision, not an automatic cleanup rule.

Unicode normalization comparison from the lab: composed café uses U+00E9 while decomposed cafe plus U+0301 starts unequal, NFC maps both to the same code-point sequence, NFC preserves the fi ligature in file, and NFKC folds it to separate f and i characters. Unicode normalization comparison from the lab: composed café uses U+00E9 while decomposed cafe plus U+0301 starts unequal, NFC maps both to the same code-point sequence, NFC preserves the fi ligature in file, and NFKC folds it to separate f and i characters.
The left lane reproduces the lab's False → True canonical-equivalence result under NFC. The right lane makes the policy boundary explicit: NFC preserves file, while NFKC changes it to file. Version that choice with cache keys and tokenizer IDs.

This final lab proves canonical equivalence and makes the compatibility choice visible. For user-visible prompts, you might choose NFC first and add separate security checks for invisible or confusable characters. Another product may deliberately choose NFKC after deciding the information loss is acceptable.

10-normalize-before-tokenizing.py
1import unicodedata 2 3composed = "café" 4decomposed = "cafe\u0301" 5ligature = "file" 6 7print("raw cafe equal:", composed == decomposed) 8print("NFC cafe equal:", unicodedata.normalize("NFC", composed) == unicodedata.normalize("NFC", decomposed)) 9print("NFC ligature:", unicodedata.normalize("NFC", ligature)) 10print("NFKC ligature:", unicodedata.normalize("NFKC", ligature)) 11 12assert composed != decomposed 13assert unicodedata.normalize("NFC", composed) == unicodedata.normalize("NFC", decomposed) 14assert unicodedata.normalize("NFC", ligature) != "file" 15assert unicodedata.normalize("NFKC", ligature) == "file"
Output
1raw cafe equal: False 2NFC cafe equal: True 3NFC ligature: file 4NFKC ligature: file

Tokenizer behavior must be versioned with this policy. If one service normalizes with NFC and another silently folds with NFKC, they can send different IDs to the same model or generate different cache keys for text that looks unchanged.

Compare algorithms without mixing contracts

The common tokenizer families are design choices, not names to memorize.

Three-lane tokenizer comparison for unbelievably: BPE merges frequent pairs, WordPiece takes longest valid pieces, and Unigram scores full paths. Three-lane tokenizer comparison for unbelievably: BPE merges frequent pairs, WordPiece takes longest valid pieces, and Unigram scores full paths.
One word exposes three different mechanics. BPE replays learned merge ranks, WordPiece scans for the longest valid piece, and Unigram selects among scored segmentation paths. Each model still depends on its exact vocabulary, normalizer, and fallback policy.
MethodTraining viewServing viewBoundary/fallback detail
BPEAdd frequent adjacent mergesReplay ordered mergesByte-level variants retain UTF-8 coverage
WordPieceGrow vocabulary for likelihood objectiveGreedy longest valid pieceBERT-style continuation uses ##; missing segmentation can yield [UNK]
SentencePiece BPEBPE trained directly on raw normalized textReplay packaged modelVisible whitespace marker can be part of pieces
SentencePiece UnigramEstimate and prune candidate-piece probabilitiesBest path or sampled alternatives when requestedSegmentation sampling supports regularized training

Production review checklist

SymptomLikely causeCheck or fix
Model output collapses after swapping tokenizer fileIDs no longer match trained embeddingsPin tokenizer artifact and model checkpoint together
Locale hits token limit sooner than EnglishUnequal fertility on translated requestsMeasure parallel message sets by locale and task
A WordPiece model emits [UNK] for new identifiersNo valid vocabulary segmentationEvaluate vocabulary/model update rather than masking the failure
Cache misses differ across clients for same visible messageUnicode preprocessing isn't consistentVersion and test normalization plus tokenizer pipeline
Repository prompt holds less code than expectedCode fixtures fragment into many tokensMeasure actual files with intended deployed tokenizer

What this lets you build

You started with a prompt string and finished with a tokenizer review harness. From here, you can:

  1. Explain why subwords balance sequence length against vocabulary coverage.
  2. Train and replay a minimal BPE merge table.
  3. Explain why byte fallback preserves representability but doesn't guarantee compactness.
  4. Implement WordPiece longest-match encoding and reproduce its unknown-token failure.
  5. Train a raw-text SentencePiece Unigram tokenizer and distinguish the framework from its segmentation algorithms.
  6. Calculate vocabulary-dependent embedding memory.
  7. Audit token length across locales and code fixtures without overstating one sample.
  8. Choose and test a Unicode normalization policy.

Mastery check

Evaluation rubric

  • Foundational: Given a five-word corpus, you can calculate one BPE winner by hand and apply it to updated pieces.
  • Intermediate: You can implement BPE replay and WordPiece longest-match segmentation, then explain why their failure behavior differs.
  • Intermediate: You can show the difference between SentencePiece BPE and SentencePiece Unigram without calling SentencePiece a merge algorithm.
  • Advanced: You can present a tokenizer audit with locale fixtures, code fixtures, normalization policy, vocabulary memory cost, and artifact versioning.

Follow-up questions

Common pitfalls

  • Retraining during inference: Pair frequencies are counted while building BPE rules, not while serving each prompt. Serving must replay fixed rules.
  • Conflating byte-level BPE with byte fallback: GPT-2 starts from byte representations. Other tokenizers may invoke explicit byte fallback only when ordinary pieces can't encode an input. Both preserve coverage, but they aren't the same mechanism.
  • Describing the WordPiece proxy as its spec: The association score clarifies the intuition. The original method is likelihood-driven, and implementations can vary.
  • Calling SentencePiece a fourth algorithm: SentencePiece packages raw-text handling and can host BPE or Unigram models.
  • Assuming byte fallback means fair multilingual cost: Coverage avoids unknown characters; it doesn't make segment lengths equal.
  • Normalizing without a policy: NFC and NFKC solve different problems. Compatibility folding can discard distinctions your product intended to preserve.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score above 75% to mark this lesson complete.

1.A character-starting BPE trainer uses the corpus counts code: 3, coder: 2, and cope: 2. Counts are weighted by term frequency. Which first merge is selected?
2.The mini BPE trainer learned the ordered rules ('c','o'), ('co','d'), ('cod','e'). When encoding coper, why should the encoder not recount pairs in that word and add a new cope merge?
3.A byte-fallback tokenizer can encode the Japanese-and-emoji string 関数✨ by emitting UTF-8 byte values when needed and can decode those bytes back to the same string. What conclusion is valid?
4.A WordPiece teaching proxy scores a pair as count(ab) / (count(a) * count(b)). Pair code + base has counts 42, 50, and 44, while the + model has counts 90, 900, and 300. Which conclusion follows from the proxy?
5.Using a BERT-style WordPiece vocabulary {'code', '##base', 'token'}, what does greedy longest-match encoding return for codebot?
6.A tokenizer must train from raw sentences, make spaces visible, use a Unigram model, sample segmentations during model training, and serve deterministically. Which implementation is consistent with those requirements?
7.A product must treat composed and decomposed forms of café as equivalent while preserving the compatibility ligature in file as distinct from the letters in file. The policy is limited to NFC or NFKC. Which policy satisfies both requirements?
8.A tokenizer proposal raises the vocabulary from 32,000 to 128,000 for a model with hidden size 4096, float16 weights, and an untied output matrix. It saves 3 tokens on one English prompt. Which conclusion follows from the stated evidence?
9.A team compares one English prompt with one Japanese prompt, observes a token-count ratio, and declares the tokenizer unsuitable for Japanese. Which audit would support a deployment decision?
10.A language model was trained with tokenizer artifact A. A team wants to swap in artifact B because it has the same vocabulary size and decodes many prompts to the same visible text. Why can this still break the model?

10 questions remaining.

Next Step
Continue to Static to Contextual Embeddings

You can now turn text into stable token IDs and measure the cost of that choice. Next you'll turn those IDs into <span data-glossary="vector">vectors</span> whose geometry lets a model learn similarity and context.

PreviousThe Bitter Lesson & Compute
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Neural Machine Translation of Rare Words with Subword Units.

Sennrich, R., Haddow, B., & Birch, A. · 2016 · ACL 2016

Language Models are Unsupervised Multitask Learners.

Radford, A., et al. · 2019

Japanese and Korean Voice Search.

Schuster, M. & Nakajima, K. · 2012

BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding.

Devlin, J., et al. · 2019 · NAACL 2019

SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing.

Kudo, T. & Richardson, J. · 2018 · EMNLP 2018

Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates.

Kudo, T. · 2018 · ACL 2018

Language Model Tokenizers Introduce Unfairness Between Languages.

Petrov, A., La Malfa, E., Torr, P. H. S., & Bibi, A. · 2023 · NeurIPS 2023

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.