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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +153 -0
- data/README.md +56 -28
- data/Rakefile +1 -1
- data/docs/ARCHITECTURE.md +50 -31
- data/docs/LIMITATIONS.md +16 -8
- data/docs/MODEL_AUDIT.md +84 -43
- data/docs/PERFORMANCE.md +28 -19
- data/ext/static_embeddings/se_embed.c +48 -8
- data/ext/static_embeddings/se_f16.c +43 -7
- data/ext/static_embeddings/se_format.c +121 -9
- data/ext/static_embeddings/se_internal.h +26 -5
- data/ext/static_embeddings/se_tokenizer.c +451 -47
- data/ext/static_embeddings/se_unicode.c +1 -1
- data/ext/static_embeddings/static_embeddings.c +136 -48
- data/lib/static_embeddings/cli.rb +1 -0
- data/lib/static_embeddings/converter.rb +51 -7
- data/lib/static_embeddings/format.rb +60 -22
- data/lib/static_embeddings/paths.rb +18 -1
- data/lib/static_embeddings/reference.rb +54 -7
- data/lib/static_embeddings/safetensors.rb +178 -34
- data/lib/static_embeddings/version.rb +1 -1
- data/lib/static_embeddings.rb +3 -5
- data/tools/check_model2vec_parity.rb +85 -54
- metadata +1 -1
|
@@ -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,
|
|
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}
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
32
|
-
raise ConversionError, "safetensors file is shorter than its length prefix" if
|
|
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 =
|
|
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 >
|
|
146
|
+
raise ConversionError, "safetensors header runs past end of file" if body_offset > file_size
|
|
41
147
|
|
|
42
|
-
raw_header =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
data/lib/static_embeddings.rb
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
ref_ids = row["token_ids"]
|
|
50
|
-
|
|
70
|
+
label = row.fetch("label", i.to_s)
|
|
51
71
|
problems = []
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 "
|
|
94
|
-
puts "
|
|
95
|
-
puts "
|
|
96
|
-
puts "
|
|
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
|
-
|
|
100
|
-
|
|
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"
|