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.
@@ -1,14 +1,18 @@
1
+ require "json"
2
+ require "static_embeddings/converter"
3
+ require "static_embeddings/safetensors"
4
+
1
5
  module StaticEmbeddings
2
6
  class Reference
3
7
  CJK_RANGES = [
4
8
  0x4E00..0x9FFF, 0x3400..0x4DBF, 0x20000..0x2A6DF, 0x2A700..0x2B73F,
5
- 0x2B740..0x2B81F, 0x2B820..0x2CEAF, 0xF900..0xFAFF, 0x2F800..0x2FA1F
9
+ 0x2B740..0x2B81F, 0x2B920..0x2CEAF, 0xF900..0xFAFF, 0x2F800..0x2FA1F
6
10
  ].freeze
7
11
 
8
12
  ASCII_PUNCT = [33..47, 58..64, 91..96, 123..126].freeze
9
13
  ASCII_SPACES = [" ", "\t", "\n", "\r"].freeze
10
14
  RE_PUNCT = /\A\p{P}\z/
11
- RE_CONTROL = /\A(?:\p{Cc}|\p{Cf}|\p{Co}|\p{Cs})\z/
15
+ RE_CONTROL = /\A(?:\p{Cc}|\p{Cf}|\p{Co})\z/
12
16
  RE_MN = /\A\p{Mn}\z/
13
17
  RE_WHITESPACE = /\A(?:\p{Zs}|[\u0085\u2028\u2029])\z/
14
18
 
@@ -29,7 +33,7 @@ module StaticEmbeddings
29
33
  tokens = tokens_from(tokenizer)
30
34
  tensor = Safetensors.read(File.join(dir, "model.safetensors"))[:tensors].values.first
31
35
  rows, dim = tensor[:shape]
32
- floats = tensor[:bytes].unpack("e*")
36
+ floats = Safetensors.f32_bytes(tensor).unpack("e*")
33
37
  normalizer = tokenizer["normalizer"] || {}
34
38
 
35
39
  new(tokens: tokens,
@@ -53,10 +57,21 @@ module StaticEmbeddings
53
57
  max_input_chars_per_word: tokenizer.dig("model", "max_input_chars_per_word") || 100,
54
58
  unk_token: tokenizer.dig("model", "unk_token") || "[UNK]",
55
59
  normalize: config.key?("normalize") ? config["normalize"] : true,
56
- max_tokens: max_tokens
60
+ max_tokens: max_tokens,
61
+ added_tokens: supported_added_tokens(tokenizer)
57
62
  }
58
63
  end
59
64
 
65
+ def self.supported_added_tokens(tokenizer)
66
+ allowed = Converter::STANDARD_SPECIAL_TOKENS
67
+ (tokenizer["added_tokens"] || []).each_with_object({}) do |token, out|
68
+ content = token["content"]
69
+ next unless token["special"] && allowed.key?(content) && token["normalized"] == false
70
+
71
+ out[content] = Integer(token.fetch("id"))
72
+ end
73
+ end
74
+
60
75
  def normalize_text(text)
61
76
  chars = text.chars
62
77
  chars = clean_chars(chars) if @meta[:clean_text]
@@ -71,13 +86,13 @@ module StaticEmbeddings
71
86
  end
72
87
 
73
88
  def tokenize(text, max_tokens: @meta[:max_tokens])
74
- ids = []
75
- pre_tokenize(normalize_text(text)).each { |word| ids.concat(wordpiece(word)) }
89
+ ids = tokenize_with_added_tokens(text)
76
90
  max_tokens && max_tokens.positive? && ids.length > max_tokens ? ids.first(max_tokens) : ids
77
91
  end
78
92
 
79
93
  def embed(text, max_tokens: @meta[:max_tokens])
80
- used = tokenize(text, max_tokens: max_tokens).reject { |id| id == unk_id }
94
+ used = tokenize(text, max_tokens: false).reject { |id| id == unk_id }
95
+ used = used.first(max_tokens) if max_tokens && max_tokens.positive? && used.length > max_tokens
81
96
  return Array.new(@dim, 0.0) if used.empty?
82
97
 
83
98
  vector = pooled(used)
@@ -86,6 +101,38 @@ module StaticEmbeddings
86
101
 
87
102
  private
88
103
 
104
+ def tokenize_with_added_tokens(text)
105
+ added = @meta[:added_tokens]
106
+ return tokenize_plain(text) if added.empty?
107
+
108
+ ids = []
109
+ cursor = 0
110
+ binary = text.b
111
+ while cursor < text.bytesize
112
+ match = added.keys.filter_map do |literal|
113
+ index = binary.index(literal.b, cursor)
114
+ index && [index, -literal.bytesize, literal]
115
+ end.min
116
+
117
+ unless match
118
+ ids.concat(tokenize_plain(text.byteslice(cursor, text.bytesize - cursor)))
119
+ break
120
+ end
121
+
122
+ index, _, literal = match
123
+ ids.concat(tokenize_plain(text.byteslice(cursor, index - cursor))) if index > cursor
124
+ ids << added.fetch(literal)
125
+ cursor = index + literal.bytesize
126
+ end
127
+ ids
128
+ end
129
+
130
+ def tokenize_plain(text)
131
+ return [] if text.nil? || text.empty?
132
+
133
+ pre_tokenize(normalize_text(text)).flat_map { |word| wordpiece(word) }
134
+ end
135
+
89
136
  def clean_chars(chars)
90
137
  chars.filter_map do |ch|
91
138
  cp = ch.ord
@@ -3,61 +3,157 @@ require "json"
3
3
  module StaticEmbeddings
4
4
  module Safetensors
5
5
  MAX_HEADER_BYTES = 100 * 1024 * 1024
6
- SUPPORTED_DTYPES = { "F32" => 4 }.freeze
6
+ SUPPORTED_DTYPES = { "F32" => 4, "F16" => 2, "BF16" => 2 }.freeze
7
+ CONVERT_CHUNK_ELEMENTS = 64 * 1024
8
+ IO_CHUNK_BYTES = 1024 * 1024
9
+
10
+ class F32Payload
11
+ attr_reader :bytesize
12
+
13
+ def initialize(path, tensor)
14
+ @path = path
15
+ @offset = tensor.fetch(:absolute_offset)
16
+ @source_bytes = tensor.fetch(:source_bytes)
17
+ @dtype = tensor.fetch(:dtype)
18
+ @bytesize = tensor.fetch(:shape).reduce(1) { |count, dim| count * dim } * 4
19
+ end
7
20
 
8
- module_function
21
+ def each_chunk
22
+ return enum_for(__method__) unless block_given?
23
+
24
+ File.open(@path, "rb") do |io|
25
+ io.seek(@offset, IO::SEEK_SET)
26
+ remaining = @source_bytes
27
+ while remaining.positive?
28
+ read_size = [remaining, IO_CHUNK_BYTES].min
29
+ # F16/BF16 elements are two bytes; never split one between chunks.
30
+ read_size -= 1 if @dtype != "F32" && read_size.odd?
31
+ read_size = remaining if read_size.zero?
32
+ chunk = io.read(read_size)
33
+ unless chunk&.bytesize == read_size
34
+ raise ConversionError, "tensor data is truncated while streaming #{@path}"
35
+ end
36
+ chunk.force_encoding(Encoding::BINARY)
37
+ yield case @dtype
38
+ when "F32" then chunk
39
+ when "F16" then Safetensors.convert_f16_to_f32(chunk)
40
+ when "BF16" then Safetensors.convert_bf16_to_f32(chunk)
41
+ else
42
+ raise ConversionError, "tensor dtype #{@dtype} is not supported"
43
+ end
44
+ remaining -= read_size
45
+ end
46
+ end
47
+ end
48
+ end
9
49
 
10
- def read(path)
11
- data = File.binread(path)
12
- header, body_offset = parse_header(data)
13
- body_size = data.bytesize - body_offset
50
+ module_function
14
51
 
15
- tensors = header.each_with_object({}) do |(name, spec), acc|
16
- next if name == "__metadata__"
52
+ def describe(path)
53
+ File.open(path, "rb") do |io|
54
+ file_size = io.stat.size
55
+ header, body_offset = read_header(io, file_size)
56
+ body_size = file_size - body_offset
57
+
58
+ tensors = header.each_with_object({}) do |(name, spec), acc|
59
+ next if name == "__metadata__"
60
+
61
+ dtype = spec["dtype"]
62
+ shape = spec["shape"]
63
+ begin_off, end_off = checked_offsets(name, spec["data_offsets"], body_size)
64
+ expected = checked_tensor_bytes(name, dtype, shape)
65
+ actual = end_off - begin_off
66
+ if actual != expected
67
+ raise ConversionError,
68
+ "tensor #{name}: shape #{shape.inspect} implies #{expected} bytes, offsets span #{actual}"
69
+ end
70
+
71
+ acc[name] = {
72
+ dtype: dtype,
73
+ shape: shape,
74
+ absolute_offset: body_offset + begin_off,
75
+ source_bytes: actual
76
+ }
77
+ end
78
+
79
+ { metadata: header["__metadata__"] || {}, tensors: tensors }
80
+ end
81
+ end
17
82
 
18
- acc[name] = tensor_from(data, body_offset, body_size, name, spec)
83
+ def read(path)
84
+ description = describe(path)
85
+ tensors = description.fetch(:tensors).transform_values do |tensor|
86
+ File.open(path, "rb") do |io|
87
+ io.seek(tensor.fetch(:absolute_offset), IO::SEEK_SET)
88
+ bytes = io.read(tensor.fetch(:source_bytes))
89
+ unless bytes&.bytesize == tensor.fetch(:source_bytes)
90
+ raise ConversionError, "tensor data is truncated while reading #{path}"
91
+ end
92
+ tensor.merge(bytes: bytes.force_encoding(Encoding::BINARY))
93
+ end
19
94
  end
95
+ { metadata: description.fetch(:metadata), tensors: tensors }
96
+ end
20
97
 
21
- { metadata: header["__metadata__"] || {}, tensors: tensors }
98
+ def f32_payload(path, tensor)
99
+ F32Payload.new(path, tensor)
22
100
  end
23
101
 
24
102
  def write(path, name, shape, floats)
25
103
  body = floats.pack("e*")
26
- header = JSON.generate(name => { "dtype" => "F32", "shape" => shape, "data_offsets" => [0, body.bytesize] })
104
+ write_raw(path, name, shape, "F32", body)
105
+ end
106
+
107
+ def write_raw(path, name, shape, dtype, body)
108
+ checked_tensor_bytes(name, dtype, shape).tap do |expected|
109
+ raise ArgumentError, "body size does not match #{dtype} shape" unless body.bytesize == expected
110
+ end
111
+
112
+ header = JSON.generate(name => { "dtype" => dtype, "shape" => shape, "data_offsets" => [0, body.bytesize] })
27
113
  padded = header << (" " * ((8 - (header.bytesize % 8)) % 8))
28
- File.binwrite(path, [padded.bytesize].pack("Q<") << padded << body)
114
+ File.open(path, "wb") do |io|
115
+ io.write([padded.bytesize].pack("Q<"))
116
+ io.write(padded)
117
+ io.write(body)
118
+ end
119
+ end
120
+
121
+ def f32_bytes(tensor)
122
+ case tensor.fetch(:dtype)
123
+ when "F32"
124
+ tensor.fetch(:bytes)
125
+ when "F16"
126
+ convert_f16_to_f32(tensor.fetch(:bytes))
127
+ when "BF16"
128
+ convert_bf16_to_f32(tensor.fetch(:bytes))
129
+ else
130
+ raise ConversionError, "tensor dtype #{tensor[:dtype]} is not supported"
131
+ end
29
132
  end
30
133
 
31
- def parse_header(data)
32
- raise ConversionError, "safetensors file is shorter than its length prefix" if data.bytesize < 8
134
+ def read_header(io, file_size)
135
+ raise ConversionError, "safetensors file is shorter than its length prefix" if file_size < 8
136
+
137
+ prefix = io.read(8)
138
+ raise ConversionError, "safetensors file is shorter than its length prefix" unless prefix&.bytesize == 8
33
139
 
34
- header_len = data.byteslice(0, 8).unpack1("Q<")
140
+ header_len = prefix.unpack1("Q<")
35
141
  unless header_len.positive? && header_len <= MAX_HEADER_BYTES
36
142
  raise ConversionError, "implausible safetensors header length #{header_len}"
37
143
  end
38
144
 
39
145
  body_offset = 8 + header_len
40
- raise ConversionError, "safetensors header runs past end of file" if body_offset > data.bytesize
146
+ raise ConversionError, "safetensors header runs past end of file" if body_offset > file_size
41
147
 
42
- raw_header = data.byteslice(8, header_len)
148
+ raw_header = io.read(header_len)
149
+ unless raw_header&.bytesize == header_len
150
+ raise ConversionError, "safetensors header runs past end of file"
151
+ end
43
152
  raise ConversionError, "safetensors header is not a JSON object" unless raw_header.lstrip.start_with?("{")
44
153
 
45
154
  [JSON.parse(raw_header), body_offset]
46
- end
47
-
48
- def tensor_from(data, body_offset, body_size, name, spec)
49
- dtype = spec["dtype"]
50
- shape = spec["shape"]
51
- begin_off, end_off = checked_offsets(name, spec["data_offsets"], body_size)
52
- expected = checked_tensor_bytes(name, dtype, shape)
53
- actual = end_off - begin_off
54
-
55
- if actual != expected
56
- raise ConversionError,
57
- "tensor #{name}: shape #{shape.inspect} implies #{expected} bytes, offsets span #{actual}"
58
- end
59
-
60
- { dtype: dtype, shape: shape, bytes: data.byteslice(body_offset + begin_off, actual) }
155
+ rescue JSON::ParserError => e
156
+ raise ConversionError, "invalid safetensors JSON header: #{e.message}"
61
157
  end
62
158
 
63
159
  def checked_offsets(name, offsets, body_size)
@@ -76,12 +172,60 @@ module StaticEmbeddings
76
172
 
77
173
  def checked_tensor_bytes(name, dtype, shape)
78
174
  element_size = SUPPORTED_DTYPES[dtype]
79
- raise ConversionError, "tensor #{name}: dtype #{dtype} is not supported (F32 only)" unless element_size
175
+ unless element_size
176
+ raise ConversionError, "tensor #{name}: dtype #{dtype} is not supported (expected F32, F16, or BF16)"
177
+ end
80
178
  unless shape.is_a?(Array) && shape.all? { |dim| dim.is_a?(Integer) && dim >= 0 }
81
179
  raise ConversionError, "tensor #{name}: malformed shape #{shape.inspect}"
82
180
  end
83
181
 
84
- shape.reduce(1, :*) * element_size
182
+ shape.reduce(1) { |count, dim| count * dim } * element_size
183
+ end
184
+
185
+ def convert_f16_to_f32(bytes)
186
+ out = String.new(capacity: (bytes.bytesize / 2) * 4, encoding: Encoding::BINARY)
187
+ offset = 0
188
+ chunk_bytes = CONVERT_CHUNK_ELEMENTS * 2
189
+ while offset < bytes.bytesize
190
+ chunk = bytes.byteslice(offset, [chunk_bytes, bytes.bytesize - offset].min)
191
+ floats = if StaticEmbeddings.respond_to?(:decode_f16)
192
+ StaticEmbeddings.decode_f16(chunk)
193
+ else
194
+ chunk.unpack("v*").map { |bits| half_to_float(bits) }
195
+ end
196
+ out << floats.pack("e*")
197
+ offset += chunk.bytesize
198
+ end
199
+ out
200
+ end
201
+
202
+ def convert_bf16_to_f32(bytes)
203
+ out = String.new(capacity: (bytes.bytesize / 2) * 4, encoding: Encoding::BINARY)
204
+ offset = 0
205
+ chunk_bytes = CONVERT_CHUNK_ELEMENTS * 2
206
+ while offset < bytes.bytesize
207
+ chunk = bytes.byteslice(offset, [chunk_bytes, bytes.bytesize - offset].min)
208
+ words = chunk.unpack("v*")
209
+ out << words.map { |bits| bits << 16 }.pack("V*")
210
+ offset += chunk.bytesize
211
+ end
212
+ out
213
+ end
214
+
215
+ def half_to_float(bits)
216
+ sign = (bits >> 15) & 1
217
+ exponent = (bits >> 10) & 0x1F
218
+ fraction = bits & 0x3FF
219
+
220
+ value =
221
+ if exponent.zero?
222
+ fraction.zero? ? 0.0 : Math.ldexp(fraction.to_f, -24)
223
+ elsif exponent == 0x1F
224
+ fraction.zero? ? Float::INFINITY : Float::NAN
225
+ else
226
+ Math.ldexp(1.0 + fraction.to_f / 1024.0, exponent - 15)
227
+ end
228
+ sign.zero? ? value : -value
85
229
  end
86
230
  end
87
231
  end
@@ -1,3 +1,3 @@
1
1
  module StaticEmbeddings
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.5"
3
3
  end
@@ -18,10 +18,6 @@ end
18
18
  require "static_embeddings/errors"
19
19
  require "static_embeddings/paths"
20
20
  require "static_embeddings/format"
21
- require "static_embeddings/unicode_tables"
22
- require "static_embeddings/safetensors"
23
- require "static_embeddings/converter"
24
- require "static_embeddings/reference"
25
21
  require "static_embeddings/model"
26
22
 
27
23
  module StaticEmbeddings
@@ -64,7 +60,9 @@ module StaticEmbeddings
64
60
  load(path, verify: verify)
65
61
  end
66
62
 
67
- def convert(source_dir, output_path:, model_id: nil, max_tokens: Converter::REFERENCE_MAX_TOKENS)
63
+ def convert(source_dir, output_path:, model_id: nil, max_tokens: nil)
64
+ require "static_embeddings/converter"
65
+ max_tokens = Converter::REFERENCE_MAX_TOKENS if max_tokens.nil?
68
66
  converter = Converter.new(source_dir)
69
67
  converter.convert(output_path: output_path, model_id: model_id, max_tokens: max_tokens)
70
68
  converter.report
@@ -4,8 +4,7 @@ require "static_embeddings"
4
4
 
5
5
  options = {
6
6
  min_cosine: 1.0 - 1e-6,
7
- max_abs: 1e-5,
8
- ids: true
7
+ max_abs: 1e-5
9
8
  }
10
9
 
11
10
  parser = OptionParser.new do |opts|
@@ -13,7 +12,6 @@ parser = OptionParser.new do |opts|
13
12
  opts.on("--oracle PATH") { |value| options[:oracle] = value }
14
13
  opts.on("--min-cosine N", Float) { |value| options[:min_cosine] = value }
15
14
  opts.on("--max-abs N", Float) { |value| options[:max_abs] = value }
16
- opts.on("--[no-]ids") { |value| options[:ids] = value }
17
15
  end
18
16
  parser.parse!(ARGV)
19
17
  unless options[:model] && options[:oracle]
@@ -22,12 +20,18 @@ end
22
20
 
23
21
  model = StaticEmbeddings.load(options[:model], verify: true)
24
22
  payload = JSON.parse(File.read(options[:oracle], encoding: "UTF-8"))
25
- rows = payload.is_a?(Hash) ? payload.fetch("rows") : payload
26
- oracle_max_length = payload.is_a?(Hash) ? payload["max_length"] : nil
23
+ abort "unsupported oracle schema #{payload["schema_version"].inspect}" unless payload["schema_version"] == 2
27
24
 
28
- if oracle_max_length && oracle_max_length != model.max_tokens
29
- warn "WARNING oracle max_length=#{oracle_max_length} but model.max_tokens=#{model.max_tokens}"
25
+ reference = payload.fetch("reference")
26
+ max_length = Integer(reference.fetch("max_length"))
27
+ if max_length != model.max_tokens
28
+ abort "oracle max_length=#{max_length} but model.max_tokens=#{model.max_tokens}"
30
29
  end
30
+ if reference["unk_token_id"] && Integer(reference["unk_token_id"]) != model.unk_id
31
+ abort "oracle unk_token_id=#{reference["unk_token_id"]} but model.unk_id=#{model.unk_id}"
32
+ end
33
+
34
+ rows = payload.fetch("rows")
31
35
 
32
36
  def dot(a, b)
33
37
  a.zip(b).sum { |x, y| x * y }
@@ -37,71 +41,98 @@ def norm(a)
37
41
  Math.sqrt(a.sum { |x| x * x })
38
42
  end
39
43
 
44
+ def vector_metrics(reference, got)
45
+ max_abs = reference.zip(got).map { |a, b| (a - b).abs }.max || 0.0
46
+ ref_zero = reference.all?(&:zero?)
47
+ got_zero = got.all?(&:zero?)
48
+ cosine =
49
+ if ref_zero && got_zero
50
+ 1.0
51
+ elsif ref_zero || got_zero
52
+ 0.0
53
+ else
54
+ dot(reference, got) / (norm(reference) * norm(got))
55
+ end
56
+ [cosine, max_abs]
57
+ end
58
+
59
+ raw_failures = []
60
+ usable_failures = []
61
+ vector_failures = []
62
+ invariant_failures = []
63
+ intentional_deviations = []
40
64
  min_cosine = 1.0
41
65
  max_abs_all = 0.0
42
- vector_failures = []
43
- id_failures = []
44
- id_rows_checked = 0
66
+ vectors_checked = 0
45
67
 
46
68
  rows.each_with_index do |row, i|
47
69
  text = row.fetch("text")
48
- ref = row.fetch("vector")
49
- ref_ids = row["token_ids"]
50
-
70
+ label = row.fetch("label", i.to_s)
51
71
  problems = []
52
72
 
53
- if options[:ids] && ref_ids
54
- id_rows_checked += 1
55
- got_ids = model.tokenize(text)
56
- if got_ids != ref_ids
57
- id_failures << i
58
- first = got_ids.zip(ref_ids).index { |a, b| a != b } || [got_ids.length, ref_ids.length].min
59
- problems << "ids differ at #{first} (got #{got_ids.length}, ref #{ref_ids.length})"
60
- end
73
+ expected_raw = row.fetch("hf_raw_token_ids")
74
+ got_raw = model.tokenize(text)
75
+ if got_raw != expected_raw
76
+ raw_failures << i
77
+ first = got_raw.zip(expected_raw).index { |a, b| a != b } || [got_raw.length, expected_raw.length].min
78
+ problems << "raw ids differ at #{first} (got #{got_raw.length}, ref #{expected_raw.length})"
61
79
  end
62
80
 
63
- got = model.embed(text).unpack("e*")
64
- max_abs = ref.zip(got).map { |a, b| (a - b).abs }.max || 0.0
65
- ref_zero = ref.all?(&:zero?)
66
- got_zero = got.all?(&:zero?)
67
- cosine =
68
- if ref_zero && got_zero
69
- 1.0
70
- elsif ref_zero || got_zero
71
- 0.0
72
- else
73
- dot(ref, got) / (norm(ref) * norm(got))
74
- end
81
+ full_raw = model.tokenize(text, max_tokens: false)
82
+ expected_usable = row.fetch("static_usable_token_ids")
83
+ got_usable = full_raw.reject { |id| id == model.unk_id }.first(max_length)
84
+ if got_usable != expected_usable
85
+ usable_failures << i
86
+ first = got_usable.zip(expected_usable).index { |a, b| a != b } || [got_usable.length, expected_usable.length].min
87
+ problems << "usable ids differ at #{first} (got #{got_usable.length}, ref #{expected_usable.length})"
88
+ end
75
89
 
76
- min_cosine = [min_cosine, cosine].min
77
- max_abs_all = [max_abs_all, max_abs].max
90
+ got_vector_blob = model.embed(text)
91
+ pooled_blob = model.embed_token_ids(full_raw)
92
+ unless got_vector_blob == pooled_blob
93
+ invariant_failures << i
94
+ problems << "embed(text) != embed_token_ids(unbounded tokenize(text))"
95
+ end
78
96
 
79
- unless cosine >= options[:min_cosine] && max_abs <= options[:max_abs]
80
- vector_failures << i
81
- problems << "vector out of tolerance"
97
+ model2vec_ids = row.fetch("model2vec_token_ids")
98
+ declared_deviation = row.fetch("model2vec_character_pretruncate_changes_ids")
99
+ actual_deviation = model2vec_ids != expected_usable
100
+ if actual_deviation != declared_deviation
101
+ problems << "oracle character-pretruncate flag is inconsistent"
102
+ usable_failures << i unless usable_failures.include?(i)
103
+ elsif actual_deviation
104
+ intentional_deviations << i
105
+ else
106
+ reference_vector = row.fetch("model2vec_vector")
107
+ got_vector = got_vector_blob.unpack("e*")
108
+ cosine, max_abs = vector_metrics(reference_vector, got_vector)
109
+ vectors_checked += 1
110
+ min_cosine = [min_cosine, cosine].min
111
+ max_abs_all = [max_abs_all, max_abs].max
112
+ unless cosine >= options[:min_cosine] && max_abs <= options[:max_abs]
113
+ vector_failures << i
114
+ problems << format("vector out of tolerance cos=%.10f max_abs=%.8g", cosine, max_abs)
115
+ end
82
116
  end
83
117
 
84
118
  status = problems.empty? ? "ok" : "FAIL"
85
- label = text.bytesize > 64 ? "#{text[0, 32].inspect}...(#{text.bytesize}B)" : text.inspect
86
- line = format("%s idx=%02d cos=%.10f max_abs=%.8g bytes=%d text=%s",
87
- status, i, cosine, max_abs, text.bytesize, label)
88
- line += " [#{problems.join('; ')}]" unless problems.empty?
89
- puts line
119
+ suffix = actual_deviation ? " intentional-character-pretruncate-deviation" : ""
120
+ puts "#{status} idx=#{format('%03d', i)} label=#{label.inspect} bytes=#{text.bytesize}#{suffix}" +
121
+ (problems.empty? ? "" : " [#{problems.join('; ')}]")
90
122
  end
91
123
 
92
124
  puts "rows=#{rows.length}"
93
- puts "id_rows_checked=#{id_rows_checked}"
94
- puts "min_cosine=#{min_cosine}"
95
- puts "max_abs_all=#{max_abs_all}"
96
- puts "token_id_failures=#{id_failures.inspect}"
125
+ puts "vectors_checked=#{vectors_checked}"
126
+ puts "intentional_character_pretruncate_deviations=#{intentional_deviations.length}"
127
+ puts "min_cosine=#{min_cosine}" if vectors_checked.positive?
128
+ puts "max_abs_all=#{max_abs_all}" if vectors_checked.positive?
129
+ puts "raw_token_id_failures=#{raw_failures.inspect}"
130
+ puts "usable_token_id_failures=#{usable_failures.inspect}"
131
+ puts "embed_invariant_failures=#{invariant_failures.inspect}"
97
132
  puts "vector_failures=#{vector_failures.inspect}"
98
133
 
99
- if id_rows_checked.zero? && options[:ids]
100
- warn "WARNING oracle has no token_ids; regenerate it with the current tools/model2vec_oracle.py"
101
- end
102
-
103
- unless id_failures.empty? && vector_failures.empty?
104
- abort "parity failed: token ids #{id_failures.inspect}, vectors #{vector_failures.inspect}"
134
+ unless raw_failures.empty? && usable_failures.empty? && invariant_failures.empty? && vector_failures.empty?
135
+ abort "parity failed"
105
136
  end
106
137
 
107
- puts "parity OK"
138
+ puts "corpus parity OK (#{rows.length}/#{rows.length}); intentional Model2Vec character pre-truncation deviations are reported separately"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: static_embeddings
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov