static_embeddings 0.1.4 → 0.1.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/README.md +50 -23
- data/Rakefile +1 -1
- data/docs/ARCHITECTURE.md +42 -31
- data/docs/LIMITATIONS.md +12 -6
- data/docs/MODEL_AUDIT.md +85 -52
- data/ext/static_embeddings/se_embed.c +2 -1
- data/ext/static_embeddings/se_format.c +41 -4
- data/ext/static_embeddings/se_internal.h +17 -5
- data/ext/static_embeddings/se_tokenizer.c +155 -18
- data/ext/static_embeddings/se_unicode.c +1 -1
- data/ext/static_embeddings/static_embeddings.c +32 -2
- data/lib/static_embeddings/cli.rb +1 -0
- data/lib/static_embeddings/converter.rb +51 -7
- data/lib/static_embeddings/format.rb +60 -22
- data/lib/static_embeddings/paths.rb +18 -1
- data/lib/static_embeddings/reference.rb +54 -7
- data/lib/static_embeddings/safetensors.rb +178 -34
- data/lib/static_embeddings/version.rb +1 -1
- data/lib/static_embeddings.rb +3 -5
- data/tools/check_model2vec_parity.rb +85 -54
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb1d3530fba4fad467251429797fdcaae527b5c6c18413e8847002e0c3f3b5e4
|
|
4
|
+
data.tar.gz: f02bb4c129449803a57b08bf9b7186af08a6d21edee1e68d18178b248b855e0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cec0a3959cb3f093c7bb2b06213b4c7e983986466a115eb79e0b045a5b52fdda9797be41365db41a74d85bb52c384c147d68d3d84c503fef10c122bba7b72333
|
|
7
|
+
data.tar.gz: e99039606af3de0106e1ac26efaa78a06f09acbd9c2faaae9b4ca17339bdeb4830ba922bef84dc141317898556d09b1ec41cf1ebfebcce5e141d877c3fc34598
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
Correctness/parity release. The native runtime architecture is unchanged, but
|
|
6
|
+
the tokenizer/pooling contract is now pinned explicitly to `model2vec 0.9.0` +
|
|
7
|
+
`tokenizers 0.23.1`, and CI contains an independent upstream boundary corpus
|
|
8
|
+
instead of treating the Ruby `Reference` twin as proof of compatibility.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`[UNK]` is filtered before the embedding token cap.** `max_tokens: 512` now
|
|
13
|
+
means the first 512 usable (non-UNK, for `UNK_DROP`) ids, matching
|
|
14
|
+
`StaticModel` pooling semantics. Long OOV-heavy text can therefore scan past
|
|
15
|
+
512 raw ids instead of silently averaging fewer rows. `embed_with_stats` and
|
|
16
|
+
`embed_token_ids_with_stats` expose `pooled_count` alongside raw
|
|
17
|
+
`token_count` / `unk_count`.
|
|
18
|
+
- **The supported Hugging Face AddedVocabulary subset is implemented.** The five
|
|
19
|
+
standard BERT special tokens (`[PAD]`, `[UNK]`, `[CLS]`, `[SEP]`, `[MASK]`)
|
|
20
|
+
are extracted literally before normalization when the source tokenizer marks
|
|
21
|
+
them `special: true, normalized: false`. Converter and runtime now share that
|
|
22
|
+
contract instead of merely whitelisting the metadata.
|
|
23
|
+
- **BertNormalizer behavior matches the pinned Rust tokenizer at the known edges.**
|
|
24
|
+
The CJK Extension E range starts at `U+2B920` as in `tokenizers 0.23.1`, not
|
|
25
|
+
the Python `transformers` boundary `U+2B820`. The boundary corpus also locks
|
|
26
|
+
the actual `unicode_categories` 0.1.1 behavior for unassigned (`Cn`)
|
|
27
|
+
codepoints: despite a misleading comment in upstream `bert.rs`, that crate's
|
|
28
|
+
`is_other()` implementation checks Cc/Cf/Co, not Cn.
|
|
29
|
+
- **Vocabulary hash lookup is bounded by the validated `max_probe`,** and model
|
|
30
|
+
loading rejects hash tables above the converter's 0.70 load factor.
|
|
31
|
+
- **`load_model` model ids cannot escape the cache** through absolute paths,
|
|
32
|
+
`..`, empty path components, backslash traversal or NUL bytes.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **`.semb` format v3.** The header records the supported added-token mask and
|
|
37
|
+
the usable-id truncation policy. v2 files must be reconverted; this avoids a
|
|
38
|
+
runtime silently applying 0.1.5 semantics to a file whose tokenizer contract
|
|
39
|
+
was recorded by an older converter.
|
|
40
|
+
- **Model2Vec's character pre-truncation heuristic is intentionally not copied.**
|
|
41
|
+
`max_tokens` means actual usable tokenizer ids. The external oracle records
|
|
42
|
+
rows where `StaticModel`'s `max_length * median_token_length` character cut
|
|
43
|
+
changes ids and reports them as an explicit semantic deviation rather than a
|
|
44
|
+
false parity failure/pass.
|
|
45
|
+
- **The converter reads safetensors incrementally and accepts F32, F16 and BF16
|
|
46
|
+
embedding tensors.** `.semb` rows remain float32. `Format.write` also streams
|
|
47
|
+
sections and computes the checksum incrementally instead of assembling a
|
|
48
|
+
second whole-file Ruby string.
|
|
49
|
+
- **Plain `require "static_embeddings"` loads runtime code only.** Converter,
|
|
50
|
+
safetensors, Unicode-table generation and the Ruby `Reference` are loaded only
|
|
51
|
+
by offline tooling/tests. The `static_embeddings convert` CLI explicitly loads
|
|
52
|
+
the converter before reading converter-specific defaults, so lazy loading does
|
|
53
|
+
not break the documented executable path.
|
|
54
|
+
- **TLS token-id scratch keeps a bounded high-water mark.** It still reserves only
|
|
55
|
+
512 ids initially, but capacities grown by OOV-heavy usable-token truncation are
|
|
56
|
+
retained up to 8192 ids between calls. This removes grow/trim realloc churn on
|
|
57
|
+
realistic truncated inputs without pinning arbitrarily large per-thread buffers.
|
|
58
|
+
|
|
59
|
+
### Verification
|
|
60
|
+
|
|
61
|
+
- Added a deterministic **434-row boundary corpus** covering control/unassigned
|
|
62
|
+
boundaries, every supported CJK range edge ±1, punctuation classes, whitespace, combining
|
|
63
|
+
marks, AddedVocabulary, 511/512/513 boundaries, OOV ratios and long-prefix
|
|
64
|
+
cases.
|
|
65
|
+
- CI now installs pinned `model2vec==0.9.0` / `tokenizers==0.23.1`, generates an
|
|
66
|
+
external oracle, and separately checks raw HF ids, usable ids, embedding
|
|
67
|
+
parity where the contracts are identical, and intentional character-precut
|
|
68
|
+
deviations. The existing 50k native-vs-Ruby differential fuzz remains as a
|
|
69
|
+
separate implementation-consistency layer.
|
|
70
|
+
|
|
3
71
|
## 0.1.4 (unreleased)
|
|
4
72
|
|
|
5
73
|
Hot-path work in the C runtime. Token ids, the pooling contract and f16
|
data/README.md
CHANGED
|
@@ -32,8 +32,11 @@ never arbitrary HuggingFace models. A `.semb` file carries validated metadata, a
|
|
|
32
32
|
BERT WordPiece tokenizer profile, an mmap-ready vocabulary lookup, float32
|
|
33
33
|
embedding rows, and provenance plus a checksum.
|
|
34
34
|
|
|
35
|
-
The only supported tokenizer profile is `BERT_WORDPIECE_V1
|
|
36
|
-
|
|
35
|
+
The only supported tokenizer profile is `BERT_WORDPIECE_V1`, pinned for
|
|
36
|
+
compatibility to `tokenizers 0.23.1`. The converter rejects unsupported
|
|
37
|
+
tokenizer features rather than approximating them. `.semb` format v3 also
|
|
38
|
+
records which of the five standard BERT added tokens are active. Files produced
|
|
39
|
+
by 0.1.4 and earlier use format v2 and must be reconverted.
|
|
37
40
|
|
|
38
41
|
## Converting a model
|
|
39
42
|
|
|
@@ -57,6 +60,9 @@ model = StaticEmbeddings.load(ENV.fetch("EMBEDDING_MODEL")) # explicit path
|
|
|
57
60
|
Convert during image build or deploy preparation; production should only ever
|
|
58
61
|
see `.semb` files.
|
|
59
62
|
|
|
63
|
+
The offline converter accepts F32, F16 and BF16 safetensors embedding matrices;
|
|
64
|
+
all three are converted to float32 rows in `.semb`.
|
|
65
|
+
|
|
60
66
|
### Verify once, not on every boot
|
|
61
67
|
|
|
62
68
|
`load` does not check the SHA-256 by default: hashing a 135 MB file at boot
|
|
@@ -98,6 +104,7 @@ stats = model.embed_with_stats("postgres pipeline mode in Ruby")
|
|
|
98
104
|
stats[:vector]
|
|
99
105
|
stats[:token_count]
|
|
100
106
|
stats[:unk_count]
|
|
107
|
+
stats[:pooled_count] # rows actually eligible for pooling
|
|
101
108
|
stats[:truncated]
|
|
102
109
|
|
|
103
110
|
ids = model.tokenize("postgres pipeline mode in Ruby")
|
|
@@ -116,11 +123,19 @@ Output is a binary `String` of little-endian float32 values in row-major order.
|
|
|
116
123
|
`embed_array` and `embed_batch_arrays` decode that into Ruby `Float` objects and
|
|
117
124
|
exist for debugging and application code, not for the hot path.
|
|
118
125
|
|
|
119
|
-
`
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
`tokenize` exposes raw WordPiece ids, including `[UNK]`, and its own
|
|
127
|
+
`max_tokens` cap is therefore a raw-id cap. Embedding has a different, upstream
|
|
128
|
+
pooling rule: for `UNK_DROP`, `[UNK]` is removed first and `max_tokens` is then
|
|
129
|
+
applied to usable ids. To cache tokenization without changing the embedding,
|
|
130
|
+
cache it unbounded:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
ids = model.tokenize(text, max_tokens: false)
|
|
134
|
+
model.embed_token_ids(ids) == model.embed(text)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`embed_token_ids` applies that same usable-id cap. This distinction matters only
|
|
138
|
+
when `[UNK]` appears around the truncation boundary.
|
|
124
139
|
|
|
125
140
|
### `format: :f16`
|
|
126
141
|
|
|
@@ -175,13 +190,13 @@ model.dot_top_k(query, MATRIX, 10) # lock-free, concurrent, GVL release
|
|
|
175
190
|
|
|
176
191
|
## Large inputs
|
|
177
192
|
|
|
178
|
-
When truncation is active the runtime
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
193
|
+
When truncation is active the runtime starts with a leading slice sized from
|
|
194
|
+
`max_tokens`, cut on a word boundary, and grows the budget until it has reached
|
|
195
|
+
`max_tokens` **usable** ids. For ordinary in-vocabulary text this keeps the
|
|
196
|
+
tokenized prefix to a few kilobytes even when the document is megabytes long.
|
|
197
|
+
For OOV-heavy input it may scan farther — or the whole string — because `[UNK]`
|
|
198
|
+
no longer consumes the usable-token budget. That is required for the corrected
|
|
199
|
+
UNK-before-truncate contract.
|
|
185
200
|
|
|
186
201
|
That bounds the tokenizer, not the whole call. `embed` also has to establish
|
|
187
202
|
that the Ruby `String` is valid UTF-8, and when Ruby has not computed the
|
|
@@ -273,22 +288,34 @@ subword splitting instead of hitting the vocabulary directly.
|
|
|
273
288
|
|
|
274
289
|
## Correctness contract
|
|
275
290
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
291
|
+
0.1.5 separates contracts that 0.1.4 accidentally mixed together:
|
|
292
|
+
|
|
293
|
+
- `tokenize` matches the pinned Hugging Face `tokenizers 0.23.1` raw
|
|
294
|
+
`BertNormalizer + BertPreTokenizer + WordPiece` ids, including the supported
|
|
295
|
+
`normalized: false` standard added tokens and `[UNK]`.
|
|
296
|
+
- Embedding drops `[UNK]` when the model records `UNK_DROP`, then applies
|
|
297
|
+
`max_tokens` to the remaining usable ids, mean-pools, and performs source-model
|
|
298
|
+
L2 normalization.
|
|
299
|
+
- The gem intentionally does **not** reproduce Model2Vec 0.9.0's character
|
|
300
|
+
pre-cut (`max_length * median_token_length`). Here `max_tokens` means actual
|
|
301
|
+
usable tokenizer ids. The oracle reports rows affected by this difference
|
|
302
|
+
separately instead of calling them parity.
|
|
280
303
|
|
|
281
|
-
|
|
282
|
-
|
|
304
|
+
Where the execution contracts are the same, vectors are compared to
|
|
305
|
+
`model2vec.StaticModel` with:
|
|
283
306
|
|
|
284
307
|
```text
|
|
285
308
|
cosine >= 1 - 1e-6
|
|
286
309
|
max_abs_diff < 1e-5
|
|
287
310
|
```
|
|
288
311
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
312
|
+
The committed boundary generator currently produces 434 systematic rows. CI
|
|
313
|
+
generates their oracle from pinned Python packages both on the deterministic
|
|
314
|
+
fixture and on the pinned `potion-retrieval-32M` snapshot. The Ruby `Reference`
|
|
315
|
+
fuzz is kept as a separate C-vs-Ruby implementation check and is not treated as
|
|
316
|
+
proof of upstream compatibility. `docs/MODEL_AUDIT.md` records per-model audits;
|
|
317
|
+
a corpus pass means exactly that corpus passed, not an exhaustive proof of all
|
|
318
|
+
Unicode/tokenizer behavior.
|
|
292
319
|
|
|
293
320
|
## Development
|
|
294
321
|
|
data/Rakefile
CHANGED
|
@@ -44,7 +44,7 @@ task parity: :compile do
|
|
|
44
44
|
Generate it first (needs Python and the model2vec package):
|
|
45
45
|
|
|
46
46
|
python3 -m venv .venv-model2vec && . .venv-model2vec/bin/activate
|
|
47
|
-
pip install
|
|
47
|
+
pip install "model2vec==0.9.0" "tokenizers==0.23.1" numpy
|
|
48
48
|
python tools/model2vec_oracle.py /path/to/source-model --out #{oracle}
|
|
49
49
|
MSG
|
|
50
50
|
end
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -33,7 +33,7 @@ in `se_internal.h`: the header fields are decoded little-endian explicitly, but
|
|
|
33
33
|
the mmapped structures and the float matrix are read in native order, so a
|
|
34
34
|
big-endian host is refused at load rather than silently misread.
|
|
35
35
|
|
|
36
|
-
## `.semb`
|
|
36
|
+
## `.semb` v3
|
|
37
37
|
|
|
38
38
|
Little-endian throughout. 320-byte header, then sections aligned to 64 bytes.
|
|
39
39
|
|
|
@@ -69,6 +69,7 @@ Little-endian throughout. 320-byte header, then sections aligned to 64 bytes.
|
|
|
69
69
|
| 208–239 | 32 | two (offset, size) u64 pairs: `root_trie`, `continuation_trie` |
|
|
70
70
|
| 240 | 32 | sha256 of the file with these 32 bytes zeroed |
|
|
71
71
|
| 304 | 4 | max_probe observed in the vocabulary hash |
|
|
72
|
+
| 308 | 4 | supported added-token bit mask |
|
|
72
73
|
|
|
73
74
|
Sections, in fixed order: `vocab_strings`, `vocab_hash`, `embeddings`,
|
|
74
75
|
`norm_tables`, `provenance`, `root_trie`, `continuation_trie`.
|
|
@@ -79,11 +80,12 @@ already has a slot for I8; there is no dead I8 path in C.
|
|
|
79
80
|
### Why the semantics live in the header
|
|
80
81
|
|
|
81
82
|
`max_tokens_default`, `unk_policy`, `empty_policy` and the normalizer flags
|
|
82
|
-
are properties of the *model*, not options the caller guesses. The
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
are properties of the *model*, not options the caller guesses. The embedding contract caps the first 512 usable token ids by default;
|
|
84
|
+
a model card's `model_max_length` of 1,000,000 is a different number entirely, and
|
|
85
|
+
confusing them silently changes every vector for long chunks. Model2Vec 0.9.0
|
|
86
|
+
also applies a character pre-cut before tokenization; this runtime intentionally
|
|
87
|
+
does not copy that heuristic. Anything that can silently change output is a
|
|
88
|
+
recorded field or an explicitly documented compatibility deviation.
|
|
87
89
|
|
|
88
90
|
## Vocabulary lookup and WordPiece trie
|
|
89
91
|
|
|
@@ -93,7 +95,7 @@ load factor ≤ 0.70, linear probing, FNV-1a with a fixed seed. The hash table i
|
|
|
93
95
|
kept for validation, direct lookup and debugging.
|
|
94
96
|
|
|
95
97
|
The hot WordPiece path no longer performs repeated hash lookups for every
|
|
96
|
-
`end--` candidate.
|
|
98
|
+
`end--` candidate. The format also stores two converter-built, mmap-readable
|
|
97
99
|
tries inspired by double-array trie libraries such as libdatrie:
|
|
98
100
|
|
|
99
101
|
- `root_trie` for tokens that can start a word;
|
|
@@ -135,8 +137,8 @@ Rejected alternatives:
|
|
|
135
137
|
|
|
136
138
|
## Unicode without a Unicode library
|
|
137
139
|
|
|
138
|
-
`BertNormalizer` needs NFD, simple lowercase, and
|
|
139
|
-
categories. Rather than vendoring utf8proc, the converter generates exactly
|
|
140
|
+
`BertNormalizer` needs NFD, simple lowercase, and selected Unicode
|
|
141
|
+
categories (Mn, punctuation, control/format/private-use, and whitespace). Rather than vendoring utf8proc, the converter generates exactly
|
|
140
142
|
those tables from the Ruby VM's own Unicode data and writes them into the
|
|
141
143
|
model file; the runtime binary-searches them.
|
|
142
144
|
|
|
@@ -146,15 +148,20 @@ maps `ß` to `ss`; `str::to_lowercase`, which HF uses, does not. A gem that
|
|
|
146
148
|
picked the wrong one would produce plausible, slightly wrong vectors forever.
|
|
147
149
|
|
|
148
150
|
The Ruby build that generated the tables is stamped into provenance, so a
|
|
149
|
-
model file and its normalizer can never drift apart unnoticed.
|
|
151
|
+
model file and its embedded normalizer tables can never drift apart unnoticed.
|
|
152
|
+
That stamp is not, by itself, proof of Hugging Face parity: 0.1.5 therefore
|
|
153
|
+
checks a systematic boundary corpus against pinned `tokenizers 0.23.1` in CI.
|
|
154
|
+
One subtle example is `Cn`: `bert.rs` says `is_other()` includes unassigned
|
|
155
|
+
codepoints, while the `unicode_categories 0.1.1` implementation actually tests
|
|
156
|
+
Cc/Cf/Co. The runtime follows the executable upstream behavior, not the comment.
|
|
150
157
|
|
|
151
158
|
## The embedding kernel
|
|
152
159
|
|
|
153
160
|
```
|
|
154
|
-
tokenize -> ids
|
|
155
|
-
truncate to max_tokens (before pooling — the reference contract)
|
|
161
|
+
tokenize -> raw ids (including [UNK])
|
|
156
162
|
drop [UNK] if unk_policy = DROP
|
|
157
|
-
|
|
163
|
+
truncate usable ids to max_tokens
|
|
164
|
+
acc[j] += row[j] for each usable id, in token order
|
|
158
165
|
divide by the number of pooled rows
|
|
159
166
|
L2 normalize if the model says so
|
|
160
167
|
```
|
|
@@ -209,10 +216,11 @@ therefore serialised the process on work that the tokenizer would discard after
|
|
|
209
216
|
|
|
210
217
|
`embed` and `embed_batch` instead copy a leading slice. The budget starts at
|
|
211
218
|
`max_tokens * 16` bytes clamped to `[4096, 65536]`, and the slice is cut at a
|
|
212
|
-
position where the tokenizer would have started a fresh word. A result is
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
position where the tokenizer would have started a fresh word. A result is accepted only when that slice actually reached `max_tokens`
|
|
220
|
+
usable ids; `[UNK]` does not consume the embedding budget. Otherwise the budget
|
|
221
|
+
doubles and the text is tokenized again. Growth is geometric. OOV-heavy input
|
|
222
|
+
may therefore reach the full document: deciding that fewer than `max_tokens`
|
|
223
|
+
usable ids exist requires reading it.
|
|
216
224
|
|
|
217
225
|
Three properties make this exact rather than approximate:
|
|
218
226
|
|
|
@@ -241,10 +249,11 @@ the predicate is Unicode-aware. Text with genuinely no legal cut inside the scan
|
|
|
241
249
|
window - one enormous word, a run of combining marks - still falls back to a
|
|
242
250
|
full copy, because there is nothing else that would be correct.
|
|
243
251
|
|
|
244
|
-
`test/prefix_chunking_test.rb` sweeps every printable ASCII separator
|
|
245
|
-
CJK, NBSP and Unicode-punctuation documents against
|
|
246
|
-
`embed_token_ids
|
|
247
|
-
|
|
252
|
+
`test/prefix_chunking_test.rb` sweeps every printable ASCII separator and
|
|
253
|
+
checks CJK, NBSP and Unicode-punctuation documents against unbounded raw
|
|
254
|
+
tokenization followed by `embed_token_ids`. It asserts bounded prefix cost for
|
|
255
|
+
in-vocabulary text; deliberately OOV-heavy input is checked for correctness, not
|
|
256
|
+
flat cost.
|
|
248
257
|
|
|
249
258
|
With `max_tokens: false` there is nothing to truncate and the whole input is
|
|
250
259
|
copied.
|
|
@@ -299,10 +308,13 @@ interrupts a GVL-free region.
|
|
|
299
308
|
No global mutable state exists in C. The model is immutable after load.
|
|
300
309
|
Scratch buffers are reused per OS thread via `pthread_key` / `FlsAlloc`, and
|
|
301
310
|
the destructor frees them when the thread exits — including the short-lived
|
|
302
|
-
threads the fiber scheduler path creates per large call. On release the
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
threads the fiber scheduler path creates per large call. On release the
|
|
312
|
+
codepoint/byte buffers are trimmed back to their reserve sizes. The token-id
|
|
313
|
+
buffer keeps its observed high-water mark up to 8192 ids so repeated OOV-heavy
|
|
314
|
+
usable-token truncation does not grow and shrink it on every call; larger
|
|
315
|
+
capacities are trimmed back to that bound so one unlimited-`max_tokens`
|
|
316
|
+
document cannot pin an arbitrary per-thread working set. A nested call on the
|
|
317
|
+
same thread allocates a one-off heap scratch that is freed on release.
|
|
306
318
|
|
|
307
319
|
## Where this fits in a RAG pipeline
|
|
308
320
|
|
|
@@ -321,12 +333,11 @@ chunks -> .semb vectors --/
|
|
|
321
333
|
|
|
322
334
|
## Open questions before v1.0
|
|
323
335
|
|
|
324
|
-
1. **Which potion models actually match `BERT_WORDPIECE_V1`.** The
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
models today; whether it has to is an audit question.
|
|
336
|
+
1. **Which potion models actually match `BERT_WORDPIECE_V1`.** The runtime now
|
|
337
|
+
implements the exact `normalized: false` extraction semantics needed by the
|
|
338
|
+
five standard BERT added tokens, but still rejects arbitrary AddedVocabulary,
|
|
339
|
+
BPE and Unigram profiles. Each source model still needs an audit before it is
|
|
340
|
+
treated as compatible.
|
|
330
341
|
2. **Russian.** Distilling a multilingual teacher into a WordPiece vocabulary
|
|
331
342
|
we control keeps the pure-C path, but skips the Tokenlearn pre-training
|
|
332
343
|
that gives the published potion models their quality. That gap has to be
|
data/docs/LIMITATIONS.md
CHANGED
|
@@ -13,9 +13,10 @@ the entry says whether that is a decision or just unfinished work.
|
|
|
13
13
|
- **Only `.semb` files from this repository's converter.** Loading a
|
|
14
14
|
HuggingFace directory at runtime is not supported and will not be. The
|
|
15
15
|
converter is the only thing that reads third-party files, and it runs offline.
|
|
16
|
-
- **Only the `BERT_WORDPIECE_V1` tokenizer profile.** SentencePiece, BPE,
|
|
17
|
-
and byte-level tokenizers are rejected
|
|
18
|
-
|
|
16
|
+
- **Only the pinned `BERT_WORDPIECE_V1` tokenizer profile.** SentencePiece, BPE,
|
|
17
|
+
Unigram and byte-level tokenizers are rejected. AddedVocabulary support is
|
|
18
|
+
deliberately limited to the five standard BERT special tokens with
|
|
19
|
+
`special: true`, `normalized: false` and no strip/single-word behavior.
|
|
19
20
|
- **No model training, distillation or fine-tuning.** Produce the model with
|
|
20
21
|
upstream `model2vec`, then convert it.
|
|
21
22
|
- **No transformer inference.** Static embeddings have no attention and no
|
|
@@ -38,6 +39,10 @@ the entry says whether that is a decision or just unfinished work.
|
|
|
38
39
|
- **Big-endian hosts are refused at load.** The header is decoded
|
|
39
40
|
little-endian explicitly, but the mmapped structures and the float matrix are
|
|
40
41
|
read in native order.
|
|
42
|
+
- **Do not truncate/overwrite a mapped `.semb` in place.** On POSIX, shrinking
|
|
43
|
+
the inode underneath an existing mmap can raise `SIGBUS` in a worker. Deploy a
|
|
44
|
+
new file and atomically rename it over the path (`write .tmp` → `fsync` →
|
|
45
|
+
`rename`); existing workers keep their old inode mapping safely.
|
|
41
46
|
|
|
42
47
|
## Search
|
|
43
48
|
|
|
@@ -76,9 +81,10 @@ the entry says whether that is a decision or just unfinished work.
|
|
|
76
81
|
- **`load` does not check the SHA-256.** Structural validation always runs, but
|
|
77
82
|
a bit flip inside the matrix is only caught by `verify: true` or
|
|
78
83
|
`StaticEmbeddings.verify`.
|
|
79
|
-
- **Parity is per model
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
- **Parity is per model, runtime, pinned upstream versions and corpus.** A
|
|
85
|
+
recorded pass means those rows passed; it is not an exhaustive proof of
|
|
86
|
+
tokenizer equivalence. The independent upstream CI and the Ruby implementation
|
|
87
|
+
fuzz intentionally test different things.
|
|
82
88
|
|
|
83
89
|
## Known open questions
|
|
84
90
|
|
data/docs/MODEL_AUDIT.md
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
# Model Audit
|
|
2
2
|
|
|
3
|
-
A converted model is trusted only after
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 |
|
|
15
|
+
| runtime | external result | note |
|
|
12
16
|
|---|---|---|
|
|
13
|
-
| 0.1.1 |
|
|
14
|
-
| 0.1.2 |
|
|
15
|
-
| 0.1.3 |
|
|
16
|
-
| 0.1.4 |
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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,70 @@ vector_failures=[]
|
|
|
67
52
|
parity OK
|
|
68
53
|
```
|
|
69
54
|
|
|
70
|
-
The
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
@@ -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,
|
|
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);
|