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
|
@@ -80,6 +80,7 @@ static ID id_blocking_p;
|
|
|
80
80
|
static ID id_vector;
|
|
81
81
|
static ID id_token_count;
|
|
82
82
|
static ID id_unk_count;
|
|
83
|
+
static ID id_pooled_count;
|
|
83
84
|
static ID id_truncated;
|
|
84
85
|
static ID id_dim;
|
|
85
86
|
static ID id_allow_unfrozen;
|
|
@@ -274,10 +275,14 @@ static void batch_worker_run(batch_job_t *job, se_scratch_t *scratch) {
|
|
|
274
275
|
|
|
275
276
|
static void *batch_execute(void *arg) {
|
|
276
277
|
batch_job_t *job = (batch_job_t *)arg;
|
|
277
|
-
se_scratch_t scratch;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
se_scratch_t *scratch = se_scratch_acquire(job->model->meta.dim);
|
|
279
|
+
if (!scratch) {
|
|
280
|
+
se_error_set(&job->error, SE_ERR_OOM, "out of memory while sizing scratch buffers");
|
|
281
|
+
job->failed = 1;
|
|
282
|
+
return NULL;
|
|
283
|
+
}
|
|
284
|
+
batch_worker_run(job, scratch);
|
|
285
|
+
se_scratch_release(scratch);
|
|
281
286
|
return NULL;
|
|
282
287
|
}
|
|
283
288
|
|
|
@@ -928,6 +933,32 @@ static VALUE embed_one_via_batch(VALUE self, VALUE text, VALUE max_tokens_opt,
|
|
|
928
933
|
return embed_texts_internal(self, text, 0, 1, max_tokens_opt, format, validation, stats);
|
|
929
934
|
}
|
|
930
935
|
|
|
936
|
+
typedef struct {
|
|
937
|
+
se_scratch_t *scratch;
|
|
938
|
+
const se_model_t *model;
|
|
939
|
+
const uint8_t *input;
|
|
940
|
+
size_t input_len;
|
|
941
|
+
uint32_t max_tokens;
|
|
942
|
+
float *out;
|
|
943
|
+
se_token_stats_t *stats;
|
|
944
|
+
se_error_t err;
|
|
945
|
+
se_status_t rc;
|
|
946
|
+
} embed_one_scratch_job_t;
|
|
947
|
+
|
|
948
|
+
static VALUE embed_one_scratch_body(VALUE arg) {
|
|
949
|
+
embed_one_scratch_job_t *job = (embed_one_scratch_job_t *)(uintptr_t)arg;
|
|
950
|
+
job->rc = se_embed_one(job->model, job->scratch, job->input, job->input_len, job->max_tokens,
|
|
951
|
+
job->out, job->stats, &job->err, NULL);
|
|
952
|
+
return Qnil;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
static VALUE embed_one_scratch_ensure(VALUE arg) {
|
|
956
|
+
embed_one_scratch_job_t *job = (embed_one_scratch_job_t *)(uintptr_t)arg;
|
|
957
|
+
se_scratch_release(job->scratch);
|
|
958
|
+
job->scratch = NULL;
|
|
959
|
+
return Qnil;
|
|
960
|
+
}
|
|
961
|
+
|
|
931
962
|
static VALUE embed_one_value(VALUE self, VALUE text, VALUE max_tokens_opt,
|
|
932
963
|
se_vector_format_t format, se_encoding_validation_t validation,
|
|
933
964
|
se_token_stats_t *stats) {
|
|
@@ -947,25 +978,28 @@ static VALUE embed_one_value(VALUE self, VALUE text, VALUE max_tokens_opt,
|
|
|
947
978
|
rb_enc_associate(result, binary_encoding);
|
|
948
979
|
float *out = (float *)RSTRING_PTR(result);
|
|
949
980
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
981
|
+
se_token_stats_t local_stats;
|
|
982
|
+
embed_one_scratch_job_t job;
|
|
983
|
+
memset(&job, 0, sizeof(job));
|
|
984
|
+
job.model = &w->model;
|
|
985
|
+
job.input = (const uint8_t *)RSTRING_PTR(text);
|
|
986
|
+
job.input_len = (size_t)RSTRING_LEN(text);
|
|
987
|
+
job.max_tokens = resolve_max_tokens(&w->model, max_tokens_opt);
|
|
988
|
+
job.out = out;
|
|
989
|
+
job.stats = stats ? stats : &local_stats;
|
|
990
|
+
se_error_clear(&job.err);
|
|
991
|
+
|
|
992
|
+
job.scratch = se_scratch_acquire(dim);
|
|
993
|
+
if (!job.scratch)
|
|
954
994
|
rb_raise(rb_eNoMemError, "out of memory");
|
|
955
|
-
}
|
|
956
995
|
|
|
957
|
-
|
|
958
|
-
|
|
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);
|
|
996
|
+
rb_ensure(embed_one_scratch_body, (VALUE)(uintptr_t)&job, embed_one_scratch_ensure,
|
|
997
|
+
(VALUE)(uintptr_t)&job);
|
|
965
998
|
RB_GC_GUARD(text);
|
|
999
|
+
RB_GC_GUARD(result);
|
|
966
1000
|
|
|
967
|
-
if (rc != SE_OK)
|
|
968
|
-
raise_se(&err);
|
|
1001
|
+
if (job.rc != SE_OK)
|
|
1002
|
+
raise_se(&job.err);
|
|
969
1003
|
|
|
970
1004
|
return result;
|
|
971
1005
|
}
|
|
@@ -997,10 +1031,46 @@ static VALUE model_embed_with_stats(int argc, VALUE *argv, VALUE self) {
|
|
|
997
1031
|
rb_hash_aset(hash, ID2SYM(id_vector), vector);
|
|
998
1032
|
rb_hash_aset(hash, ID2SYM(id_token_count), UINT2NUM(stats.token_count));
|
|
999
1033
|
rb_hash_aset(hash, ID2SYM(id_unk_count), UINT2NUM(stats.unk_count));
|
|
1034
|
+
uint32_t pooled_count = get_model(self)->model.meta.unk_policy == SE_UNK_DROP
|
|
1035
|
+
? stats.token_count - stats.unk_count
|
|
1036
|
+
: stats.token_count;
|
|
1037
|
+
rb_hash_aset(hash, ID2SYM(id_pooled_count), UINT2NUM(pooled_count));
|
|
1000
1038
|
rb_hash_aset(hash, ID2SYM(id_truncated), stats.truncated ? Qtrue : Qfalse);
|
|
1001
1039
|
return hash;
|
|
1002
1040
|
}
|
|
1003
1041
|
|
|
1042
|
+
typedef struct {
|
|
1043
|
+
se_scratch_t *scratch;
|
|
1044
|
+
const se_model_t *model;
|
|
1045
|
+
const uint8_t *input;
|
|
1046
|
+
size_t input_len;
|
|
1047
|
+
uint32_t max_tokens;
|
|
1048
|
+
se_token_stats_t stats;
|
|
1049
|
+
se_error_t err;
|
|
1050
|
+
se_status_t rc;
|
|
1051
|
+
VALUE ids;
|
|
1052
|
+
} tokenize_scratch_job_t;
|
|
1053
|
+
|
|
1054
|
+
static VALUE tokenize_scratch_body(VALUE arg) {
|
|
1055
|
+
tokenize_scratch_job_t *job = (tokenize_scratch_job_t *)(uintptr_t)arg;
|
|
1056
|
+
job->rc = se_tokenize(job->model, job->scratch, job->input, job->input_len, job->max_tokens,
|
|
1057
|
+
SE_TOKEN_LIMIT_RAW, &job->stats, &job->err, NULL);
|
|
1058
|
+
if (job->rc != SE_OK)
|
|
1059
|
+
return Qnil;
|
|
1060
|
+
|
|
1061
|
+
job->ids = rb_ary_new_capa((long)job->stats.token_count);
|
|
1062
|
+
for (uint32_t i = 0; i < job->stats.token_count; i++)
|
|
1063
|
+
rb_ary_push(job->ids, UINT2NUM(job->scratch->ids[i]));
|
|
1064
|
+
return Qnil;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
static VALUE tokenize_scratch_ensure(VALUE arg) {
|
|
1068
|
+
tokenize_scratch_job_t *job = (tokenize_scratch_job_t *)(uintptr_t)arg;
|
|
1069
|
+
se_scratch_release(job->scratch);
|
|
1070
|
+
job->scratch = NULL;
|
|
1071
|
+
return Qnil;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1004
1074
|
static VALUE model_tokenize(int argc, VALUE *argv, VALUE self) {
|
|
1005
1075
|
VALUE text, opts;
|
|
1006
1076
|
rb_scan_args(argc, argv, "1:", &text, &opts);
|
|
@@ -1011,32 +1081,27 @@ static VALUE model_tokenize(int argc, VALUE *argv, VALUE self) {
|
|
|
1011
1081
|
check_text_encoding_mode(
|
|
1012
1082
|
text, -1, resolve_encoding_validation(lookup_option(opts, id_validate_encoding)));
|
|
1013
1083
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1084
|
+
tokenize_scratch_job_t job;
|
|
1085
|
+
memset(&job, 0, sizeof(job));
|
|
1086
|
+
job.model = &w->model;
|
|
1087
|
+
job.input = (const uint8_t *)RSTRING_PTR(text);
|
|
1088
|
+
job.input_len = (size_t)RSTRING_LEN(text);
|
|
1089
|
+
job.max_tokens = resolve_max_tokens(&w->model, lookup_option(opts, id_max_tokens));
|
|
1090
|
+
job.ids = Qnil;
|
|
1091
|
+
se_error_clear(&job.err);
|
|
1092
|
+
|
|
1093
|
+
job.scratch = se_scratch_acquire(w->model.meta.dim);
|
|
1094
|
+
if (!job.scratch)
|
|
1020
1095
|
rb_raise(rb_eNoMemError, "out of memory");
|
|
1021
|
-
}
|
|
1022
1096
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
}
|
|
1097
|
+
rb_ensure(tokenize_scratch_body, (VALUE)(uintptr_t)&job, tokenize_scratch_ensure,
|
|
1098
|
+
(VALUE)(uintptr_t)&job);
|
|
1099
|
+
RB_GC_GUARD(text);
|
|
1032
1100
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
rb_ary_push(ids, UINT2NUM(scratch.ids[i]));
|
|
1101
|
+
if (job.rc != SE_OK)
|
|
1102
|
+
raise_se(&job.err);
|
|
1036
1103
|
|
|
1037
|
-
|
|
1038
|
-
RB_GC_GUARD(text);
|
|
1039
|
-
return ids;
|
|
1104
|
+
return job.ids;
|
|
1040
1105
|
}
|
|
1041
1106
|
|
|
1042
1107
|
typedef struct {
|
|
@@ -1077,17 +1142,15 @@ static VALUE num2ull_at_value(VALUE arg) {
|
|
|
1077
1142
|
|
|
1078
1143
|
static void *ids_execute(void *arg) {
|
|
1079
1144
|
ids_job_t *job = (ids_job_t *)arg;
|
|
1080
|
-
se_scratch_t scratch;
|
|
1081
|
-
|
|
1082
|
-
if (!se_scratch_reserve(&scratch, job->model->meta.dim)) {
|
|
1145
|
+
se_scratch_t *scratch = se_scratch_acquire(job->model->meta.dim);
|
|
1146
|
+
if (!scratch) {
|
|
1083
1147
|
se_error_set(&job->error, SE_ERR_OOM, "out of memory while sizing scratch buffers");
|
|
1084
|
-
se_scratch_free(&scratch);
|
|
1085
1148
|
return NULL;
|
|
1086
1149
|
}
|
|
1087
1150
|
se_error_clear(&job->error);
|
|
1088
|
-
se_embed_ids(job->model,
|
|
1151
|
+
se_embed_ids(job->model, scratch, job->ids, job->n_ids, job->out, &job->stats, &job->error,
|
|
1089
1152
|
&job->cancelled);
|
|
1090
|
-
|
|
1153
|
+
se_scratch_release(scratch);
|
|
1091
1154
|
return NULL;
|
|
1092
1155
|
}
|
|
1093
1156
|
|
|
@@ -1179,7 +1242,27 @@ static VALUE embed_token_ids_value(VALUE self, VALUE ids_value, VALUE max_tokens
|
|
|
1179
1242
|
size_t n = (size_t)n_long;
|
|
1180
1243
|
int truncated = 0;
|
|
1181
1244
|
|
|
1182
|
-
if (max_tokens != 0 &&
|
|
1245
|
+
if (max_tokens != 0 && w->model.meta.unk_policy == SE_UNK_DROP) {
|
|
1246
|
+
size_t usable = 0;
|
|
1247
|
+
size_t cutoff = n;
|
|
1248
|
+
for (size_t i = 0; i < n; i++) {
|
|
1249
|
+
unsigned long long value = NUM2ULL(rb_ary_entry(ids_value, (long)i));
|
|
1250
|
+
if (value >= w->model.meta.vocab_size)
|
|
1251
|
+
rb_raise(rb_eArgError, "token id at index %zu is out of range", i);
|
|
1252
|
+
|
|
1253
|
+
if ((uint32_t)value == w->model.meta.unk_id)
|
|
1254
|
+
continue;
|
|
1255
|
+
|
|
1256
|
+
usable++;
|
|
1257
|
+
if (usable == (size_t)max_tokens) {
|
|
1258
|
+
cutoff = i + 1;
|
|
1259
|
+
} else if (usable > (size_t)max_tokens) {
|
|
1260
|
+
n = cutoff;
|
|
1261
|
+
truncated = 1;
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
} else if (max_tokens != 0 && n > (size_t)max_tokens) {
|
|
1183
1266
|
n = (size_t)max_tokens;
|
|
1184
1267
|
truncated = 1;
|
|
1185
1268
|
}
|
|
@@ -1239,6 +1322,10 @@ static VALUE model_embed_token_ids_with_stats(int argc, VALUE *argv, VALUE self)
|
|
|
1239
1322
|
rb_hash_aset(hash, ID2SYM(id_vector), vector);
|
|
1240
1323
|
rb_hash_aset(hash, ID2SYM(id_token_count), UINT2NUM(stats.token_count));
|
|
1241
1324
|
rb_hash_aset(hash, ID2SYM(id_unk_count), UINT2NUM(stats.unk_count));
|
|
1325
|
+
uint32_t pooled_count = get_model(self)->model.meta.unk_policy == SE_UNK_DROP
|
|
1326
|
+
? stats.token_count - stats.unk_count
|
|
1327
|
+
: stats.token_count;
|
|
1328
|
+
rb_hash_aset(hash, ID2SYM(id_pooled_count), UINT2NUM(pooled_count));
|
|
1242
1329
|
rb_hash_aset(hash, ID2SYM(id_truncated), stats.truncated ? Qtrue : Qfalse);
|
|
1243
1330
|
return hash;
|
|
1244
1331
|
}
|
|
@@ -1549,6 +1636,7 @@ RUBY_FUNC_EXPORTED void Init_static_embeddings(void) {
|
|
|
1549
1636
|
id_vector = rb_intern("vector");
|
|
1550
1637
|
id_token_count = rb_intern("token_count");
|
|
1551
1638
|
id_unk_count = rb_intern("unk_count");
|
|
1639
|
+
id_pooled_count = rb_intern("pooled_count");
|
|
1552
1640
|
id_truncated = rb_intern("truncated");
|
|
1553
1641
|
id_dim = rb_intern("dim");
|
|
1554
1642
|
id_allow_unfrozen = rb_intern("allow_unfrozen");
|
|
@@ -65,6 +65,7 @@ module StaticEmbeddings
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def convert(argv)
|
|
68
|
+
require "static_embeddings/converter"
|
|
68
69
|
options = parse_convert_options(argv)
|
|
69
70
|
source = required_arg(argv, "usage: static_embeddings convert SOURCE_DIR [--out PATH]")
|
|
70
71
|
model_id = options[:id] || File.basename(File.expand_path(source))
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require "digest"
|
|
3
|
+
require "static_embeddings/format"
|
|
4
|
+
require "static_embeddings/safetensors"
|
|
5
|
+
require "static_embeddings/unicode_tables"
|
|
3
6
|
|
|
4
7
|
module StaticEmbeddings
|
|
5
8
|
class Converter
|
|
6
9
|
REFERENCE_IMPL = "model2vec.StaticModel"
|
|
10
|
+
REFERENCE_MODEL2VEC_VERSION = "0.9.0"
|
|
11
|
+
REFERENCE_TOKENIZERS_VERSION = "0.23.1"
|
|
12
|
+
REFERENCE_UNICODE_CATEGORIES_VERSION = "0.1.1"
|
|
7
13
|
REFERENCE_MAX_TOKENS = 512
|
|
8
14
|
|
|
9
15
|
ALLOWED_NORMALIZER_KEYS = %w[type clean_text handle_chinese_chars strip_accents lowercase].freeze
|
|
10
|
-
STANDARD_SPECIAL_TOKENS =
|
|
16
|
+
STANDARD_SPECIAL_TOKENS = {
|
|
17
|
+
"[PAD]" => Format::ADDED_PAD,
|
|
18
|
+
"[UNK]" => Format::ADDED_UNK,
|
|
19
|
+
"[CLS]" => Format::ADDED_CLS,
|
|
20
|
+
"[SEP]" => Format::ADDED_SEP,
|
|
21
|
+
"[MASK]" => Format::ADDED_MASK
|
|
22
|
+
}.freeze
|
|
11
23
|
SOURCE_FILES = %w[tokenizer.json config.json tokenizer_config.json model.safetensors].freeze
|
|
12
24
|
TOKENIZER_PROFILE = "BERT_WORDPIECE_V1"
|
|
13
25
|
|
|
@@ -22,6 +34,7 @@ module StaticEmbeddings
|
|
|
22
34
|
source = load_source
|
|
23
35
|
profile = audit_tokenizer(source[:tokenizer], source[:tokenizer_config])
|
|
24
36
|
tokens = extract_vocab(source[:tokenizer])
|
|
37
|
+
validate_added_token_ids!(profile[:added_tokens], tokens)
|
|
25
38
|
matrix, dim = extract_matrix(tokens.length)
|
|
26
39
|
meta = runtime_meta(source[:config], profile, tokens, max_tokens)
|
|
27
40
|
|
|
@@ -62,9 +75,13 @@ module StaticEmbeddings
|
|
|
62
75
|
assert_wordpiece!(model)
|
|
63
76
|
assert_normalizer!(normalizer)
|
|
64
77
|
assert_pre_tokenizer!(pre_tokenizer)
|
|
65
|
-
audit_added_tokens(tokenizer)
|
|
78
|
+
added_tokens = audit_added_tokens(tokenizer)
|
|
66
79
|
|
|
67
80
|
profile = profile_from(model, normalizer, tokenizer_config)
|
|
81
|
+
profile[:added_tokens] = added_tokens
|
|
82
|
+
profile[:added_token_mask] = added_tokens.reduce(0) do |mask, token|
|
|
83
|
+
mask | STANDARD_SPECIAL_TOKENS.fetch(token.fetch("content"))
|
|
84
|
+
end
|
|
68
85
|
reject!("clean_text=false is not supported by the runtime") unless profile[:clean_text]
|
|
69
86
|
profile
|
|
70
87
|
end
|
|
@@ -123,17 +140,40 @@ module StaticEmbeddings
|
|
|
123
140
|
def audit_added_tokens(tokenizer)
|
|
124
141
|
added = tokenizer["added_tokens"] || []
|
|
125
142
|
bad_content = added.reject { |token| standard_special?(token) }
|
|
126
|
-
|
|
143
|
+
unless bad_content.empty?
|
|
144
|
+
reject!("tokenizer declares non-standard added_tokens #{bad_content.map { |t| t['content'] }.inspect}")
|
|
145
|
+
end
|
|
127
146
|
|
|
128
147
|
whitespace = added.find { |token| token["content"].to_s.match?(/\s/) }
|
|
129
148
|
reject!("added token #{whitespace['content'].inspect} contains whitespace") if whitespace
|
|
130
149
|
|
|
131
150
|
flagged = added.find { |token| token["lstrip"] || token["rstrip"] || token["single_word"] }
|
|
132
151
|
reject!("added token #{flagged['content'].inspect} uses lstrip/rstrip/single_word") if flagged
|
|
152
|
+
|
|
153
|
+
normalized = added.find { |token| token["normalized"] != false }
|
|
154
|
+
if normalized
|
|
155
|
+
reject!("added token #{normalized['content'].inspect} must use normalized=false")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
duplicate = added.group_by { |token| token["content"] }.find { |_, rows| rows.length > 1 }
|
|
159
|
+
reject!("duplicate added token #{duplicate[0].inspect}") if duplicate
|
|
160
|
+
|
|
161
|
+
added
|
|
133
162
|
end
|
|
134
163
|
|
|
135
164
|
def standard_special?(token)
|
|
136
|
-
token["special"] && STANDARD_SPECIAL_TOKENS.
|
|
165
|
+
token["special"] && STANDARD_SPECIAL_TOKENS.key?(token["content"])
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def validate_added_token_ids!(added_tokens, tokens)
|
|
169
|
+
added_tokens.each do |token|
|
|
170
|
+
content = token.fetch("content")
|
|
171
|
+
id = token["id"]
|
|
172
|
+
reject!("added token #{content.inspect} has non-integer id #{id.inspect}") unless id.is_a?(Integer)
|
|
173
|
+
unless id.between?(0, tokens.length - 1) && tokens[id] == content
|
|
174
|
+
reject!("added token #{content.inspect} id #{id} does not match model.vocab")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
137
177
|
end
|
|
138
178
|
|
|
139
179
|
def extract_vocab(tokenizer)
|
|
@@ -159,7 +199,7 @@ module StaticEmbeddings
|
|
|
159
199
|
path = File.join(source_dir, "model.safetensors")
|
|
160
200
|
raise ConversionError, "missing model.safetensors in #{source_dir}" unless File.file?(path)
|
|
161
201
|
|
|
162
|
-
name, tensor = sole_matrix_tensor(Safetensors.
|
|
202
|
+
name, tensor = sole_matrix_tensor(Safetensors.describe(path)[:tensors])
|
|
163
203
|
rows, dim = tensor[:shape]
|
|
164
204
|
if rows != vocab_size
|
|
165
205
|
raise ConversionError,
|
|
@@ -167,7 +207,7 @@ module StaticEmbeddings
|
|
|
167
207
|
"#{vocab_size} tokens — refusing to guess the mapping"
|
|
168
208
|
end
|
|
169
209
|
|
|
170
|
-
[tensor
|
|
210
|
+
[Safetensors.f32_payload(path, tensor), dim]
|
|
171
211
|
end
|
|
172
212
|
|
|
173
213
|
def sole_matrix_tensor(tensors)
|
|
@@ -203,6 +243,7 @@ module StaticEmbeddings
|
|
|
203
243
|
strip_accents: profile[:strip_accents],
|
|
204
244
|
handle_chinese_chars: profile[:handle_chinese_chars],
|
|
205
245
|
clean_text: profile[:clean_text],
|
|
246
|
+
added_token_mask: profile.fetch(:added_token_mask),
|
|
206
247
|
max_input_chars_per_word: profile[:max_input_chars_per_word],
|
|
207
248
|
max_token_chars: max_token_chars(tokens, profile[:continuing_subword_prefix]),
|
|
208
249
|
subword_prefix: profile[:continuing_subword_prefix]
|
|
@@ -241,7 +282,7 @@ module StaticEmbeddings
|
|
|
241
282
|
def source_digests
|
|
242
283
|
SOURCE_FILES.each_with_object({}) do |name, acc|
|
|
243
284
|
path = File.join(source_dir, name)
|
|
244
|
-
acc[name] = Digest::SHA256.
|
|
285
|
+
acc[name] = Digest::SHA256.file(path).hexdigest if File.file?(path)
|
|
245
286
|
end
|
|
246
287
|
end
|
|
247
288
|
|
|
@@ -252,6 +293,9 @@ module StaticEmbeddings
|
|
|
252
293
|
"source_model_id" => model_id || File.basename(File.expand_path(source_dir)),
|
|
253
294
|
"source_files_sha256" => source_digests,
|
|
254
295
|
"reference_impl" => REFERENCE_IMPL,
|
|
296
|
+
"reference_model2vec_version" => REFERENCE_MODEL2VEC_VERSION,
|
|
297
|
+
"reference_tokenizers_version" => REFERENCE_TOKENIZERS_VERSION,
|
|
298
|
+
"reference_unicode_categories_version" => REFERENCE_UNICODE_CATEGORIES_VERSION,
|
|
255
299
|
"reference_max_tokens" => meta[:max_tokens_default],
|
|
256
300
|
"unicode_source" => UnicodeTables.source_stamp,
|
|
257
301
|
"tokenizer_profile" => TOKENIZER_PROFILE,
|
|
@@ -3,7 +3,7 @@ require "digest"
|
|
|
3
3
|
module StaticEmbeddings
|
|
4
4
|
module Format
|
|
5
5
|
MAGIC = "SEMBv1\0\0"
|
|
6
|
-
VERSION =
|
|
6
|
+
VERSION = 3
|
|
7
7
|
HEADER_SIZE = 320
|
|
8
8
|
ALIGNMENT = 64
|
|
9
9
|
|
|
@@ -12,7 +12,7 @@ module StaticEmbeddings
|
|
|
12
12
|
POOLING_MEAN = 1
|
|
13
13
|
NORMALIZATION_NONE = 0
|
|
14
14
|
NORMALIZATION_L2 = 1
|
|
15
|
-
|
|
15
|
+
TRUNCATE_USABLE_IDS_BEFORE_POOLING = 2
|
|
16
16
|
UNK_INCLUDE = 0
|
|
17
17
|
UNK_DROP = 1
|
|
18
18
|
EMPTY_ZERO_VECTOR = 0
|
|
@@ -26,6 +26,14 @@ module StaticEmbeddings
|
|
|
26
26
|
CHECKSUM_OFFSET = 240
|
|
27
27
|
CHECKSUM_SIZE = 32
|
|
28
28
|
MAX_PROBE_OFFSET = 304
|
|
29
|
+
ADDED_TOKEN_MASK_OFFSET = 308
|
|
30
|
+
|
|
31
|
+
ADDED_PAD = 1 << 0
|
|
32
|
+
ADDED_UNK = 1 << 1
|
|
33
|
+
ADDED_CLS = 1 << 2
|
|
34
|
+
ADDED_SEP = 1 << 3
|
|
35
|
+
ADDED_MASK = 1 << 4
|
|
36
|
+
ADDED_TOKEN_MASK_ALL = ADDED_PAD | ADDED_UNK | ADDED_CLS | ADDED_SEP | ADDED_MASK
|
|
29
37
|
|
|
30
38
|
SECTION_FIELDS = {
|
|
31
39
|
vocab_strings: 128,
|
|
@@ -44,7 +52,7 @@ module StaticEmbeddings
|
|
|
44
52
|
28 => TOKENIZER_BERT_WORDPIECE_V1,
|
|
45
53
|
32 => DTYPE_F32,
|
|
46
54
|
36 => POOLING_MEAN,
|
|
47
|
-
48 =>
|
|
55
|
+
48 => TRUNCATE_USABLE_IDS_BEFORE_POOLING,
|
|
48
56
|
108 => HASH_SEED
|
|
49
57
|
}.freeze
|
|
50
58
|
|
|
@@ -102,7 +110,7 @@ module StaticEmbeddings
|
|
|
102
110
|
def write(path:, meta:, tokens:, matrix:, norm_tables:, provenance:)
|
|
103
111
|
hash_size, strings, hash_blob, max_probe = build_hash_table(tokens)
|
|
104
112
|
root_trie, continuation_trie = build_wordpiece_tries(tokens, meta.fetch(:subword_prefix))
|
|
105
|
-
|
|
113
|
+
payloads = {
|
|
106
114
|
vocab_strings: strings,
|
|
107
115
|
vocab_hash: hash_blob,
|
|
108
116
|
embeddings: matrix,
|
|
@@ -110,15 +118,48 @@ module StaticEmbeddings
|
|
|
110
118
|
provenance: provenance,
|
|
111
119
|
root_trie: root_trie,
|
|
112
120
|
continuation_trie: continuation_trie
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
}
|
|
122
|
+
sections, file_size = layout_sections(payloads)
|
|
115
123
|
header = build_header(meta, tokens.length, hash_size, max_probe, sections)
|
|
116
|
-
file = header << body
|
|
117
|
-
digest = Digest::SHA256.digest(file)
|
|
118
|
-
file[CHECKSUM_OFFSET, CHECKSUM_SIZE] = digest
|
|
119
124
|
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
digest = Digest::SHA256.new
|
|
126
|
+
File.open(path, "wb") do |io|
|
|
127
|
+
io.write(header)
|
|
128
|
+
digest << header
|
|
129
|
+
offset = HEADER_SIZE
|
|
130
|
+
|
|
131
|
+
payloads.each do |name, payload|
|
|
132
|
+
target = sections.fetch(name).first
|
|
133
|
+
padding = target - offset
|
|
134
|
+
if padding.positive?
|
|
135
|
+
zeros = "\0".b * padding
|
|
136
|
+
io.write(zeros)
|
|
137
|
+
digest << zeros
|
|
138
|
+
end
|
|
139
|
+
write_payload(io, digest, payload)
|
|
140
|
+
offset = target + payload.bytesize
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
checksum = digest.digest
|
|
145
|
+
File.open(path, "r+b") do |io|
|
|
146
|
+
io.seek(CHECKSUM_OFFSET, IO::SEEK_SET)
|
|
147
|
+
io.write(checksum)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
{ bytes: file_size, sha256: checksum.unpack1("H*"), hash_table_size: hash_size }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def write_payload(io, digest, payload)
|
|
154
|
+
if payload.respond_to?(:each_chunk)
|
|
155
|
+
payload.each_chunk do |chunk|
|
|
156
|
+
io.write(chunk)
|
|
157
|
+
digest << chunk
|
|
158
|
+
end
|
|
159
|
+
else
|
|
160
|
+
io.write(payload)
|
|
161
|
+
digest << payload
|
|
162
|
+
end
|
|
122
163
|
end
|
|
123
164
|
|
|
124
165
|
def verify(path)
|
|
@@ -240,20 +281,16 @@ module StaticEmbeddings
|
|
|
240
281
|
end
|
|
241
282
|
end
|
|
242
283
|
|
|
243
|
-
def
|
|
244
|
-
|
|
284
|
+
def layout_sections(payloads)
|
|
285
|
+
offset = HEADER_SIZE
|
|
245
286
|
sections = {}
|
|
246
287
|
payloads.each do |name, payload|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
288
|
+
padding = (ALIGNMENT - (offset % ALIGNMENT)) % ALIGNMENT
|
|
289
|
+
offset += padding
|
|
290
|
+
sections[name] = [offset, payload.bytesize]
|
|
291
|
+
offset += payload.bytesize
|
|
250
292
|
end
|
|
251
|
-
[sections,
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def align_body!(body)
|
|
255
|
-
padding = (ALIGNMENT - ((HEADER_SIZE + body.bytesize) % ALIGNMENT)) % ALIGNMENT
|
|
256
|
-
body << "\0".b * padding if padding.positive?
|
|
293
|
+
[sections, offset]
|
|
257
294
|
end
|
|
258
295
|
|
|
259
296
|
def build_header(meta, vocab_size, hash_size, max_probe, sections)
|
|
@@ -268,6 +305,7 @@ module StaticEmbeddings
|
|
|
268
305
|
put_u32(header, 104, hash_size)
|
|
269
306
|
put_u32(header, MAX_TOKEN_CHARS_OFFSET, meta.fetch(:max_token_chars))
|
|
270
307
|
put_u32(header, MAX_PROBE_OFFSET, max_probe)
|
|
308
|
+
put_u32(header, ADDED_TOKEN_MASK_OFFSET, meta.fetch(:added_token_mask, 0))
|
|
271
309
|
put_prefix(header, meta.fetch(:subword_prefix))
|
|
272
310
|
SECTION_FIELDS.each { |name, field| put_section(header, field, sections.fetch(name)) }
|
|
273
311
|
|
|
@@ -11,7 +11,24 @@ module StaticEmbeddings
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def model_path(model_id, env = ENV)
|
|
14
|
-
|
|
14
|
+
id = model_id.to_s
|
|
15
|
+
raise ArgumentError, "model_id must not be empty" if id.empty?
|
|
16
|
+
raise ArgumentError, "model_id contains a NUL byte" if id.include?("\0")
|
|
17
|
+
|
|
18
|
+
normalized = id.tr("\\", "/")
|
|
19
|
+
parts = normalized.split("/", -1)
|
|
20
|
+
if normalized.start_with?("/") ||
|
|
21
|
+
parts.any? { |part| part.empty? || part == "." || part == ".." || part.include?(":") }
|
|
22
|
+
raise ArgumentError, "model_id must be a relative slash-separated identifier"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
base = File.expand_path(File.join(cache_dir(env), "models"))
|
|
26
|
+
path = File.expand_path(File.join(base, "#{normalized}.semb"))
|
|
27
|
+
prefix = base.end_with?(File::SEPARATOR) ? base : "#{base}#{File::SEPARATOR}"
|
|
28
|
+
unless path.start_with?(prefix)
|
|
29
|
+
raise ArgumentError, "model_id escapes the model cache"
|
|
30
|
+
end
|
|
31
|
+
path
|
|
15
32
|
end
|
|
16
33
|
|
|
17
34
|
def builtin_path(name)
|