gigatoken 0.1.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 +7 -0
- data/Cargo.lock +3016 -0
- data/Cargo.toml +135 -0
- data/LICENSE +21 -0
- data/README.md +141 -0
- data/exe/gigatoken +9 -0
- data/ext/gigatoken/Cargo.toml +24 -0
- data/ext/gigatoken/extconf.rb +19 -0
- data/ext/gigatoken/src/error.rs +20 -0
- data/ext/gigatoken/src/gvl.rs +122 -0
- data/ext/gigatoken/src/lib.rs +50 -0
- data/ext/gigatoken/src/sentencepiece.rs +205 -0
- data/ext/gigatoken/src/sources.rs +207 -0
- data/ext/gigatoken/src/tokenizer.rs +571 -0
- data/lib/gigatoken/cli/bench.rb +64 -0
- data/lib/gigatoken/cli/support.rb +132 -0
- data/lib/gigatoken/cli/validate.rb +55 -0
- data/lib/gigatoken/cli.rb +18 -0
- data/lib/gigatoken/hub.rb +242 -0
- data/lib/gigatoken/packed_result.rb +48 -0
- data/lib/gigatoken/tokenizer.rb +117 -0
- data/lib/gigatoken/version.rb +5 -0
- data/lib/gigatoken.rb +29 -0
- data/rust-toolchain.toml +8 -0
- data/src/batch.rs +1808 -0
- data/src/bindings/bridge.rs +396 -0
- data/src/bindings/hub.rs +42 -0
- data/src/bindings/matcher.rs +114 -0
- data/src/bindings/mod.rs +14 -0
- data/src/bindings/padding.rs +177 -0
- data/src/bindings/pretokenize.rs +53 -0
- data/src/bindings/sources.rs +273 -0
- data/src/bindings/train.rs +125 -0
- data/src/bpe/mod.rs +1217 -0
- data/src/bpe/pretoken_cache.rs +495 -0
- data/src/bpe/sentencepiece.rs +1485 -0
- data/src/bpe/tiktoken.rs +2555 -0
- data/src/bpe_train.rs +351 -0
- data/src/input/decompress.rs +11 -0
- data/src/input/file_source.rs +514 -0
- data/src/input/jsonl.rs +94 -0
- data/src/input/mod.rs +333 -0
- data/src/input/parquet.rs +303 -0
- data/src/lib.rs +578 -0
- data/src/load_tokenizer/hf.rs +1036 -0
- data/src/load_tokenizer/hub.rs +344 -0
- data/src/load_tokenizer/mod.rs +3 -0
- data/src/load_tokenizer/tiktoken.rs +87 -0
- data/src/main.rs +95 -0
- data/src/pretokenize/fast/cl100k.rs +426 -0
- data/src/pretokenize/fast/cl100k_family.rs +891 -0
- data/src/pretokenize/fast/deepseek_v3.rs +605 -0
- data/src/pretokenize/fast/kimi.rs +281 -0
- data/src/pretokenize/fast/mask.rs +1486 -0
- data/src/pretokenize/fast/mod.rs +446 -0
- data/src/pretokenize/fast/nemotron.rs +138 -0
- data/src/pretokenize/fast/o200k.rs +347 -0
- data/src/pretokenize/fast/o200k_family.rs +1734 -0
- data/src/pretokenize/fast/olmo3.rs +505 -0
- data/src/pretokenize/fast/qwen2.rs +429 -0
- data/src/pretokenize/fast/qwen3_5.rs +541 -0
- data/src/pretokenize/fast/r50k.rs +1250 -0
- data/src/pretokenize/mod.rs +1079 -0
- data/src/pretokenize/options.rs +188 -0
- data/src/pretokenize/pretoken.rs +20 -0
- data/src/pretokenize/pretokenize_traits.rs +49 -0
- data/src/pretokenize/reference/avx512.rs +522 -0
- data/src/pretokenize/reference/combinator.rs +572 -0
- data/src/pretokenize/reference/mod.rs +28 -0
- data/src/pretokenize/reference/simd.rs +852 -0
- data/src/pretokenize/reference/state_machine.rs +365 -0
- data/src/pretokenize/unicode.rs +546 -0
- data/src/test_hub.rs +28 -0
- data/src/token.rs +42 -0
- metadata +161 -0
data/src/bpe_train.rs
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
use dashmap::DashMap;
|
|
2
|
+
use indicatif::ProgressBar;
|
|
3
|
+
use itertools::Itertools;
|
|
4
|
+
use priority_queue::PriorityQueue;
|
|
5
|
+
use rayon::prelude::*;
|
|
6
|
+
use rustc_hash::FxBuildHasher;
|
|
7
|
+
use std::collections::{BTreeSet, HashMap};
|
|
8
|
+
|
|
9
|
+
use std::hash::Hash;
|
|
10
|
+
|
|
11
|
+
#[derive(Clone)]
|
|
12
|
+
pub struct Word {
|
|
13
|
+
pub symbols: Vec<u32>,
|
|
14
|
+
pub word_count: isize,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Pair = (u32, u32);
|
|
18
|
+
|
|
19
|
+
fn count_pairs(words: &[Word]) -> HashMap<Pair, isize> {
|
|
20
|
+
let mut symbol_counts: HashMap<Pair, isize> = HashMap::new();
|
|
21
|
+
for word in words.iter() {
|
|
22
|
+
for i in 0..word.symbols.len() - 1 {
|
|
23
|
+
let pair = (word.symbols[i], word.symbols[i + 1]);
|
|
24
|
+
let count = symbol_counts.entry(pair).or_insert(0);
|
|
25
|
+
*count += word.word_count;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
symbol_counts
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn update_word(
|
|
32
|
+
w: &mut Word,
|
|
33
|
+
pair: Pair,
|
|
34
|
+
new_symbol: u32,
|
|
35
|
+
mut record_changes: impl FnMut((u32, u32), isize),
|
|
36
|
+
) {
|
|
37
|
+
let mut i = 0;
|
|
38
|
+
while i < w.symbols.len() - 1 {
|
|
39
|
+
if w.symbols[i] == pair.0 && w.symbols[i + 1] == pair.1 {
|
|
40
|
+
// Perform the merge
|
|
41
|
+
if i >= 1 {
|
|
42
|
+
record_changes((w.symbols[i - 1], pair.0), -w.word_count);
|
|
43
|
+
record_changes((w.symbols[i - 1], new_symbol), w.word_count);
|
|
44
|
+
}
|
|
45
|
+
if w.symbols.len() >= 3 && i <= w.symbols.len() - 3 {
|
|
46
|
+
record_changes((pair.1, w.symbols[i + 2]), -w.word_count);
|
|
47
|
+
record_changes((new_symbol, w.symbols[i + 2]), w.word_count);
|
|
48
|
+
}
|
|
49
|
+
w.symbols[i] = new_symbol;
|
|
50
|
+
w.symbols.remove(i + 1);
|
|
51
|
+
}
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Unsafe hack to parallelize as efficiently as possible.
|
|
57
|
+
/// The borrow checker doesn't allow several mutable references to the same underlying array, so
|
|
58
|
+
/// we need to do unsafe dereferencing.
|
|
59
|
+
/// This is only safe if you there is _only one_ mutable reference to the underlying value.
|
|
60
|
+
#[derive(Clone)]
|
|
61
|
+
struct SendPtr(*mut Word);
|
|
62
|
+
|
|
63
|
+
unsafe impl Sync for SendPtr {}
|
|
64
|
+
unsafe impl Send for SendPtr {}
|
|
65
|
+
|
|
66
|
+
/// Update words by merging the given pair into a new symbol.
|
|
67
|
+
/// Update the contained_in_words map to _add_ associations between the newly created pairs and the words they are contained in (we don't remove old ones, though they will be stale).
|
|
68
|
+
/// Return a map of pair -> change in count (can be negative) to update the priority queue.
|
|
69
|
+
fn update_words(
|
|
70
|
+
words: &mut [Word],
|
|
71
|
+
contained_in_words: &mut HashMap<(u32, u32), BTreeSet<u32>>,
|
|
72
|
+
pair: Pair,
|
|
73
|
+
new_symbol: u32,
|
|
74
|
+
) -> DashMap<(u32, u32), isize, FxBuildHasher> {
|
|
75
|
+
let count_changes: DashMap<(u32, u32), isize, FxBuildHasher> = DashMap::default();
|
|
76
|
+
|
|
77
|
+
let n_threads = rayon::current_num_threads();
|
|
78
|
+
|
|
79
|
+
// Iterate through all words containing first or second
|
|
80
|
+
let word_idcs = &contained_in_words[&(pair.0, pair.1)];
|
|
81
|
+
let words_ptr = SendPtr(words.as_mut_ptr());
|
|
82
|
+
|
|
83
|
+
// TODO(perf): There is a lot of contention on this map early in merging, since the updated pairs overlap a lot in the beginning.
|
|
84
|
+
// Pair -> Word, pair was added to the word, make sure to update contained_in_words
|
|
85
|
+
let contained_updates: DashMap<(u32, u32), BTreeSet<u32>, FxBuildHasher> = DashMap::default();
|
|
86
|
+
|
|
87
|
+
if word_idcs.len() > 2 * n_threads {
|
|
88
|
+
word_idcs
|
|
89
|
+
.iter()
|
|
90
|
+
.copied()
|
|
91
|
+
.collect::<Vec<_>>()
|
|
92
|
+
.par_chunks(word_idcs.len().div_ceil(n_threads))
|
|
93
|
+
.for_each(|idcs_chunk| {
|
|
94
|
+
for &i in idcs_chunk {
|
|
95
|
+
// Smuggle in a mutable reference to the word
|
|
96
|
+
let local_words_ptr = words_ptr.clone();
|
|
97
|
+
// SAFETY: Only this thread has access to this word, since word_idcs is a set of unique indices.
|
|
98
|
+
let word = unsafe { &mut *local_words_ptr.0.add(i as usize) };
|
|
99
|
+
let count_changes = |pair, change| {
|
|
100
|
+
if change > 0 {
|
|
101
|
+
// Was added to the word, need to track this immediately, since other threads might subtract
|
|
102
|
+
contained_updates.entry(pair).or_default().insert(i);
|
|
103
|
+
}
|
|
104
|
+
*count_changes.entry(pair).or_default() += change;
|
|
105
|
+
};
|
|
106
|
+
update_word(word, pair, new_symbol, count_changes);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
// Single-threaded for small updates
|
|
111
|
+
word_idcs.iter().copied().for_each(|i| {
|
|
112
|
+
// Smuggle in a mutable reference to the word
|
|
113
|
+
let local_words_ptr = words_ptr.clone();
|
|
114
|
+
// SAFETY: Only this thread has access to this word, since word_idcs is a set of unique indices.
|
|
115
|
+
let word = unsafe { &mut *local_words_ptr.0.add(i as usize) };
|
|
116
|
+
let count_changes = |pair, change| {
|
|
117
|
+
if change > 0 {
|
|
118
|
+
// Was added to the word, need to track this
|
|
119
|
+
contained_updates.entry(pair).or_default().insert(i);
|
|
120
|
+
}
|
|
121
|
+
*count_changes.entry(pair).or_default() += change;
|
|
122
|
+
};
|
|
123
|
+
update_word(word, pair, new_symbol, count_changes);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (pair, mut word_idcs) in contained_updates.into_iter() {
|
|
128
|
+
let set = contained_in_words.entry(pair).or_default();
|
|
129
|
+
set.append(&mut word_idcs);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
count_changes
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pub fn assemble_token(token: u32, symbols: &[Vec<u8>]) -> String {
|
|
136
|
+
symbols[token as usize]
|
|
137
|
+
.iter()
|
|
138
|
+
.map(|x| *x as char)
|
|
139
|
+
.collect::<String>()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pub struct BPEResult {
|
|
143
|
+
pub vocab: HashMap<u32, Vec<u8>>,
|
|
144
|
+
pub merges: Vec<(Vec<u8>, Vec<u8>)>,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// How to break ties when multiple pairs have the same frequency count.
|
|
148
|
+
#[derive(Clone, Copy, Debug, Default)]
|
|
149
|
+
pub enum TieBreaking {
|
|
150
|
+
/// Compare by token IDs remapped to match HuggingFace tokenizers' BpeTrainer
|
|
151
|
+
/// initial vocabulary ordering (ByteLevel unicode codepoint order for bytes 0-255).
|
|
152
|
+
#[default]
|
|
153
|
+
HuggingFace,
|
|
154
|
+
/// Compare by raw (u32, u32) token IDs (byte value = token ID).
|
|
155
|
+
RawTokenIds,
|
|
156
|
+
/// Assemble each token's bytes into a string and compare lexicographically.
|
|
157
|
+
AssembledBytes,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Build a mapping from byte value (0-255) to the rank it would receive in
|
|
161
|
+
/// HuggingFace's BpeTrainer initial vocabulary. HF sorts the ByteLevel alphabet
|
|
162
|
+
/// by unicode codepoint: printable ASCII/Latin-1 bytes keep their codepoint,
|
|
163
|
+
/// while the remaining 68 bytes are remapped to U+0100..U+0143.
|
|
164
|
+
fn build_byte_to_hf_rank() -> [u32; 256] {
|
|
165
|
+
let mut byte_to_cp = [0u32; 256];
|
|
166
|
+
let mut n = 0u32;
|
|
167
|
+
for b in 0..=255u8 {
|
|
168
|
+
let is_allowed = matches!(b, 33..=126 | 161..=172 | 174..=255);
|
|
169
|
+
if is_allowed {
|
|
170
|
+
byte_to_cp[b as usize] = b as u32;
|
|
171
|
+
} else {
|
|
172
|
+
byte_to_cp[b as usize] = 256 + n;
|
|
173
|
+
n += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Sort bytes by their unicode codepoint, then record position as rank
|
|
178
|
+
let mut bytes_sorted: Vec<u8> = (0..=255).collect();
|
|
179
|
+
bytes_sorted.sort_by_key(|&b| byte_to_cp[b as usize]);
|
|
180
|
+
|
|
181
|
+
let mut rank = [0u32; 256];
|
|
182
|
+
for (i, &b) in bytes_sorted.iter().enumerate() {
|
|
183
|
+
rank[b as usize] = i as u32;
|
|
184
|
+
}
|
|
185
|
+
rank
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
pub fn train_bpe<K: AsRef<[u8]> + Eq + Hash>(
|
|
189
|
+
counts: HashMap<K, usize, FxBuildHasher>,
|
|
190
|
+
vocab_size: usize,
|
|
191
|
+
special_tokens: Vec<String>,
|
|
192
|
+
tie_breaking: TieBreaking,
|
|
193
|
+
) -> BPEResult {
|
|
194
|
+
// Indicates which word indices contain a given symbol
|
|
195
|
+
let mut contained_in_words: HashMap<(u32, u32), BTreeSet<u32>> = HashMap::new();
|
|
196
|
+
let mut contained_in_words_arr = vec![vec![vec![]; 256]; 256];
|
|
197
|
+
let mut words: Vec<Word> = counts
|
|
198
|
+
.into_iter()
|
|
199
|
+
.enumerate()
|
|
200
|
+
.map(|(word_i, (word, count))| {
|
|
201
|
+
// At first we have only bytes, so we won't need to hash the u32 pairs
|
|
202
|
+
let word_symbols: Vec<u32> = word.as_ref().iter().map(|&b| b as u32).collect();
|
|
203
|
+
for c in word_symbols.iter().copied().tuple_windows::<(u32, u32)>() {
|
|
204
|
+
contained_in_words_arr[c.0 as usize][c.1 as usize].push(word_i as u32);
|
|
205
|
+
}
|
|
206
|
+
Word {
|
|
207
|
+
symbols: word_symbols,
|
|
208
|
+
word_count: count as isize,
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
.collect();
|
|
212
|
+
|
|
213
|
+
for (i, j) in (0..256).cartesian_product(0..256) {
|
|
214
|
+
if !contained_in_words_arr[i][j].is_empty() {
|
|
215
|
+
contained_in_words.insert(
|
|
216
|
+
(i as u32, j as u32),
|
|
217
|
+
BTreeSet::from_iter(contained_in_words_arr[i][j].iter().copied()),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
drop(contained_in_words_arr);
|
|
222
|
+
|
|
223
|
+
println!("{} unique words", words.len());
|
|
224
|
+
let max_symbols = vocab_size;
|
|
225
|
+
|
|
226
|
+
let symbol_counts = count_pairs(&words);
|
|
227
|
+
|
|
228
|
+
// Symbols 0 through 255 are unicode characters
|
|
229
|
+
let mut symbols: Vec<Vec<u8>> = (0..=255).map(|x| vec![x]).collect();
|
|
230
|
+
symbols.extend(
|
|
231
|
+
special_tokens
|
|
232
|
+
.into_iter()
|
|
233
|
+
.map(|x| x.bytes().collect::<Vec<u8>>()),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Build HF rank table for tie-breaking (only used in HuggingFace mode)
|
|
237
|
+
let hf_rank = build_byte_to_hf_rank();
|
|
238
|
+
|
|
239
|
+
let mut pq = PriorityQueue::new();
|
|
240
|
+
symbol_counts.into_iter().for_each(|(pair, count)| {
|
|
241
|
+
pq.push(pair, count);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
let mut merges = vec![];
|
|
245
|
+
|
|
246
|
+
println!("Starting merges");
|
|
247
|
+
let bar = ProgressBar::new(max_symbols as u64).with_style(
|
|
248
|
+
indicatif::ProgressStyle::default_bar()
|
|
249
|
+
.template("[{elapsed_precise}] [{bar}] {pos}/{len} ({eta})")
|
|
250
|
+
.unwrap(),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
while !pq.is_empty() && symbols.len() < max_symbols {
|
|
254
|
+
bar.set_position(symbols.len() as u64);
|
|
255
|
+
let pair = {
|
|
256
|
+
let (first_pair, first_count) = pq.pop().unwrap();
|
|
257
|
+
let mut tied_pairs = vec![first_pair];
|
|
258
|
+
while let Some((_next_pair, &next_count)) = pq.peek() {
|
|
259
|
+
if next_count != first_count {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
tied_pairs.push(pq.pop().unwrap().0);
|
|
263
|
+
}
|
|
264
|
+
// Find the smallest pair according to the chosen tie-breaking rule
|
|
265
|
+
let mut smallest_pair = first_pair;
|
|
266
|
+
match tie_breaking {
|
|
267
|
+
TieBreaking::HuggingFace => {
|
|
268
|
+
// Remap initial byte token IDs to HF's ByteLevel unicode
|
|
269
|
+
// codepoint ordering before comparison.
|
|
270
|
+
let remap = |id: u32| -> u32 {
|
|
271
|
+
if id < 256 {
|
|
272
|
+
hf_rank[id as usize]
|
|
273
|
+
} else {
|
|
274
|
+
id
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
for &pair in &tied_pairs {
|
|
278
|
+
let remapped = (remap(pair.0), remap(pair.1));
|
|
279
|
+
let remapped_smallest =
|
|
280
|
+
(remap(smallest_pair.0), remap(smallest_pair.1));
|
|
281
|
+
if remapped < remapped_smallest {
|
|
282
|
+
smallest_pair = pair;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
TieBreaking::RawTokenIds => {
|
|
287
|
+
for &pair in &tied_pairs {
|
|
288
|
+
if pair < smallest_pair {
|
|
289
|
+
smallest_pair = pair;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
TieBreaking::AssembledBytes => {
|
|
294
|
+
let assemble_pair = |(p0, p1)| {
|
|
295
|
+
(assemble_token(p0, &symbols), assemble_token(p1, &symbols))
|
|
296
|
+
};
|
|
297
|
+
for pair in tied_pairs.iter().copied() {
|
|
298
|
+
if assemble_pair(pair) < assemble_pair(smallest_pair) {
|
|
299
|
+
smallest_pair = pair;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for pair in tied_pairs {
|
|
306
|
+
if pair != smallest_pair {
|
|
307
|
+
pq.push(pair, first_count);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
smallest_pair
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Merge the pair
|
|
315
|
+
let new_symbol: Vec<u8> = [&symbols[pair.0 as usize], &symbols[pair.1 as usize]]
|
|
316
|
+
.into_iter()
|
|
317
|
+
.flatten()
|
|
318
|
+
.copied()
|
|
319
|
+
.collect();
|
|
320
|
+
|
|
321
|
+
merges.push((
|
|
322
|
+
symbols[pair.0 as usize].clone(),
|
|
323
|
+
symbols[pair.1 as usize].clone(),
|
|
324
|
+
));
|
|
325
|
+
|
|
326
|
+
symbols.push(new_symbol);
|
|
327
|
+
|
|
328
|
+
let count_changes = update_words(
|
|
329
|
+
&mut words,
|
|
330
|
+
&mut contained_in_words,
|
|
331
|
+
pair,
|
|
332
|
+
symbols.len() as u32 - 1,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
for (pair, change) in count_changes.into_iter() {
|
|
336
|
+
let found_item = pq.change_priority_by(&pair, |p| *p += change);
|
|
337
|
+
if !found_item {
|
|
338
|
+
pq.push(pair, change);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
bar.finish();
|
|
343
|
+
|
|
344
|
+
let vocab: HashMap<_, _> = symbols
|
|
345
|
+
.into_iter()
|
|
346
|
+
.enumerate()
|
|
347
|
+
.map(|(i, v)| (i as u32, v))
|
|
348
|
+
.collect();
|
|
349
|
+
|
|
350
|
+
BPEResult { vocab, merges }
|
|
351
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
use std::fs::File;
|
|
2
|
+
use std::io::{self, BufReader};
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
pub(crate) fn open_gzip(path: &Path) -> io::Result<BufReader<flate2::read::GzDecoder<File>>> {
|
|
6
|
+
Ok(BufReader::new(flate2::read::GzDecoder::new(File::open(path)?)))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
pub(crate) fn open_zstd(path: &Path) -> io::Result<BufReader<zstd::Decoder<'static, BufReader<File>>>> {
|
|
10
|
+
Ok(BufReader::new(zstd::Decoder::new(File::open(path)?)?))
|
|
11
|
+
}
|