sentencepiece-rust
Library New
From-scratch pure-Rust SentencePiece inference (encode/decode), differentially verified against the upstream C++/Python implementation.
- Downloads
- 8,442
- Dependents
- 3
- Stars
- 1
- Forks
- 0
- Contributors
- 1
- Commits
- 16
- Latest release
- 0.1.1 ·
About
A from-scratch, pure-Rust port of SentencePiece inference (encode/decode), differentially verified against the upstream C++/Python implementation.
This is not a binding. The existing sentencepiece crate wraps Google's C++
library; this crate reimplements the tokenisation algorithms in Rust from the
reference source, the same way the author's other library ports/reimplement their Python originals and verify
against them.
It is also independent of the similarly named sentencepiece-rs crate. This code
was written from scratch against the upstream C++/Python reference. It is not a
copy, fork, or derivative of that crate or of any other Rust port; the shared
subject matter is all they have in common.
Training is out of scope. A trained .model is loaded and used to tokenise.
Correctness goal
The point of this crate is to match upstream SentencePiece exactly:
Read the full README
- Every module is a line-by-line port of the reference file it names in its header, so its behaviour can be checked against the original.
- Output is verified against the Python
sentencepieceoracle over thousands of real-text lines, both encode and decode. See The Rust tokenizer test and Verification breadth. A difference from the oracle is treated as a bug until it turns out to be an equally-optimal tie (the one documented precision fence).
Two things fall out of that:
- Zero dependencies, so there is nothing to audit but this crate and
std. - Speed is measured, not assumed (see Speed), and never traded against matching the oracle.
Why
The scary parts of the upstream repo don't apply to inference:
- The 138k-line
normalization_rule.his compile-time charsmap generation; a trained model embeds its ownprecompiled_charsmap, which we just replay. - The suffix-array trainer,
builder, andesaxxare training-only. - The
.protois tiny, so a hand-written proto2 reader replacesprost/protocand keeps the crate dependency-free.
Milestones
| Version | Scope | Verified against |
|---|---|---|
| v0.1 | BPE segmentation; ASCII / whitespace normalisation; byte-fallback encode | Python sentencepiece, ASCII corpora |
| v0.2 | Darts charsmap normaliser to full Unicode input | + Unicode corpora (full-width, ligatures, CJK, …) |
| v0.3 (current) | Unigram Viterbi segmentation; byte-fallback and byte-piece decode | 3 models (BPE, Unigram, BPE+byte_fallback), encode and decode |
Inference is feature-complete for both BPE and Unigram models, with faithful decode (byte-piece reassembly, unk surface, dummy-prefix handling). Both directions are verified against the Python oracle.
Verification breadth
Beyond the small committed corpora, the encoders were run over the upstream
training corpora (botchan.txt, 4.3k English lines; wagahaiwa_nekodearu.txt,
2.3k Japanese lines) through all three models: 13,264 lines, encode and decode.
The result was 13,258 exact, 6 equal-score reorderings, and 0 real mismatches.
The 6 differences are all runs of repeated punctuation (......., long ----)
on the Unigram model. Each is a same-multiset reordering of pieces, that is, an
equally-optimal Viterbi path with the same total score. This is a documented
precision fence: the compiled oracle binary's optimised float arithmetic
resolves these exact ties differently than portable f32, and both
segmentations are optimal. See the note in src/unigram.rs. The
test counts these separately and only fails on a genuine (different-piece)
mismatch.
To reproduce the broad run, point gen_oracle.py at a larger corpus instead of
the small corpus_*.txt files. For example, use botchan.txt and
wagahaiwa_nekodearu.txt from the upstream SentencePiece repo's
data/ directory.
Those corpora are not vendored here; download them from the original source.
Layout
src/
proto.rs hand-written proto2 wire reader (no deps)
model.rs ModelProto -> typed struct (pieces, flags, normalizer spec)
vocab.rs piece <-> id maps, scores, byte-fallback ids
normalizer.rs whitespace/▁ normalisation + Darts charsmap replay
bpe.rs BPE merge encoder
unigram.rs Unigram Viterbi best-path (+ piece prefix-trie)
trie.rs Darts double-array (common_prefix_search / traverse)
processor.rs SentencePieceProcessor: load + encode/decode orchestration
examples/inspect.rs load a model and dump a summary
tests/oracle.rs differential test vs the Python oracle
oracle/gen_oracle.py generates oracle/cases.tsv from Python sentencepiece
Each module names the reference file it ports in its header comment, from the upstream google/sentencepiece source. That source is not vendored here. During development it sits as a local clone outside the repo; clone it from the original if you want to follow the ports.
Try it
# in-tree toolchain (see global setup); plain `cargo` works too
cargo run --example inspect -- tests/models/botchan_1000_bpe.model
Verify against the oracle
# reference implementation, used only to produce the oracle
python -m pip install sentencepiece
# For each (model, output) pair, run over the ASCII corpus (>) then the
# Unicode corpus (>>):
# botchan_1000_bpe.model -> cases.tsv (BPE)
# test_oss_model_unigram.model -> cases_unigram.tsv (Unigram)
# wagahaiwa_2000_bpe_byte.model -> cases_byte.tsv (BPE + byte_fallback)
python oracle/gen_oracle.py tests/models/botchan_1000_bpe.model \
oracle/corpus_ascii.txt > oracle/cases.tsv
python oracle/gen_oracle.py tests/models/botchan_1000_bpe.model \
oracle/corpus_unicode.txt >> oracle/cases.tsv
# ... likewise for the Unigram and byte-fallback fixtures.
cargo test
The Rust tokenizer test
tests/oracle.rs is a plain cargo test integration test. It
is pure Rust, with no Python at run time. For each fixture model it:
- loads the
.modelwithSentencePieceProcessor(the same public API a user calls); - reads the generated
oracle/cases*.tsv(hex(text)\t<ids>\thex(decode(ids))); - encodes each text and compares the ids to the oracle's, and decodes the oracle's ids and compares the text, so both directions are checked;
- classifies any encode difference: a same-multiset reordering counts as an equally-optimal Viterbi tie (accepted, counted separately), and anything else is a genuine mismatch that fails the test.
It runs across all three fixtures (BPE, Unigram, BPE+byte_fallback) and prints a breakdown, for example:
checked 117 cases against the Python oracle: 116 exact, 1 equal-score reorderings, 0 real mismatches
The test self-skips green on a fresh checkout until the oracle/cases*.tsv files
exist, so cargo test never fails just because the oracle hasn't been generated
yet. The oracle (Python sentencepiece) is only used to produce those files; the
test itself, and the crate, need nothing but Rust.
Speed
A rough throughput cross-check: examples/bench.rs against oracle/bench_oracle.py, same per-line workload, release build, single thread (numbers from one machine; run it yourself for yours):
| model / corpus | encode, Rust | encode, Py sp | decode, Rust | decode, Py sp |
|---|---|---|---|---|
| BPE / English | 6.7 MB/s | 2.9 MB/s | 28 MB/s | 5.9 MB/s |
| Unigram / English | 10.0 MB/s | 4.8 MB/s | 26 MB/s | 5.5 MB/s |
| BPE + byte / Japanese | 24.5 MB/s | 6.2 MB/s | 50 MB/s | 6.9 MB/s |
Read these as a sign that the port runs at a healthy native throughput, not as
proof that Rust beats C++. The Python column includes per-call CPython/FFI
overhead (the C++ core in batch mode would be faster), and this crate is
straightforward single-threaded inference tuned for matching the oracle rather
than for speed (for example, the normaliser re-runs common_prefix_search per
position). Even so, native Rust encode runs about 2 to 4 times the per-line
Python path, and decode about 5 to 7 times, with no native dependency to link.
cargo run --release --example bench -- tests/models/botchan_1000_bpe.model <corpus> 50
python oracle/bench_oracle.py tests/models/botchan_1000_bpe.model <corpus> 50
License
Apache-2.0, matching upstream SentencePiece. The bundled tests/models/*.model
fixtures originate from the SentencePiece repository.
Compatibility
- Runs on
- GPU support unknown
What shows it
- no dependency or feature matches a backend row
- Native code
- None found
- No native code found
- Python bindings
- No Python binding crate found
- License
- Apache-2.0
Compare with similar projects
Order: strongest tier first, then most downloads, then name, with unmeasured values last. A library counts downloads over 30 days. An app counts binary downloads per release. An app with none counts its crate downloads over 30 days. ML Rust runs this catalogue, so its closest project always gets a row.
| Project | Downloads | Dependents | Stars | Latest release | Runs on (GPU) | Native code | License |
|---|---|---|---|---|---|---|---|
|
From-scratch pure-Rust SentencePiece inference (encode/decode), differentially verified against the upstream C++/Python implementation. |
8,442 downloads over 30 days | 3 | 1 | 0.1.1 | GPU support unknown | No native code found | Apache-2.0 |
|
text-splitter Featured
Split text into semantic chunks, up to a desired chunk size. Supports calculating length by characters and tokens, and is callable from Rust and Python. |
271,174 downloads over 30 days | 56 | 645 | 0.33.1 | GPU support unknown | Binds native code: tree-sitter (feature code), tree-sitter-language (feature code) | MIT |
|
rust-tiktoken Listed
A high-performance pure-Rust implementation of OpenAI's tiktoken BPE tokenizer |
25,714 downloads over 30 days | 12 | 8 | 4.1.2 | GPU support unknown | No native code found | Apache-2.0 OR MIT |
|
splintr Listed
Tokenizer for Rust and Python: pure Rust by default, with 18 bundled vocabularies and four backends covering BPE, SentencePiece and WordPiece. Its output matches Hugging Face tokenizers id for id on 14 model families, including GPT-2, Qwen, Whisper, T5 and BERT. Python bindings |
8,204 downloads over 30 days | 7 | 63 | 0.22.2 | GPU support unknown | Binds native code: pcre2-sys (feature pcre2) | MIT |
|
shimmytok Listed
Pure Rust tokenizer for GGUF models with llama.cpp compatibility (SentencePiece + BPE + WPM + UGM + RWKV) |
914 downloads over 30 days | 2 | 26 | 0.8.5 | GPU support unknown | No native code found | Apache-2.0 OR MIT |
|
🦀 A Rust implementation of a RoBERTa classification model for the SNLI dataset |
Not measured | Not applicable | 13 | Not measured | Not measured | Not measured | None stated |
Why NewIts first release on 24 Jun 2026 is under 180 days old.
The check tests the Featured gates once the first release passes this age limit.
What else the check found
- The last activity is on 25 Jun 2026, 137 days ago. The rules expect an update within 365 days. Dormancy rule
Exact rules: New. The check on applied thresholds version 9.
Every rule outcome, as the check records it
expected-interval: version 0.1.1 base 300, capped at 2 releases = 100, raised to the 365-day floor: E = 365 daysdormancy: last activity 2026-06-25, 137 days ago, E = 365: activenew: new: first release 2026-06-24 is under 180 days old
Measurements and sources
Each signal row shows the raw value, its source and the fetch date. The Sources list gathers every distinct source on this page. Movement needs more history.
Sources6 sources
- Description: the crates.io description
- crates.io database dump, fetched
- RustSec advisory database, fetched
- GitHub repository, fetched
- crates.io package, fetched
- GitHub contributors, fetched
Adoption2 active dependents, 2 new dependents in 365 days1 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Active dependents | 2 | crates.io database dump | Yes | |
| New dependents (365 days) | 2 | crates.io database dump | Yes | |
| Peak active dependents | 2 | crates.io database dump | Yes | |
| Active dependents as a share of the peak | 1 | crates.io database dump | Yes | |
| Binary downloads per release | Not measured | Yes |
MaintenanceLatest human commit: 4 months ago3 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Latest human commit | 2026-06-25 | GitHub repository | Yes | |
| Latest release | 2026-06-25 | crates.io database dump | Yes | |
| First release | 2026-06-24 | crates.io database dump | Yes | |
| Latest version | 0.1.1 | crates.io database dump | Yes | |
| Days the oldest outside issue waits for a reply | 0 | GitHub repository | Yes | |
| Maintainer replies over 365 days | 0 | GitHub repository | Yes | |
| Repository archived | false | GitHub repository | Yes | |
| Repository gone | false | GitHub repository | Yes | |
| Release commit or manifest in the repo | true | crates.io package | Yes | |
| Published docs | true | crates.io database dump | Yes | |
| Latest release fails cargo check | Not measured | Yes | ||
| Build check date | Not measured | Yes | ||
| Build check toolchain | Not measured | Yes |
SecurityUnpatched vulnerabilities: none
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Unpatched vulnerabilities past the grace window | none | RustSec advisory database | Yes | |
| RustSec unmaintained advisory | none | RustSec advisory database | Yes | |
| Deprecation notice | none | crates.io database dump | Yes | |
| Deprecated requirement | none | crates.io database dump | Yes |
Distribution2 releases, 8,442 downloads over 30 days5 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Releases | 2 | crates.io database dump | Yes | |
| Downloads over 30 days | 8442 | crates.io database dump | Yes | |
| Binary downloads over the last 3 releases | Not measured | Yes | ||
| Homebrew installs on request over 365 days | Not measured | Yes | ||
| Repology repository families | Not measured | Yes | ||
| Docker pull growth over 90 days | Not measured | Yes | ||
| Docker pulls at the growth baseline | Not measured | Yes | ||
| Every version a prerelease | false | crates.io database dump | Yes | |
| Every version 0.0.x | false | crates.io database dump | Yes | |
| Every version yanked | false | crates.io database dump | Yes | |
| Crate deleted from crates.io | false | crates.io database dump | Yes |
Repository1 stars, 0 forks, 1 contributors, 16 commits. Stars alone never qualify a project.
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| GitHub stars | 1 | GitHub repository | Yes — one part of the repository route | |
| GitHub forks | 0 | GitHub repository | Yes — one part of the repository route | |
| Contributors with a GitHub account | 1 | GitHub contributors | Yes — one part of the repository route | |
| Commits on the default branch | 16 | GitHub repository | No | |
| Repository's main crate left out of the entry | none | crates.io database dump | Yes |
Facts that never count
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| License | Apache-2.0 | crates.io database dump | No | |
| Runs on, from listed crates and their direct dependencies | GPU support unknown | crates.io database dump | No | |
| What shows each backend | no dependency or feature matches a backend row | crates.io database dump | No | |
| Native code, from listed crates and their dependencies | No native code found | crates.io database dump | No | |
| Python bindings, from direct dependencies of listed crates | No Python binding crate found | crates.io database dump | No | |
| Crates that depend on a listed crate directly | 3 | crates.io database dump | No |
Corrections and review history
No correction applies to this entry.
Judgments
A judgment enters only through an approved issue and a merged pull request.
No judgment applies to this entry.