unparallelled commited on
Commit
7cb8e0f
·
verified ·
1 Parent(s): 46c6b71

Upload scripts/longeval_gen.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/longeval_gen.py +281 -0
scripts/longeval_gen.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ longeval_gen.py -- build GENERATIVE long-context benchmark prompts for bluscriptCP.
4
+
5
+ Unlike needle_gen.py (which plants a random-token key/value pair and is scored by
6
+ teacher-forced NLL over an answer span that is ALREADY IN THE INPUT), everything
7
+ here is scored by what the model actually GENERATES. That is what RULER, NIAH and
8
+ VAL Probing measure, and it is the only form comparable to published numbers.
9
+
10
+ WHY THE PROMPTS LOOK LIKE THIS
11
+ The target is a ~1B BASE language model with no instruction tuning, so a bare
12
+ "Question: ...\\nAnswer:" often fails for reasons that have nothing to do with
13
+ retrieval. Every task therefore ends with an answer PREFIX that makes the
14
+ correct answer the natural continuation, e.g.
15
+
16
+ ... haystack ...
17
+ Question: What is the special magic number for aardvark?
18
+ Answer: The special magic number for aardvark is
19
+
20
+ The model only has to emit the digits. Scoring looks for the expected string
21
+ anywhere in the generated tokens, so trailing chatter does not cost a point.
22
+
23
+ TASKS
24
+ niah_single RULER NIAH-single one needle, one key, retrieve its value
25
+ niah_multikey RULER NIAH-multikey K needles, retrieve ONE named key
26
+ niah_multivalue RULER NIAH-multivalue one key with V values, retrieve all
27
+ vt RULER VT chain of variable assignments, resolve
28
+ val_forward VAL forward retrieval key stated before the query (default)
29
+ val_backward VAL backward retrieval value stated first, retrieve its KEY
30
+
31
+ OUTPUT (per task, into --out-dir)
32
+ <task>_prompts.bin int32 token stream, N prompts x --prompt-len tokens, end to end
33
+ (exactly what CP_GEN_TOKENS_BIN expects)
34
+ <task>_meta.json per-prompt expected answers + placement metadata
35
+
36
+ The prompt length is EXACT: the haystack is trimmed or extended so every prompt is
37
+ --prompt-len tokens, because the C++ reader slices the stream at a fixed stride.
38
+ """
39
+
40
+ import argparse
41
+ import json
42
+ import os
43
+ import random
44
+ import struct
45
+ import sys
46
+
47
+ try:
48
+ import tiktoken
49
+ except ImportError:
50
+ sys.exit("longeval_gen.py needs tiktoken: pip install tiktoken")
51
+
52
+ ENC = tiktoken.get_encoding("gpt2")
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Haystack: real text from the flux shards (uint16 GPT-2 token stream), so the
57
+ # distractor context has the same distribution the model was trained on. Falls
58
+ # back to a synthetic filler sentence when no shard is given, which keeps the
59
+ # generator usable on a box with no data.
60
+ # ---------------------------------------------------------------------------
61
+ FALLBACK = ENC.encode(
62
+ "The grass swayed in the long afternoon light while the river ran on "
63
+ "unhurried past the old stone bridge and the fields beyond. "
64
+ )
65
+
66
+
67
+ def load_haystack(path, need):
68
+ """Return >= `need` GPT-2 token ids of filler.
69
+
70
+ SLICE BEFORE CONVERTING. The flux val shard is 200 MB = 100M uint16 tokens;
71
+ `toks.astype(np.int64).tolist()` over the whole array costs ~800 MB of numpy
72
+ plus a ~2.8 GB Python list. Callers now ask for prompt_len*(n+2) so each
73
+ prompt can start at its own offset, which makes the slice essential.
74
+ """
75
+ if not path:
76
+ reps = need // len(FALLBACK) + 2
77
+ return (FALLBACK * reps)[:need + len(FALLBACK)]
78
+ import numpy as np
79
+ toks = np.fromfile(path, dtype=np.uint16)
80
+ if toks.size < need:
81
+ reps = int(need // toks.size) + 2
82
+ toks = np.tile(toks, reps)
83
+ return toks[:need].astype(np.int64).tolist()
84
+
85
+
86
+ # vt's chain needs room AFTER its anchor for the remaining links, so the anchor
87
+ # cannot sit at depth 1.0. Both t_vt and the metadata writer call this, so the
88
+ # recorded depth can never drift from the depth actually built.
89
+ VT_ANCHOR_MAX = 0.8
90
+
91
+
92
+ def effective_depth(task, depth):
93
+ return min(depth, VT_ANCHOR_MAX) if task == "vt" else depth
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Needle vocabulary. Words are common English so they tokenize compactly and the
98
+ # model has seen them; numbers are 7 digits so a lucky guess is ~1e-7.
99
+ # ---------------------------------------------------------------------------
100
+ WORDS = [
101
+ "aardvark", "buffalo", "cactus", "dolphin", "emerald", "falcon", "glacier",
102
+ "harbor", "iceberg", "jaguar", "kestrel", "lantern", "meadow", "nutmeg",
103
+ "orchid", "penguin", "quartz", "raven", "sapphire", "tundra", "umbrella",
104
+ "violet", "walnut", "xylophone", "yarrow", "zephyr", "anchor", "bramble",
105
+ "cinder", "driftwood", "ember", "fjord", "granite", "hollow", "ivory",
106
+ ]
107
+
108
+
109
+ def rnd_number(rng):
110
+ return str(rng.randint(1000000, 9999999))
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Task builders. Each returns (needle_sentences, question, answer_prefix, expected)
115
+ # needle_sentences : list of (sentence_text, depth_fraction) to plant
116
+ # expected : list of strings; ALL must appear in the generation to score 1
117
+ # ---------------------------------------------------------------------------
118
+ def t_niah_single(rng, depth):
119
+ k, v = rng.choice(WORDS), rnd_number(rng)
120
+ s = f" The special magic number for {k} is {v}. "
121
+ q = f"\nQuestion: What is the special magic number for {k}?\n"
122
+ a = f"Answer: The special magic number for {k} is"
123
+ return [(s, depth)], q, a, [v]
124
+
125
+
126
+ def t_niah_multikey(rng, depth, n_keys=4):
127
+ keys = rng.sample(WORDS, n_keys)
128
+ vals = [rnd_number(rng) for _ in keys]
129
+ # Distractors spread across the whole context; the TARGET sits at `depth`.
130
+ sents = []
131
+ tgt = rng.randrange(n_keys)
132
+ for i, (k, v) in enumerate(zip(keys, vals)):
133
+ d = depth if i == tgt else rng.random()
134
+ sents.append((f" The special magic number for {k} is {v}. ", d))
135
+ k, v = keys[tgt], vals[tgt]
136
+ q = f"\nQuestion: What is the special magic number for {k}?\n"
137
+ a = f"Answer: The special magic number for {k} is"
138
+ return sents, q, a, [v]
139
+
140
+
141
+ def t_niah_multivalue(rng, depth, n_vals=3):
142
+ k = rng.choice(WORDS)
143
+ vals = [rnd_number(rng) for _ in range(n_vals)]
144
+ # All values share one key, scattered; depth anchors the FIRST one.
145
+ sents = [(f" One of the special magic numbers for {k} is {v}. ",
146
+ depth if i == 0 else rng.random())
147
+ for i, v in enumerate(vals)]
148
+ q = f"\nQuestion: What are all the special magic numbers for {k}?\n"
149
+ a = f"Answer: The special magic numbers for {k} are"
150
+ return sents, q, a, vals # every value must appear
151
+
152
+
153
+ def t_vt(rng, depth, hops=4):
154
+ """RULER variable tracking: X1 = <num>, X2 = X1, X3 = X2 ... resolve the chain.
155
+
156
+ Depths must INCREASE along the chain. build_one sorts planted sentences by
157
+ depth, so random depths would scatter the chain in random document order --
158
+ that is backward-reference resolution, strictly harder than RULER's ordered
159
+ VT and not comparable to published numbers. Monotone depths put document
160
+ order = chain order (Python's sort is stable, so ties keep chain order).
161
+ """
162
+ names = rng.sample([w.upper()[:3] + str(rng.randint(10, 99)) for w in WORDS], hops)
163
+ val = rnd_number(rng)
164
+ anchor = effective_depth("vt", depth)
165
+ sents = [(f" VAR {names[0]} = {val}. ", anchor)]
166
+ for i in range(1, hops):
167
+ sents.append((f" VAR {names[i]} = {names[i-1]}. ",
168
+ anchor + (1.0 - anchor) * i / hops))
169
+ q = f"\nQuestion: What is the value of VAR {names[-1]}?\n"
170
+ a = f"Answer: VAR {names[-1]} ="
171
+ return sents, q, a, [val]
172
+
173
+
174
+ def t_val_forward(rng, depth):
175
+ return t_niah_single(rng, depth)
176
+
177
+
178
+ def t_val_backward(rng, depth):
179
+ """VAL backward retrieval: the VALUE is the query, the KEY is the answer."""
180
+ k, v = rng.choice(WORDS), rnd_number(rng)
181
+ s = f" The special magic number for {k} is {v}. "
182
+ q = f"\nQuestion: Which item has the special magic number {v}?\n"
183
+ a = f"Answer: The item with the special magic number {v} is"
184
+ return [(s, depth)], q, a, [k]
185
+
186
+
187
+ TASKS = {
188
+ "niah_single": t_niah_single,
189
+ "niah_multikey": t_niah_multikey,
190
+ "niah_multivalue": t_niah_multivalue,
191
+ "vt": t_vt,
192
+ "val_forward": t_val_forward,
193
+ "val_backward": t_val_backward,
194
+ }
195
+
196
+
197
+ def build_one(task, rng, prompt_len, hay, depth, hay_off=0):
198
+ """One prompt of EXACTLY prompt_len tokens.
199
+
200
+ hay_off shifts this prompt's filler window. Without it every prompt starts
201
+ the haystack at 0 and differs only in the needle, making the N prompts
202
+ replicates rather than independent draws -- which inflates apparent
203
+ precision because the effective sample size is far below N.
204
+ """
205
+ sents, q, a, expected = TASKS[task](rng, depth)
206
+ q_ids, a_ids = ENC.encode(q), ENC.encode(a)
207
+ tail = len(q_ids) + len(a_ids)
208
+ body_budget = prompt_len - tail
209
+ if body_budget <= 0:
210
+ raise SystemExit(f"--prompt-len {prompt_len} too small for task {task}")
211
+
212
+ # Plant each needle at its depth fraction of the BODY, then fill with haystack.
213
+ planted = sorted(((max(0.0, min(1.0, d)), ENC.encode(s)) for s, d in sents),
214
+ key=lambda x: x[0])
215
+ total_needle = sum(len(ids) for _, ids in planted)
216
+ fill = body_budget - total_needle
217
+ if fill <= 0:
218
+ raise SystemExit(f"needles do not fit in --prompt-len {prompt_len}")
219
+
220
+ H = lambda lo, n: hay[hay_off + lo: hay_off + lo + max(0, n)]
221
+
222
+ body, used, hay_pos = [], 0, 0
223
+ for frac, ids in planted:
224
+ want = int(frac * fill)
225
+ take = max(0, want - used)
226
+ body.extend(H(hay_pos, take)); hay_pos += take; used += take
227
+ body.extend(ids)
228
+ body.extend(H(hay_pos, fill - used))
229
+
230
+ out = body + q_ids + a_ids
231
+ # Exact length: the C++ reader slices at a fixed stride, so drift is fatal.
232
+ if len(out) < prompt_len:
233
+ out = body + H(hay_pos, prompt_len - len(out)) + q_ids + a_ids
234
+ out = out[:prompt_len - tail] + q_ids + a_ids
235
+ assert len(out) == prompt_len, (len(out), prompt_len)
236
+ return out, expected
237
+
238
+
239
+ def main():
240
+ ap = argparse.ArgumentParser()
241
+ ap.add_argument("--task", required=True, choices=sorted(TASKS) + ["all"])
242
+ ap.add_argument("--prompt-len", type=int, required=True,
243
+ help="exact tokens per prompt; must leave room for CP_GEN_MAX_NEW under CP_T")
244
+ ap.add_argument("--n", type=int, default=50, help="prompts per task")
245
+ ap.add_argument("--depths", default="0.1,0.3,0.5,0.7,0.9",
246
+ help="needle depth fractions, cycled across prompts")
247
+ ap.add_argument("--haystack", default="", help="flux .bin shard for filler text")
248
+ ap.add_argument("--seed", type=int, default=0)
249
+ ap.add_argument("--out-dir", required=True)
250
+ args = ap.parse_args()
251
+
252
+ os.makedirs(args.out_dir, exist_ok=True)
253
+ depths = [float(d) for d in args.depths.split(",")]
254
+ tasks = sorted(TASKS) if args.task == "all" else [args.task]
255
+
256
+ for task in tasks:
257
+ rng = random.Random(args.seed) # per-task, so tasks are independent
258
+ # (n + 2) windows so every prompt gets its own filler offset
259
+ hay = load_haystack(args.haystack, args.prompt_len * (args.n + 2))
260
+ bin_p = os.path.join(args.out_dir, f"{task}_prompts.bin")
261
+ meta = []
262
+ with open(bin_p, "wb") as f:
263
+ for i in range(args.n):
264
+ want = depths[i % len(depths)]
265
+ eff = effective_depth(task, want)
266
+ ids, expected = build_one(task, rng, args.prompt_len, hay, want,
267
+ hay_off=i * args.prompt_len)
268
+ f.write(struct.pack(f"<{len(ids)}i", *ids))
269
+ # Record the EFFECTIVE depth: longeval_score.py groups on this
270
+ # field, and depth-pooling is the primary unit of analysis, so a
271
+ # clamped vt row must not be counted in the depth-1.0 cell.
272
+ meta.append({"i": i, "depth": eff, "depth_requested": want,
273
+ "expected": expected})
274
+ with open(os.path.join(args.out_dir, f"{task}_meta.json"), "w") as f:
275
+ json.dump({"task": task, "prompt_len": args.prompt_len, "n": args.n,
276
+ "seed": args.seed, "items": meta}, f, indent=1)
277
+ print(f" {task:<16} {args.n} x {args.prompt_len} tok -> {bin_p}")
278
+
279
+
280
+ if __name__ == "__main__":
281
+ main()