static_embeddings 0.1.4 → 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.
@@ -102,7 +102,7 @@ int se_vocab_lookup_piece(const se_model_t *model, const uint8_t *prefix, size_t
102
102
  uint32_t pos = h & mask;
103
103
  uint32_t total_len = (uint32_t)(prefix_len + len);
104
104
 
105
- for (uint32_t probe = 0; probe <= mask; probe++) {
105
+ for (uint32_t probe = 0; probe < model->meta.max_probe; probe++) {
106
106
  const se_vocab_slot_t *slot = &model->vocab_hash[pos];
107
107
  if (slot->token_id == SE_SLOT_EMPTY)
108
108
  return 0;
@@ -556,6 +556,31 @@ static se_status_t validate_vocab_hash(const se_model_t *model, struct section v
556
556
  se_error_set(err, SE_ERR_INVALID_FORMAT,
557
557
  "[UNK] is not reachable through the vocabulary hash");
558
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
+ }
559
584
  }
560
585
 
561
586
  done:
@@ -646,6 +671,7 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
646
671
  m->subword_prefix_len = read_u32(base, SE_OFF_SUBWORD_PREFIX_LEN);
647
672
  m->max_token_chars = read_u32(base, SE_OFF_MAX_TOKEN_CHARS);
648
673
  m->max_probe = read_u32(base, SE_OFF_MAX_PROBE);
674
+ m->added_token_mask = read_u32(base, SE_OFF_ADDED_TOKEN_MASK);
649
675
  memcpy(m->subword_prefix, base + SE_OFF_SUBWORD_PREFIX, 8);
650
676
 
651
677
  if (m->tokenizer_type != SE_TOKENIZER_BERT_WORDPIECE_V1) {
@@ -669,7 +695,7 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
669
695
  m->normalization_type);
670
696
  return SE_ERR_INVALID_FORMAT;
671
697
  }
672
- if (m->truncation_policy != SE_TRUNCATE_IDS_BEFORE_POOLING) {
698
+ if (m->truncation_policy != SE_TRUNCATE_USABLE_IDS_BEFORE_POOLING) {
673
699
  se_error_set(err, SE_ERR_INVALID_FORMAT, "truncation policy %u is not supported",
674
700
  m->truncation_policy);
675
701
  return SE_ERR_INVALID_FORMAT;
@@ -682,6 +708,11 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
682
708
  se_error_set(err, SE_ERR_INVALID_FORMAT, "unknown UNK policy %u", m->unk_policy);
683
709
  return SE_ERR_INVALID_FORMAT;
684
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
+ }
685
716
  if (m->empty_policy != SE_EMPTY_ZERO_VECTOR && m->empty_policy != SE_EMPTY_RAISE) {
686
717
  se_error_set(err, SE_ERR_INVALID_FORMAT, "unknown empty-input policy %u", m->empty_policy);
687
718
  return SE_ERR_INVALID_FORMAT;
@@ -704,8 +735,14 @@ static se_status_t validate(se_model_t *model, se_error_t *err) {
704
735
  m->hash_table_size);
705
736
  return SE_ERR_INVALID_FORMAT;
706
737
  }
707
- if (m->hash_table_size < m->vocab_size) {
708
- se_error_set(err, SE_ERR_INVALID_FORMAT, "hash table smaller than vocabulary");
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);
709
746
  return SE_ERR_INVALID_FORMAT;
710
747
  }
711
748
  if (m->subword_prefix_len > 8) {
@@ -8,7 +8,7 @@
8
8
 
9
9
  #define SE_MAGIC "SEMBv1\0\0"
10
10
  #define SE_MAGIC_LEN 8
11
- #define SE_FORMAT_VERSION 2u
11
+ #define SE_FORMAT_VERSION 3u
12
12
  #define SE_HEADER_SIZE 320u
13
13
  #define SE_ALIGNMENT 64u
14
14
 
@@ -18,7 +18,7 @@
18
18
  #define SE_NORMALIZATION_NONE 0u
19
19
  #define SE_NORMALIZATION_L2 1u
20
20
 
21
- #define SE_TRUNCATE_IDS_BEFORE_POOLING 1u
21
+ #define SE_TRUNCATE_USABLE_IDS_BEFORE_POOLING 2u
22
22
 
23
23
  #define SE_UNK_INCLUDE 0u
24
24
  #define SE_UNK_DROP 1u
@@ -26,6 +26,14 @@
26
26
  #define SE_EMPTY_ZERO_VECTOR 0u
27
27
  #define SE_EMPTY_RAISE 1u
28
28
 
29
+ #define SE_ADDED_PAD (1u << 0)
30
+ #define SE_ADDED_UNK (1u << 1)
31
+ #define SE_ADDED_CLS (1u << 2)
32
+ #define SE_ADDED_SEP (1u << 3)
33
+ #define SE_ADDED_MASK (1u << 4)
34
+ #define SE_ADDED_TOKEN_MASK_ALL \
35
+ (SE_ADDED_PAD | SE_ADDED_UNK | SE_ADDED_CLS | SE_ADDED_SEP | SE_ADDED_MASK)
36
+
29
37
  #define SE_SLOT_EMPTY 0xFFFFFFFFu
30
38
  #define SE_SIZE_MAX ((size_t)-1)
31
39
 
@@ -83,7 +91,8 @@ enum {
83
91
  SE_OFF_SEC_ROOT_TRIE = 208,
84
92
  SE_OFF_SEC_CONT_TRIE = 224,
85
93
  SE_OFF_CHECKSUM = 240,
86
- SE_OFF_MAX_PROBE = 304
94
+ SE_OFF_MAX_PROBE = 304,
95
+ SE_OFF_ADDED_TOKEN_MASK = 308
87
96
  };
88
97
 
89
98
  typedef struct {
@@ -163,6 +172,7 @@ typedef struct {
163
172
  uint32_t max_input_chars_per_word;
164
173
  uint32_t max_token_chars;
165
174
  uint32_t max_probe;
175
+ uint32_t added_token_mask;
166
176
  uint32_t pad_id;
167
177
  uint32_t unk_id;
168
178
  uint32_t cls_id;
@@ -401,9 +411,11 @@ typedef struct {
401
411
  uint32_t truncated;
402
412
  } se_token_stats_t;
403
413
 
414
+ typedef enum { SE_TOKEN_LIMIT_RAW = 0, SE_TOKEN_LIMIT_USABLE = 1 } se_token_limit_t;
415
+
404
416
  se_status_t se_tokenize(const se_model_t *model, se_scratch_t *scratch, const uint8_t *input,
405
- size_t input_len, uint32_t max_tokens, se_token_stats_t *stats,
406
- se_error_t *err, volatile sig_atomic_t *cancelled);
417
+ size_t input_len, uint32_t max_tokens, se_token_limit_t limit_mode,
418
+ se_token_stats_t *stats, se_error_t *err, volatile sig_atomic_t *cancelled);
407
419
 
408
420
  se_status_t se_embed_one(const se_model_t *model, se_scratch_t *scratch, const uint8_t *input,
409
421
  size_t input_len, uint32_t max_tokens, float *out, se_token_stats_t *stats,
@@ -87,16 +87,17 @@ static int push_id(se_scratch_t *sc, size_t *n_ids, uint32_t id) {
87
87
  return 1;
88
88
  }
89
89
 
90
- #define SE_SCRATCH_CPS_KEEP 256
91
- #define SE_SCRATCH_IDS_KEEP 512
92
- #define SE_SCRATCH_BYTES_KEEP 1024
90
+ #define SE_SCRATCH_CPS_KEEP 256
91
+ #define SE_SCRATCH_IDS_RESERVE 512
92
+ #define SE_SCRATCH_IDS_RETAIN_MAX 8192
93
+ #define SE_SCRATCH_BYTES_KEEP 1024
93
94
 
94
95
  int se_scratch_reserve(se_scratch_t *s, uint32_t dim) {
95
96
  if (!grow_u32(&s->cps, &s->cps_cap, SE_SCRATCH_CPS_KEEP))
96
97
  return 0;
97
98
  if (!grow_u32(&s->cps2, &s->cps2_cap, SE_SCRATCH_CPS_KEEP))
98
99
  return 0;
99
- if (!grow_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_KEEP))
100
+ if (!grow_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_RESERVE))
100
101
  return 0;
101
102
  if (!grow_bytes(&s->bytes, &s->bytes_cap, SE_SCRATCH_BYTES_KEEP))
102
103
  return 0;
@@ -149,7 +150,8 @@ static int shrink_bytes(uint8_t **buf, size_t *cap, size_t keep) {
149
150
  static void se_scratch_trim(se_scratch_t *s) {
150
151
  (void)shrink_u32(&s->cps, &s->cps_cap, SE_SCRATCH_CPS_KEEP);
151
152
  (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
+ if (s->ids_cap > SE_SCRATCH_IDS_RETAIN_MAX)
154
+ (void)shrink_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_RETAIN_MAX);
153
155
  (void)shrink_bytes(&s->bytes, &s->bytes_cap, SE_SCRATCH_BYTES_KEEP);
154
156
  }
155
157
 
@@ -393,6 +395,37 @@ static int cp_is_cjk_segment(const se_model_t *m, uint32_t cp) {
393
395
  return m->meta.handle_chinese_chars && cp >= 0x3400 && se_is_cjk(cp);
394
396
  }
395
397
 
398
+ typedef struct {
399
+ const char *text;
400
+ uint32_t len;
401
+ uint32_t bit;
402
+ } se_added_token_spec_t;
403
+
404
+ static const se_added_token_spec_t SE_ADDED_TOKENS[] = {
405
+ {"[MASK]", 6u, SE_ADDED_MASK}, {"[PAD]", 5u, SE_ADDED_PAD}, {"[UNK]", 5u, SE_ADDED_UNK},
406
+ {"[CLS]", 5u, SE_ADDED_CLS}, {"[SEP]", 5u, SE_ADDED_SEP},
407
+ };
408
+
409
+ static int boundary_splits_added_token(const se_model_t *model, const uint8_t *input,
410
+ size_t input_len, size_t boundary) {
411
+ if (model->meta.added_token_mask == 0 || boundary == 0 || boundary >= input_len)
412
+ return 0;
413
+
414
+ for (size_t k = 0; k < SE_ARRAY_LEN(SE_ADDED_TOKENS); k++) {
415
+ const se_added_token_spec_t *token = &SE_ADDED_TOKENS[k];
416
+ if ((model->meta.added_token_mask & token->bit) == 0)
417
+ continue;
418
+ for (size_t back = 1; back < token->len && back <= boundary; back++) {
419
+ size_t start = boundary - back;
420
+ if (start + token->len > input_len || input[start] != '[')
421
+ continue;
422
+ if (memcmp(input + start, token->text, token->len) == 0)
423
+ return 1;
424
+ }
425
+ }
426
+ return 0;
427
+ }
428
+
396
429
  size_t se_prefix_boundary_len(const se_model_t *model, const uint8_t *input, size_t input_len,
397
430
  size_t target, size_t backscan) {
398
431
  if (target >= input_len)
@@ -420,14 +453,15 @@ size_t se_prefix_boundary_len(const se_model_t *model, const uint8_t *input, siz
420
453
  continue;
421
454
 
422
455
  if (cp_is_cjk_segment(model, cp)) {
423
- if (after <= target)
456
+ if (after <= target && !boundary_splits_added_token(model, input, input_len, after))
424
457
  return after;
425
- if (pos > 0)
458
+ if (pos > 0 && !boundary_splits_added_token(model, input, input_len, pos))
426
459
  return pos;
427
- return 0;
460
+ continue;
428
461
  }
429
462
 
430
- if ((is_whitespace(model, cp) || is_punct(model, cp)) && after <= target)
463
+ if ((is_whitespace(model, cp) || is_punct(model, cp)) && after <= target &&
464
+ !boundary_splits_added_token(model, input, input_len, after))
431
465
  return after;
432
466
  }
433
467
 
@@ -454,6 +488,7 @@ typedef struct {
454
488
  const se_model_t *model;
455
489
  se_scratch_t *scratch;
456
490
  uint32_t max_tokens;
491
+ se_token_limit_t limit_mode;
457
492
  size_t n_ids;
458
493
  size_t n_unk;
459
494
  size_t segment_len;
@@ -481,17 +516,44 @@ static int append_segment_cp(token_state_t *st, uint32_t cp) {
481
516
  return 1;
482
517
  }
483
518
 
519
+ static size_t limited_token_count(const token_state_t *st) {
520
+ if (st->limit_mode == SE_TOKEN_LIMIT_USABLE && st->model->meta.unk_policy == SE_UNK_DROP)
521
+ return st->n_ids - st->n_unk;
522
+ return st->n_ids;
523
+ }
524
+
484
525
  static int cap_after_append(token_state_t *st) {
485
- if (st->max_tokens == 0 || st->n_ids <= (size_t)st->max_tokens)
526
+ if (st->max_tokens == 0 || limited_token_count(st) <= (size_t)st->max_tokens)
486
527
  return 0;
487
528
 
488
- size_t dropped_unk = 0;
489
- for (size_t k = st->max_tokens; k < st->n_ids; k++) {
490
- if (st->scratch->ids[k] == st->model->meta.unk_id)
491
- dropped_unk++;
529
+ if (st->limit_mode == SE_TOKEN_LIMIT_RAW || st->model->meta.unk_policy != SE_UNK_DROP) {
530
+ size_t dropped_unk = 0;
531
+ for (size_t k = st->max_tokens; k < st->n_ids; k++) {
532
+ if (st->scratch->ids[k] == st->model->meta.unk_id)
533
+ dropped_unk++;
534
+ }
535
+ st->n_unk -= dropped_unk;
536
+ st->n_ids = st->max_tokens;
537
+ } else {
538
+ size_t usable = 0;
539
+ size_t kept_unk = 0;
540
+ size_t keep = 0;
541
+ for (size_t k = 0; k < st->n_ids; k++) {
542
+ uint32_t id = st->scratch->ids[k];
543
+ if (id == st->model->meta.unk_id) {
544
+ kept_unk++;
545
+ continue;
546
+ }
547
+ usable++;
548
+ if (usable == (size_t)st->max_tokens) {
549
+ keep = k + 1;
550
+ break;
551
+ }
552
+ }
553
+ st->n_ids = keep;
554
+ st->n_unk = kept_unk;
492
555
  }
493
- st->n_unk -= dropped_unk;
494
- st->n_ids = st->max_tokens;
556
+
495
557
  st->stats->truncated = 1;
496
558
  return 1;
497
559
  }
@@ -602,6 +664,54 @@ static wordpiece_status_t wordpiece(const se_model_t *m, se_scratch_t *sc, const
602
664
  return 1;
603
665
  }
604
666
 
667
+ static uint32_t added_token_id(const se_model_t *model, uint32_t bit) {
668
+ switch (bit) {
669
+ case SE_ADDED_PAD:
670
+ return model->meta.pad_id;
671
+ case SE_ADDED_UNK:
672
+ return model->meta.unk_id;
673
+ case SE_ADDED_CLS:
674
+ return model->meta.cls_id;
675
+ case SE_ADDED_SEP:
676
+ return model->meta.sep_id;
677
+ case SE_ADDED_MASK:
678
+ return model->meta.mask_id;
679
+ default:
680
+ return SE_SLOT_EMPTY;
681
+ }
682
+ }
683
+
684
+ static int match_added_token(const se_model_t *model, const uint8_t *input, size_t input_len,
685
+ size_t pos, uint32_t *id_out, size_t *len_out) {
686
+ if (model->meta.added_token_mask == 0 || pos >= input_len || input[pos] != '[')
687
+ return 0;
688
+
689
+ for (size_t k = 0; k < SE_ARRAY_LEN(SE_ADDED_TOKENS); k++) {
690
+ const se_added_token_spec_t *token = &SE_ADDED_TOKENS[k];
691
+ if ((model->meta.added_token_mask & token->bit) == 0)
692
+ continue;
693
+ if (token->len > input_len - pos)
694
+ continue;
695
+ if (memcmp(input + pos, token->text, token->len) != 0)
696
+ continue;
697
+
698
+ *id_out = added_token_id(model, token->bit);
699
+ *len_out = token->len;
700
+ return 1;
701
+ }
702
+ return 0;
703
+ }
704
+
705
+ static se_status_t append_direct_id(token_state_t *st, uint32_t id, int *stop) {
706
+ if (!push_id(st->scratch, &st->n_ids, id))
707
+ return oom(st, "adding a special token");
708
+ if (id == st->model->meta.unk_id)
709
+ st->n_unk++;
710
+ if (cap_after_append(st))
711
+ *stop = 1;
712
+ return SE_OK;
713
+ }
714
+
605
715
  static se_status_t append_wordpiece(token_state_t *st, const uint32_t *word, size_t word_len,
606
716
  int *stop) {
607
717
  wordpiece_status_t wp =
@@ -752,6 +862,31 @@ static se_status_t tokenize_ascii_run(token_state_t *st, const uint8_t *input, s
752
862
  if (b >= 0x80)
753
863
  break;
754
864
 
865
+ if (b == '[' && st->model->meta.added_token_mask != 0) {
866
+ uint32_t added_id = 0;
867
+ size_t added_len = 0;
868
+ if (match_added_token(st->model, input, input_len, i, &added_id, &added_len)) {
869
+ se_status_t rc = flush_segment(st, stop);
870
+ if (rc != SE_OK) {
871
+ *ip = i;
872
+ return rc;
873
+ }
874
+ if (*stop) {
875
+ *ip = i;
876
+ return SE_OK;
877
+ }
878
+ rc = append_direct_id(st, added_id, stop);
879
+ if (rc != SE_OK) {
880
+ *ip = i;
881
+ return rc;
882
+ }
883
+ i += added_len;
884
+ if (*stop)
885
+ break;
886
+ continue;
887
+ }
888
+ }
889
+
755
890
  if (i >= next_cancel_check) {
756
891
  if (token_cancelled(st)) {
757
892
  *ip = i;
@@ -811,8 +946,9 @@ static se_status_t tokenize_ascii_run(token_state_t *st, const uint8_t *input, s
811
946
  }
812
947
 
813
948
  se_status_t se_tokenize(const se_model_t *model, se_scratch_t *sc, const uint8_t *input,
814
- size_t input_len, uint32_t max_tokens, se_token_stats_t *stats,
815
- se_error_t *err, volatile sig_atomic_t *cancelled) {
949
+ size_t input_len, uint32_t max_tokens, se_token_limit_t limit_mode,
950
+ se_token_stats_t *stats, se_error_t *err,
951
+ volatile sig_atomic_t *cancelled) {
816
952
  memset(stats, 0, sizeof(*stats));
817
953
 
818
954
  token_state_t st;
@@ -820,6 +956,7 @@ se_status_t se_tokenize(const se_model_t *model, se_scratch_t *sc, const uint8_t
820
956
  st.model = model;
821
957
  st.scratch = sc;
822
958
  st.max_tokens = max_tokens;
959
+ st.limit_mode = limit_mode;
823
960
  st.stats = stats;
824
961
  st.err = err;
825
962
  st.cancelled = cancelled;
@@ -115,6 +115,6 @@ const se_map_entry_t *se_map_lookup(const se_map_entry_t *entries, uint32_t coun
115
115
  int se_is_cjk(uint32_t cp) {
116
116
  return (cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3400 && cp <= 0x4DBF) ||
117
117
  (cp >= 0x20000 && cp <= 0x2A6DF) || (cp >= 0x2A700 && cp <= 0x2B73F) ||
118
- (cp >= 0x2B740 && cp <= 0x2B81F) || (cp >= 0x2B820 && cp <= 0x2CEAF) ||
118
+ (cp >= 0x2B740 && cp <= 0x2B81F) || (cp >= 0x2B920 && cp <= 0x2CEAF) ||
119
119
  (cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0x2F800 && cp <= 0x2FA1F);
120
120
  }
@@ -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;
@@ -1030,6 +1031,10 @@ static VALUE model_embed_with_stats(int argc, VALUE *argv, VALUE self) {
1030
1031
  rb_hash_aset(hash, ID2SYM(id_vector), vector);
1031
1032
  rb_hash_aset(hash, ID2SYM(id_token_count), UINT2NUM(stats.token_count));
1032
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));
1033
1038
  rb_hash_aset(hash, ID2SYM(id_truncated), stats.truncated ? Qtrue : Qfalse);
1034
1039
  return hash;
1035
1040
  }
@@ -1049,7 +1054,7 @@ typedef struct {
1049
1054
  static VALUE tokenize_scratch_body(VALUE arg) {
1050
1055
  tokenize_scratch_job_t *job = (tokenize_scratch_job_t *)(uintptr_t)arg;
1051
1056
  job->rc = se_tokenize(job->model, job->scratch, job->input, job->input_len, job->max_tokens,
1052
- &job->stats, &job->err, NULL);
1057
+ SE_TOKEN_LIMIT_RAW, &job->stats, &job->err, NULL);
1053
1058
  if (job->rc != SE_OK)
1054
1059
  return Qnil;
1055
1060
 
@@ -1237,7 +1242,27 @@ static VALUE embed_token_ids_value(VALUE self, VALUE ids_value, VALUE max_tokens
1237
1242
  size_t n = (size_t)n_long;
1238
1243
  int truncated = 0;
1239
1244
 
1240
- if (max_tokens != 0 && n > (size_t)max_tokens) {
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) {
1241
1266
  n = (size_t)max_tokens;
1242
1267
  truncated = 1;
1243
1268
  }
@@ -1297,6 +1322,10 @@ static VALUE model_embed_token_ids_with_stats(int argc, VALUE *argv, VALUE self)
1297
1322
  rb_hash_aset(hash, ID2SYM(id_vector), vector);
1298
1323
  rb_hash_aset(hash, ID2SYM(id_token_count), UINT2NUM(stats.token_count));
1299
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));
1300
1329
  rb_hash_aset(hash, ID2SYM(id_truncated), stats.truncated ? Qtrue : Qfalse);
1301
1330
  return hash;
1302
1331
  }
@@ -1607,6 +1636,7 @@ RUBY_FUNC_EXPORTED void Init_static_embeddings(void) {
1607
1636
  id_vector = rb_intern("vector");
1608
1637
  id_token_count = rb_intern("token_count");
1609
1638
  id_unk_count = rb_intern("unk_count");
1639
+ id_pooled_count = rb_intern("pooled_count");
1610
1640
  id_truncated = rb_intern("truncated");
1611
1641
  id_dim = rb_intern("dim");
1612
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 = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"].freeze
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
- reject!("tokenizer declares non-standard added_tokens #{bad_content.map { |t| t['content'] }.inspect}") unless bad_content.empty?
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.include?(token["content"])
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.read(path)[:tensors])
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[:bytes], dim]
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.hexdigest(File.binread(path)) if File.file?(path)
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,