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,191 @@
1
+ require "static_embeddings/errors"
2
+ require "static_embeddings/format/constants"
3
+
4
+ module StaticEmbeddings
5
+ module BertWordPiece
6
+ TOKENIZER_PROFILE = "BERT_WORDPIECE_V1"
7
+ ALLOWED_NORMALIZER_KEYS = %w[type clean_text handle_chinese_chars strip_accents lowercase].freeze
8
+ STANDARD_SPECIAL_TOKENS = {
9
+ "[PAD]" => Format::ADDED_PAD,
10
+ "[UNK]" => Format::ADDED_UNK,
11
+ "[CLS]" => Format::ADDED_CLS,
12
+ "[SEP]" => Format::ADDED_SEP,
13
+ "[MASK]" => Format::ADDED_MASK
14
+ }.freeze
15
+
16
+ module_function
17
+
18
+ def compile(tokenizer, tokenizer_config = {})
19
+ profile = audit(tokenizer, tokenizer_config)
20
+ tokens = vocabulary(tokenizer)
21
+ validate_added_token_ids(profile.fetch(:added_tokens), tokens)
22
+ [profile.freeze, tokens]
23
+ end
24
+
25
+ def runtime_meta(profile, tokens)
26
+ prefix = profile.fetch(:continuing_subword_prefix)
27
+ {
28
+ tokenizer_type: Format::TOKENIZER_BERT_WORDPIECE_V1,
29
+ tokenizer_profile: TOKENIZER_PROFILE,
30
+ do_lower_case: profile.fetch(:lowercase),
31
+ strip_accents: profile.fetch(:strip_accents),
32
+ handle_chinese_chars: profile.fetch(:handle_chinese_chars),
33
+ clean_text: profile.fetch(:clean_text),
34
+ added_token_mask: profile.fetch(:added_token_mask),
35
+ max_input_chars_per_word: profile.fetch(:max_input_chars_per_word),
36
+ max_token_chars: max_token_chars(tokens, prefix),
37
+ subword_prefix: prefix,
38
+ pad_id: token_id(tokens, "[PAD]", 0),
39
+ unk_id: token_id(tokens, profile.fetch(:unk_token)),
40
+ cls_id: token_id(tokens, "[CLS]", 0),
41
+ sep_id: token_id(tokens, "[SEP]", 0),
42
+ mask_id: token_id(tokens, "[MASK]", 0)
43
+ }.freeze
44
+ end
45
+
46
+ def audit(tokenizer, tokenizer_config)
47
+ unsupported!("tokenizer.json is not an object") unless tokenizer.is_a?(Hash)
48
+ unsupported!("tokenizer_config.json is not an object") unless tokenizer_config.is_a?(Hash)
49
+ model = tokenizer["model"] || unsupported!("tokenizer.json has no model section")
50
+ normalizer = tokenizer["normalizer"] || unsupported!("tokenizer has no normalizer")
51
+ pre_tokenizer = tokenizer["pre_tokenizer"]
52
+ unsupported!("tokenizer model section is not an object") unless model.is_a?(Hash)
53
+ unsupported!("tokenizer normalizer is not an object") unless normalizer.is_a?(Hash)
54
+
55
+ audit_model(model)
56
+ audit_normalizer(normalizer)
57
+ audit_pre_tokenizer(pre_tokenizer)
58
+ added_tokens = audit_added_tokens(tokenizer["added_tokens"] || [])
59
+ lowercase = flag(normalizer, "lowercase", tokenizer_config["do_lower_case"], true)
60
+ clean_text = flag(normalizer, "clean_text", nil, true)
61
+ max_input_chars = Integer(model.fetch("max_input_chars_per_word", 100))
62
+ unsupported!("max_input_chars_per_word must be positive") unless max_input_chars.positive?
63
+ unsupported!("clean_text=false is not supported by the runtime") unless clean_text
64
+
65
+ {
66
+ lowercase: lowercase,
67
+ strip_accents: strip_accents(normalizer, lowercase),
68
+ clean_text: clean_text,
69
+ handle_chinese_chars: flag(normalizer, "handle_chinese_chars",
70
+ tokenizer_config["tokenize_chinese_chars"], true),
71
+ continuing_subword_prefix: model.fetch("continuing_subword_prefix", "##"),
72
+ unk_token: model.fetch("unk_token", "[UNK]"),
73
+ max_input_chars_per_word: max_input_chars,
74
+ tokenizer_class: tokenizer_config["tokenizer_class"],
75
+ added_tokens: added_tokens,
76
+ added_token_mask: added_tokens.reduce(0) do |mask, token|
77
+ mask | STANDARD_SPECIAL_TOKENS.fetch(token.fetch("content"))
78
+ end
79
+ }
80
+ end
81
+
82
+ def audit_model(model)
83
+ unsupported!("tokenizer model.type is #{model['type'].inspect}, expected WordPiece") unless model["type"] == "WordPiece"
84
+
85
+ prefix = model.fetch("continuing_subword_prefix", "##")
86
+ unsupported!("continuing_subword_prefix #{prefix.inspect} is not supported") unless prefix == "##"
87
+
88
+ unk_token = model.fetch("unk_token", "[UNK]")
89
+ unsupported!("unk_token #{unk_token.inspect} is not supported") unless unk_token == "[UNK]"
90
+ end
91
+
92
+ def audit_normalizer(normalizer)
93
+ unsupported!("normalizer type #{normalizer['type'].inspect} is not BertNormalizer") unless normalizer["type"] == "BertNormalizer"
94
+
95
+ unknown = normalizer.keys - ALLOWED_NORMALIZER_KEYS
96
+ unsupported!("normalizer has unsupported keys #{unknown.inspect}") unless unknown.empty?
97
+ end
98
+
99
+ def audit_pre_tokenizer(pre_tokenizer)
100
+ return if pre_tokenizer.is_a?(Hash) && pre_tokenizer["type"] == "BertPreTokenizer"
101
+
102
+ unsupported!("pre_tokenizer type #{pre_tokenizer && pre_tokenizer['type'].inspect} is not BertPreTokenizer")
103
+ end
104
+
105
+ def audit_added_tokens(tokens)
106
+ unsupported!("added_tokens is not an array") unless tokens.is_a?(Array)
107
+ unsupported!("added_tokens contains a non-object entry") unless tokens.all? { |token| token.is_a?(Hash) }
108
+ bad = tokens.reject { |token| standard_special?(token) }
109
+ unsupported!("tokenizer declares non-standard added_tokens #{bad.map { |token| token['content'] }.inspect}") unless bad.empty?
110
+
111
+ tokens.each do |token|
112
+ content = token["content"].to_s
113
+ unsupported!("added token #{content.inspect} contains whitespace") if content.match?(/\s/)
114
+ unsupported!("added token #{content.inspect} uses lstrip/rstrip/single_word") if token["lstrip"] || token["rstrip"] || token["single_word"]
115
+ unsupported!("added token #{content.inspect} must use normalized=false") unless token["normalized"] == false
116
+ end
117
+
118
+ duplicate = tokens.group_by { |token| token["content"] }.find { |_, rows| rows.length > 1 }
119
+ unsupported!("duplicate added token #{duplicate.first.inspect}") if duplicate
120
+ tokens.freeze
121
+ end
122
+
123
+ def vocabulary(tokenizer)
124
+ vocab = tokenizer.dig("model", "vocab") || unsupported!("tokenizer.json has no model.vocab")
125
+ unsupported!("tokenizer model.vocab is not an object") unless vocab.is_a?(Hash)
126
+ tokens = Array.new(vocab.length)
127
+ vocab.each { |token, id| assign_vocab(tokens, token, id) }
128
+ missing = tokens.index(nil)
129
+ invalid!("vocab has a hole at id #{missing}") if missing
130
+ tokens
131
+ end
132
+
133
+ def assign_vocab(tokens, token, id)
134
+ unsupported!("vocab id #{id.inspect} is not an integer") unless id.is_a?(Integer)
135
+ invalid!("vocab id #{id} for #{token.inspect} is outside 0...#{tokens.length}") unless id.between?(0, tokens.length - 1)
136
+ invalid!("duplicate vocab id #{id}") unless tokens[id].nil?
137
+ tokens[id] = token
138
+ end
139
+
140
+ def validate_added_token_ids(added_tokens, tokens)
141
+ added_tokens.each do |token|
142
+ content = token.fetch("content")
143
+ id = token["id"]
144
+ unsupported!("added token #{content.inspect} has non-integer id #{id.inspect}") unless id.is_a?(Integer)
145
+ valid = id.between?(0, tokens.length - 1) && tokens[id] == content
146
+ unsupported!("added token #{content.inspect} id #{id} does not match model.vocab") unless valid
147
+ end
148
+ end
149
+
150
+ def flag(hash, key, fallback, default)
151
+ return !!hash[key] if hash.key?(key) && !hash[key].nil?
152
+ return !!fallback unless fallback.nil?
153
+
154
+ default
155
+ end
156
+
157
+ def strip_accents(normalizer, lowercase)
158
+ value = normalizer["strip_accents"]
159
+ normalizer.key?("strip_accents") && !value.nil? ? !!value : lowercase
160
+ end
161
+
162
+ def standard_special?(token)
163
+ token["special"] && STANDARD_SPECIAL_TOKENS.key?(token["content"])
164
+ end
165
+
166
+ def max_token_chars(tokens, prefix)
167
+ tokens.reduce(1) do |maximum, token|
168
+ body = token.start_with?(prefix) ? token[prefix.length..] : token
169
+ [maximum, body.each_char.count].max
170
+ end
171
+ end
172
+
173
+ def token_id(tokens, token, fallback = nil)
174
+ id = tokens.index(token)
175
+ return id unless id.nil?
176
+ return fallback unless fallback.nil?
177
+
178
+ invalid!("vocabulary has no #{token.inspect}")
179
+ end
180
+
181
+ def unsupported!(message)
182
+ raise UnsupportedModelError,
183
+ "#{message}. Supported tokenizer profile: #{TOKENIZER_PROFILE}; " \
184
+ "other tokenizer behaviour requires a separate runtime capability."
185
+ end
186
+
187
+ def invalid!(message)
188
+ raise ConversionError, message
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,50 @@
1
+ module StaticEmbeddings
2
+ module Canonical
3
+ Dimensions = Struct.new(:native, :output, :trained, keyword_init: true)
4
+ Runtime = Struct.new(:normalization, :unk_policy, :empty_policy, :max_tokens, :add_special_tokens,
5
+ keyword_init: true)
6
+ Source = Struct.new(:family, :model, :revision, :oracle, :files_sha256, :tokenizer_class,
7
+ :config_seq_length, keyword_init: true)
8
+ Model = Struct.new(:tokens, :matrix, :dimensions, :runtime, :tokenizer, :source, keyword_init: true)
9
+
10
+ module_function
11
+
12
+ def dimensions(native:, output:, trained: nil)
13
+ Dimensions.new(native: native, output: output, trained: trained&.freeze).freeze
14
+ end
15
+
16
+ def runtime(normalization:, unk_policy:, empty_policy:, max_tokens:, add_special_tokens: false)
17
+ Runtime.new(
18
+ normalization: normalization,
19
+ unk_policy: unk_policy,
20
+ empty_policy: empty_policy,
21
+ max_tokens: max_tokens,
22
+ add_special_tokens: add_special_tokens
23
+ ).freeze
24
+ end
25
+
26
+ def source(family:, model:, oracle:, files_sha256:, revision: nil, tokenizer_class: nil,
27
+ config_seq_length: nil)
28
+ Source.new(
29
+ family: family,
30
+ model: model,
31
+ revision: revision,
32
+ oracle: oracle,
33
+ files_sha256: files_sha256.freeze,
34
+ tokenizer_class: tokenizer_class,
35
+ config_seq_length: config_seq_length
36
+ ).freeze
37
+ end
38
+
39
+ def model(tokens:, matrix:, dimensions:, runtime:, tokenizer:, source:)
40
+ Model.new(
41
+ tokens: tokens.freeze,
42
+ matrix: matrix,
43
+ dimensions: dimensions,
44
+ runtime: runtime,
45
+ tokenizer: tokenizer.freeze,
46
+ source: source
47
+ ).freeze
48
+ end
49
+ end
50
+ end
@@ -1,16 +1,16 @@
1
+ require "fileutils"
1
2
  require "json"
2
3
  require "optparse"
3
- require "fileutils"
4
4
 
5
5
  module StaticEmbeddings
6
- class CLI
6
+ module CLI
7
7
  COMMANDS = {
8
- "convert" => :convert,
9
- "verify" => :verify,
10
- "inspect" => :inspect_model,
11
- "tokenize" => :tokenize,
12
- "embed" => :embed,
13
- "cache-path" => :cache_path,
8
+ "convert" => :convert_command,
9
+ "verify" => :verify_command,
10
+ "inspect" => :inspect_command,
11
+ "tokenize" => :tokenize_command,
12
+ "embed" => :embed_command,
13
+ "cache-path" => :cache_path_command,
14
14
  "help" => :usage,
15
15
  "-h" => :usage,
16
16
  "--help" => :usage,
@@ -20,10 +20,14 @@ module StaticEmbeddings
20
20
  HELP = <<~TEXT
21
21
  static_embeddings <command> [options]
22
22
 
23
- convert SOURCE_DIR Convert a HuggingFace/Model2Vec directory to .semb
23
+ convert SOURCE_DIR Convert Model2Vec or Sentence Transformers StaticEmbedding to .semb
24
24
  --out PATH Output file (default: <cache>/models/<id>.semb)
25
25
  --id ID Model id recorded in provenance
26
- --max-tokens N Truncation limit baked into the file (default 512)
26
+ --max-tokens N Model2Vec default: 512; Sentence Transformers default: unlimited
27
+ --max-tokens unlimited
28
+ --dimensions N Keep the first N dims (Matryoshka prefix slice)
29
+ --revision SHA Source revision recorded in provenance
30
+ --trained-mrl-dims 1024,512,256,... training dims recorded in provenance
27
31
 
28
32
  verify PATH Recompute the SHA-256 embedded in the header
29
33
  inspect PATH Print header fields and provenance
@@ -32,67 +36,78 @@ module StaticEmbeddings
32
36
  cache-path Print the model cache directory
33
37
  TEXT
34
38
 
35
- def self.run(argv)
36
- new.run(argv)
37
- end
39
+ module_function
38
40
 
39
41
  def run(argv)
40
- method = COMMANDS[argv.shift]
41
- return unknown unless method
42
+ command = COMMANDS[argv.shift]
43
+ return unknown_command unless command
42
44
 
43
- send(method, argv)
44
- rescue StaticEmbeddings::Error => e
45
+ public_send(command, argv)
46
+ rescue StaticEmbeddings::Error, ArgumentError, OptionParser::ParseError => e
45
47
  warn "#{e.class.name.split('::').last}: #{e.message}"
46
48
  1
47
49
  end
48
50
 
49
- private
50
-
51
51
  def usage(*)
52
52
  puts HELP
53
53
  0
54
54
  end
55
55
 
56
- def unknown
56
+ def unknown_command
57
57
  warn "unknown command"
58
58
  usage
59
59
  1
60
60
  end
61
61
 
62
- def cache_path(*)
62
+ def cache_path_command(*)
63
63
  puts StaticEmbeddings.cache_dir
64
64
  0
65
65
  end
66
66
 
67
- def convert(argv)
68
- require "static_embeddings/converter"
69
- options = parse_convert_options(argv)
67
+ def convert_command(argv)
68
+ options = convert_options(argv)
70
69
  source = required_arg(argv, "usage: static_embeddings convert SOURCE_DIR [--out PATH]")
71
70
  model_id = options[:id] || File.basename(File.expand_path(source))
72
- out = options[:out] || StaticEmbeddings.model_path(model_id)
73
- FileUtils.mkdir_p(File.dirname(out))
71
+ output = options[:out] || StaticEmbeddings.model_path(model_id)
72
+ FileUtils.mkdir_p(File.dirname(output))
74
73
 
75
- report = StaticEmbeddings.convert(source, output_path: out, model_id: model_id,
76
- max_tokens: options[:max_tokens])
77
- puts conversion_report(out, report, options[:max_tokens])
74
+ report = StaticEmbeddings.convert(source, output_path: output, model_id: model_id, **conversion_options(options))
75
+ puts conversion_report(output, report)
78
76
  0
79
77
  end
80
78
 
81
- def parse_convert_options(argv)
82
- options = { max_tokens: Converter::REFERENCE_MAX_TOKENS }
83
- OptionParser.new do |parser|
84
- parser.on("--out PATH") { |value| options[:out] = value }
85
- parser.on("--id ID") { |value| options[:id] = value }
86
- parser.on("--max-tokens N", Integer) { |value| options[:max_tokens] = value }
87
- end.parse!(argv)
88
- options
79
+ def convert_options(argv)
80
+ {}.tap do |options|
81
+ OptionParser.new do |parser|
82
+ parser.on("--out PATH") { |value| options[:out] = value }
83
+ parser.on("--id ID") { |value| options[:id] = value }
84
+ parser.on("--max-tokens N") { |value| options[:max_tokens] = parse_max_tokens(value) }
85
+ parser.on("--dimensions N", Integer) { |value| options[:dimensions] = value }
86
+ parser.on("--revision SHA") { |value| options[:source_revision] = value }
87
+ parser.on("--trained-mrl-dims LIST") { |value| options[:trained_mrl_dims] = value }
88
+ end.parse!(argv)
89
+ end
89
90
  end
90
91
 
91
- def conversion_report(path, report, max_tokens)
92
+ def conversion_options(options)
93
+ options.select { |key, _| %i[max_tokens dimensions source_revision trained_mrl_dims].include?(key) }
94
+ end
95
+
96
+ def parse_max_tokens(value)
97
+ return :unlimited if value == "unlimited" || value == "0"
98
+
99
+ integer = Integer(value)
100
+ raise OptionParser::InvalidArgument, "--max-tokens must be positive or unlimited" unless integer.positive?
101
+ integer
102
+ end
103
+
104
+ def conversion_report(path, report)
105
+ max_tokens = report[:max_tokens].to_i.zero? ? "unlimited" : report[:max_tokens]
106
+ dim = report[:native_dim] == report[:dim] ? report[:dim].to_s : "#{report[:dim]} (from native #{report[:native_dim]})"
92
107
  [
93
108
  "wrote #{path}",
94
109
  " vocab #{report[:vocab_size]}",
95
- " dim #{report[:dim]}",
110
+ " dim #{dim}",
96
111
  " bytes #{report[:bytes]}",
97
112
  " sha256 #{report[:sha256]}",
98
113
  " max_tokens #{max_tokens}",
@@ -101,20 +116,25 @@ module StaticEmbeddings
101
116
  ].join("\n")
102
117
  end
103
118
 
104
- def verify(argv)
119
+ def verify_command(argv)
105
120
  result = StaticEmbeddings.verify(required_arg(argv, "usage: static_embeddings verify PATH"))
106
- return puts("ok #{result[:expected]}") || 0 if result[:ok]
107
-
108
- warn "CHECKSUM MISMATCH"
109
- warn " stored #{result[:stored]}"
110
- warn " computed #{result[:expected]}"
111
- 1
112
- end
113
-
114
- def inspect_model(argv)
121
+ if result[:ok]
122
+ puts "ok #{result[:expected]}"
123
+ 0
124
+ else
125
+ warn "CHECKSUM MISMATCH"
126
+ warn " stored #{result[:stored]}"
127
+ warn " computed #{result[:expected]}"
128
+ 1
129
+ end
130
+ end
131
+
132
+ def inspect_command(argv)
115
133
  model = StaticEmbeddings.load(required_arg(argv, "usage: static_embeddings inspect PATH"))
116
134
  puts JSON.pretty_generate(model_summary(model))
117
135
  0
136
+ ensure
137
+ model&.close
118
138
  end
119
139
 
120
140
  def model_summary(model)
@@ -131,18 +151,20 @@ module StaticEmbeddings
131
151
  }
132
152
  end
133
153
 
134
- def tokenize(argv)
135
- model, text = model_and_text(argv, "usage: static_embeddings tokenize PATH TEXT")
136
- ids = model.tokenize(text)
137
- puts JSON.generate("ids" => ids, "count" => ids.length, "unk" => ids.count(model.unk_id))
154
+ def tokenize_command(argv)
155
+ with_model_and_text(argv, "usage: static_embeddings tokenize PATH TEXT") do |model, text|
156
+ ids = model.tokenize(text)
157
+ puts JSON.generate("ids" => ids, "count" => ids.length, "unk" => ids.count(model.unk_id))
158
+ end
138
159
  0
139
160
  end
140
161
 
141
- def embed(argv)
142
- model, text = model_and_text(argv, "usage: static_embeddings embed PATH TEXT")
143
- stats = model.embed_with_stats(text)
144
- warn_high_unk(stats) if high_unk?(stats)
145
- puts JSON.generate(stats_payload(model, stats))
162
+ def embed_command(argv)
163
+ with_model_and_text(argv, "usage: static_embeddings embed PATH TEXT") do |model, text|
164
+ stats = model.embed_with_stats(text)
165
+ warn_high_unk(stats) if high_unk?(stats)
166
+ puts JSON.generate(stats_payload(model, stats))
167
+ end
146
168
  0
147
169
  end
148
170
 
@@ -151,7 +173,7 @@ module StaticEmbeddings
151
173
  "token_count" => stats[:token_count],
152
174
  "unk_count" => stats[:unk_count],
153
175
  "truncated" => stats[:truncated],
154
- "vector" => StaticEmbeddings.unpack(stats[:vector], model.dim).first.map { |v| v.round(6) }
176
+ "vector" => StaticEmbeddings.unpack(stats[:vector], model.dim).first.map { |value| value.round(6) }
155
177
  }
156
178
  end
157
179
 
@@ -164,16 +186,19 @@ module StaticEmbeddings
164
186
  warn "warning: #{(ratio * 100).round}% of tokens are [UNK] — wrong model for this language?"
165
187
  end
166
188
 
167
- def model_and_text(argv, usage)
189
+ def with_model_and_text(argv, usage)
168
190
  path = argv.shift
169
191
  text = argv.join(" ")
170
- abort usage if path.nil? || text.empty?
192
+ raise InvalidOptionError, usage if path.nil? || text.empty?
171
193
 
172
- [StaticEmbeddings.load(path), text]
194
+ model = StaticEmbeddings.load(path)
195
+ yield model, text
196
+ ensure
197
+ model&.close
173
198
  end
174
199
 
175
200
  def required_arg(argv, usage)
176
- argv.shift || abort(usage)
201
+ argv.shift || raise(InvalidOptionError, usage)
177
202
  end
178
203
  end
179
204
  end
@@ -0,0 +1,45 @@
1
+ module StaticEmbeddings
2
+ module Codec
3
+ module_function
4
+
5
+ def unpack(blob, dim, format: :f32)
6
+ dim = Integer(dim)
7
+ raise InvalidOptionError, "dim must be positive" unless dim.positive?
8
+
9
+ values = decode(blob, normalize_format(format))
10
+ raise InvalidOptionError, "blob is not a multiple of dim" unless (values.length % dim).zero?
11
+
12
+ values.each_slice(dim).to_a
13
+ end
14
+
15
+ def pack(rows, format: :f32)
16
+ values = rows.first.is_a?(Array) ? rows.flatten(1) : rows
17
+ floats = values.map(&:to_f)
18
+
19
+ case normalize_format(format)
20
+ when :f32 then floats.pack("e*")
21
+ when :f16 then StaticEmbeddings.encode_f16(floats)
22
+ end
23
+ end
24
+
25
+ def decode(blob, format)
26
+ case format
27
+ when :f32
28
+ raise InvalidOptionError, "f32 blob byte size must be a multiple of 4" unless (blob.bytesize % 4).zero?
29
+ blob.unpack("e*")
30
+ when :f16
31
+ raise InvalidOptionError, "f16 blob byte size must be a multiple of 2" unless (blob.bytesize % 2).zero?
32
+ StaticEmbeddings.decode_f16(blob)
33
+ end
34
+ end
35
+
36
+ def normalize_format(format)
37
+ case format&.to_sym
38
+ when nil, :f32, :float32 then :f32
39
+ when :f16, :float16 then :f16
40
+ else
41
+ raise InvalidOptionError, "unsupported embedding format #{format.inspect} (expected :f32 or :f16)"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,58 @@
1
+ require "json"
2
+ require "static_embeddings/format/writer"
3
+ require "static_embeddings/importers"
4
+ require "static_embeddings/provenance"
5
+ require "static_embeddings/unicode_tables"
6
+
7
+ module StaticEmbeddings
8
+ module Conversion
9
+ module_function
10
+
11
+ def call(source_dir, output_path:, **options)
12
+ model = Importers.import(source_dir, **options)
13
+ provenance = Provenance.json(model, unicode_source: UnicodeTables.source_stamp)
14
+ result = Format::Writer.call(
15
+ path: output_path,
16
+ meta: format_meta(model),
17
+ tokens: model.tokens,
18
+ matrix: model.matrix,
19
+ norm_tables: UnicodeTables.packed,
20
+ provenance: provenance
21
+ )
22
+
23
+ result.merge(
24
+ vocab_size: model.tokens.length,
25
+ dim: model.dimensions.output,
26
+ native_dim: model.dimensions.native,
27
+ max_tokens: model.runtime.max_tokens,
28
+ provenance: JSON.parse(provenance)
29
+ )
30
+ end
31
+
32
+ def format_meta(model)
33
+ tokenizer = model.tokenizer
34
+ runtime = model.runtime
35
+ {
36
+ dim: model.dimensions.output,
37
+ normalization_type: runtime.normalization,
38
+ max_tokens_default: runtime.max_tokens,
39
+ add_special_tokens: runtime.add_special_tokens,
40
+ unk_policy: runtime.unk_policy,
41
+ empty_policy: runtime.empty_policy,
42
+ do_lower_case: tokenizer.fetch(:do_lower_case),
43
+ strip_accents: tokenizer.fetch(:strip_accents),
44
+ handle_chinese_chars: tokenizer.fetch(:handle_chinese_chars),
45
+ clean_text: tokenizer.fetch(:clean_text),
46
+ added_token_mask: tokenizer.fetch(:added_token_mask),
47
+ max_input_chars_per_word: tokenizer.fetch(:max_input_chars_per_word),
48
+ max_token_chars: tokenizer.fetch(:max_token_chars),
49
+ subword_prefix: tokenizer.fetch(:subword_prefix),
50
+ pad_id: tokenizer.fetch(:pad_id),
51
+ unk_id: tokenizer.fetch(:unk_id),
52
+ cls_id: tokenizer.fetch(:cls_id),
53
+ sep_id: tokenizer.fetch(:sep_id),
54
+ mask_id: tokenizer.fetch(:mask_id)
55
+ }
56
+ end
57
+ end
58
+ end
@@ -1,6 +1,6 @@
1
1
  module StaticEmbeddings
2
-
3
2
  class ConversionError < Error; end
4
-
3
+ class InvalidSourceError < ConversionError; end
4
+ class InvalidOptionError < ArgumentError; end
5
5
  class ModelNotFound < Error; end
6
6
  end