cohere-transcribe 0.1.1 → 0.1.2
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 +8 -0
- data/README.md +5 -5
- data/ext/cohere_transcribe_native/CMakeLists.txt +1 -1
- data/ext/cohere_transcribe_native/README.md +1 -1
- data/ext/cohere_transcribe_native/extconf.rb +12 -2
- data/lib/cohere/transcribe/asr/native.rb +72 -8
- data/lib/cohere/transcribe/audio/decoder.rb +130 -82
- data/lib/cohere/transcribe/audio/ffmpeg_native.rb +68 -39
- data/lib/cohere/transcribe/cli.rb +1 -0
- data/lib/cohere/transcribe/configuration.rb +2 -0
- data/lib/cohere/transcribe/dense_converter.rb +29 -4
- data/lib/cohere/transcribe/doctor.rb +2 -2
- data/lib/cohere/transcribe/hub.rb +26 -5
- data/lib/cohere/transcribe/input.rb +4 -4
- data/lib/cohere/transcribe/python_text.rb +21 -3
- data/lib/cohere/transcribe/pytorch_checkpoint.rb +68 -11
- data/lib/cohere/transcribe/runtime/engine.rb +11 -1
- data/lib/cohere/transcribe/runtime/preparation.rb +81 -13
- data/lib/cohere/transcribe/runtime/resources.rb +105 -17
- data/lib/cohere/transcribe/state/io.rb +12 -2
- data/lib/cohere/transcribe/state/manifest.rb +30 -16
- data/lib/cohere/transcribe/version.rb +1 -1
- metadata +1 -1
|
@@ -28,6 +28,7 @@ module Cohere
|
|
|
28
28
|
▁ <|startofcontext|> <|startoftranscript|> <|emo:undefined|>
|
|
29
29
|
<|ar|> <|pnc|> <|noitn|> <|notimestamp|> <|nodiarize|> <|endoftext|>
|
|
30
30
|
].freeze
|
|
31
|
+
MAX_VOCABULARY_SIZE = (1 << 32) - 1
|
|
31
32
|
OUTPUT_TYPES = %i[f16 f32 bf16].freeze
|
|
32
33
|
REQUIRED_ARTIFACT_FILENAMES = %w[config.json tokenizer.json].freeze
|
|
33
34
|
WEIGHT_ARTIFACT_FILENAMES = %w[
|
|
@@ -291,7 +292,9 @@ module Cohere
|
|
|
291
292
|
|
|
292
293
|
def load_vocabulary
|
|
293
294
|
embedding = fetch_source_tensor("transf_decoder._embedding.token_embedding.weight")
|
|
294
|
-
unless embedding.shape.length == 2 &&
|
|
295
|
+
unless embedding.shape.length == 2 &&
|
|
296
|
+
embedding.shape.all? { |dimension| dimension.is_a?(Integer) && dimension.positive? } &&
|
|
297
|
+
embedding.shape[0] <= MAX_VOCABULARY_SIZE
|
|
295
298
|
raise Error, "Tensor #{embedding.name.inspect} has an invalid vocabulary shape #{embedding.shape.inspect}"
|
|
296
299
|
end
|
|
297
300
|
|
|
@@ -320,13 +323,35 @@ module Cohere
|
|
|
320
323
|
)
|
|
321
324
|
end
|
|
322
325
|
|
|
323
|
-
|
|
324
|
-
|
|
326
|
+
ordered_ids = tokens_by_id.keys.sort
|
|
327
|
+
if ordered_ids.length != expected_size
|
|
328
|
+
missing = first_missing_vocabulary_ids(ordered_ids, expected_size, limit: 8)
|
|
329
|
+
raise Error, "tokenizer.json vocabulary has missing token IDs: #{missing.inspect}"
|
|
330
|
+
end
|
|
325
331
|
|
|
326
332
|
missing_prompt = REQUIRED_PROMPT_TOKENS.reject { |token| ids_by_token.key?(token) }
|
|
327
333
|
raise Error, "tokenizer.json is missing Cohere prompt tokens: #{missing_prompt.join(", ")}" unless missing_prompt.empty?
|
|
328
334
|
|
|
329
|
-
|
|
335
|
+
ordered_ids.map { |id| tokens_by_id.fetch(id) }
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def first_missing_vocabulary_ids(ordered_ids, expected_size, limit:)
|
|
339
|
+
missing = []
|
|
340
|
+
expected_id = 0
|
|
341
|
+
ordered_ids.each do |id|
|
|
342
|
+
while expected_id < id && missing.length < limit
|
|
343
|
+
missing << expected_id
|
|
344
|
+
expected_id += 1
|
|
345
|
+
end
|
|
346
|
+
break if missing.length == limit
|
|
347
|
+
|
|
348
|
+
expected_id = id + 1
|
|
349
|
+
end
|
|
350
|
+
while expected_id < expected_size && missing.length < limit
|
|
351
|
+
missing << expected_id
|
|
352
|
+
expected_id += 1
|
|
353
|
+
end
|
|
354
|
+
missing
|
|
330
355
|
end
|
|
331
356
|
|
|
332
357
|
def add_vocabulary_entry!(tokens_by_id, ids_by_token, token, id, expected_size:)
|
|
@@ -282,7 +282,7 @@ module Cohere
|
|
|
282
282
|
library = native_library || ASR::NativeLibrary.load
|
|
283
283
|
results.ok("native Cohere ASR runtime: #{library.path}")
|
|
284
284
|
report_native_device_capabilities(results, library)
|
|
285
|
-
rescue StandardError => e
|
|
285
|
+
rescue LoadError, StandardError => e
|
|
286
286
|
results.fail("native Cohere ASR runtime: #{e.class}: #{e.message}")
|
|
287
287
|
end
|
|
288
288
|
|
|
@@ -347,7 +347,7 @@ module Cohere
|
|
|
347
347
|
"word aligner: #{Alignment::ModelProvider::REPOSITORY}@#{Alignment::ModelProvider::REVISION}, " \
|
|
348
348
|
"FP32 SHA-256 #{fp32.sha256}, bounded per-segment uniform fallback"
|
|
349
349
|
)
|
|
350
|
-
rescue StandardError => e
|
|
350
|
+
rescue LoadError, StandardError => e
|
|
351
351
|
results.fail("word alignment: #{e.class}: #{e.message}")
|
|
352
352
|
end
|
|
353
353
|
|
|
@@ -14,6 +14,7 @@ module Cohere
|
|
|
14
14
|
# clients are reused without copying multi-gigabyte model weights.
|
|
15
15
|
class Hub
|
|
16
16
|
class Error < StandardError; end
|
|
17
|
+
class ConnectionError < Error; end
|
|
17
18
|
class AuthenticationError < Error; end
|
|
18
19
|
class NotFoundError < Error; end
|
|
19
20
|
|
|
@@ -22,12 +23,17 @@ module Cohere
|
|
|
22
23
|
|
|
23
24
|
attr_reader :cache_dir, :endpoint
|
|
24
25
|
|
|
25
|
-
def initialize(cache_dir: nil, endpoint: nil, token: nil)
|
|
26
|
+
def initialize(cache_dir: nil, endpoint: nil, token: nil, offline: nil)
|
|
26
27
|
hf_home = ENV.fetch("HF_HOME", File.expand_path("~/.cache/huggingface"))
|
|
27
28
|
@cache_dir = Pathname(cache_dir || ENV.fetch("HF_HUB_CACHE", File.join(hf_home, "hub"))).expand_path
|
|
28
29
|
@endpoint = (endpoint || ENV.fetch("HF_ENDPOINT", DEFAULT_ENDPOINT)).sub(%r{/+\z}, "")
|
|
29
30
|
@endpoint_uri = URI(@endpoint)
|
|
30
31
|
@token = token || ENV["HF_TOKEN"] || cached_token(hf_home)
|
|
32
|
+
@offline = offline.nil? ? truthy_environment?(ENV.fetch("HF_HUB_OFFLINE", nil)) : !!offline
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def offline?
|
|
36
|
+
@offline
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
def resolve_revision(repo_id, revision = nil, filename: "config.json")
|
|
@@ -36,13 +42,23 @@ module Cohere
|
|
|
36
42
|
validate_revision!(requested)
|
|
37
43
|
return requested.downcase if COMMIT_PATTERN.match?(requested)
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
cached = cached_revision(repo_id, requested, filename: filename)
|
|
46
|
+
if offline?
|
|
47
|
+
return cached if cached
|
|
48
|
+
|
|
49
|
+
raise Error,
|
|
50
|
+
"Hub offline mode has no cached #{filename} snapshot for #{repo_id.inspect} at #{requested.inspect}"
|
|
41
51
|
end
|
|
42
52
|
|
|
43
53
|
encoded_repo = repo_id.split("/").map { |part| URI.encode_www_form_component(part) }.join("/")
|
|
44
54
|
encoded_revision = URI.encode_www_form_component(requested)
|
|
45
|
-
|
|
55
|
+
begin
|
|
56
|
+
response = request(URI("#{endpoint}/api/models/#{encoded_repo}/revision/#{encoded_revision}"))
|
|
57
|
+
rescue ConnectionError
|
|
58
|
+
raise unless cached
|
|
59
|
+
|
|
60
|
+
return cached
|
|
61
|
+
end
|
|
46
62
|
payload = JSON.parse(response.body)
|
|
47
63
|
commit = payload["sha"]
|
|
48
64
|
unless commit.is_a?(String) && COMMIT_PATTERN.match?(commit)
|
|
@@ -132,6 +148,7 @@ module Cohere
|
|
|
132
148
|
private
|
|
133
149
|
|
|
134
150
|
def request(uri, stream: nil, redirects: 0)
|
|
151
|
+
raise Error, "Hub offline mode prevents a request to #{uri}" if offline?
|
|
135
152
|
raise Error, "Too many redirects while fetching #{uri}" if redirects > 8
|
|
136
153
|
|
|
137
154
|
request = Net::HTTP::Get.new(uri)
|
|
@@ -185,7 +202,7 @@ module Cohere
|
|
|
185
202
|
raise_http_error!(response, uri)
|
|
186
203
|
rescue Timeout::Error, SocketError, SystemCallError, EOFError,
|
|
187
204
|
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, OpenSSL::SSL::SSLError => e
|
|
188
|
-
raise
|
|
205
|
+
raise ConnectionError, "Cannot reach Hugging Face Hub at #{uri}: #{e.class}: #{e.message}"
|
|
189
206
|
end
|
|
190
207
|
|
|
191
208
|
def response_content_length(response, uri)
|
|
@@ -382,6 +399,10 @@ module Cohere
|
|
|
382
399
|
nil
|
|
383
400
|
end
|
|
384
401
|
|
|
402
|
+
def truthy_environment?(value)
|
|
403
|
+
%w[1 ON YES TRUE].include?(value.to_s.upcase)
|
|
404
|
+
end
|
|
405
|
+
|
|
385
406
|
def cache_repo_name(repo_id)
|
|
386
407
|
"models--#{repo_id.gsub("/", "--")}"
|
|
387
408
|
end
|
|
@@ -34,12 +34,12 @@ module Cohere
|
|
|
34
34
|
raise TranscriptionInputError, "#{label} must not be empty" if PythonText.blank?(text)
|
|
35
35
|
|
|
36
36
|
text.dup.freeze
|
|
37
|
-
rescue TypeError, ArgumentError, SystemCallError => e
|
|
37
|
+
rescue EncodingError, TypeError, ArgumentError, SystemCallError => e
|
|
38
38
|
raise e if e.is_a?(TranscriptionInputError)
|
|
39
39
|
|
|
40
40
|
raise TranscriptionInputError, "#{label} is not a valid text path: #{e.message}"
|
|
41
41
|
end.freeze
|
|
42
|
-
rescue TypeError, ArgumentError, SystemCallError => e
|
|
42
|
+
rescue EncodingError, TypeError, ArgumentError, SystemCallError => e
|
|
43
43
|
raise e if e.is_a?(TranscriptionInputError)
|
|
44
44
|
|
|
45
45
|
raise TranscriptionInputError, "audio is not a valid ordered path sequence: #{e.message}"
|
|
@@ -86,7 +86,7 @@ module Cohere
|
|
|
86
86
|
paths.sort_by { |path| path.to_s.downcase(:fold) }.map do |path|
|
|
87
87
|
[strict_realpath(path), path.relative_path_from(source)]
|
|
88
88
|
end
|
|
89
|
-
rescue SystemCallError, ArgumentError => e
|
|
89
|
+
rescue EncodingError, SystemCallError, ArgumentError => e
|
|
90
90
|
raise TranscriptionInputError, "Cannot inspect input #{source}: #{e.message}"
|
|
91
91
|
end
|
|
92
92
|
private_class_method :directory_candidates
|
|
@@ -95,7 +95,7 @@ module Cohere
|
|
|
95
95
|
Pathname(value).expand_path.realpath
|
|
96
96
|
rescue Errno::ENOENT
|
|
97
97
|
raise TranscriptionInputError, "Input does not exist: #{Pathname(value).expand_path}"
|
|
98
|
-
rescue SystemCallError, ArgumentError => e
|
|
98
|
+
rescue EncodingError, SystemCallError, ArgumentError => e
|
|
99
99
|
detail = e.is_a?(Errno::ELOOP) ? "Symlink loop" : e.message
|
|
100
100
|
raise TranscriptionInputError, "Invalid input path #{value.inspect}: #{detail}"
|
|
101
101
|
end
|
|
@@ -10,6 +10,10 @@ module Cohere
|
|
|
10
10
|
LEADING_WHITESPACE = /\A#{WHITESPACE_CLASS}+/u
|
|
11
11
|
TRAILING_WHITESPACE = /#{WHITESPACE_CLASS}+\z/u
|
|
12
12
|
WHITESPACE_RUN = /#{WHITESPACE_CLASS}+/u
|
|
13
|
+
BINARY_WHITESPACE_CLASS = "[\x09-\x0D\x1C-\x20]"
|
|
14
|
+
BINARY_LEADING_WHITESPACE = /\A#{BINARY_WHITESPACE_CLASS}+/n
|
|
15
|
+
BINARY_TRAILING_WHITESPACE = /#{BINARY_WHITESPACE_CLASS}+\z/n
|
|
16
|
+
BINARY_WHITESPACE_RUN = /#{BINARY_WHITESPACE_CLASS}+/n
|
|
13
17
|
# Python 3.12 uses Unicode 15.0. Ruby 4 recognizes a small Unicode 16
|
|
14
18
|
# set whose newly assigned case/compatibility mappings would otherwise
|
|
15
19
|
# make the same input normalize differently from the reference package.
|
|
@@ -21,12 +25,15 @@ module Cohere
|
|
|
21
25
|
*(0x1CCD6..0x1CCF9)
|
|
22
26
|
].to_h { |codepoint| [codepoint, true] }.freeze
|
|
23
27
|
private_constant :WHITESPACE_CLASS, :LEADING_WHITESPACE, :TRAILING_WHITESPACE, :WHITESPACE_RUN,
|
|
28
|
+
:BINARY_WHITESPACE_CLASS, :BINARY_LEADING_WHITESPACE, :BINARY_TRAILING_WHITESPACE,
|
|
29
|
+
:BINARY_WHITESPACE_RUN,
|
|
24
30
|
:UNICODE_15_NORMALIZATION_BOUNDARIES
|
|
25
31
|
|
|
26
32
|
module_function
|
|
27
33
|
|
|
28
34
|
def strip(value)
|
|
29
|
-
|
|
35
|
+
leading, trailing, = whitespace_patterns(value)
|
|
36
|
+
value.sub(leading, "").sub(trailing, "")
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
def blank?(value)
|
|
@@ -35,11 +42,13 @@ module Cohere
|
|
|
35
42
|
|
|
36
43
|
def split(value)
|
|
37
44
|
stripped = strip(value)
|
|
38
|
-
|
|
45
|
+
_, _, run = whitespace_patterns(stripped)
|
|
46
|
+
stripped.empty? ? [] : stripped.split(run)
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def collapse(value)
|
|
42
|
-
|
|
50
|
+
_, _, run = whitespace_patterns(value)
|
|
51
|
+
strip(value.gsub(run, " "))
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
def nfkc_lower(value)
|
|
@@ -64,6 +73,15 @@ module Cohere
|
|
|
64
73
|
def whitespace_class
|
|
65
74
|
WHITESPACE_CLASS
|
|
66
75
|
end
|
|
76
|
+
|
|
77
|
+
def whitespace_patterns(value)
|
|
78
|
+
if value.encoding == Encoding::ASCII_8BIT
|
|
79
|
+
[BINARY_LEADING_WHITESPACE, BINARY_TRAILING_WHITESPACE, BINARY_WHITESPACE_RUN]
|
|
80
|
+
else
|
|
81
|
+
[LEADING_WHITESPACE, TRAILING_WHITESPACE, WHITESPACE_RUN]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
private_class_method :whitespace_patterns
|
|
67
85
|
end
|
|
68
86
|
private_constant :PythonText
|
|
69
87
|
end
|
|
@@ -25,11 +25,14 @@ module Cohere
|
|
|
25
25
|
ZIP_CENTRAL_LIMIT = 256 * 1024 * 1024
|
|
26
26
|
ZIP_COMPRESSED_OVERHEAD = 1024 * 1024
|
|
27
27
|
ZIP_INFLATE_CHUNK = 16 * 1024
|
|
28
|
+
STORAGE_CRC_CHUNK_BYTES = 4 * 1024 * 1024
|
|
28
29
|
ZIP_EOCD = 0x0605_4b50
|
|
29
30
|
ZIP64_EOCD = 0x0606_4b50
|
|
30
31
|
ZIP64_LOCATOR = 0x0706_4b50
|
|
31
32
|
ZIP_CENTRAL = 0x0201_4b50
|
|
32
33
|
ZIP_LOCAL = 0x0403_4b50
|
|
34
|
+
ZIP64_UINT16_MARKER = 0xffff
|
|
35
|
+
ZIP64_UINT32_MARKER = 0xffff_ffff
|
|
33
36
|
|
|
34
37
|
STORAGE_DTYPES = {
|
|
35
38
|
"FloatStorage" => ["F32", 4],
|
|
@@ -41,6 +44,7 @@ module Cohere
|
|
|
41
44
|
|
|
42
45
|
Global = Data.define(:module_name, :name)
|
|
43
46
|
StorageRef = Data.define(:dtype, :key, :elements, :base_offset)
|
|
47
|
+
StoragePayload = Data.define(:data_offset, :size, :crc32)
|
|
44
48
|
TensorSpec = Data.define(:storage, :storage_offset, :shape, :stride)
|
|
45
49
|
Entry = Data.define(
|
|
46
50
|
:name, :compression_method, :flags, :crc32, :compressed_size, :size,
|
|
@@ -176,10 +180,12 @@ module Cohere
|
|
|
176
180
|
raise Error, "Multi-disk PyTorch ZIP archives are not supported"
|
|
177
181
|
end
|
|
178
182
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
zip64_marker = disk_entries == ZIP64_UINT16_MARKER ||
|
|
184
|
+
total_entries == ZIP64_UINT16_MARKER ||
|
|
185
|
+
central_size == ZIP64_UINT32_MARKER ||
|
|
186
|
+
central_offset == ZIP64_UINT32_MARKER
|
|
187
|
+
total_entries, central_size, central_offset = zip64_directory(eocd_offset) if
|
|
188
|
+
zip64_marker && zip64_locator_present?(eocd_offset)
|
|
183
189
|
if total_entries > ZIP_ENTRY_LIMIT || central_size > ZIP_CENTRAL_LIMIT ||
|
|
184
190
|
central_size > size || central_offset > size - central_size
|
|
185
191
|
raise Error, "PyTorch archive #{path} has invalid central-directory bounds"
|
|
@@ -215,6 +221,12 @@ module Cohere
|
|
|
215
221
|
[total_entries, central_size, central_offset]
|
|
216
222
|
end
|
|
217
223
|
|
|
224
|
+
def zip64_locator_present?(eocd_offset)
|
|
225
|
+
return false if eocd_offset < 20
|
|
226
|
+
|
|
227
|
+
read_range(eocd_offset - 20, 4).unpack1("V") == ZIP64_LOCATOR
|
|
228
|
+
end
|
|
229
|
+
|
|
218
230
|
def parse_central_directory(bytes, expected_entries:, archive_size:, data_limit:)
|
|
219
231
|
offset = 0
|
|
220
232
|
result = {}
|
|
@@ -301,9 +313,8 @@ module Cohere
|
|
|
301
313
|
cursor += length
|
|
302
314
|
end
|
|
303
315
|
|
|
304
|
-
required = [size, compressed_size, local_offset
|
|
305
|
-
|
|
306
|
-
end
|
|
316
|
+
required = [size, compressed_size, local_offset].count(ZIP64_UINT32_MARKER)
|
|
317
|
+
required += 1 if disk == ZIP64_UINT16_MARKER
|
|
307
318
|
return [size, compressed_size, local_offset, disk] if required.zero?
|
|
308
319
|
raise Error, "PyTorch archive #{path} lacks required ZIP64 size data" unless values
|
|
309
320
|
|
|
@@ -322,10 +333,10 @@ module Cohere
|
|
|
322
333
|
cursor += 4
|
|
323
334
|
value
|
|
324
335
|
end
|
|
325
|
-
size = take_qword.call if size ==
|
|
326
|
-
compressed_size = take_qword.call if compressed_size ==
|
|
327
|
-
local_offset = take_qword.call if local_offset ==
|
|
328
|
-
disk = take_dword.call if disk ==
|
|
336
|
+
size = take_qword.call if size == ZIP64_UINT32_MARKER
|
|
337
|
+
compressed_size = take_qword.call if compressed_size == ZIP64_UINT32_MARKER
|
|
338
|
+
local_offset = take_qword.call if local_offset == ZIP64_UINT32_MARKER
|
|
339
|
+
disk = take_dword.call if disk == ZIP64_UINT16_MARKER
|
|
329
340
|
[size, compressed_size, local_offset, disk]
|
|
330
341
|
end
|
|
331
342
|
|
|
@@ -771,6 +782,9 @@ module Cohere
|
|
|
771
782
|
def initialize(path)
|
|
772
783
|
@path = Pathname(path).expand_path
|
|
773
784
|
@storages = {}
|
|
785
|
+
@storage_payloads = {}
|
|
786
|
+
@verified_storage_payloads = {}
|
|
787
|
+
@storage_verification_mutex = Mutex.new
|
|
774
788
|
@tensors = read_checkpoint.freeze
|
|
775
789
|
end
|
|
776
790
|
|
|
@@ -798,6 +812,8 @@ module Cohere
|
|
|
798
812
|
end
|
|
799
813
|
raise ArgumentError, "chunk_bytes must be positive" unless chunk_bytes.positive?
|
|
800
814
|
|
|
815
|
+
verify_storage_payload!(tensor)
|
|
816
|
+
|
|
801
817
|
source_width = Safetensors::DTYPE_BYTES.fetch(tensor.dtype)
|
|
802
818
|
elements_per_chunk = [chunk_bytes / source_width, 1].max
|
|
803
819
|
bytes_per_chunk = elements_per_chunk * source_width
|
|
@@ -874,6 +890,12 @@ module Cohere
|
|
|
874
890
|
raise Error, "PyTorch storage #{storage.key.inspect} has #{entry.size} bytes; expected #{expected}"
|
|
875
891
|
end
|
|
876
892
|
|
|
893
|
+
@storage_payloads[storage.key] ||= StoragePayload.new(
|
|
894
|
+
data_offset: entry.data_offset,
|
|
895
|
+
size: entry.size,
|
|
896
|
+
crc32: entry.crc32
|
|
897
|
+
)
|
|
898
|
+
|
|
877
899
|
entry.data_offset
|
|
878
900
|
end
|
|
879
901
|
end
|
|
@@ -1013,6 +1035,41 @@ module Cohere
|
|
|
1013
1035
|
true
|
|
1014
1036
|
end
|
|
1015
1037
|
|
|
1038
|
+
def verify_storage_payload!(tensor)
|
|
1039
|
+
payload = @storage_payloads[tensor.storage_key]
|
|
1040
|
+
return unless payload
|
|
1041
|
+
|
|
1042
|
+
@storage_verification_mutex.synchronize do
|
|
1043
|
+
return if @verified_storage_payloads.key?(tensor.storage_key)
|
|
1044
|
+
|
|
1045
|
+
verify_storage_crc!(tensor.storage_key, payload)
|
|
1046
|
+
@verified_storage_payloads[tensor.storage_key] = true
|
|
1047
|
+
end
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
def verify_storage_crc!(storage_key, payload)
|
|
1051
|
+
crc32 = 0
|
|
1052
|
+
remaining = payload.size
|
|
1053
|
+
File.open(path, "rb") do |source|
|
|
1054
|
+
source.seek(payload.data_offset)
|
|
1055
|
+
while remaining.positive?
|
|
1056
|
+
requested = [remaining, STORAGE_CRC_CHUNK_BYTES].min
|
|
1057
|
+
chunk = source.read(requested)
|
|
1058
|
+
if chunk.nil? || chunk.bytesize != requested
|
|
1059
|
+
raise Error, "Unexpected end of #{path} while checking storage #{storage_key.inspect}"
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
crc32 = Zlib.crc32(chunk, crc32)
|
|
1063
|
+
remaining -= requested
|
|
1064
|
+
end
|
|
1065
|
+
end
|
|
1066
|
+
return if crc32 == payload.crc32
|
|
1067
|
+
|
|
1068
|
+
raise Error, "PyTorch tensor storage #{storage_key.inspect} failed its CRC check"
|
|
1069
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
1070
|
+
raise Error, "Cannot read PyTorch checkpoint #{path}: #{e.message}"
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1016
1073
|
# General strided tensors occur in legitimate torch state dictionaries
|
|
1017
1074
|
# (notably transposed projection weights). A read-only file mapping
|
|
1018
1075
|
# keeps heap usage bounded while preserving logical row-major order.
|
|
@@ -249,12 +249,22 @@ module Cohere
|
|
|
249
249
|
nil
|
|
250
250
|
end
|
|
251
251
|
memory_byte_limit = [(resolved_options.audio_memory_gb * (1024**3)).to_i, 1].max
|
|
252
|
+
estimate_bytes = if @decoder.respond_to?(:estimate_decoded_bytes)
|
|
253
|
+
lambda do |item|
|
|
254
|
+
@decoder.estimate_decoded_bytes(
|
|
255
|
+
item.entry.path,
|
|
256
|
+
backend: resolved_options.audio_backend,
|
|
257
|
+
sample_rate: SAMPLE_RATE
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
252
261
|
pipeline = Preparation::Pipeline.new(
|
|
253
262
|
preparation_items,
|
|
254
263
|
memory_byte_limit: memory_byte_limit,
|
|
255
264
|
requested_workers: resolved_options.preprocess_workers,
|
|
256
265
|
enabled: resolved_options.pipeline_preparation,
|
|
257
|
-
worker_limit: vad_file_concurrency_limit(resolved_options)
|
|
266
|
+
worker_limit: vad_file_concurrency_limit(resolved_options),
|
|
267
|
+
estimate_bytes: estimate_bytes
|
|
258
268
|
) do |item, decoded_byte_limit, worker_slot|
|
|
259
269
|
prepare_entry(
|
|
260
270
|
item,
|
|
@@ -10,6 +10,7 @@ module Cohere
|
|
|
10
10
|
module Preparation
|
|
11
11
|
MAX_PIPELINE_GROUP_BYTES = 512 * (1024**2)
|
|
12
12
|
MAX_GROUP_JOBS = 128
|
|
13
|
+
ESTIMATE_HEADROOM = 0.05
|
|
13
14
|
|
|
14
15
|
# The result of decode and VAD preparation. Timings are accumulated by
|
|
15
16
|
# the engine on the consuming thread, keeping statistics deterministic.
|
|
@@ -29,13 +30,17 @@ module Cohere
|
|
|
29
30
|
# Bounded ordered preparation with at most one group ahead of ASR.
|
|
30
31
|
#
|
|
31
32
|
# A pipelined group receives at most half of the configured decoded-PCM
|
|
32
|
-
# budget (and never more than 512 MiB).
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
33
|
+
# budget (and never more than 512 MiB). Estimated decode sizes determine
|
|
34
|
+
# group membership and per-file ceilings. A file that cannot fit that cap
|
|
35
|
+
# is prepared alone with the full configured ceiling and without overlap.
|
|
36
|
+
# Native decoder implementation transients are outside this retained-PCM
|
|
37
|
+
# accounting, as they are in the Python path.
|
|
36
38
|
class Pipeline
|
|
37
39
|
include Enumerable
|
|
38
40
|
|
|
41
|
+
Group = Data.define(:items, :limits, :exclusive)
|
|
42
|
+
private_constant :Group
|
|
43
|
+
|
|
39
44
|
class WorkerPool
|
|
40
45
|
STOP = Object.new.freeze
|
|
41
46
|
|
|
@@ -60,10 +65,10 @@ module Cohere
|
|
|
60
65
|
end
|
|
61
66
|
end
|
|
62
67
|
|
|
63
|
-
def prepare(group
|
|
64
|
-
responses = group.each_with_index.map do |item, slot|
|
|
68
|
+
def prepare(group)
|
|
69
|
+
responses = group.items.each_with_index.map do |item, slot|
|
|
65
70
|
response = Queue.new
|
|
66
|
-
@queues.fetch(slot) << [item,
|
|
71
|
+
@queues.fetch(slot) << [item, group.limits.fetch(slot), response]
|
|
67
72
|
response
|
|
68
73
|
end
|
|
69
74
|
responses.map do |response|
|
|
@@ -88,7 +93,8 @@ module Cohere
|
|
|
88
93
|
|
|
89
94
|
attr_reader :effective_workers, :group_byte_limit, :memory_byte_limit, :wait_seconds
|
|
90
95
|
|
|
91
|
-
def initialize(items, memory_byte_limit:, requested_workers:, enabled:, worker_limit: nil,
|
|
96
|
+
def initialize(items, memory_byte_limit:, requested_workers:, enabled:, worker_limit: nil,
|
|
97
|
+
estimate_bytes: nil, &prepare)
|
|
92
98
|
raise ArgumentError, "prepare block is required" unless prepare
|
|
93
99
|
|
|
94
100
|
@items = items.to_a.freeze
|
|
@@ -96,6 +102,7 @@ module Cohere
|
|
|
96
102
|
raise ArgumentError, "memory_byte_limit must be positive" unless @memory_byte_limit.positive?
|
|
97
103
|
|
|
98
104
|
@prepare = prepare
|
|
105
|
+
@estimate_bytes = estimate_bytes
|
|
99
106
|
@wait_seconds = 0.0
|
|
100
107
|
@enabled = enabled && @items.length > 1
|
|
101
108
|
@worker_limit = worker_limit.nil? ? nil : Integer(worker_limit)
|
|
@@ -122,14 +129,16 @@ module Cohere
|
|
|
122
129
|
return
|
|
123
130
|
end
|
|
124
131
|
|
|
125
|
-
groups =
|
|
132
|
+
groups = build_groups
|
|
126
133
|
pool = WorkerPool.new(@effective_workers, &@prepare)
|
|
127
134
|
pending = submit(groups.first, 0, pool)
|
|
128
|
-
groups.each_with_index do |
|
|
135
|
+
groups.each_with_index do |group, group_index|
|
|
129
136
|
prepared = resolve(pending)
|
|
130
137
|
pending = nil
|
|
131
|
-
|
|
138
|
+
next_group = groups[group_index + 1]
|
|
139
|
+
pending = submit(next_group, group_index + 1, pool) if next_group && !group.exclusive && !next_group.exclusive
|
|
132
140
|
consume(prepared, &block)
|
|
141
|
+
pending ||= submit(next_group, group_index + 1, pool) if next_group
|
|
133
142
|
end
|
|
134
143
|
completed = true
|
|
135
144
|
ensure
|
|
@@ -179,8 +188,67 @@ module Cohere
|
|
|
179
188
|
end
|
|
180
189
|
|
|
181
190
|
def prepare_group(group, pool)
|
|
182
|
-
|
|
183
|
-
|
|
191
|
+
pool.prepare(group)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def build_groups
|
|
195
|
+
return equal_groups unless @estimate_bytes
|
|
196
|
+
|
|
197
|
+
groups = []
|
|
198
|
+
items = []
|
|
199
|
+
estimates = []
|
|
200
|
+
@items.each do |item|
|
|
201
|
+
estimate = estimated_reservation(item)
|
|
202
|
+
if estimate.nil? || estimate > @group_byte_limit
|
|
203
|
+
groups << bounded_group(items, estimates) unless items.empty?
|
|
204
|
+
groups << Group.new(items: [item].freeze, limits: [@memory_byte_limit].freeze, exclusive: true)
|
|
205
|
+
items = []
|
|
206
|
+
estimates = []
|
|
207
|
+
next
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
if items.length >= @effective_workers || estimates.sum + estimate > @group_byte_limit
|
|
211
|
+
groups << bounded_group(items, estimates)
|
|
212
|
+
items = []
|
|
213
|
+
estimates = []
|
|
214
|
+
end
|
|
215
|
+
items << item
|
|
216
|
+
estimates << estimate
|
|
217
|
+
end
|
|
218
|
+
groups << bounded_group(items, estimates) unless items.empty?
|
|
219
|
+
groups
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def equal_groups
|
|
223
|
+
@items.each_slice(@effective_workers).map do |items|
|
|
224
|
+
limit = [@group_byte_limit / items.length, 1].max
|
|
225
|
+
Group.new(items: items.freeze, limits: Array.new(items.length, limit).freeze, exclusive: false)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def estimated_reservation(item)
|
|
230
|
+
value = @estimate_bytes.call(item)
|
|
231
|
+
return if value.nil?
|
|
232
|
+
|
|
233
|
+
bytes = Integer(value)
|
|
234
|
+
return unless bytes.positive?
|
|
235
|
+
|
|
236
|
+
[(bytes * (1.0 + ESTIMATE_HEADROOM)).ceil, 1].max
|
|
237
|
+
rescue ArgumentError, TypeError, SystemCallError
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def bounded_group(items, estimates)
|
|
242
|
+
limits = estimates.dup
|
|
243
|
+
remaining = @group_byte_limit - limits.sum
|
|
244
|
+
if remaining.positive?
|
|
245
|
+
weight = limits.sum
|
|
246
|
+
extras = limits.map { |limit| (remaining * limit).div(weight) }
|
|
247
|
+
leftover = remaining - extras.sum
|
|
248
|
+
extras[limits.each_index.max_by { |index| limits.fetch(index) }] += leftover
|
|
249
|
+
limits = limits.each_index.map { |index| limits.fetch(index) + extras.fetch(index) }
|
|
250
|
+
end
|
|
251
|
+
Group.new(items: items.freeze, limits: limits.freeze, exclusive: false)
|
|
184
252
|
end
|
|
185
253
|
|
|
186
254
|
def consume(prepared, &block)
|