Tokenizer Comparison Cheat Sheet¶
This cheat sheet compares the capabilities, API designs, and performance features of three major tokenizer libraries: SentencePiece (new pybind11 API), Hugging Face Tokenizers, and OpenAI's tiktoken.
Note: Please feel free to send a PR if you find any problems or inaccuracies.
1. Capability Matrix¶
| Feature / Capability | SentencePiece | Hugging Face tokenizers |
tiktoken |
|---|---|---|---|
| Compared Version | >=v0.2.2 |
v0.23.1 |
v0.13.0 |
| Native Backend | C++ / pybind11 | Rust / PyO3 | Rust / PyO3 |
| Supported Algorithms | BPE, Unigram, Char, Word | BPE, WordPiece, Unigram, WordLevel | BPE |
| OOV / Unknown Handling | <unk> token, byte_fallback |
<unk> token, byte_fallback (BPE/Unigram), or Byte-level BPE (no <unk>) |
Byte-level BPE (no <unk> token) |
| Training Support | ✅ Yes (via SentencePieceTrainer) |
✅ Yes (via Trainer classes) |
❌ No |
| GIL Release (Parallelism) | ✅ Yes (Releases GIL in almost all cases including single/batch encode & decode; supports Free-Threading) | ⚠️ Partial (Releases GIL during core Rust execution, but string copying is done with GIL held) | ⚠️ Partial (Releases GIL during core Rust execution, but python-level threading and conversions hold GIL) |
| Encode Offset Mapping (Unicode) | ✅ Yes (via return_type='offset_mapping') |
✅ Yes (via Encoding.offsets) |
❌ No |
| Decode Offset Mapping (Unicode) | ✅ Yes (via return_type='offset_mapping', includes text key) |
❌ No | ⚠️ Partial (via Python decode_with_offsets; start offsets only) |
| Raw Byte Offset Mapping | ✅ Yes (triggered by bytes input or return_bytes=True for both encode and decode) |
❌ No | ❌ No |
| Direct UTF-8 Bytes I/O | ✅ Yes (accepts bytes for encode, returns bytes for decode) | ❌ No (requires str) | ⚠️ Partial (requires str for encode, returns bytes via decode_bytes) |
| Zero-Copy Input (str/bytes) | ✅ Yes (zero-copy for both str and bytes via Pybind11 memory view) | ❌ No (always copies input str to Rust owned String via PyO3) | ⚠️ Partial (zero-copy for str under some conditions, no bytes support) |
| NumPy Array Output (Encode) | ✅ Yes (Direct to NumPy, zero-copy) | ❌ No (Transformers wrapper only) | ✅ Yes (via encode_to_numpy, zero-copy) |
| NumPy Array Input (Decode) | ✅ Yes (Accepts 1D/2D; zero-copy on best-effort basis) | ✅ Yes (Accepts 1D/2D; copied) | ✅ Yes (Accepts 1D/2D; copied) |
| Batch Encoding Parallelism | ✅ Yes (via native C++ WorkerPool) |
✅ Yes (via native Rust Rayon multi-threading) | ⚠️ Partial (via Python ThreadPoolExecutor mapping to Rust) |
| Within-Doc Parallelism | ✅ Yes (parallel_encode) |
❌ No (sequential per document) | ❌ No (sequential per document) |
| Training from Iterator | ✅ Yes (sentence_iterator) |
✅ Yes (train_from_iterator) |
❌ No |
| Subword Regularization (Sampling / N-best) | ✅ Yes (at call-time; supports Unigram sampling and BPE-dropout) | ⚠️ Partial (BPE-dropout only, configured at model training/load time, no call-time sampling) | ❌ No |
| In-Memory Add Tokens | ⚠️ Partial (Intentional design: dynamic adding is not supported; requires model recreation via modified protobuf) | ✅ Yes (Easy, via add_tokens) |
⚠️ Partial (Intentional design: dynamic adding not supported; requires model recreation via modified dicts) |
| Pre-Tokenization (Modular) | ❌ No (Intentional design: parses raw Unicode stream without pre-splitting) | ✅ Yes (fully modular: tokenizer.pre_tokenizer = ...) |
⚠️ Partial (static via regex pat_str in constructor) |
| Modular Post-Processing | ⚠️ Partial (BOS/EOS toggles and extra options only) | ✅ Yes (template post-processor) | ❌ No |
| Text Normalization Support | ✅ Yes (baked into model, highly optimized precompiled rules) | ✅ Yes (fully modular pipeline, e.g. Lowercase, NFKC) | ❌ No (Intentional design: tokenizes raw input exactly as-is) |
| Custom Normalizers | ✅ Yes (defined at training time via TSV mapping, or at runtime via Python mapping passed to trainer) | ✅ Yes (custom Python/Rust functions or sequences) | ❌ No |
| Special Token Security Policy | ✅ Yes (static per-token: control_symbols [safe] vs user_defined_symbols [unsafe] in same model) |
⚠️ Partial (global toggle split_special_tokens, cannot mix per-token) |
⚠️ Partial (global call-time toggle allowed_special/disallowed_special, cannot mix per-token) |
| Token/Char Alignment Helpers | ❌ No (returns raw offsets only) | ✅ Yes (char_to_token(), token_to_word(), etc.) |
❌ No |
2. Usage & Code Comparison¶
Below is a side-by-side comparison of common tasks across the three libraries.
2.1. Instantiate¶
- SentencePiece:
- Hugging Face
tokenizers: - tiktoken:
2.2. Encode (IDs)¶
- SentencePiece:
- Hugging Face
tokenizers: - tiktoken:
2.3. Encode (Pieces)¶
- SentencePiece:
- Hugging Face
tokenizers: - tiktoken:
2.4. Decode (Str)¶
- SentencePiece:
- Hugging Face
tokenizers: - tiktoken:
2.5. Decode (Bytes)¶
- SentencePiece:
- Hugging Face
tokenizers:- Not Supported (must manually decode string to bytes)
- tiktoken:
2.6. Offset Mapping (Unicode)¶
- SentencePiece:
# Encode with Unicode character offsets: res = sp.encode("text", return_type='offset_mapping') # Or force Unicode offsets on bytes input explicitly: res = sp.encode(b"text", return_type='offset_mapping', return_bytes=False) # res['offsets'] -> list[tuple[int, int]] # Decode with Unicode character offsets: res = sp.decode(ids, return_type='offset_mapping') # res['text'] -> str, res['offsets'] -> list[tuple[int, int]] - Hugging Face
tokenizers: - tiktoken:
- Encode: Not Supported
2.7. Offset Mapping (Bytes)¶
- SentencePiece:
# Encode with raw byte offsets (triggered by bytes input): res = sp.encode(b"text", return_type='offset_mapping') # Or force raw byte offsets on str input explicitly: res = sp.encode("text", return_type='offset_mapping', return_bytes=True) # res['offsets'] -> list[tuple[int, int]] (byte offsets) # Decode with raw byte offsets (explicit return_bytes=True): res = sp.decode(ids, return_type='offset_mapping', return_bytes=True) # res['text'] -> bytes, res['offsets'] -> list[tuple[int, int]] (byte offsets) - Hugging Face
tokenizers:- Not Supported
- tiktoken:
- Not Supported
2.8. NumPy Support¶
- SentencePiece:
- Hugging Face
tokenizers: - tiktoken:
2.9. Encode Batch¶
- SentencePiece:
- Hugging Face
tokenizers: - tiktoken:
2.10. Parallel (Within-Doc)¶
- SentencePiece:
- Hugging Face
tokenizers:- Not Supported (encodes single document sequentially on one thread)
- tiktoken:
- Not Supported (encodes single document sequentially on one thread)
2.11. Thread Pool¶
- SentencePiece:
- Hugging Face
tokenizers:- Managed internally in Rust via global Rayon thread pool.
- tiktoken:
- Managed internally in Rust thread pool.
2.12. Train from Iterator¶
- SentencePiece:
- Hugging Face
tokenizers: - tiktoken:
- Not Supported (training is not exposed in public Python API)
2.13. Model Modification (In-Memory)¶
- SentencePiece:
# Intentional design: The processor is immutable. # To modify, rewrite the model protobuf and recreate the processor: import sentencepiece_model_pb2 as pb proto = pb.ModelProto() proto.ParseFromString(open("m.model", "rb").read()) # ... modify proto (e.g. proto.pieces.add()) ... sp = spm.SentencePieceProcessor.from_proto(proto.SerializeToString()) - Hugging Face
tokenizers: - tiktoken:
# Recreate encoding with modified ranks/special tokens dicts: enc = tiktoken.get_encoding("cl100k_base") ranks = dict(enc._mergeable_ranks) special = dict(enc._special_tokens) # Modify dicts ranks[b"new_token"] = len(ranks) new_enc = tiktoken.Encoding( name="modified_cl100k", pat_str=enc._pat_str, mergeable_ranks=ranks, special_tokens=special )
2.14. Configure Pre-tokenizer¶
- SentencePiece:
- Not Supported dynamically (pre-tokenization is baked into model normalization at training time)
- Hugging Face
tokenizers: - tiktoken:
- Partial (static regex pattern passed during custom class initialization)
2.15. Special Token Security¶
- SentencePiece:
- Hugging Face
tokenizers: - tiktoken:
2.16. Alignment Helpers¶
- SentencePiece:
- Not Supported (returns raw offsets, but no high-level helper APIs to map char to token)
- Hugging Face
tokenizers: - tiktoken:
- Not Supported
2.17. Configure Normalizer¶
- SentencePiece:
- Training-time only (normalization rule defined via
normalization_rule_nameornormalization_rule_tsvduring training). You can also pass a pre-configuredSentencePieceNormalizerinstance:
- Training-time only (normalization rule defined via
- Hugging Face
tokenizers: - tiktoken:
- Not Supported (tokenizes raw input exactly as-is)
2.18. Independent Normalization¶
- SentencePiece:
# Normalize text independently of tokenization: norm = spm.SentencePieceNormalizer(model_file='m.model') print(norm.normalize("text")) # Or via processor: print(sp.normalize("text")) # Or initialize with custom mapping directly: norm_custom = spm.SentencePieceNormalizer(norm_map=[('foo', 'bar')]) print(norm_custom.normalize("foo")) # Output: bar - Hugging Face
tokenizers: - tiktoken:
- Not Supported
2.19. Normalization Offsets¶
- SentencePiece:
- Hugging Face
tokenizers:- Not Supported directly on normalizer (offsets are computed during full encoding only)
- tiktoken:
- Not Supported
2.20. Subword Regularization / Sampling (N-best)¶
- SentencePiece:
# Encode with sampling (Unigram sampling or BPE-dropout depending on model) # nbest_size: -1 (unlimited, default), alpha: 0.1 (smoothing parameter) ids = sp.encode("Hello world", enable_sampling=True, alpha=0.1, nbest_size=-1) # N-best encoding (returns list of list of segmentations) nbest_ids = sp.nbest_encode("Hello world", nbest_size=5, return_type=int) # nbest_ids -> list[list[int]] (5 segmentations) - Hugging Face
tokenizers: - tiktoken:
- Not Supported
2.21. Decode NumPy Input¶
- SentencePiece:
- Hugging Face
tokenizers: - tiktoken: