gigatoken 0.3.0 → 0.4.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/Cargo.lock +1 -1
- data/Cargo.toml +7 -3
- data/README.md +9 -3
- data/benches/common/mod.rs +105 -0
- data/benches/encode.rs +79 -0
- data/benches/encode_doc.rs +60 -0
- data/benches/encode_st.rs +105 -0
- data/benches/encode_st_sp.rs +51 -0
- data/benches/pretokenize.rs +151 -0
- data/benches/pretokenize_profile.rs +36 -0
- data/benches/pretokenize_profile_all.rs +52 -0
- data/benches/simdutf_transcode.rs +82 -0
- data/benches/unicode.rs +88 -0
- data/ext/gigatoken/Cargo.toml +1 -1
- data/ext/gigatoken/src/error.rs +29 -9
- data/ext/gigatoken/src/gvl.rs +169 -35
- data/ext/gigatoken/src/lib.rs +18 -3
- data/ext/gigatoken/src/sentencepiece.rs +27 -15
- data/ext/gigatoken/src/tokenizer.rs +223 -129
- data/lib/gigatoken/cli/bench.rb +10 -4
- data/lib/gigatoken/cli/support.rb +40 -8
- data/lib/gigatoken/cli/validate.rb +2 -1
- data/lib/gigatoken/encodings.rb +9 -4
- data/lib/gigatoken/hub.rb +232 -37
- data/lib/gigatoken/packed_result.rb +6 -2
- data/lib/gigatoken/tokenizer.rb +78 -12
- data/lib/gigatoken/version.rb +1 -1
- data/lib/gigatoken.rb +17 -1
- data/src/batch.rs +294 -56
- data/src/bpe/tiktoken.rs +30 -5
- metadata +25 -1
data/lib/gigatoken/tokenizer.rb
CHANGED
|
@@ -10,18 +10,35 @@ module Gigatoken
|
|
|
10
10
|
FILE_SOURCE_CLASSES = [Native::TextFileSource, Native::JsonlFileSource, Native::ParquetFileSource].freeze
|
|
11
11
|
private_constant :FILE_SOURCE_CLASSES
|
|
12
12
|
|
|
13
|
+
# Encodings whose bytes reach the native call untouched. UTF-8 is checked
|
|
14
|
+
# by identity first — it is the hot path, see #encode — and these are the
|
|
15
|
+
# rest: US-ASCII already is UTF-8 bytes, and ASCII-8BIT is deliberately
|
|
16
|
+
# raw bytes. Anything else — ISO-8859-1, Windows-1252, UTF-16LE — is a
|
|
17
|
+
# real encoding whose bytes are not the text's UTF-8 bytes, so encoding it
|
|
18
|
+
# raw would give different ids than the same text read as UTF-8. (Invalid
|
|
19
|
+
# bytes in a UTF-8-tagged String stay raw: a documented difference from
|
|
20
|
+
# tiktoken, which rejects them.)
|
|
21
|
+
BYTEWISE_ENCODINGS = [Encoding::US_ASCII, Encoding::BINARY].freeze
|
|
22
|
+
private_constant :BYTEWISE_ENCODINGS
|
|
23
|
+
|
|
13
24
|
# Load from in-memory tokenizer.json contents (String or bytes). Backed
|
|
14
25
|
# by a BPETokenizer or a SentencePieceTokenizer, per the model's
|
|
15
|
-
# byte_fallback flag.
|
|
26
|
+
# byte_fallback flag. The document is parsed twice, deliberately: the Ruby
|
|
27
|
+
# parse runs first because JSON.parse's depth limit is what keeps hostile
|
|
28
|
+
# nesting away from the native parser (see parse_json), and it is also
|
|
29
|
+
# where the special-token table is read from.
|
|
16
30
|
def self.from_json(data)
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
json = String.try_convert(data) or raise TypeError, "no implicit conversion of #{data.class} into String"
|
|
32
|
+
parsed = parse_json(json)
|
|
33
|
+
new(Native.load_hf_json(json), special_tokens: special_tokens_from(parsed))
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
# Load from a tokenizer.json path, or a directory containing one.
|
|
22
37
|
def self.from_file(path)
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
file = File.directory?(path) ? File.join(path, "tokenizer.json") : path.to_s
|
|
39
|
+
raise ModelError, "#{file.inspect}: no such file" unless File.file?(file)
|
|
40
|
+
|
|
41
|
+
from_json(File.binread(file))
|
|
25
42
|
end
|
|
26
43
|
|
|
27
44
|
# Load from a .tiktoken mergeable-ranks file. The file carries neither a
|
|
@@ -44,7 +61,7 @@ module Gigatoken
|
|
|
44
61
|
|
|
45
62
|
reason = Encodings.unpackable_reason(name)
|
|
46
63
|
detail = reason ? " — #{reason}" : ""
|
|
47
|
-
raise
|
|
64
|
+
raise ModelError, "#{name.inspect}: not a packaged encoding#{detail} (packaged encodings: #{Encodings::NAMES.join(", ")})"
|
|
48
65
|
end
|
|
49
66
|
|
|
50
67
|
# Load tokenizer.json from HuggingFace Hub repo `repo_id` at `revision`
|
|
@@ -71,7 +88,7 @@ module Gigatoken
|
|
|
71
88
|
source = source.to_s
|
|
72
89
|
if source.end_with?(".tiktoken")
|
|
73
90
|
unless pretokenizer
|
|
74
|
-
raise
|
|
91
|
+
raise ModelError, "#{source.inspect}: a .tiktoken file carries no pretokenizer scheme of its own — " \
|
|
75
92
|
"pass pretokenizer: (one of #{Native.pretokenizer_names.join(", ")})"
|
|
76
93
|
end
|
|
77
94
|
return from_tiktoken(source, pretokenizer: pretokenizer, special_tokens: special_tokens)
|
|
@@ -80,25 +97,44 @@ module Gigatoken
|
|
|
80
97
|
return from_encoding(source) if Encodings::NAMES.include?(source) || Encodings.unpackable_reason(source)
|
|
81
98
|
return from_hub(source, revision: revision, hub: hub) if Hub.looks_like_repo_id?(source)
|
|
82
99
|
|
|
83
|
-
raise
|
|
100
|
+
raise ModelError, "#{source.inspect}: no such file or directory, not a .tiktoken path, and doesn't look like a HuggingFace Hub repo id"
|
|
84
101
|
end
|
|
85
102
|
|
|
86
103
|
# `data` is UTF-8 JSON whatever its encoding tag says (File.binread tags
|
|
87
104
|
# binary; a US-ASCII default_external tags that), so retag rather than let
|
|
88
105
|
# JSON.parse transcode from the tag; a UTF-8-tagged String needs no copy.
|
|
89
|
-
|
|
106
|
+
# JSON.parse's default max_nesting of 100 is the guard: the native parser
|
|
107
|
+
# is recursive with no depth limit, and deep enough nesting overflows its
|
|
108
|
+
# stack rather than raising.
|
|
109
|
+
def self.parse_json(data)
|
|
90
110
|
data = data.dup.force_encoding(Encoding::UTF_8) unless data.encoding == Encoding::UTF_8
|
|
91
|
-
|
|
111
|
+
JSON.parse(data)
|
|
112
|
+
rescue JSON::ParserError => e
|
|
113
|
+
raise ModelError, "failed to parse tokenizer JSON: #{e.message}"
|
|
114
|
+
end
|
|
115
|
+
private_class_method :parse_json
|
|
116
|
+
|
|
117
|
+
def self.special_tokens_from(parsed)
|
|
118
|
+
added = parsed["added_tokens"] || []
|
|
92
119
|
added.each_with_object({}) { |t, h| h[t["content"]] = t["id"] if t["special"] }
|
|
93
120
|
end
|
|
94
|
-
private_class_method :
|
|
121
|
+
private_class_method :special_tokens_from
|
|
95
122
|
|
|
123
|
+
# The table is frozen so `#special_tokens` can neither alias the
|
|
124
|
+
# deep-frozen registry's Hash nor a `from_tiktoken` caller's; an
|
|
125
|
+
# already-frozen one is safe to share as-is.
|
|
96
126
|
def initialize(native, special_tokens: {})
|
|
97
127
|
@native = native
|
|
98
|
-
@special_tokens = special_tokens
|
|
128
|
+
@special_tokens = special_tokens.frozen? ? special_tokens : special_tokens.dup.freeze
|
|
99
129
|
end
|
|
100
130
|
|
|
131
|
+
# A String already tagged UTF-8 — every ordinary document — reaches the
|
|
132
|
+
# native call over one inline identity check. A helper frame here is
|
|
133
|
+
# measurable against the encode itself at document sizes this small, so
|
|
134
|
+
# the check is spelled out rather than delegated to #transcode?; anything
|
|
135
|
+
# else takes the slow path in #utf8.
|
|
101
136
|
def encode(text)
|
|
137
|
+
text = utf8(text) unless text.is_a?(String) && text.encoding == Encoding::UTF_8
|
|
102
138
|
@native.encode(text)
|
|
103
139
|
end
|
|
104
140
|
|
|
@@ -107,6 +143,7 @@ module Gigatoken
|
|
|
107
143
|
# token ids plus per-document lengths), avoiding the per-token Ruby
|
|
108
144
|
# array materialization the ragged shape costs.
|
|
109
145
|
def encode_batch(texts, packed: false)
|
|
146
|
+
texts = utf8_batch(texts)
|
|
110
147
|
if packed
|
|
111
148
|
PackedResult.new(*@native.encode_batch_packed(texts))
|
|
112
149
|
else
|
|
@@ -154,5 +191,34 @@ module Gigatoken
|
|
|
154
191
|
end
|
|
155
192
|
|
|
156
193
|
attr_reader :special_tokens
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
# UTF-8 first and by identity, so the batch walk below costs the same one
|
|
198
|
+
# comparison per document that #encode costs; the list is only consulted
|
|
199
|
+
# for the encodings that aren't it.
|
|
200
|
+
def transcode?(text)
|
|
201
|
+
text.is_a?(String) && text.encoding != Encoding::UTF_8 && !BYTEWISE_ENCODINGS.include?(text.encoding)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# A transcode that can't be done — a dummy encoding like UTF-7, bytes the
|
|
205
|
+
# tag doesn't allow, a character the target can't hold — is a fact about
|
|
206
|
+
# the document, not about the tokenizer, and reaches the caller as one.
|
|
207
|
+
def utf8(text)
|
|
208
|
+
transcode?(text) ? text.encode(Encoding::UTF_8) : text
|
|
209
|
+
rescue EncodingError => e
|
|
210
|
+
raise InputError, e.message
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The same rule per document, without copying the Array when — as in
|
|
214
|
+
# every UTF-8 batch — there is nothing to transcode. Elements are only
|
|
215
|
+
# type-checked, never coerced: a `to_str` object is left for the native
|
|
216
|
+
# side to convert under its own snapshot of the input.
|
|
217
|
+
def utf8_batch(texts)
|
|
218
|
+
array = Array.try_convert(texts)
|
|
219
|
+
return texts if array.nil? || array.none? { |text| transcode?(text) }
|
|
220
|
+
|
|
221
|
+
array.map { |text| utf8(text) }
|
|
222
|
+
end
|
|
157
223
|
end
|
|
158
224
|
end
|
data/lib/gigatoken/version.rb
CHANGED
data/lib/gigatoken.rb
CHANGED
|
@@ -4,9 +4,25 @@ require_relative "gigatoken/version"
|
|
|
4
4
|
|
|
5
5
|
module Gigatoken
|
|
6
6
|
# Raised for tokenizer load and encode failures surfaced from the native
|
|
7
|
-
# extension — never a raw Rust panic across the Ruby boundary.
|
|
7
|
+
# extension — never a raw Rust panic across the Ruby boundary. The base of
|
|
8
|
+
# the three below: rescue this to catch everything gigatoken raises.
|
|
9
|
+
# Anything narrower than the three has no class of its own — CLI usage
|
|
10
|
+
# errors and the odd leftover are this one directly.
|
|
8
11
|
class Error < StandardError; end
|
|
9
12
|
|
|
13
|
+
# Everything Gigatoken::Hub raises: HTTP status, transport, timeout, repo-id
|
|
14
|
+
# / revision / filename / x-repo-commit validation.
|
|
15
|
+
class HubError < Error; end
|
|
16
|
+
|
|
17
|
+
# A document the tokenizer cannot take: an untranscodable or invalid-byte
|
|
18
|
+
# String, invalid UTF-8 on the SentencePiece path, an id outside the
|
|
19
|
+
# vocabulary in #decode.
|
|
20
|
+
class InputError < Error; end
|
|
21
|
+
|
|
22
|
+
# A tokenizer that cannot be loaded: bad or hostile JSON, a missing file or
|
|
23
|
+
# directory, an unknown or unpackable encoding name, a malformed .tiktoken.
|
|
24
|
+
class ModelError < Error; end
|
|
25
|
+
|
|
10
26
|
class << self
|
|
11
27
|
# The process-global encode-cache budget in bytes per worker (a parallel
|
|
12
28
|
# batch encode may use up to workers x budget), applied to tokenizers of
|