unparallelled commited on
Commit
3fa8659
·
verified ·
1 Parent(s): aa3a64c

Upload scripts/mix_buckets.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/mix_buckets.py +190 -0
scripts/mix_buckets.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ mix_buckets.py -- build a mixed-length training set from the per-length buckets.
4
+
5
+ python3 mix_buckets.py --out Data_Loader/Data/mix50 \
6
+ --src b5_65536_131072=0.50 --src b3_16384_32768=0.30 --src b1_4096_8192=0.20 \
7
+ --total-tokens 900e6 --window 65536 --grad-accum 8
8
+
9
+ WHY THIS EXISTS
10
+ The 2026-09-12 run trained a full epoch on 100% b5 (every document >= 65,536
11
+ tokens) and lost long-context retrieval: niah_single went 67/65/52/29 ->
12
+ 45/17/2/1 at 8k/16k/32k/64k while training loss FELL from 2.89 to 1.14.
13
+ The literature is consistent that 100% long-context data causes severe
14
+ degradation and that 25-66% short-context data should be mixed in.
15
+
16
+ FORMAT (verified against the real shards, not assumed)
17
+ Raw uint16, NO header, documents delimited by EOT = 50256. b5 shard 0 holds
18
+ 1,059 documents of 65,573-130,893 tokens.
19
+
20
+ DOCUMENT-LEVEL INTERLEAVING
21
+ Shard-level mixing would give ~183 consecutive steps of one bucket at 96M
22
+ tokens/shard -- long enough for the model to settle into a per-distribution
23
+ strategy, which is the thing we are trying to prevent. This picks the next
24
+ document from whichever bucket is furthest BELOW its target token share, so
25
+ the mixture is right at every scale.
26
+
27
+ A NOTE ON WHAT "SHORT DATA" MEANS AT A 64k WINDOW
28
+ The training window stays 65,536. Short documents are PACKED, so a window
29
+ drawn from b1 holds ~10 documents separated by EOT rather than one contiguous
30
+ text. That is the standard recipe and it is the point: the model has to learn
31
+ that not everything in context is relevant, which is exactly the signal that
32
+ 100%-b5 training removed. It does NOT give the model short sequences -- that
33
+ would need a variable T the trainer does not support mid-run.
34
+
35
+ STEPS ARE COMPUTED CORRECTLY HERE
36
+ The old sizing was STEPS = total_tokens // global_batch. That ignores the
37
+ per-shard tail the loader discards on advance_shard(), and it came within one
38
+ batch of silently wrapping on the last run. The manifest below emulates
39
+ DataLoaderLite exactly and reports the true usable step count.
40
+ """
41
+ import argparse, os, sys
42
+ import numpy as np
43
+
44
+ EOT = 50256
45
+
46
+
47
+ def doc_stream(paths, eot=EOT):
48
+ """Yield documents (each ending with EOT) from a list of .bin shards.
49
+
50
+ Streams with memmap: b5 shards are 192 MB each and there can be 139 of them.
51
+ A document that straddles a shard boundary is stitched across the join.
52
+ """
53
+ carry = np.empty(0, dtype=np.uint16)
54
+ for p in paths:
55
+ a = np.memmap(p, dtype=np.uint16, mode="r")
56
+ idx = np.flatnonzero(np.asarray(a) == eot)
57
+ prev = 0
58
+ for i in idx:
59
+ seg = np.asarray(a[prev:i + 1])
60
+ if carry.size:
61
+ seg = np.concatenate([carry, seg]); carry = np.empty(0, dtype=np.uint16)
62
+ yield seg
63
+ prev = i + 1
64
+ tail = np.asarray(a[prev:])
65
+ if tail.size:
66
+ carry = np.concatenate([carry, tail]) if carry.size else tail.copy()
67
+ if carry.size:
68
+ yield carry
69
+
70
+
71
+ def main():
72
+ ap = argparse.ArgumentParser()
73
+ ap.add_argument("--out", required=True, help="output directory")
74
+ ap.add_argument("--src", action="append", required=True,
75
+ metavar="BUCKET=WEIGHT", help="repeatable, weights are TOKEN shares")
76
+ ap.add_argument("--data-root", default="Data_Loader/Data")
77
+ ap.add_argument("--total-tokens", type=float, required=True)
78
+ ap.add_argument("--shard-tokens", type=float, default=96e6)
79
+ ap.add_argument("--window", type=int, default=65536, help="CP_T")
80
+ ap.add_argument("--grad-accum", type=int, default=8)
81
+ ap.add_argument("--split", default="train")
82
+ a = ap.parse_args()
83
+
84
+ srcs = []
85
+ for s in a.src:
86
+ name, _, w = s.partition("=")
87
+ srcs.append((name, float(w)))
88
+ tw = sum(w for _, w in srcs)
89
+ srcs = [(n, w / tw) for n, w in srcs]
90
+
91
+ total = int(a.total_tokens)
92
+ shard_tokens = int(a.shard_tokens)
93
+ os.makedirs(a.out, exist_ok=True)
94
+
95
+ gens, want = {}, {}
96
+ print(" sources:")
97
+ for name, w in srcs:
98
+ d = os.path.join(a.data_root, name)
99
+ paths = sorted(
100
+ os.path.join(d, f) for f in os.listdir(d)
101
+ if f.endswith(".bin") and a.split in f)
102
+ if not paths:
103
+ sys.exit(f"no {a.split} shards in {d}")
104
+ avail = sum(os.path.getsize(p) for p in paths) // 2
105
+ need = int(total * w)
106
+ print(f" {name:<22} weight {w:5.1%} need {need/1e6:8.1f} M "
107
+ f"have {avail/1e6:8.1f} M {'OK' if avail >= need else '*** SHORT ***'}")
108
+ if avail < need:
109
+ print(f" (will wrap within {name}; reduce its weight or --total-tokens)")
110
+ gens[name] = doc_stream(paths)
111
+ want[name] = w
112
+
113
+ got = {n: 0 for n, _ in srcs}
114
+ written = 0
115
+ shard_idx = 0
116
+ buf = []
117
+ buf_n = 0
118
+ exhausted = set()
119
+
120
+ def flush():
121
+ nonlocal buf, buf_n, shard_idx
122
+ if not buf:
123
+ return
124
+ arr = np.concatenate(buf)
125
+ p = os.path.join(a.out, f"{a.split}_shard_{shard_idx:05d}.bin")
126
+ arr.tofile(p)
127
+ print(f" wrote {os.path.basename(p)} {arr.size/1e6:7.2f} M tokens")
128
+ shard_idx += 1
129
+ buf = []
130
+ buf_n = 0
131
+
132
+ print("\n building:")
133
+ while written < total and len(exhausted) < len(srcs):
134
+ # the bucket furthest below its target share goes next
135
+ pick, deficit = None, None
136
+ for n, w in srcs:
137
+ if n in exhausted:
138
+ continue
139
+ d = w - (got[n] / written if written else 0.0)
140
+ if deficit is None or d > deficit:
141
+ pick, deficit = n, d
142
+ if pick is None:
143
+ break
144
+ try:
145
+ doc = next(gens[pick])
146
+ except StopIteration:
147
+ exhausted.add(pick)
148
+ print(f" {pick} exhausted at {got[pick]/1e6:.1f} M tokens")
149
+ continue
150
+ buf.append(doc)
151
+ buf_n += doc.size
152
+ got[pick] += doc.size
153
+ written += doc.size
154
+ if buf_n >= shard_tokens:
155
+ flush()
156
+ flush()
157
+
158
+ # --- emulate DataLoaderLite so STEPS is right ---------------------------
159
+ BT = a.window # B=1
160
+ paths = sorted(os.path.join(a.out, f) for f in os.listdir(a.out) if f.endswith(".bin"))
161
+ batches = 0
162
+ for p in paths:
163
+ n = os.path.getsize(p) // 2
164
+ pos = 0
165
+ while pos + BT + 1 <= n:
166
+ batches += 1
167
+ pos += BT
168
+ if pos + BT + 1 > n:
169
+ break
170
+ steps = batches // a.grad_accum
171
+
172
+ print("\n MIX SUMMARY")
173
+ for n, w in srcs:
174
+ print(f" {n:<22} target {w:5.1%} actual {got[n]/written:5.1%} "
175
+ f"({got[n]/1e6:.1f} M tokens)")
176
+ print(f" total {written/1e6:.1f} M tokens in {len(paths)} shards")
177
+ print(f"\n USABLE STEPS at window {BT:,} x grad_accum {a.grad_accum}: {steps}")
178
+ print(f" (naive total//global_batch would say "
179
+ f"{written // (BT * a.grad_accum)} -- use {steps}, it accounts for the")
180
+ print(f" per-shard tail the loader discards, and for the silent wrap)")
181
+ with open(os.path.join(a.out, "MANIFEST.txt"), "w") as f:
182
+ f.write(f"tokens {written}\nshards {len(paths)}\nbatches {batches}\n"
183
+ f"window {BT}\ngrad_accum {a.grad_accum}\nusable_steps {steps}\n")
184
+ for n, w in srcs:
185
+ f.write(f"mix {n} target {w:.4f} actual {got[n]/written:.4f} tokens {got[n]}\n")
186
+ print(f" wrote {a.out}/MANIFEST.txt")
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()