02 Foundations 11 min read 2,321 words

Tokenization Deep Dive

Tokenization is the process of converting text into discrete units (tokens) that models can process. It directly impacts model capabilities, costs, and performance.

fundamentalstokenizationcostcore
01matters

Why Tokenization Matters

1.1

For System Design

  1. Cost: LLM APIs charge per token. Tokenization efficiency directly affects costs.
  2. Context limits: Token count, not word count, determines what fits in context.
  3. Capability: Some tasks (character counting, anagrams) are hard because of tokenization.
  4. Consistency: Same text tokenizes differently across models.
1.2

For Understanding LLM Behavior

Classic interview question: Why does GPT struggle to count letters in "strawberry"?

Because "strawberry" is tokenized as multiple subwords. The model never sees individual characters; it sees subword units. Counting letters requires reasoning about internal structure of tokens.

02algorithms

Tokenization Algorithms

2.1

Byte Pair Encoding (BPE)

The most common algorithm. Used by GPT-series, Llama, Claude.

Training algorithm:

  1. Start with vocabulary of individual bytes (256 tokens)
  2. Count all adjacent token pairs in training corpus
  3. Merge most frequent pair into a new token
  4. Repeat until vocabulary size reached

Example:

Texttext · 13 lines
12345678910111213
Corpus: "low lower lowest"
Initial: ['l', 'o', 'w', ' ', 'l', 'o', 'w', 'e', 'r', ' ', 'l', 'o', 'w', 'e', 's', 't']

Step 1: Most frequent pair is ('l', 'o'). Merge to 'lo'.
['lo', 'w', ' ', 'lo', 'w', 'e', 'r', ' ', 'lo', 'w', 'e', 's', 't']

Step 2: Most frequent pair is ('lo', 'w'). Merge to 'low'.
['low', ' ', 'low', 'e', 'r', ' ', 'low', 'e', 's', 't']

Step 3: Most frequent pair is ('low', 'e'). Merge to 'lowe'.
['low', ' ', 'lowe', 'r', ' ', 'lowe', 's', 't']

Continue until vocabulary size target...

Properties:

  • Deterministic tokenization given trained vocabulary
  • Common words tend to be single tokens
  • Rare words split into subwords
2.2

WordPiece

Used by BERT-family models.

Key difference from BPE:

  • BPE: Merge based on frequency
  • WordPiece: Merge based on likelihood improvement
Texttext · 1 line
1
Score = freq(AB) / (freq(A) * freq(B))

This favors merges that are more meaningful than random co-occurrence.

Visual marker: WordPiece uses ## prefix for continuation tokens:

Texttext · 1 line
1
"embedding" becomes ["em", "##bed", "##ding"]
2.3

Unigram (SentencePiece)

Used by T5, ALBERT, some multilingual models.

Training algorithm:

  1. Start with large candidate vocabulary
  2. Compute loss if each token were removed
  3. Remove tokens that increase loss least
  4. Repeat until vocabulary size reached

Key difference: Works with probabilities rather than frequencies. Can recover from suboptimal early merges.

2.4

Comparison

AlgorithmMerge CriterionTokenizationUsed By
BPEFrequencyDeterministicGPT, Llama, Claude
WordPieceLikelihoodDeterministicBERT, DistilBERT
UnigramProbabilityProbabilisticT5, mT5, XLNet
03design tradeoffs

Vocabulary Design Tradeoffs

3.1

Vocabulary Size

SizeExampleProsCons
Small (10K)Some early modelsSmaller embeddingsLong token sequences
Medium (32K)Llama 2Good balanceMultilingual inefficiency
Large (128K)Llama 3/4, GPT-4oStandard in late 2025. High compression ratio.Larger embeddings table
Huge (200K+)GPT-5.2 (o200k)Native multimodal & multilingual efficiencyMemory pressure at the LM Head

The 2025 Vocab Expansion (Deep Dive):

  • Llama 3/4 (128k): By moving from 32k to 128k, Meta improved English compression by ~15% and non-English languages like Hindi by 3-4x.
  • GPT-4o/5.2 (o200k_base): Tiktoken's latest encoding provides superior compression for code and multilingual text, reducing API costs indirectly by using fewer tokens for the same meaning.
3.2

Character vs Subword vs Word

GranularityExampleTokens for "running"Tradeoffs
CharacterByT5['r','u','n','n','i','n','g']Handles any text but very long sequences
SubwordGPT['running'] or ['run','ning']Good balance
WordEarly NLP['running']Short sequences but cannot handle OOV

Modern LLMs universally use subword tokenization for the balance of vocabulary size and sequence length.

3.3

Byte-Level BPE

GPT-2 introduced byte-level BPE:

  • Base vocabulary is 256 bytes, not characters
  • Can represent any text without UNK tokens
  • Unicode handled naturally as byte sequences
Pythonpython · 5 lines
12345
# Character-level: Needs explicit handling of characters
text = "cafe"  # Unknown character might become [UNK]

# Byte-level: Works with any text (no UNK needed)
text = "cafe"  # Becomes bytes, then BPE operates on bytes
04tokens

Special Tokens

Special tokens handle structural information outside normal text:

TokenPurposeExample
BOSBeginning of sequenceSignals start of generation
EOSEnd of sequenceSignals completion
PADPaddingFill batches to equal length
UNKUnknown tokenFallback for OOV (rare with byte BPE)
SEPSeparatorDivide segments (BERT-style)
4.1

Chat Templates

Modern chat models use special tokens for conversation structure:

Llama 2 format:

Texttext · 5 lines
12345
[INST] <<SYS>>
You are a helpful assistant.
<</SYS>>

User message here [/INST] Assistant response here

ChatML (OpenAI style):

Texttext · 6 lines
123456
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
Hello!<|im_end|>
<|im_start|>assistant
Hi there!<|im_end|>

Why this matters:

  • Wrong formatting leads to poor results
  • Special tokens are not in pre-training data
  • Libraries like transformers use chat_template for automatic formatting
05tokenization

Multilingual Tokenization

5.1

The Challenge

Tokenizers trained primarily on English have poor efficiency for other languages:

LanguageTokens for "Hello"Tokens for equivalent greeting
English1 ("Hello")-
Chinese-2-3+ for equivalent
Japanese-3-5+ for equivalent
Korean-2-4+ for equivalent

Cost implication: Non-English users pay 2-3x more per semantic unit.

5.2

Solutions

  1. Multilingual training corpus: Train tokenizer on balanced multilingual data
  2. Larger vocabulary: More room for non-English tokens
  3. Language-specific tokenizers: Separate tokenizers per language family

Models with good multilingual support:

  • mT5, XLM-R: Trained on 100+ languages
  • GPT-4, Claude 3.5: Large vocabulary with multilingual coverage
  • Gemini: Designed for multilingual from the start
ModelChineseJapaneseKoreanHindi
GPT-22.5x3.0x2.8x6.0x
GPT-4 (cl100k)1.4x1.6x1.5x3.2x
GPT-5.2 (o200k)1.1x1.2x1.1x1.4x
Llama 3/4 (128k)1.2x1.3x1.2x1.5x
06tokenization pixels-to-tokens

Multimodal Tokenization (pixels-to-tokens)

Modern native multimodal models do not just "see" images; they tokenize them.

6.1

Image Tokenization (Vision Transformers)

Images are split into patches (e.g., 14x14 pixels). Each patch is passed through a vision encoder (like SigLIP) to produce a single visual token.

  • Fixed Token Cost: Most models use a fixed number of tokens per image at a specific resolution (e.g., 256 or 729 tokens per image).
  • Dynamic Resolution: Some models (Gemini 3) use a variable number of tokens depending on image aspect ratio and detail level.
6.2

Audio/Video Tokenization

  • Audio: Compressed into discrete units using codecs like EnCodec, then represented as a sequence of audio tokens.
  • Video: Treated as a sequence of image frames (temporal tokenization). A 1-second video @ 1FPS might cost as much as 1 high-res image.
07cost estimation

Token Counting for Cost Estimation

7.1

Quick Estimation Rules

For English text:

Words to tokens

~1.3 tokens per word

Characters to tokens

~4 characters per token

Pages to tokens

~500-800 tokens per page

Pythonpython · 4 lines
1234
def estimate_tokens(text: str) -> int:
    # Rough estimation for English
    word_count = len(text.split())
    return int(word_count * 1.3)
7.2

Accurate Counting

Use the model-specific tokenizer:

Pythonpython · 12 lines
123456789101112
import tiktoken

# For OpenAI models
encoding = tiktoken.encoding_for_model("gpt-4")
tokens = encoding.encode("Your text here")
token_count = len(tokens)

# For Llama/Anthropic, use transformers
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b")
tokens = tokenizer.encode("Your text here")
token_count = len(tokens)
7.3

Cost Calculation

Pythonpython · 16 lines
12345678910111213141516
def calculate_cost(input_text: str, output_text: str, model: str) -> float:
    pricing = {
        "gpt-4o": {"input": 2.50, "output": 10.00},  # per 1M tokens
        "gpt-4o-mini": {"input": 0.15, "output": 0.60},
        "claude-3.5-sonnet": {"input": 3.00, "output": 15.00},
    }
    
    encoding = tiktoken.encoding_for_model(model)
    input_tokens = len(encoding.encode(input_text))
    output_tokens = len(encoding.encode(output_text))
    
    cost = (
        (input_tokens / 1_000_000) * pricing[model]["input"] +
        (output_tokens / 1_000_000) * pricing[model]["output"]
    )
    return cost
08tokenization issues

Common Tokenization Issues

8.1

Issue 1: Token Boundary Misalignment

Problem: Text operations may not align with token boundaries.

Pythonpython · 4 lines
1234
text = "Hello world"
# Tokens: ["Hello", " world"]  # Note: space is part of second token

# Truncating at character 6 ("Hello ") splits a token

Solution: Always truncate at token boundaries when managing context.

8.2

Issue 2: Inconsistent Tokenization

Problem: Same text tokenizes differently based on context.

Pythonpython · 4 lines
1234
# GPT tokenizer example
"New York"     # Might be ["New", " York"]
"NewYork"      # Might be ["New", "York"]
" New York"    # Might be [" New", " York"]

Implication: Token counts can vary based on surrounding text. Always tokenize the full context.

8.3

Issue 3: Code and Structured Data

Problem: Code and JSON often tokenize inefficiently.

Pythonpython · 7 lines
1234567
# Python code often tokenizes poorly
"def calculate_average(numbers):"
# Becomes many tokens: ["def", " calculate", "_", "average", "(", "numbers", "):", ...]

# JSON keys tokenize individually
'{"firstName": "John"}'
# Many tokens for structure

Mitigation:

  • Some models have code-optimized tokenizers
  • Consider compressing JSON before sending
  • Use structured output modes when available
8.4

Issue 4: Whitespace Handling

Problem: Tokenizers handle whitespace differently.

Pythonpython · 5 lines
12345
# Leading spaces often become separate tokens
" Hello"  # [" ", "Hello"] or [" Hello"]

# Multiple spaces may merge or stay separate
"Hello  world"  # Behavior varies by tokenizer

Best practice: Normalize whitespace before tokenizing.

09tokenization patterns

Practical Tokenization Patterns

9.1

Pattern 1: Context Window Management

Pythonpython · 30 lines
123456789101112131415161718192021222324252627282930
def fit_to_context(
    system_prompt: str,
    user_message: str,
    history: list[str],
    max_tokens: int = 8000,
    reserve_for_output: int = 2000
) -> str:
    encoding = tiktoken.encoding_for_model("gpt-4")
    
    available = max_tokens - reserve_for_output
    
    # System prompt always included
    tokens_used = len(encoding.encode(system_prompt))
    available -= tokens_used
    
    # User message always included
    tokens_used = len(encoding.encode(user_message))
    available -= tokens_used
    
    # Add history from most recent, drop oldest if needed
    included_history = []
    for msg in reversed(history):
        msg_tokens = len(encoding.encode(msg))
        if msg_tokens <= available:
            included_history.insert(0, msg)
            available -= msg_tokens
        else:
            break
    
    return format_prompt(system_prompt, included_history, user_message)
9.2

Pattern 2: Chunking at Token Boundaries

Pythonpython · 18 lines
123456789101112131415161718
def chunk_at_token_boundaries(
    text: str,
    chunk_size: int = 500,
    overlap: int = 50
) -> list[str]:
    encoding = tiktoken.encoding_for_model("gpt-4")
    tokens = encoding.encode(text)
    
    chunks = []
    start = 0
    while start < len(tokens):
        end = min(start + chunk_size, len(tokens))
        chunk_tokens = tokens[start:end]
        chunk_text = encoding.decode(chunk_tokens)
        chunks.append(chunk_text)
        start = end - overlap
    
    return chunks
9.3

Pattern 3: Token Budget Allocation

Pythonpython · 22 lines
12345678910111213141516171819202122
class TokenBudget:
    def __init__(self, total: int):
        self.total = total
        self.allocated = {}
    
    def allocate(self, component: str, tokens: int) -> bool:
        used = sum(self.allocated.values())
        if used + tokens > self.total:
            return False
        self.allocated[component] = tokens
        return True
    
    def remaining(self) -> int:
        return self.total - sum(self.allocated.values())

# Usage
budget = TokenBudget(total=8000)
budget.allocate("system_prompt", 500)
budget.allocate("retrieved_context", 2000)
budget.allocate("user_message", 200)
budget.allocate("output_reserve", 2000)
# Remaining: 3300 tokens for conversation history
10questions

Interview Questions

Q: Why does GPT-4 struggle with simple character counting?

Strong answer: Tokenization converts text to subword units, not characters. When asked "How many 'r's in strawberry?", the model sees tokens like ["str", "aw", "berry"] rather than individual letters.

The model has to reason about the internal structure of tokens it does not directly observe. This requires memorizing or computing character compositions of tokens, which is an emergent capability that is not always reliable.

The solution is to prompt the model to spell out the word character by character first, then count. This forces the creation of character-level tokens.

Q: How would you estimate token count for cost planning?

Strong answer: For rough estimation: multiply word count by 1.3 for English text.

For accurate counting: Use the model-specific tokenizer.

  • OpenAI: tiktoken library
  • Others: transformers AutoTokenizer

Important considerations:

  • Non-English text uses 1.5-3x more tokens
  • Code and structured data tokenize inefficiently
  • Always budget extra for output tokens (typically priced higher)
  • Include system prompts and formatting tokens

For production cost estimation, I sample real requests and measure actual token usage, then apply safety margins.

Q: What happens when switching tokenizers between models?

Strong answer: Every model family has its own tokenizer. You cannot reuse tokens across models because:

  1. Vocabulary differs: Token IDs mean different strings
  2. Merge rules differ: Same text splits differently
  3. Special tokens differ: Chat formatting varies

Practical implications:

  • Always use the correct tokenizer for token counting
  • Cached embeddings are model-specific
  • Prompt templates need per-model adjustment
  • Fine-tuned models inherit their base tokenizer

Q: How do you handle tokenization for RAG chunking?

Strong answer: Key considerations:

  1. Chunk at token boundaries: Splitting mid-token corrupts text when decoded
  2. Account for template tokens: System prompt, formatting consume tokens
  3. Leave headroom: Retrieved chunks plus question must fit context

Implementation approach:

Pythonpython · 14 lines
1234567891011121314
# Determine available tokens for chunks
available = max_context - system_prompt_tokens - question_tokens - output_reserve

# Chunk with overlap at token boundaries
chunks = chunk_at_token_boundaries(document, chunk_size=500, overlap=50)

# Select chunks until budget exhausted
selected = []
tokens_used = 0
for chunk in ranked_chunks:
    chunk_tokens = count_tokens(chunk)
    if tokens_used + chunk_tokens <= available:
        selected.append(chunk)
        tokens_used += chunk_tokens
11references

References

  • Sennrich et al. "Neural Machine Translation of Rare Words with Subword Units" (BPE, 2016)
  • Wu et al. "Google's Neural Machine Translation System" (WordPiece, 2016)
  • Kudo and Richardson "SentencePiece: A simple and language independent subword tokenizer" (2018)
  • OpenAI tiktoken library: https://github.com/openai/tiktoken github.com
  • HuggingFace tokenizers: https://github.com/huggingface/tokenizers github.com

Previous: LLM Internals | Next: Attention Mechanisms

summary · added by this rebuild

Key takeaways

01

Vocabulary size is a cost decision

Moving Llama from 32K to 128K tokens improved English compression about 15% and Hindi three- to fourfold, at the price of a larger embedding table and LM head.

02

Three algorithms, three merge rules

BPE merges the most frequent pair; WordPiece merges by the freq(AB)/(freq(A)*freq(B)) likelihood gain; Unigram prunes a large candidate vocabulary by which removals cost least loss.

03

Estimate at 1.3 tokens per English word

Roughly four characters per token, 500-800 tokens per page, for planning only — anything billable needs the model's own tokenizer, since IDs and merge rules differ per family.

04

Character tasks fail because characters are invisible

The model sees subword units, so counting the r's in "strawberry" means reasoning about token internals; prompting it to spell the word out first restores character granularity.

05

Chunk on token boundaries, not characters

Splitting mid-token corrupts the decoded text, and a leading space usually belongs to the following token; budget system prompt, retrieved context and output reserve separately.