interscript 2.4.5 → 3.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.
- checksums.yaml +4 -4
- data/README.md +15 -0
- data/docs/demo/20191118-interscript-demo-cast.gif +0 -0
- data/exe/codemod-imp-to-isc +5 -0
- data/exe/diagnose_parse_failures +47 -0
- data/exe/interscript +2 -1
- data/exe/verify_isc_deep +211 -0
- data/exe/verify_isc_equivalence +128 -0
- data/interscript.gemspec +27 -20
- data/lib/interscript/command.rb +21 -12
- data/lib/interscript/compiler/javascript.rb +34 -35
- data/lib/interscript/compiler/json_ir.rb +214 -0
- data/lib/interscript/compiler/python.rb +354 -0
- data/lib/interscript/compiler/ruby.rb +23 -22
- data/lib/interscript/compiler.rb +20 -3
- data/lib/interscript/detector.rb +14 -7
- data/lib/interscript/dsl/aliases.rb +1 -1
- data/lib/interscript/dsl/document.rb +6 -3
- data/lib/interscript/dsl/group/parallel.rb +1 -1
- data/lib/interscript/dsl/group.rb +18 -9
- data/lib/interscript/dsl/items.rb +28 -16
- data/lib/interscript/dsl/metadata.rb +19 -18
- data/lib/interscript/dsl/stage.rb +1 -1
- data/lib/interscript/dsl/symbol_mm.rb +4 -2
- data/lib/interscript/dsl/tests.rb +1 -1
- data/lib/interscript/dsl.rb +21 -13
- data/lib/interscript/interpreter.rb +32 -21
- data/lib/interscript/isc/codemod.rb +791 -0
- data/lib/interscript/isc/document_builder.rb +354 -0
- data/lib/interscript/isc/generator.rb +191 -0
- data/lib/interscript/isc/grammar/concerns/aliases.rb +29 -0
- data/lib/interscript/isc/grammar/concerns/dependencies.rb +23 -0
- data/lib/interscript/isc/grammar/concerns/items.rb +176 -0
- data/lib/interscript/isc/grammar/concerns/metadata.rb +142 -0
- data/lib/interscript/isc/grammar/concerns/primitives.rb +112 -0
- data/lib/interscript/isc/grammar/concerns/stages.rb +129 -0
- data/lib/interscript/isc/grammar/concerns/system.rb +37 -0
- data/lib/interscript/isc/grammar/concerns/tests.rb +31 -0
- data/lib/interscript/isc/grammar/concerns.rb +18 -0
- data/lib/interscript/isc/grammar/core.rb +23 -0
- data/lib/interscript/isc/grammar/isc.artifact.json +1 -0
- data/lib/interscript/isc/grammar/isc.parg +196 -0
- data/lib/interscript/isc/grammar.rb +12 -0
- data/lib/interscript/isc/items.rb +185 -0
- data/lib/interscript/isc/model/alias.rb +20 -0
- data/lib/interscript/isc/model/constraint.rb +20 -0
- data/lib/interscript/isc/model/dependency.rb +19 -0
- data/lib/interscript/isc/model/document.rb +31 -0
- data/lib/interscript/isc/model/item.rb +50 -0
- data/lib/interscript/isc/model/rule.rb +23 -0
- data/lib/interscript/isc/model/stage.rb +20 -0
- data/lib/interscript/isc/model/stage_item.rb +33 -0
- data/lib/interscript/isc/model/test.rb +21 -0
- data/lib/interscript/isc/model.rb +19 -0
- data/lib/interscript/isc/node_adapter.rb +310 -0
- data/lib/interscript/isc/normalizer.rb +127 -0
- data/lib/interscript/isc/parser.rb +78 -0
- data/lib/interscript/isc/serializer.rb +248 -0
- data/lib/interscript/isc/transform.rb +138 -0
- data/lib/interscript/isc/yaml_bridge.rb +236 -0
- data/lib/interscript/isc.rb +30 -0
- data/lib/interscript/ml/byt5_onnx.rb +102 -0
- data/lib/interscript/ml/imf.rb +182 -0
- data/lib/interscript/ml/model.rb +52 -0
- data/lib/interscript/ml/provisioning.rb +189 -0
- data/lib/interscript/ml/translator.rb +38 -0
- data/lib/interscript/ml/vocab.rb +48 -0
- data/lib/interscript/ml.rb +25 -0
- data/lib/interscript/node/alias_def.rb +5 -5
- data/lib/interscript/node/dependency.rb +7 -7
- data/lib/interscript/node/document.rb +10 -10
- data/lib/interscript/node/group.rb +11 -9
- data/lib/interscript/node/item/alias.rb +14 -10
- data/lib/interscript/node/item/any.rb +14 -7
- data/lib/interscript/node/item/capture.rb +14 -9
- data/lib/interscript/node/item/group.rb +22 -17
- data/lib/interscript/node/item/repeat.rb +3 -3
- data/lib/interscript/node/item/stage.rb +4 -5
- data/lib/interscript/node/item/string.rb +18 -13
- data/lib/interscript/node/item.rb +27 -17
- data/lib/interscript/node/metadata.rb +6 -5
- data/lib/interscript/node/rule/funcall.rb +4 -5
- data/lib/interscript/node/rule/run.rb +4 -5
- data/lib/interscript/node/rule/sub.rb +55 -55
- data/lib/interscript/node/rule.rb +6 -5
- data/lib/interscript/node/stage.rb +11 -14
- data/lib/interscript/node/tests.rb +6 -6
- data/lib/interscript/node.rb +14 -17
- data/lib/interscript/stdlib/functions/rababa_adapter.rb +56 -0
- data/lib/interscript/stdlib/functions/secryst_adapter.rb +37 -0
- data/lib/interscript/stdlib/functions.rb +49 -0
- data/lib/interscript/stdlib.rb +25 -95
- data/lib/interscript/utils/helpers.rb +8 -5
- data/lib/interscript/utils/regexp_converter.rb +143 -144
- data/lib/interscript/version.rb +1 -1
- data/lib/interscript/visualize/json.rb +12 -13
- data/lib/interscript/visualize/nodes.rb +13 -13
- data/lib/interscript/visualize.rb +18 -18
- data/lib/interscript.rb +59 -45
- metadata +88 -31
- data/.github/workflows/assets.yml +0 -91
- data/.github/workflows/rake.yml +0 -52
- data/.github/workflows/release.yml +0 -68
- data/.gitignore +0 -79
- data/.rspec +0 -3
- data/Gemfile +0 -38
- data/Rakefile +0 -139
- data/bin/console +0 -10
- data/bin/interscript +0 -5
- data/bin/maps_analyze_staging +0 -168
- data/bin/maps_debug_compilers +0 -58
- data/bin/maps_debug_ordering +0 -88
- data/bin/maps_debug_ruby_compile +0 -24
- data/bin/maps_debug_step_by_step +0 -44
- data/bin/maps_optimize_order +0 -112
- data/bin/maps_v1_analyze_regexps +0 -45
- data/bin/maps_v1_to_v2 +0 -426
- data/bin/set_version +0 -16
- data/bin/setup +0 -8
- data/requirements.txt +0 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parsanol"
|
|
4
|
+
|
|
5
|
+
module Interscript
|
|
6
|
+
module Isc
|
|
7
|
+
autoload :Parser, "interscript/isc/parser"
|
|
8
|
+
autoload :Transform, "interscript/isc/transform"
|
|
9
|
+
autoload :DocumentBuilder, "interscript/isc/document_builder"
|
|
10
|
+
autoload :Grammar, "interscript/isc/grammar"
|
|
11
|
+
autoload :Items, "interscript/isc/items"
|
|
12
|
+
autoload :Codemod, "interscript/isc/codemod"
|
|
13
|
+
autoload :NodeAdapter, "interscript/isc/node_adapter"
|
|
14
|
+
autoload :Model, "interscript/isc/model"
|
|
15
|
+
autoload :YamlBridge, "interscript/isc/yaml_bridge"
|
|
16
|
+
autoload :Serializer, "interscript/isc/serializer"
|
|
17
|
+
autoload :Normalizer, "interscript/isc/normalizer"
|
|
18
|
+
autoload :Generator, "interscript/isc/generator"
|
|
19
|
+
|
|
20
|
+
SCHEMA_VERSION = 1
|
|
21
|
+
|
|
22
|
+
def self.parse(source, filename: nil)
|
|
23
|
+
Parser.parse(source, filename: filename)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.load_file(path)
|
|
27
|
+
parse(File.read(path, encoding: "UTF-8"), filename: path)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require 'tmpdir'
|
|
2
|
+
|
|
3
|
+
module Interscript::ML
|
|
4
|
+
# Byte-level seq2seq (ByT5 family) inference over ONNX sessions from an
|
|
5
|
+
# IMF v1 model zip: sha256-verified graphs, greedy KV-cache decode when
|
|
6
|
+
# the zip ships decoder-kv.onnx (default), plain full-recompute
|
|
7
|
+
# fallback otherwise. Tokenization is the canonical ByT5 table
|
|
8
|
+
# (byte b -> id b+3, trailing EOS) — see Interscript::ML::IMF.
|
|
9
|
+
class Byt5Onnx
|
|
10
|
+
def initialize(zip_path)
|
|
11
|
+
Interscript::ML.require_optional!("onnxruntime")
|
|
12
|
+
@manifest = IMF.manifest(zip_path)
|
|
13
|
+
graphs = IMF.verify_and_read(zip_path)
|
|
14
|
+
@tmpdir = Dir.mktmpdir('secryst-imf')
|
|
15
|
+
@encoder = build_session(graphs, 'encoder.onnx')
|
|
16
|
+
@kv = @manifest['decoder'] == 'kv' && graphs.key?('decoder-kv.onnx')
|
|
17
|
+
@decoder = build_session(graphs, @kv ? 'decoder-kv.onnx' : 'decoder.onnx')
|
|
18
|
+
@pasts = @kv ? zero_pasts : {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def translate(text, max_seq_length: 256)
|
|
22
|
+
ids = IMF.encode(text)
|
|
23
|
+
return '' if ids.length == 1 # only the trailing EOS: empty input
|
|
24
|
+
|
|
25
|
+
hidden = @encoder.predict({ input_ids: [ids] })['last_hidden_state']
|
|
26
|
+
tokens = @kv ? greedy_kv(hidden, max_seq_length) : greedy_plain(hidden, max_seq_length)
|
|
27
|
+
IMF.decode(tokens)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def id
|
|
31
|
+
@manifest['id']
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# The onnxruntime gem loads from paths, not bytes: verified bytes go
|
|
37
|
+
# to a private tmpdir.
|
|
38
|
+
def build_session(graphs, name)
|
|
39
|
+
path = File.join(@tmpdir, name)
|
|
40
|
+
File.binwrite(path, graphs.fetch(name))
|
|
41
|
+
OnnxRuntime::Model.new(path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def zero_pasts
|
|
45
|
+
@decoder.inputs.each_with_object({}) do |meta, pasts|
|
|
46
|
+
next unless meta[:name].start_with?('past_')
|
|
47
|
+
shape = meta[:shape] # [batch, heads, past_seq, d_kv]; dynamic dims are Strings
|
|
48
|
+
heads = shape[1].is_a?(Integer) ? shape[1] : 4
|
|
49
|
+
d_kv = shape[3].is_a?(Integer) ? shape[3] : 8
|
|
50
|
+
# Numo carries the explicit [1, heads, 0, d_kv] shape through;
|
|
51
|
+
# nested Ruby arrays cannot express a zero-length dim.
|
|
52
|
+
pasts[meta[:name]] = Numo::DFloat.zeros(1, heads, 0, d_kv)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def argmax(row)
|
|
57
|
+
best = 0
|
|
58
|
+
best_val = -Float::INFINITY
|
|
59
|
+
row.each_with_index do |value, index|
|
|
60
|
+
if value > best_val
|
|
61
|
+
best_val = value
|
|
62
|
+
best = index
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
best
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def greedy_kv(hidden, max_seq_length)
|
|
69
|
+
pasts = @pasts.dup
|
|
70
|
+
current = [IMF::PAD_ID]
|
|
71
|
+
generated = []
|
|
72
|
+
max_seq_length.times do
|
|
73
|
+
results = @decoder.predict(
|
|
74
|
+
{ input_ids: [current], encoder_hidden_states: hidden }.merge(pasts)
|
|
75
|
+
)
|
|
76
|
+
token = argmax(results['logits'].first.last)
|
|
77
|
+
break if token == IMF::EOS_ID
|
|
78
|
+
|
|
79
|
+
generated << token
|
|
80
|
+
pasts = pasts.keys.to_h { |name| [name, results[name.sub('past_', 'present_')]] }
|
|
81
|
+
current = [token]
|
|
82
|
+
end
|
|
83
|
+
generated
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def greedy_plain(hidden, max_seq_length)
|
|
87
|
+
decoder_ids = [IMF::PAD_ID]
|
|
88
|
+
generated = []
|
|
89
|
+
max_seq_length.times do
|
|
90
|
+
logits = @decoder.predict(
|
|
91
|
+
{ input_ids: [decoder_ids], encoder_hidden_states: hidden }
|
|
92
|
+
)['logits']
|
|
93
|
+
token = argmax(logits.first.last)
|
|
94
|
+
break if token == IMF::EOS_ID
|
|
95
|
+
|
|
96
|
+
generated << token
|
|
97
|
+
decoder_ids = decoder_ids + [token]
|
|
98
|
+
end
|
|
99
|
+
generated
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "digest/sha2"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open-uri"
|
|
5
|
+
|
|
6
|
+
module Interscript::ML
|
|
7
|
+
# Interscript Model Format v1 — the byte-level runtime contract shared
|
|
8
|
+
# with the Python (interscript-ml) and TypeScript (npm: secryst)
|
|
9
|
+
# runtimes. Token ids follow the canonical ByT5 table: byte b -> b+3,
|
|
10
|
+
# trailing EOS; pad=0, unk=2. Ids are NOT raw byte values.
|
|
11
|
+
module IMF
|
|
12
|
+
BYTE_OFFSET = 3
|
|
13
|
+
PAD_ID = 0
|
|
14
|
+
EOS_ID = 1
|
|
15
|
+
UNK_ID = 2
|
|
16
|
+
|
|
17
|
+
DEFAULT_INDEX_URL = "https://github.com/interscript/interscript-ml/releases/download/index-v7/models-index.yaml"
|
|
18
|
+
|
|
19
|
+
class FormatError < StandardError; end
|
|
20
|
+
class RegistryError < StandardError; end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def require_zip!
|
|
24
|
+
Interscript::ML.require_optional!("rubyzip", feature: "zip")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def encode(text)
|
|
28
|
+
text.bytes.map { |b| b + BYTE_OFFSET } + [EOS_ID]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def decode(token_ids)
|
|
32
|
+
out = +""
|
|
33
|
+
token_ids.each do |token|
|
|
34
|
+
break if token == EOS_ID
|
|
35
|
+
next if token == PAD_ID || token == UNK_ID
|
|
36
|
+
out << ((token - BYTE_OFFSET) % 256).chr
|
|
37
|
+
end
|
|
38
|
+
out.force_encoding(Encoding::UTF_8)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def manifest(zip_path)
|
|
42
|
+
require_zip!
|
|
43
|
+
Zip::File.open(zip_path) do |zf|
|
|
44
|
+
raise FormatError, "missing metadata.yaml" unless zf.find_entry("metadata.yaml")
|
|
45
|
+
meta = YAML.safe_load(zf.read("metadata.yaml"), permitted_classes: [], aliases: false)
|
|
46
|
+
raise FormatError, "unsupported format: #{meta["format"].inspect}" if meta["format"] != "imf-v1"
|
|
47
|
+
if meta["tokenizer"] != "bytes"
|
|
48
|
+
raise FormatError, "tokenizer #{meta["tokenizer"].inspect}: this runtime is byte-level only"
|
|
49
|
+
end
|
|
50
|
+
%w[encoder.onnx decoder.onnx].each do |required|
|
|
51
|
+
raise FormatError, "missing #{required}" unless zf.find_entry(required)
|
|
52
|
+
end
|
|
53
|
+
meta
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Reads every .onnx member after verifying its sha256 against the
|
|
58
|
+
# manifest — corrupt zips fail loudly, before any session loads.
|
|
59
|
+
def verify_and_read(zip_path)
|
|
60
|
+
meta = manifest(zip_path)
|
|
61
|
+
sha = meta.fetch("sha256", {})
|
|
62
|
+
graphs = {}
|
|
63
|
+
require_zip!
|
|
64
|
+
Zip::File.open(zip_path) do |zf|
|
|
65
|
+
zf.entries.select { |e| e.name.end_with?(".onnx") }.each do |entry|
|
|
66
|
+
recorded = sha[entry.name]
|
|
67
|
+
raise FormatError, "#{entry.name} is not covered by metadata sha256" unless recorded
|
|
68
|
+
bytes = entry.get_input_stream.read
|
|
69
|
+
actual = Digest::SHA256.hexdigest(bytes)
|
|
70
|
+
if actual != recorded
|
|
71
|
+
raise FormatError, "#{entry.name} sha256 mismatch: zip has #{actual}, metadata says #{recorded}"
|
|
72
|
+
end
|
|
73
|
+
graphs[entry.name] = bytes
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
graphs
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def cache_dir
|
|
80
|
+
ENV["INTERSCRIPT_ML_CACHE"] || ENV["SECRYST_CACHE"] || File.join(Dir.home, ".cache", "interscript-ml")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# models.yaml resolution: cache hit (re-verified), or download ->
|
|
84
|
+
# verify whole-file sha256 -> atomic install into the cache.
|
|
85
|
+
# Entries with `parts` (GitHub's 2 GiB per-asset cap) are streamed
|
|
86
|
+
# in order, each part sha256-verified as it lands, then the
|
|
87
|
+
# assembled file is checked against the whole-file sha256.
|
|
88
|
+
def resolve(model_id, index_url: nil)
|
|
89
|
+
source = index_url || ENV["INTERSCRIPT_ML_INDEX"] || ENV["SECRYST_INDEX"] || DEFAULT_INDEX_URL
|
|
90
|
+
entries = load_index(source)
|
|
91
|
+
entry = entries[model_id]
|
|
92
|
+
raise RegistryError, "unknown model id #{model_id.inspect} (known: #{entries.keys.sort})" unless entry
|
|
93
|
+
|
|
94
|
+
target = File.join(cache_dir, "models", model_id, entry["filename"])
|
|
95
|
+
if File.file?(target) && Digest::SHA256.file(target).hexdigest == entry["sha256"]
|
|
96
|
+
return target
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
100
|
+
tmp = target + ".part.#{Process.pid}"
|
|
101
|
+
if entry["parts"]
|
|
102
|
+
download_parts(entry, tmp)
|
|
103
|
+
else
|
|
104
|
+
channel = entry["url"]
|
|
105
|
+
if channel.start_with?("file://")
|
|
106
|
+
FileUtils.cp(channel.sub(%r{\Afile://}, ""), tmp)
|
|
107
|
+
else
|
|
108
|
+
URI.open(channel) { |remote| IO.copy_stream(remote, tmp) }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
actual = Digest::SHA256.file(tmp).hexdigest
|
|
112
|
+
unless actual == entry["sha256"]
|
|
113
|
+
File.delete(tmp)
|
|
114
|
+
raise RegistryError, "downloaded #{entry["filename"]} sha256 mismatch: got #{actual}, index says #{entry["sha256"]}"
|
|
115
|
+
end
|
|
116
|
+
File.rename(tmp, target)
|
|
117
|
+
target
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def download_parts(entry, tmp)
|
|
123
|
+
File.open(tmp, "wb") do |out|
|
|
124
|
+
entry["parts"].each_with_index do |part, index|
|
|
125
|
+
digest = Digest::SHA256.new
|
|
126
|
+
source = part["url"].start_with?("file://") ? part["url"].sub(%r{\Afile://}, "") : part["url"]
|
|
127
|
+
open_stream = lambda do |io|
|
|
128
|
+
while (chunk = io.read(1024 * 1024))
|
|
129
|
+
out.write(chunk)
|
|
130
|
+
digest.update(chunk)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
if part["url"].start_with?("file://")
|
|
134
|
+
File.open(source, "rb", &open_stream)
|
|
135
|
+
else
|
|
136
|
+
URI.open(source, "rb", &open_stream)
|
|
137
|
+
end
|
|
138
|
+
unless digest.hexdigest == part["sha256"]
|
|
139
|
+
raise RegistryError, "part #{index} of #{entry["filename"]} sha256 mismatch: got #{digest.hexdigest}, index says #{part["sha256"]}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
rescue StandardError
|
|
144
|
+
File.delete(tmp) if File.file?(tmp)
|
|
145
|
+
raise
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def load_index(source)
|
|
149
|
+
text = if source.start_with?("http://", "https://")
|
|
150
|
+
fetch_index_with_sidecar(source)
|
|
151
|
+
else
|
|
152
|
+
File.read(source)
|
|
153
|
+
end
|
|
154
|
+
raw = YAML.safe_load(text, permitted_classes: [], aliases: false)
|
|
155
|
+
raise RegistryError, "index must have version: 1" if raw["version"] != 1
|
|
156
|
+
raw.fetch("models", {})
|
|
157
|
+
end
|
|
158
|
+
public :load_index
|
|
159
|
+
|
|
160
|
+
# HTTP sources must ship a sibling .sha256 sidecar; verify the
|
|
161
|
+
# body against it before parsing (GitHub Releases index assets do).
|
|
162
|
+
def fetch_index_with_sidecar(source)
|
|
163
|
+
body = URI.open(source, "rb") { |remote| remote.read }
|
|
164
|
+
sidecar =
|
|
165
|
+
begin
|
|
166
|
+
URI.open("#{source}.sha256") { |remote| remote.read }
|
|
167
|
+
rescue OpenURI::HTTPError => e
|
|
168
|
+
raise RegistryError, "index sha256 sidecar missing: #{source}.sha256 (#{e.message})"
|
|
169
|
+
end
|
|
170
|
+
expected = sidecar.to_s.strip.split(/\s+/).first.to_s
|
|
171
|
+
unless expected.match?(/\A[0-9a-fA-F]{64}\z/)
|
|
172
|
+
raise RegistryError, "index sha256 sidecar malformed: #{source}.sha256"
|
|
173
|
+
end
|
|
174
|
+
actual = Digest::SHA256.hexdigest(body)
|
|
175
|
+
unless actual == expected.downcase
|
|
176
|
+
raise RegistryError, "index sha256 mismatch: got #{actual}, sidecar says #{expected.downcase}"
|
|
177
|
+
end
|
|
178
|
+
body.force_encoding(Encoding::UTF_8)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Interscript::ML
|
|
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
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
require 'open-uri'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'digest/sha2'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
module Interscript::ML
|
|
8
|
+
# Module Interscript::ML::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["INTERSCRIPT_ML_DATA"] || 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
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Interscript::ML
|
|
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.
|
|
5
|
+
class Translator
|
|
6
|
+
attr_accessor :model
|
|
7
|
+
|
|
8
|
+
def initialize(model_file:)
|
|
9
|
+
@device = 'cpu'
|
|
10
|
+
@model = Model.from_file(model_file)
|
|
11
|
+
end
|
|
12
|
+
|
|
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
|
+
|
|
16
|
+
Interscript::ML.require_optional!("numo-narray", feature: "numo/narray")
|
|
17
|
+
input = ['<sos>'] + phrase.chars + ['<eos>']
|
|
18
|
+
input = Numo::NArray[input.map { |i| @model.input_vocab.stoi[i] }].transpose
|
|
19
|
+
output = Numo::NArray[[@model.target_vocab.stoi['<sos>']]]
|
|
20
|
+
src_key_padding_mask = input.transpose.eq(1)
|
|
21
|
+
|
|
22
|
+
max_seq_length.times do |i|
|
|
23
|
+
tgt_key_padding_mask = output.transpose.eq(1)
|
|
24
|
+
tgt_mask = Numo::DFloat.ones(i + 1, i + 1).triu.transpose.eq(0)
|
|
25
|
+
prediction = @model.argmax(input, output.dup,
|
|
26
|
+
tgt_mask: tgt_mask,
|
|
27
|
+
src_key_padding_mask: src_key_padding_mask,
|
|
28
|
+
tgt_key_padding_mask: tgt_key_padding_mask,
|
|
29
|
+
memory_key_padding_mask: src_key_padding_mask)
|
|
30
|
+
break if @model.target_vocab.itos[prediction[i]] == '<eos>'
|
|
31
|
+
|
|
32
|
+
output = Numo::NArray.concatenate([output, Numo::NArray[[prediction[i]]]])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
output[1..-1].to_a.flatten.map { |i| @model.target_vocab.itos[i] }.join('')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Interscript::ML
|
|
2
|
+
class Vocab
|
|
3
|
+
UNK = "<unk>"
|
|
4
|
+
attr_reader :stoi, :itos
|
|
5
|
+
|
|
6
|
+
def initialize(
|
|
7
|
+
list, specials: ["<unk>", "<pad>", "<sos>", "<eos>"], specials_first: true
|
|
8
|
+
)
|
|
9
|
+
@unk_index = nil
|
|
10
|
+
@itos = []
|
|
11
|
+
if specials_first && (list & specials).length == 0
|
|
12
|
+
@itos = specials
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@itos += list
|
|
16
|
+
|
|
17
|
+
if !specials_first && (list & specials).length == 0
|
|
18
|
+
@itos.concat(specials)
|
|
19
|
+
end
|
|
20
|
+
|
|
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)
|
|
28
|
+
else
|
|
29
|
+
@stoi = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# stoi is simply a reverse dict for itos
|
|
34
|
+
@itos.each_with_index do |tok, i|
|
|
35
|
+
@stoi[tok] = i
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def [](token)
|
|
40
|
+
@stoi.fetch(token, @stoi.fetch(UNK))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def length
|
|
44
|
+
@itos.length
|
|
45
|
+
end
|
|
46
|
+
alias_method :size, :length
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The neural layer: IMF v1 artifact runtime + index provisioning for
|
|
4
|
+
# byte-level ByT5 models, ported from the secryst crystal (1.1.1) into
|
|
5
|
+
# interscript proper. Runtime dependencies rubyzip and onnxruntime are
|
|
6
|
+
# OPTIONAL: they load lazily with a helpful error when absent.
|
|
7
|
+
#
|
|
8
|
+
# Interscript::ML::Translator.new(model_file: "khm-latn-1.0")
|
|
9
|
+
# .translate("ភាសា")
|
|
10
|
+
module Interscript::ML
|
|
11
|
+
autoload :IMF, "interscript/ml/imf"
|
|
12
|
+
autoload :Provisioning, "interscript/ml/provisioning"
|
|
13
|
+
autoload :Translator, "interscript/ml/translator"
|
|
14
|
+
autoload :Model, "interscript/ml/model"
|
|
15
|
+
autoload :Vocab, "interscript/ml/vocab"
|
|
16
|
+
autoload :Byt5Onnx, "interscript/ml/byt5_onnx"
|
|
17
|
+
|
|
18
|
+
# Expected to be loaded before IMF/Byt5Onnx entry points.
|
|
19
|
+
def self.require_optional!(gem_name, feature: gem_name)
|
|
20
|
+
require feature
|
|
21
|
+
rescue LoadError => e
|
|
22
|
+
raise Interscript::ExternalUtilError,
|
|
23
|
+
"the neural layer needs the #{gem_name} gem: gem install #{gem_name} (or use a bundle including it)"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -9,13 +9,13 @@ class Interscript::Node::AliasDef < Interscript::Node
|
|
|
9
9
|
|
|
10
10
|
def ==(other)
|
|
11
11
|
super &&
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
name == other.name &&
|
|
13
|
+
data == other.data
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def to_hash
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
{class: self.class.to_s,
|
|
18
|
+
name: @name,
|
|
19
|
+
data: @data.to_hash}
|
|
20
20
|
end
|
|
21
21
|
end
|