jvonrad commited on
Commit
94c46f3
·
verified ·
1 Parent(s): 694072a

Upload src/xscript/data/loader.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/xscript/data/loader.py +176 -0
src/xscript/data/loader.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic streaming loader over packed uint16 shards.
2
+
3
+ - One PackedStream per language: contiguous (seq_len+1)-token windows,
4
+ epoch-level window permutation (seeded), transparent epoch rollover with
5
+ epoch counting (matters for AR, whose pool may be smaller than the budget).
6
+ - MixedStream: bilingual token-level mixing. For every global sample slot a
7
+ stateless hash of (seed, slot) picks the language, so the mixture is exactly
8
+ reproducible, independent of world size, and resumable from a slot counter.
9
+
10
+ Mixing mode (thesis decision, configurable in the run config):
11
+ token-level p=0.5 matches ATLAS's compute-matched framing: each language
12
+ contributes half the training *tokens*; under a starved tokenizer the
13
+ cross-script partner therefore sees less *content* -- that is exactly the
14
+ effect under audit. `content` mode instead sets p from the byte premiums so
15
+ content is matched and token counts differ.
16
+ """
17
+ import json
18
+ from pathlib import Path
19
+
20
+ import numpy as np
21
+
22
+ from ..paths import shard_dir
23
+
24
+
25
+ def _splitmix64(x: int) -> int:
26
+ x = (x + 0x9E3779B97F4A7C15) & 0xFFFFFFFFFFFFFFFF
27
+ z = x
28
+ z = ((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9) & 0xFFFFFFFFFFFFFFFF
29
+ z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & 0xFFFFFFFFFFFFFFFF
30
+ return z ^ (z >> 31)
31
+
32
+
33
+ class PackedStream:
34
+ def __init__(self, lang: str, tok_name: str, seq_len: int, seed: int):
35
+ self.lang, self.seq_len, self.seed = lang, seq_len, seed
36
+ d = shard_dir(lang, tok_name)
37
+ index = json.loads((d / "index.json").read_text())
38
+ # Pool checkpoints can create complete but empty zstd shards. Packing
39
+ # those historically left zero-token .bin entries in index.json; mmap
40
+ # cannot open a zero-byte file when a window crosses that boundary.
41
+ # They carry no data, so filter them losslessly at load time.
42
+ names = [name for name in sorted(index) if index[name] > 0]
43
+ self.paths = [d / name for name in names]
44
+ self.lens = np.array([index[name] for name in names], dtype=np.int64)
45
+ self.cum = np.concatenate([[0], np.cumsum(self.lens)])
46
+ self.total_tokens = int(self.cum[-1])
47
+ self.n_windows = self.total_tokens // (seq_len + 1)
48
+ if self.n_windows == 0:
49
+ raise RuntimeError(f"shards for {lang}/{tok_name} too small")
50
+ self._mm = [None] * len(self.paths)
51
+ self._epoch = -1
52
+ self._perm = None
53
+ self.consumed = 0 # windows consumed (resume state)
54
+
55
+ def _memmap(self, i: int):
56
+ if self._mm[i] is None:
57
+ self._mm[i] = np.memmap(self.paths[i], dtype=np.uint16, mode="r")
58
+ return self._mm[i]
59
+
60
+ def _window(self, w: int) -> np.ndarray:
61
+ start = w * (self.seq_len + 1)
62
+ i = int(np.searchsorted(self.cum, start, side="right") - 1)
63
+ out = np.empty(self.seq_len + 1, dtype=np.uint16)
64
+ filled = 0
65
+ while filled < self.seq_len + 1:
66
+ off = start + filled - self.cum[i]
67
+ take = int(min(self.lens[i] - off, self.seq_len + 1 - filled))
68
+ out[filled:filled + take] = self._memmap(i)[off:off + take]
69
+ filled += take
70
+ i += 1
71
+ return out
72
+
73
+ def get(self, k: int) -> np.ndarray:
74
+ """k-th window in the global consumption order (deterministic)."""
75
+ epoch, pos = divmod(k, self.n_windows)
76
+ if epoch != self._epoch:
77
+ rng = np.random.default_rng([self.seed, epoch, 0xDA7A])
78
+ self._perm = rng.permutation(self.n_windows)
79
+ self._epoch = epoch
80
+ return self._window(int(self._perm[pos]))
81
+
82
+ def next(self) -> np.ndarray:
83
+ x = self.get(self.consumed)
84
+ self.consumed += 1
85
+ return x
86
+
87
+ @property
88
+ def epochs(self) -> float:
89
+ return self.consumed / self.n_windows
90
+
91
+ def state_dict(self):
92
+ return {"consumed": self.consumed}
93
+
94
+ def load_state_dict(self, s):
95
+ self.consumed = s["consumed"]
96
+
97
+
98
+ class MixedStream:
99
+ """Token-level language mixing across one or two PackedStreams."""
100
+
101
+ def __init__(self, langs: list[str], tok_name: str, seq_len: int, seed: int,
102
+ probs: list[float] | None = None):
103
+ self.langs = langs
104
+ self.streams = {l: PackedStream(l, tok_name, seq_len, seed + j)
105
+ for j, l in enumerate(langs)}
106
+ self.probs = probs or [1.0 / len(langs)] * len(langs)
107
+ assert abs(sum(self.probs) - 1.0) < 1e-6
108
+ self.seed = seed
109
+ self.slot = 0 # global sample counter (resume state)
110
+
111
+ def _choose(self, slot: int) -> str:
112
+ if len(self.langs) == 1:
113
+ return self.langs[0]
114
+ u = _splitmix64(self.seed * 0x100000001 + slot) / 2**64
115
+ acc = 0.0
116
+ for l, p in zip(self.langs, self.probs):
117
+ acc += p
118
+ if u < acc:
119
+ return l
120
+ return self.langs[-1]
121
+
122
+ def next_batch(self, n: int) -> tuple[np.ndarray, dict[str, int]]:
123
+ """n windows -> array (n, seq_len+1); also per-language window counts."""
124
+ rows, counts = [], {l: 0 for l in self.langs}
125
+ for _ in range(n):
126
+ l = self._choose(self.slot)
127
+ rows.append(self.streams[l].next())
128
+ counts[l] += 1
129
+ self.slot += 1
130
+ return np.stack(rows), counts
131
+
132
+ def draw(self) -> tuple[str, int]:
133
+ """Advance one global slot; return (lang, occurrence-index) WITHOUT reading.
134
+
135
+ Occurrence-index is the k-th window of that language in global order, so
136
+ stream.get(occ) is a pure deterministic lookup. All DDP ranks call draw()
137
+ for every slot (keeping counters identical) but only materialise their own
138
+ rows via streams[lang].get(occ) -- giving each rank a disjoint, reproducible
139
+ 1/world shard of the global batch.
140
+ """
141
+ l = self._choose(self.slot)
142
+ occ = self.streams[l].consumed
143
+ self.streams[l].consumed += 1
144
+ self.slot += 1
145
+ return l, occ
146
+
147
+ def rank_batch(self, n_global: int, rank: int, world: int
148
+ ) -> tuple[np.ndarray, dict[str, int]]:
149
+ """n_global windows drawn globally; rows n_global/world owned by `rank`."""
150
+ rows, counts = [], {l: 0 for l in self.langs}
151
+ for j in range(n_global):
152
+ l, occ = self.draw()
153
+ if j % world == rank:
154
+ rows.append(self.streams[l].get(occ))
155
+ counts[l] += 1
156
+ return np.stack(rows), counts
157
+
158
+ def skip_to(self, slot: int) -> None:
159
+ """Fast-forward mixture state without reading data (for resume)."""
160
+ assert slot >= self.slot
161
+ for s in range(self.slot, slot):
162
+ self.streams[self._choose(s)].consumed += 1
163
+ self.slot = slot
164
+
165
+ def state_dict(self):
166
+ return {"slot": self.slot,
167
+ "streams": {l: s.state_dict() for l, s in self.streams.items()}}
168
+
169
+ def load_state_dict(self, s):
170
+ self.slot = s["slot"]
171
+ for l, st in s["streams"].items():
172
+ self.streams[l].load_state_dict(st)
173
+
174
+ def stats(self) -> dict:
175
+ return {l: {"windows": s.consumed, "epochs": round(s.epochs, 4)}
176
+ for l, s in self.streams.items()}