static_embeddings 0.1.5 → 1.5.7

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +17 -2
  4. data/docs/ARCHITECTURE.md +14 -5
  5. data/docs/MODEL_AUDIT.md +110 -0
  6. data/ext/static_embeddings/se_alloc_stats.c +5 -6
  7. data/ext/static_embeddings/se_f16.c +1 -1
  8. data/ext/static_embeddings/se_tokenizer.c +6 -0
  9. data/ext/static_embeddings/se_topk.c +1 -2
  10. data/ext/static_embeddings/static_embeddings.c +60 -10
  11. data/lib/models/demo.semb +0 -0
  12. data/lib/static_embeddings/bert_wordpiece.rb +191 -0
  13. data/lib/static_embeddings/canonical.rb +50 -0
  14. data/lib/static_embeddings/cli.rb +88 -63
  15. data/lib/static_embeddings/codec.rb +45 -0
  16. data/lib/static_embeddings/conversion.rb +58 -0
  17. data/lib/static_embeddings/errors.rb +2 -2
  18. data/lib/static_embeddings/format/constants.rb +109 -0
  19. data/lib/static_embeddings/format/hash_table.rb +69 -0
  20. data/lib/static_embeddings/format/trie.rb +78 -0
  21. data/lib/static_embeddings/format/verifier.rb +41 -0
  22. data/lib/static_embeddings/format/writer.rb +131 -0
  23. data/lib/static_embeddings/format.rb +3 -338
  24. data/lib/static_embeddings/importers/model2vec.rb +52 -0
  25. data/lib/static_embeddings/importers/sentence_transformers_static.rb +103 -0
  26. data/lib/static_embeddings/importers/support.rb +111 -0
  27. data/lib/static_embeddings/importers.rb +50 -0
  28. data/lib/static_embeddings/model.rb +35 -20
  29. data/lib/static_embeddings/paths.rb +8 -12
  30. data/lib/static_embeddings/provenance.rb +58 -0
  31. data/lib/static_embeddings/reference.rb +50 -40
  32. data/lib/static_embeddings/row_prefix_payload.rb +59 -0
  33. data/lib/static_embeddings/version.rb +1 -1
  34. data/lib/static_embeddings.rb +29 -55
  35. data/static_embeddings.gemspec +2 -2
  36. data/tools/check_model2vec_parity.rb +5 -1
  37. data/tools/check_st_parity.rb +125 -0
  38. data/tools/eval_retrieval.rb +58 -0
  39. metadata +24 -6
  40. data/lib/static_embeddings/converter.rb +0 -328
@@ -0,0 +1,109 @@
1
+ module StaticEmbeddings
2
+ module Format
3
+ MAGIC = "SEMBv1\0\0"
4
+ VERSION = 3
5
+ VERSION_WORDPIECE = VERSION
6
+ HEADER_SIZE = 320
7
+ ALIGNMENT = 64
8
+
9
+ TOKENIZER_BERT_WORDPIECE_V1 = 1
10
+ DTYPE_F32 = 1
11
+ POOLING_MEAN = 1
12
+ NORMALIZATION_NONE = 0
13
+ NORMALIZATION_L2 = 1
14
+ TRUNCATE_USABLE_IDS_BEFORE_POOLING = 2
15
+ UNK_INCLUDE = 0
16
+ UNK_DROP = 1
17
+ EMPTY_ZERO_VECTOR = 0
18
+ EMPTY_RAISE = 1
19
+
20
+ SLOT_EMPTY = 0xFFFFFFFF
21
+ HASH_SEED = 2_166_136_261
22
+ LOAD_FACTOR = 0.70
23
+ UINT32_MAX = 0xFFFFFFFF
24
+ UINT64_MAX = 0xFFFFFFFFFFFFFFFF
25
+
26
+ MAX_TOKEN_CHARS_OFFSET = 124
27
+ CHECKSUM_OFFSET = 240
28
+ CHECKSUM_SIZE = 32
29
+ MAX_PROBE_OFFSET = 304
30
+ ADDED_TOKEN_MASK_OFFSET = 308
31
+
32
+ ADDED_PAD = 1 << 0
33
+ ADDED_UNK = 1 << 1
34
+ ADDED_CLS = 1 << 2
35
+ ADDED_SEP = 1 << 3
36
+ ADDED_MASK = 1 << 4
37
+ ADDED_TOKEN_MASK_ALL = ADDED_PAD | ADDED_UNK | ADDED_CLS | ADDED_SEP | ADDED_MASK
38
+
39
+ SECTION_FIELDS = {
40
+ vocab_strings: 128,
41
+ vocab_hash: 144,
42
+ embeddings: 160,
43
+ norm_tables: 176,
44
+ provenance: 192,
45
+ root_trie: 208,
46
+ continuation_trie: 224
47
+ }.freeze
48
+
49
+ HEADER_U32 = {
50
+ 8 => VERSION,
51
+ 12 => HEADER_SIZE,
52
+ 16 => 1,
53
+ 28 => TOKENIZER_BERT_WORDPIECE_V1,
54
+ 32 => DTYPE_F32,
55
+ 36 => POOLING_MEAN,
56
+ 48 => TRUNCATE_USABLE_IDS_BEFORE_POOLING,
57
+ 108 => HASH_SEED
58
+ }.freeze
59
+
60
+ META_U32 = {
61
+ 20 => :dim,
62
+ 40 => :normalization_type,
63
+ 44 => :max_tokens_default,
64
+ 56 => :unk_policy,
65
+ 60 => :empty_policy,
66
+ 80 => :max_input_chars_per_word,
67
+ 84 => :pad_id,
68
+ 88 => :unk_id,
69
+ 92 => :cls_id,
70
+ 96 => :sep_id,
71
+ 100 => :mask_id
72
+ }.freeze
73
+
74
+ META_BOOL = {
75
+ 52 => :add_special_tokens,
76
+ 64 => :do_lower_case,
77
+ 68 => :strip_accents,
78
+ 72 => :handle_chinese_chars,
79
+ 76 => :clean_text
80
+ }.freeze
81
+
82
+ module_function
83
+
84
+ def binary_string(capacity = nil)
85
+ string = capacity ? String.new(capacity: capacity) : +""
86
+ string.force_encoding(Encoding::BINARY)
87
+ end
88
+
89
+ def verify(path)
90
+ require "static_embeddings/format/verifier"
91
+ Verifier.call(path)
92
+ end
93
+
94
+ def write(**kwargs)
95
+ require "static_embeddings/format/writer"
96
+ Writer.call(**kwargs)
97
+ end
98
+
99
+ def hash_bytes(string, seed = HASH_SEED)
100
+ require "static_embeddings/format/hash_table"
101
+ HashTable.hash_bytes(string, seed)
102
+ end
103
+
104
+ def next_power_of_two(value)
105
+ require "static_embeddings/format/hash_table"
106
+ HashTable.next_power_of_two(value)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,69 @@
1
+ require "static_embeddings/format/constants"
2
+
3
+ module StaticEmbeddings
4
+ module Format
5
+ module HashTable
6
+ module_function
7
+
8
+ def build(tokens)
9
+ size = next_power_of_two((tokens.length / LOAD_FACTOR).ceil + 1)
10
+ slots = Array.new(size)
11
+ strings = Format.binary_string
12
+ max_probe = 0
13
+
14
+ tokens.each_with_index do |token, id|
15
+ bytes = token.b
16
+ offset = strings.bytesize
17
+ strings << bytes
18
+ probe = insert(slots, tokens, size, bytes, hash_bytes(bytes), offset, id)
19
+ max_probe = probe if probe > max_probe
20
+ end
21
+
22
+ [size, strings, pack(slots), max_probe]
23
+ end
24
+
25
+ def hash_bytes(string, seed = HASH_SEED)
26
+ string.each_byte.reduce(seed) { |hash, byte| ((hash ^ byte) * 16_777_619) & UINT32_MAX }
27
+ end
28
+
29
+ def next_power_of_two(value)
30
+ 1 << (value - 1).bit_length
31
+ end
32
+
33
+ def insert(slots, tokens, size, bytes, hash, offset, id)
34
+ position = hash & (size - 1)
35
+ probe = 1
36
+
37
+ loop do
38
+ slot = slots[position]
39
+ unless slot
40
+ slots[position] = [hash, offset, bytes.bytesize, id]
41
+ return probe
42
+ end
43
+
44
+ if slot[0] == hash && slot[2] == bytes.bytesize && tokens.fetch(slot[3]).b == bytes
45
+ raise ArgumentError, "duplicate token in vocabulary: #{tokens.fetch(slot[3]).inspect}"
46
+ end
47
+
48
+ position = (position + 1) & (size - 1)
49
+ probe += 1
50
+ end
51
+ end
52
+
53
+ def pack(slots)
54
+ empty = [0, 0, 0, SLOT_EMPTY].pack("V4")
55
+ slots.each_with_object(Format.binary_string(slots.length * 16)) do |slot, packed|
56
+ packed << (slot ? slot.pack("V4") : empty)
57
+ end
58
+ end
59
+ end
60
+
61
+ def self.hash_bytes(string, seed = HASH_SEED)
62
+ HashTable.hash_bytes(string, seed)
63
+ end
64
+
65
+ def self.next_power_of_two(value)
66
+ HashTable.next_power_of_two(value)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,78 @@
1
+ require "static_embeddings/format/constants"
2
+
3
+ module StaticEmbeddings
4
+ module Format
5
+ class Trie
6
+ Node = Struct.new(:terminal, :children, keyword_init: true)
7
+
8
+ def initialize
9
+ @nodes = [Node.new(terminal: SLOT_EMPTY, children: {})]
10
+ end
11
+
12
+ def insert(bytes, id)
13
+ return if bytes.empty?
14
+
15
+ node_index = 0
16
+ bytes.each_byte do |byte|
17
+ node = @nodes[node_index]
18
+ node_index = node.children[byte] ||= append_node
19
+ end
20
+
21
+ node = @nodes[node_index]
22
+ raise ArgumentError, "duplicate trie key for token id #{id}" unless node.terminal == SLOT_EMPTY
23
+
24
+ node.terminal = id
25
+ end
26
+
27
+ def pack
28
+ edges = []
29
+ nodes = @nodes.map do |node|
30
+ start = edges.length
31
+ node.children.sort.each { |byte, child| edges << [byte, child] }
32
+ [start, node.children.length, node.terminal, 0]
33
+ end
34
+
35
+ nodes.each_with_object(header(nodes.length, edges.length)) { |record, out| out << record.pack("V4") }
36
+ .tap { |out| edges.each { |edge| out << edge.pack("V2") } }
37
+ end
38
+
39
+ private
40
+
41
+ def append_node
42
+ @nodes << Node.new(terminal: SLOT_EMPTY, children: {})
43
+ @nodes.length - 1
44
+ end
45
+
46
+ def header(node_count, edge_count)
47
+ capacity = 16 + node_count * 16 + edge_count * 8
48
+ Format.binary_string(capacity).tap { |out| out << [node_count, edge_count, 0, 0].pack("V4") }
49
+ end
50
+ end
51
+
52
+ module WordPieceTrie
53
+ module_function
54
+
55
+ def build(tokens, prefix)
56
+ root = Trie.new
57
+ continuation = Trie.new
58
+ prefix_bytes = prefix.b
59
+
60
+ tokens.each_with_index do |token, id|
61
+ bytes = token.b
62
+ if continuation?(bytes, prefix_bytes)
63
+ body = bytes.byteslice(prefix_bytes.bytesize, bytes.bytesize - prefix_bytes.bytesize)
64
+ continuation.insert(body, id)
65
+ else
66
+ root.insert(bytes, id)
67
+ end
68
+ end
69
+
70
+ [root.pack, continuation.pack]
71
+ end
72
+
73
+ def continuation?(bytes, prefix)
74
+ !prefix.empty? && bytes.start_with?(prefix) && bytes.bytesize > prefix.bytesize
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,41 @@
1
+ require "digest"
2
+ require "static_embeddings/errors"
3
+ require "static_embeddings/format/constants"
4
+
5
+ module StaticEmbeddings
6
+ module Format
7
+ module Verifier
8
+ CHUNK_BYTES = 1024 * 1024
9
+
10
+ module_function
11
+
12
+ def call(path)
13
+ raise InvalidModelError, "file too small" if File.size(path) < HEADER_SIZE
14
+
15
+ File.open(path, "rb") do |io|
16
+ header = io.read(HEADER_SIZE)
17
+ raise InvalidModelError, "bad magic" unless header.byteslice(0, 8) == MAGIC.b
18
+
19
+ stored = header.byteslice(CHECKSUM_OFFSET, CHECKSUM_SIZE)
20
+ actual = checksum(io, header)
21
+ { ok: stored == actual, expected: actual.unpack1("H*"), stored: stored.unpack1("H*") }
22
+ end
23
+ end
24
+
25
+ def checksum(io, header)
26
+ digest = Digest::SHA256.new
27
+ zeroed = header.dup
28
+ zeroed[CHECKSUM_OFFSET, CHECKSUM_SIZE] = "\0".b * CHECKSUM_SIZE
29
+ digest << zeroed
30
+
31
+ buffer = String.new(capacity: CHUNK_BYTES)
32
+ digest << buffer while io.read(CHUNK_BYTES, buffer)
33
+ digest.digest
34
+ end
35
+ end
36
+
37
+ def self.verify(path)
38
+ Verifier.call(path)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,131 @@
1
+ require "digest"
2
+ require "static_embeddings/format/constants"
3
+ require "static_embeddings/format/hash_table"
4
+ require "static_embeddings/format/trie"
5
+
6
+ module StaticEmbeddings
7
+ module Format
8
+ module Writer
9
+ module_function
10
+
11
+ def call(path:, meta:, tokens:, matrix:, norm_tables:, provenance:)
12
+ hash_size, strings, hash_blob, max_probe = HashTable.build(tokens)
13
+ root_trie, continuation_trie = WordPieceTrie.build(tokens, meta.fetch(:subword_prefix))
14
+ payloads = payloads(strings, hash_blob, matrix, norm_tables, provenance, root_trie, continuation_trie)
15
+ sections, file_size = layout(payloads)
16
+ header = build_header(meta, tokens.length, hash_size, max_probe, sections)
17
+ checksum = write_file(path, header, payloads, sections)
18
+
19
+ { bytes: file_size, sha256: checksum.unpack1("H*"), hash_table_size: hash_size }
20
+ end
21
+
22
+ def payloads(strings, hash_blob, matrix, norm_tables, provenance, root_trie, continuation_trie)
23
+ {
24
+ vocab_strings: strings,
25
+ vocab_hash: hash_blob,
26
+ embeddings: matrix,
27
+ norm_tables: norm_tables,
28
+ provenance: provenance,
29
+ root_trie: root_trie,
30
+ continuation_trie: continuation_trie
31
+ }
32
+ end
33
+
34
+ def layout(payloads)
35
+ offset = HEADER_SIZE
36
+ sections = payloads.each_with_object({}) do |(name, payload), out|
37
+ offset += (ALIGNMENT - (offset % ALIGNMENT)) % ALIGNMENT
38
+ out[name] = [offset, payload.bytesize]
39
+ offset += payload.bytesize
40
+ end
41
+ [sections, offset]
42
+ end
43
+
44
+ def write_file(path, header, payloads, sections)
45
+ digest = Digest::SHA256.new
46
+ File.open(path, "wb") do |io|
47
+ write_chunk(io, digest, header)
48
+ offset = HEADER_SIZE
49
+
50
+ payloads.each do |name, payload|
51
+ target = sections.fetch(name).first
52
+ padding = target - offset
53
+ write_chunk(io, digest, "\0".b * padding) if padding.positive?
54
+ write_payload(io, digest, payload)
55
+ offset = target + payload.bytesize
56
+ end
57
+ end
58
+
59
+ digest.digest.tap do |checksum|
60
+ File.open(path, "r+b") do |io|
61
+ io.seek(CHECKSUM_OFFSET, IO::SEEK_SET)
62
+ io.write(checksum)
63
+ end
64
+ end
65
+ end
66
+
67
+ def write_payload(io, digest, payload)
68
+ if payload.respond_to?(:each_chunk)
69
+ payload.each_chunk { |chunk| write_chunk(io, digest, chunk) }
70
+ else
71
+ write_chunk(io, digest, payload)
72
+ end
73
+ end
74
+
75
+ def write_chunk(io, digest, chunk)
76
+ io.write(chunk)
77
+ digest << chunk
78
+ end
79
+
80
+ def build_header(meta, vocab_size, hash_size, max_probe, sections)
81
+ header = "\0".b * HEADER_SIZE
82
+ header[0, 8] = MAGIC.b
83
+
84
+ HEADER_U32.each { |offset, value| put_u32(header, offset, value) }
85
+ META_U32.each { |offset, key| put_u32(header, offset, meta.fetch(key)) }
86
+ META_BOOL.each { |offset, key| put_u32(header, offset, meta.fetch(key) ? 1 : 0) }
87
+ put_u32(header, 24, vocab_size)
88
+ put_u32(header, 104, hash_size)
89
+ put_u32(header, MAX_TOKEN_CHARS_OFFSET, meta.fetch(:max_token_chars))
90
+ put_u32(header, MAX_PROBE_OFFSET, max_probe)
91
+ put_u32(header, ADDED_TOKEN_MASK_OFFSET, meta.fetch(:added_token_mask, 0))
92
+ put_prefix(header, meta.fetch(:subword_prefix))
93
+ SECTION_FIELDS.each { |name, field| put_section(header, field, sections.fetch(name)) }
94
+ header
95
+ end
96
+
97
+ def put_prefix(header, prefix)
98
+ bytes = prefix.b
99
+ raise ArgumentError, "subword prefix too long" if bytes.bytesize > 8
100
+
101
+ put_u32(header, 112, bytes.bytesize)
102
+ header[116, 8] = bytes.ljust(8, "\0")
103
+ end
104
+
105
+ def put_section(header, offset, section)
106
+ put_u64(header, offset, section.fetch(0))
107
+ put_u64(header, offset + 8, section.fetch(1))
108
+ end
109
+
110
+ def put_u32(buffer, offset, value)
111
+ integer = Integer(value)
112
+ unless integer.between?(0, UINT32_MAX)
113
+ raise ArgumentError, "u32 value out of range: #{integer}"
114
+ end
115
+ buffer[offset, 4] = [integer].pack("V")
116
+ end
117
+
118
+ def put_u64(buffer, offset, value)
119
+ integer = Integer(value)
120
+ unless integer.between?(0, UINT64_MAX)
121
+ raise ArgumentError, "u64 value out of range: #{integer}"
122
+ end
123
+ buffer[offset, 8] = [integer & UINT32_MAX, integer >> 32].pack("V2")
124
+ end
125
+ end
126
+
127
+ def self.write(**kwargs)
128
+ Writer.call(**kwargs)
129
+ end
130
+ end
131
+ end