Skip to main content

BPE from Scratch

A ground-up Python implementation of Byte-Level Byte Pair Encoding (BBPE) — the tokenization algorithm used by GPT-2, GPT-3, GPT-4, and other modern LLMs.

What is Byte-Level BPE?

BPE was originally a data compression algorithm that replaces the most frequent pair of bytes in a sequence with a single unused symbol. Applied to NLP, it builds a subword vocabulary by iteratively merging the most frequent adjacent token pairs in a corpus.

The byte-level variant (introduced by OpenAI for GPT-2) operates directly on raw UTF-8 bytes rather than characters or words:

  • Base vocabulary of 256 — one token per possible byte value (0–255), no unknown tokens ever
  • Language-agnostic — any Unicode text (code, math, emoji, CJK, ...) is representable without a special <UNK> token
  • Lossless — encoding and decoding are exact roundtrips
  • Merges learned greedily — at each step, the most frequent adjacent pair is merged and assigned a new token ID (256, 257, ...)

This is the same fundamental approach used by tiktoken (OpenAI) and Hugging Face tokenizers for GPT-style models.

Algorithm Phases

Phase Description
1 UTF-8 encoding — normalize and encode input text to bytes
2 Byte → token conversion — represent each byte as an integer token ID in [0, 255]
3 Pair counting — count all adjacent token pairs in the sequence
4 Merge — replace the most frequent pair with a new token ID
5 Repeat — iterate until the target vocabulary size is reached

Installation

pip install bpe-from-scratch

Usage

Train from scratch

from bpe_from_scratch import ByteLevelBPE

bpe = ByteLevelBPE()
bpe.train(text, vocab_size=1024)  # 1024 total tokens, 768 merge rules
bpe.save("my_model.json")

Or train directly from a folder of .txt files:

from bpe_from_scratch.train import train_from_folder

bpe = train_from_folder(
    folder_path="data/corpus_A/",
    model_path="my_model.json",
    vocab_size=1024,
)

Utilize all CPU cores

Pass num_workers=os.cpu_count() to parallelize pre-tokenization across all cores (useful for large corpora):

import os
from bpe_from_scratch import ByteLevelBPE

bpe = ByteLevelBPE()
bpe.train(text, vocab_size=50_257, num_workers=os.cpu_count())

Or via the folder helper:

import os
from bpe_from_scratch.train import train_from_folder

train_from_folder(
    folder_path="data/corpus/",
    model_path="my_model.json",
    vocab_size=50_257,
    num_workers=os.cpu_count(),
)

Note: On Windows, guard the call site with if __name__ == "__main__": due to spawn-based multiprocessing. On macOS/Linux, fork is used by default and no guard is needed.

Encode and decode

tokens = bpe.encode("Hello, world!")  # list[int]
text   = bpe.decode(tokens)           # "Hello, world!"

Continue training on new data

Load an existing model and extend the vocabulary without discarding what was already learned:

bpe = ByteLevelBPE()
bpe.load("my_model.json")
bpe.continue_train(new_text, new_vocab_size=1280)  # extend to 1280 total tokens
bpe.save("my_model.json")

All previously learned token IDs remain stable — documents encoded with the old model are still valid after the update.

Or use the folder helper:

from bpe_from_scratch.train import continue_train_from_folder

bpe = continue_train_from_folder(
    folder_path="data/corpus_B/",
    model_path="my_model.json",
    new_vocab_size=1280,
)

Note: continue_train uses a frozen-base approach — existing merges are replayed on the new text before new rules are learned. This keeps token IDs stable but is not equivalent to a full retrain on the combined corpus. See TRAINING_GUIDE.md for details and tradeoffs.

Project Structure

src/bpe.py        # Core implementation
tests/test_bpe.py # Unit tests
tests/manual/     # Interactive notebooks for experimentation

Running Tests

PYTHONPATH=src python3 -m unittest discover -s tests -v

Acknowledgements

Inspired by Andrej Karpathy's minbpe.

References

Metadata

Release files for bpe-from-scratch 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bpe-from-scratch 0.3.0
File Size Uploaded
bpe_from_scratch-0.3.0.tar.gz 681.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bpe-from-scratch 0.3.0
File Interpreter ABI Platform
bpe_from_scratch-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 691.8 kB

Release files / bpe_from_scratch-0.3.0.tar.gz

Download URL bpe_from_scratch-0.3.0.tar.gz
Size 681.6 kB
Tags Source
SHA-256 checksum
How to use checksums
88ad6bec25e58091c5190f4fdc2178997e2185cd7782b3db14a8b3837fce775f
BLAKE2b-256 checksum
How to use checksums
005c7c2f259835c471c6fcd5ea567e8098d88de52b20eefe3f44bbba31367289
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.8

Release files / bpe_from_scratch-0.3.0-py3-none-any.whl

Download URL bpe_from_scratch-0.3.0-py3-none-any.whl
Size 10.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dd675c0ec6f6c6488fa7b315080853e100b68d395fa38f43789d68fa5d6cff5b
BLAKE2b-256 checksum
How to use checksums
ac3bbcb5c87518f2f7947794c3b4c2f6975b6b07f23fa9363b233023c954fd6d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.8

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page