kron-llm / kronllm /data.py
Icold's picture
Upload folder using huggingface_hub
6a0e18e verified
Raw History Blame Contribute Delete
7.56 kB
"""Byte-level data pipeline (TinyStories -> uint8 memmaps).
TinyStories is stored as UTF-8 bytes (no BPE): ``V = 256``. Stories are
separated by ``0xFF`` which can never occur inside valid UTF-8, so it is a safe
end-of-text marker.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any, Iterator, Mapping, Optional, Tuple
import numpy as np
import torch
from torch.utils.data import Dataset
EOT = 255
# ---------------------------------------------------------------------------
# dataset
# ---------------------------------------------------------------------------
class ByteDataset(Dataset):
"""Random-access token blocks backed by a memmap.
``dtype=np.uint8`` is the byte-level corpus (V=256); ``np.uint16`` is the
BPE corpus (V<=65535). The class name is historical.
"""
def __init__(self, path: str | Path, seq_len: int,
dtype: Any = np.uint8) -> None:
self.path = Path(path)
if not self.path.exists():
raise FileNotFoundError(
f"{self.path} not found -- run `python scripts/prepare_data.py` first")
self.seq_len = int(seq_len)
self.dtype = np.dtype(dtype)
self.data = np.memmap(self.path, dtype=self.dtype, mode="r")
if len(self.data) < self.seq_len + 1:
raise ValueError(f"{self.path} is too small ({len(self.data)} tokens)")
def __len__(self) -> int:
return max(1, (len(self.data) - 1) // self.seq_len)
def __getitem__(self, i: int) -> Tuple[torch.Tensor, torch.Tensor]:
start = (i * self.seq_len) % (len(self.data) - self.seq_len - 1)
chunk = np.asarray(self.data[start:start + self.seq_len + 1], dtype=np.int64)
x = torch.from_numpy(chunk[:-1])
y = torch.from_numpy(chunk[1:])
return x, y
def get_batch(self, batch_size: int, device: torch.device | str = "cpu",
generator: Optional[torch.Generator] = None
) -> Tuple[torch.Tensor, torch.Tensor]:
n = len(self.data)
ix = torch.randint(0, n - self.seq_len - 1, (batch_size,), generator=generator)
x = torch.stack([torch.from_numpy(
np.asarray(self.data[i:i + self.seq_len], dtype=np.int64)) for i in ix])
y = torch.stack([torch.from_numpy(
np.asarray(self.data[i + 1:i + 1 + self.seq_len], dtype=np.int64)) for i in ix])
return x.to(device, non_blocking=True), y.to(device, non_blocking=True)
def iter_blocks(self, batch_size: int, device: torch.device | str = "cpu"
) -> Iterator[Tuple[torch.Tensor, torch.Tensor]]:
"""Deterministic sequential pass (used for validation)."""
n_blocks = len(self)
for start in range(0, n_blocks, batch_size):
idxs = range(start, min(start + batch_size, n_blocks))
xs, ys = [], []
for b in idxs:
x, y = self[b]
xs.append(x)
ys.append(y)
yield torch.stack(xs).to(device), torch.stack(ys).to(device)
def make_synthetic_bin(path: str | Path, n_bytes: int = 1 << 20,
seed: int = 0) -> Path:
"""Random bytes with a learnable first-order structure (smoke tests)."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
rng = np.random.default_rng(seed)
alphabet = np.arange(32, 127, dtype=np.uint8)
data = rng.choice(alphabet, size=n_bytes)
# inject simple Markov structure so the model has something to learn
for i in range(1, n_bytes):
if rng.random() < 0.6:
data[i] = min(255, data[i - 1] + 1)
data.tofile(path)
return path
# ---------------------------------------------------------------------------
# preparation
# ---------------------------------------------------------------------------
SEP = b"\xff"
def dataset_paths(cfg: Mapping[str, Any], data_dir: str | Path | None = None
) -> dict:
"""Resolve corpus paths + dtype from the tokenizer choice.
``bpe`` -> ``data/{train,val}.bpe.bin`` as ``uint16``
``byte`` -> ``data/{train,val}.bin`` as ``uint8``
"""
dc = cfg["data"]
base = Path(data_dir or dc.get("data_dir", "data"))
kind = str(dc.get("tokenizer", "byte")).lower()
if kind in ("byte", "bytes", "byte_level"):
return {"train": base / dc.get("train_bin", "train.bin"),
"val": base / dc.get("val_bin", "val.bin"),
"dtype": np.uint8, "tokenizer": "byte"}
if kind == "bpe":
return {"train": base / dc.get("train_bin", "train.bpe.bin"),
"val": base / dc.get("val_bin", "val.bpe.bin"),
"dtype": np.uint16, "tokenizer": "bpe"}
raise ValueError(f"unknown tokenizer {kind!r}")
def open_dataset(cfg: Mapping[str, Any], split: str = "train",
data_dir: str | Path | None = None,
seq_len: int | None = None) -> "ByteDataset":
paths = dataset_paths(cfg, data_dir)
n = int(seq_len if seq_len is not None else cfg["data"]["seq_len"])
return ByteDataset(paths[split], n, dtype=paths["dtype"])
def _encode_texts(texts, eot: int = EOT, budget: Optional[int] = None) -> bytes:
sep = bytes([eot])
out = bytearray()
for text in texts:
out += text.encode("utf-8", errors="ignore")
out += sep
if budget is not None and len(out) >= budget:
break
return bytes(out)
def prepare_tinystories(cfg: Mapping[str, Any]) -> dict:
"""Download/encode TinyStories into ``{data_dir}/train.bin`` and ``val.bin``."""
from datasets import load_dataset
dc = cfg["data"]
data_dir = Path(dc.get("data_dir", "data"))
data_dir.mkdir(parents=True, exist_ok=True)
eot = int(dc.get("eot_id", EOT))
max_stories = dc.get("max_stories", 200_000)
max_val = dc.get("max_val_stories", 10_000)
train_path = data_dir / "train.bin"
val_path = data_dir / "val.bin"
meta: dict = {"dataset": dc["dataset"], "eot": eot}
if train_path.exists() and val_path.exists() and not dc.get("force", False):
meta["train_bytes"] = train_path.stat().st_size
meta["val_bytes"] = val_path.stat().st_size
meta["cached"] = True
return meta
ds = load_dataset(dc["dataset"])
tr_key = "train" if "train" in ds else list(ds.keys())[0]
va_key = "validation" if "validation" in ds else (
"valid" if "valid" in ds else None)
del ds
n_tr = int(max_stories) if max_stories else None
tr_split = f"{tr_key}[:{n_tr}]" if n_tr else tr_key
print(f"[data] encoding train split {tr_split} ...")
tr_ds = load_dataset(dc["dataset"], split=tr_split)
tr_bytes = _encode_texts(tr_ds["text"], eot)
train_path.write_bytes(tr_bytes)
del tr_ds
if va_key is not None:
n_va = int(max_val) if max_val else None
va_split = f"{va_key}[:{n_va}]" if n_va else va_key
print(f"[data] encoding validation split {va_split} ...")
va_ds = load_dataset(dc["dataset"], split=va_split)
va_bytes = _encode_texts(va_ds["text"], eot)
del va_ds
else:
n = len(tr_bytes)
split_at = int(n * (1.0 - float(dc.get("val_split", 0.05))))
va_bytes = tr_bytes[split_at:]
tr_bytes = tr_bytes[:split_at]
train_path.write_bytes(tr_bytes)
val_path.write_bytes(va_bytes)
meta["train_bytes"] = len(tr_bytes)
meta["val_bytes"] = len(va_bytes)
meta["train_stories"] = n_tr
meta["cached"] = False
return meta