static_embeddings 0.1.3 → 0.1.4

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: 7ec20ad222235414c2e6f081a2cea5f932a7e3004cb78dcca5552c20208bd6a8
4
- data.tar.gz: 80553b44c62b46625562accee74ac9bfe689400f05258e551dc9603105f64f7a
3
+ metadata.gz: f72b45c2d6fa5310e3b6d5a9b0a86ab0430fc4af71f6811f9aa02655de6272a8
4
+ data.tar.gz: 51f0bb96a3b0adc5f2799e1df425cf1747ca521d92ae90afba59e0014f1d4106
5
5
  SHA512:
6
- metadata.gz: 7fc51c583e8d0f05e5e6b7181767450e4fc93601180a638444cb715be0321d993454003d93bd9bd9f7154e5d678b57dc0fdeba7d5f86c81154f0b61886898101
7
- data.tar.gz: 1739214fe214cbb86a81e643878a371547b36365d408f2e81e7e95c6c0f0b66c2e214c839f41744347c5015c82832f5a34410e585eb504dab4fe2d3a106ff459
6
+ metadata.gz: 53630b742cd2723656b5d826a82f57042dcaab1e3259758aa6a71eac20ba387a23b3fb3905f0f36f3ce641b122229f366c9af1f029205b44cd82110f538a49cb
7
+ data.tar.gz: 199cd93e9898b7457d5e19b9cf1857b4d78ffc46777ca26778b094fa70ee02559dc90733e4e815eba74297ef9c066940792cef09e022801ef48c704e868fa8db
data/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4 (unreleased)
4
+
5
+ Hot-path work in the C runtime. Token ids, the pooling contract and f16
6
+ rounding are unchanged: the ASCII parity sweep and the differential fuzz
7
+ digest against `StaticEmbeddings::Reference` still match. The
8
+ `model2vec.StaticModel` oracle was re-run for 0.1.4 against the same
9
+ `potion-retrieval-32m` snapshot and `.semb` as 0.1.3: 31/31 id rows, 31/31
10
+ vectors, `min_cosine=0.9999999999989528`, `max_abs_all=2.980232238769531e-07`
11
+ — the same printed numbers as 0.1.3. L2 still accumulates sum-of-squares in
12
+ double; 0.1.4 does that with SIMD pairwise adds.
13
+
14
+ Measured on an M1 Pro, `potion-retrieval-32m`, `samples/run_all.sh`
15
+ `DURATION=25`, production build (no alloc-stats). The 0.1.3 comparison run
16
+ had `STATIC_EMBEDDINGS_ALLOC_STATS=1`, which the docs cost at about 3% on
17
+ `embed` and 13% on `tokenize`; differences smaller than that, or inside the
18
+ usual 5–10% laptop spread, are not claimed.
19
+
20
+ ### Changed
21
+
22
+ - **L2 normalisation is a SIMD double reduction plus one scale into `out`.**
23
+ It used to `scale_copy` with `1.0` and then walk the vector twice in scalar
24
+ double. On this model `dim=512` and short in-vocabulary English, that L2
25
+ slot was about 19% of an ASCII `embed_batch` sample tree. The fused path
26
+ has a NEON/`__aarch64__` kernel and an SSE2 kernel; the scalar tail is
27
+ unchanged. ASCII batch went 307k → 338k texts/s on the M1 Pro (+10%). The
28
+ SSE2 path is compiled, not timed.
29
+
30
+ - **The AArch64 f16 top-k kernel decodes 16 halves per iteration, not 8.**
31
+ The x86 F16C kernel is untouched. Same 50 000-row, `k=10`, ASCII matrix:
32
+
33
+ ```text
34
+ f32 f16
35
+ x86-64, f16c 3.00 ms 1.65 ms f16 1.8x faster (0.1.2, F16C)
36
+ M1 Pro, neon-fp16 2.36 ms 1.81 ms f16 1.3x faster (0.1.4)
37
+ ```
38
+
39
+ The M1 Pro row used to read 2.29 / 2.88 ms, f16 1.3x *slower*. `docs/PERFORMANCE.md`
40
+ and the README follow the new numbers. On the lookup-table fallback f16 is
41
+ still usually slower than f32.
42
+
43
+ - **Scratch buffers are reused per OS thread, and freed when the thread
44
+ exits.** `embed`, `tokenize`, `embed_batch` and `embed_token_ids` used to
45
+ `malloc`/`free` a scratch set on every call. A first cut of 0.1.4 used
46
+ `__thread` storage with no destructor, which leaked the heap buffers inside
47
+ the slot — bounded for a Puma pool, unbounded on the fiber-scheduler path
48
+ that `rb_thread_create`s one OS thread per large call. The slot is now a
49
+ `pthread_key` / `FlsAlloc` value whose destructor frees it. On release the
50
+ slot is trimmed back to the reserve sizes, so one `max_tokens: false`
51
+ document does not pin megabytes on that thread until process exit.
52
+ `tokenize` and the small `embed` path release the slot through `rb_ensure`,
53
+ so `rb_ary_push` raising cannot leave `in_use` stuck. `memory_smoke` calls
54
+ acquire/release so ASan/valgrind actually see this code.
55
+
56
+ - **ASCII WordPiece copies an all-ASCII word as bytes** instead of
57
+ encoding UTF-8 through the codepoint buffer, then hashes and splits on the
58
+ trie with identity offsets. Output is the same ids. On the OOV tokenize
59
+ probes this is inside the run-to-run floor.
60
+
61
+ - **Latin-1 (`U+0080..U+00FF`) lower/NFD/Mn/P/C/Zs tables are built at
62
+ load.** Codepoints below 256 skip the binary search into the mmap'd maps.
63
+ ASCII (`< 0x80`) still uses the existing 128-entry class table. Not visible
64
+ as texts/s on the English batch corpus.
65
+
66
+ - **Vocabulary compare is an inline unaligned equality test** instead of
67
+ libc `memcmp`. Trie child lookup special-cases one- and two-edge nodes
68
+ before the linear/binary search. Same ids; no measured throughput claim.
69
+
70
+ Two things were measured and not shipped. Unrolling `add_row` to 32 floats
71
+ and stretching the prefetch distance to 8 cost about 13% on
72
+ `random_pooling_hot_path` — that loop is already memory-bound on random 2 KiB
73
+ row gathers. Walking the ASCII trie without `cps2` (`start += matched_len`)
74
+ cost about 38% on the OOV tokenize probes.
75
+
76
+ ### Added
77
+
78
+ - **Batch samples print `mean_tokens_per_text`, `unk_ratio` and
79
+ `tokens_per_sec`.** 307k vs 103k texts/s is not readable without token
80
+ density: on this corpus ASCII is 31 tokens/text and 0 unk, hashes 73 and 0,
81
+ unicode 68 and 4.4% unk.
82
+
83
+ - **`test/scratch_tls_test.rb`.** When the instrumented allocator is loaded:
84
+ scratch bytes plateau across eight generations of helper threads, and a
85
+ large `max_tokens: false` embed must not leave the calling thread's slot
86
+ pinned. The `alloc_stats` CI job is what actually runs it.
87
+
3
88
  ## 0.1.3 (unreleased)
4
89
 
5
90
  Hardening and tooling, mostly borrowed from the sibling `tg_geometry` gem after
data/README.md CHANGED
@@ -132,10 +132,11 @@ in float32. For a 512-dimensional model one vector goes from 2048 to 1024 bytes.
132
132
  It is accepted by `embed`, `embed_batch`, `embed_with_stats`, `embed_token_ids`,
133
133
  `embed_token_ids_with_stats`, `pack`, `unpack`, and the top-k helpers.
134
134
 
135
- Choose `f16` for the RAM and storage it saves, not for speed. It halves the
136
- bytes a top-k scan streams, but every row still has to be decoded before
137
- scoring, and which effect wins is a property of the machine — this repository's
138
- own samples show `f16` winning on x86-64 F16C and losing on M1 Pro. See
135
+ Choose `f16` for the RAM and storage it saves; speed is a property of the
136
+ machine and of the decode kernel. It halves the bytes a top-k scan streams, but
137
+ every row still has to be decoded before scoring — this repository's own
138
+ samples show `f16` winning on x86-64 F16C and, as of 0.1.4, on M1 Pro neon-fp16
139
+ as well. On the lookup-table fallback it usually loses. See
139
140
  `docs/PERFORMANCE.md` before assuming either.
140
141
 
141
142
  `StaticEmbeddings.simd_backend` reports the live kernel: `"neon-fp16"`,
@@ -323,7 +324,7 @@ cc -O2 -std=c99 -Wall -Wextra -Iext/static_embeddings \
323
324
  ext/static_embeddings/se_unicode.c \
324
325
  ext/static_embeddings/se_tokenizer.c \
325
326
  ext/static_embeddings/se_embed.c \
326
- -lm -o tmp/memory_smoke
327
+ -lm -pthread -o tmp/memory_smoke
327
328
  ./tmp/memory_smoke tmp/test-tiny.semb
328
329
  ```
329
330
 
data/docs/ARCHITECTURE.md CHANGED
@@ -174,10 +174,13 @@ storage/transport choice, not a different model. `embed_array` and
174
174
 
175
175
  The f32 compute path uses small explicit SIMD kernels where they keep the format
176
176
  simple: row accumulation and scaling use NEON/SSE when the compiler target
177
- exposes them, with scalar fallback everywhere else. `dot_top_k`/`cosine_top_k`
177
+ exposes them, L2 sum-of-squares uses SIMD double (NEON on AArch64, SSE2 on
178
+ x86) so the inverse stays a `1/sqrt` of a double accumulation, and there is a
179
+ scalar fallback everywhere else. `dot_top_k`/`cosine_top_k`
178
180
  share the SIMD dot kernel for `format: :f32`; `cosine_top_k` additionally
179
181
  accumulates each row's sum of squares so it can divide by the true norms.
180
- `format: :f16` top-k decodes half components on the fly. What is still
182
+ `format: :f16` top-k decodes half components on the fly (16-wide on AArch64
183
+ NEON-FP16, 16-wide on x86 F16C). What is still
181
184
  deliberately absent is a dependency on BLAS: pooling is gathering random
182
185
  embedding rows, not a dense matrix multiply.
183
186
 
@@ -293,8 +296,13 @@ cadence in bytes so a long ASCII run is no less interruptible. The pooling loop
293
296
  checks every 256 rows, and the top-k scan every 1024 rows. `unblock_cancel` sets that flag when Ruby
294
297
  interrupts a GVL-free region.
295
298
 
296
- No global mutable state exists in C. The model is immutable after load and
297
- scratch buffers are per call.
299
+ No global mutable state exists in C. The model is immutable after load.
300
+ Scratch buffers are reused per OS thread via `pthread_key` / `FlsAlloc`, and
301
+ 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 slot
303
+ is trimmed back to the reserve sizes, so a single unlimited-`max_tokens`
304
+ document does not pin its working set until the thread dies. A nested call on
305
+ the same thread allocates a one-off heap scratch that is freed on release.
298
306
 
299
307
  ## Where this fits in a RAG pipeline
300
308
 
data/docs/LIMITATIONS.md CHANGED
@@ -57,8 +57,10 @@ the entry says whether that is a decision or just unfinished work.
57
57
  ## Storage formats
58
58
 
59
59
  - **`f32` and `f16` only.** No int8, no binary quantisation.
60
- - **`f16` is a storage trade-off, not a speed feature.** See
61
- `docs/PERFORMANCE.md`; it wins on some CPUs and loses on others.
60
+ - **`f16` is a storage encoding, not a different model.** Top-k over an f16
61
+ matrix can be faster or slower than f32 depending on the decode kernel; see
62
+ `docs/PERFORMANCE.md`. `embed_batch(format: :f16)` still encodes the blob
63
+ with the scalar converter.
62
64
  - **`f16` rounding is half-up, not ties-to-even.** Blobs written by this gem can
63
65
  differ from NumPy or PyTorch by one ULP on exact halfway values.
64
66
 
data/docs/MODEL_AUDIT.md CHANGED
@@ -12,26 +12,28 @@ runtime half of it even though the file bytes are untouched.
12
12
  |---|---|---|
13
13
  | 0.1.1 | `parity OK`, recorded below | superseded |
14
14
  | 0.1.2 | **not re-run** | superseded before release |
15
- | 0.1.3 | `parity OK`, recorded below | current |
15
+ | 0.1.3 | `parity OK` | tokenizer / pooling contract; same numbers as 0.1.4 |
16
+ | 0.1.4 | `parity OK`, recorded below | current |
16
17
 
17
18
  0.1.2 changed `is_control()`, which changes token ids for any input containing
18
19
  `U+007F`. `StaticEmbeddings::Reference` cannot settle whether the new behaviour
19
20
  matches HuggingFace, because it is an implementation twin of the C runtime
20
21
  written in this repository. The 0.1.3 release candidate was checked against a
21
22
  fresh upstream `model2vec.StaticModel` oracle that includes DEL, control
22
- characters, Unicode, OOV, long-word and truncation rows.
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`.
23
25
 
24
26
  Source model:
25
27
 
26
28
  - Hugging Face repository: `minishlab/potion-retrieval-32M`
27
29
  - Hugging Face snapshot: `6fc8051fab2a1e0ee76689cf08c853792ac285e7`
28
30
  - Oracle implementation: `model2vec.StaticModel.from_pretrained`
29
- - Python package: `model2vec 0.9.0`
31
+ - Python package: `model2vec 0.9.0` (`tokenizers 0.23.1`, `numpy 2.5.2`)
30
32
  - Oracle file: `tmp/model2vec_oracle.json`
31
33
  - Oracle rows in this recorded run: `31`
32
34
  - Oracle dimension: `512`
33
35
  - Oracle max length: `512`
34
- - Runtime at time of this record: `static_embeddings 0.1.3`
36
+ - Runtime at time of this record: `static_embeddings 0.1.4`
35
37
 
36
38
  Converted `.semb`:
37
39
 
@@ -44,7 +46,8 @@ Converted `.semb`:
44
46
  Parity command:
45
47
 
46
48
  ```bash
47
- python tools/model2vec_oracle.py minishlab/potion-retrieval-32M \
49
+ python tools/model2vec_oracle.py \
50
+ ~/.cache/huggingface/hub/models--minishlab--potion-retrieval-32M/snapshots/6fc8051fab2a1e0ee76689cf08c853792ac285e7 \
48
51
  --out tmp/model2vec_oracle.json
49
52
 
50
53
  bundle exec rake parity \
@@ -52,7 +55,7 @@ bundle exec rake parity \
52
55
  ORACLE=tmp/model2vec_oracle.json
53
56
  ```
54
57
 
55
- Parity result:
58
+ Parity result (0.1.4, 2026-08-31):
56
59
 
57
60
  ```text
58
61
  rows=31
@@ -64,6 +67,11 @@ vector_failures=[]
64
67
  parity OK
65
68
  ```
66
69
 
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
+
67
75
  Decision:
68
76
 
69
77
  - Tokenization parity: pass
@@ -75,6 +83,6 @@ Decision:
75
83
  - Long input / truncation behavior: pass
76
84
  - DEL / control-character behavior: pass
77
85
 
78
- Accepted for runtime and benchmark use **under 0.1.3**. The `.semb` file is
79
- unchanged and its SHA256 still matches; the runtime half of the record was
80
- refreshed after the DEL fix.
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.
data/docs/PERFORMANCE.md CHANGED
@@ -22,33 +22,42 @@ ruby tools/benchmark.rb [model.semb]
22
22
 
23
23
  ## Where the time actually goes
24
24
 
25
- Tokenization dominates; pooling is a handful of row reads and adds. That is why
26
- the tokenizer is the file to profile, and why SIMD in the pooling loop is not
27
- where the wins are that loop is memory-bound on random row lookups and the
28
- compiler already vectorises it.
29
-
30
- Out-of-vocabulary text is the main cliff. In-vocabulary words hit the
31
- vocabulary hash directly; everything else falls through to trie-driven subword
32
- splitting, which on a synthetic all-OOV corpus measured several times the
33
- per-text cost. Non-English input on an English model pays this on top of the
34
- `[UNK]` quality problem.
25
+ Which file to profile depends on the model and the text. On a short-dimension
26
+ demo model, or on synthetic all-OOV input, tokenization dominates. On
27
+ `potion-retrieval-32m` (`dim=512`) with in-vocabulary English, pooling is the
28
+ larger share: each token is a random 2 KiB row add, and before 0.1.4 the scalar
29
+ L2 pass showed up as about 19% of an ASCII `embed_batch` sample tree. SIMD in
30
+ that L2 slot was a real win; another unroll of the add loop was not it is
31
+ memory-bound on those gathers, and the compiler already vectorises it.
32
+
33
+ Out-of-vocabulary text is the remaining tokenizer cliff. In-vocabulary words
34
+ hit the vocabulary hash directly; everything else falls through to trie-driven
35
+ subword splitting, which on a synthetic all-OOV corpus measured several times
36
+ the per-text cost. Non-English input on an English model pays this on top of
37
+ the `[UNK]` quality problem. Quote `mean_tokens_per_text` and `unk_ratio` from
38
+ the batch samples before comparing texts/s across modes: hashes are slower
39
+ than ASCII on this corpus mostly because they emit ~73 tokens/text against
40
+ ~31, not only because they miss the hash.
35
41
 
36
42
  ## `f16` is a storage trade-off
37
43
 
38
- `format: :f16` halves the bytes a top-k scan streams and doubles the decode
39
- work. Which one wins is a property of the machine, and this repository's own
40
- sample runs disagree with each other — same corpus, same `k`, native decode
41
- kernel in both cases:
44
+ `format: :f16` halves the bytes a top-k scan streams and costs a decode per
45
+ row. Which one wins is a property of the machine and of the decode kernel, and
46
+ this repository's own sample runs disagree with each other — same corpus, same
47
+ `k`, native decode kernel in both cases:
42
48
 
43
49
  ```text
44
50
  f32 f16
45
- x86-64, f16c 3.00 ms 1.65 ms f16 1.8x faster
46
- M1 Pro, neon-fp16 2.29 ms 2.88 ms f16 1.3x slower
51
+ x86-64, f16c 3.00 ms 1.65 ms f16 1.8x faster (F16C kernel, unchanged)
52
+ M1 Pro, neon-fp16 2.36 ms 1.81 ms f16 1.3x faster (0.1.4, 16-wide NEON)
47
53
  ```
48
54
 
49
- Neither ratio transfers. Confirm `StaticEmbeddings.simd_backend`, then measure
50
- on the hardware you will run on. On the lookup-table fallback `f16` is usually
51
- slower than `f32`.
55
+ The M1 Pro row in 0.1.3 was 2.29 / 2.88 ms, f16 1.3x *slower*, with an 8-wide
56
+ decode. Neither ratio transfers. Confirm `StaticEmbeddings.simd_backend`, then
57
+ measure on the hardware you will run on. On the lookup-table fallback `f16` is
58
+ usually slower than `f32`. `embed_batch(format: :f16)` still encodes the
59
+ returned blob with the scalar half converter, so a batch that is not a top-k
60
+ scan can still be slower than `f32` on the same machine.
52
61
 
53
62
  ## C allocation counters
54
63
 
@@ -8,6 +8,7 @@
8
8
  #define SE_HAVE_NEON 1
9
9
  #elif defined(__SSE__)
10
10
  #include <xmmintrin.h>
11
+ #include <emmintrin.h>
11
12
  #define SE_HAVE_SSE 1
12
13
  #endif
13
14
 
@@ -71,15 +72,49 @@ static void scale_copy(float *out, const float *acc, uint32_t dim, float inv) {
71
72
  out[j] = acc[j] * inv;
72
73
  }
73
74
 
74
- void se_l2_normalize(float *vec, uint32_t dim) {
75
+ static double sum_sq_d(const float *vec, uint32_t dim) {
76
+ uint32_t j = 0;
75
77
  double sum = 0.0;
76
- for (uint32_t j = 0; j < dim; j++)
78
+ #if defined(SE_HAVE_NEON) && defined(__aarch64__)
79
+ float64x2_t s0 = vdupq_n_f64(0.0);
80
+ float64x2_t s1 = vdupq_n_f64(0.0);
81
+ for (; j + 7 < dim; j += 8) {
82
+ float32x4_t a = vld1q_f32(vec + j);
83
+ float32x4_t b = vld1q_f32(vec + j + 4);
84
+ float64x2_t a0 = vcvt_f64_f32(vget_low_f32(a));
85
+ float64x2_t a1 = vcvt_f64_f32(vget_high_f32(a));
86
+ float64x2_t b0 = vcvt_f64_f32(vget_low_f32(b));
87
+ float64x2_t b1 = vcvt_f64_f32(vget_high_f32(b));
88
+ s0 = vaddq_f64(s0, vmulq_f64(a0, a0));
89
+ s1 = vaddq_f64(s1, vmulq_f64(a1, a1));
90
+ s0 = vaddq_f64(s0, vmulq_f64(b0, b0));
91
+ s1 = vaddq_f64(s1, vmulq_f64(b1, b1));
92
+ }
93
+ sum = vaddvq_f64(vaddq_f64(s0, s1));
94
+ #elif defined(SE_HAVE_SSE)
95
+ __m128d s0 = _mm_setzero_pd();
96
+ __m128d s1 = _mm_setzero_pd();
97
+ for (; j + 3 < dim; j += 4) {
98
+ __m128 v = _mm_loadu_ps(vec + j);
99
+ __m128d lo = _mm_cvtps_pd(v);
100
+ __m128d hi = _mm_cvtps_pd(_mm_movehl_ps(v, v));
101
+ s0 = _mm_add_pd(s0, _mm_mul_pd(lo, lo));
102
+ s1 = _mm_add_pd(s1, _mm_mul_pd(hi, hi));
103
+ }
104
+ double tmp[2];
105
+ _mm_storeu_pd(tmp, _mm_add_pd(s0, s1));
106
+ sum = tmp[0] + tmp[1];
107
+ #endif
108
+ for (; j < dim; j++)
77
109
  sum += (double)vec[j] * (double)vec[j];
110
+ return sum;
111
+ }
112
+
113
+ void se_l2_normalize(float *vec, uint32_t dim) {
114
+ double sum = sum_sq_d(vec, dim);
78
115
  if (sum <= 0.0)
79
116
  return;
80
- float inv = (float)(1.0 / sqrt(sum));
81
- for (uint32_t j = 0; j < dim; j++)
82
- vec[j] *= inv;
117
+ scale_copy(vec, vec, dim, (float)(1.0 / sqrt(sum)));
83
118
  }
84
119
 
85
120
  static se_status_t embed_ids_core(const se_model_t *model, se_scratch_t *sc, const uint32_t *ids,
@@ -128,8 +163,12 @@ static se_status_t embed_ids_core(const se_model_t *model, se_scratch_t *sc, con
128
163
  }
129
164
 
130
165
  if (model->meta.normalization_type == SE_NORMALIZATION_L2) {
131
- scale_copy(out, acc, dim, 1.0f);
132
- se_l2_normalize(out, dim);
166
+ double sum = sum_sq_d(acc, dim);
167
+ if (sum <= 0.0) {
168
+ memset(out, 0, (size_t)dim * sizeof(float));
169
+ return SE_OK;
170
+ }
171
+ scale_copy(out, acc, dim, (float)(1.0 / sqrt(sum)));
133
172
  } else {
134
173
  const float inv = 1.0f / (float)used;
135
174
  scale_copy(out, acc, dim, inv);
@@ -221,17 +221,31 @@ static float dot_product_f16_neon(const float *q, const uint8_t *row, size_t dim
221
221
  size_t j = 0;
222
222
  float32x4_t a0 = vdupq_n_f32(0.0f);
223
223
  float32x4_t a1 = vdupq_n_f32(0.0f);
224
+ float32x4_t a2 = vdupq_n_f32(0.0f);
225
+ float32x4_t a3 = vdupq_n_f32(0.0f);
226
+ for (; j + 15 < dim; j += 16) {
227
+ float16x8_t h0 =
228
+ vreinterpretq_f16_u16(vld1q_u16((const uint16_t *)(const void *)(row + j * 2)));
229
+ float16x8_t h1 =
230
+ vreinterpretq_f16_u16(vld1q_u16((const uint16_t *)(const void *)(row + (j + 8) * 2)));
231
+ float32x4_t r0 = vcvt_f32_f16(vget_low_f16(h0));
232
+ float32x4_t r1 = vcvt_f32_f16(vget_high_f16(h0));
233
+ float32x4_t r2 = vcvt_f32_f16(vget_low_f16(h1));
234
+ float32x4_t r3 = vcvt_f32_f16(vget_high_f16(h1));
235
+ a0 = vmlaq_f32(a0, vld1q_f32(q + j), r0);
236
+ a1 = vmlaq_f32(a1, vld1q_f32(q + j + 4), r1);
237
+ a2 = vmlaq_f32(a2, vld1q_f32(q + j + 8), r2);
238
+ a3 = vmlaq_f32(a3, vld1q_f32(q + j + 12), r3);
239
+ }
224
240
  for (; j + 7 < dim; j += 8) {
225
241
  float16x4_t h0 =
226
242
  vreinterpret_f16_u16(vld1_u16((const uint16_t *)(const void *)(row + j * 2)));
227
243
  float16x4_t h1 =
228
244
  vreinterpret_f16_u16(vld1_u16((const uint16_t *)(const void *)(row + (j + 4) * 2)));
229
- float32x4_t r0 = vcvt_f32_f16(h0);
230
- float32x4_t r1 = vcvt_f32_f16(h1);
231
- a0 = vmlaq_f32(a0, vld1q_f32(q + j), r0);
232
- a1 = vmlaq_f32(a1, vld1q_f32(q + j + 4), r1);
245
+ a0 = vmlaq_f32(a0, vld1q_f32(q + j), vcvt_f32_f16(h0));
246
+ a1 = vmlaq_f32(a1, vld1q_f32(q + j + 4), vcvt_f32_f16(h1));
233
247
  }
234
- float32x4_t sumv = vaddq_f32(a0, a1);
248
+ float32x4_t sumv = vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3));
235
249
  float dot = vaddvq_f32(sumv);
236
250
  for (; j < dim; j++)
237
251
  dot += q[j] * se_f16_bits_to_float(read_u16le(row + j * 2));
@@ -243,8 +257,30 @@ static float dot_and_row_sq_f16_neon(const float *q, const uint8_t *row, size_t
243
257
  size_t j = 0;
244
258
  float32x4_t d0 = vdupq_n_f32(0.0f);
245
259
  float32x4_t d1 = vdupq_n_f32(0.0f);
260
+ float32x4_t d2 = vdupq_n_f32(0.0f);
261
+ float32x4_t d3 = vdupq_n_f32(0.0f);
246
262
  float32x4_t s0 = vdupq_n_f32(0.0f);
247
263
  float32x4_t s1 = vdupq_n_f32(0.0f);
264
+ float32x4_t s2 = vdupq_n_f32(0.0f);
265
+ float32x4_t s3 = vdupq_n_f32(0.0f);
266
+ for (; j + 15 < dim; j += 16) {
267
+ float16x8_t h0 =
268
+ vreinterpretq_f16_u16(vld1q_u16((const uint16_t *)(const void *)(row + j * 2)));
269
+ float16x8_t h1 =
270
+ vreinterpretq_f16_u16(vld1q_u16((const uint16_t *)(const void *)(row + (j + 8) * 2)));
271
+ float32x4_t r0 = vcvt_f32_f16(vget_low_f16(h0));
272
+ float32x4_t r1 = vcvt_f32_f16(vget_high_f16(h0));
273
+ float32x4_t r2 = vcvt_f32_f16(vget_low_f16(h1));
274
+ float32x4_t r3 = vcvt_f32_f16(vget_high_f16(h1));
275
+ d0 = vmlaq_f32(d0, vld1q_f32(q + j), r0);
276
+ d1 = vmlaq_f32(d1, vld1q_f32(q + j + 4), r1);
277
+ d2 = vmlaq_f32(d2, vld1q_f32(q + j + 8), r2);
278
+ d3 = vmlaq_f32(d3, vld1q_f32(q + j + 12), r3);
279
+ s0 = vmlaq_f32(s0, r0, r0);
280
+ s1 = vmlaq_f32(s1, r1, r1);
281
+ s2 = vmlaq_f32(s2, r2, r2);
282
+ s3 = vmlaq_f32(s3, r3, r3);
283
+ }
248
284
  for (; j + 7 < dim; j += 8) {
249
285
  float16x4_t h0 =
250
286
  vreinterpret_f16_u16(vld1_u16((const uint16_t *)(const void *)(row + j * 2)));
@@ -257,8 +293,8 @@ static float dot_and_row_sq_f16_neon(const float *q, const uint8_t *row, size_t
257
293
  s0 = vmlaq_f32(s0, r0, r0);
258
294
  s1 = vmlaq_f32(s1, r1, r1);
259
295
  }
260
- float dot = vaddvq_f32(vaddq_f32(d0, d1));
261
- float row_sq = vaddvq_f32(vaddq_f32(s0, s1));
296
+ float dot = vaddvq_f32(vaddq_f32(vaddq_f32(d0, d1), vaddq_f32(d2, d3)));
297
+ float row_sq = vaddvq_f32(vaddq_f32(vaddq_f32(s0, s1), vaddq_f32(s2, s3)));
262
298
  for (; j < dim; j++) {
263
299
  float r = se_f16_bits_to_float(read_u16le(row + j * 2));
264
300
  dot += q[j] * r;
@@ -61,6 +61,34 @@ static uint32_t hash_bytes_continue(uint32_t h, const uint8_t *data, size_t len)
61
61
  return h;
62
62
  }
63
63
 
64
+ static int se_memeq(const uint8_t *a, const uint8_t *b, size_t n) {
65
+ while (n >= 8) {
66
+ uint64_t ua, ub;
67
+ memcpy(&ua, a, 8);
68
+ memcpy(&ub, b, 8);
69
+ if (ua != ub)
70
+ return 0;
71
+ a += 8;
72
+ b += 8;
73
+ n -= 8;
74
+ }
75
+ if (n >= 4) {
76
+ uint32_t ua, ub;
77
+ memcpy(&ua, a, 4);
78
+ memcpy(&ub, b, 4);
79
+ if (ua != ub)
80
+ return 0;
81
+ a += 4;
82
+ b += 4;
83
+ n -= 4;
84
+ }
85
+ while (n--) {
86
+ if (*a++ != *b++)
87
+ return 0;
88
+ }
89
+ return 1;
90
+ }
91
+
64
92
  int se_vocab_lookup_piece(const se_model_t *model, const uint8_t *prefix, size_t prefix_len,
65
93
  const uint8_t *bytes, size_t len, uint32_t *id_out) {
66
94
  if (prefix_len > UINT32_MAX || len > UINT32_MAX || prefix_len > UINT32_MAX - len)
@@ -79,9 +107,9 @@ int se_vocab_lookup_piece(const se_model_t *model, const uint8_t *prefix, size_t
79
107
  if (slot->token_id == SE_SLOT_EMPTY)
80
108
  return 0;
81
109
  if (slot->hash == h && slot->str_len == total_len) {
82
- const char *token = model->vocab_strings + slot->str_off;
83
- if ((prefix_len == 0 || memcmp(token, prefix, prefix_len) == 0) &&
84
- (len == 0 || memcmp(token + prefix_len, bytes, len) == 0)) {
110
+ const uint8_t *token = (const uint8_t *)(model->vocab_strings + slot->str_off);
111
+ if ((prefix_len == 0 || se_memeq(token, prefix, prefix_len)) &&
112
+ (len == 0 || se_memeq(token + prefix_len, bytes, len))) {
85
113
  *id_out = slot->token_id;
86
114
  return 1;
87
115
  }
@@ -103,6 +131,17 @@ static inline uint32_t trie_find_child(const se_trie_t *trie, const se_trie_node
103
131
  if (count == 0)
104
132
  return SE_SLOT_EMPTY;
105
133
 
134
+ if (count == 1)
135
+ return trie->edges[start].byte == byte ? trie->edges[start].child : SE_SLOT_EMPTY;
136
+
137
+ if (count == 2) {
138
+ if (trie->edges[start].byte == byte)
139
+ return trie->edges[start].child;
140
+ if (trie->edges[start + 1u].byte == byte)
141
+ return trie->edges[start + 1u].child;
142
+ return SE_SLOT_EMPTY;
143
+ }
144
+
106
145
  if (count <= 8) {
107
146
  for (uint32_t i = 0; i < count; i++) {
108
147
  const se_trie_edge_t *edge = &trie->edges[start + i];
@@ -342,6 +381,37 @@ static se_status_t validate_norm_ranges(const se_range_t *ranges, uint32_t count
342
381
  return SE_OK;
343
382
  }
344
383
 
384
+ static void fill_map256(const se_map_entry_t **out, const se_map_entry_t *entries, uint32_t count) {
385
+ memset(out, 0, 256 * sizeof(*out));
386
+ for (uint32_t i = 0; i < count; i++) {
387
+ if (entries[i].cp < 256)
388
+ out[entries[i].cp] = &entries[i];
389
+ }
390
+ }
391
+
392
+ static void fill_range256(uint8_t *out, const se_range_t *ranges, uint32_t count) {
393
+ memset(out, 0, 256);
394
+ for (uint32_t i = 0; i < count; i++) {
395
+ uint32_t lo = ranges[i].lo;
396
+ uint32_t hi = ranges[i].hi;
397
+ if (lo > 255)
398
+ continue;
399
+ if (hi > 255)
400
+ hi = 255;
401
+ for (uint32_t cp = lo; cp <= hi; cp++)
402
+ out[cp] = 1;
403
+ }
404
+ }
405
+
406
+ static void prepare_norm_fast_tables(se_model_t *model) {
407
+ fill_map256(model->norm.lower256, model->norm.lower, model->norm.lower_count);
408
+ fill_map256(model->norm.nfd256, model->norm.nfd, model->norm.nfd_count);
409
+ fill_range256(model->norm.mn256, model->norm.mn, model->norm.mn_count);
410
+ fill_range256(model->norm.punct256, model->norm.punct, model->norm.punct_count);
411
+ fill_range256(model->norm.control256, model->norm.control, model->norm.control_count);
412
+ fill_range256(model->norm.whitespace256, model->norm.whitespace, model->norm.whitespace_count);
413
+ }
414
+
345
415
  static se_status_t parse_norm_tables(se_model_t *model, const uint8_t *base, struct section sec,
346
416
  se_error_t *err) {
347
417
  memset(&model->norm, 0, sizeof(model->norm));
@@ -417,8 +487,13 @@ static se_status_t parse_norm_tables(se_model_t *model, const uint8_t *base, str
417
487
  rc = validate_norm_ranges(model->norm.control, model->norm.control_count, "control", err);
418
488
  if (rc != SE_OK)
419
489
  return rc;
420
- return validate_norm_ranges(model->norm.whitespace, model->norm.whitespace_count, "whitespace",
421
- err);
490
+ rc = validate_norm_ranges(model->norm.whitespace, model->norm.whitespace_count, "whitespace",
491
+ err);
492
+ if (rc != SE_OK)
493
+ return rc;
494
+
495
+ prepare_norm_fast_tables(model);
496
+ return SE_OK;
422
497
  }
423
498
 
424
499
  static se_status_t validate_vocab_hash(const se_model_t *model, struct section vocab_strings,
@@ -136,6 +136,12 @@ typedef struct {
136
136
  uint32_t control_count;
137
137
  const se_range_t *whitespace;
138
138
  uint32_t whitespace_count;
139
+ const se_map_entry_t *lower256[256];
140
+ const se_map_entry_t *nfd256[256];
141
+ uint8_t mn256[256];
142
+ uint8_t punct256[256];
143
+ uint8_t control256[256];
144
+ uint8_t whitespace256[256];
139
145
  } se_norm_tables_t;
140
146
 
141
147
  typedef struct {
@@ -382,6 +388,9 @@ static inline int se_is_ascii_boundary(uint32_t cp) {
382
388
  void se_scratch_init(se_scratch_t *s);
383
389
  void se_scratch_free(se_scratch_t *s);
384
390
  int se_scratch_reserve(se_scratch_t *s, uint32_t dim);
391
+ se_scratch_t *se_scratch_acquire(uint32_t dim);
392
+ void se_scratch_release(se_scratch_t *s);
393
+ void se_scratch_drop_thread(void);
385
394
 
386
395
  size_t se_prefix_boundary_len(const se_model_t *model, const uint8_t *input, size_t input_len,
387
396
  size_t target, size_t backscan);
@@ -4,6 +4,18 @@
4
4
  #include <stdlib.h>
5
5
  #include <string.h>
6
6
 
7
+ #ifndef _WIN32
8
+ #include <pthread.h>
9
+ #else
10
+ #ifndef WIN32_LEAN_AND_MEAN
11
+ #define WIN32_LEAN_AND_MEAN
12
+ #endif
13
+ #ifndef _WIN32_WINNT
14
+ #define _WIN32_WINNT 0x0600
15
+ #endif
16
+ #include <windows.h>
17
+ #endif
18
+
7
19
  #define SE_CANCEL_CHECK_MASK 0x3ffu
8
20
 
9
21
  void se_scratch_init(se_scratch_t *s) {
@@ -75,14 +87,18 @@ static int push_id(se_scratch_t *sc, size_t *n_ids, uint32_t id) {
75
87
  return 1;
76
88
  }
77
89
 
90
+ #define SE_SCRATCH_CPS_KEEP 256
91
+ #define SE_SCRATCH_IDS_KEEP 512
92
+ #define SE_SCRATCH_BYTES_KEEP 1024
93
+
78
94
  int se_scratch_reserve(se_scratch_t *s, uint32_t dim) {
79
- if (!grow_u32(&s->cps, &s->cps_cap, 256))
95
+ if (!grow_u32(&s->cps, &s->cps_cap, SE_SCRATCH_CPS_KEEP))
80
96
  return 0;
81
- if (!grow_u32(&s->cps2, &s->cps2_cap, 256))
97
+ if (!grow_u32(&s->cps2, &s->cps2_cap, SE_SCRATCH_CPS_KEEP))
82
98
  return 0;
83
- if (!grow_u32(&s->ids, &s->ids_cap, 256))
99
+ if (!grow_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_KEEP))
84
100
  return 0;
85
- if (!grow_bytes(&s->bytes, &s->bytes_cap, 256))
101
+ if (!grow_bytes(&s->bytes, &s->bytes_cap, SE_SCRATCH_BYTES_KEEP))
86
102
  return 0;
87
103
  if (!grow_float(&s->acc, &s->acc_cap, dim))
88
104
  return 0;
@@ -90,21 +106,223 @@ int se_scratch_reserve(se_scratch_t *s, uint32_t dim) {
90
106
  return 1;
91
107
  }
92
108
 
109
+ static int shrink_u32(uint32_t **buf, size_t *cap, size_t keep) {
110
+ size_t bytes = 0;
111
+ void *p;
112
+
113
+ if (*cap <= keep)
114
+ return 1;
115
+ if (!*buf) {
116
+ *cap = 0;
117
+ return 1;
118
+ }
119
+ if (!se_array_bytes(keep, sizeof(uint32_t), &bytes))
120
+ return 0;
121
+ p = se_realloc(SE_ALLOC_SCRATCH, *buf, bytes);
122
+ if (!p)
123
+ return 0;
124
+ *buf = (uint32_t *)p;
125
+ *cap = keep;
126
+ return 1;
127
+ }
128
+
129
+ static int shrink_bytes(uint8_t **buf, size_t *cap, size_t keep) {
130
+ size_t bytes = 0;
131
+ void *p;
132
+
133
+ if (*cap <= keep)
134
+ return 1;
135
+ if (!*buf) {
136
+ *cap = 0;
137
+ return 1;
138
+ }
139
+ if (!se_array_bytes(keep, sizeof(uint8_t), &bytes))
140
+ return 0;
141
+ p = se_realloc(SE_ALLOC_SCRATCH, *buf, bytes);
142
+ if (!p)
143
+ return 0;
144
+ *buf = (uint8_t *)p;
145
+ *cap = keep;
146
+ return 1;
147
+ }
148
+
149
+ static void se_scratch_trim(se_scratch_t *s) {
150
+ (void)shrink_u32(&s->cps, &s->cps_cap, SE_SCRATCH_CPS_KEEP);
151
+ (void)shrink_u32(&s->cps2, &s->cps2_cap, SE_SCRATCH_CPS_KEEP);
152
+ (void)shrink_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_KEEP);
153
+ (void)shrink_bytes(&s->bytes, &s->bytes_cap, SE_SCRATCH_BYTES_KEEP);
154
+ }
155
+
156
+ typedef struct {
157
+ se_scratch_t scratch;
158
+ int in_use;
159
+ } se_tls_scratch_t;
160
+
161
+ static void se_scratch_tls_dtor(void *p) {
162
+ se_tls_scratch_t *tls = (se_tls_scratch_t *)p;
163
+ if (!tls)
164
+ return;
165
+ se_scratch_free(&tls->scratch);
166
+ se_free(tls);
167
+ }
168
+
169
+ #ifndef _WIN32
170
+ static pthread_key_t se_scratch_key;
171
+ static pthread_once_t se_scratch_once = PTHREAD_ONCE_INIT;
172
+
173
+ static void se_scratch_key_init(void) {
174
+ (void)pthread_key_create(&se_scratch_key, se_scratch_tls_dtor);
175
+ }
176
+
177
+ static se_tls_scratch_t *se_scratch_tls_get(void) {
178
+ (void)pthread_once(&se_scratch_once, se_scratch_key_init);
179
+ return (se_tls_scratch_t *)pthread_getspecific(se_scratch_key);
180
+ }
181
+
182
+ static se_tls_scratch_t *se_scratch_tls_slot(void) {
183
+ se_tls_scratch_t *tls = se_scratch_tls_get();
184
+ if (tls)
185
+ return tls;
186
+
187
+ tls = (se_tls_scratch_t *)se_malloc(SE_ALLOC_SCRATCH, sizeof(*tls));
188
+ if (!tls)
189
+ return NULL;
190
+ memset(tls, 0, sizeof(*tls));
191
+ se_scratch_init(&tls->scratch);
192
+ if (pthread_setspecific(se_scratch_key, tls) != 0) {
193
+ se_free(tls);
194
+ return NULL;
195
+ }
196
+ return tls;
197
+ }
198
+
199
+ static void se_scratch_tls_clear(void) {
200
+ se_tls_scratch_t *tls = se_scratch_tls_get();
201
+ if (!tls)
202
+ return;
203
+ (void)pthread_setspecific(se_scratch_key, NULL);
204
+ se_scratch_tls_dtor(tls);
205
+ }
206
+ #else
207
+ static DWORD se_fls_index = FLS_OUT_OF_INDEXES;
208
+ static INIT_ONCE se_fls_once = INIT_ONCE_STATIC_INIT;
209
+
210
+ static VOID WINAPI se_scratch_fls_dtor(PVOID p) {
211
+ se_scratch_tls_dtor(p);
212
+ }
213
+
214
+ static BOOL CALLBACK se_scratch_fls_init(PINIT_ONCE once, PVOID param, PVOID *ctx) {
215
+ (void)once;
216
+ (void)param;
217
+ (void)ctx;
218
+ se_fls_index = FlsAlloc(se_scratch_fls_dtor);
219
+ return se_fls_index != FLS_OUT_OF_INDEXES;
220
+ }
221
+
222
+ static se_tls_scratch_t *se_scratch_tls_get(void) {
223
+ if (se_fls_index == FLS_OUT_OF_INDEXES)
224
+ return NULL;
225
+ return (se_tls_scratch_t *)FlsGetValue(se_fls_index);
226
+ }
227
+
228
+ static se_tls_scratch_t *se_scratch_tls_slot(void) {
229
+ if (!InitOnceExecuteOnce(&se_fls_once, se_scratch_fls_init, NULL, NULL) ||
230
+ se_fls_index == FLS_OUT_OF_INDEXES)
231
+ return NULL;
232
+
233
+ se_tls_scratch_t *tls = se_scratch_tls_get();
234
+ if (tls)
235
+ return tls;
236
+
237
+ tls = (se_tls_scratch_t *)se_malloc(SE_ALLOC_SCRATCH, sizeof(*tls));
238
+ if (!tls)
239
+ return NULL;
240
+ memset(tls, 0, sizeof(*tls));
241
+ se_scratch_init(&tls->scratch);
242
+ if (!FlsSetValue(se_fls_index, tls)) {
243
+ se_free(tls);
244
+ return NULL;
245
+ }
246
+ return tls;
247
+ }
248
+
249
+ static void se_scratch_tls_clear(void) {
250
+ se_tls_scratch_t *tls = se_scratch_tls_get();
251
+ if (!tls)
252
+ return;
253
+ (void)FlsSetValue(se_fls_index, NULL);
254
+ se_scratch_tls_dtor(tls);
255
+ }
256
+ #endif
257
+
258
+ se_scratch_t *se_scratch_acquire(uint32_t dim) {
259
+ se_tls_scratch_t *tls = se_scratch_tls_slot();
260
+ if (!tls)
261
+ return NULL;
262
+
263
+ if (!tls->in_use) {
264
+ if (!se_scratch_reserve(&tls->scratch, dim))
265
+ return NULL;
266
+ tls->in_use = 1;
267
+ return &tls->scratch;
268
+ }
269
+
270
+ se_scratch_t *heap = (se_scratch_t *)se_malloc(SE_ALLOC_SCRATCH, sizeof(*heap));
271
+ if (!heap)
272
+ return NULL;
273
+ se_scratch_init(heap);
274
+ if (!se_scratch_reserve(heap, dim)) {
275
+ se_scratch_free(heap);
276
+ se_free(heap);
277
+ return NULL;
278
+ }
279
+ return heap;
280
+ }
281
+
282
+ void se_scratch_release(se_scratch_t *s) {
283
+ se_tls_scratch_t *tls;
284
+
285
+ if (!s)
286
+ return;
287
+
288
+ tls = se_scratch_tls_get();
289
+ if (tls && s == &tls->scratch) {
290
+ tls->in_use = 0;
291
+ se_scratch_trim(&tls->scratch);
292
+ return;
293
+ }
294
+ se_scratch_free(s);
295
+ se_free(s);
296
+ }
297
+
298
+ void se_scratch_drop_thread(void) {
299
+ se_tls_scratch_t *tls = se_scratch_tls_get();
300
+ if (tls && tls->in_use)
301
+ tls->in_use = 0;
302
+ se_scratch_tls_clear();
303
+ }
304
+
93
305
  static int is_whitespace(const se_model_t *m, uint32_t cp) {
94
306
  if (cp < 0x80)
95
307
  return se_is_ascii_whitespace(cp);
308
+ if (cp < 256)
309
+ return m->norm.whitespace256[cp];
96
310
  return se_range_contains(m->norm.whitespace, m->norm.whitespace_count, cp);
97
311
  }
98
312
 
99
313
  static int is_control(const se_model_t *m, uint32_t cp) {
100
314
  if (cp < 0x80)
101
315
  return (cp < 0x20 || cp == 0x7f) && cp != '\t' && cp != '\n' && cp != '\r';
316
+ if (cp < 256)
317
+ return m->norm.control256[cp];
102
318
  return se_range_contains(m->norm.control, m->norm.control_count, cp);
103
319
  }
104
320
 
105
321
  static int is_punct(const se_model_t *m, uint32_t cp) {
106
322
  if (cp < 0x80)
107
323
  return se_is_ascii_punct(cp);
324
+ if (cp < 256)
325
+ return m->norm.punct256[cp];
108
326
  return se_range_contains(m->norm.punct, m->norm.punct_count, cp);
109
327
  }
110
328
 
@@ -154,12 +372,18 @@ static int decode_one(const uint8_t *src, size_t len, size_t *i, uint32_t *cp_ou
154
372
  static int normalization_stable(const se_model_t *m, uint32_t cp) {
155
373
  if (cp < 0x80)
156
374
  return 1;
157
- if (m->meta.do_lower_case && se_map_lookup(m->norm.lower, m->norm.lower_count, cp))
158
- return 0;
375
+ if (m->meta.do_lower_case) {
376
+ const se_map_entry_t *lower =
377
+ cp < 256 ? m->norm.lower256[cp] : se_map_lookup(m->norm.lower, m->norm.lower_count, cp);
378
+ if (lower)
379
+ return 0;
380
+ }
159
381
  if (m->meta.strip_accents) {
160
- if (se_map_lookup(m->norm.nfd, m->norm.nfd_count, cp))
382
+ const se_map_entry_t *nfd =
383
+ cp < 256 ? m->norm.nfd256[cp] : se_map_lookup(m->norm.nfd, m->norm.nfd_count, cp);
384
+ if (nfd)
161
385
  return 0;
162
- if (se_range_contains(m->norm.mn, m->norm.mn_count, cp))
386
+ if (cp < 256 ? m->norm.mn256[cp] : se_range_contains(m->norm.mn, m->norm.mn_count, cp))
163
387
  return 0;
164
388
  }
165
389
  return 1;
@@ -233,6 +457,7 @@ typedef struct {
233
457
  size_t n_ids;
234
458
  size_t n_unk;
235
459
  size_t segment_len;
460
+ int segment_ascii;
236
461
  se_token_stats_t *stats;
237
462
  se_error_t *err;
238
463
  volatile sig_atomic_t *cancelled;
@@ -248,6 +473,8 @@ static se_status_t oom(token_state_t *st, const char *where) {
248
473
  }
249
474
 
250
475
  static int append_segment_cp(token_state_t *st, uint32_t cp) {
476
+ if (cp >= 0x80)
477
+ st->segment_ascii = 0;
251
478
  if (!grow_u32(&st->scratch->cps, &st->scratch->cps_cap, st->segment_len + 1))
252
479
  return 0;
253
480
  st->scratch->cps[st->segment_len++] = cp;
@@ -272,7 +499,8 @@ static int cap_after_append(token_state_t *st) {
272
499
  typedef enum { WORDPIECE_OK = 1, WORDPIECE_OOM = 0, WORDPIECE_INVALID = -1 } wordpiece_status_t;
273
500
 
274
501
  static wordpiece_status_t wordpiece(const se_model_t *m, se_scratch_t *sc, const uint32_t *word,
275
- size_t word_len, size_t *n_ids, size_t *n_unk) {
502
+ size_t word_len, size_t *n_ids, size_t *n_unk,
503
+ int known_ascii) {
276
504
  const se_meta_t *meta = &m->meta;
277
505
 
278
506
  if (word_len == 0)
@@ -285,30 +513,49 @@ static wordpiece_status_t wordpiece(const se_model_t *m, se_scratch_t *sc, const
285
513
  return 1;
286
514
  }
287
515
 
288
- size_t bytes_need = 0;
289
- if (!se_checked_mul_size(word_len, 4, &bytes_need) ||
290
- !se_checked_add_size(bytes_need, 4, &bytes_need))
291
- return 0;
292
- if (!grow_bytes(&sc->bytes, &sc->bytes_cap, bytes_need))
293
- return 0;
294
- if (!grow_u32(&sc->cps2, &sc->cps2_cap, word_len + 1))
295
- return 0;
516
+ int ascii_identity = known_ascii;
517
+ if (!ascii_identity) {
518
+ ascii_identity = 1;
519
+ for (size_t k = 0; k < word_len; k++) {
520
+ if (word[k] >= 0x80) {
521
+ ascii_identity = 0;
522
+ break;
523
+ }
524
+ }
525
+ }
296
526
 
297
527
  size_t blen = 0;
298
- for (size_t k = 0; k < word_len; k++) {
299
- if (blen > UINT32_MAX)
528
+ if (ascii_identity) {
529
+ if (!grow_bytes(&sc->bytes, &sc->bytes_cap, word_len))
530
+ return 0;
531
+ if (!grow_u32(&sc->cps2, &sc->cps2_cap, word_len + 1))
300
532
  return 0;
301
- sc->cps2[k] = (uint32_t)blen;
302
- uint32_t cp = word[k];
303
- if (cp < 0x80) {
304
- sc->bytes[blen++] = (uint8_t)cp;
305
- } else {
306
- blen += se_utf8_encode(cp, sc->bytes + blen);
533
+ for (size_t k = 0; k < word_len; k++) {
534
+ sc->bytes[k] = (uint8_t)word[k];
535
+ sc->cps2[k] = (uint32_t)k;
307
536
  }
537
+ blen = word_len;
538
+ sc->cps2[word_len] = (uint32_t)word_len;
539
+ } else {
540
+ size_t bytes_need = 0;
541
+ if (!se_checked_mul_size(word_len, 4, &bytes_need) ||
542
+ !se_checked_add_size(bytes_need, 4, &bytes_need))
543
+ return 0;
544
+ if (!grow_bytes(&sc->bytes, &sc->bytes_cap, bytes_need))
545
+ return 0;
546
+ if (!grow_u32(&sc->cps2, &sc->cps2_cap, word_len + 1))
547
+ return 0;
548
+
549
+ for (size_t k = 0; k < word_len; k++) {
550
+ if (blen > UINT32_MAX)
551
+ return 0;
552
+ sc->cps2[k] = (uint32_t)blen;
553
+ blen += se_utf8_encode(word[k], sc->bytes + blen);
554
+ }
555
+ if (blen > UINT32_MAX)
556
+ return 0;
557
+ sc->cps2[word_len] = (uint32_t)blen;
308
558
  }
309
- if (blen > UINT32_MAX)
310
- return 0;
311
- sc->cps2[word_len] = (uint32_t)blen;
312
559
 
313
560
  if (word_len <= meta->max_token_chars) {
314
561
  uint32_t exact_id = 0;
@@ -358,7 +605,8 @@ static wordpiece_status_t wordpiece(const se_model_t *m, se_scratch_t *sc, const
358
605
  static se_status_t append_wordpiece(token_state_t *st, const uint32_t *word, size_t word_len,
359
606
  int *stop) {
360
607
  wordpiece_status_t wp =
361
- wordpiece(st->model, st->scratch, word, word_len, &st->n_ids, &st->n_unk);
608
+ wordpiece(st->model, st->scratch, word, word_len, &st->n_ids, &st->n_unk,
609
+ word_len == 1 ? (word[0] < 0x80) : st->segment_ascii);
362
610
  if (wp == WORDPIECE_OOM)
363
611
  return oom(st, "tokenizing");
364
612
  if (wp == WORDPIECE_INVALID) {
@@ -375,6 +623,7 @@ static se_status_t flush_segment(token_state_t *st, int *stop) {
375
623
  return SE_OK;
376
624
  se_status_t rc = append_wordpiece(st, st->scratch->cps, st->segment_len, stop);
377
625
  st->segment_len = 0;
626
+ st->segment_ascii = 1;
378
627
  return rc;
379
628
  }
380
629
 
@@ -394,13 +643,30 @@ static se_status_t feed_token_cp(token_state_t *st, uint32_t cp, int *stop) {
394
643
  return SE_OK;
395
644
  }
396
645
 
646
+ static const se_map_entry_t *lower_entry(const se_model_t *m, uint32_t cp) {
647
+ if (cp < 256)
648
+ return m->norm.lower256[cp];
649
+ return se_map_lookup(m->norm.lower, m->norm.lower_count, cp);
650
+ }
651
+
652
+ static const se_map_entry_t *nfd_entry(const se_model_t *m, uint32_t cp) {
653
+ if (cp < 256)
654
+ return m->norm.nfd256[cp];
655
+ return se_map_lookup(m->norm.nfd, m->norm.nfd_count, cp);
656
+ }
657
+
658
+ static int is_mn(const se_model_t *m, uint32_t cp) {
659
+ if (cp < 256)
660
+ return m->norm.mn256[cp];
661
+ return se_range_contains(m->norm.mn, m->norm.mn_count, cp);
662
+ }
663
+
397
664
  static se_status_t emit_lowered(token_state_t *st, uint32_t cp, int *stop) {
398
665
  if (st->model->meta.do_lower_case) {
399
666
  if (cp >= 'A' && cp <= 'Z')
400
667
  cp += 32;
401
668
  else if (cp >= 0x80) {
402
- const se_map_entry_t *e =
403
- se_map_lookup(st->model->norm.lower, st->model->norm.lower_count, cp);
669
+ const se_map_entry_t *e = lower_entry(st->model, cp);
404
670
  if (e) {
405
671
  for (uint32_t k = 0; k < e->len; k++) {
406
672
  se_status_t rc = feed_token_cp(st, e->out[k], stop);
@@ -418,16 +684,16 @@ static se_status_t emit_stripped(token_state_t *st, uint32_t cp, int *stop) {
418
684
  if (!st->model->meta.strip_accents || cp < 0x80)
419
685
  return emit_lowered(st, cp, stop);
420
686
 
421
- const se_map_entry_t *e = se_map_lookup(st->model->norm.nfd, st->model->norm.nfd_count, cp);
687
+ const se_map_entry_t *e = nfd_entry(st->model, cp);
422
688
  if (!e) {
423
- if (se_range_contains(st->model->norm.mn, st->model->norm.mn_count, cp))
689
+ if (is_mn(st->model, cp))
424
690
  return SE_OK;
425
691
  return emit_lowered(st, cp, stop);
426
692
  }
427
693
 
428
694
  for (uint32_t k = 0; k < e->len; k++) {
429
695
  uint32_t d = e->out[k];
430
- if (se_range_contains(st->model->norm.mn, st->model->norm.mn_count, d))
696
+ if (is_mn(st->model, d))
431
697
  continue;
432
698
  se_status_t rc = emit_lowered(st, d, stop);
433
699
  if (rc != SE_OK || *stop)
@@ -557,6 +823,7 @@ se_status_t se_tokenize(const se_model_t *model, se_scratch_t *sc, const uint8_t
557
823
  st.stats = stats;
558
824
  st.err = err;
559
825
  st.cancelled = cancelled;
826
+ st.segment_ascii = 1;
560
827
 
561
828
  size_t i = 0;
562
829
  size_t iterations = 0;
@@ -274,10 +274,14 @@ static void batch_worker_run(batch_job_t *job, se_scratch_t *scratch) {
274
274
 
275
275
  static void *batch_execute(void *arg) {
276
276
  batch_job_t *job = (batch_job_t *)arg;
277
- se_scratch_t scratch;
278
- se_scratch_init(&scratch);
279
- batch_worker_run(job, &scratch);
280
- se_scratch_free(&scratch);
277
+ se_scratch_t *scratch = se_scratch_acquire(job->model->meta.dim);
278
+ if (!scratch) {
279
+ se_error_set(&job->error, SE_ERR_OOM, "out of memory while sizing scratch buffers");
280
+ job->failed = 1;
281
+ return NULL;
282
+ }
283
+ batch_worker_run(job, scratch);
284
+ se_scratch_release(scratch);
281
285
  return NULL;
282
286
  }
283
287
 
@@ -928,6 +932,32 @@ static VALUE embed_one_via_batch(VALUE self, VALUE text, VALUE max_tokens_opt,
928
932
  return embed_texts_internal(self, text, 0, 1, max_tokens_opt, format, validation, stats);
929
933
  }
930
934
 
935
+ typedef struct {
936
+ se_scratch_t *scratch;
937
+ const se_model_t *model;
938
+ const uint8_t *input;
939
+ size_t input_len;
940
+ uint32_t max_tokens;
941
+ float *out;
942
+ se_token_stats_t *stats;
943
+ se_error_t err;
944
+ se_status_t rc;
945
+ } embed_one_scratch_job_t;
946
+
947
+ static VALUE embed_one_scratch_body(VALUE arg) {
948
+ embed_one_scratch_job_t *job = (embed_one_scratch_job_t *)(uintptr_t)arg;
949
+ job->rc = se_embed_one(job->model, job->scratch, job->input, job->input_len, job->max_tokens,
950
+ job->out, job->stats, &job->err, NULL);
951
+ return Qnil;
952
+ }
953
+
954
+ static VALUE embed_one_scratch_ensure(VALUE arg) {
955
+ embed_one_scratch_job_t *job = (embed_one_scratch_job_t *)(uintptr_t)arg;
956
+ se_scratch_release(job->scratch);
957
+ job->scratch = NULL;
958
+ return Qnil;
959
+ }
960
+
931
961
  static VALUE embed_one_value(VALUE self, VALUE text, VALUE max_tokens_opt,
932
962
  se_vector_format_t format, se_encoding_validation_t validation,
933
963
  se_token_stats_t *stats) {
@@ -947,25 +977,28 @@ static VALUE embed_one_value(VALUE self, VALUE text, VALUE max_tokens_opt,
947
977
  rb_enc_associate(result, binary_encoding);
948
978
  float *out = (float *)RSTRING_PTR(result);
949
979
 
950
- se_scratch_t scratch;
951
- se_scratch_init(&scratch);
952
- if (!se_scratch_reserve(&scratch, dim)) {
953
- se_scratch_free(&scratch);
980
+ se_token_stats_t local_stats;
981
+ embed_one_scratch_job_t job;
982
+ memset(&job, 0, sizeof(job));
983
+ job.model = &w->model;
984
+ job.input = (const uint8_t *)RSTRING_PTR(text);
985
+ job.input_len = (size_t)RSTRING_LEN(text);
986
+ job.max_tokens = resolve_max_tokens(&w->model, max_tokens_opt);
987
+ job.out = out;
988
+ job.stats = stats ? stats : &local_stats;
989
+ se_error_clear(&job.err);
990
+
991
+ job.scratch = se_scratch_acquire(dim);
992
+ if (!job.scratch)
954
993
  rb_raise(rb_eNoMemError, "out of memory");
955
- }
956
994
 
957
- se_error_t err;
958
- se_error_clear(&err);
959
- se_token_stats_t local_stats;
960
- se_status_t rc =
961
- se_embed_one(&w->model, &scratch, (const uint8_t *)RSTRING_PTR(text),
962
- (size_t)RSTRING_LEN(text), resolve_max_tokens(&w->model, max_tokens_opt), out,
963
- stats ? stats : &local_stats, &err, NULL);
964
- se_scratch_free(&scratch);
995
+ rb_ensure(embed_one_scratch_body, (VALUE)(uintptr_t)&job, embed_one_scratch_ensure,
996
+ (VALUE)(uintptr_t)&job);
965
997
  RB_GC_GUARD(text);
998
+ RB_GC_GUARD(result);
966
999
 
967
- if (rc != SE_OK)
968
- raise_se(&err);
1000
+ if (job.rc != SE_OK)
1001
+ raise_se(&job.err);
969
1002
 
970
1003
  return result;
971
1004
  }
@@ -1001,6 +1034,38 @@ static VALUE model_embed_with_stats(int argc, VALUE *argv, VALUE self) {
1001
1034
  return hash;
1002
1035
  }
1003
1036
 
1037
+ typedef struct {
1038
+ se_scratch_t *scratch;
1039
+ const se_model_t *model;
1040
+ const uint8_t *input;
1041
+ size_t input_len;
1042
+ uint32_t max_tokens;
1043
+ se_token_stats_t stats;
1044
+ se_error_t err;
1045
+ se_status_t rc;
1046
+ VALUE ids;
1047
+ } tokenize_scratch_job_t;
1048
+
1049
+ static VALUE tokenize_scratch_body(VALUE arg) {
1050
+ tokenize_scratch_job_t *job = (tokenize_scratch_job_t *)(uintptr_t)arg;
1051
+ job->rc = se_tokenize(job->model, job->scratch, job->input, job->input_len, job->max_tokens,
1052
+ &job->stats, &job->err, NULL);
1053
+ if (job->rc != SE_OK)
1054
+ return Qnil;
1055
+
1056
+ job->ids = rb_ary_new_capa((long)job->stats.token_count);
1057
+ for (uint32_t i = 0; i < job->stats.token_count; i++)
1058
+ rb_ary_push(job->ids, UINT2NUM(job->scratch->ids[i]));
1059
+ return Qnil;
1060
+ }
1061
+
1062
+ static VALUE tokenize_scratch_ensure(VALUE arg) {
1063
+ tokenize_scratch_job_t *job = (tokenize_scratch_job_t *)(uintptr_t)arg;
1064
+ se_scratch_release(job->scratch);
1065
+ job->scratch = NULL;
1066
+ return Qnil;
1067
+ }
1068
+
1004
1069
  static VALUE model_tokenize(int argc, VALUE *argv, VALUE self) {
1005
1070
  VALUE text, opts;
1006
1071
  rb_scan_args(argc, argv, "1:", &text, &opts);
@@ -1011,32 +1076,27 @@ static VALUE model_tokenize(int argc, VALUE *argv, VALUE self) {
1011
1076
  check_text_encoding_mode(
1012
1077
  text, -1, resolve_encoding_validation(lookup_option(opts, id_validate_encoding)));
1013
1078
 
1014
- uint32_t max_tokens = resolve_max_tokens(&w->model, lookup_option(opts, id_max_tokens));
1015
-
1016
- se_scratch_t scratch;
1017
- se_scratch_init(&scratch);
1018
- if (!se_scratch_reserve(&scratch, w->model.meta.dim)) {
1019
- se_scratch_free(&scratch);
1079
+ tokenize_scratch_job_t job;
1080
+ memset(&job, 0, sizeof(job));
1081
+ job.model = &w->model;
1082
+ job.input = (const uint8_t *)RSTRING_PTR(text);
1083
+ job.input_len = (size_t)RSTRING_LEN(text);
1084
+ job.max_tokens = resolve_max_tokens(&w->model, lookup_option(opts, id_max_tokens));
1085
+ job.ids = Qnil;
1086
+ se_error_clear(&job.err);
1087
+
1088
+ job.scratch = se_scratch_acquire(w->model.meta.dim);
1089
+ if (!job.scratch)
1020
1090
  rb_raise(rb_eNoMemError, "out of memory");
1021
- }
1022
1091
 
1023
- se_token_stats_t stats;
1024
- se_error_t err;
1025
- se_error_clear(&err);
1026
- se_status_t rc = se_tokenize(&w->model, &scratch, (const uint8_t *)RSTRING_PTR(text),
1027
- (size_t)RSTRING_LEN(text), max_tokens, &stats, &err, NULL);
1028
- if (rc != SE_OK) {
1029
- se_scratch_free(&scratch);
1030
- raise_se(&err);
1031
- }
1092
+ rb_ensure(tokenize_scratch_body, (VALUE)(uintptr_t)&job, tokenize_scratch_ensure,
1093
+ (VALUE)(uintptr_t)&job);
1094
+ RB_GC_GUARD(text);
1032
1095
 
1033
- VALUE ids = rb_ary_new_capa((long)stats.token_count);
1034
- for (uint32_t i = 0; i < stats.token_count; i++)
1035
- rb_ary_push(ids, UINT2NUM(scratch.ids[i]));
1096
+ if (job.rc != SE_OK)
1097
+ raise_se(&job.err);
1036
1098
 
1037
- se_scratch_free(&scratch);
1038
- RB_GC_GUARD(text);
1039
- return ids;
1099
+ return job.ids;
1040
1100
  }
1041
1101
 
1042
1102
  typedef struct {
@@ -1077,17 +1137,15 @@ static VALUE num2ull_at_value(VALUE arg) {
1077
1137
 
1078
1138
  static void *ids_execute(void *arg) {
1079
1139
  ids_job_t *job = (ids_job_t *)arg;
1080
- se_scratch_t scratch;
1081
- se_scratch_init(&scratch);
1082
- if (!se_scratch_reserve(&scratch, job->model->meta.dim)) {
1140
+ se_scratch_t *scratch = se_scratch_acquire(job->model->meta.dim);
1141
+ if (!scratch) {
1083
1142
  se_error_set(&job->error, SE_ERR_OOM, "out of memory while sizing scratch buffers");
1084
- se_scratch_free(&scratch);
1085
1143
  return NULL;
1086
1144
  }
1087
1145
  se_error_clear(&job->error);
1088
- se_embed_ids(job->model, &scratch, job->ids, job->n_ids, job->out, &job->stats, &job->error,
1146
+ se_embed_ids(job->model, scratch, job->ids, job->n_ids, job->out, &job->stats, &job->error,
1089
1147
  &job->cancelled);
1090
- se_scratch_free(&scratch);
1148
+ se_scratch_release(scratch);
1091
1149
  return NULL;
1092
1150
  }
1093
1151
 
@@ -1,3 +1,3 @@
1
1
  module StaticEmbeddings
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: static_embeddings
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov