Token IDs
Master token IDs as integer vocabulary addresses — the bridge from tokenizer output to embedding lookup and the gateway into a model's numeric world.
Token IDs: The Librarian's Index Card Numbers
A grand library doesn't search for books by wandering through millions of aisles reading covers. The librarian looks up the exact catalog index number (e.g. #17242 for "apple"). In neural networks, a Token ID is simply that catalog number: it directs the model to pull Row #17242 out of the massive Embedding Matrix to retrieve the rich concept vector stored inside.
IntroductionWhat is a Token ID?

"If you were writing a letter to a friend, why would you translate every single word into a dry postal box address before mailing it? Why does an AI secretly turn your warm words into numeric barcodes?"
To humans, words are ideas, feelings, and memories. But to a computer's processor, text is completely un-multipliable. A computer cannot multiply the word "love" by a matrix. It needs a barcode address system: Token IDs.
"I", Apartment 47 houses "love", and Apartment 91 houses "AI". When you write "I love AI", the tokenizer translates it into a sequence of mailbox addresses: [12, 47, 91]. To the model, your sentence is just a string of delivery stops. The AI opens those specific apartment doors to pull out the actual floating-point numbers describing what those words mean!
A token ID is a single integer. It is the row index of a token in the model's embedding matrix. The tokenizer converts text to a list of token strings, then maps each string to its integer ID. The model never sees the string — only the number.
What a token is
A piece of text — a word, subword, character, or special symbol. It is a string like " love" or "##ing". Humans and linguists think in tokens.
What an ID is
A non-negative integer: 47. It is the address of that token in the vocabulary. Models and matrix multiplications think in IDs.
💡 IDs are just addresses. Token ID = the integer row number of a token inside the embedding matrix. Nothing more, nothing less.
WhyWhy do we need IDs at all?
Neural networks operate on floating-point tensors. There is no operation "multiply a string by a matrix." You need a way to turn a discrete category (the token) into a number so the model can retrieve its learned representation.
- A matrix multiply requires numeric input — strings are meaningless to PyTorch.
- IDs are the categorical lookup keys that map each token to a row in the embedding matrix.
- Without IDs, there is no addressable way to retrieve the learned float vector for a token.
- IDs let you represent a full sentence as a compact integer array — far more efficient than storing strings in memory.
Without IDs — impossible
You have a string "love". You cannot index a matrix with a string. You cannot pass it to nn.Embedding. You cannot batch it into a tensor.
With IDs — everything works
ID 47 is an integer. embedding_matrix[47] returns a row. You can build a batch tensor [[12,47,91],[5,3,47]] and feed it to the model directly.
IntuitionLibrary call numbers
Token IDs are like library call numbers in the Dewey Decimal System. The number 791.43 tells you the shelf where films are shelved. It does not mean films are 791.43 times anything. It is just an address.
Token ID 500 is not "more meaningful" or "more important" than ID 20. It is not five times anything. It just lives on a different shelf — row 500 versus row 20 in the embedding matrix. The number tells you where to look, never what to think about what you find.
💡 Critical mental model: Never do arithmetic on IDs. ID_500 − ID_20 = 480 means nothing. The semantic relationship between tokens lives in their embedding vectors, not in the distance between their IDs.
TheoryThe vocabulary bijection
The vocabulary is a bijective mapping (one-to-one, onto) between token strings and integers:
- IDs start from 0. They are contiguous integers: 0, 1, 2, …, vocab_size−1.
- Special tokens (
[PAD],[UNK],[BOS],[EOS],[MASK], etc.) occupy reserved IDs, usually at the low end. - The embedding matrix has shape
[vocab_size × embed_dim]. Row N is the learned float vector for token ID N. - Accessing row N:
embedding_matrix[N]returns a vector ofembed_dimfloats (e.g. 768 for BERT-base).
How It WorksEncode and decode cycle
There are exactly two operations on the vocabulary mapping:
- encode (token_string → ID): look up the string in the
vocabdict. If missing, return the ID of[UNK]. - decode (ID → token_string): look up the integer in the
id_to_tokenreverse dict.
CalculateManual encode / decode exercise
Build a toy vocabulary with 5 entries, then encode and decode by hand. Follow each step carefully.
{ "[PAD]": 0, "[UNK]": 1, "I": 2, "love": 3, "AI": 4 }
"I" → 2 "love" → 3 "AI" → 4 → Result: [2, 3, 4] ✓
"I" → 2 "love" → 3 "pizza" → [UNK] = 1 → Result: [2, 3, 1]
2 → "I" 3 → "love" 1 → "[UNK]" → Decoded: "I love [UNK]" ← information lost!
[UNK] during encoding, the original string cannot be recovered during decoding. This is why modern tokenizers (BPE, WordPiece) aim for zero-UNK coverage by building large subword vocabularies.MathematicsID as row index into the embedding matrix
The embedding matrix $\mathbf{E}$ has shape $[V, D]$ where $V$ is the vocabulary size and $D$ is the embedding dimension. A token ID is simply a row selector.
For token ID $i$, the embedding lookup retrieves row $i$:
$$\mathbf{e}_i = \mathbf{E}[i, :] \in \mathbb{R}^{D}$$For a sequence of $L$ token IDs $[i_1, i_2, \ldots, i_L]$, the model receives:
$$\mathbf{X} = \mathbf{E}[[i_1, i_2, \ldots, i_L]] \in \mathbb{R}^{L \times D}$$ID has no numeric meaning
ID 91 is not "91 units of something". It is an address. The meaning of the token lives entirely in row 91 of E — a learned 768-float vector.
Embedding lookup is O(1)
Matrix row selection is a constant-time operation. No computation is done — the model just reads the pre-learned row. The learning happened during training.
nn.Embedding(vocab_size, embed_dim) is literally a matrix of shape [vocab_size, embed_dim] with a forward pass that does weight[input_ids] — pure row indexing.Vocabulary row vs embedding row
A token ID can be treated as a row ID in two tables. The numbers below are illustrative.
- The vocabulary maps tokens to IDs (and often back). The embedding matrix maps IDs to learned vectors.
- The matrix stores no token strings; it lines up with the vocabulary only through the shared ID.
- The ID is the key. The learned vector is the content, and it is what carries the numerical representation. See Token Embeddings for the full walkthrough.
Does a token ID contain the vector numbers?
No. A token ID identifies which row of the embedding matrix to read. The numbers stored in that row form the token's vector. Using the toy 4-number row for cat from the table above (illustrative values):
💡 Token ID = which row? Vector = the learned numbers stored in that row. ID 1 is not the vector [0.80, −0.30, 0.40, 0.15]; it only tells the model which vector to retrieve. Where those numbers come from is covered in How Embeddings Are Learned, and what "4 numbers" means is covered under vector dimensions.
VisualizeVocabulary table, encode/decode diagram, and live demo
Sample vocabulary entries showing the ID ↔ token mapping:
| ID | Token | Type |
|---|---|---|
| 0 | [PAD] | Special — padding |
| 1 | [UNK] | Special — unknown |
| 2 | [BOS] | Special — beginning of sequence |
| 3 | [EOS] | Special — end of sequence |
| 4 | the | Common word |
| 5 | of | Common word |
| 6 | and | Common word |
| 7 | ing | Common subword suffix |
🔄 Encode / Decode Flow
graph LR
A["String: 'AI'"] -->|Vocabulary Map| B["Token ID: 9821"]
B -->|Embedding Table Index| C["Row #9821 of E"]
C -->|Output| D["Vector: [0.15, -0.92, ..., 0.38]"]
D -->|argmax during generation| B2["Token ID: 9821"]
B2 -->|Reverse vocab lookup| A2["String: 'AI'"]
style A fill:#3b82f6,stroke:#1d4ed8,color:#fff
style B fill:#10b981,stroke:#047857,color:#fff
style D fill:#8b5cf6,stroke:#6d28d9,color:#fff
style B2 fill:#10b981,stroke:#047857,color:#fff
style A2 fill:#3b82f6,stroke:#1d4ed8,color:#fff
| Row (ID) | Token String | $d_0$ | $d_1$ | $d_2$ | $d_3$ | ... | $d_{767}$ |
|---|
🔍 Behind-the-Scenes: PyTorch nn.Embedding Implementation
How a framework actually runs embedding lookup behind the scenes:
- Zero Arithmetic:
nn.Embeddingis NOT a neural network with matrix multiplications; it is a direct lookup table in GPU memory ($O(1)$ memory fetch). - Gather Operation: When given
torch.tensor([2, 47, 91]), PyTorch callstorch.index_select(weight, 0, ids). - Resulting Tensor: Stacks 3 float vectors of dimension 768 to output a tensor of shape
[3, 768]ready for Self-Attention!
CodePython implementation + HuggingFace comparison
Part A — toy vocabulary in pure Python:
# ── Toy vocabulary ────────────────────────────────────────────────
vocab = {
"[PAD]": 0, "[UNK]": 1, "[BOS]": 2, "[EOS]": 3,
"I": 4, "love": 5, "AI": 6, "and": 7, "learning": 8,
"the": 9, "of": 10, "to": 11, "a": 12, "is": 13,
"tokens": 14, "model": 15, "text": 16, "word": 17,
}
id_to_token = {v: k for k, v in vocab.items()} # reverse map
def encode(sentence: str) -> list[int]:
"""Whitespace-tokenize and look up IDs; unknown → UNK."""
return [vocab.get(tok, vocab["[UNK]"]) for tok in sentence.split()]
def decode(ids: list[int]) -> str:
"""Convert IDs back to token strings."""
return " ".join(id_to_token.get(i, "[UNK]") for i in ids)
# ── Encode ─────────────────────────────────────────────────────────
ids = encode("I love AI")
print(ids) # [4, 5, 6]
ids_unk = encode("I love pizza")
print(ids_unk) # [4, 5, 1] ← pizza → UNK (ID 1)
# ── Decode ─────────────────────────────────────────────────────────
print(decode([4, 5, 6])) # "I love AI"
print(decode([4, 5, 1])) # "I love [UNK]"
Part B — HuggingFace tokenizer (GPT-2):
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2")
# ── Encode ─────────────────────────────────────────────────────────
ids = tokenizer.encode("I love AI")
print(ids) # [40, 1842, 9552] (real GPT-2 IDs)
# ── Decode ─────────────────────────────────────────────────────────
text = tokenizer.decode(ids)
print(text) # "I love AI"
# ── Convert to tokens (strings) ────────────────────────────────────
tokens = tokenizer.convert_ids_to_tokens(ids)
print(tokens) # ['I', 'Ġlove', 'ĠAI']
# Note: Ġ is GPT-2's representation of a leading space
# ── Get ID of a single token ────────────────────────────────────────
print(tokenizer.convert_tokens_to_ids("love")) # a specific integer
# ── Special token IDs ──────────────────────────────────────────────
print(tokenizer.eos_token_id) # 50256 for GPT-2
print(tokenizer.pad_token_id) # None for GPT-2 (no PAD in GPT-2)
"love" will have a different integer ID in each tokenizer. IDs are model-specific — always know which tokenizer you are using.ExperimentIDs change across tokenizers
The same surface form gets completely different integer IDs in different tokenizers. Even within one tokenizer, context like a leading space changes the ID.
Same word, different tokenizers (GPT-2 style):
Try these experiments yourself with a real tokenizer:
CompareToken String vs Token ID vs Embedding Vector
Here is exactly where token IDs sit relative to the other representations in an LLM pipeline:
| Property | Token String | Token ID | Embedding Vector |
|---|---|---|---|
| Type | String (text) | Integer | Array of floats |
| Example | " love" | 47 | [0.12, -0.34, …] (768 values) |
| Semantic meaning | Human-readable ✓ | None (just an address) | Full learned meaning ✓ |
| Fed to model directly | No — strings not accepted | Yes — integer index | Yes — after lookup |
| Size in memory | Variable (bytes per char) | 4 bytes (int32) | embed_dim × 4 bytes (≈ 3 KB) |
| Can do arithmetic on | Concatenation only | No (meaningless) | Yes — cosine similarity, addition, etc. |
| Survives round-trip | Source of truth | Yes (via id_to_token) | Not invertible |
| Used for | Tokenizer I/O, display | Embedding lookup, batching | Attention, FFN, generation |
🧠 What does the model know right after tokenization?
Not much yet. After tokenization the text has become discrete token IDs. Those IDs identify tokens, but the integers themselves do not encode meaning in a numeric geometry the transformer can use. Compare the three stages (IDs below are illustrative; real IDs depend on the tokenizer):
- Why not feed IDs directly?
cat→ 3797 anddog→ 8531 does not mean a dog is "more" of anything than a cat. A larger ID is not more important or more similar. - Same ID, different context.
bankhas one ID in "The bank approved my loan" and in "We sat on the bank of the river". The transformer builds a different contextual representation for each from the surrounding words. - Knowledge is not acquired at the embedding step. The embedding matrix and transformer weights were learned in training; the lookup and the layers apply them to your input.
ApplyToken IDs in real model pipelines
- Model input tensors are integer ID arrays. The first thing any Transformer sees is a
torch.LongTensorof shape[batch_size, sequence_length]filled with token IDs. - Batching requires padding: sentences in a batch have different lengths. All are padded to the same length using PAD IDs so they form a rectangular tensor.
- Attention mask: a companion binary tensor (1 = real token, 0 = PAD) tells the model which IDs are real content versus padding. Without it, the model attends to PAD positions.
- Context length is measured in number of IDs, not words. GPT-4's 128k context means 128,000 token IDs maximum.
- nn.Embedding in PyTorch takes an integer tensor of IDs and returns the corresponding embedding rows — this is the first layer of every language model.
import torch
import torch.nn as nn
# Batch of 2 sentences, padded to length 5 with PAD ID=0
input_ids = torch.LongTensor([
[4, 5, 6, 0, 0], # "I love AI [PAD] [PAD]"
[9, 10, 7, 8, 3], # "the of ing learning [EOS]"
])
attention_mask = torch.LongTensor([
[1, 1, 1, 0, 0], # 1=real token, 0=padding
[1, 1, 1, 1, 1],
])
# Embedding layer — the first thing the model does with IDs
embedding = nn.Embedding(num_embeddings=50257, embedding_dim=768)
vectors = embedding(input_ids)
print(vectors.shape) # torch.Size([2, 5, 768])
# batch=2, seq_len=5, embed_dim=768
💡 The integer tensor explodes in size. Input shape is [B, L] of 4-byte ints. After embedding lookup it becomes [B, L, D] of 4-byte floats — a >1000× memory expansion at the very first layer.
LimitationsCommon mistakes and traps
tokenizer.pad_token_id. GPT-2 has no PAD token at all by default.Mistakes to avoid
- Sorting tokens by ID expecting semantic order
- Comparing IDs across different models
- Assuming the same word always gets the same ID
- Using ID magnitude to infer token importance
- Feeding IDs from tokenizer A to model B
Correct mental models
- ID = row address in the embedding matrix
- IDs are vocabulary-specific; always know which tokenizer
- Meaning = embedding vector, not the integer
- Do arithmetic on embedding vectors, not on IDs
- Store tokenizer name alongside any saved IDs
ExplainFeynman Challenge: Library Catalogue Numbers
Token IDs are like library catalogue numbers. In a large library, every book has a Dewey Decimal call number like 791.43. That number tells you exactly which shelf and slot to look in. It does not tell you anything about the book's content — it is purely an address.
Token ID 14592 for "apple" is the call number for apple's shelf in the embedding library. When the model "opens that shelf" it retrieves a 768-float vector encoding everything the model learned about apples during training. The number itself carries none of that meaning.
Try explaining this to someone right now: "Why do language models use integer token IDs instead of working directly with strings? And why is it wrong to treat a higher ID as more meaningful than a lower ID?"
🚫 Common Misconceptions to Correct
- Misconception: Token IDs are learned during backpropagation.
Reality: No. Token IDs are assigned statically before training begins. They are permanent, deterministic indices. The neural network only trains the embedding values that reside at those indices. - Misconception: Reordering the vocabulary list breaks the model's intelligence.
Reality: It does, but only because the embedding rows become misaligned. The indices are purely symbolic — if you swap both the vocabulary mapping table and the corresponding rows in the embedding matrix together, the model would still work correctly. - Misconception: ID 200 is "between" ID 100 and ID 300 in semantic space.
Reality: No. Semantic space is defined by embedding vectors. The distance between IDs means nothing.
BuildMini-Project: Encode/Decode Pipeline + Batch Padder + Vocab Inspector
🎯 Four Things to Build for Mastery
1 — Full encode/decode pipeline
Build a Tokenizer class with a real vocabulary dict. Implement encode(text) and decode(ids). Handle UNK gracefully. Add a batch_encode(texts) that pads to the longest sequence.
2 — Batch padder
Write pad_batch(sequences, pad_id) that takes a list of variable-length integer lists and returns a rectangular 2D list padded with pad_id. Verify the shape is [batch_size, max_len].
3 — Attention mask generator
Given a list of token ID sequences of varying lengths, write make_attention_mask(batch) that pads all to the same length and returns the parallel attention mask tensor (1s for real tokens, 0s for PAD).
4 — Vocabulary inspector
Load a real HuggingFace tokenizer. Write a script that: (a) prints the 20 most common token IDs in a text corpus, (b) shows the string for any given ID, (c) finds whether two tokenizers assign the same ID to the word "the".
Extra credit: Add a JSON serializer/deserializer to your vocabulary class. Write save_to_json(filepath) and load_from_json(filepath). Add a validation step that checks for no duplicate IDs and no index gaps — critical for preventing segmentation faults during model load time.
📌 Summary — Everything in Six Bullets
- ID = integer address. A token ID is a non-negative integer that identifies a token by its row position in the embedding matrix. It is not a score, not a frequency, not a magnitude — just an address.
- Embedding matrix lookup. The embedding matrix E has shape [vocab_size, embed_dim]. A token ID N selects row N:
E[N]returns the 768-float (or similar) vector that the model actually uses. - No semantic numeric meaning. Never do arithmetic on IDs. ID 200 is not more meaningful than ID 20. Semantic similarity lives in the embedding vectors, not in the distance between IDs.
- Special token IDs. Reserved IDs (PAD=0, UNK=1, BOS=2, EOS=3 by convention) handle padding, unknowns, and sequence boundaries. Always check your specific tokenizer's special token IDs — conventions differ.
- Padding uses PAD IDs. Batches require rectangular tensors. Shorter sequences are right-padded with PAD IDs. An attention mask (1=real, 0=pad) prevents the model from attending to padding positions.
- IDs are model-specific. BERT ID 500 ≠ GPT-2 ID 500. Token IDs are only meaningful within the tokenizer and model they were created for. Always store the tokenizer name alongside any saved IDs.
