static_embeddings 0.1.2 → 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 +4 -4
- data/CHANGELOG.md +161 -0
- data/README.md +13 -6
- 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 +12 -4
- data/docs/BENCHMARKING.md +87 -0
- data/docs/LIMITATIONS.md +93 -0
- data/docs/MODEL_AUDIT.md +27 -13
- data/docs/PERFORMANCE.md +36 -19
- data/ext/static_embeddings/se_embed.c +46 -7
- data/ext/static_embeddings/se_f16.c +43 -7
- data/ext/static_embeddings/se_format.c +83 -5
- data/ext/static_embeddings/se_internal.h +9 -0
- data/ext/static_embeddings/se_tokenizer.c +301 -34
- data/ext/static_embeddings/static_embeddings.c +157 -47
- data/lib/static_embeddings/version.rb +1 -1
- data/static_embeddings.gemspec +2 -0
- metadata +7 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Benchmarking
|
|
2
|
+
|
|
3
|
+
Two different tools, for two different questions.
|
|
4
|
+
|
|
5
|
+
`benchmark/` answers "how fast is this, and is the difference I am looking at
|
|
6
|
+
real". `samples/` answers "where does the time go" by keeping a process alive
|
|
7
|
+
for a profiler to attach to. Do not use one for the other's job.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle exec rake benchmark # everything
|
|
11
|
+
bundle exec rake benchmark:core_paths
|
|
12
|
+
bundle exec rake benchmark:gvl_threshold
|
|
13
|
+
|
|
14
|
+
BENCH_MODEL="$HOME/.cache/static_embeddings/models/potion-retrieval-32m.semb" \
|
|
15
|
+
bundle exec rake benchmark:core_paths
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Without `BENCH_MODEL` the demo model is used, which has `dim=8` and a 176-token
|
|
19
|
+
vocabulary. It proves the harness runs; it says nothing about production speed.
|
|
20
|
+
|
|
21
|
+
## Reading the output
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
case ns/op ops/sec spread% alloc/op
|
|
25
|
+
embed words=20 8540.5 117089 3.0% 1.00
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`spread_pct` is the distance between the fastest and slowest repeat. **A
|
|
29
|
+
difference smaller than `spread_pct` is not a result.** On a laptop or a shared
|
|
30
|
+
CI runner it is routinely 5-10%, which is wider than most changes worth making.
|
|
31
|
+
If you want to claim an improvement, either get the spread down or show the
|
|
32
|
+
change across several independent runs.
|
|
33
|
+
|
|
34
|
+
`alloc/op` is Ruby object allocations per operation. It moves for different
|
|
35
|
+
reasons than time does, and it is the more stable signal of the two.
|
|
36
|
+
|
|
37
|
+
Generated text inputs are primed with `valid_encoding?` before timing, so these
|
|
38
|
+
benchmarks measure the cached-coderange hot path. Fresh strings from disk or the
|
|
39
|
+
database can pay an extra whole-string UTF-8 scan; see `docs/PERFORMANCE.md`.
|
|
40
|
+
|
|
41
|
+
Iteration counts are calibrated automatically so every measurement runs for at
|
|
42
|
+
least `BENCH_MIN_SECONDS`. Knobs:
|
|
43
|
+
|
|
44
|
+
| Variable | Default | Meaning |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `BENCH_MODEL` | demo model | model to load |
|
|
47
|
+
| `BENCH_REPEATS` | 7 | repeats per case, median reported |
|
|
48
|
+
| `BENCH_MIN_SECONDS` | 0.25 | minimum duration of one repeat |
|
|
49
|
+
| `BENCH_WARMUP_SECONDS` | 0.05 | warmup before timing |
|
|
50
|
+
| `BENCH_ADAPTIVE` | 1 | set to 0 to use the literal iteration count |
|
|
51
|
+
| `BENCH_FORMAT` | table | set to `kv` for machine-readable lines |
|
|
52
|
+
|
|
53
|
+
## What the two benchmarks are for
|
|
54
|
+
|
|
55
|
+
`core_paths` covers tokenize, single embed, batch embed, pooling in isolation
|
|
56
|
+
and top-k in both formats. The `embed` and `embed rotating corpus` rows are
|
|
57
|
+
deliberately both present: the first repeats one string and keeps its matrix
|
|
58
|
+
rows in cache, the second rotates through 2000 texts and does not. The gap
|
|
59
|
+
between them is the cache effect, and quoting the first as throughput is how you
|
|
60
|
+
end up with a number nobody can reproduce.
|
|
61
|
+
|
|
62
|
+
`gvl_threshold` sweeps input sizes around `SE_GVL_UNLOCK_THRESHOLD`, the point
|
|
63
|
+
where a single `embed` stops running under the GVL and is handed to the batch
|
|
64
|
+
machinery. Look for the step, not the slope. On one x86-64 run the step cost
|
|
65
|
+
about 32% latency and four extra allocations, and it is also the point where a
|
|
66
|
+
second Ruby thread starts making progress at all. Both halves of that trade have
|
|
67
|
+
to be measured on your hardware before the constant is worth changing.
|
|
68
|
+
|
|
69
|
+
## Traps this repository has already fallen into
|
|
70
|
+
|
|
71
|
+
**Quoting noise.** An `embed_batch f16` change was reported as a 6% speedup; it
|
|
72
|
+
was inside the run-to-run spread and the next run showed the opposite sign. The
|
|
73
|
+
memory result from the same change was real and reproducible. Report the one you
|
|
74
|
+
can reproduce.
|
|
75
|
+
|
|
76
|
+
**Cache-resident working sets.** Embedding the same text in a loop keeps a tiny
|
|
77
|
+
slice of the matrix hot. Real corpora do not.
|
|
78
|
+
|
|
79
|
+
**Logical vs processed bytes.** With truncation active, a 3 MB document is
|
|
80
|
+
tokenized only until `max_tokens` is reached. Any MB/s figure computed from the
|
|
81
|
+
string length divides by bytes that were never read.
|
|
82
|
+
|
|
83
|
+
**GC state.** `GC.disable` does not stop an in-flight incremental cycle from
|
|
84
|
+
sweeping. The harness leaves GC on and reports allocations instead.
|
|
85
|
+
|
|
86
|
+
See `docs/PERFORMANCE.md` for the budget itself and where the time actually
|
|
87
|
+
goes.
|
data/docs/LIMITATIONS.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Limitations
|
|
2
|
+
|
|
3
|
+
`static_embeddings` is a runtime for one narrow thing: turning text into a
|
|
4
|
+
vector with a converted Model2Vec / potion model, and scanning a matrix of such
|
|
5
|
+
vectors. It is not a vector database, not a general embedding library, and not a
|
|
6
|
+
tokenizer toolkit.
|
|
7
|
+
|
|
8
|
+
This page is the answer to "does it do X". If X is here, the answer is no, and
|
|
9
|
+
the entry says whether that is a decision or just unfinished work.
|
|
10
|
+
|
|
11
|
+
## Models
|
|
12
|
+
|
|
13
|
+
- **Only `.semb` files from this repository's converter.** Loading a
|
|
14
|
+
HuggingFace directory at runtime is not supported and will not be. The
|
|
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, Unigram
|
|
17
|
+
and byte-level tokenizers are rejected at conversion time rather than
|
|
18
|
+
approximated.
|
|
19
|
+
- **No model training, distillation or fine-tuning.** Produce the model with
|
|
20
|
+
upstream `model2vec`, then convert it.
|
|
21
|
+
- **No transformer inference.** Static embeddings have no attention and no
|
|
22
|
+
context; a word contributes the same row wherever it appears. If you need
|
|
23
|
+
contextual embeddings, this is the wrong tool.
|
|
24
|
+
|
|
25
|
+
## Runtime
|
|
26
|
+
|
|
27
|
+
- **No internal parallelism.** `threads:` is rejected rather than ignored. Split
|
|
28
|
+
work at the job level and run more application workers.
|
|
29
|
+
- **No Ractor support.** Model, and everything holding native state, are not
|
|
30
|
+
shareable; `Ractor.make_shareable` raises.
|
|
31
|
+
- **No GPU, no BLAS, no external SIMD library.** The kernels are plain C with
|
|
32
|
+
compiler autovectorisation, plus a hand-written half-precision path.
|
|
33
|
+
- **No streaming input.** `embed` and `embed_batch` take Ruby `String`s that are
|
|
34
|
+
already in memory.
|
|
35
|
+
- **`embed_batch` peaks at roughly twice the returned blob**, because the result
|
|
36
|
+
is built in C memory and then copied into a Ruby `String`. Chunk large
|
|
37
|
+
corpora.
|
|
38
|
+
- **Big-endian hosts are refused at load.** The header is decoded
|
|
39
|
+
little-endian explicitly, but the mmapped structures and the float matrix are
|
|
40
|
+
read in native order.
|
|
41
|
+
|
|
42
|
+
## Search
|
|
43
|
+
|
|
44
|
+
- **No index.** `dot_top_k` and `cosine_top_k` are exhaustive scans. There is no
|
|
45
|
+
ANN, no IVF, no HNSW, no clustering.
|
|
46
|
+
- **No batched queries.** One query per scan; N queries stream the matrix N
|
|
47
|
+
times.
|
|
48
|
+
- **No candidate filtering.** You cannot restrict a scan to a subset of rows.
|
|
49
|
+
- **No precomputed row norms.** `cosine_top_k` recomputes each row's norm on
|
|
50
|
+
every query. Normalise your rows once and use `dot_top_k` instead.
|
|
51
|
+
- **No persistence, no ids, no updates.** A matrix is a frozen `String`; mapping
|
|
52
|
+
rows to documents, appending, and deleting are the caller's problem. If you
|
|
53
|
+
want that, use pgvector and keep this gem for producing the vectors.
|
|
54
|
+
- **No metric other than dot product and cosine.** No Euclidean, no Manhattan,
|
|
55
|
+
no Hamming.
|
|
56
|
+
|
|
57
|
+
## Storage formats
|
|
58
|
+
|
|
59
|
+
- **`f32` and `f16` only.** No int8, no binary quantisation.
|
|
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.
|
|
64
|
+
- **`f16` rounding is half-up, not ties-to-even.** Blobs written by this gem can
|
|
65
|
+
differ from NumPy or PyTorch by one ULP on exact halfway values.
|
|
66
|
+
|
|
67
|
+
## Platforms
|
|
68
|
+
|
|
69
|
+
- **Windows is built and smoke-tested by cross-compilation under wine, not on a
|
|
70
|
+
real Windows runner.** The loader uses `CreateFileMapping`, with a
|
|
71
|
+
read-into-heap fallback.
|
|
72
|
+
- **No JRuby or TruffleRuby.** The gem is a CRuby C extension.
|
|
73
|
+
|
|
74
|
+
## Verification
|
|
75
|
+
|
|
76
|
+
- **`load` does not check the SHA-256.** Structural validation always runs, but
|
|
77
|
+
a bit flip inside the matrix is only caught by `verify: true` or
|
|
78
|
+
`StaticEmbeddings.verify`.
|
|
79
|
+
- **Parity is per model and per runtime version.** A converted model is not
|
|
80
|
+
trusted until it has a record in `docs/MODEL_AUDIT.md`, and that record
|
|
81
|
+
expires when the tokenizer changes.
|
|
82
|
+
|
|
83
|
+
## Known open questions
|
|
84
|
+
|
|
85
|
+
These are not decisions, just work that has not been done or measured:
|
|
86
|
+
|
|
87
|
+
- The `SE_GVL_UNLOCK_THRESHOLD` value has a benchmark now
|
|
88
|
+
(`rake benchmark:gvl_threshold`) but has not been retuned from it.
|
|
89
|
+
- Top-k keeps its candidates in a sorted array, which degrades for large `k`; a
|
|
90
|
+
heap would be better somewhere above a few dozen.
|
|
91
|
+
- `madvise(MADV_RANDOM)` and huge pages for the embedding section are untested.
|
|
92
|
+
- The C allocation counters cover the runtime's own allocations only; Ruby heap,
|
|
93
|
+
fragmentation and mmap residency are outside them.
|
data/docs/MODEL_AUDIT.md
CHANGED
|
@@ -11,25 +11,29 @@ runtime half of it even though the file bytes are untouched.
|
|
|
11
11
|
| runtime | parity | note |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| 0.1.1 | `parity OK`, recorded below | superseded |
|
|
14
|
-
| 0.1.2 | **not re-run** |
|
|
14
|
+
| 0.1.2 | **not re-run** | superseded before release |
|
|
15
|
+
| 0.1.3 | `parity OK` | tokenizer / pooling contract; same numbers as 0.1.4 |
|
|
16
|
+
| 0.1.4 | `parity OK`, recorded below | current |
|
|
15
17
|
|
|
16
18
|
0.1.2 changed `is_control()`, which changes token ids for any input containing
|
|
17
19
|
`U+007F`. `StaticEmbeddings::Reference` cannot settle whether the new behaviour
|
|
18
20
|
matches HuggingFace, because it is an implementation twin of the C runtime
|
|
19
|
-
written in this repository.
|
|
20
|
-
|
|
21
|
-
|
|
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`.
|
|
22
25
|
|
|
23
26
|
Source model:
|
|
24
27
|
|
|
25
28
|
- Hugging Face repository: `minishlab/potion-retrieval-32M`
|
|
29
|
+
- Hugging Face snapshot: `6fc8051fab2a1e0ee76689cf08c853792ac285e7`
|
|
26
30
|
- Oracle implementation: `model2vec.StaticModel.from_pretrained`
|
|
27
|
-
- Python package: `model2vec 0.9.0`
|
|
31
|
+
- Python package: `model2vec 0.9.0` (`tokenizers 0.23.1`, `numpy 2.5.2`)
|
|
28
32
|
- Oracle file: `tmp/model2vec_oracle.json`
|
|
29
|
-
- Oracle rows in this recorded run: `
|
|
33
|
+
- Oracle rows in this recorded run: `31`
|
|
30
34
|
- Oracle dimension: `512`
|
|
31
35
|
- Oracle max length: `512`
|
|
32
|
-
- Runtime at time of this record: `static_embeddings 0.1.
|
|
36
|
+
- Runtime at time of this record: `static_embeddings 0.1.4`
|
|
33
37
|
|
|
34
38
|
Converted `.semb`:
|
|
35
39
|
|
|
@@ -42,16 +46,20 @@ Converted `.semb`:
|
|
|
42
46
|
Parity command:
|
|
43
47
|
|
|
44
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
|
+
|
|
45
53
|
bundle exec rake parity \
|
|
46
54
|
MODEL="$HOME/.cache/static_embeddings/models/potion-retrieval-32m.semb" \
|
|
47
55
|
ORACLE=tmp/model2vec_oracle.json
|
|
48
56
|
```
|
|
49
57
|
|
|
50
|
-
Parity result:
|
|
58
|
+
Parity result (0.1.4, 2026-08-31):
|
|
51
59
|
|
|
52
60
|
```text
|
|
53
|
-
rows=
|
|
54
|
-
id_rows_checked=
|
|
61
|
+
rows=31
|
|
62
|
+
id_rows_checked=31
|
|
55
63
|
min_cosine=0.9999999999989528
|
|
56
64
|
max_abs_all=2.980232238769531e-07
|
|
57
65
|
token_id_failures=[]
|
|
@@ -59,6 +67,11 @@ vector_failures=[]
|
|
|
59
67
|
parity OK
|
|
60
68
|
```
|
|
61
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
|
+
|
|
62
75
|
Decision:
|
|
63
76
|
|
|
64
77
|
- Tokenization parity: pass
|
|
@@ -68,7 +81,8 @@ Decision:
|
|
|
68
81
|
- Unicode normalization behavior: pass
|
|
69
82
|
- Unknown-word behavior: pass
|
|
70
83
|
- Long input / truncation behavior: pass
|
|
84
|
+
- DEL / control-character behavior: pass
|
|
71
85
|
|
|
72
|
-
Accepted for runtime and benchmark use **under 0.1.
|
|
73
|
-
unchanged and its SHA256 still matches
|
|
74
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
sample runs disagree with each other — same corpus, same
|
|
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.
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
|
@@ -72,6 +81,14 @@ for those. And the instrumentation is not free: about 13% on `tokenize`, 3% on
|
|
|
72
81
|
`embed`, within noise on `embed_batch`. A run captured with it is not
|
|
73
82
|
comparable to one captured without it.
|
|
74
83
|
|
|
84
|
+
## Running the benchmarks
|
|
85
|
+
|
|
86
|
+
`bundle exec rake benchmark`, or one at a time with `rake benchmark:core_paths`
|
|
87
|
+
and `rake benchmark:gvl_threshold`. Generated text inputs are
|
|
88
|
+
`valid_encoding?`-primed before timing, so these are cached-coderange hot-path
|
|
89
|
+
numbers. Every row carries `spread_pct`, and a difference smaller than that is
|
|
90
|
+
not a result. See `docs/BENCHMARKING.md`.
|
|
91
|
+
|
|
75
92
|
## Benchmark hygiene
|
|
76
93
|
|
|
77
94
|
Three failure modes have bitten this repository already.
|
|
@@ -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
|
-
|
|
75
|
+
static double sum_sq_d(const float *vec, uint32_t dim) {
|
|
76
|
+
uint32_t j = 0;
|
|
75
77
|
double sum = 0.0;
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
|
83
|
-
if ((prefix_len == 0 ||
|
|
84
|
-
(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
|
-
|
|
421
|
-
|
|
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,
|
|
@@ -845,5 +920,8 @@ size_t se_model_memsize(const se_model_t *model) {
|
|
|
845
920
|
if (!model)
|
|
846
921
|
return 0;
|
|
847
922
|
|
|
923
|
+
if (model->mapped)
|
|
924
|
+
return sizeof(se_model_t);
|
|
925
|
+
|
|
848
926
|
return sizeof(se_model_t) + model->map_size;
|
|
849
927
|
}
|
|
@@ -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);
|