File size: 3,493 Bytes
45121cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"""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