Download source/files/needle_gen.py from unparallelled/BluScriptCP: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/unparallelled/BluScriptCP/resolve/main/source/files/needle_gen.py
- Command line
-
hf download hf://unparallelled/BluScriptCP/source/files/needle_gen.py
-
curl -L -o needle_gen.py https://huggingface.co/unparallelled/BluScriptCP/resolve/main/source/files/needle_gen.py
14.1 kB
| #!/usr/bin/env python3 | |
| """ | |
| needle_gen.py -- synthesize retrieval-probe token streams for the existing | |
| CP_EVAL_PPL / CP_EVAL_TOKENS_BIN path. No C++ changes are needed: the eval | |
| already windows a raw int32 token file and already reports PER-POSITION | |
| mean_nll, so planting an answer span at a known offset lets us read retrieval | |
| NLL straight out of the CSV. | |
| Stdlib only (array + random) -- deliberately no numpy, because the local pyenv | |
| python3 lacks it while /usr/bin/python3 has it, and this must run identically on | |
| the local box and the server regardless of which `python3` resolves first. | |
| FIXED-SLOT LAYOUT (the construction that makes the differencing valid) | |
| --------------------------------------------------------------------- | |
| Every variant of a trial has byte-identical structure except for WHERE the | |
| needle sits (or whether it is present). The probe prefix and the answer span are | |
| always at the SAME absolute offsets, so we always difference NLLs measured at | |
| the same point on a position-varying curve: | |
| trial (T tokens): | |
| [ filler .......... N0..N(k-1) ......... filler ][ probe prefix ][ ANSWER ][pad] | |
| ^needle, k tokens k/2 k/2 >=2 | |
| slot_start = round(p_frac * max_slot_start) | |
| p_frac = 0.0 -> needle as far from the probe as possible | |
| p_frac = 1.0 -> needle immediately adjacent to the probe (the RETRIEVAL FLOOR) | |
| VARIANTS (7 per model/T/cache) | |
| p0, p0.25, p0.5, p0.75, p1 needle planted at that slot; p1 is the floor | |
| absent no needle planted; probe prefix and answer tokens | |
| still present (the no-information ceiling) | |
| null needle planted at p_frac=0 but the probe prefix is | |
| an UNRELATED k/2-gram, so retrieval is impossible | |
| -> supplies the significance threshold (RAW NLL) | |
| CRITICAL OFF-BY-ONE | |
| The eval scores input[t] = toks[base+t] against target[t] = toks[base+t+1] and | |
| accumulates the NLL at the position of the INPUT. So the NLL of the answer token | |
| living at trial offset `a` appears in the CSV at row `a-1`. The manifest records | |
| BOTH; needle_score.py must read `csv_positions`, never `answer_offsets`. | |
| Usage | |
| python3 needle_gen.py --T 32768 --variant p0.5 --block 0 --out-dir needles | |
| python3 needle_gen.py --T 32768 --all --blocks 5 --out-dir needles | |
| """ | |
| import argparse, array, json, os, random, sys | |
| VOCAB = 50304 # must match cfg.vocab_size; the C++ side bound-checks too | |
| PAD_TAIL = 4 # >=2: windows stride by exactly T, so the target at T-1 | |
| # belongs to the NEXT trial. Keep the answer clear of it. | |
| P_FRACS = [0.0, 0.25, 0.5, 0.75, 1.0] | |
| MIX = 1_000_003 # odd multiplier for deterministic seed derivation | |
| # Active p_frac grid. Defaults to P_FRACS; --p-fracs overrides it for the S>R | |
| # ablation, whose predicted dead gap [R,S) falls BETWEEN the default points and | |
| # would otherwise be unobservable. Overriding changes only which slots are probed, | |
| # never the layout, so answer-span offsets stay invariant. | |
| _ACTIVE_P_FRACS = list(P_FRACS) | |
| def set_p_fracs(vals): | |
| global _ACTIVE_P_FRACS | |
| _ACTIVE_P_FRACS = list(vals) | |
| def variant_names(): | |
| return [f"p{p:g}" for p in _ACTIVE_P_FRACS] + ["absent", "null"] | |
| def parse_variant(name): | |
| """-> (p_frac or None, is_null). absent -> (None, False); null -> (0.0, True).""" | |
| if name == "absent": | |
| return None, False | |
| if name == "null": | |
| return 0.0, True | |
| if name.startswith("p"): | |
| p = float(name[1:]) | |
| if not 0.0 <= p <= 1.0: | |
| raise SystemExit(f"p_frac must be in [0,1] (got {p})") | |
| return p, False | |
| raise SystemExit(f"unknown variant '{name}' (expected one of {variant_names()})") | |
| def check_platform(): | |
| if sys.byteorder != "little": | |
| raise SystemExit("CP_EVAL_TOKENS_BIN expects int32 LITTLE-endian; this host is big-endian") | |
| if array.array("i").itemsize != 4 or array.array("H").itemsize != 2: | |
| raise SystemExit("unexpected array itemsizes ('i' must be 4B, 'H' must be 2B)") | |
| def load_filler(path, need, rng): | |
| """Real tokens, so the LM is not off-distribution (fabricated filler would | |
| inflate NLL everywhere and mask the effect). Source is uint16; the eval file | |
| must be int32 LE, so widen explicitly and bound-check.""" | |
| ntok = os.path.getsize(path) // 2 | |
| if ntok < need + 1: | |
| raise SystemExit(f"{path} has {ntok} tokens, need >= {need + 1}") | |
| # One contiguous run per trial, so filler keeps local textual statistics | |
| # instead of being a shuffled bag of tokens. | |
| start = rng.randrange(0, ntok - need) | |
| buf = array.array("H") | |
| with open(path, "rb") as f: | |
| f.seek(start * 2) | |
| buf.fromfile(f, need) | |
| out = array.array("i", buf) # C-level uint16 -> int32 | |
| lo, hi = min(out), max(out) | |
| if lo < 0 or hi >= VOCAB: | |
| raise SystemExit(f"{path} has ids outside [0,{VOCAB}): min={lo} max={hi}") | |
| return out | |
| def build_layout(T, k, probe_pos=None): | |
| """Offsets are a pure function of (T, k, probe_pos) -- identical for every variant. | |
| probe_pos is None (default): probe+answer sit at the END of the trial, so the | |
| needle-to-probe DISTANCE is T - slot. Distance and the needle's ABSOLUTE | |
| position are then perfectly anti-correlated -- one variable, not two -- which | |
| makes "far from the question" and "early in the document" indistinguishable. | |
| probe_pos = P: probe+answer are planted at absolute offset P and everything | |
| after them is ordinary filler. Pinning the needle's slot while varying P (or | |
| vice versa) breaks that confound. Legal because the eval reports NLL at EVERY | |
| position, so the scored span does not have to be at the end -- and the | |
| manifest's csv_positions carries the offsets, so needle_score.py follows | |
| automatically with no change. | |
| """ | |
| if k % 2: | |
| raise SystemExit(f"--needle-len must be even (got {k})") | |
| if probe_pos is None: | |
| answer_end = T - PAD_TAIL | |
| answer_start = answer_end - k // 2 | |
| probe_start = answer_start - k // 2 # probe prefix: [probe_start, answer_start) | |
| else: | |
| probe_start = probe_pos | |
| answer_start = probe_start + k // 2 | |
| answer_end = probe_start + k | |
| if answer_end > T - PAD_TAIL: | |
| raise SystemExit(f"--probe-pos {probe_pos} puts the answer span end at " | |
| f"{answer_end}, past T-PAD_TAIL={T - PAD_TAIL}; the last " | |
| "position's target belongs to the next trial") | |
| max_slot_start = probe_start - k # needle must fit before the probe | |
| if max_slot_start < 0: | |
| raise SystemExit(f"probe at {probe_start} leaves no room for a needle of " | |
| f"{k} tokens before it (T={T})") | |
| return dict( | |
| T=T, needle_len=k, pad_tail=PAD_TAIL, probe_pos=probe_pos, | |
| probe_start=probe_start, answer_start=answer_start, answer_end=answer_end, | |
| max_slot_start=max_slot_start, | |
| answer_offsets=[answer_start, answer_end], | |
| # NLL is reported at the INPUT position, so the answer token at offset a is | |
| # scored on CSV row a-1. See "CRITICAL OFF-BY-ONE" above. | |
| csv_positions=[answer_start - 1, answer_end - 1], | |
| ) | |
| def make_trial(lay, p_frac, is_null, filler, rng, reverse=False): | |
| """One trial of exactly T int32 tokens. `filler` is consumed (modified).""" | |
| k = lay["needle_len"] | |
| half = k // 2 | |
| trial = filler # already a fresh copy per trial | |
| needle = array.array("i", [rng.randrange(VOCAB) for _ in range(k)]) | |
| # The answer span is the needle's second half, at fixed offsets -- unless | |
| # `reverse`, which swaps the halves: probe = needle's SECOND half, answer = its | |
| # FIRST, so the model must retrieve what PRECEDES the probe. Forward retrieval | |
| # (answer following the keyword) is an induction-head copy and "an easily | |
| # learned pattern" (arXiv:2404.16811 s3.1), so it is exactly what training on | |
| # forward data would Goodhart; backward retrieval cannot be satisfied that way, | |
| # which makes it the transfer test. Layout OFFSETS are untouched, so | |
| # csv_positions stays valid and needle_score.py needs no change. | |
| trial[lay["answer_start"]:lay["answer_end"]] = needle[:half] if reverse else needle[half:] | |
| # Probe prefix = the needle's other half, EXCEPT for `null`, where an unrelated | |
| # k/2-gram makes retrieval impossible while the answer TARGETS stay identical. | |
| if is_null: | |
| prefix = array.array("i", [rng.randrange(VOCAB) for _ in range(half)]) | |
| else: | |
| prefix = needle[half:] if reverse else needle[:half] | |
| trial[lay["probe_start"]:lay["answer_start"]] = prefix | |
| # Plant the needle in exactly one slot (or none, for `absent`). | |
| slot_start = None | |
| if p_frac is not None: | |
| slot_start = int(round(p_frac * lay["max_slot_start"])) | |
| trial[slot_start:slot_start + k] = needle | |
| return trial, slot_start | |
| def gen(args, variant, block): | |
| p_frac, is_null = parse_variant(variant) | |
| lay = build_layout(args.T, args.needle_len, args.probe_pos) | |
| # Seed depends on (seed, T, block, trial) and NOT on the variant, so every | |
| # variant of a block shares identical filler and identical needle tokens -- | |
| # the whole point of the fixed-slot construction. | |
| base_seed = (args.seed * MIX + args.T) * MIX + block | |
| stream = array.array("i") | |
| slots = [] | |
| for i in range(args.trials): | |
| rng = random.Random(base_seed * MIX + i) | |
| filler = load_filler(args.filler, args.T, rng) | |
| trial, slot = make_trial(lay, p_frac, is_null, filler, rng, args.reverse) | |
| if len(trial) != args.T: | |
| raise SystemExit(f"trial length {len(trial)} != T={args.T} -- bug") | |
| stream.extend(trial) | |
| slots.append(slot) | |
| # +1 token: window w reads toks[base+t] and toks[base+t+1] for t in [0,T), so | |
| # the final window needs index N*T to exist. | |
| stream.append(stream[-1]) | |
| if len(stream) != args.trials * args.T + 1: | |
| raise SystemExit("stream length wrong -- bug") | |
| os.makedirs(args.out_dir, exist_ok=True) | |
| tag = f"T{args.T}_{variant}_b{block}" | |
| bin_path = os.path.join(args.out_dir, f"needle_{tag}.bin") | |
| with open(bin_path, "wb") as f: | |
| stream.tofile(f) | |
| man = dict(bin=bin_path, variant=variant, block=block, p_frac=p_frac, is_null=is_null, | |
| trials=args.trials, seed=args.seed, vocab=VOCAB, reverse=args.reverse, | |
| slot_starts=slots, **lay) | |
| man_path = os.path.join(args.out_dir, f"needle_{tag}.json") | |
| with open(man_path, "w") as f: | |
| json.dump(man, f, indent=2) | |
| return bin_path, man_path, man | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--T", type=int, required=True, help="tokens per trial (= CP_T)") | |
| ap.add_argument("--trials", type=int, default=None, | |
| help="trials per block (= CP_EVAL_WINDOWS). Default is the plan's " | |
| "pinned N: 8 at T<=131072, 4 above.") | |
| ap.add_argument("--blocks", type=int, default=5, | |
| help="blocks = SEPARATE files, each needing its own eval invocation; " | |
| "the only source of spread (CP_EVAL_PPL emits no variance)") | |
| ap.add_argument("--block", type=int, default=None, help="generate just this block index") | |
| ap.add_argument("--variant", default=None, help=f"one of {variant_names()}") | |
| ap.add_argument("--all", action="store_true", help="every variant x every block") | |
| ap.add_argument("--needle-len", type=int, default=8, help="k; probe=k/2, answer=k/2") | |
| ap.add_argument("--filler", default="Data_Loader/Data/edufineweb_val_00000.bin") | |
| ap.add_argument("--seed", type=int, default=0) | |
| ap.add_argument("--probe-pos", type=int, default=None, | |
| help="absolute offset for the probe+answer span. Default: the end " | |
| "of the trial. Setting it lets DISTANCE vary independently of " | |
| "the needle's absolute position -- the confound the default " | |
| "layout cannot separate. Isolate runs by --out-dir: filenames " | |
| "are unchanged, so needle_score.py needs no flag.") | |
| ap.add_argument("--out-dir", default="needles") | |
| ap.add_argument("--reverse", action="store_true", | |
| help="BACKWARD retrieval (transfer test): probe = needle's 2nd half, " | |
| "answer = its 1st. Use a SEPARATE --out-dir -- filenames do not " | |
| "encode it, and absent/null must be regenerated in reverse too.") | |
| ap.add_argument("--p-fracs", default=None, | |
| help="comma-separated p_frac list overriding the default " | |
| f"{P_FRACS}. Needed by the STRING S>R ablation, whose predicted " | |
| "dead gap lies between the default points.") | |
| args = ap.parse_args() | |
| if args.p_fracs: | |
| vals = [float(x) for x in args.p_fracs.split(",") if x.strip()] | |
| if not vals or any(not 0.0 <= v <= 1.0 for v in vals): | |
| raise SystemExit(f"--p-fracs must be values in [0,1] (got {args.p_fracs})") | |
| set_p_fracs(vals) | |
| check_platform() | |
| if args.trials is None: # pinned N from the plan (A1) | |
| args.trials = 8 if args.T <= 131072 else 4 | |
| if args.all: | |
| jobs = [(v, b) for v in variant_names() for b in range(args.blocks)] | |
| else: | |
| if args.variant is None: | |
| raise SystemExit("give --variant NAME (and optionally --block), or --all") | |
| jobs = [(args.variant, args.block if args.block is not None else 0)] | |
| for v, b in jobs: | |
| bp, _, man = gen(args, v, b) | |
| slot = man["slot_starts"][0] | |
| print(f"{v:8s} block={b} {bp} answer_offsets={man['answer_offsets']} " | |
| f"csv_positions={man['csv_positions']} slot={slot}") | |
| if __name__ == "__main__": | |
| main() | |