secryst 0.1.0 → 1.0.0

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.
@@ -0,0 +1,155 @@
1
+ require "zip"
2
+ require "yaml"
3
+ require "digest/sha2"
4
+ require "fileutils"
5
+ require "open-uri"
6
+
7
+ module Secryst
8
+ # Interscript Model Format v1 — the byte-level runtime contract shared
9
+ # with the Python (interscript-ml) and TypeScript (npm: secryst)
10
+ # runtimes. Token ids follow the canonical ByT5 table: byte b -> b+3,
11
+ # trailing EOS; pad=0, unk=2. Ids are NOT raw byte values.
12
+ module IMF
13
+ BYTE_OFFSET = 3
14
+ PAD_ID = 0
15
+ EOS_ID = 1
16
+ UNK_ID = 2
17
+
18
+ DEFAULT_INDEX_URL = "https://raw.githubusercontent.com/interscript/interscript-ml/main/models.yaml"
19
+
20
+ class FormatError < StandardError; end
21
+ class RegistryError < StandardError; end
22
+
23
+ class << self
24
+ def encode(text)
25
+ text.bytes.map { |b| b + BYTE_OFFSET } + [EOS_ID]
26
+ end
27
+
28
+ def decode(token_ids)
29
+ out = +""
30
+ token_ids.each do |token|
31
+ break if token == EOS_ID
32
+ next if token == PAD_ID || token == UNK_ID
33
+ out << ((token - BYTE_OFFSET) % 256).chr
34
+ end
35
+ out.force_encoding(Encoding::UTF_8)
36
+ end
37
+
38
+ def manifest(zip_path)
39
+ Zip::File.open(zip_path) do |zf|
40
+ raise FormatError, "missing metadata.yaml" unless zf.find_entry("metadata.yaml")
41
+ meta = YAML.safe_load(zf.read("metadata.yaml"), permitted_classes: [], aliases: false)
42
+ raise FormatError, "unsupported format: #{meta["format"].inspect}" if meta["format"] != "imf-v1"
43
+ if meta["tokenizer"] != "bytes"
44
+ raise FormatError, "tokenizer #{meta["tokenizer"].inspect}: this runtime is byte-level only"
45
+ end
46
+ %w[encoder.onnx decoder.onnx].each do |required|
47
+ raise FormatError, "missing #{required}" unless zf.find_entry(required)
48
+ end
49
+ meta
50
+ end
51
+ end
52
+
53
+ # Reads every .onnx member after verifying its sha256 against the
54
+ # manifest — corrupt zips fail loudly, before any session loads.
55
+ def verify_and_read(zip_path)
56
+ meta = manifest(zip_path)
57
+ sha = meta.fetch("sha256", {})
58
+ graphs = {}
59
+ Zip::File.open(zip_path) do |zf|
60
+ zf.entries.select { |e| e.name.end_with?(".onnx") }.each do |entry|
61
+ recorded = sha[entry.name]
62
+ raise FormatError, "#{entry.name} is not covered by metadata sha256" unless recorded
63
+ bytes = entry.get_input_stream.read
64
+ actual = Digest::SHA256.hexdigest(bytes)
65
+ if actual != recorded
66
+ raise FormatError, "#{entry.name} sha256 mismatch: zip has #{actual}, metadata says #{recorded}"
67
+ end
68
+ graphs[entry.name] = bytes
69
+ end
70
+ end
71
+ graphs
72
+ end
73
+
74
+ def cache_dir
75
+ ENV["SECRYST_CACHE"] || File.join(Dir.home, ".cache", "secryst")
76
+ end
77
+
78
+ # models.yaml resolution: cache hit (re-verified), or download ->
79
+ # verify whole-file sha256 -> atomic install into the cache.
80
+ # Entries with `parts` (GitHub's 2 GiB per-asset cap) are streamed
81
+ # in order, each part sha256-verified as it lands, then the
82
+ # assembled file is checked against the whole-file sha256.
83
+ def resolve(model_id, index_url: nil)
84
+ source = index_url || ENV["SECRYST_INDEX"] || DEFAULT_INDEX_URL
85
+ entries = load_index(source)
86
+ entry = entries[model_id]
87
+ raise RegistryError, "unknown model id #{model_id.inspect} (known: #{entries.keys.sort})" unless entry
88
+
89
+ target = File.join(cache_dir, "models", model_id, entry["filename"])
90
+ if File.file?(target) && Digest::SHA256.file(target).hexdigest == entry["sha256"]
91
+ return target
92
+ end
93
+
94
+ FileUtils.mkdir_p(File.dirname(target))
95
+ tmp = target + ".part.#{Process.pid}"
96
+ if entry["parts"]
97
+ download_parts(entry, tmp)
98
+ else
99
+ channel = entry["url"]
100
+ if channel.start_with?("file://")
101
+ FileUtils.cp(channel.sub(%r{\Afile://}, ""), tmp)
102
+ else
103
+ URI.open(channel) { |remote| IO.copy_stream(remote, tmp) }
104
+ end
105
+ end
106
+ actual = Digest::SHA256.file(tmp).hexdigest
107
+ unless actual == entry["sha256"]
108
+ File.delete(tmp)
109
+ raise RegistryError, "downloaded #{entry["filename"]} sha256 mismatch: got #{actual}, index says #{entry["sha256"]}"
110
+ end
111
+ File.rename(tmp, target)
112
+ target
113
+ end
114
+
115
+ private
116
+
117
+ def download_parts(entry, tmp)
118
+ File.open(tmp, "wb") do |out|
119
+ entry["parts"].each_with_index do |part, index|
120
+ digest = Digest::SHA256.new
121
+ source = part["url"].start_with?("file://") ? part["url"].sub(%r{\Afile://}, "") : part["url"]
122
+ open_stream = lambda do |io|
123
+ while (chunk = io.read(1024 * 1024))
124
+ out.write(chunk)
125
+ digest.update(chunk)
126
+ end
127
+ end
128
+ if part["url"].start_with?("file://")
129
+ File.open(source, "rb", &open_stream)
130
+ else
131
+ URI.open(source, "rb", &open_stream)
132
+ end
133
+ unless digest.hexdigest == part["sha256"]
134
+ raise RegistryError, "part #{index} of #{entry["filename"]} sha256 mismatch: got #{digest.hexdigest}, index says #{part["sha256"]}"
135
+ end
136
+ end
137
+ end
138
+ rescue StandardError
139
+ File.delete(tmp) if File.file?(tmp)
140
+ raise
141
+ end
142
+
143
+ def load_index(source)
144
+ text = if source.start_with?("http://", "https://")
145
+ URI.open(source) { |remote| remote.read }
146
+ else
147
+ File.read(source)
148
+ end
149
+ raw = YAML.safe_load(text, permitted_classes: [], aliases: false)
150
+ raise RegistryError, "index must have version: 1" if raw["version"] != 1
151
+ raw.fetch("models", {})
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,52 @@
1
+ module Secryst
2
+ class Model
3
+ attr_accessor :model, :input_vocab, :target_vocab
4
+
5
+ def self.from_file(model_file)
6
+ # A models.yaml model id resolves (download -> verify -> cache)
7
+ # through the same contract as the Python/TS runtimes.
8
+ model_file = IMF.resolve(model_file) unless model_file.to_s.end_with?('.zip') || File.file?(model_file.to_s)
9
+ model_file = Provisioning.locate(model_file)
10
+
11
+ Zip::File.open(model_file) do |zip_file|
12
+ metadata = zip_file.glob('metadata.yaml').first
13
+ metadata = YAML.safe_load(metadata.get_input_stream.read) if metadata
14
+
15
+ # IMF v1: the Interscript Model Format zip.
16
+ return Byt5Onnx.new(model_file) if metadata && metadata['format'] == 'imf-v1'
17
+
18
+ name = metadata && metadata['name']
19
+
20
+ # Modern byte-level seq2seq (ByT5 family): encoder.onnx + decoder.onnx.
21
+ return Byt5Onnx.new(model_file) if name == 'byt5'
22
+
23
+ # Legacy single-file ONNX transformer zips (vocabs.yaml based).
24
+ vocabs = zip_file.glob('vocabs.yaml').first
25
+ raise 'vocabs.yaml is missing in model zip!' unless vocabs
26
+ vocabs = YAML.safe_load(vocabs.get_input_stream.read)
27
+ input_vocab = Vocab.new(vocabs['input'], specials: [])
28
+ target_vocab = Vocab.new(vocabs['target'], specials: [])
29
+
30
+ onnx = zip_file.glob('*.onnx').first
31
+ raise 'onnx model file is missing in model zip!' unless onnx
32
+ Onnx.new(onnx.get_input_stream.read, input_vocab, target_vocab)
33
+ end
34
+ end
35
+
36
+ class Onnx < Model
37
+ def initialize(model_path_or_bytes, input_vocab, target_vocab)
38
+ @model = OnnxRuntime::Model.new(model_path_or_bytes)
39
+ @input_vocab = input_vocab
40
+ @target_vocab = target_vocab
41
+ end
42
+
43
+ def call(input, output, opts)
44
+ @model.predict({ src: input, tgt: output }.merge(opts))['output']
45
+ end
46
+
47
+ def argmax(*args)
48
+ self.call(*args).map { |i| i.flatten.each_with_index.max[1] }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -175,7 +175,7 @@ module Secryst
175
175
  if attn_mask
176
176
  raise ArgumentError, 'Only float, byte, and bool types are supported for attn_mask, not %s' % attn_mask.dtype unless attn_mask.dtype == Torch.float32 || attn_mask.dtype == Torch.float64 || attn_mask.dtype == Torch.float16 || attn_mask.dtype == Torch.uint8 || attn_mask.dtype == Torch.bool
177
177
  if attn_mask.dtype == Torch.uint8
178
- puts "Byte tensor for attn_mask in NN::MultiheadAttention is deprecated. Use bool tensor instead."
178
+ # puts "Byte tensor for attn_mask in NN::MultiheadAttention is deprecated. Use bool tensor instead."
179
179
  attn_mask = attn_mask.to(Torch.bool)
180
180
  end
181
181
 
@@ -192,7 +192,7 @@ module Secryst
192
192
 
193
193
  # convert ByteTensor key_padding_mask to bool
194
194
  if key_padding_mask && key_padding_mask.dtype == Torch.uint8
195
- puts("Byte tensor for key_padding_mask in NN::MultiheadAttention is deprecated. Use bool tensor instead.")
195
+ # puts("Byte tensor for key_padding_mask in NN::MultiheadAttention is deprecated. Use bool tensor instead.")
196
196
  key_padding_mask = key_padding_mask.to(Torch.bool)
197
197
  end
198
198
 
@@ -0,0 +1,189 @@
1
+ require 'open-uri'
2
+ require 'uri'
3
+ require 'fileutils'
4
+ require 'digest/sha2'
5
+ require 'yaml'
6
+
7
+ module Secryst
8
+ # Module Secryst::Provisioning is to provision remote models locally and to
9
+ # dispatch them later on.
10
+ module Provisioning
11
+ extend self
12
+
13
+ @remotes = [] # Here's a place for a global model repository
14
+ @preload_models = []
15
+ attr_accessor :remotes, :preload_models
16
+
17
+ def add_remote(path)
18
+ @remotes << path
19
+ @remotes = @remotes.uniq
20
+ end
21
+
22
+ def prepare_environment
23
+ return if @set_up
24
+
25
+ # We provision the environment in the following way:
26
+ # First, we try the SECRYST_DATA environment variable. If that's available,
27
+ # we use it to store the Secryst data we need. Otherwise, we try the following
28
+ # paths:
29
+
30
+ possible_paths = [
31
+ "/var/lib/secryst",
32
+ "/usr/local/share/secryst",
33
+ "/usr/share/secryst",
34
+ File.join(Dir.home, ".local/share/secryst")
35
+ ]
36
+
37
+ # We find the first writable path to become the primary one. The remaining
38
+ # ones will be used read-only if they exist
39
+
40
+ @write_path = nil
41
+ @read_paths = []
42
+
43
+ ([ENV["SECRYST_DATA"]] + possible_paths).compact.each do |path|
44
+ FileUtils.mkdir_p(path)
45
+ @write_path = path unless @write_path
46
+ rescue
47
+ ensure
48
+ @read_paths << path if File.readable?(path)
49
+ end
50
+
51
+ raise StandardError, "Can't find a writable path for Secryst. Consider setting a SECRYST_DATA environment variable" unless @write_path
52
+
53
+ # Now, let's locate the first Secrystfile to be found
54
+ path = Dir.pwd
55
+ secrystfilepath = loop do
56
+ break unless path =~ %r{[/\\]}
57
+ if File.readable?(path + "/Secrystfile")
58
+ break path + "/Secrystfile"
59
+ end
60
+ path = path.sub(%r{[/\\][^/\\]*?\z}, '')
61
+ end
62
+
63
+ # It's found, so let's parse it.
64
+ if secrystfilepath
65
+ secrystfile = Secrystfile.new(secrystfilepath)
66
+
67
+ @remotes = secrystfile.remotes + @remotes
68
+ @remotes = @remotes.uniq
69
+
70
+ @preload_models = secrystfile.models
71
+ end
72
+
73
+ # Load the remotes if they are older than 1 minute
74
+ FileUtils.mkdir_p(@write_path + "/remotes/")
75
+ @loaded_remotes = @remotes.map do |uri|
76
+ cache_path = "#{@write_path}/remotes/#{Digest::SHA256.hexdigest(uri)}.yaml"
77
+ if !File.exist?(cache_path)
78
+ data = URI.open(uri).read
79
+ File.write(cache_path, data)
80
+ elsif File.mtime(cache_path) + 60 < Time.now
81
+ begin
82
+ # Just *try* to download it.
83
+ data = URI.open(uri).read
84
+ File.write(cache_path, data)
85
+ rescue
86
+ end
87
+ end
88
+ Remotefile.new(cache_path, uri: uri)
89
+ end
90
+
91
+ # Ok we are done now. We still need to resolve the required paths, but for
92
+ # that let's reuse our existing facilities.
93
+ @set_up = true
94
+
95
+ @preload_models.each { |i| locate(i) }
96
+ end
97
+
98
+ def locate(name)
99
+ # Shortcut this if user gave a filename.
100
+ return name if name =~ %r{[.\\/]}
101
+
102
+ prepare_environment
103
+
104
+ @loaded_remotes.each do |i|
105
+ model = i.resolve(name)
106
+ if model
107
+ model_path = @read_paths.map do |j|
108
+ path = j + "/models/" + model.name
109
+ next path if File.readable?(path)
110
+ nil
111
+ end.compact.first
112
+
113
+ if model_path
114
+ version = File.read(model_path + "/version").to_f
115
+ if version >= model.version
116
+ return model_path + "/model.zip"
117
+ else
118
+ return download(model, remote: i)
119
+ end
120
+ else
121
+ return download(model, remote: i)
122
+ end
123
+ end
124
+ end
125
+
126
+ raise StandardError, "Model #{name} not found"
127
+ end
128
+
129
+ def download(model, remote:)
130
+ uri = model.uri
131
+ if uri.start_with?("./") || uri.start_with?("../")
132
+ remote_uri = remote.uri
133
+ if remote_uri =~ %r{\A/|\A\w:[\\/]}
134
+ remote_uri = "file://"+remote_uri
135
+ end
136
+
137
+ uri = URI(remote_uri).merge(uri).to_s
138
+ uri = uri.sub(%r{\Afile://}, '')
139
+ end
140
+
141
+ warn "* Downloading a Secryst model #{model.name}:#{model.version} from #{uri}..."
142
+
143
+ data = URI.open(uri).read
144
+ path = @write_path + "/models/" + model.name
145
+ FileUtils.mkdir_p path
146
+ File.write(path + "/version", model.version)
147
+ File.write(path + "/model.zip", data)
148
+ return path + "/model.zip"
149
+ end
150
+
151
+ class Remotefile
152
+ attr_accessor :uri
153
+
154
+ def initialize(path, uri:)
155
+ @yaml = YAML.load_file(path)
156
+ @uri = uri
157
+ end
158
+
159
+ def resolve(name)
160
+ @yaml["models"].find do |n, desc|
161
+ if name == n
162
+ return Model.new(name: n, uri: desc["path"], version: desc["version"])
163
+ end
164
+ end
165
+ end
166
+
167
+ class Model < Struct.new(:name, :uri, :version, keyword_init: true)
168
+ end
169
+ end
170
+
171
+ class Secrystfile
172
+ def initialize(path)
173
+ @path = path
174
+ @models, @remotes = [], []
175
+ self.instance_eval(File.read(path), path)
176
+ end
177
+
178
+ def model(name)
179
+ @models << name
180
+ end
181
+
182
+ def source(src)
183
+ @remotes << src
184
+ end
185
+
186
+ attr_accessor :models, :remotes
187
+ end
188
+ end
189
+ end
@@ -1,51 +1,37 @@
1
1
  module Secryst
2
+ # Dispatching translator: byte-level ByT5 models (name: byt5 in
3
+ # metadata.yaml) translate themselves; legacy char-vocab ONNX zips use
4
+ # the original greedy loop.
2
5
  class Translator
3
- def initialize(model:, vocabs_dir:, hyperparameters:, model_file:)
4
- @device = "cpu"
5
- @vocabs_dir = vocabs_dir
6
+ attr_accessor :model
6
7
 
7
- load_vocabs
8
-
9
- if model == 'transformer'
10
- @model = Secryst::Transformer.new(hyperparameters.merge({
11
- input_vocab_size: @input_vocab.length,
12
- target_vocab_size: @target_vocab.length,
13
- }))
14
- else
15
- raise ArgumentError, 'Only transformer model is currently supported'
16
- end
17
-
18
- @model.load_state_dict(Torch.load(model_file))
19
- @model.eval
8
+ def initialize(model_file:)
9
+ @device = 'cpu'
10
+ @model = Model.from_file(model_file)
20
11
  end
21
12
 
22
13
  def translate(phrase, max_seq_length: 100)
14
+ return @model.translate(phrase, max_seq_length: max_seq_length) if @model.is_a?(Byt5Onnx)
15
+
23
16
  input = ['<sos>'] + phrase.chars + ['<eos>']
24
- input = Torch.tensor([input.map {|i| @input_vocab.stoi[i]}]).t
25
- output = Torch.tensor([[@target_vocab.stoi['<sos>']]])
26
- src_key_padding_mask = input.t.eq(1)
17
+ input = Numo::NArray[input.map { |i| @model.input_vocab.stoi[i] }].transpose
18
+ output = Numo::NArray[[@model.target_vocab.stoi['<sos>']]]
19
+ src_key_padding_mask = input.transpose.eq(1)
27
20
 
28
21
  max_seq_length.times do |i|
29
- tgt_key_padding_mask = output.t.eq(1)
30
- tgt_mask = Torch.triu(Torch.ones(i+1,i+1)).eq(0).transpose(0,1)
31
- opts = {
22
+ tgt_key_padding_mask = output.transpose.eq(1)
23
+ tgt_mask = Numo::DFloat.ones(i + 1, i + 1).triu.transpose.eq(0)
24
+ prediction = @model.argmax(input, output.dup,
32
25
  tgt_mask: tgt_mask,
33
26
  src_key_padding_mask: src_key_padding_mask,
34
27
  tgt_key_padding_mask: tgt_key_padding_mask,
35
- memory_key_padding_mask: src_key_padding_mask,
36
- }
37
- prediction = @model.call(input, output, opts).map {|i| i.argmax.item }
38
- break if @target_vocab.itos[prediction[i]] == '<eos>'
39
- output = Torch.cat([output, Torch.tensor([[prediction[i]]])])
40
- end
28
+ memory_key_padding_mask: src_key_padding_mask)
29
+ break if @model.target_vocab.itos[prediction[i]] == '<eos>'
41
30
 
42
- puts "#{output[1..-1].map {|i| @target_vocab.itos[i.item]}.join('')}"
43
- end
31
+ output = Numo::NArray.concatenate([output, Numo::NArray[[prediction[i]]]])
32
+ end
44
33
 
45
- private
46
- def load_vocabs
47
- @input_vocab = Vocab.new(JSON.parse(File.read("#{@vocabs_dir}/input_vocab.json")))
48
- @target_vocab = Vocab.new(JSON.parse(File.read("#{@vocabs_dir}/target_vocab.json")))
34
+ output[1..-1].to_a.flatten.map { |i| @model.target_vocab.itos[i] }.join('')
49
35
  end
50
36
  end
51
- end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module Secryst
2
- VERSION = "0.1.0"
3
- end
2
+ VERSION = "1.0.0"
3
+ end
data/lib/secryst/vocab.rb CHANGED
@@ -1,66 +1,39 @@
1
1
  module Secryst
2
2
  class Vocab
3
3
  UNK = "<unk>"
4
- attr_reader :stoi, :itos, :freqs
4
+ attr_reader :stoi, :itos
5
5
 
6
6
  def initialize(
7
- counter, max_size: nil, min_freq: 1, specials: ["<unk>", "<pad>", "<sos>", "<eos>"],
8
- vectors: nil, unk_init: nil, vectors_cache: nil, specials_first: true
7
+ list, specials: ["<unk>", "<pad>", "<sos>", "<eos>"], specials_first: true
9
8
  )
10
-
11
- @freqs = counter
12
- counter = counter.dup
13
- min_freq = [min_freq, 1].max
14
-
15
- @itos = []
16
9
  @unk_index = nil
17
-
18
- if specials_first
10
+ @itos = []
11
+ if specials_first && (list & specials).length == 0
19
12
  @itos = specials
20
- # only extend max size if specials are prepended
21
- max_size += specials.size if max_size
22
- end
23
-
24
- # frequencies of special tokens are not counted when building vocabulary
25
- # in frequency order
26
- specials.each do |tok|
27
- counter.delete(tok)
28
13
  end
29
14
 
30
- # sort by frequency, then alphabetically
31
- words_and_frequencies = counter.sort_by { |k, v| [-v, k] }
15
+ @itos += list
32
16
 
33
- words_and_frequencies.each do |word, freq|
34
- break if freq < min_freq || @itos.length == max_size
35
- @itos << word
17
+ if !specials_first && (list & specials).length == 0
18
+ @itos.concat(specials)
36
19
  end
37
20
 
38
- if specials.include?(UNK) # hard-coded for now
39
- unk_index = specials.index(UNK) # position in list
40
- # account for ordering of specials, set variable
41
- @unk_index = specials_first ? unk_index : @itos.length + unk_index
42
- @stoi = Hash.new(@unk_index)
21
+ # Automatic substitution of unknown symbols
22
+ if @itos.include?("<unk>")
23
+ unk_index = @itos.index("<unk>")
24
+ @stoi = Hash.new(unk_index)
25
+ elsif @itos.include?("[UNK]")
26
+ unk_index = @itos.index("[UNK]")
27
+ @stoi = Hash.new(unk_index)
43
28
  else
44
29
  @stoi = {}
45
30
  end
46
31
 
47
- if !specials_first
48
- @itos.concat(specials)
49
- end
50
32
 
51
33
  # stoi is simply a reverse dict for itos
52
34
  @itos.each_with_index do |tok, i|
53
35
  @stoi[tok] = i
54
36
  end
55
-
56
- @vectors = nil
57
- if !vectors.nil?
58
- # self.load_vectors(vectors, unk_init=unk_init, cache=vectors_cache)
59
- raise "Not implemented yet"
60
- else
61
- raise "Failed assertion" unless unk_init.nil?
62
- raise "Failed assertion" unless vectors_cache.nil?
63
- end
64
37
  end
65
38
 
66
39
  def [](token)
@@ -71,18 +44,5 @@ module Secryst
71
44
  @itos.length
72
45
  end
73
46
  alias_method :size, :length
74
-
75
- def self.build_vocab_from_iterator(iterator)
76
- counter = Hash.new(0)
77
- i = 0
78
- iterator.each do |tokens|
79
- tokens.each do |token|
80
- counter[token] += 1
81
- end
82
- i += 1
83
- puts "Processed #{i}" if i % 10000 == 0
84
- end
85
- Vocab.new(counter)
86
- end
87
47
  end
88
48
  end
data/lib/secryst.rb CHANGED
@@ -1,11 +1,22 @@
1
- require 'json'
1
+ require 'yaml'
2
+ require 'zip'
3
+ require 'numo/narray'
2
4
 
3
- # torch
4
- require "torch-rb"
5
+ require 'secryst/vocab'
6
+ require 'secryst/imf'
7
+ require 'secryst/byt5_onnx'
8
+ require 'secryst/translator'
9
+ require 'secryst/model'
10
+ require 'secryst/provisioning'
5
11
 
6
- # transformer model
7
- require "secryst/multihead_attention"
8
- require "secryst/vocab"
9
- require "secryst/transformer"
10
-
11
- require "secryst/translator"
12
+ module Secryst
13
+ DEFAULT_HYPERPARAMETERS = {
14
+ d_model: 64,
15
+ nhead: 8,
16
+ num_encoder_layers: 4,
17
+ num_decoder_layers: 4,
18
+ dim_feedforward: 256,
19
+ dropout: 0.05,
20
+ activation: 'relu',
21
+ }.freeze
22
+ end
Binary file
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+ require "json"
3
+ require "secryst/byt5_onnx"
4
+
5
+ # Cross-crystal parity (interscript-ml v1, requirement C5): the Ruby
6
+ # crystal must reproduce the Python reference goldens byte-for-byte on
7
+ # the deterministic decode-loop fixture. CI checks out secryst/secryst-py
8
+ # (parity/ = zip + golden.jsonl); locally point SECRYST_PARITY_DIR at it.
9
+ parity_dir = ENV["SECRYST_PARITY_DIR"] || File.expand_path("../../secryst-py/parity", __dir__)
10
+ zip_path = File.join(parity_dir, "tiny-1.0.zip")
11
+ goldens_path = File.join(parity_dir, "golden.jsonl")
12
+
13
+ describe "cross-crystal parity" do
14
+ let(:model) { Secryst::Byt5Onnx.new(zip_path) }
15
+
16
+ it "reproduces the reference goldens exactly" do
17
+ skip "parity kit not found (#{parity_dir})" unless File.exist?(goldens_path)
18
+
19
+ goldens = File.readlines(goldens_path).map { |l| JSON.parse(l) }
20
+ expect(goldens).not_to be_empty
21
+
22
+ failures = goldens.reject do |row|
23
+ model.translate(row["input"], max_seq_length: 8) == row["output"]
24
+ end
25
+
26
+ aggregate_failures "golden diff" do
27
+ failures.each do |row|
28
+ expect(model.translate(row["input"], max_seq_length: 8))
29
+ .to eq(row["output"]), "input #{row['input'].inspect}"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'secryst/byt5_onnx'
3
+
4
+ RSpec.describe Secryst::Byt5Onnx do
5
+ it 'uses the ByT5 byte conventions' do
6
+ expect(Secryst::IMF::PAD_ID).to eq(0)
7
+ expect(Secryst::IMF::EOS_ID).to eq(1)
8
+ end
9
+
10
+ describe 'byte round-trip (as used for generated output)' do
11
+ it 'packs generated bytes back into UTF-8 text' do
12
+ text = 'ភាសាខ្មែរ'
13
+ bytes = text.bytes
14
+ expect(bytes.pack('C*').force_encoding(Encoding::UTF_8)).to eq(text)
15
+ end
16
+ end
17
+ end