Spaces:
Sleeping
Sleeping
Commit ·
dc2cbf1
1
Parent(s): a4d49a5
Update tokenizer.py
Browse files- 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 =
|
| 232 |
) -> List[int]:
|
| 233 |
-
"""
|
| 234 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 325 |
-
|
|
|
|
|
|
|
| 326 |
|
| 327 |
-
|
| 328 |
-
|
| 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,
|