gigatoken 0.3.0 → 0.5.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 +13 -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 +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/PROVENANCE.md +113 -1
- data/lib/gigatoken/encodings/muse_spark.json.gz +0 -0
- data/lib/gigatoken/encodings/qwen35.json.gz +0 -0
- data/lib/gigatoken/encodings/qwen38.json.gz +0 -0
- data/lib/gigatoken/encodings.rb +30 -10
- data/lib/gigatoken/hub.rb +232 -37
- data/lib/gigatoken/packed_result.rb +6 -2
- data/lib/gigatoken/tokenizer.rb +84 -15
- 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 +28 -1
data/lib/gigatoken/cli/bench.rb
CHANGED
|
@@ -20,30 +20,36 @@ module Gigatoken
|
|
|
20
20
|
option :pretokenizer, desc: "pretokenizer scheme, required when TOKENIZER is a .tiktoken file (one of #{Native.pretokenizer_names.join(", ")}); ignored otherwise"
|
|
21
21
|
|
|
22
22
|
def call(tokenizer:, files:, doc_separator: nil, limit_bytes: "none", parallel: true, packed: false, pretokenizer: nil, **)
|
|
23
|
+
Support.check_usage!(files, doc_separator)
|
|
23
24
|
limit = Support.parse_size(limit_bytes)
|
|
24
25
|
out.puts "#{label("cpu")}: #{Support.cpu_info}"
|
|
25
26
|
|
|
26
27
|
gt_tokenizer = Support.load_tokenizer(tokenizer, pretokenizer: pretokenizer)
|
|
27
28
|
|
|
29
|
+
# Only the batch path materializes the documents; the native paths
|
|
30
|
+
# leave this nil and count their bytes off the clock below.
|
|
31
|
+
docs = nil
|
|
28
32
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
29
33
|
if packed
|
|
30
34
|
encoded = gt_tokenizer.encode_files(Support.text_file_source(files, doc_separator), parallel: parallel, packed: true)
|
|
31
|
-
n_bytes = files.sum { |file| File.size(file) }
|
|
32
35
|
n_tokens = encoded.token_count
|
|
33
36
|
elsif parallel
|
|
34
37
|
docs = Support.subset_docs(Support.split_docs(files, doc_separator), limit)
|
|
35
38
|
encoded = gt_tokenizer.encode_batch(docs)
|
|
36
|
-
n_bytes = docs.sum(&:bytesize)
|
|
37
39
|
n_tokens = encoded.sum(&:length)
|
|
38
40
|
else
|
|
39
41
|
encoded = gt_tokenizer.encode_files(Support.text_file_source(files, doc_separator), parallel: false)
|
|
40
|
-
n_bytes = files.sum { |file| File.size(file) }
|
|
41
42
|
n_tokens = encoded.sum(&:length)
|
|
42
43
|
end
|
|
43
44
|
seconds = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
44
45
|
|
|
46
|
+
# The throughput is over the bytes encoded, which for a compressed
|
|
47
|
+
# file are its decompressed bytes. Counting those means reading the
|
|
48
|
+
# file, so the native paths count off the clock; the batch path
|
|
49
|
+
# already holds the documents it encoded.
|
|
50
|
+
n_bytes = docs ? docs.sum(&:bytesize) : Support.input_bytesize(files)
|
|
45
51
|
out.puts report("gigatoken", seconds, n_bytes, n_tokens)
|
|
46
|
-
rescue Gigatoken::Error => e
|
|
52
|
+
rescue Gigatoken::Error, SystemCallError => e
|
|
47
53
|
err.puts "error: #{e.message}"
|
|
48
54
|
exit(1)
|
|
49
55
|
end
|
|
@@ -4,16 +4,29 @@ require "etc"
|
|
|
4
4
|
|
|
5
5
|
module Gigatoken
|
|
6
6
|
module CLI
|
|
7
|
-
# Helpers shared by the bench and validate commands:
|
|
8
|
-
# byte-size parsing, document splitting, and CPU
|
|
7
|
+
# Helpers shared by the bench and validate commands: argument checking,
|
|
8
|
+
# tokenizer loading, byte-size parsing, document splitting, and CPU
|
|
9
|
+
# identification.
|
|
9
10
|
module Support
|
|
10
11
|
SIZE_UNITS = {"" => 1, "k" => 10**3, "m" => 10**6, "g" => 10**9, "t" => 10**12}.freeze
|
|
11
12
|
private_constant :SIZE_UNITS
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
# "KiB" and friends are binary, as everywhere else that spells the i.
|
|
15
|
+
BINARY_SIZE_UNITS = {"" => 1, "k" => 2**10, "m" => 2**20, "g" => 2**30, "t" => 2**40}.freeze
|
|
16
|
+
private_constant :BINARY_SIZE_UNITS
|
|
17
|
+
|
|
18
|
+
SIZE_PATTERN = /\A\s*(\d+(?:\.\d+)?)\s*([kmgt]?)(i?)b?\s*\z/i
|
|
14
19
|
private_constant :SIZE_PATTERN
|
|
15
20
|
|
|
16
21
|
class << self
|
|
22
|
+
# The argument shapes dry-cli itself lets through: FILES declared
|
|
23
|
+
# `required: true` still arrives empty, and an empty separator would
|
|
24
|
+
# split every byte into its own document.
|
|
25
|
+
def check_usage!(files, separator)
|
|
26
|
+
raise Gigatoken::Error, "FILES is required: name at least one file to encode" if files.empty?
|
|
27
|
+
raise Gigatoken::Error, "--doc-separator cannot be empty" if separator == ""
|
|
28
|
+
end
|
|
29
|
+
|
|
17
30
|
# Load TOKENIZER: a tokenizer.json path/directory, a packaged
|
|
18
31
|
# tiktoken encoding name, a HuggingFace repo id, or a .tiktoken file
|
|
19
32
|
# — see Gigatoken::Tokenizer.load. `pretokenizer:` is forwarded
|
|
@@ -23,15 +36,17 @@ module Gigatoken
|
|
|
23
36
|
Gigatoken::Tokenizer.load(spec, pretokenizer: pretokenizer)
|
|
24
37
|
end
|
|
25
38
|
|
|
26
|
-
# Parse a
|
|
27
|
-
#
|
|
39
|
+
# Parse a byte size like "100MB", "2.5GB", "64KiB" or "1000000" —
|
|
40
|
+
# decimal units unless the prefix spells the i, which makes it
|
|
41
|
+
# binary; "none"/"unlimited" means no limit.
|
|
28
42
|
def parse_size(text)
|
|
29
43
|
return nil if ["none", "unlimited"].include?(text.strip.downcase)
|
|
30
44
|
|
|
31
45
|
match = SIZE_PATTERN.match(text)
|
|
32
46
|
raise Gigatoken::Error, "cannot parse size #{text.inspect}; expected something like 100MB (or 'none')" unless match
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
units = match[3].empty? ? SIZE_UNITS : BINARY_SIZE_UNITS
|
|
49
|
+
(match[1].to_f * units.fetch(match[2].downcase)).to_i
|
|
35
50
|
end
|
|
36
51
|
|
|
37
52
|
# A Native::TextFileSource for FILES, split on `separator` when
|
|
@@ -42,15 +57,23 @@ module Gigatoken
|
|
|
42
57
|
|
|
43
58
|
# Whole files as raw bytes, one document per file, or (with a
|
|
44
59
|
# separator) the separator-split pieces of each file in order, empty
|
|
45
|
-
# documents skipped.
|
|
60
|
+
# documents skipped. Compressed files are decompressed first, as the
|
|
61
|
+
# native file sources do, so both paths split the same bytes.
|
|
46
62
|
def split_docs(files, separator)
|
|
47
|
-
raws = files.map { |file|
|
|
63
|
+
raws = files.map { |file| read_decompressed(file.to_s) }
|
|
48
64
|
return raws if separator.nil?
|
|
49
65
|
|
|
50
66
|
sep = separator.b
|
|
51
67
|
raws.flat_map { |raw| raw.split(sep).reject(&:empty?) }
|
|
52
68
|
end
|
|
53
69
|
|
|
70
|
+
# The bytes the tokenizer actually sees for FILES — a compressed
|
|
71
|
+
# file's decompressed size, not its size on disk — so throughput is
|
|
72
|
+
# reported over the input that was tokenized.
|
|
73
|
+
def input_bytesize(files)
|
|
74
|
+
files.sum { |file| read_decompressed(file.to_s).bytesize }
|
|
75
|
+
end
|
|
76
|
+
|
|
54
77
|
# The prefix of `docs` totalling at most `limit_bytes`, byte-
|
|
55
78
|
# truncating the final document to fill the budget. Unlike a
|
|
56
79
|
# text-comparison tool, gigatoken encodes raw bytes and does not
|
|
@@ -90,6 +113,15 @@ module Gigatoken
|
|
|
90
113
|
|
|
91
114
|
private
|
|
92
115
|
|
|
116
|
+
# One file's bytes, decompressed. `Native.read_input` is the core's
|
|
117
|
+
# own decoder — the one the native file sources load through — so
|
|
118
|
+
# `.gz`, `.zst`/`.zstd` and plain files are detected and read here
|
|
119
|
+
# exactly as they are on the other side of `validate`, and an
|
|
120
|
+
# unreadable file raises Gigatoken::Error naming it.
|
|
121
|
+
def read_decompressed(path)
|
|
122
|
+
Gigatoken::Native.read_input(path)
|
|
123
|
+
end
|
|
124
|
+
|
|
93
125
|
def darwin_cpu_info
|
|
94
126
|
name = sysctl("machdep.cpu.brand_string")
|
|
95
127
|
[name, sysctl_int("hw.physicalcpu"), sysctl_int("hw.packages")]
|
|
@@ -20,6 +20,7 @@ module Gigatoken
|
|
|
20
20
|
option :pretokenizer, desc: "pretokenizer scheme, required when TOKENIZER is a .tiktoken file (one of #{Native.pretokenizer_names.join(", ")}); ignored otherwise"
|
|
21
21
|
|
|
22
22
|
def call(tokenizer:, files:, doc_separator: nil, pretokenizer: nil, **)
|
|
23
|
+
Support.check_usage!(files, doc_separator)
|
|
23
24
|
gt_tokenizer = Support.load_tokenizer(tokenizer, pretokenizer: pretokenizer)
|
|
24
25
|
|
|
25
26
|
via_files = gt_tokenizer.encode_files(Support.text_file_source(files, doc_separator))
|
|
@@ -38,7 +39,7 @@ module Gigatoken
|
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
out.puts "validation OK: #{via_files.length} documents match"
|
|
41
|
-
rescue Gigatoken::Error => e
|
|
42
|
+
rescue Gigatoken::Error, SystemCallError => e
|
|
42
43
|
err.puts "error: #{e.message}"
|
|
43
44
|
exit(1)
|
|
44
45
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Vendored
|
|
1
|
+
# Vendored encodings — provenance
|
|
2
2
|
|
|
3
3
|
The `.tiktoken` files in this directory are OpenAI's published BPE mergeable-rank
|
|
4
4
|
tables, vendored verbatim so `gigatoken` can resolve these encodings by name with
|
|
@@ -9,6 +9,10 @@ carries **mergeable ranks only** — the pretokenizer split regex and the specia
|
|
|
9
9
|
tokens belong to the encoding's *definition*, not to the file, and live in code
|
|
10
10
|
(see the table below).
|
|
11
11
|
|
|
12
|
+
The three `.json.gz` files are HuggingFace `tokenizer.json` files, not tiktoken
|
|
13
|
+
tables; they are covered in [their own section](#huggingface-tokenizerjson-files)
|
|
14
|
+
at the end.
|
|
15
|
+
|
|
12
16
|
They live under `lib/` rather than a top-level `data/` directory because this
|
|
13
17
|
repo's `.gitignore` ignores `/data/` ("downloaded test data"); these are shipped
|
|
14
18
|
gem payload, not test fixtures.
|
|
@@ -108,3 +112,111 @@ The `tiktoken` project and its published encoding files are MIT licensed,
|
|
|
108
112
|
Copyright (c) 2022 OpenAI, Shantanu Jain. See
|
|
109
113
|
<https://github.com/openai/tiktoken/blob/main/LICENSE>. The files are vendored
|
|
110
114
|
here unmodified.
|
|
115
|
+
|
|
116
|
+
## HuggingFace `tokenizer.json` files
|
|
117
|
+
|
|
118
|
+
`qwen35.json.gz`, `qwen38.json.gz` and `muse_spark.json.gz` are HuggingFace
|
|
119
|
+
`tokenizer.json` files, vendored byte-for-byte and gzipped. Unlike a
|
|
120
|
+
`.tiktoken` table, a `tokenizer.json` carries the encoding's whole definition
|
|
121
|
+
— vocabulary, merges, normalizer, pretokenizer, added tokens — so the registry
|
|
122
|
+
records only the file, and `Tokenizer.from_encoding` loads it with
|
|
123
|
+
`Tokenizer.from_json`, reading the special tokens from the file's
|
|
124
|
+
`added_tokens` (those marked `"special": true`) exactly as `from_json` does.
|
|
125
|
+
|
|
126
|
+
### Files
|
|
127
|
+
|
|
128
|
+
| File | Decompressed bytes | sha256 (decompressed) |
|
|
129
|
+
|------|-------------------:|-----------------------|
|
|
130
|
+
| `qwen35.json.gz` | 12,807,982 | `5f9e4d4901a92b997e463c1f46055088b6cca5ca61a6522d1b9f64c4bb81cb42` |
|
|
131
|
+
| `qwen38.json.gz` | 12,809,320 | `0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3` |
|
|
132
|
+
| `muse_spark.json.gz` | 28,129,897 | `c9dbee66967b58f31a7c27f723c3760da3526ccd0427578e8905b0abb0031c4d` |
|
|
133
|
+
|
|
134
|
+
### Source
|
|
135
|
+
|
|
136
|
+
Retrieved **2026-10-07** over HTTPS from the Hub's `resolve` endpoint, each at
|
|
137
|
+
a pinned commit:
|
|
138
|
+
|
|
139
|
+
| Encoding | Repo | Revision | URL |
|
|
140
|
+
|---|---|---|---|
|
|
141
|
+
| `qwen35` | `Qwen/Qwen3.5-27B` | `fc05daec18b0a78c049392ed2e771dde82bdf654` | <https://huggingface.co/Qwen/Qwen3.5-27B/resolve/fc05daec18b0a78c049392ed2e771dde82bdf654/tokenizer.json> |
|
|
142
|
+
| `qwen38` | `Qwen/Qwen3.8-27B` | `1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0` | <https://huggingface.co/Qwen/Qwen3.8-27B/resolve/1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0/tokenizer.json> |
|
|
143
|
+
| `muse_spark` | `meta-models/Muse-Glimmer-30B` | `a4e59da52a7bc87ae7251dd5545c0dd437c44b68` | <https://huggingface.co/meta-models/Muse-Glimmer-30B/resolve/a4e59da52a7bc87ae7251dd5545c0dd437c44b68/tokenizer.json> |
|
|
144
|
+
|
|
145
|
+
**`qwen35` is shared across releases.** The Hub's tree API
|
|
146
|
+
(`https://huggingface.co/api/models/<repo>?blobs=true`) reports the identical
|
|
147
|
+
LFS sha256 `5f9e4d49…` for `tokenizer.json` in `Qwen/Qwen3.5-{0.8B, 2B, 4B, 9B,
|
|
148
|
+
27B, 35B-A3B, 122B-A10B, 397B-A17B}` and `Qwen/Qwen3.6-{27B, 35B-A3B}`
|
|
149
|
+
(queried 2026-10-07), so one file serves Qwen 3.5 and 3.6.
|
|
150
|
+
`Qwen/Qwen3.6-Plus` is gated and was not checked.
|
|
151
|
+
|
|
152
|
+
**`qwen38` ships its own file**, identical (`0997f410…`) across
|
|
153
|
+
`Qwen/Qwen3.8-{27B, Flash-Next, 2.4T-A95B}` and their `-FP8` twins. It differs
|
|
154
|
+
from `qwen35` only in seven more `"special": true` added tokens, ids
|
|
155
|
+
248070–248076: `<|audio_start|>`, `<|audio_end|>`, `<tts_pad>`,
|
|
156
|
+
`<tts_text_bos>`, `<tts_text_eod>`, `<tts_text_bos_single>`, `<|audio_pad|>`.
|
|
157
|
+
The vocabulary, merges, normalizer, pretokenizer, decoder and post-processor
|
|
158
|
+
are identical, so ordinary text encodes the same under both; only those seven
|
|
159
|
+
literals, `vocab_size` and `special_tokens` differ. It is vendored as a second
|
|
160
|
+
file rather than derived from the first because the engine reads added tokens
|
|
161
|
+
from the JSON, and a JSON-backed tokenizer has no way to add specials after
|
|
162
|
+
the fact. `Qwen3.8-27B` is the repo cited; `Flash-Next` and `2.4T-A95B` carry an
|
|
163
|
+
"other" licence on the *model*, but their tokenizer file is byte-identical to
|
|
164
|
+
the Apache-2.0 repo's.
|
|
165
|
+
|
|
166
|
+
**`muse_spark` is the Muse-Glimmer-30B file.** Meta publishes no standalone
|
|
167
|
+
Muse Spark tokenizer (a Hub search over `meta-models` finds none, and Meta's
|
|
168
|
+
model-API docs at <https://dev.meta.ai/docs/muse-glimmer> point at none). The
|
|
169
|
+
Muse-Glimmer-30B model card states "Tokenizer: 200,000 BPE tokens + 2,048
|
|
170
|
+
special tokens" (vocabulary 202,048), trained from Muse Spark's outputs, and
|
|
171
|
+
this exact repo and revision is the one the reference application pins. As of
|
|
172
|
+
2026-10-07 that revision is also `main`.
|
|
173
|
+
|
|
174
|
+
### Authenticity
|
|
175
|
+
|
|
176
|
+
Verified three independent ways at retrieval time:
|
|
177
|
+
|
|
178
|
+
1. **Transport** — HTTPS directly from `huggingface.co`, at the pinned
|
|
179
|
+
commit above.
|
|
180
|
+
2. **Publisher checksum** — each file's sha256 above matches the LFS sha256 the
|
|
181
|
+
Hub's tree API reports for `tokenizer.json` at that revision, and so does
|
|
182
|
+
its size. Two independent channels agree on the bytes.
|
|
183
|
+
3. **Behavioral, against the reference implementation** — loaded through
|
|
184
|
+
`Tokenizer.from_encoding`, every token id matched HuggingFace
|
|
185
|
+
[`tokenizers`](https://rubygems.org/gems/tokenizers) 0.7.0 loading the same
|
|
186
|
+
decompressed file with `add_special_tokens: false`, across this repo's own
|
|
187
|
+
source and docs (`spec/gigatoken/differential_spec.rb`). Resulting
|
|
188
|
+
`vocab_size`: 248070 / 248077 / 202048; `special_tokens.size`: 14 / 21 / 2048.
|
|
189
|
+
|
|
190
|
+
### Compression
|
|
191
|
+
|
|
192
|
+
Each file was compressed with `gzip -9 -n` (no name or timestamp in the gzip
|
|
193
|
+
header, so the bytes are reproducible), and `gunzip -c` of it reproduces the
|
|
194
|
+
published file byte-for-byte — the sha256s above are of the decompressed
|
|
195
|
+
bytes. Compressed sizes: 3,514,616 (`qwen35`), 3,514,699 (`qwen38`), 4,475,666
|
|
196
|
+
(`muse_spark`).
|
|
197
|
+
|
|
198
|
+
### What gigatoken does not take from the file
|
|
199
|
+
|
|
200
|
+
- **The post-processor.** gigatoken applies none. `muse_spark`'s is a
|
|
201
|
+
`TemplateProcessing` that prepends `<|begin_of_text|>`; `Tokenizer#encode`
|
|
202
|
+
does not, matching `tokenizers`' `add_special_tokens: false`. The Qwen files'
|
|
203
|
+
is a `ByteLevel` post-processor, a no-op on ids.
|
|
204
|
+
- **Non-special added tokens in `special_tokens`.** Qwen's 12 non-special
|
|
205
|
+
added tokens (`<tool_call>`, `<think>`, the `<|fim_*|>` tokens,
|
|
206
|
+
`<|repo_name|>`, `<|file_sep|>`, `<tool_response>` and so on) are matched
|
|
207
|
+
atomically by the engine, like the special ones, but only `"special": true`
|
|
208
|
+
tokens appear in `Tokenizer#special_tokens` — 14 for `qwen35`, 21 for
|
|
209
|
+
`qwen38`.
|
|
210
|
+
- **`tokenizer_config.json`.** Not vendored; gigatoken reads only
|
|
211
|
+
`tokenizer.json`.
|
|
212
|
+
|
|
213
|
+
### Licence
|
|
214
|
+
|
|
215
|
+
All three repos are Apache-2.0, each with its own `LICENSE` at the pinned
|
|
216
|
+
revision:
|
|
217
|
+
|
|
218
|
+
- <https://huggingface.co/Qwen/Qwen3.5-27B/blob/fc05daec18b0a78c049392ed2e771dde82bdf654/LICENSE>
|
|
219
|
+
- <https://huggingface.co/Qwen/Qwen3.8-27B/blob/1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0/LICENSE>
|
|
220
|
+
- <https://huggingface.co/meta-models/Muse-Glimmer-30B/blob/a4e59da52a7bc87ae7251dd5545c0dd437c44b68/LICENSE>
|
|
221
|
+
|
|
222
|
+
The files are vendored here unmodified (gzipped).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/gigatoken/encodings.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Gigatoken
|
|
4
|
-
# The
|
|
5
|
-
# .tiktoken file doesn't carry: its pretokenizer scheme and
|
|
6
|
-
# table (see Tokenizer.from_tiktoken and lib/gigatoken/
|
|
7
|
-
# PROVENANCE.md, the source of truth this is transcribed from).
|
|
4
|
+
# The encodings gigatoken vendors a file for. For the tiktoken ones, the
|
|
5
|
+
# pieces a .tiktoken file doesn't carry: its pretokenizer scheme and
|
|
6
|
+
# special-token table (see Tokenizer.from_tiktoken and lib/gigatoken/
|
|
7
|
+
# encodings/PROVENANCE.md, the source of truth this is transcribed from).
|
|
8
|
+
# The HuggingFace ones are whole tokenizer.json files, which carry theirs.
|
|
8
9
|
module Encodings
|
|
9
10
|
DATA_DIR = File.expand_path("encodings", __dir__)
|
|
10
11
|
private_constant :DATA_DIR
|
|
@@ -40,6 +41,10 @@ module Gigatoken
|
|
|
40
41
|
HARMONY_RESERVED_TAIL = (200013..201087).to_h { |id| ["<|reserved_#{id}|>", id] }.freeze
|
|
41
42
|
private_constant :HARMONY_RESERVED_TAIL
|
|
42
43
|
|
|
44
|
+
# Deep-frozen: entries, their `special_tokens` tables and the `rank_file` /
|
|
45
|
+
# `json_file` paths. `Tokenizer#special_tokens` hands the registry's own
|
|
46
|
+
# Hash back to callers, so anything less lets one caller's poke rewrite
|
|
47
|
+
# what every later `from_encoding` in the process loads.
|
|
43
48
|
REGISTRY = {
|
|
44
49
|
"r50k_base" => {
|
|
45
50
|
rank_file: File.join(DATA_DIR, "r50k_base.tiktoken"),
|
|
@@ -66,8 +71,22 @@ module Gigatoken
|
|
|
66
71
|
rank_file: File.join(DATA_DIR, "o200k_base.tiktoken"),
|
|
67
72
|
pretokenizer: "o200k",
|
|
68
73
|
special_tokens: HARMONY_HEAD_TOKENS.merge(HARMONY_RESERVED_TAIL).freeze
|
|
69
|
-
}
|
|
70
|
-
|
|
74
|
+
},
|
|
75
|
+
# The three below are gzipped HuggingFace tokenizer.json files, vendored
|
|
76
|
+
# verbatim (see PROVENANCE.md for repos, revisions and hashes). Nothing
|
|
77
|
+
# else is registered: the pretokenizer and the special tokens are the
|
|
78
|
+
# file's own, read at load as Tokenizer.from_json reads them.
|
|
79
|
+
#
|
|
80
|
+
# Qwen 3.5 and 3.6 share this one file.
|
|
81
|
+
"qwen35" => {json_file: File.join(DATA_DIR, "qwen35.json.gz")},
|
|
82
|
+
# The qwen35 file plus seven audio/TTS special tokens — vendored
|
|
83
|
+
# separately because a JSON-backed tokenizer takes its added tokens
|
|
84
|
+
# from the file.
|
|
85
|
+
"qwen38" => {json_file: File.join(DATA_DIR, "qwen38.json.gz")},
|
|
86
|
+
# meta-models/Muse-Glimmer-30B's file: Meta publishes no standalone
|
|
87
|
+
# Muse Spark tokenizer.
|
|
88
|
+
"muse_spark" => {json_file: File.join(DATA_DIR, "muse_spark.json.gz")}
|
|
89
|
+
}.each_value { |encoding| encoding.each_value(&:freeze).freeze }.freeze
|
|
71
90
|
private_constant :REGISTRY
|
|
72
91
|
|
|
73
92
|
# The packaged encoding names — the single source error messages naming
|
|
@@ -87,16 +106,17 @@ module Gigatoken
|
|
|
87
106
|
private_constant :UNPACKABLE_REASONS
|
|
88
107
|
|
|
89
108
|
class << self
|
|
90
|
-
# The {rank_file:, pretokenizer:, special_tokens:}
|
|
91
|
-
# packaged encoding name, or
|
|
109
|
+
# The {rank_file:, pretokenizer:, special_tokens:} (tiktoken) or
|
|
110
|
+
# {json_file:} (HuggingFace) registered for a packaged encoding name, or
|
|
111
|
+
# nil. Names are Strings or Symbols, as Tokenizer.load accepts both.
|
|
92
112
|
def [](name)
|
|
93
|
-
REGISTRY[name]
|
|
113
|
+
REGISTRY[name.to_s]
|
|
94
114
|
end
|
|
95
115
|
|
|
96
116
|
# Why `name` can't be packaged, or nil when there's no reason on
|
|
97
117
|
# record (it's either packaged, or simply not one gigatoken knows of).
|
|
98
118
|
def unpackable_reason(name)
|
|
99
|
-
UNPACKABLE_REASONS[name]
|
|
119
|
+
UNPACKABLE_REASONS[name.to_s]
|
|
100
120
|
end
|
|
101
121
|
end
|
|
102
122
|
end
|