| """Runtime data loading: memmapped byte shards -> training batches. |
| |
| A batch is a set of `ctx_len`-length windows sampled uniformly at random from |
| the flat token stream (nanoGPT-style). Because windows cross document |
| boundaries, we also compute, per position: |
| |
| * segment ids -> which document the byte belongs to (cumulative BOS count). |
| The model uses these to block cross-document attention. |
| * position ids -> index WITHIN the current document, so RoPE resets at each |
| document start instead of drifting across concatenated docs. |
| |
| Both are cheap, fully vectorized, and ignored by the model when |
| `cfg.doc_attention_mask` is False (then it falls back to plain causal + arange). |
| """ |
| from __future__ import annotations |
|
|
| import os |
| from typing import Dict |
|
|
| import numpy as np |
| import torch |
|
|
| from .config import BOS_ID, ByteLMConfig |
|
|
|
|
| class ByteDataset: |
| """Memmap-backed sampler for one split.""" |
|
|
| def __init__(self, cfg: ByteLMConfig, split: str): |
| self.cfg = cfg |
| self.split = split |
| path = os.path.join(cfg.data_dir, f"{split}.bin") |
| if not os.path.exists(path): |
| raise FileNotFoundError( |
| f"missing shard {path}; run data_prep.prepare_data() first" |
| ) |
| |
| self.data = np.memmap(path, dtype=np.uint16, mode="r") |
| self.n = self.data.shape[0] |
| need = cfg.ctx_len + 1 |
| if self.n < need: |
| raise ValueError( |
| f"split {split!r} has {self.n} tokens < ctx_len+1 ({need}); " |
| f"use a larger corpus or a smaller cfg.ctx_len" |
| ) |
|
|
| def __len__(self) -> int: |
| return self.n |
|
|
| def _doc_aux(self, x: torch.Tensor) -> Dict[str, torch.Tensor]: |
| """Segment ids and within-doc position ids for a [B,T] input batch.""" |
| B, T = x.shape |
| idx = torch.arange(T, device=x.device).unsqueeze(0).expand(B, T) |
| is_bos = x == BOS_ID |
| seg_ids = torch.cumsum(is_bos.to(torch.int32), dim=1) |
| |
| bos_pos = torch.where(is_bos, idx, torch.full_like(idx, -1)) |
| last_bos = torch.cummax(bos_pos, dim=1).values.clamp_min(0) |
| pos_ids = idx - last_bos |
| return {"seg_ids": seg_ids, "pos_ids": pos_ids} |
|
|
| def get_batch(self, device: str | torch.device = "cpu", |
| generator: torch.Generator | None = None) -> Dict[str, torch.Tensor]: |
| cfg = self.cfg |
| T = cfg.ctx_len |
| hi = self.n - (T + 1) |
| ix = torch.randint(0, hi + 1, (cfg.batch_size,), generator=generator) |
| |
| xb = torch.empty((cfg.batch_size, T), dtype=torch.long) |
| yb = torch.empty((cfg.batch_size, T), dtype=torch.long) |
| for i, start in enumerate(ix.tolist()): |
| chunk = torch.from_numpy(self.data[start:start + T + 1].astype(np.int64)) |
| xb[i] = chunk[:-1] |
| yb[i] = chunk[1:] |
| batch = {"x": xb, "y": yb} |
| if cfg.doc_attention_mask: |
| batch.update(self._doc_aux(xb)) |
| |
| non_blocking = isinstance(device, torch.device) and device.type == "cuda" or device == "cuda" |
| if device not in ("cpu", torch.device("cpu")): |
| batch = {k: v.to(device, non_blocking=non_blocking) for k, v in batch.items()} |
| return batch |
|
|