gigatoken 0.2.2 → 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 +22 -5
- 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 +170 -36
- data/ext/gigatoken/src/lib.rs +18 -3
- data/ext/gigatoken/src/sentencepiece.rs +27 -15
- data/ext/gigatoken/src/tokenizer.rs +225 -131
- 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 +240 -38
- data/lib/gigatoken/packed_result.rb +12 -4
- data/lib/gigatoken/tokenizer.rb +85 -15
- data/lib/gigatoken/version.rb +1 -1
- data/lib/gigatoken.rb +17 -1
- data/rust-toolchain.toml +1 -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,13 +61,13 @@ 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`
|
|
51
68
|
# (downloaded directly; huggingface_hub is not required).
|
|
52
|
-
def self.from_hub(repo_id, revision: "main", hub:
|
|
53
|
-
from_file(hub.hub_file(repo_id, "tokenizer.json", revision: revision))
|
|
69
|
+
def self.from_hub(repo_id, revision: "main", hub: nil)
|
|
70
|
+
from_file((hub || Hub.new).hub_file(repo_id, "tokenizer.json", revision: revision))
|
|
54
71
|
end
|
|
55
72
|
|
|
56
73
|
# Load from any of the supported source shapes: an existing file or
|
|
@@ -67,11 +84,11 @@ module Gigatoken
|
|
|
67
84
|
# here too, raising the same explanation from_encoding gives rather than
|
|
68
85
|
# reaching the Hub — but only those; an unrecognized bare name like
|
|
69
86
|
# "gpt2" still dispatches to the Hub.
|
|
70
|
-
def self.load(source, pretokenizer: nil, special_tokens: {}, revision: "main", hub:
|
|
87
|
+
def self.load(source, pretokenizer: nil, special_tokens: {}, revision: "main", hub: nil)
|
|
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,21 +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"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# `data` is UTF-8 JSON whatever its encoding tag says (File.binread tags
|
|
104
|
+
# binary; a US-ASCII default_external tags that), so retag rather than let
|
|
105
|
+
# JSON.parse transcode from the tag; a UTF-8-tagged String needs no copy.
|
|
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)
|
|
110
|
+
data = data.dup.force_encoding(Encoding::UTF_8) unless data.encoding == Encoding::UTF_8
|
|
111
|
+
JSON.parse(data)
|
|
112
|
+
rescue JSON::ParserError => e
|
|
113
|
+
raise ModelError, "failed to parse tokenizer JSON: #{e.message}"
|
|
84
114
|
end
|
|
115
|
+
private_class_method :parse_json
|
|
85
116
|
|
|
86
|
-
def self.
|
|
87
|
-
added =
|
|
117
|
+
def self.special_tokens_from(parsed)
|
|
118
|
+
added = parsed["added_tokens"] || []
|
|
88
119
|
added.each_with_object({}) { |t, h| h[t["content"]] = t["id"] if t["special"] }
|
|
89
120
|
end
|
|
90
|
-
private_class_method :
|
|
121
|
+
private_class_method :special_tokens_from
|
|
91
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.
|
|
92
126
|
def initialize(native, special_tokens: {})
|
|
93
127
|
@native = native
|
|
94
|
-
@special_tokens = special_tokens
|
|
128
|
+
@special_tokens = special_tokens.frozen? ? special_tokens : special_tokens.dup.freeze
|
|
95
129
|
end
|
|
96
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.
|
|
97
136
|
def encode(text)
|
|
137
|
+
text = utf8(text) unless text.is_a?(String) && text.encoding == Encoding::UTF_8
|
|
98
138
|
@native.encode(text)
|
|
99
139
|
end
|
|
100
140
|
|
|
@@ -103,6 +143,7 @@ module Gigatoken
|
|
|
103
143
|
# token ids plus per-document lengths), avoiding the per-token Ruby
|
|
104
144
|
# array materialization the ragged shape costs.
|
|
105
145
|
def encode_batch(texts, packed: false)
|
|
146
|
+
texts = utf8_batch(texts)
|
|
106
147
|
if packed
|
|
107
148
|
PackedResult.new(*@native.encode_batch_packed(texts))
|
|
108
149
|
else
|
|
@@ -150,5 +191,34 @@ module Gigatoken
|
|
|
150
191
|
end
|
|
151
192
|
|
|
152
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
|
|
153
223
|
end
|
|
154
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
|
data/rust-toolchain.toml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[toolchain]
|
|
2
|
-
channel = "nightly"
|
|
2
|
+
channel = "nightly-2026-09-18"
|
|
3
3
|
# Cross triples for the precompiled-gem builds (ruby-gem.yml). rustup's
|
|
4
4
|
# toolchain-file auto-install is the only thing that provisions nightly
|
|
5
5
|
# inside the rb-sys-dock containers, so the targets must be declared here —
|