static_embeddings 0.1.1 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a6d1a316162408c80a3de7afce3e1bc1f6e7cce0707c58df8737c86e0748183
4
- data.tar.gz: c6e117117e09740437da333ec7203b62695b53233d5f64caa8cd15cc16f82d7c
3
+ metadata.gz: 7ec20ad222235414c2e6f081a2cea5f932a7e3004cb78dcca5552c20208bd6a8
4
+ data.tar.gz: 80553b44c62b46625562accee74ac9bfe689400f05258e551dc9603105f64f7a
5
5
  SHA512:
6
- metadata.gz: 274b82bff6ffbe3f650687b9e30a611c919ffeedea859d98864857d095100f0142f3ed56954b9420eddf17ab9aac1d879dcf7648539af9a852c4d4a08df150c3
7
- data.tar.gz: e9f11243e8e73ea99ab215620fa024b7cf78ccae8c5e9b2bb1d06dd88e60862fadaa37b8d2fb56b2251740a474c5d556f262e827b35ec898b1dfeadac938fc3d
6
+ metadata.gz: 7fc51c583e8d0f05e5e6b7181767450e4fc93601180a638444cb715be0321d993454003d93bd9bd9f7154e5d678b57dc0fdeba7d5f86c81154f0b61886898101
7
+ data.tar.gz: 1739214fe214cbb86a81e643878a371547b36365d408f2e81e7e95c6c0f0b66c2e214c839f41744347c5015c82832f5a34410e585eb504dab4fe2d3a106ff459
data/CHANGELOG.md CHANGED
@@ -1,5 +1,202 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3 (unreleased)
4
+
5
+ Hardening and tooling, mostly borrowed from the sibling `tg_geometry` gem after
6
+ reading its C extension side by side with this one. No behaviour changes to
7
+ tokenization, pooling or search: the differential fuzz digest against
8
+ `StaticEmbeddings::Reference` is unchanged.
9
+
10
+ ### Fixed
11
+
12
+ - **Unknown keywords are rejected instead of ignored.** `rb_hash_lookup2`
13
+ returns the default for an absent key, so every misspelled keyword used to be
14
+ silently dropped and the caller got a plausible wrong answer. Two cases were
15
+ actively dangerous: `embed(text, fromat: :f16)` returned a full-width f32
16
+ vector, and `embed_batch(texts, threds: 4)` slipped past the guard whose only
17
+ job is to say that `threads:` buys nothing. All eight keyword-accepting
18
+ methods now validate against a whitelist and name the accepted keywords in the
19
+ error.
20
+
21
+ ```ruby
22
+ model.embed("hi", fromat: :f16)
23
+ # ArgumentError: unknown keyword: :fromat
24
+ # (accepted: :max_tokens, :format, :validate_encoding, :threads)
25
+ ```
26
+
27
+ - **`memsize` no longer charges the mapped model to the Ruby heap.** A mapped
28
+ `.semb` is file-backed, shared between forked workers and reclaimable by the
29
+ kernel, but `ObjectSpace.memsize_of(model)` reported its full size, which fed
30
+ GC heuristics memory the VM could neither free nor account for. It now reports
31
+ only process-owned bytes; `mapped_bytes` still reports the mapping. The
32
+ Windows heap fallback really is process memory and is still counted.
33
+
34
+ ### Added
35
+
36
+ - **`benchmark/` with a shared harness.** Iteration counts are calibrated so
37
+ every measurement runs for at least `BENCH_MIN_SECONDS`, repeats are taken,
38
+ the median is reported, and every row carries `spread_pct` — the best-to-worst
39
+ distance across repeats. This release exists partly because a 6% throughput
40
+ claim in 0.1.2 turned out to be inside that spread. `BENCH_FORMAT=kv` gives
41
+ machine-readable output. Run with `rake benchmark`. Inputs are
42
+ `valid_encoding?`-primed, so rows report `coderange=cached` and measure the
43
+ hot path rather than a string fresh from a file or a database.
44
+
45
+ - **`benchmark/gvl_threshold.rb`.** `SE_GVL_UNLOCK_THRESHOLD` has never been
46
+ justified by data. The sweep measures both halves of the trade: on one x86-64
47
+ run the step at 2048 bytes cost about 32% latency and four extra allocations,
48
+ and it is also the point where a second Ruby thread starts making progress at
49
+ all. The constant is unchanged; now there is something to argue from.
50
+
51
+ - **`benchmark/core_paths.rb`,** covering tokenize, single embed, batch embed,
52
+ pooling in isolation and top-k in both formats. It reports single-text embed
53
+ twice, once repeating one string and once rotating a corpus, because the gap
54
+ between them is the cache effect that makes hot-loop numbers unreproducible.
55
+
56
+ - **`test/gc_compaction_test.rb` and a `gc_compaction` CI job.** `top_k`,
57
+ `embed` and `embed_batch` hand raw pointers into C and release the GVL, so
58
+ compaction moving Ruby objects would show up as wrong numbers rather than a
59
+ crash.
60
+
61
+ Getting this test to test anything took two corrections. It first used a
62
+ 64-row matrix, which is 2 KiB against a 1 MiB `SE_TOPK_GVL_UNLOCK_THRESHOLD`,
63
+ so the scan never released the GVL and compaction could not interleave with
64
+ it. The same mistake then turned out to be hiding in the embed case: 2038
65
+ bytes of corpus against a 2048-byte `SE_GVL_UNLOCK_THRESHOLD`, ten bytes
66
+ under. Measured with a spinning neighbour thread, both old cases produced zero
67
+ ticks during the call and both fixed ones produce hundreds of millions. Every
68
+ concurrency case now asserts it is over the relevant threshold, because
69
+ without that assertion the test silently stops testing the moment a constant
70
+ or a fixture changes.
71
+
72
+ - **`docs/LIMITATIONS.md`.** One page answering "does it do X", split into
73
+ decisions and unfinished work: no ANN index, no batched queries, no candidate
74
+ filtering, no int8, no Ractor, no internal parallelism, and so on.
75
+
76
+ - **`docs/BENCHMARKING.md`,** covering how to read `spread_pct` and the four
77
+ measurement traps this repository has already fallen into.
78
+
79
+ ## 0.1.2 (unreleased)
80
+
81
+ A tokenizer correctness fix, the ASCII fast path it made safe to write, and an
82
+ optional build that accounts for the runtime's own C heap.
83
+
84
+ External parity against `model2vec.StaticModel` has **not** been re-run for this
85
+ version. The DEL fix below changes token ids for any input containing `U+007F`,
86
+ so the 0.1.1 audit record no longer covers the shipping runtime. See
87
+ `docs/MODEL_AUDIT.md`; that re-run is the release blocker.
88
+
89
+ ### Fixed
90
+
91
+ - **`U+007F DEL` was not treated as a control character.** `is_control()`
92
+ matched `cp < 0x20` only, so DEL survived `clean_text` and stayed a word byte
93
+ while the reference normalizer drops it. The effect was not one token: the
94
+ word carrying DEL fell out of the vocabulary and collapsed to `[UNK]`.
95
+ A 20 000-case differential fuzz against `StaticEmbeddings::Reference` produced
96
+ 4 084 mismatches before the fix and 0 after; an exhaustive sweep of
97
+ `0x00..0x7F` shows DEL was the only divergent ASCII byte.
98
+
99
+ - **`embed_batch(format: :f16)` allocated a second output buffer.** The f32
100
+ batch buffer was encoded into a freshly allocated half buffer, so an f16 call
101
+ peaked at 1.5x the f32 output and cost more transient C heap than f32 for the
102
+ same work. It now encodes in place. Output is byte-identical; on 5000 x 512
103
+ the transient peak drops from 16.4 MB to 11.3 MB, the same peak the f32 call
104
+ has. Throughput is unchanged: the run-to-run spread on that sample is wider
105
+ than any difference the change makes.
106
+
107
+ ### Added
108
+
109
+ - **ASCII fast path in the tokenizer.** For `cp < 0x80` the normalizer chain
110
+ resolves the CJK, NFD, combining-mark and case-folding branches identically
111
+ every time, so ASCII runs now go through a 128-entry classification table and
112
+ reserve their codepoint buffer once per word instead of once per character.
113
+ Output is unchanged: the token-stream digest over 20 000 fuzz cases is
114
+ identical with and without it. Roughly 1.7x on `tokenize` and 1.2x on `embed`
115
+ for short English text, 2x on `tokenize` with `max_tokens: false`. Non-ASCII
116
+ input is unaffected.
117
+
118
+ - **`test/ascii_parity_test.rb`.** Exhaustive `0x00..0x7F` sweep in six
119
+ contexts, boundary codepoints across the ASCII/Unicode edge, deterministic
120
+ differential fuzz against `Reference` (`FUZZ_N`, `FUZZ_SEED`), truncation
121
+ boundaries, and DEL walked across the prefix window so the `embed` path is
122
+ covered and not just `tokenize`. Four of its cases fail on 0.1.1.
123
+
124
+ - **`test/validate_encoding_test.rb`** and verify coverage in
125
+ `test/format_test.rb`: mode agreement on valid input, invalid bytes inside
126
+ and past the window, cached-broken coderange, non-UTF-8 encodings, batch
127
+ index reporting, tamper detection, truncated and foreign files, and an
128
+ allocation bound proving `verify` does not hold the file in memory.
129
+
130
+ - **`test/cancellation_timing_test.rb`.** Measures how far past a `Timeout`
131
+ deadline `embed` keeps running. It is excluded from `rake test` because it is
132
+ timing-sensitive; run it with `rake cancellation_timing`.
133
+
134
+ - **`validate_encoding: :full | :prefix`** on `embed`, `embed_batch`,
135
+ `embed_with_stats` and `tokenize`. `embed` has to establish that its `String`
136
+ is valid UTF-8, and when Ruby has not computed the coderange yet that scan is
137
+ O(total bytes) and runs before the prefix window is chosen — so the
138
+ large-input path was not actually bounded on a freshly read document.
139
+ `:prefix` skips the up-front scan and lets the tokenizer validate the bytes it
140
+ reads: 442 µs to 78 µs on a 3 MB string, against 71 µs for the same string
141
+ with its coderange cached. `:full` stays the default, because `:prefix`
142
+ changes behaviour — invalid bytes past the truncation window are no longer
143
+ seen. A coderange Ruby has already computed is honoured in both modes.
144
+
145
+ - **Optional C allocation accounting.** Building with
146
+ `STATIC_EMBEDDINGS_ALLOC_STATS=1` wraps the runtime's own allocations and
147
+ exposes per-category bytes and counts, which the sample harness prints as
148
+ `c_alloc.<category>.<metric>`. Default builds do not define the internal
149
+ methods and do not pay for the counters. See `docs/PERFORMANCE.md`.
150
+
151
+ - **CI jobs `parity_fuzz`, `timing`, `alloc_stats` and `windows_loader`,** plus
152
+ a sanitizer build of the instrumented allocator inside the existing `memory`
153
+ job. The Windows job cross-compiles with mingw-w64 and runs the result under
154
+ wine, because that branch of `se_model_open` is the one path no other job
155
+ builds. `alloc_stats` is separate because it has to `rake clobber` first, and
156
+ clobber removes `tmp/`.
157
+
158
+ ### Changed
159
+
160
+ - **The ASCII fast path checks the cancellation flag** on the same cadence as
161
+ the main loop. A run of ASCII bytes is consumed inside one C call, so without
162
+ the check a `max_tokens: false` call on a large document ignored its deadline
163
+ until the whole document was tokenized: 0.33 s against a 0.05 s `Timeout` on a
164
+ 16 MB input, versus 0.05 s now.
165
+
166
+ - **`ext/static_embeddings/static_embeddings.c` was split.** The f16 codec and
167
+ kernels moved to `se_f16.c`, the top-k kernels to `se_topk.c`, and the
168
+ allocation counters live in `se_alloc_stats.c`. Shared overflow-checked size
169
+ helpers, `static_assert`s on the mmapped struct layouts, and an explicit
170
+ rejection of big-endian hosts moved into `se_internal.h`.
171
+
172
+ - **`Format.verify` streams the file.** It read the whole model and then
173
+ duplicated it to zero the checksum field, so verifying a 135 MB artifact
174
+ needed roughly 270 MB of transient `String` before hashing started. Peak is
175
+ now one 1 MiB chunk: measured on a 62 MB model, 433 ms and 280 MB of peak RSS
176
+ down to 249 ms and 30 MB.
177
+
178
+ - **Windows maps the model instead of reading it into the heap.** The POSIX
179
+ loader has always used `mmap`; Windows did `fopen` plus `fread` into a
180
+ private allocation, so every process paid a full read before the first query
181
+ and a private copy of the whole model. It now uses
182
+ `CreateFileMapping`/`MapViewOfFile`, with the old read path kept as a
183
+ fallback for filesystems that refuse to map. Verified by cross-compiling with
184
+ mingw-w64 and running under wine against a real `.semb`: same `map_size`,
185
+ same vectors as the POSIX build, and `mapped=1`.
186
+
187
+ - **Throughput figures separate logical from processed bytes.**
188
+ `tools/benchmark.rb` reports how many texts were truncated and refuses a
189
+ ns/byte figure when truncation makes the two differ; the large-input samples
190
+ print a measured `processed_input_mb_per_sec` alongside the logical one.
191
+
192
+ - **`samples/run_all.sh` captures `rake test`** into `00_test.log`, so a run
193
+ directory can back both "samples green" and "tests green". `TEST=0` skips it.
194
+
195
+ - **Documentation.** The README no longer implies `embed` is constant-time in
196
+ input size (the prefix window bounds the tokenizer, not the UTF-8 validity
197
+ scan) and no longer claims `f16` is faster, since this repository's own
198
+ samples show it winning on x86-64 F16C and losing on M1 Pro.
199
+
3
200
  ## 0.1.1 (unreleased)
4
201
 
5
202
  Safety and correctness release. Everything here was found by re-reviewing 0.1.0