ArushBuilds commited on
Commit
dc2cbf1
·
1 Parent(s): a4d49a5

Update tokenizer.py

Browse files
Files changed (1) hide show
  1. tokenizer.py +44 -67
tokenizer.py CHANGED
@@ -5,8 +5,11 @@ import logging
5
  from pathlib import Path
6
  from typing import Sequence, List, Optional
7
 
 
8
  import sentencepiece as spm
9
 
 
 
10
  logger = logging.getLogger(__name__)
11
 
12
  # 32K is the well-established baseline vocab size for BPE/SentencePiece
@@ -199,6 +202,7 @@ class TokenizerWrapper:
199
  model_path = str(Path(model_path))
200
  if not Path(model_path).exists():
201
  raise FileNotFoundError(model_path)
 
202
  self.sp = spm.SentencePieceProcessor(model_file=model_path)
203
  self.vocab_size = int(self.sp.vocab_size())
204
  self.pad_id = self.sp.pad_id()
@@ -228,53 +232,52 @@ class TokenizerWrapper:
228
  text: str,
229
  add_bos: bool = True,
230
  add_eos: bool = False,
231
- chunk_chars: int = 2_000_000,
232
  ) -> List[int]:
233
- """OOM-safe encode for large (multi-MB+) strings, e.g. a whole
234
- training corpus file read in one go. `encode()` above calls
235
- self.sp.encode() on the ENTIRE string in one call -- even though
236
- SentencePiece's C++ core is fast (not a slow Python BPE loop),
237
- the RETURN VALUE is still built as one giant Python list[int],
238
- and Python's own string handling means a large UTF-8 file can
239
- already be 2-4x its byte size once decoded into a `str` object.
240
- For a real, confirmed example: a 300MB single-file corpus OOM'd
241
- a ~12-13GB Colab/Kaggle box within ~30 seconds calling encode()
242
- on the whole file at once -- the failure was in Python-level
243
- memory (str + list[int] materialization), not inside
244
- SentencePiece's own encoding step.
245
-
246
- This method processes `text` in fixed-size character chunks,
247
- converting each chunk's ids to the accumulator and discarding
248
- the chunk's own Python list before moving to the next chunk, so
249
- peak memory is bounded by `chunk_chars` instead of len(text).
250
- Chunk boundaries are placed at whitespace (never mid-word/
251
- mid-token), so the output is IDENTICAL to what encode(text,
252
- add_bos, add_eos) would produce on the whole string at once --
253
- this is not an approximation, it's the same encoding, just
254
- computed incrementally. Verified by direct comparison across
255
- many chunk sizes and edge cases (empty string, no-whitespace
256
- text, exact chunk-boundary alignment) against the whole-string
257
- encode() path.
258
-
259
- Use this instead of encode() for anything that might be large
260
- (a whole file's contents) -- keep encode() for genuinely small
261
- strings (a single sentence/prompt at inference time) where the
262
- chunking overhead isn't worth paying.
263
- """
264
  if text is None:
265
  raise ValueError('encode_large_text() received None')
 
 
 
 
 
 
 
 
 
 
 
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  if text == '':
268
  ids: List[int] = []
269
  if add_bos:
270
  ids = [self.bos_id] + ids
271
  if add_eos:
272
  ids = ids + [self.eos_id]
273
- return ids
 
274
 
275
- all_ids: List[int] = []
276
  pos = 0
277
  n = len(text)
 
278
  while pos < n:
279
  end = min(pos + chunk_chars, n)
280
  is_eof = (end >= n)
@@ -283,52 +286,26 @@ class TokenizerWrapper:
283
  piece = window
284
  pos = end
285
  else:
286
- # Cut at the LAST whitespace boundary inside this
287
- # window, so no token is split across two encode()
288
- # calls (which would silently tokenize the seam
289
- # differently than encoding the whole string at once).
290
  cut = max(window.rfind(' '), window.rfind('\n'))
291
  if cut <= 0:
292
- # cut == -1: no whitespace anywhere in this window
293
- # (pathological -- one token/URL longer than
294
- # chunk_chars). cut == 0: the window's FIRST
295
- # character is whitespace, which would make
296
- # `piece = window[:0]` empty and, worse, silently
297
- # drop that whitespace character (it's before the
298
- # cut, so it's never included in this piece OR the
299
- # next one, since pos would advance past it without
300
- # encoding it). Both cases: take the whole window
301
- # as one piece and advance past it -- correctness
302
- # at this one seam matters less than a silently
303
- # dropped character or a stalled loop, and this
304
- # only ever triggers when chunk_chars is smaller
305
- # than a typical word (never true at the real
306
- # ~2MB default; only reachable with a deliberately
307
- # tiny chunk_chars in tests).
308
  piece = window
309
  pos = end
310
  else:
311
  piece = window[:cut]
312
  pos += cut
313
  if not piece:
314
- # Should be unreachable now that cut<=0 is folded into
315
- # the "take whole window" branch above, but keep this
316
- # as a hard backstop against any future edge case that
317
- # produces an empty piece with is_eof=False -- without
318
- # it, pos would never advance and the loop would spin
319
- # forever.
320
  if not is_eof:
321
  pos += 1
322
  continue
 
323
  piece_ids = self.sp.encode(piece, out_type=int)
324
- all_ids.extend(piece_ids)
325
- del piece_ids
 
 
326
 
327
- if add_bos:
328
- all_ids = [self.bos_id] + all_ids
329
- if add_eos:
330
- all_ids = all_ids + [self.eos_id]
331
- return all_ids
332
 
333
  def encode_batch(
334
  self,
 
5
  from pathlib import Path
6
  from typing import Sequence, List, Optional
7
 
8
+ import numpy as np
9
  import sentencepiece as spm
10
 
11
+ DEFAULT_CHUNK_CHARS = 2_000_000
12
+
13
  logger = logging.getLogger(__name__)
14
 
15
  # 32K is the well-established baseline vocab size for BPE/SentencePiece
 
202
  model_path = str(Path(model_path))
203
  if not Path(model_path).exists():
204
  raise FileNotFoundError(model_path)
205
+ self.model_path = model_path
206
  self.sp = spm.SentencePieceProcessor(model_file=model_path)
207
  self.vocab_size = int(self.sp.vocab_size())
208
  self.pad_id = self.sp.pad_id()
 
232
  text: str,
233
  add_bos: bool = True,
234
  add_eos: bool = False,
235
+ chunk_chars: int = DEFAULT_CHUNK_CHARS,
236
  ) -> List[int]:
237
+ """Backward-compatible wrapper that now delegates to the chunk iterator
238
+ so encode_large_text and iter_encode_chunks share exactly one loop."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  if text is None:
240
  raise ValueError('encode_large_text() received None')
241
+ parts = [
242
+ arr for arr in self.iter_encode_chunks(
243
+ text,
244
+ add_bos=add_bos,
245
+ add_eos=add_eos,
246
+ chunk_chars=chunk_chars,
247
+ )
248
+ ]
249
+ if not parts:
250
+ return []
251
+ return np.concatenate(parts, axis=0).astype(np.int32).tolist()
252
 
253
+ def iter_encode_chunks(
254
+ self,
255
+ text: str,
256
+ add_bos: bool = True,
257
+ add_eos: bool = True,
258
+ chunk_chars: int = DEFAULT_CHUNK_CHARS,
259
+ ):
260
+ """Yield per-chunk int32 arrays while preserving the first/last chunk
261
+ BOS/EOS semantics of the existing whole-file encode path.
262
+
263
+ This iterator mirrors `encode_large_text()` chunk placement logic,
264
+ but streams one chunk's token ids out as a NumPy array instead of
265
+ materializing a Python list for the whole file.
266
+ """
267
+ if text is None:
268
+ raise ValueError('iter_encode_chunks() received None')
269
  if text == '':
270
  ids: List[int] = []
271
  if add_bos:
272
  ids = [self.bos_id] + ids
273
  if add_eos:
274
  ids = ids + [self.eos_id]
275
+ yield np.asarray(ids, dtype=np.int32)
276
+ return
277
 
 
278
  pos = 0
279
  n = len(text)
280
+ first_chunk = True
281
  while pos < n:
282
  end = min(pos + chunk_chars, n)
283
  is_eof = (end >= n)
 
286
  piece = window
287
  pos = end
288
  else:
 
 
 
 
289
  cut = max(window.rfind(' '), window.rfind('\n'))
290
  if cut <= 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  piece = window
292
  pos = end
293
  else:
294
  piece = window[:cut]
295
  pos += cut
296
  if not piece:
 
 
 
 
 
 
297
  if not is_eof:
298
  pos += 1
299
  continue
300
+
301
  piece_ids = self.sp.encode(piece, out_type=int)
302
+ if add_bos and first_chunk:
303
+ piece_ids = [self.bos_id] + piece_ids
304
+ if add_eos and is_eof:
305
+ piece_ids = piece_ids + [self.eos_id]
306
 
307
+ yield np.asarray(piece_ids, dtype=np.int32)
308
+ first_chunk = False
 
 
 
309
 
310
  def encode_batch(
311
  self,