nanotoken
WordPiece tokenization at GB/s, with exactly the same output as HuggingFace tokenizers.
nanotoken does one thing: WordPiece, the tokenizer behind BERT and the encoder/embedding models built on it (bert-*, DistilBERT, MiniLM, BGE, E5, and many more). It is a Rust core with Python bindings. It reads HuggingFace tokenizer.json files and can drop in for a transformers tokenizer.
Standing on the shoulders of gigatoken. nanotoken exists because of Marcel Rød's excellent gigatoken, which tokenizes BPE and SentencePiece at tens of GB/s and lists WordPiece as not yet supported. nanotoken fills that gap by applying gigatoken's ideas to WordPiece:
- persistent per-worker caches of pre-token encodings,
- coarse parallel work units, with huge documents split at boundaries that provably don't change the output,
- mmap'd file sources read entirely in Rust,
- a compatibility wrapper that stands in for transformers.
If you tokenize BPE or SentencePiece models, use gigatoken. See Acknowledgements.
Performance
Encoding the OpenWebText validation sample (owt_valid.txt, 290 MB) with the BAAI/bge-small-en-v1.5 tokenizer, which is uncased BERT WordPiece. Measured on an Apple M3 laptop (8 cores: 4 performance + 4 efficiency):
| throughput | vs HF | |
|---|---|---|
nanotoken encode_files |
1,447 MB/s (317 M tokens/s) | 56× |
nanotoken encode_batch(list[str]) |
1,277 MB/s | 50× |
HF tokenizers encode_batch_fast (multithreaded Rust) |
25.7 MB/s | 1× |
All 20,177 documents in the compared 100 MB produce identical ids.
As a drop-in for transformers (tokenizer(texts, padding=True, truncation=True, return_tensors="pt") on OWT sentences, 138 characters on average):
| batch size | transformers | nanotoken as_hf() |
speedup |
|---|---|---|---|
| 32 | 1.86 ms | 0.035 ms | 53× |
| 256 | 24.3 ms | 0.120 ms | 203× |
Reproduce with the built-in benchmark:
curl -LO https://huggingface.co/datasets/stanford-cs336/owt-sample/resolve/main/owt_valid.txt.gz && gunzip owt_valid.txt.gz
nanotoken bench BAAI/bge-small-en-v1.5 owt_valid.txt --separator "<|endoftext|>" --validate
Installation
pip install nanotoken
Prebuilt abi3 wheels cover Python 3.10+ on Linux (x86_64, aarch64), macOS (Apple Silicon, Intel) and Windows (x64). Other platforms build from the source distribution, which needs a Rust toolchain.
Usage
Native API
import nanotoken as nt
tok = nt.Tokenizer("BAAI/bge-small-en-v1.5") # Hub repo id, tokenizer.json, vocab.txt, a directory,
# a tokenizers.Tokenizer or a transformers tokenizer
tok.encode("Hello, world!") # array([ 101, 7592, 1010, 2088, 999, 102], dtype=uint32)
tok.tokenize("unaffable") # ['una', '##ffa', '##ble']
batch = tok.encode_batch(texts) # RaggedIds: one flat uint32 buffer + int64 row offsets
batch[0], len(batch), batch.lengths, batch.to_list()
inputs = tok.encode_batch_padded(texts, max_length=512) # {'input_ids', 'token_type_ids', 'attention_mask'}
# int64 (rows x width) numpy arrays, model-ready
tok.decode(batch[0], skip_special_tokens=True)
Batch methods release the GIL and fan out across a thread pool. Inside multiprocessing workers and forked children they automatically run on the calling thread, because a thread pool inherited across fork() deadlocks. Pass parallel=True/False to override. The output is identical either way.
Drop-in for transformers
from transformers import AutoTokenizer
import nanotoken as nt
tokenizer = nt.Tokenizer(AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")).as_hf()
# or: nt.Tokenizer("sentence-transformers/all-MiniLM-L6-v2").as_hf()
batch = tokenizer(texts, padding=True, truncation=True, max_length=256, return_tensors="pt").to(device)
as_hf() supports:
__call__,encode,encode_plus,batch_encode_plus,tokenizeandpaddecodeandbatch_decodeconvert_ids_to_tokens,convert_tokens_to_idsandconvert_tokens_to_stringget_vocab,get_added_vocabandlen()- the special-token attributes (
cls_token_id,pad_token,all_special_ids, ...) model_max_length,padding_sideandtruncation_side
It accepts every padding and truncation strategy, pad_to_multiple_of, return_special_tokens_mask, return_length, and return_tensors of None, "np" or "pt". The test suite compares it with transformers over a grid of 2,400 calls. Sentence pairs, offset mappings, overflowing tokens and pre-tokenized input raise NotImplementedError.
Files
tokens = tok.encode_files(nt.TextFileSource("corpus.txt", separator="<|endoftext|>"))
tokens = tok.encode_files(nt.JsonlFileSource(["a.jsonl", "b.jsonl"], field="text"))
tokens = tok.encode_files(["doc1.txt", "doc2.txt"]) # each file is one document
Files are memory-mapped and split into documents in Rust, with no per-document Python objects:
- A text file becomes
data.split(separator), with a trailing empty document dropped. - A JSONL file yields one document per non-blank line.
- Invalid UTF-8 is replaced with U+FFFD.
What is supported
nanotoken implements the WordPiece subset of tokenizer.json. Loading a tokenizer that uses anything else raises nanotoken.UnsupportedTokenizerError rather than silently tokenizing differently.
| component | supported |
|---|---|
| model | WordPiece, including untyped legacy files: unk_token, continuing_subword_prefix, max_input_chars_per_word |
| normalizer | BertNormalizer, NFC, NFD, NFKC, NFKD, Lowercase, StripAccents, Sequence, none |
| pre_tokenizer | BertPreTokenizer, Whitespace, WhitespaceSplit, Punctuation, Digits, Sequence, none |
| post_processor | BertProcessing, RobertaProcessing, TemplateProcessing (single-sequence template) |
| decoder | WordPiece (with cleanup), none |
| added tokens | special and non-special, with lstrip, rstrip, single_word and normalized |
Parity with tokenizers 0.23.2 is tested on these corpora:
- 38 adversarial strings covering accents, combining marks, CJK, emoji and ZWJ sequences, control and zero-width characters, exotic whitespace, Greek final sigma, Turkish dotted I, ligatures, overlong words and embedded special tokens,
- 400 seeded random-Unicode documents,
- their concatenation.
The tests run these corpora against:
google-bert/bert-base-{uncased,cased,multilingual-cased,chinese}distilbert/distilbert-base-uncasedsentence-transformers/all-MiniLM-L6-v2BAAI/bge-small-en-v1.5intfloat/e5-base-v2- 11 synthetic normalizer and pre-tokenizer combinations
- every added-token flag
How it works
- Added tokens first. Special tokens are matched on the raw text with a leftmost-longest Aho–Corasick automaton. This follows HF's
lstrip/rstrip/single_wordrules exactly. - ASCII whitespace is a hard boundary. Every supported pre-tokenizer removes whitespace, and every supported normalizer is char-local and keeps
' ' \t \n \ras whitespace. So the tokens of a whitespace-delimited chunk depend only on that chunk's bytes. (This is not true of\x0b/\x0c, which BERT'sclean_textdeletes, joining their neighbours; nanotoken treats them accordingly.) Three things follow from this:- Two-level cache. Chunks such as
"world,"are cached by their raw bytes, and normalized pre-tokens such as"world"in a second level. Keys of up to 15 bytes are packed into au128and stored in a linear-probing table with 32-byte slots. The scan finds the next 64 chunks and prefetches their slots before looking any of them up, so tail-of-Zipf DRAM misses overlap. Caches belong to pooled workers and persist across calls. - ASCII fast path. On a cache miss, an ASCII chunk is normalized through a 128-entry table derived from the Unicode normalizer itself, so the two cannot disagree. For BERT, splitting happens in the same pass. Anything non-ASCII goes through the full Unicode pipeline, which uses the same Unicode tables as HF.
- Safe parallel splits. Batches are grouped into work units of at least 1 MiB. A huge document is split at whitespace that no added-token match touches, and the tokens are reassembled identically.
- Two-level cache. Chunks such as
- WordPiece runs greedy longest-match-first over two byte tries (word-initial pieces and
##continuations), and replaces the whole word with[UNK]if any part fails to match, as HF does. - Rows are assembled in Rust. Special tokens, truncation, padding and masks are written in parallel straight into the numpy buffers.
Limitations
- Only single sequences are supported.
text_pair,return_offsets_mapping,return_overflowing_tokensandis_split_into_wordsraiseNotImplementedError. - If
max_lengthis smaller than the number of special tokens, HF keeps one whole word. nanotoken raisesValueErrorinstead. - There is no WordPiece training, and no reading of compressed (
.gz/.zst) files. - The caches hold up to 2²⁰ entries per level per thread, up to about 130 MiB per thread when full. Tune this with
nanotoken.set_cache_entries(n)or theNANOTOKEN_CACHE_ENTRIESenvironment variable. - CJK-heavy text has long whitespace-free chunks that bypass the chunk cache, so it is slower than Latin-script text.
Development
uv venv && uv pip install maturin numpy pytest ruff tokenizers transformers torch
maturin develop --release
pytest # add -m "not network" to skip Hub downloads
cargo test --no-default-features
NANOTOKEN_BENCH_FILE=owt_valid.txt cargo bench --no-default-features --bench encode
Acknowledgements
-
gigatoken by Marcel Rød. nanotoken's design comes directly from it:
- pooled workers with persistent pre-token caches and packed keys,
- prefetched cache probes,
- chunked parallel encoding that splits oversized documents at output-preserving boundaries,
- memory-mapped
TextFileSource/JsonlFileSourceinputs, - fork-aware parallelism defaults,
- the
as_hf()compatibility wrapper, - the
bench --validateCLI.
No code was copied; the ideas are gigatoken's. If you use nanotoken in research, please cite gigatoken:
@software{roed2026gigatoken, author = {Marcel R{\o}d}, title = {{G}igatoken: SIMD and Cache Hierarchies for 1000x Faster Byte-Pair Encoding Tokenization on Modern CPUs}, url = {https://github.com/marcelroed/gigatoken}, year = {2026}, }
-
HuggingFace tokenizers is the reference implementation that nanotoken matches.
License
MIT
Metadata
Release files for nanotoken 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nanotoken-0.1.0.tar.gz | 268.7 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| nanotoken-0.1.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| nanotoken-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| nanotoken-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| nanotoken-0.1.0-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| nanotoken-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 5.5 MB
Release files / nanotoken-0.1.0.tar.gz
| Download URL | nanotoken-0.1.0.tar.gz |
|---|---|
| Size | 268.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c92a2508d7fe3d90fd5d3275b99027f91f20c85b74794ca5b2095a602be934b6
|
|
BLAKE2b-256 checksum How to use checksums |
77c25915de5dd6d96c968cce5caae54f3d48c9b1f17b71c0e2dd5b100699778f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / nanotoken-0.1.0-cp310-abi3-win_amd64.whl
| Download URL | nanotoken-0.1.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 1.0 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
97f0caac059b4a4a2be1622a283438dc562336fb26c29b6183cce818792fd910
|
|
BLAKE2b-256 checksum How to use checksums |
045f35f774830156ae0089ffbe894ebc6db7fdef242c05301762e8bec0db2c05
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / nanotoken-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | nanotoken-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 1.1 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
57e6a2eaccbab1b472635a263a97d8a7fbf0e6c7ab9a753f58a237ac1cec6ca0
|
|
BLAKE2b-256 checksum How to use checksums |
bffec06ccc6235d8c64759c96352b2cb8dc919942e41a67fc4c412c6f1378ca3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / nanotoken-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | nanotoken-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 1.1 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
d6377516d637bbe123754c1533b2eeccd1b09c6204a8f6a57246ba87ed8b40ff
|
|
BLAKE2b-256 checksum How to use checksums |
eb1b1dbf94a5cd2648e8856f0e909da34c3ffcfb1e506b262ac12ebefb7b4863
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / nanotoken-0.1.0-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | nanotoken-0.1.0-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 980.8 kB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
8cf10f579254b45c25a56be4a1eaed02d86b87e33255394ddf1529ea42983916
|
|
BLAKE2b-256 checksum How to use checksums |
0d01c9664f2608a7d6322acb6f31d2b5c210393df06c3b2b1b0f1a95f5eaf602
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / nanotoken-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | nanotoken-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.0 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
543d9f10f5574461e5ffb32b4421f0a69f7257ed1da377223c9b4fae73c98869
|
|
BLAKE2b-256 checksum How to use checksums |
b9fd4b525284b31657a0fde9140c1f741cd2e5ae7905d430080b09d640777c47
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency log