Tokenizer
What a tokenizer does
A neural network cannot consume raw text — it consumes integer IDs. A tokenizer is the bridge: it maps a string to a sequence of IDs that index into a vocabulary of size V. The model's embedding layer is then a V × d_model matrix of learned vectors, one per token ID.
Two extremes on the vocabulary-size axis:
- Character-level.
Vis tiny (~256 for ASCII + some Unicode), but sequences become very long. The model wastes compute on short patterns it has to rediscover from scratch (t,h,e→ "the"). - Word-level. Sequences are short, but
Vexplodes and any rare or unseen word collapses to<unk>, destroying information.
Byte-Pair Encoding (BPE) is the pragmatic middle. Start from bytes (256 symbols), then repeatedly merge the most frequent adjacent pair into a new token. Frequent sub-words collapse to single tokens; rare words decompose into a few sub-tokens. No <unk> is ever needed because bytes cover everything.
What changed in v0.2.0
The v0.1.x line trained its own byte-level BPE from scratch on the upsampled training corpus (32 K dev vocab, 128 K target). That worked but forced the downstream cache chain to anchor on a tokenizer that could drift between training runs, and it took tens of minutes to an hour on a real corpus.
v0.2.0 replaces that with tiktoken's o200k_base encoding — the encoding tiktoken.encoding_for_model("gpt-4o") resolves to. Vocabulary is fixed (n_vocab = 200_019), ships bundled in the tiktoken wheel, loads in milliseconds, and is identical across every machine.
The extended encoding we build at runtime (o200k_base_lavender_chat) adds four chat-format specials on top:
| Token | Purpose |
|---|---|
<|im_start|> | turn start |
<|im_sep|> | role / content separator |
<|im_end|> | turn end |
<|pad|> | batch padding |
Final vocab size: 200,023.
The TiktokenAdapter
The rest of the notebook expects the HuggingFace tokenizers-style API: tok.encode(text).ids, tok.decode(ids), tok.token_to_id(...), tok.get_vocab_size(), tok.save(path), tok.to_str(). TiktokenAdapter is a thin wrapper that exposes those method names on top of a tiktoken.Encoding:
encode(text)passesallowed_special=ALLOWED_SPECIALso the four chat specials are recognised as tokens rather than raising.token_to_id(s)looks up specials in the adapter's local table and falls back toencode_single_token(s)for regular tokens.save(path)writes a small JSON sidecar ({adapter, base, specials, n_vocab}) — not loadable as a HuggingFacetokenizer.json, butTiktokenAdapter.from_file(path)rebuilds the live adapter from it.to_str()returns a content-derived JSON blob used by_tokenizer_sha; any change to the base encoding or specials invalidates every downstream dataset cache automatically.
Chat template
The rendered template at training time is GPT-4o's production ChatML:
<|im_start|>user<|im_sep|>What is the weather?<|im_end|><|im_start|>assistant<|im_sep|>It's sunny.<|im_end|>No whitespace between turns. The role string (user, assistant, system) is regular text, encoded with the base BPE — only the structural markers are specials. That matches OpenAI's serving behavior and keeps the representation portable to any model that uses o200k_base.
Why this is usually the right default now
- No training run.
tiktoken.get_encoding("o200k_base")loads in milliseconds; the old BPE trainer took tens of minutes on the real corpus. - Stable across runs. Every machine and every session gets the exact same vocabulary. No merge-order drift when the corpus order changes.
- Portability. Checkpoints tokenised with o200k_base are directly comparable to any GPT-4o / GPT-4.1 / ChatGPT-4o-latest representation. Migrating to or from pretrained weights is tokenisation-compatible.
- Coverage. o200k_base is optimised for modern multilingual text (including code, emoji, CJK); it dominates hand-rolled small BPEs on those segments.
What hasn't changed
- Still byte-safe.
o200k_baseis a byte-level BPE just like the v0.1.x tokenizer. Any UTF-8 input encodes losslessly. - Still CPU-only at encode time. tiktoken is Rust-backed; there is no CUDA encode path, and there doesn't need to be — encoding a batch of conversations at
__getitem__time is microseconds. - Still content-keyed downstream. The
tok_shain thePretrainDataset/SFTDataset/DPODatasetcache keys comes fromadapter.to_str()now instead of the HFto_str(), but the invalidation semantics are identical: change the encoding, the specials, or the chat template → the caches rebuild.
Reading the code
The tokenizer cell lives in full_llm_pipeline.ipynb; search for TiktokenAdapter. The chat template is rendered by conversation_to_text(...) (for pretrain) and by explicit [im_start] + role_ids + [im_sep] + content_ids + [im_end] construction inside SFTDataset.__init__ and DPODataset.__init__.
Papers and references
- Sennrich, Haddow & Birch, Neural Machine Translation of Rare Words with Subword Units, ACL 2016. https://arxiv.org/abs/1508.07909.
- Byte-level variant: Radford et al., Language Models are Unsupervised Multitask Learners (GPT-2 tech report), 2019.
- tiktoken library: https://github.com/openai/tiktoken.
- HuggingFace mirror of the same vocabulary: https://huggingface.co/toksuite/tiktoken-gpt-4o.
Next stage: Architecture — the Transformer block that consumes these token IDs.