AR / ar /data /readonce.py
AjayRangoji's picture
Upload AR-4B pretraining codebase, configs, and cluster scripts
2ae6691 verified
Raw History Blame Contribute Delete
4.19 kB
"""Read-once data loader (Stage 2).
Each file is cut into windows of seq_len+1 tokens that overlap by exactly one token
(x = first seq_len tokens, y = the same window shifted by one), so every token is a
prediction target once. Each file's windows are shuffled once with a fixed seed, and a
fixed schedule says which file every sample comes from, following the mix weights.
Everything is computed from (seed, step, slot), so a restart at any step continues
exactly where training stopped, with no saved loader state. A window is only reused
after its whole file has been read once.
"""
import numpy as np
# 524,288 tokens / 4096 = 128 sequences: the most any node count uses in one step.
# Each step owns 128 sample slots; node counts that use fewer just skip the rest.
SAMPLES_PER_STEP = 128
class ReadOnceDataset:
def __init__(self, bin_paths, seq_len, weights, start_step, end_step, eot_token_id, seed=1234):
assert end_step > start_step, "empty stage"
self.seq_len = seq_len
self.start_step = start_step
self.eot = eot_token_id
self.data = [np.memmap(p, dtype=np.uint32, mode="r") for p in bin_paths]
self.n_windows = [(len(d) - 1) // seq_len for d in self.data]
assert all(n > 0 for n in self.n_windows), "a file is shorter than one window"
w = np.asarray(weights, dtype=np.float64)
assert len(w) == len(self.data) and (w >= 0).all() and w.sum() > 0, "bad weights"
w = w / w.sum()
# each file's windows in one fixed shuffled order (int32 keeps RAM small)
self.perm = [np.random.default_rng(seed + 1000 + f).permutation(n).astype(np.int32)
for f, n in enumerate(self.n_windows)]
# which file each sample of the stage reads, and how many earlier samples used that file
self.total = (end_step - start_step) * SAMPLES_PER_STEP
self.src = np.random.default_rng(seed).choice(len(w), size=self.total, p=w).astype(np.int16)
self.occ = np.empty(self.total, dtype=np.int64)
for f in range(len(w)):
idx = np.flatnonzero(self.src == f)
self.occ[idx] = np.arange(len(idx))
@staticmethod
def slot(micro, i, rank, world, batch_size):
"""Position of one sequence inside its step's 128 slots; unique per (micro, i, rank)."""
s = (micro * batch_size + i) * world + rank
assert s < SAMPLES_PER_STEP, "more than 128 sequences per step: raise SAMPLES_PER_STEP"
return s
def window(self, step, slot):
"""(file index, window index, tokens) for one sample; tokens has seq_len+1 entries."""
k = (step - self.start_step) * SAMPLES_PER_STEP + slot
assert 0 <= k < self.total, f"step {step} is outside this stage"
f = int(self.src[k])
w = int(self.perm[f][self.occ[k] % self.n_windows[f]])
start = w * self.seq_len
tokens = np.asarray(self.data[f][start : start + self.seq_len + 1], dtype=np.int64)
return f, w, tokens
def get_numpy(self, step, micro, batch_size, rank, world):
"""One micro-batch as numpy arrays: inputs, targets, document ids."""
x = np.empty((batch_size, self.seq_len), dtype=np.int64)
y = np.empty((batch_size, self.seq_len), dtype=np.int64)
seg = np.empty((batch_size, self.seq_len), dtype=np.int32)
for i in range(batch_size):
_, _, chunk = self.window(step, self.slot(micro, i, rank, world, batch_size))
x[i], y[i] = chunk[:-1], chunk[1:]
# document id goes up AFTER each end-of-text, so the EOT stays with its own document
is_eot = (x[i] == self.eot).astype(np.int32)
seg[i] = np.cumsum(is_eot) - is_eot
return x, y, seg
def get_batch(self, step, micro, batch_size, rank, world, device="cuda"):
"""Same as get_numpy, as torch tensors on `device`."""
import torch
out = []
for a in self.get_numpy(step, micro, batch_size, rank, world):
t = torch.from_numpy(a)
if torch.device(device).type == "cuda":
t = t.pin_memory().to(device, non_blocking=True)
out.append(t)
return tuple(out)