static_embeddings 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +153 -0
- data/README.md +56 -28
- data/Rakefile +1 -1
- data/docs/ARCHITECTURE.md +50 -31
- data/docs/LIMITATIONS.md +16 -8
- data/docs/MODEL_AUDIT.md +84 -43
- data/docs/PERFORMANCE.md +28 -19
- data/ext/static_embeddings/se_embed.c +48 -8
- data/ext/static_embeddings/se_f16.c +43 -7
- data/ext/static_embeddings/se_format.c +121 -9
- data/ext/static_embeddings/se_internal.h +26 -5
- data/ext/static_embeddings/se_tokenizer.c +451 -47
- data/ext/static_embeddings/se_unicode.c +1 -1
- data/ext/static_embeddings/static_embeddings.c +136 -48
- data/lib/static_embeddings/cli.rb +1 -0
- data/lib/static_embeddings/converter.rb +51 -7
- data/lib/static_embeddings/format.rb +60 -22
- data/lib/static_embeddings/paths.rb +18 -1
- data/lib/static_embeddings/reference.rb +54 -7
- data/lib/static_embeddings/safetensors.rb +178 -34
- data/lib/static_embeddings/version.rb +1 -1
- data/lib/static_embeddings.rb +3 -5
- data/tools/check_model2vec_parity.rb +85 -54
- metadata +1 -1
data/docs/MODEL_AUDIT.md
CHANGED
|
@@ -1,58 +1,46 @@
|
|
|
1
1
|
# Model Audit
|
|
2
2
|
|
|
3
|
-
A converted model is trusted only after
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
A converted model is trusted only after a recorded comparison against an
|
|
4
|
+
external upstream oracle. An audit record covers one source-model snapshot,
|
|
5
|
+
one `.semb` format/runtime contract, pinned upstream package versions, and the
|
|
6
|
+
specific corpus that was checked. A passing corpus is evidence for those rows;
|
|
7
|
+
it is **not** an exhaustive proof that every Unicode string is equivalent.
|
|
8
|
+
|
|
9
|
+
`StaticEmbeddings::Reference` remains useful for high-volume differential fuzz
|
|
10
|
+
of the C implementation, but it is an implementation twin and is not counted as
|
|
11
|
+
independent upstream evidence.
|
|
8
12
|
|
|
9
13
|
## potion-retrieval-32m
|
|
10
14
|
|
|
11
|
-
| runtime |
|
|
15
|
+
| runtime | external result | note |
|
|
12
16
|
|---|---|---|
|
|
13
|
-
| 0.1.1 |
|
|
14
|
-
| 0.1.2 |
|
|
15
|
-
| 0.1.3 |
|
|
17
|
+
| 0.1.1 | 31-row corpus passed | superseded |
|
|
18
|
+
| 0.1.2 | not re-run | superseded before release |
|
|
19
|
+
| 0.1.3 | 31-row corpus passed | superseded |
|
|
20
|
+
| 0.1.4 | 31-row corpus passed | historical result below; later review found uncovered boundary cases |
|
|
21
|
+
| 0.1.5 | **434-row corpus passed** | format v3; CI `potion_audit` record below |
|
|
16
22
|
|
|
17
|
-
0.1.
|
|
18
|
-
`U+007F`. `StaticEmbeddings::Reference` cannot settle whether the new behaviour
|
|
19
|
-
matches HuggingFace, because it is an implementation twin of the C runtime
|
|
20
|
-
written in this repository. The 0.1.3 release candidate was checked against a
|
|
21
|
-
fresh upstream `model2vec.StaticModel` oracle that includes DEL, control
|
|
22
|
-
characters, Unicode, OOV, long-word and truncation rows.
|
|
23
|
+
### Historical 0.1.4 record
|
|
23
24
|
|
|
24
25
|
Source model:
|
|
25
26
|
|
|
26
27
|
- Hugging Face repository: `minishlab/potion-retrieval-32M`
|
|
27
28
|
- Hugging Face snapshot: `6fc8051fab2a1e0ee76689cf08c853792ac285e7`
|
|
28
29
|
- Oracle implementation: `model2vec.StaticModel.from_pretrained`
|
|
29
|
-
- Python package: `model2vec 0.9.0`
|
|
30
|
-
- Oracle file: `tmp/model2vec_oracle.json`
|
|
30
|
+
- Python package: `model2vec 0.9.0` (`tokenizers 0.23.1`, `numpy 2.5.2`)
|
|
31
31
|
- Oracle rows in this recorded run: `31`
|
|
32
32
|
- Oracle dimension: `512`
|
|
33
33
|
- Oracle max length: `512`
|
|
34
|
-
- Runtime at time of this record: `static_embeddings 0.1.
|
|
34
|
+
- Runtime at time of this record: `static_embeddings 0.1.4`
|
|
35
35
|
|
|
36
36
|
Converted `.semb`:
|
|
37
37
|
|
|
38
|
-
- Path: `$HOME/.cache/static_embeddings/models/potion-retrieval-32m.semb`
|
|
39
38
|
- Format version: `2`
|
|
40
39
|
- Header size: `320`
|
|
41
40
|
- Bytes: `135411608`
|
|
42
41
|
- SHA256: `79e087863d2bab825779fd7de3574e5625542ef6a5ecad33fe681ea16d4b3ab0`
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
python tools/model2vec_oracle.py minishlab/potion-retrieval-32M \
|
|
48
|
-
--out tmp/model2vec_oracle.json
|
|
49
|
-
|
|
50
|
-
bundle exec rake parity \
|
|
51
|
-
MODEL="$HOME/.cache/static_embeddings/models/potion-retrieval-32m.semb" \
|
|
52
|
-
ORACLE=tmp/model2vec_oracle.json
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Parity result:
|
|
43
|
+
Recorded result (2026-08-31):
|
|
56
44
|
|
|
57
45
|
```text
|
|
58
46
|
rows=31
|
|
@@ -64,17 +52,70 @@ vector_failures=[]
|
|
|
64
52
|
parity OK
|
|
65
53
|
```
|
|
66
54
|
|
|
67
|
-
|
|
55
|
+
The last line means **31/31 rows in that historical oracle passed**. It must not
|
|
56
|
+
be read as a global tokenizer-equivalence claim. The corpus did not cover, for
|
|
57
|
+
example, the corrected UNK-before-truncation ordering, standard AddedVocabulary
|
|
58
|
+
literals, or the Rust/Python CJK Extension E boundary.
|
|
59
|
+
|
|
60
|
+
## 0.1.5 record
|
|
61
|
+
|
|
62
|
+
0.1.5 separates three contracts that the old oracle mixed together:
|
|
63
|
+
|
|
64
|
+
1. raw Hugging Face `tokenizers 0.23.1` ids, including `[UNK]`;
|
|
65
|
+
2. this runtime's usable-id embedding contract (`[UNK]` drop, then token cap);
|
|
66
|
+
3. `model2vec.StaticModel` vectors where its character pre-cut does not change
|
|
67
|
+
the usable token sequence.
|
|
68
|
+
|
|
69
|
+
Rows changed solely by Model2Vec's `max_length * median_token_length` character
|
|
70
|
+
pre-cut are reported as intentional deviations rather than hidden inside a pass.
|
|
71
|
+
A passing corpus is evidence for those 434 rows, not an exhaustive proof of
|
|
72
|
+
every Unicode string.
|
|
73
|
+
|
|
74
|
+
Recorded from GitHub Actions (`potion_audit`, Python 3.12.14, pinned
|
|
75
|
+
`model2vec==0.9.0` / `tokenizers==0.23.1` / `numpy==2.5.2`):
|
|
76
|
+
|
|
77
|
+
Source model:
|
|
78
|
+
|
|
79
|
+
- Hugging Face repository: `minishlab/potion-retrieval-32M`
|
|
80
|
+
- Hugging Face snapshot: `6fc8051fab2a1e0ee76689cf08c853792ac285e7`
|
|
81
|
+
- Oracle implementation: `model2vec.StaticModel.from_pretrained`
|
|
82
|
+
- Oracle rows: `434` from `tools/parity_cases.py`
|
|
83
|
+
- Oracle dimension: `512`
|
|
84
|
+
- Oracle max length: `512`
|
|
85
|
+
- Runtime at time of this record: `static_embeddings 0.1.5`
|
|
86
|
+
|
|
87
|
+
Converted `.semb` (CI artifact; Unicode tables stamped from the Ubuntu Ruby that
|
|
88
|
+
converted it, so the SHA is not expected to match a macOS local convert of the
|
|
89
|
+
same snapshot):
|
|
90
|
+
|
|
91
|
+
- Format version: `3`
|
|
92
|
+
- Bytes: `135411800`
|
|
93
|
+
- SHA256: `747231b5afbcb3b16bf2b04538f81d3a96be88a798982214b7cf01eddbdcf4eb`
|
|
94
|
+
- `dim=512` `vocab=63091`
|
|
95
|
+
|
|
96
|
+
Recorded result:
|
|
68
97
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
```text
|
|
99
|
+
rows=434
|
|
100
|
+
vectors_checked=432
|
|
101
|
+
intentional_character_pretruncate_deviations=2
|
|
102
|
+
min_cosine=0.9999999999999989
|
|
103
|
+
max_abs_all=1.4901161193847656e-08
|
|
104
|
+
raw_token_id_failures=[]
|
|
105
|
+
usable_token_id_failures=[]
|
|
106
|
+
embed_invariant_failures=[]
|
|
107
|
+
vector_failures=[]
|
|
108
|
+
corpus parity OK (434/434); intentional Model2Vec character pre-truncation deviations are reported separately
|
|
109
|
+
```
|
|
77
110
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
The two intentional deviations are `long:sparse-whitespace` and
|
|
112
|
+
`long:unknown-prefix`. Vectors were not required to match Model2Vec on those
|
|
113
|
+
rows. The remaining 432 rows were inside `cosine >= 1 - 1e-6` and
|
|
114
|
+
`max_abs <= 1e-5`.
|
|
115
|
+
|
|
116
|
+
The preceding CI job `upstream_parity` ran the same 434-row corpus against the
|
|
117
|
+
synthetic `tiny-wordpiece` fixture: `434/434`, all four failure lists empty,
|
|
118
|
+
`vectors_checked=429`, `intentional_character_pretruncate_deviations=5`,
|
|
119
|
+
`min_cosine=0.9999999999999988`, `max_abs_all=5.960464477539063e-08`. That job
|
|
120
|
+
proves the fixture loads in `StaticModel.from_pretrained` and that the checker
|
|
121
|
+
contracts hold; it is not a potion audit.
|
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
|
|
|
@@ -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);
|
|
@@ -158,7 +197,8 @@ se_status_t se_embed_ids(const se_model_t *model, se_scratch_t *sc, const uint32
|
|
|
158
197
|
se_status_t se_embed_one(const se_model_t *model, se_scratch_t *sc, const uint8_t *input,
|
|
159
198
|
size_t input_len, uint32_t max_tokens, float *out, se_token_stats_t *stats,
|
|
160
199
|
se_error_t *err, volatile sig_atomic_t *cancelled) {
|
|
161
|
-
se_status_t rc = se_tokenize(model, sc, input, input_len, max_tokens,
|
|
200
|
+
se_status_t rc = se_tokenize(model, sc, input, input_len, max_tokens, SE_TOKEN_LIMIT_USABLE,
|
|
201
|
+
stats, err, cancelled);
|
|
162
202
|
if (rc != SE_OK)
|
|
163
203
|
return rc;
|
|
164
204
|
return embed_ids_core(model, sc, sc->ids, stats->token_count, out, err, cancelled);
|
|
@@ -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)
|
|
@@ -74,14 +102,14 @@ int se_vocab_lookup_piece(const se_model_t *model, const uint8_t *prefix, size_t
|
|
|
74
102
|
uint32_t pos = h & mask;
|
|
75
103
|
uint32_t total_len = (uint32_t)(prefix_len + len);
|
|
76
104
|
|
|
77
|
-
for (uint32_t probe = 0; probe
|
|
105
|
+
for (uint32_t probe = 0; probe < model->meta.max_probe; probe++) {
|
|
78
106
|
const se_vocab_slot_t *slot = &model->vocab_hash[pos];
|
|
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,
|
|
@@ -481,6 +556,31 @@ static se_status_t validate_vocab_hash(const se_model_t *model, struct section v
|
|
|
481
556
|
se_error_set(err, SE_ERR_INVALID_FORMAT,
|
|
482
557
|
"[UNK] is not reachable through the vocabulary hash");
|
|
483
558
|
rc = SE_ERR_INVALID_FORMAT;
|
|
559
|
+
goto done;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
static const struct {
|
|
563
|
+
uint32_t bit;
|
|
564
|
+
const char *text;
|
|
565
|
+
uint32_t len;
|
|
566
|
+
} added[] = {
|
|
567
|
+
{SE_ADDED_PAD, "[PAD]", 5u}, {SE_ADDED_UNK, "[UNK]", 5u}, {SE_ADDED_CLS, "[CLS]", 5u},
|
|
568
|
+
{SE_ADDED_SEP, "[SEP]", 5u}, {SE_ADDED_MASK, "[MASK]", 6u},
|
|
569
|
+
};
|
|
570
|
+
const uint32_t ids[] = {m->pad_id, m->unk_id, m->cls_id, m->sep_id, m->mask_id};
|
|
571
|
+
|
|
572
|
+
for (size_t i = 0; i < SE_ARRAY_LEN(added); i++) {
|
|
573
|
+
if ((m->added_token_mask & added[i].bit) == 0)
|
|
574
|
+
continue;
|
|
575
|
+
uint32_t got = 0;
|
|
576
|
+
if (ids[i] >= m->vocab_size ||
|
|
577
|
+
!se_vocab_lookup(model, (const uint8_t *)added[i].text, added[i].len, &got) ||
|
|
578
|
+
got != ids[i]) {
|
|
579
|
+
se_error_set(err, SE_ERR_INVALID_FORMAT,
|
|
580
|
+
"added token %s is not reachable at its recorded id", added[i].text);
|
|
581
|
+
rc = SE_ERR_INVALID_FORMAT;
|
|
582
|
+
goto done;
|
|
583
|
+
}
|
|
484
584
|
}
|
|
485
585
|
|
|
486
586
|
done:
|
|
@@ -571,6 +671,7 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
|
|
|
571
671
|
m->subword_prefix_len = read_u32(base, SE_OFF_SUBWORD_PREFIX_LEN);
|
|
572
672
|
m->max_token_chars = read_u32(base, SE_OFF_MAX_TOKEN_CHARS);
|
|
573
673
|
m->max_probe = read_u32(base, SE_OFF_MAX_PROBE);
|
|
674
|
+
m->added_token_mask = read_u32(base, SE_OFF_ADDED_TOKEN_MASK);
|
|
574
675
|
memcpy(m->subword_prefix, base + SE_OFF_SUBWORD_PREFIX, 8);
|
|
575
676
|
|
|
576
677
|
if (m->tokenizer_type != SE_TOKENIZER_BERT_WORDPIECE_V1) {
|
|
@@ -594,7 +695,7 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
|
|
|
594
695
|
m->normalization_type);
|
|
595
696
|
return SE_ERR_INVALID_FORMAT;
|
|
596
697
|
}
|
|
597
|
-
if (m->truncation_policy !=
|
|
698
|
+
if (m->truncation_policy != SE_TRUNCATE_USABLE_IDS_BEFORE_POOLING) {
|
|
598
699
|
se_error_set(err, SE_ERR_INVALID_FORMAT, "truncation policy %u is not supported",
|
|
599
700
|
m->truncation_policy);
|
|
600
701
|
return SE_ERR_INVALID_FORMAT;
|
|
@@ -607,6 +708,11 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
|
|
|
607
708
|
se_error_set(err, SE_ERR_INVALID_FORMAT, "unknown UNK policy %u", m->unk_policy);
|
|
608
709
|
return SE_ERR_INVALID_FORMAT;
|
|
609
710
|
}
|
|
711
|
+
if ((m->added_token_mask & ~SE_ADDED_TOKEN_MASK_ALL) != 0) {
|
|
712
|
+
se_error_set(err, SE_ERR_INVALID_FORMAT, "unknown added-token mask bits 0x%x",
|
|
713
|
+
m->added_token_mask & ~SE_ADDED_TOKEN_MASK_ALL);
|
|
714
|
+
return SE_ERR_INVALID_FORMAT;
|
|
715
|
+
}
|
|
610
716
|
if (m->empty_policy != SE_EMPTY_ZERO_VECTOR && m->empty_policy != SE_EMPTY_RAISE) {
|
|
611
717
|
se_error_set(err, SE_ERR_INVALID_FORMAT, "unknown empty-input policy %u", m->empty_policy);
|
|
612
718
|
return SE_ERR_INVALID_FORMAT;
|
|
@@ -629,8 +735,14 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
|
|
|
629
735
|
m->hash_table_size);
|
|
630
736
|
return SE_ERR_INVALID_FORMAT;
|
|
631
737
|
}
|
|
632
|
-
if (m->hash_table_size
|
|
633
|
-
|
|
738
|
+
if (m->hash_table_size <= m->vocab_size ||
|
|
739
|
+
(uint64_t)m->vocab_size * 100u > (uint64_t)m->hash_table_size * 70u) {
|
|
740
|
+
se_error_set(err, SE_ERR_INVALID_FORMAT,
|
|
741
|
+
"hash table load factor exceeds the supported 0.70 maximum");
|
|
742
|
+
return SE_ERR_INVALID_FORMAT;
|
|
743
|
+
}
|
|
744
|
+
if (m->max_probe == 0 || m->max_probe > m->hash_table_size) {
|
|
745
|
+
se_error_set(err, SE_ERR_INVALID_FORMAT, "invalid recorded max probe %u", m->max_probe);
|
|
634
746
|
return SE_ERR_INVALID_FORMAT;
|
|
635
747
|
}
|
|
636
748
|
if (m->subword_prefix_len > 8) {
|