static_embeddings 0.1.4 → 1.5.6
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 +118 -0
- data/README.md +67 -25
- data/Rakefile +1 -1
- data/docs/ARCHITECTURE.md +54 -34
- data/docs/LIMITATIONS.md +12 -6
- data/docs/MODEL_AUDIT.md +195 -52
- data/ext/static_embeddings/se_embed.c +2 -1
- data/ext/static_embeddings/se_format.c +41 -4
- data/ext/static_embeddings/se_internal.h +17 -5
- data/ext/static_embeddings/se_tokenizer.c +155 -18
- data/ext/static_embeddings/se_unicode.c +1 -1
- data/ext/static_embeddings/static_embeddings.c +32 -2
- data/lib/models/demo.semb +0 -0
- data/lib/static_embeddings/bert_wordpiece.rb +191 -0
- data/lib/static_embeddings/canonical.rb +50 -0
- data/lib/static_embeddings/cli.rb +88 -62
- data/lib/static_embeddings/codec.rb +45 -0
- data/lib/static_embeddings/conversion.rb +58 -0
- data/lib/static_embeddings/errors.rb +2 -2
- data/lib/static_embeddings/format/constants.rb +109 -0
- data/lib/static_embeddings/format/hash_table.rb +69 -0
- data/lib/static_embeddings/format/trie.rb +78 -0
- data/lib/static_embeddings/format/verifier.rb +41 -0
- data/lib/static_embeddings/format/writer.rb +131 -0
- data/lib/static_embeddings/format.rb +3 -300
- data/lib/static_embeddings/importers/model2vec.rb +52 -0
- data/lib/static_embeddings/importers/sentence_transformers_static.rb +103 -0
- data/lib/static_embeddings/importers/support.rb +111 -0
- data/lib/static_embeddings/importers.rb +50 -0
- data/lib/static_embeddings/model.rb +35 -20
- data/lib/static_embeddings/paths.rb +17 -4
- data/lib/static_embeddings/provenance.rb +58 -0
- data/lib/static_embeddings/reference.rb +90 -33
- data/lib/static_embeddings/row_prefix_payload.rb +59 -0
- data/lib/static_embeddings/safetensors.rb +178 -34
- data/lib/static_embeddings/version.rb +1 -1
- data/lib/static_embeddings.rb +29 -57
- data/static_embeddings.gemspec +2 -2
- data/tools/check_model2vec_parity.rb +89 -54
- data/tools/check_st_parity.rb +125 -0
- data/tools/eval_retrieval.rb +58 -0
- metadata +24 -6
- data/lib/static_embeddings/converter.rb +0 -284
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "static_embeddings/errors"
|
|
2
|
+
require "static_embeddings/bert_wordpiece"
|
|
3
|
+
require "static_embeddings/canonical"
|
|
4
|
+
require "static_embeddings/importers/support"
|
|
5
|
+
require "static_embeddings/importers/model2vec"
|
|
6
|
+
require "static_embeddings/importers/sentence_transformers_static"
|
|
7
|
+
|
|
8
|
+
module StaticEmbeddings
|
|
9
|
+
module Importers
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def import(source_dir, **options)
|
|
13
|
+
root = source_root(source_dir)
|
|
14
|
+
case detect(root)
|
|
15
|
+
when :model2vec then Model2Vec.call(root, **options)
|
|
16
|
+
when :sentence_transformers_static then SentenceTransformersStatic.call(root, **options)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def detect(root)
|
|
21
|
+
config = Support.load_object(File.join(root, "config.json"), optional: true) || {}
|
|
22
|
+
tokenizer = File.join(root, "tokenizer.json")
|
|
23
|
+
weights = File.join(root, "model.safetensors")
|
|
24
|
+
modules = File.join(root, "modules.json")
|
|
25
|
+
|
|
26
|
+
return :model2vec if model2vec_config?(config) && File.file?(tokenizer) && File.file?(weights)
|
|
27
|
+
return :sentence_transformers_static if File.file?(modules)
|
|
28
|
+
|
|
29
|
+
if File.file?(tokenizer) && File.file?(weights)
|
|
30
|
+
raise InvalidSourceError,
|
|
31
|
+
"cannot prove source family in #{root}: config.json is not Model2Vec and modules.json is absent"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
raise InvalidSourceError,
|
|
35
|
+
"cannot detect a static embedding source in #{root}: need a Model2Vec config with " \
|
|
36
|
+
"tokenizer.json + model.safetensors, or modules.json for Sentence Transformers StaticEmbedding"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def source_root(source_dir)
|
|
40
|
+
root = File.expand_path(source_dir.to_s)
|
|
41
|
+
raise InvalidSourceError, "source directory #{root} does not exist" unless File.directory?(root)
|
|
42
|
+
|
|
43
|
+
root
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def model2vec_config?(config)
|
|
47
|
+
config["model_type"].to_s == "model2vec" || Array(config["architectures"]).include?("StaticModel")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -4,39 +4,41 @@ module StaticEmbeddings
|
|
|
4
4
|
class Model
|
|
5
5
|
attr_reader :path
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return {} if raw.nil?
|
|
7
|
+
if method_defined?(:max_tokens) && !method_defined?(:max_tokens_limit)
|
|
8
|
+
alias_method :max_tokens_limit, :max_tokens
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
def max_tokens
|
|
11
|
+
limit = max_tokens_limit
|
|
12
|
+
limit.zero? ? false : limit
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def provenance
|
|
17
|
+
@provenance ||= parse_provenance.freeze
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
def model_id
|
|
17
21
|
provenance["source_model_id"]
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
def embed_array(text, **
|
|
21
|
-
format =
|
|
22
|
-
StaticEmbeddings.unpack(embed(text, **
|
|
24
|
+
def embed_array(text, **options)
|
|
25
|
+
format = options.fetch(:format, :f32)
|
|
26
|
+
StaticEmbeddings.unpack(embed(text, **options), dim, format: format).first
|
|
23
27
|
end
|
|
24
28
|
|
|
25
|
-
def embed_batch_arrays(texts, **
|
|
26
|
-
format =
|
|
27
|
-
StaticEmbeddings.unpack(embed_batch(texts, **
|
|
29
|
+
def embed_batch_arrays(texts, **options)
|
|
30
|
+
format = options.fetch(:format, :f32)
|
|
31
|
+
StaticEmbeddings.unpack(embed_batch(texts, **options), dim, format: format)
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
def cosine_top_k(query_blob, matrix_blob, k, **
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
StaticEmbeddings.cosine_top_k(query_blob, matrix_blob, k, **opts.merge(dim: dim))
|
|
34
|
+
def cosine_top_k(query_blob, matrix_blob, k, **options)
|
|
35
|
+
reject_runtime_dim!(options)
|
|
36
|
+
StaticEmbeddings.cosine_top_k(query_blob, matrix_blob, k, **options.merge(dim: dim))
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
def dot_top_k(query_blob, matrix_blob, k, **
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
StaticEmbeddings.dot_top_k(query_blob, matrix_blob, k, **opts.merge(dim: dim))
|
|
39
|
+
def dot_top_k(query_blob, matrix_blob, k, **options)
|
|
40
|
+
reject_runtime_dim!(options)
|
|
41
|
+
StaticEmbeddings.dot_top_k(query_blob, matrix_blob, k, **options.merge(dim: dim))
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def to_s
|
|
@@ -44,5 +46,18 @@ module StaticEmbeddings
|
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
alias inspect to_s
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def parse_provenance
|
|
53
|
+
raw = provenance_json
|
|
54
|
+
raw.nil? ? {} : JSON.parse(raw)
|
|
55
|
+
rescue JSON::ParserError, EncodingError => e
|
|
56
|
+
raise InvalidModelError, "invalid provenance JSON: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def reject_runtime_dim!(options)
|
|
60
|
+
raise InvalidOptionError, "dim: is set by the model" if options.key?(:dim)
|
|
61
|
+
end
|
|
47
62
|
end
|
|
48
63
|
end
|
|
@@ -11,13 +11,26 @@ 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 InvalidOptionError, "model_id must not be empty" if id.empty?
|
|
16
|
+
raise InvalidOptionError, "model_id contains a NUL byte" if id.include?("\0")
|
|
17
|
+
|
|
18
|
+
normalized = id.tr("\\", "/")
|
|
19
|
+
parts = normalized.split("/", -1)
|
|
20
|
+
invalid = normalized.start_with?("/") ||
|
|
21
|
+
parts.any? { |part| part.empty? || part == "." || part == ".." || part.include?(":") }
|
|
22
|
+
raise InvalidOptionError, "model_id must be a relative slash-separated identifier" if invalid
|
|
23
|
+
|
|
24
|
+
base = File.expand_path(File.join(cache_dir(env), "models"))
|
|
25
|
+
path = File.expand_path(File.join(base, "#{normalized}.semb"))
|
|
26
|
+
prefix = base.end_with?(File::SEPARATOR) ? base : "#{base}#{File::SEPARATOR}"
|
|
27
|
+
raise InvalidOptionError, "model_id escapes the model cache" unless path.start_with?(prefix)
|
|
28
|
+
|
|
29
|
+
path
|
|
15
30
|
end
|
|
16
31
|
|
|
17
32
|
def builtin_path(name)
|
|
18
|
-
file = BUILTIN_MODELS.fetch(name)
|
|
19
|
-
raise ModelNotFound, "unknown builtin model #{name.inspect}"
|
|
20
|
-
end
|
|
33
|
+
file = BUILTIN_MODELS.fetch(name) { raise ModelNotFound, "unknown builtin model #{name.inspect}" }
|
|
21
34
|
File.join(BUILTIN_DIR, file)
|
|
22
35
|
end
|
|
23
36
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "static_embeddings/format/constants"
|
|
3
|
+
require "static_embeddings/bert_wordpiece"
|
|
4
|
+
|
|
5
|
+
module StaticEmbeddings
|
|
6
|
+
module Provenance
|
|
7
|
+
MODEL2VEC_VERSION = "0.9.0"
|
|
8
|
+
TOKENIZERS_VERSION = "0.23.1"
|
|
9
|
+
UNICODE_CATEGORIES_VERSION = "0.1.1"
|
|
10
|
+
NOTES = "Vectors are only reference-compatible if docs/MODEL_AUDIT.md records a passing oracle run for this source revision."
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def json(model, unicode_source:)
|
|
15
|
+
JSON.generate(payload(model, unicode_source: unicode_source).sort.to_h)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def payload(model, unicode_source:)
|
|
19
|
+
dimensions = model.dimensions
|
|
20
|
+
runtime = model.runtime
|
|
21
|
+
source = model.source
|
|
22
|
+
tokenizer = model.tokenizer
|
|
23
|
+
|
|
24
|
+
data = {
|
|
25
|
+
"add_special_tokens" => runtime.add_special_tokens,
|
|
26
|
+
"config_seq_length" => source.config_seq_length,
|
|
27
|
+
"converter_version" => StaticEmbeddings::VERSION,
|
|
28
|
+
"dim" => dimensions.output,
|
|
29
|
+
"format_version" => Format::VERSION,
|
|
30
|
+
"native_dim" => dimensions.native,
|
|
31
|
+
"normalize" => runtime.normalization == Format::NORMALIZATION_L2,
|
|
32
|
+
"notes" => NOTES,
|
|
33
|
+
"oracle" => source.oracle,
|
|
34
|
+
"output_dim" => dimensions.output,
|
|
35
|
+
"reference_impl" => source.oracle,
|
|
36
|
+
"reference_max_tokens" => runtime.max_tokens,
|
|
37
|
+
"reference_tokenizers_version" => TOKENIZERS_VERSION,
|
|
38
|
+
"source_family" => source.family,
|
|
39
|
+
"source_files_sha256" => source.files_sha256,
|
|
40
|
+
"source_model_id" => source.model,
|
|
41
|
+
"source_revision" => source.revision,
|
|
42
|
+
"tokenizer_class" => source.tokenizer_class,
|
|
43
|
+
"tokenizer_profile" => tokenizer.fetch(:tokenizer_profile, BertWordPiece::TOKENIZER_PROFILE),
|
|
44
|
+
"unicode_source" => unicode_source,
|
|
45
|
+
"unk_policy" => runtime.unk_policy == Format::UNK_DROP ? "drop" : "include",
|
|
46
|
+
"vocab_size" => model.tokens.length
|
|
47
|
+
}
|
|
48
|
+
data["trained_mrl_dims"] = dimensions.trained if dimensions.trained
|
|
49
|
+
add_model2vec_versions(data) if source.family == "model2vec"
|
|
50
|
+
data
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def add_model2vec_versions(data)
|
|
54
|
+
data["reference_model2vec_version"] = MODEL2VEC_VERSION
|
|
55
|
+
data["reference_unicode_categories_version"] = UNICODE_CATEGORIES_VERSION
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
require "static_embeddings/importers"
|
|
2
|
+
require "static_embeddings/format/constants"
|
|
3
|
+
|
|
1
4
|
module StaticEmbeddings
|
|
2
5
|
class Reference
|
|
3
6
|
CJK_RANGES = [
|
|
4
7
|
0x4E00..0x9FFF, 0x3400..0x4DBF, 0x20000..0x2A6DF, 0x2A700..0x2B73F,
|
|
5
|
-
0x2B740..0x2B81F,
|
|
8
|
+
0x2B740..0x2B81F, 0x2B920..0x2CEAF, 0xF900..0xFAFF, 0x2F800..0x2FA1F
|
|
6
9
|
].freeze
|
|
7
10
|
|
|
8
11
|
ASCII_PUNCT = [33..47, 58..64, 91..96, 123..126].freeze
|
|
9
12
|
ASCII_SPACES = [" ", "\t", "\n", "\r"].freeze
|
|
10
13
|
RE_PUNCT = /\A\p{P}\z/
|
|
11
|
-
RE_CONTROL = /\A(?:\p{Cc}|\p{Cf}|\p{Co}
|
|
14
|
+
RE_CONTROL = /\A(?:\p{Cc}|\p{Cf}|\p{Co})\z/
|
|
12
15
|
RE_MN = /\A\p{Mn}\z/
|
|
13
16
|
RE_WHITESPACE = /\A(?:\p{Zs}|[\u0085\u2028\u2029])\z/
|
|
14
17
|
|
|
@@ -22,41 +25,55 @@ module StaticEmbeddings
|
|
|
22
25
|
@dim = meta.fetch(:dim)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
def self.from_source_dir(dir, max_tokens:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
config = File.file?(config_path) ? JSON.parse(File.binread(config_path)) : {}
|
|
29
|
-
tokens = tokens_from(tokenizer)
|
|
30
|
-
tensor = Safetensors.read(File.join(dir, "model.safetensors"))[:tensors].values.first
|
|
31
|
-
rows, dim = tensor[:shape]
|
|
32
|
-
floats = tensor[:bytes].unpack("e*")
|
|
33
|
-
normalizer = tokenizer["normalizer"] || {}
|
|
28
|
+
def self.from_source_dir(dir, max_tokens: nil, dimensions: nil)
|
|
29
|
+
from_canonical(Importers.import(dir, max_tokens: max_tokens, dimensions: dimensions))
|
|
30
|
+
end
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
def self.from_canonical(model)
|
|
33
|
+
dim = model.dimensions.output
|
|
34
|
+
floats = matrix_bytes(model.matrix).unpack("e*")
|
|
35
|
+
rows = model.tokens.length
|
|
36
|
+
new(
|
|
37
|
+
tokens: model.tokens,
|
|
38
|
+
matrix: Array.new(rows) { |index| floats[index * dim, dim] },
|
|
39
|
+
meta: meta_from_canonical(model)
|
|
40
|
+
)
|
|
38
41
|
end
|
|
39
42
|
|
|
40
|
-
def self.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
def self.matrix_bytes(matrix)
|
|
44
|
+
return matrix unless matrix.respond_to?(:each_chunk)
|
|
45
|
+
|
|
46
|
+
matrix.each_chunk.each_with_object(Format.binary_string) { |chunk, packed| packed << chunk }
|
|
43
47
|
end
|
|
44
48
|
|
|
45
|
-
def self.
|
|
46
|
-
|
|
49
|
+
def self.meta_from_canonical(model)
|
|
50
|
+
tokenizer = model.tokenizer
|
|
51
|
+
runtime = model.runtime
|
|
47
52
|
{
|
|
48
|
-
dim:
|
|
49
|
-
lowercase:
|
|
50
|
-
strip_accents:
|
|
51
|
-
clean_text:
|
|
52
|
-
handle_chinese_chars:
|
|
53
|
-
max_input_chars_per_word: tokenizer.
|
|
54
|
-
unk_token: tokenizer.
|
|
55
|
-
normalize:
|
|
56
|
-
max_tokens: max_tokens
|
|
53
|
+
dim: model.dimensions.output,
|
|
54
|
+
lowercase: tokenizer.fetch(:do_lower_case),
|
|
55
|
+
strip_accents: tokenizer.fetch(:strip_accents),
|
|
56
|
+
clean_text: tokenizer.fetch(:clean_text),
|
|
57
|
+
handle_chinese_chars: tokenizer.fetch(:handle_chinese_chars),
|
|
58
|
+
max_input_chars_per_word: tokenizer.fetch(:max_input_chars_per_word),
|
|
59
|
+
unk_token: model.tokens.fetch(tokenizer.fetch(:unk_id)),
|
|
60
|
+
normalize: runtime.normalization == Format::NORMALIZATION_L2,
|
|
61
|
+
max_tokens: runtime.max_tokens,
|
|
62
|
+
unk_policy: runtime.unk_policy,
|
|
63
|
+
added_tokens: added_tokens(model)
|
|
57
64
|
}
|
|
58
65
|
end
|
|
59
66
|
|
|
67
|
+
def self.added_tokens(model)
|
|
68
|
+
mask = model.tokenizer.fetch(:added_token_mask)
|
|
69
|
+
BertWordPiece::STANDARD_SPECIAL_TOKENS.each_with_object({}) do |(content, bit), tokens|
|
|
70
|
+
next if (mask & bit).zero?
|
|
71
|
+
|
|
72
|
+
id = model.tokens.index(content)
|
|
73
|
+
tokens[content] = id unless id.nil?
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
60
77
|
def normalize_text(text)
|
|
61
78
|
chars = text.chars
|
|
62
79
|
chars = clean_chars(chars) if @meta[:clean_text]
|
|
@@ -71,21 +88,61 @@ module StaticEmbeddings
|
|
|
71
88
|
end
|
|
72
89
|
|
|
73
90
|
def tokenize(text, max_tokens: @meta[:max_tokens])
|
|
74
|
-
ids =
|
|
75
|
-
pre_tokenize(normalize_text(text)).each { |word| ids.concat(wordpiece(word)) }
|
|
91
|
+
ids = tokenize_with_added_tokens(text)
|
|
76
92
|
max_tokens && max_tokens.positive? && ids.length > max_tokens ? ids.first(max_tokens) : ids
|
|
77
93
|
end
|
|
78
94
|
|
|
79
95
|
def embed(text, max_tokens: @meta[:max_tokens])
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
ids = apply_unk_policy(tokenize(text, max_tokens: false))
|
|
97
|
+
ids = ids.first(max_tokens) if max_tokens && max_tokens.positive? && ids.length > max_tokens
|
|
98
|
+
return Array.new(@dim, 0.0) if ids.empty?
|
|
82
99
|
|
|
83
|
-
vector = pooled(
|
|
100
|
+
vector = pooled(ids)
|
|
84
101
|
@meta[:normalize] ? l2_normalize(vector) : vector
|
|
85
102
|
end
|
|
86
103
|
|
|
104
|
+
def apply_unk_policy(ids)
|
|
105
|
+
case @meta.fetch(:unk_policy)
|
|
106
|
+
when Format::UNK_DROP then ids.reject { |id| id == unk_id }
|
|
107
|
+
when Format::UNK_INCLUDE then ids
|
|
108
|
+
else raise InvalidModelError, "unknown unk_policy #{@meta[:unk_policy].inspect}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
87
112
|
private
|
|
88
113
|
|
|
114
|
+
def tokenize_with_added_tokens(text)
|
|
115
|
+
added = @meta[:added_tokens]
|
|
116
|
+
return tokenize_plain(text) if added.empty?
|
|
117
|
+
|
|
118
|
+
ids = []
|
|
119
|
+
cursor = 0
|
|
120
|
+
binary = text.b
|
|
121
|
+
while cursor < text.bytesize
|
|
122
|
+
match = added.keys.filter_map do |literal|
|
|
123
|
+
index = binary.index(literal.b, cursor)
|
|
124
|
+
index && [index, -literal.bytesize, literal]
|
|
125
|
+
end.min
|
|
126
|
+
|
|
127
|
+
unless match
|
|
128
|
+
ids.concat(tokenize_plain(text.byteslice(cursor, text.bytesize - cursor)))
|
|
129
|
+
break
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
index, _, literal = match
|
|
133
|
+
ids.concat(tokenize_plain(text.byteslice(cursor, index - cursor))) if index > cursor
|
|
134
|
+
ids << added.fetch(literal)
|
|
135
|
+
cursor = index + literal.bytesize
|
|
136
|
+
end
|
|
137
|
+
ids
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def tokenize_plain(text)
|
|
141
|
+
return [] if text.nil? || text.empty?
|
|
142
|
+
|
|
143
|
+
pre_tokenize(normalize_text(text)).flat_map { |word| wordpiece(word) }
|
|
144
|
+
end
|
|
145
|
+
|
|
89
146
|
def clean_chars(chars)
|
|
90
147
|
chars.filter_map do |ch|
|
|
91
148
|
cp = ch.ord
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require "static_embeddings/errors"
|
|
2
|
+
require "static_embeddings/format/constants"
|
|
3
|
+
|
|
4
|
+
module StaticEmbeddings
|
|
5
|
+
class RowPrefixPayload
|
|
6
|
+
FLOAT_BYTES = 4
|
|
7
|
+
|
|
8
|
+
attr_reader :bytesize
|
|
9
|
+
|
|
10
|
+
def initialize(inner, native_dim:, output_dim:)
|
|
11
|
+
raise InvalidOptionError, "native_dim must be positive" unless native_dim.positive?
|
|
12
|
+
unless output_dim.between?(1, native_dim)
|
|
13
|
+
raise InvalidOptionError, "output_dim must be between 1 and #{native_dim}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@inner = inner
|
|
17
|
+
@source_row_bytes = native_dim * FLOAT_BYTES
|
|
18
|
+
@output_row_bytes = output_dim * FLOAT_BYTES
|
|
19
|
+
unless (inner.bytesize % @source_row_bytes).zero?
|
|
20
|
+
raise ConversionError, "embedding payload is not a whole number of #{native_dim}-d rows"
|
|
21
|
+
end
|
|
22
|
+
@bytesize = (inner.bytesize / @source_row_bytes) * @output_row_bytes
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def each_chunk
|
|
26
|
+
return enum_for(__method__) unless block_given?
|
|
27
|
+
|
|
28
|
+
remainder = Format.binary_string
|
|
29
|
+
@inner.each_chunk do |chunk|
|
|
30
|
+
data = remainder.empty? ? chunk : remainder + chunk
|
|
31
|
+
complete_rows, remainder = split_complete_rows(data)
|
|
32
|
+
yield slice_rows(complete_rows) unless complete_rows.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
raise ConversionError, "truncated embedding matrix while slicing dimensions" unless remainder.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def split_complete_rows(data)
|
|
41
|
+
bytes = (data.bytesize / @source_row_bytes) * @source_row_bytes
|
|
42
|
+
complete = data.byteslice(0, bytes) || Format.binary_string
|
|
43
|
+
remainder = data.byteslice(bytes, data.bytesize - bytes) || Format.binary_string
|
|
44
|
+
remainder.force_encoding(Encoding::BINARY)
|
|
45
|
+
[complete, remainder]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def slice_rows(data)
|
|
49
|
+
rows = data.bytesize / @source_row_bytes
|
|
50
|
+
out = Format.binary_string(rows * @output_row_bytes)
|
|
51
|
+
offset = 0
|
|
52
|
+
rows.times do
|
|
53
|
+
out << data.byteslice(offset, @output_row_bytes)
|
|
54
|
+
offset += @source_row_bytes
|
|
55
|
+
end
|
|
56
|
+
out
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|