Download src/tinyllm/data.py from CoolDip00/tiny-llm: direct link, hf CLI and curl.
- Browser
- Download file 5.36 kB
-
https://huggingface.co/spaces/CoolDip00/tiny-llm/resolve/main/src/tinyllm/data.py
- Command line
-
hf download hf://spaces/CoolDip00/tiny-llm/src/tinyllm/data.py
-
curl -L -o data.py https://huggingface.co/spaces/CoolDip00/tiny-llm/resolve/main/src/tinyllm/data.py
5.36 kB
| """Dataset loading and text cleanup (D-008, D-009, D-018). | |
| Every step that reads TinyStories (tokenizer training, tokenization) goes | |
| through `load_clean`, so they all see exactly the same text. | |
| """ | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from datasets import Dataset, load_dataset | |
| DATASET = "roneneldan/TinyStories" | |
| DATA_DIR = Path(__file__).resolve().parents[2] / "data" # git-ignored | |
| TOKEN_DTYPE = np.uint16 # vocab 8192 < 65536 (D-009) | |
| # D-018: TinyStories has UTF-8 punctuation that was decoded as Windows-1252 | |
| # ("mojibake"), e.g. “ -> "“". Map it back to plain ASCII. | |
| # Order matters: the 3-character sequences must run before the bare "â€". | |
| _REPLACEMENTS: list[tuple[str, str]] = [ | |
| ("“", '"'), # “ left double quote | |
| ("â€\x9d", '"'), # ” right double quote (when the 3rd byte survived) | |
| ("’", "'"), # ’ apostrophe / right single quote | |
| ("‘", "'"), # ‘ left single quote | |
| ("–", "-"), # – en dash | |
| ("—", "-"), # — em dash | |
| ("…", "..."), # … ellipsis | |
| ("â€", '"'), # ” right double quote whose 3rd byte (0x9D) was lost | |
| ("Â\xa0", " "), # no-break space | |
| ("Â\xad", ""), # soft hyphen | |
| # Real (correctly encoded) typographic punctuation -> ASCII. | |
| ("“", '"'), ("”", '"'), ("‘", "'"), ("’", "'"), | |
| ("–", "-"), ("—", "-"), ("…", "..."), ("\xa0", " "), ("\xad", ""), | |
| ] | |
| def clean_text(text: str) -> str | None: | |
| """Return cleaned ASCII text, or None if the story should be dropped. | |
| Dropped: empty stories, and stories that still contain non-ASCII | |
| characters after the replacements (rare leftovers such as 'é'). | |
| """ | |
| if not text.isascii(): | |
| for bad, good in _REPLACEMENTS: | |
| if bad in text: | |
| text = text.replace(bad, good) | |
| text = text.strip() | |
| if not text or not text.isascii(): | |
| return None | |
| return text | |
| def _clean_batch(batch: dict) -> dict: | |
| return {"text": [clean_text(t) or "" for t in batch["text"]]} | |
| def _keep_batch(batch: dict) -> list[bool]: | |
| return [t != "" for t in batch["text"]] | |
| def load_clean(split: str, num_proc: int = 4) -> Dataset: | |
| """Load a TinyStories split ('train' or 'validation') with D-018 cleanup applied. | |
| `datasets` caches the result, so only the first call does the work. | |
| """ | |
| ds = load_dataset(DATASET, split=split) | |
| # With split=... (and no streaming) this is always a Dataset. The check narrows | |
| # the declared Union return type for type checkers, and fails loudly otherwise. | |
| assert isinstance(ds, Dataset), f"expected Dataset, got {type(ds).__name__}" | |
| ds = ds.map(_clean_batch, batched=True, num_proc=num_proc, desc=f"clean {split}") | |
| return ds.filter(_keep_batch, batched=True, num_proc=num_proc, desc=f"drop {split}") | |
| def bin_path(split: str) -> Path: | |
| """'train' -> data/train.bin, 'validation' -> data/val.bin""" | |
| return DATA_DIR / f"{'val' if split == 'validation' else split}.bin" | |
| def write_tokens(split: str, tok, eos_id: int, batch_size: int = 10_000) -> tuple[int, int]: | |
| """Encode a cleaned split into one flat uint16 file: story EOS story EOS ... | |
| Streams batch by batch, so RAM use stays small. Writes to a .tmp file and | |
| renames at the end, so a crash never leaves a half-written .bin behind. | |
| Returns (n_stories, n_tokens). | |
| """ | |
| ds = load_clean(split) | |
| out = bin_path(split) | |
| out.parent.mkdir(exist_ok=True) | |
| tmp = out.with_suffix(".tmp") | |
| n_tokens = 0 | |
| with open(tmp, "wb") as f: | |
| for i in range(0, len(ds), batch_size): | |
| encs = tok.encode_batch(ds[i: i + batch_size]["text"]) # Rust, multi-threaded | |
| ids = np.fromiter( | |
| (t for e in encs for t in (*e.ids, eos_id)), dtype=np.int64 | |
| ) | |
| assert ids.max() < np.iinfo(TOKEN_DTYPE).max | |
| ids.astype(TOKEN_DTYPE).tofile(f) | |
| n_tokens += len(ids) | |
| tmp.replace(out) | |
| return len(ds), n_tokens | |
| def load_tokens(split: str) -> np.memmap: | |
| """Memory-map a .bin file: no RAM cost, the OS pages it in on demand.""" | |
| return np.memmap(bin_path(split), dtype=TOKEN_DTYPE, mode="r") | |
| def get_batch( | |
| tokens: np.ndarray, | |
| batch_size: int, | |
| seq_len: int, | |
| rng: np.random.Generator, | |
| device: str | torch.device = "cpu", | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """Random training windows: x = tokens[s : s+T], y = tokens[s+1 : s+T+1]. | |
| Windows start anywhere and cross story boundaries; EOS tells the model where a | |
| story ends. Sampling with replacement from a seeded `rng` makes runs reproducible, | |
| and saving `rng.bit_generator.state` in a checkpoint makes resume exact. | |
| On CUDA the batch goes through pinned memory so the copy can overlap with compute. | |
| """ | |
| if len(tokens) < seq_len + 1: | |
| raise ValueError(f"need at least {seq_len + 1} tokens, got {len(tokens)}") | |
| starts = rng.integers(0, len(tokens) - seq_len, size=batch_size) # last valid start: len-T-1 | |
| idx = starts[:, None] + np.arange(seq_len + 1) # (B, T+1) | |
| chunk = torch.from_numpy(tokens[idx].astype(np.int64)) # gather, then widen for embedding | |
| if torch.device(device).type == "cuda": | |
| chunk = chunk.pin_memory().to(device, non_blocking=True) | |
| return chunk[:, :-1], chunk[:, 1:] | |