static_embeddings 0.1.4 → 1.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +118 -0
  3. data/README.md +67 -25
  4. data/Rakefile +1 -1
  5. data/docs/ARCHITECTURE.md +54 -34
  6. data/docs/LIMITATIONS.md +12 -6
  7. data/docs/MODEL_AUDIT.md +195 -52
  8. data/ext/static_embeddings/se_embed.c +2 -1
  9. data/ext/static_embeddings/se_format.c +41 -4
  10. data/ext/static_embeddings/se_internal.h +17 -5
  11. data/ext/static_embeddings/se_tokenizer.c +155 -18
  12. data/ext/static_embeddings/se_unicode.c +1 -1
  13. data/ext/static_embeddings/static_embeddings.c +32 -2
  14. data/lib/models/demo.semb +0 -0
  15. data/lib/static_embeddings/bert_wordpiece.rb +191 -0
  16. data/lib/static_embeddings/canonical.rb +50 -0
  17. data/lib/static_embeddings/cli.rb +88 -62
  18. data/lib/static_embeddings/codec.rb +45 -0
  19. data/lib/static_embeddings/conversion.rb +58 -0
  20. data/lib/static_embeddings/errors.rb +2 -2
  21. data/lib/static_embeddings/format/constants.rb +109 -0
  22. data/lib/static_embeddings/format/hash_table.rb +69 -0
  23. data/lib/static_embeddings/format/trie.rb +78 -0
  24. data/lib/static_embeddings/format/verifier.rb +41 -0
  25. data/lib/static_embeddings/format/writer.rb +131 -0
  26. data/lib/static_embeddings/format.rb +3 -300
  27. data/lib/static_embeddings/importers/model2vec.rb +52 -0
  28. data/lib/static_embeddings/importers/sentence_transformers_static.rb +103 -0
  29. data/lib/static_embeddings/importers/support.rb +111 -0
  30. data/lib/static_embeddings/importers.rb +50 -0
  31. data/lib/static_embeddings/model.rb +35 -20
  32. data/lib/static_embeddings/paths.rb +17 -4
  33. data/lib/static_embeddings/provenance.rb +58 -0
  34. data/lib/static_embeddings/reference.rb +90 -33
  35. data/lib/static_embeddings/row_prefix_payload.rb +59 -0
  36. data/lib/static_embeddings/safetensors.rb +178 -34
  37. data/lib/static_embeddings/version.rb +1 -1
  38. data/lib/static_embeddings.rb +29 -57
  39. data/static_embeddings.gemspec +2 -2
  40. data/tools/check_model2vec_parity.rb +89 -54
  41. data/tools/check_st_parity.rb +125 -0
  42. data/tools/eval_retrieval.rb +58 -0
  43. metadata +24 -6
  44. data/lib/static_embeddings/converter.rb +0 -284
data/docs/MODEL_AUDIT.md CHANGED
@@ -1,27 +1,26 @@
1
1
  # Model Audit
2
2
 
3
- A converted model is trusted only after parity against upstream
4
- `model2vec.StaticModel` is recorded here. An audit record covers one `.semb`
5
- file **and** one runtime version: changing the tokenizer, normalizer, prefix
6
- window, pooling, output normalization or the oracle's corpus invalidates the
7
- runtime half of it even though the file bytes are untouched.
3
+ A converted model is trusted only after a recorded comparison against an
4
+ external upstream oracle. An audit record covers one source-model snapshot,
5
+ one `.semb` format/runtime contract, pinned upstream package versions, and the
6
+ specific corpus that was checked. A passing corpus is evidence for those rows;
7
+ it is **not** an exhaustive proof that every Unicode string is equivalent.
8
+
9
+ `StaticEmbeddings::Reference` remains useful for high-volume differential fuzz
10
+ of the C implementation, but it is an implementation twin and is not counted as
11
+ independent upstream evidence.
8
12
 
9
13
  ## potion-retrieval-32m
10
14
 
11
- | runtime | parity | note |
15
+ | runtime | external result | note |
12
16
  |---|---|---|
13
- | 0.1.1 | `parity OK`, recorded below | superseded |
14
- | 0.1.2 | **not re-run** | superseded before release |
15
- | 0.1.3 | `parity OK` | tokenizer / pooling contract; same numbers as 0.1.4 |
16
- | 0.1.4 | `parity OK`, recorded below | current |
17
-
18
- 0.1.2 changed `is_control()`, which changes token ids for any input containing
19
- `U+007F`. `StaticEmbeddings::Reference` cannot settle whether the new behaviour
20
- matches HuggingFace, because it is an implementation twin of the C runtime
21
- written in this repository. The 0.1.3 release candidate was checked against a
22
- fresh upstream `model2vec.StaticModel` oracle that includes DEL, control
23
- characters, Unicode, OOV, long-word and truncation rows. 0.1.4 re-ran that
24
- oracle against the SIMD L2 runtime on the same snapshot and `.semb`.
17
+ | 0.1.1 | 31-row corpus passed | superseded |
18
+ | 0.1.2 | not re-run | superseded before release |
19
+ | 0.1.3 | 31-row corpus passed | superseded |
20
+ | 0.1.4 | 31-row corpus passed | historical result below; later review found uncovered boundary cases |
21
+ | 0.1.5 | **434-row corpus passed** | format v3; CI `potion_audit` record below |
22
+
23
+ ### Historical 0.1.4 record
25
24
 
26
25
  Source model:
27
26
 
@@ -29,7 +28,6 @@ Source model:
29
28
  - Hugging Face snapshot: `6fc8051fab2a1e0ee76689cf08c853792ac285e7`
30
29
  - Oracle implementation: `model2vec.StaticModel.from_pretrained`
31
30
  - Python package: `model2vec 0.9.0` (`tokenizers 0.23.1`, `numpy 2.5.2`)
32
- - Oracle file: `tmp/model2vec_oracle.json`
33
31
  - Oracle rows in this recorded run: `31`
34
32
  - Oracle dimension: `512`
35
33
  - Oracle max length: `512`
@@ -37,25 +35,12 @@ Source model:
37
35
 
38
36
  Converted `.semb`:
39
37
 
40
- - Path: `$HOME/.cache/static_embeddings/models/potion-retrieval-32m.semb`
41
38
  - Format version: `2`
42
39
  - Header size: `320`
43
40
  - Bytes: `135411608`
44
41
  - SHA256: `79e087863d2bab825779fd7de3574e5625542ef6a5ecad33fe681ea16d4b3ab0`
45
42
 
46
- Parity command:
47
-
48
- ```bash
49
- python tools/model2vec_oracle.py \
50
- ~/.cache/huggingface/hub/models--minishlab--potion-retrieval-32M/snapshots/6fc8051fab2a1e0ee76689cf08c853792ac285e7 \
51
- --out tmp/model2vec_oracle.json
52
-
53
- bundle exec rake parity \
54
- MODEL="$HOME/.cache/static_embeddings/models/potion-retrieval-32m.semb" \
55
- ORACLE=tmp/model2vec_oracle.json
56
- ```
57
-
58
- Parity result (0.1.4, 2026-08-31):
43
+ Recorded result (2026-08-31):
59
44
 
60
45
  ```text
61
46
  rows=31
@@ -67,22 +52,180 @@ vector_failures=[]
67
52
  parity OK
68
53
  ```
69
54
 
70
- The printed `min_cosine` and `max_abs_all` are identical to the 0.1.3 record.
71
- Worst vector row is still idx=29 (`"word" + 400 spaces`, 80 800 bytes) at
72
- `max_abs=2.9802322e-07`. SIMD pairwise double L2 did not move the oracle
73
- agreement on this corpus.
74
-
75
- Decision:
76
-
77
- - Tokenization parity: pass
78
- - Vector parity: pass
79
- - Empty input behavior: pass
80
- - Whitespace behavior: pass
81
- - Unicode normalization behavior: pass
82
- - Unknown-word behavior: pass
83
- - Long input / truncation behavior: pass
84
- - DEL / control-character behavior: pass
85
-
86
- Accepted for runtime and benchmark use **under 0.1.4**. The `.semb` file is
87
- unchanged and its SHA256 still matches. The runtime half of the record was
88
- refreshed after the 0.1.4 L2 kernel change.
55
+ The last line means **31/31 rows in that historical oracle passed**. It must not
56
+ be read as a global tokenizer-equivalence claim. The corpus did not cover, for
57
+ example, the corrected UNK-before-truncation ordering, standard AddedVocabulary
58
+ literals, or the Rust/Python CJK Extension E boundary.
59
+
60
+ ## 0.1.5 record
61
+
62
+ 0.1.5 separates three contracts that the old oracle mixed together:
63
+
64
+ 1. raw Hugging Face `tokenizers 0.23.1` ids, including `[UNK]`;
65
+ 2. this runtime's usable-id embedding contract (`[UNK]` drop, then token cap);
66
+ 3. `model2vec.StaticModel` vectors where its character pre-cut does not change
67
+ the usable token sequence.
68
+
69
+ Rows changed solely by Model2Vec's `max_length * median_token_length` character
70
+ pre-cut are reported as intentional deviations rather than hidden inside a pass.
71
+ A passing corpus is evidence for those 434 rows, not an exhaustive proof of
72
+ every Unicode string.
73
+
74
+ Recorded from GitHub Actions (`potion_audit`, Python 3.12.14, pinned
75
+ `model2vec==0.9.0` / `tokenizers==0.23.1` / `numpy==2.5.2`):
76
+
77
+ Source model:
78
+
79
+ - Hugging Face repository: `minishlab/potion-retrieval-32M`
80
+ - Hugging Face snapshot: `6fc8051fab2a1e0ee76689cf08c853792ac285e7`
81
+ - Oracle implementation: `model2vec.StaticModel.from_pretrained`
82
+ - Oracle rows: `434` from `tools/parity_cases.py`
83
+ - Oracle dimension: `512`
84
+ - Oracle max length: `512`
85
+ - Runtime at time of this record: `static_embeddings 0.1.5`
86
+
87
+ Converted `.semb` (CI artifact; Unicode tables stamped from the Ubuntu Ruby that
88
+ converted it, so the SHA is not expected to match a macOS local convert of the
89
+ same snapshot):
90
+
91
+ - Format version: `3`
92
+ - Bytes: `135411800`
93
+ - SHA256: `747231b5afbcb3b16bf2b04538f81d3a96be88a798982214b7cf01eddbdcf4eb`
94
+ - `dim=512` `vocab=63091`
95
+
96
+ Recorded result:
97
+
98
+ ```text
99
+ rows=434
100
+ vectors_checked=432
101
+ intentional_character_pretruncate_deviations=2
102
+ min_cosine=0.9999999999999989
103
+ max_abs_all=1.4901161193847656e-08
104
+ raw_token_id_failures=[]
105
+ usable_token_id_failures=[]
106
+ embed_invariant_failures=[]
107
+ vector_failures=[]
108
+ corpus parity OK (434/434); intentional Model2Vec character pre-truncation deviations are reported separately
109
+ ```
110
+
111
+ The two intentional deviations are `long:sparse-whitespace` and
112
+ `long:unknown-prefix`. Vectors were not required to match Model2Vec on those
113
+ rows. The remaining 432 rows were inside `cosine >= 1 - 1e-6` and
114
+ `max_abs <= 1e-5`.
115
+
116
+ The preceding CI job `upstream_parity` ran the same 434-row corpus against the
117
+ synthetic `tiny-wordpiece` fixture: `434/434`, all four failure lists empty,
118
+ `vectors_checked=429`, `intentional_character_pretruncate_deviations=5`,
119
+ `min_cosine=0.9999999999999988`, `max_abs_all=5.960464477539063e-08`. That job
120
+ proves the fixture loads in `StaticModel.from_pretrained` and that the checker
121
+ contracts hold; it is not a potion audit.
122
+
123
+ ## 1.5.6 Patch 1 proof
124
+
125
+ Architecture: Model2Vec / Sentence Transformers import functions → immutable canonical data →
126
+ existing WordPiece `.semb` v3 writer. C runtime unchanged.
127
+
128
+ ### potion-retrieval-32M reconvert
129
+
130
+ Official snapshot `6fc8051fab2a1e0ee76689cf08c853792ac285e7` ships a Sentence
131
+ Transformers `modules.json` (`StaticEmbedding` + `Normalize`). Detection uses
132
+ `config.json` `model_type=model2vec` first, so this stays UNK_DROP / L2 / 512.
133
+
134
+ New file `potion-retrieval-32m-reconvert.semb` vs existing
135
+ `potion-retrieval-32m-v3.semb`: vocab/hash/embeddings/norm_tables/tries
136
+ byte-identical. 10-text corpus `max_abs=0`. Provenance JSON differs (new keys).
137
+
138
+ ### static-retrieval-mrl-en-v1 vs SentenceTransformer.encode
139
+
140
+ Oracle: `tools/st_oracle.py` + `tools/check_st_parity.rb`,
141
+ `sentence-transformers 6.0.1`, `add_special_tokens=false`,
142
+ `normalize_embeddings=false`, 436 rows including `all-unk:private-use` and
143
+ `long:known-words-600`.
144
+
145
+ ```text
146
+ 1024: 436/436 min_cosine=0.999999999999999 max_abs_all=7.62939453125e-06
147
+ 512: 436/436 min_cosine=0.9999999999999989 max_abs_all=7.62939453125e-06
148
+ ```
149
+
150
+ 512 compared as prefix of the 1024-d encode vectors. Ruby Reference matched C
151
+ separately. Diagnostic `StaticEmbedding.forward` matched encode on this model.
152
+
153
+ ### UNK and max_tokens on the real models
154
+
155
+ - potion-retrieval-32M `🧬 🧬 🧬` → UNK ids dropped → zero vector; 600×`hello`
156
+ → `truncated=true pooled=512`.
157
+ - static-retrieval-mrl-en-v1-1024 same emoji string → three UNK ids pooled
158
+ (non-zero, mean of UNK rows); 600×`hello` → `truncated=false pooled=600`.
159
+ - Private-use codepoints are BertNormalizer `Co` and become empty, not UNK.
160
+
161
+ ## 1.5.6 Type A conversions
162
+
163
+ These records prove the offline import layer maps extra WordPiece sources onto
164
+ the existing runtime. They are **not** a replacement for the
165
+ potion-retrieval-32m Python oracle above. `static-retrieval-mrl-en-v1` has the
166
+ separate `SentenceTransformer.encode` oracle recorded above; the multilingual
167
+ similarity artifact was checked against the Ruby `.semb` Reference twin and the
168
+ small Russian retrieval fixture, not claimed as a general upstream retrieval
169
+ benchmark.
170
+
171
+ All conversions used `static_embeddings 1.5.6` on arm64-darwin24.
172
+
173
+ ### minishlab/potion-base-8M
174
+
175
+ - Snapshot: `bf8b056651a2c21b8d2565580b8569da283cab23`
176
+ - Layout: Model2Vec (`UNK_DROP`, L2, `max_tokens` 512)
177
+ - `.semb`: `potion-base-8m.semb` `dim=256` `vocab=29528` `33346200` bytes
178
+ SHA256 `8abd8d1f26511959e14ca26ec4dcacad1d3db9b4b66b2e67683fb2d06074f490`
179
+ - Native C matched Ruby Reference on short English/OOV/empty texts (`max_abs < 1e-5`)
180
+ - `model2vec 0.9.0` / `tokenizers 0.23.1` oracle: 434/434, vectors_checked=432,
181
+ intentional character-pretruncate deviations=2, min_cosine=0.9999999999999989,
182
+ max_abs_all=2.98e-08
183
+
184
+ ### minishlab/potion-science-32M
185
+
186
+ - Snapshot: `7366079845507de14a4330007cdfa01bb92bca52`
187
+ - Layout: Model2Vec (`UNK_DROP`, L2, `max_tokens` 512)
188
+ - `.semb`: `potion-science-32m.semb` `dim=256` `vocab=124428` `140353240` bytes
189
+ SHA256 `e968f93b89d59d9c63c6a8152495283d1138144e61ed5c08d306c21c00b7cdb1`
190
+ - Native C matched Ruby Reference on English and Russian snippets (`max_abs < 1e-5`)
191
+ - `model2vec 0.9.0` / `tokenizers 0.23.1` oracle: 434/434, vectors_checked=432,
192
+ intentional character-pretruncate deviations=2, min_cosine=0.9999999999999986,
193
+ max_abs_all=2.98e-08
194
+
195
+ ### sentence-transformers/static-retrieval-mrl-en-v1
196
+
197
+ - Snapshot: `f60985c706f192d45d218078e49e5a8b6f15283a`
198
+ - Layout: Sentence Transformers StaticEmbedding (`UNK_INCLUDE`, no L2, unlimited)
199
+ - `static-retrieval-mrl-en-v1-1024.semb` `dim=1024` `vocab=30522` `128186264` bytes
200
+ SHA256 `f316bb07348503418ed0f6d897ddbf7b44422d2077ca4860518f31f328610946`
201
+ - `static-retrieval-mrl-en-v1-512.semb` `--dimensions 512` `65677208` bytes
202
+ SHA256 `efbb43c14def793d0bbb42ef591a0951caed23a49cdd94dcf72312569d1b7cff`
203
+ - Prefix of a 1024-d vector matched the 512-d artifact on in-vocabulary English
204
+ (valid because this source does not L2-normalize)
205
+ - Native C matched Ruby Reference on short texts
206
+
207
+ ### sentence-transformers/static-similarity-mrl-multilingual-v1
208
+
209
+ - Snapshot: `b68f4122911bcffcd6e1f695f2d99cd6788972d8`
210
+ - Layout: Sentence Transformers StaticEmbedding (`UNK_INCLUDE`, no L2, unlimited)
211
+ - `static-similarity-mrl-multilingual-v1-512.semb` `--dimensions 512` `vocab=105879`
212
+ `227983824` bytes SHA256 `8d9a63d20ee23468ae2fd2584ef3f0ec75e720e68410e416157aaf2b873e110e`
213
+ - `static-similarity-mrl-multilingual-v1-256.semb` `--dimensions 256` `119563728` bytes
214
+ SHA256 `2e9e689bd3afd18ed50a77f0f8842b4b5a5be39bfad75e3fa24ad99b478de068`
215
+ - Native C matched Ruby Reference on Russian text (`max_abs 9.1e-7`)
216
+
217
+ This model is published as similarity, not retrieval. `tools/eval_retrieval.rb`
218
+ on `test/fixtures/russian_faq_eval.json` (10 queries / 10 docs, cosine@10,
219
+ arm64-darwin24, 1.5.6):
220
+
221
+ | model | dim | MRR | nDCG@10 |
222
+ |---|---:|---:|---:|
223
+ | static-similarity-mrl-multilingual-v1-256 | 256 | 0.875 | 0.906 |
224
+ | static-similarity-mrl-multilingual-v1-512 | 512 | 0.850 | 0.889 |
225
+ | static-retrieval-mrl-en-v1-512 | 512 | 0.792 | 0.842 |
226
+ | potion-retrieval-32m | 512 | 0.733 | 0.799 |
227
+ | potion-base-8m | 256 | 0.712 | 0.780 |
228
+
229
+ Hit@10 was 1.0 for every model on this tiny labeled set. That is a domain sanity
230
+ check, not a public retrieval benchmark, and it does not make the similarity
231
+ model a hard-coded Russian default.
@@ -197,7 +197,8 @@ se_status_t se_embed_ids(const se_model_t *model, se_scratch_t *sc, const uint32
197
197
  se_status_t se_embed_one(const se_model_t *model, se_scratch_t *sc, const uint8_t *input,
198
198
  size_t input_len, uint32_t max_tokens, float *out, se_token_stats_t *stats,
199
199
  se_error_t *err, volatile sig_atomic_t *cancelled) {
200
- se_status_t rc = se_tokenize(model, sc, input, input_len, max_tokens, stats, err, cancelled);
200
+ se_status_t rc = se_tokenize(model, sc, input, input_len, max_tokens, SE_TOKEN_LIMIT_USABLE,
201
+ stats, err, cancelled);
201
202
  if (rc != SE_OK)
202
203
  return rc;
203
204
  return embed_ids_core(model, sc, sc->ids, stats->token_count, out, err, cancelled);
@@ -102,7 +102,7 @@ int se_vocab_lookup_piece(const se_model_t *model, const uint8_t *prefix, size_t
102
102
  uint32_t pos = h & mask;
103
103
  uint32_t total_len = (uint32_t)(prefix_len + len);
104
104
 
105
- for (uint32_t probe = 0; probe <= mask; probe++) {
105
+ for (uint32_t probe = 0; probe < model->meta.max_probe; probe++) {
106
106
  const se_vocab_slot_t *slot = &model->vocab_hash[pos];
107
107
  if (slot->token_id == SE_SLOT_EMPTY)
108
108
  return 0;
@@ -556,6 +556,31 @@ static se_status_t validate_vocab_hash(const se_model_t *model, struct section v
556
556
  se_error_set(err, SE_ERR_INVALID_FORMAT,
557
557
  "[UNK] is not reachable through the vocabulary hash");
558
558
  rc = SE_ERR_INVALID_FORMAT;
559
+ goto done;
560
+ }
561
+
562
+ static const struct {
563
+ uint32_t bit;
564
+ const char *text;
565
+ uint32_t len;
566
+ } added[] = {
567
+ {SE_ADDED_PAD, "[PAD]", 5u}, {SE_ADDED_UNK, "[UNK]", 5u}, {SE_ADDED_CLS, "[CLS]", 5u},
568
+ {SE_ADDED_SEP, "[SEP]", 5u}, {SE_ADDED_MASK, "[MASK]", 6u},
569
+ };
570
+ const uint32_t ids[] = {m->pad_id, m->unk_id, m->cls_id, m->sep_id, m->mask_id};
571
+
572
+ for (size_t i = 0; i < SE_ARRAY_LEN(added); i++) {
573
+ if ((m->added_token_mask & added[i].bit) == 0)
574
+ continue;
575
+ uint32_t got = 0;
576
+ if (ids[i] >= m->vocab_size ||
577
+ !se_vocab_lookup(model, (const uint8_t *)added[i].text, added[i].len, &got) ||
578
+ got != ids[i]) {
579
+ se_error_set(err, SE_ERR_INVALID_FORMAT,
580
+ "added token %s is not reachable at its recorded id", added[i].text);
581
+ rc = SE_ERR_INVALID_FORMAT;
582
+ goto done;
583
+ }
559
584
  }
560
585
 
561
586
  done:
@@ -646,6 +671,7 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
646
671
  m->subword_prefix_len = read_u32(base, SE_OFF_SUBWORD_PREFIX_LEN);
647
672
  m->max_token_chars = read_u32(base, SE_OFF_MAX_TOKEN_CHARS);
648
673
  m->max_probe = read_u32(base, SE_OFF_MAX_PROBE);
674
+ m->added_token_mask = read_u32(base, SE_OFF_ADDED_TOKEN_MASK);
649
675
  memcpy(m->subword_prefix, base + SE_OFF_SUBWORD_PREFIX, 8);
650
676
 
651
677
  if (m->tokenizer_type != SE_TOKENIZER_BERT_WORDPIECE_V1) {
@@ -669,7 +695,7 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
669
695
  m->normalization_type);
670
696
  return SE_ERR_INVALID_FORMAT;
671
697
  }
672
- if (m->truncation_policy != SE_TRUNCATE_IDS_BEFORE_POOLING) {
698
+ if (m->truncation_policy != SE_TRUNCATE_USABLE_IDS_BEFORE_POOLING) {
673
699
  se_error_set(err, SE_ERR_INVALID_FORMAT, "truncation policy %u is not supported",
674
700
  m->truncation_policy);
675
701
  return SE_ERR_INVALID_FORMAT;
@@ -682,6 +708,11 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
682
708
  se_error_set(err, SE_ERR_INVALID_FORMAT, "unknown UNK policy %u", m->unk_policy);
683
709
  return SE_ERR_INVALID_FORMAT;
684
710
  }
711
+ if ((m->added_token_mask & ~SE_ADDED_TOKEN_MASK_ALL) != 0) {
712
+ se_error_set(err, SE_ERR_INVALID_FORMAT, "unknown added-token mask bits 0x%x",
713
+ m->added_token_mask & ~SE_ADDED_TOKEN_MASK_ALL);
714
+ return SE_ERR_INVALID_FORMAT;
715
+ }
685
716
  if (m->empty_policy != SE_EMPTY_ZERO_VECTOR && m->empty_policy != SE_EMPTY_RAISE) {
686
717
  se_error_set(err, SE_ERR_INVALID_FORMAT, "unknown empty-input policy %u", m->empty_policy);
687
718
  return SE_ERR_INVALID_FORMAT;
@@ -704,8 +735,14 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
704
735
  m->hash_table_size);
705
736
  return SE_ERR_INVALID_FORMAT;
706
737
  }
707
- if (m->hash_table_size < m->vocab_size) {
708
- se_error_set(err, SE_ERR_INVALID_FORMAT, "hash table smaller than vocabulary");
738
+ if (m->hash_table_size <= m->vocab_size ||
739
+ (uint64_t)m->vocab_size * 100u > (uint64_t)m->hash_table_size * 70u) {
740
+ se_error_set(err, SE_ERR_INVALID_FORMAT,
741
+ "hash table load factor exceeds the supported 0.70 maximum");
742
+ return SE_ERR_INVALID_FORMAT;
743
+ }
744
+ if (m->max_probe == 0 || m->max_probe > m->hash_table_size) {
745
+ se_error_set(err, SE_ERR_INVALID_FORMAT, "invalid recorded max probe %u", m->max_probe);
709
746
  return SE_ERR_INVALID_FORMAT;
710
747
  }
711
748
  if (m->subword_prefix_len > 8) {
@@ -8,7 +8,7 @@
8
8
 
9
9
  #define SE_MAGIC "SEMBv1\0\0"
10
10
  #define SE_MAGIC_LEN 8
11
- #define SE_FORMAT_VERSION 2u
11
+ #define SE_FORMAT_VERSION 3u
12
12
  #define SE_HEADER_SIZE 320u
13
13
  #define SE_ALIGNMENT 64u
14
14
 
@@ -18,7 +18,7 @@
18
18
  #define SE_NORMALIZATION_NONE 0u
19
19
  #define SE_NORMALIZATION_L2 1u
20
20
 
21
- #define SE_TRUNCATE_IDS_BEFORE_POOLING 1u
21
+ #define SE_TRUNCATE_USABLE_IDS_BEFORE_POOLING 2u
22
22
 
23
23
  #define SE_UNK_INCLUDE 0u
24
24
  #define SE_UNK_DROP 1u
@@ -26,6 +26,14 @@
26
26
  #define SE_EMPTY_ZERO_VECTOR 0u
27
27
  #define SE_EMPTY_RAISE 1u
28
28
 
29
+ #define SE_ADDED_PAD (1u << 0)
30
+ #define SE_ADDED_UNK (1u << 1)
31
+ #define SE_ADDED_CLS (1u << 2)
32
+ #define SE_ADDED_SEP (1u << 3)
33
+ #define SE_ADDED_MASK (1u << 4)
34
+ #define SE_ADDED_TOKEN_MASK_ALL \
35
+ (SE_ADDED_PAD | SE_ADDED_UNK | SE_ADDED_CLS | SE_ADDED_SEP | SE_ADDED_MASK)
36
+
29
37
  #define SE_SLOT_EMPTY 0xFFFFFFFFu
30
38
  #define SE_SIZE_MAX ((size_t)-1)
31
39
 
@@ -83,7 +91,8 @@ enum {
83
91
  SE_OFF_SEC_ROOT_TRIE = 208,
84
92
  SE_OFF_SEC_CONT_TRIE = 224,
85
93
  SE_OFF_CHECKSUM = 240,
86
- SE_OFF_MAX_PROBE = 304
94
+ SE_OFF_MAX_PROBE = 304,
95
+ SE_OFF_ADDED_TOKEN_MASK = 308
87
96
  };
88
97
 
89
98
  typedef struct {
@@ -163,6 +172,7 @@ typedef struct {
163
172
  uint32_t max_input_chars_per_word;
164
173
  uint32_t max_token_chars;
165
174
  uint32_t max_probe;
175
+ uint32_t added_token_mask;
166
176
  uint32_t pad_id;
167
177
  uint32_t unk_id;
168
178
  uint32_t cls_id;
@@ -401,9 +411,11 @@ typedef struct {
401
411
  uint32_t truncated;
402
412
  } se_token_stats_t;
403
413
 
414
+ typedef enum { SE_TOKEN_LIMIT_RAW = 0, SE_TOKEN_LIMIT_USABLE = 1 } se_token_limit_t;
415
+
404
416
  se_status_t se_tokenize(const se_model_t *model, se_scratch_t *scratch, const uint8_t *input,
405
- size_t input_len, uint32_t max_tokens, se_token_stats_t *stats,
406
- se_error_t *err, volatile sig_atomic_t *cancelled);
417
+ size_t input_len, uint32_t max_tokens, se_token_limit_t limit_mode,
418
+ se_token_stats_t *stats, se_error_t *err, volatile sig_atomic_t *cancelled);
407
419
 
408
420
  se_status_t se_embed_one(const se_model_t *model, se_scratch_t *scratch, const uint8_t *input,
409
421
  size_t input_len, uint32_t max_tokens, float *out, se_token_stats_t *stats,