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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -0
- data/README.md +17 -2
- data/docs/ARCHITECTURE.md +14 -5
- data/docs/MODEL_AUDIT.md +110 -0
- data/ext/static_embeddings/se_alloc_stats.c +5 -6
- data/ext/static_embeddings/se_f16.c +1 -1
- data/ext/static_embeddings/se_tokenizer.c +6 -0
- data/ext/static_embeddings/se_topk.c +1 -2
- data/ext/static_embeddings/static_embeddings.c +60 -10
- 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 -63
- 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 -338
- 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 +8 -12
- data/lib/static_embeddings/provenance.rb +58 -0
- data/lib/static_embeddings/reference.rb +50 -40
- data/lib/static_embeddings/row_prefix_payload.rb +59 -0
- data/lib/static_embeddings/version.rb +1 -1
- data/lib/static_embeddings.rb +29 -55
- data/static_embeddings.gemspec +2 -2
- data/tools/check_model2vec_parity.rb +5 -1
- 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 -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
|