Skip to main content

unitoken

CI PyPI crates.io docs.rs

unitoken is a fast BPE tokenizer/trainer with a Rust core and optional Python bindings.

Install

Rust:

cargo add unitoken

Python (wheels via PyPI):

pip install uni-tokenizer

Quickstart (Python)

from uni_tokenizer import BpeTrainer, BpeEncoder

trainer = BpeTrainer(["<|endoftext|>"], unit="byte")
trainer.add_words({"hello": 10, "world": 7})
trainer.train(vocab_size=256)
model = trainer.validate_model()
model.save("demo", format="gpt2")

enc = BpeEncoder.load("demo")
ids = enc.encode("hello")

Encoding uses model-vocabulary bigrams to partition long PAT words by default. This does not change token ids, but it is not always profitable for byte models. Disable it after benchmarking the target workload:

enc = BpeEncoder.load("demo", split_on_vocab_bigrams=False)

For a model trained from a retained Unicode-bigram selection, configure its inclusive cutoff on the trainer:

trainer = BpeTrainer(
  [],
  unit="unicode",
  bigram_cutoff_freq=selection.cutoff_freq,
)
model = trainer.validate_model()

Automatic train() and train_with_bbpe_fallback() calls stop before a merge below the cutoff. Manual step() calls remain unrestricted, while validation rejects a final pair merge below the cutoff. Equality is valid because bigram selection retains every tie at the cutoff.

Unicode BBPE fallback

Unicode training can use part of its learned vocabulary for byte-BPE merges inside Unicode scalars that are omitted from the direct Unicode alphabet:

trainer = BpeTrainer(
  [],
  unit="unicode",
)
trainer.add_word_counter(word_counter)
trainer.train_with_bbpe_fallback(
  vocab_size=10_000,
  primary_vocab_ratio=0.9,
)

The ratio applies only to learned slots after special tokens and the mandatory 256-byte alphabet. Training first advances the primary Unicode trainer through its configured share of learned slots, then freezes any still-unmaterialized Unicode scalars. The fallback pass may use the remaining slots for byte merges whose frequency reaches that primary boundary; unused fallback slots return to primary pair training. The primary and fallback merge streams are combined by frequency only after both phases finish. Fallback pseudo-words are isolated per Unicode scalar, so the byte pass never learns across scalar boundaries.

train_with_bbpe_fallback() is only valid with unit="unicode". Ordinary train(), init_training(), and step() always perform normal Unicode BPE; fallback is a separate, target-aware operation. A call that reserves fallback slots must start before ordinary vocabulary growth because its phase boundary depends on the requested vocabulary size. Once the fallback pass has run, the trainer is finalized; create a new trainer for further training. A ratio of 1.0 reserves no fallback slots, delegates to ordinary training, and remains extendable. The resulting model is still a Unicode Unitoken model; encoding behavior is carried by its merge rules, so no fallback option is needed when loading it.

Streaming two-pass counting

For corpora that do not fit in memory, expose a replayable source whose scan() method returns a fresh iterator of text records. Rust pulls and processes bounded batches from each scan:

from uni_tokenizer import BpeTrainer, PreTokenizer

pretokenizer = PreTokenizer([])

bigram_counter = pretokenizer.bigram_counter()
bigram_counter.add_source(source.scan())
bigrams = bigram_counter.selected(top_k=100_000, min_freq=16)

word_counter = pretokenizer.with_unicode_bigrams(bigrams).word_counter()
word_counter.add_source(source.scan())

trainer = BpeTrainer([], unit="byte")
trainer.add_word_counter(word_counter)

add_source defaults to at most 4,096 records or 64 MiB per batch. Override max_records and max_bytes for the record sizes and worker memory available. By default, it overlaps Python source iteration with Rust processing using one bounded look-ahead batch; pass prefetch=0 for synchronous processing. Counters can also be merged, so separately counted corpus partitions can be reduced before selecting bigrams or training. add_word_counter consumes the native word inventory without constructing a Python dictionary; the counter is empty and reusable afterward. word_counter.words() remains available for small inventories, but copies the complete result into Python memory.

Bounded-memory BPE training

By default, the trainer retains occurrence sets for every discovered pair. For large word inventories, hot_pair_window_size bounds persistent occurrence sets while preserving exact global pair frequencies, winner selection, and tie-breaking:

trainer = BpeTrainer(
  [],
  unit="unicode",
  hot_pair_window_size=4096,
)
trainer.add_word_counter(word_counter)
trainer.train(vocab_size=10_000)

4096 is a measured starting point, not a correctness setting. Smaller K uses less memory but can require more full inventory scans when a cold pair wins. Larger K retains more occurrence sets and generally reduces those scans. On a cold winner, the trainer hydrates the exact current top K; newly created pairs at or above the latest top-K frequency threshold are admitted immediately. If resident pairs grow beyond 2K, they are pruned back to the exact top K.

On the 1 GiB FineWeb2 Chinese Unicode-bigram inventory used by the benchmark suite (3,855,974 unique words, vocabulary size 10,000), one release run measured:

occurrence mode observed training peak RSS total training hydration scans
exact (default) 1,797 MiB 5.58s
K=4096 1,649 MiB 5.85s 2

Both modes produced the same final merge frequency and model. Inspect trainer.hot_pair_window_stats for hydration, pruning, resident-pair, and occurrence-capacity diagnostics. Corpus shape and target vocabulary size affect the best K, so benchmark representative inventories before changing the default for a deployment.

Tiktoken-compatible API

unitoken also exposes a tiktoken-shaped Python API:

from uni_tokenizer import Encoding

enc = Encoding.from_files(
  "demo",
  vocab_file="vocab.demo[u8].json",
  merges_file="merges.demo[u8].txt",
  special_tokens={"<|endoftext|>": 0},
)

ids = enc.encode("hello world")
text = enc.decode(ids)

The package also includes a uni_tokenizer.tiktoken namespace with Encoding, get_encoding, encoding_for_model, encoding_name_for_model, and list_encoding_names. Built-in registry names are limited to local unitoken fixture models for now; use Encoding.from_files(...) for trained models.

Benchmark against tiktoken

Install the dev dependency and run:

uv pip install "tiktoken>=0.12.0"
python benchmarks/compare_tiktoken.py

The benchmark reports unitoken encode/decode timings and, when upstream tiktoken is importable, matching upstream timings.

Benchmark training against Hugging Face

Install the dev dependency and run:

uv pip install "tokenizers>=0.22.1"
python benchmarks/compare_hf_training.py

The benchmark trains unitoken and Hugging Face tokenizers on the same word-frequency fixture, checks that the learned byte-level BPE vocabularies match, and reports median training speed.

For an end-to-end raw text comparison:

python benchmarks/compare_hf_training.py --text out/fineweb2_1GiB.txt --chunk-size 1048576 --boundary line --repeats 1

Raw text mode reports unitoken pretokenization and BPE training phases separately, then compares the total against Hugging Face raw training. By default, Hugging Face receives the same chunk boundaries as unitoken so vocab parity is not affected by iterator boundary differences. Pass --hf-chunk-bytes to force fixed byte chunks for Hugging Face.

Rust regression benchmark suites

Complete benchmark profiles live in benches/regression/config/. A profile can combine trainer, pretokenizer, and codec cases while keeping their inputs, correctness hashes, run settings, and report names in one reviewable file:

cargo bench --bench regression -- suite smoke
cargo bench --bench regression -- suite 64mib
cargo bench --bench regression -- suite 1gib

smoke.yml uses checked-in fixtures, includes a 90% primary Unicode BBPE trainer case plus a codec case for its pinned model, and is the profile run for pull requests. Codec cases accept split_on_vocab_bigrams: false for measured opt-out comparisons; reports and encoder fingerprints record the selected value. The 64mib.yml and 1gib.yml profiles compare ordinary and 90% primary BBPE training on the prepared FineWeb2 Chinese word inventories under out/data/, so those local inputs must exist before running them. Validate a profile without executing its cases with --check:

cargo bench --bench regression -- suite 64mib --check

Run an unregistered profile with --config; relative config and input paths are resolved from the repository root:

cargo bench --bench regression -- suite \
  --config benches/regression/config/smoke.yml \
  --output-dir /tmp/unitoken-regression

Report paths are relative to --output-dir. Pretokenizer outputs consumed by later codec cases use logical artifact names, and validation requires every artifact consumer to have exactly one producer in the same suite. The legacy smoke subcommand remains a trainer-only shorthand whose cases now come from smoke.yml; use suite smoke for the complete smoke pipeline.

Latest fixed-word trainer profile on the release build, using compressed _words.json inventories and vocab_size=10000:

dataset unique words occurrences total train train steps
FineWeb English 64MiB 298,156 13,720,494 1.151s 0.968s
FineWeb English 1GiB 1,656,501 219,082,524 4.522s 3.258s
FineWeb2 Chinese 64MiB 1,803,009 5,774,521 26.681s 20.416s
FineWeb2 Chinese bigram 64MiB 606,153 15,901,831 3.702s 3.034s
FineWeb2 Chinese bigram 1GiB 3,855,974 249,919,657 20.197s 14.169s

The Chinese bigram rows use the unicode-bigram split inventory. The default Chinese 1GiB inventory is intentionally omitted from this run; only the bigram 1GiB Chinese inventory was profiled.

Chunking supports explicit boundary modes:

  • auto: split on the EOT token when present, otherwise line boundaries, then UTF-8 byte boundaries as a last resort.
  • eot: split only on the EOT token.
  • line: split on newline boundaries.
  • utf8: split near byte boundaries while preserving valid UTF-8.

Use --chunk-size BYTES when you want target chunk size instead of a fixed chunk count.

Prepare benchmark data

To create a larger raw UTF-8 text sample from local FineWeb2 Parquet shards:

python benchmarks/create_fineweb2_sample.py --input-dir /path/to/fineweb2/10BT

This is a data-preparation step. Use the generated text with the CLI or a separate benchmark that measures pretokenization/training on raw input.

Building from source

This project uses maturin for the Python extension module.

maturin develop

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

uni_tokenizer-0.1.6.tar.gz (199.1 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

uni_tokenizer-0.1.6-cp38-abi3-win_amd64.whl (1.6 MB view details)

Uploaded CPython 3.8+Windows x86-64

uni_tokenizer-0.1.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.8 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ x86-64

uni_tokenizer-0.1.6-cp38-abi3-macosx_11_0_arm64.whl (1.6 MB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

File details

Details for the file uni_tokenizer-0.1.6.tar.gz.

File metadata

  • Download URL: uni_tokenizer-0.1.6.tar.gz
  • Upload date:
  • Size: 199.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for uni_tokenizer-0.1.6.tar.gz
Algorithm Hash digest
SHA256 1039c846d106d523cf1293a458d9d1f6062bab531224ab47264e38611f183369
MD5 6f4d1e7816a5c93f7a9bf9bbe7056668
BLAKE2b-256 79aaa539180480293061af3b7f3fe5542ecbcbf2339801c25f2f3b6e0e28b34a

See more details on using hashes here.

File details

Details for the file uni_tokenizer-0.1.6-cp38-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for uni_tokenizer-0.1.6-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 44471cfd1a2aeb0d0fd6696ebdb6dfc81fcbcf5559164417ab2173ed8e94bd08
MD5 f4186967bc338f0422633d5c38419480
BLAKE2b-256 b85fee64e399deda7ff7f7311a776cdd46d97e984c872aa8e4f9454bd5f6c015

See more details on using hashes here.

File details

Details for the file uni_tokenizer-0.1.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for uni_tokenizer-0.1.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 26c2cc153e403ace2e3a86b2a3d8bd24c8b134119c837eb8c9836fc09aa0acd1
MD5 71bc2df3ff35c739f92883f573bceffc
BLAKE2b-256 f5fdce722e4d0665893c622967188af28e49dce830101b675c57426307a94c16

See more details on using hashes here.

File details

Details for the file uni_tokenizer-0.1.6-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for uni_tokenizer-0.1.6-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ed7ace879bcf857deea304b1e00c2588afc1c934621fe74baf10c3ab2fae3c97
MD5 14f6d8f8713643cc27e5cf4208f213de
BLAKE2b-256 ed20cde844de5efd729c939f4a7f7ec42ca6a62587b2a7e11bb00163735b2d03

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.7

4 files

This release

0.1.6 This release

4 files

0.1.5

4 files

0.1.4

4 files

0.1.3

4 files

0.1.2

4 files

0.1.1

4 files

0.1.0

2 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