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 177 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/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠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/16
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued 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
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
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.

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

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

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.

A seven-piece tokenization trace for prompt cache failed unexpectedly: three words stay whole, unexpectedly becomes un, expect, ed, ly, seven local IDs are assigned, and each ID selects one colored row in 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.
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 comparison for tokenizer failed for prompt128k: character pieces represent the fixture in 31 positions, eight subwords preserve spaces while staying open-vocabulary, and four whole words minimize positions but carry 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 BPE recount panels use weighted code, coder, and cope terms. Round one selects c plus o at count 7, round two selects co plus d at count 5 over a tied d plus e pair, and round three selects cod plus e at count 5.
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']

During BPE encoding, why don't you count pairs in the user's new message?

Answer

Pair counting belongs to tokenizer training. At inference time the model needs the same stable input contract used during its training, so the encoder replays the fixed ordered merge list.

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 trace for token file delayed: normalization changes fi to fi, spaces become ▁ markers, the exact Unigram pieces are emitted, and neighboring panels contrast additive BPE merges with Unigram path pruning.
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.

Is SentencePiece a fourth merge algorithm alongside BPE and WordPiece?

Answer

No. SentencePiece is a raw-text tokenizer and detokenizer framework. It can train a BPE model or a Unigram model, while also packaging its normalization and boundary-handling policy.

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. Input embeddings use 62.5, 250, and 1000 MiB at 8K, 32K, and 128K entries; an untied output matrix doubles each total to 125, 500, and 2000 MiB.
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: English uses 7 tokens, Portuguese 6, and Japanese 16. A compact follow-up flow shows the production checks of parallel messages, token distributions, system budget, and downstream task quality.
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 trace for const tokenCount = encodePrompt(prompt).length; showing ten code pieces, their tokenizer-local IDs, two identifier pieces, leading-space markers, and a round-trip check.
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: composed café and decomposed cafe plus U+0301 begin unequal but NFC makes them equal, while NFC preserves the fi ligature in file and NFKC folds it to file before token IDs are assigned.
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 compact lanes tokenize unbelievably with different inference rules: BPE replays ranked merges, WordPiece scans longest valid spans with ## continuation markers, and Unigram compares complete segmentation paths by score.
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

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

Why must a deployed model keep the same tokenizer artifact used during training?

Answer

The model learned embeddings and output probabilities for specific token IDs. A different vocabulary, merge list, normalization policy, or ID mapping changes the integers representing the text, so the learned parameters no longer receive the intended inputs.

What does byte fallback solve, and what does it leave unsolved?

Answer

Byte fallback makes every UTF-8 input representable without an unknown character token. It doesn't guarantee good compression or equal token cost across languages; those properties require corpus coverage and measurement.

Why isn't a low token count on one English prompt enough to choose a tokenizer?

Answer

A production workload includes many message forms, locales, and possibly code or documents. Measure token distributions and downstream quality on representative fixtures, then account for vocabulary memory and normalization behavior.

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 75% or higher 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?

Correct answer: Merge c + o at weighted count 7.

The pair c + o occurs once in every instance of all three terms. Its weighted count is therefore 3 + 2 + 2 = 7, which exceeds the competing counts of 5.

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?

Correct answer: Encoding must replay the fixed ordered merge list learned during tokenizer training; otherwise the model would see token IDs from a different input contract.

Pair counts select merge rules during tokenizer training. Serving applies those rules in their learned order, so coper receives only the available co merge rather than a newly invented cope token.

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?

Correct answer: The message is representable without an unknown character token, but it may still use many tokens if those bytes or script were rare in training.

UTF-8 bytes provide complete fallback coverage, allowing the string to be represented and reconstructed. Coverage is not compression: uncommon scripts or emoji may remain split across several byte-level pieces.

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?

Correct answer: The proxy favors code + base because its components are more tightly associated despite the lower pair count.

The scores are about 0.0191 for code + base and 0.0003 for the + model. The denominator penalizes pieces that occur widely in other contexts. This illustrates WordPiece's likelihood intuition, but it isn't a complete specification of every WordPiece trainer.

5.Using a BERT-style WordPiece vocabulary {'code', '##base', 'token'}, what does greedy longest-match encoding return for codebot?

Correct answer: [[UNK]]

The encoder can match code initially, but no valid continuation such as ##bot reaches the end of the word. The demonstrated BERT-style algorithm therefore returns [UNK] for the whole word rather than preserving the matched prefix.

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?

Correct answer: Use SentencePiece for normalization and boundaries, prune Unigram candidates, sample during training, and select the highest-probability serving path.

SentencePiece packages raw-text normalization and boundary handling rather than defining one merge algorithm. Its Unigram mode starts with candidate pieces and prunes them by likelihood contribution. It can sample valid segmentations during model training while using the highest-probability segmentation for deterministic serving.

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?

Correct answer: Use NFC, which composes canonical equivalents without folding the compatibility ligature.

NFC makes canonically equivalent composed and decomposed accents consistent while retaining compatibility distinctions such as fi versus fi. NFKC would also fold the ligature, violating the stated preservation requirement.

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?

Correct answer: Vocabulary memory rises from about 500 MiB to 2000 MiB, and one shorter prompt is insufficient evidence for approval.

Each matrix uses V * 4096 * 2 bytes, and the untied output matrix doubles the total. The 32,000-entry vocabulary therefore costs about 500 MiB across both matrices, while 128,000 entries cost about 2000 MiB. One English example doesn't establish representative compression or quality gains.

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?

Correct answer: Use reviewed parallel messages across target locales, summarize token distributions, record the tokenizer version, test real code fixtures, and validate downstream quality.

One nonparallel sample can't establish a language ranking. A defensible audit uses equivalent reviewed messages, distribution summaries, a pinned artifact, representative repository files, and downstream task checks. Token counts reveal budget disparities, but they don't by themselves prove model quality.

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?

Correct answer: The model learned embeddings and output probabilities for specific token IDs. A different vocabulary, merge list, normalization policy, or ID mapping changes the integers representing the text, so the learned parameters no longer receive the intended inputs.

Token IDs are addresses into learned embedding and output rows, not universal meanings. Equal vocabulary size or similar decoded text doesn't preserve compatibility when segmentation, normalization, or ID assignments differ.

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

https://arxiv.org/abs/1508.07909

Language Models are Unsupervised Multitask Learners.

Radford, A., et al. · 2019

https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf

Japanese and Korean Voice Search.

Schuster, M. & Nakajima, K. · 2012

https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37842.pdf

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

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

https://arxiv.org/abs/1810.04805

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

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

https://arxiv.org/abs/1808.06226

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

Kudo, T. · 2018 · ACL 2018

https://arxiv.org/abs/1804.10959

Language Model Tokenizers Introduce Unfairness Between Languages.

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

https://arxiv.org/abs/2305.15425

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.