Omarrran's picture
Add Transformers-compatible ks_byte_lm SpaceByte release
45121cf verified
Raw
History Blame Contribute Delete
3.49 kB
"""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"
)
# uint16 on disk; mmap so we never load the whole corpus into RAM.
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)
# within-doc position: index minus index of the most recent BOS.
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)
# Gather windows; cast uint16->int64 for embedding lookup.
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))
# Move to device; non_blocking only helps with pinned CUDA transfers.
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