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 +4 -4
- data/CHANGELOG.md +197 -0
- data/README.md +172 -275
- data/Rakefile +95 -0
- data/benchmark/_support.rb +196 -0
- data/benchmark/core_paths.rb +57 -0
- data/benchmark/gvl_threshold.rb +48 -0
- data/docs/ARCHITECTURE.md +56 -6
- data/docs/BENCHMARKING.md +87 -0
- data/docs/LIMITATIONS.md +91 -0
- data/docs/MODEL_AUDIT.md +29 -10
- data/docs/PERFORMANCE.md +112 -36
- data/ext/static_embeddings/extconf.rb +4 -0
- data/ext/static_embeddings/se_alloc_stats.c +244 -0
- data/ext/static_embeddings/se_f16.c +378 -0
- data/ext/static_embeddings/se_format.c +238 -148
- data/ext/static_embeddings/se_internal.h +159 -0
- data/ext/static_embeddings/se_tokenizer.c +144 -41
- data/ext/static_embeddings/se_topk.c +236 -0
- data/ext/static_embeddings/static_embeddings.c +277 -744
- data/lib/static_embeddings/format.rb +22 -11
- data/lib/static_embeddings/version.rb +1 -1
- data/static_embeddings.gemspec +2 -0
- data/tools/benchmark.rb +13 -4
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ec20ad222235414c2e6f081a2cea5f932a7e3004cb78dcca5552c20208bd6a8
|
|
4
|
+
data.tar.gz: 80553b44c62b46625562accee74ac9bfe689400f05258e551dc9603105f64f7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|