Upload scripts/mix_buckets.py with huggingface_hub
Browse files- 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()
|