unparallelled commited on
Commit
0b72d45
·
verified ·
1 Parent(s): b809b1f

Upload scripts/peer/peer_gen.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/peer/peer_gen.py +213 -0
scripts/peer/peer_gen.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ peer_gen.py -- build NIAH prompts for a PEER model, in that model's own tokenizer.
4
+
5
+ Why this exists rather than reusing longeval_gen.py: that script emits GPT-2 token
6
+ ids, and the peer models do not share GPT-2's vocabulary (Qwen3 151,936,
7
+ Spark-X2.5 131,072). Feeding GPT-2 ids to them is meaningless, and decoding to
8
+ text then letting each model re-tokenize would make "16,384 tokens" mean a
9
+ different amount of text per model, which is exactly the comparison we are trying
10
+ to make fair.
11
+
12
+ THE INVARIANT: a prompt is EXACTLY --prompt-len tokens OF THE TARGET TOKENIZER,
13
+ and the haystack TEXT is byte-identical across models. So every model sees the
14
+ same story, cut to the same number of its own tokens.
15
+
16
+ Prompts are emitted as token IDS, not text. A decode->re-encode round trip is not
17
+ length-preserving for BPE, so building in token space and handing ids straight to
18
+ the model is the only way to guarantee the exact length.
19
+
20
+ Tasks and needle vocabulary are copied from longeval_gen.py deliberately, so a
21
+ peer number is directly comparable to ours. Same WORDS list, same 7-digit values,
22
+ same answer-prefix prompting, same depth semantics (0.0 = window start, farthest
23
+ from the question; 1.0 = adjacent to it).
24
+
25
+ usage:
26
+ peer_gen.py --tokenizer Qwen/Qwen3-4B-Base --haystack hay.txt \
27
+ --task niah_single --prompt-len 16384 --n 100 \
28
+ --depths 0.0,0.25,0.5,0.75,1.0 --out-dir out/
29
+ """
30
+
31
+ import argparse
32
+ import json
33
+ import os
34
+ import random
35
+ import sys
36
+
37
+ # --- copied verbatim from longeval_gen.py so peer numbers are comparable -------
38
+ WORDS = [
39
+ "aardvark", "buffalo", "cactus", "dolphin", "emerald", "falcon", "glacier",
40
+ "harbor", "iceberg", "jaguar", "kestrel", "lantern", "meadow", "nutmeg",
41
+ "orchid", "penguin", "quartz", "raven", "sapphire", "tundra", "umbrella",
42
+ "violet", "walnut", "xylophone", "yarrow", "zephyr", "anchor", "bramble",
43
+ "cinder", "driftwood", "ember", "fjord", "granite", "hollow", "ivory",
44
+ ]
45
+ VT_ANCHOR_MAX = 0.8
46
+
47
+
48
+ def rnd_number(rng):
49
+ return str(rng.randint(1000000, 9999999))
50
+
51
+
52
+ def effective_depth(task, depth):
53
+ return min(depth, VT_ANCHOR_MAX) if task == "vt" else depth
54
+
55
+
56
+ def t_niah_single(rng, depth):
57
+ k, v = rng.choice(WORDS), rnd_number(rng)
58
+ return ([(f" The special magic number for {k} is {v}. ", depth)],
59
+ f"\nQuestion: What is the special magic number for {k}?\n",
60
+ f"Answer: The special magic number for {k} is", [v])
61
+
62
+
63
+ def t_niah_multikey(rng, depth, n_keys=4):
64
+ keys = rng.sample(WORDS, n_keys)
65
+ vals = [rnd_number(rng) for _ in keys]
66
+ tgt = rng.randrange(n_keys)
67
+ sents = [(f" The special magic number for {k} is {v}. ",
68
+ depth if i == tgt else rng.random())
69
+ for i, (k, v) in enumerate(zip(keys, vals))]
70
+ k, v = keys[tgt], vals[tgt]
71
+ return (sents, f"\nQuestion: What is the special magic number for {k}?\n",
72
+ f"Answer: The special magic number for {k} is", [v])
73
+
74
+
75
+ def t_niah_multivalue(rng, depth, n_vals=3):
76
+ k = rng.choice(WORDS)
77
+ vals = [rnd_number(rng) for _ in range(n_vals)]
78
+ sents = [(f" One of the special magic numbers for {k} is {v}. ",
79
+ depth if i == 0 else rng.random()) for i, v in enumerate(vals)]
80
+ return (sents, f"\nQuestion: What are all the special magic numbers for {k}?\n",
81
+ f"Answer: The special magic numbers for {k} are", vals)
82
+
83
+
84
+ def t_vt(rng, depth, hops=4):
85
+ names = rng.sample([w.upper()[:3] + str(rng.randint(10, 99)) for w in WORDS], hops)
86
+ val = rnd_number(rng)
87
+ anchor = effective_depth("vt", depth)
88
+ sents = [(f" VAR {names[0]} = {val}. ", anchor)]
89
+ for i in range(1, hops):
90
+ sents.append((f" VAR {names[i]} = {names[i-1]}. ",
91
+ anchor + (1.0 - anchor) * i / hops))
92
+ return (sents, f"\nQuestion: What is the value of VAR {names[-1]}?\n",
93
+ f"Answer: VAR {names[-1]} =", [val])
94
+
95
+
96
+ def t_val_backward(rng, depth):
97
+ k, v = rng.choice(WORDS), rnd_number(rng)
98
+ return ([(f" The special magic number for {k} is {v}. ", depth)],
99
+ f"\nQuestion: Which item has the special magic number {v}?\n",
100
+ f"Answer: The item with the special magic number {v} is", [k])
101
+
102
+
103
+ TASKS = {"niah_single": t_niah_single, "niah_multikey": t_niah_multikey,
104
+ "niah_multivalue": t_niah_multivalue, "vt": t_vt,
105
+ "val_forward": t_niah_single, "val_backward": t_val_backward}
106
+
107
+
108
+ def build_one(task, rng, enc, prompt_len, hay_ids, depth, hay_off):
109
+ """One prompt of EXACTLY prompt_len tokens in `enc`'s vocabulary."""
110
+ sents, q, a, expected = TASKS[task](rng, depth)
111
+ q_ids, a_ids = enc(q), enc(a)
112
+ tail = len(q_ids) + len(a_ids)
113
+ body_budget = prompt_len - tail
114
+ if body_budget <= 0:
115
+ raise SystemExit(f"--prompt-len {prompt_len} too small for task {task}")
116
+
117
+ planted = sorted(((max(0.0, min(1.0, d)), enc(s)) for s, d in sents),
118
+ key=lambda x: x[0])
119
+ fill = body_budget - sum(len(i) for _, i in planted)
120
+ if fill <= 0:
121
+ raise SystemExit(f"needles do not fit in --prompt-len {prompt_len}")
122
+
123
+ H = lambda lo, n: hay_ids[hay_off + lo: hay_off + lo + max(0, n)]
124
+ body, used, pos = [], 0, 0
125
+ for frac, ids in planted:
126
+ take = max(0, int(frac * fill) - used)
127
+ body.extend(H(pos, take)); pos += take; used += take
128
+ body.extend(ids)
129
+ body.extend(H(pos, fill - used))
130
+
131
+ out = body + q_ids + a_ids
132
+ if len(out) < prompt_len: # haystack ran short
133
+ out = body + H(pos, prompt_len - len(out)) + q_ids + a_ids
134
+ out = out[:prompt_len - tail] + q_ids + a_ids
135
+ assert len(out) == prompt_len, (len(out), prompt_len)
136
+ return out, expected
137
+
138
+
139
+ def main():
140
+ ap = argparse.ArgumentParser()
141
+ ap.add_argument("--tokenizer", required=True,
142
+ help="HF repo id, or the literal 'gpt2' for tiktoken parity")
143
+ ap.add_argument("--haystack", required=True, help="UTF-8 text file of filler")
144
+ ap.add_argument("--task", required=True, choices=sorted(TASKS) + ["all"])
145
+ ap.add_argument("--prompt-len", type=int, required=True)
146
+ ap.add_argument("--n", type=int, default=100)
147
+ ap.add_argument("--depths", default="0.0,0.25,0.5,0.75,1.0")
148
+ ap.add_argument("--seed", type=int, default=0)
149
+ ap.add_argument("--out-dir", required=True)
150
+ args = ap.parse_args()
151
+
152
+ if args.tokenizer == "gpt2":
153
+ import tiktoken
154
+ _e = tiktoken.get_encoding("gpt2")
155
+ # disallowed_special=() matches the corpus's own encode_ordinary: any
156
+ # "<|endoftext|>"-looking text is ordinary text, never a special id.
157
+ # Without this, tiktoken raises on the first such string in the haystack.
158
+ enc = lambda s: _e.encode(s, disallowed_special=())
159
+ name = "tiktoken-gpt2"
160
+ else:
161
+ from transformers import AutoTokenizer
162
+ _t = AutoTokenizer.from_pretrained(args.tokenizer, trust_remote_code=True)
163
+ enc = lambda s: _t.encode(s, add_special_tokens=False)
164
+ name = args.tokenizer
165
+
166
+ text = open(args.haystack, encoding="utf-8", errors="ignore").read()
167
+ hay_ids = enc(text)
168
+ print(f" tokenizer {name}: haystack {len(text):,} chars -> {len(hay_ids):,} tokens")
169
+
170
+ os.makedirs(args.out_dir, exist_ok=True)
171
+ depths = [float(d) for d in args.depths.split(",")]
172
+ tasks = sorted(TASKS) if args.task == "all" else [args.task]
173
+
174
+ need = args.prompt_len * (args.n + 2)
175
+ if len(hay_ids) < need:
176
+ # tile rather than fail: a short haystack repeats, which is weaker
177
+ # distraction and INFLATES accuracy, so it is reported loudly.
178
+ reps = need // len(hay_ids) + 2
179
+ print(f" WARN: haystack has {len(hay_ids):,} tokens, need {need:,} -- "
180
+ f"tiling {reps}x. Repetitive filler inflates accuracy; record this.")
181
+ hay_ids = hay_ids * reps
182
+
183
+ for task in tasks:
184
+ rng = random.Random(args.seed) # per task, so tasks are independent
185
+ meta, rows = [], []
186
+ for i in range(args.n):
187
+ want = depths[i % len(depths)]
188
+ eff = effective_depth(task, want)
189
+ ids, expected = build_one(task, rng, enc, args.prompt_len, hay_ids,
190
+ want, hay_off=i * args.prompt_len)
191
+ rows.append({"i": i, "ids": ids})
192
+ meta.append({"i": i, "depth": eff, "depth_requested": want,
193
+ "expected": expected})
194
+ # A special id inside the body would act as a document reset and silently
195
+ # shorten the effective context -- the exact thing NIAH measures.
196
+ if args.tokenizer != "gpt2":
197
+ bad = sorted(set(_t.all_special_ids or []))
198
+ if bad:
199
+ hit = {t for r in rows for t in r["ids"] if t in set(bad)}
200
+ if hit:
201
+ print(f" WARN: {len(hit)} special id(s) {sorted(hit)} appear "
202
+ f"inside {task} prompts -- these truncate attention")
203
+ with open(os.path.join(args.out_dir, f"{task}_prompts.jsonl"), "w") as f:
204
+ for r in rows:
205
+ f.write(json.dumps(r) + "\n")
206
+ with open(os.path.join(args.out_dir, f"{task}_meta.json"), "w") as f:
207
+ json.dump({"task": task, "prompt_len": args.prompt_len, "n": args.n,
208
+ "seed": args.seed, "tokenizer": name, "items": meta}, f)
209
+ print(f" {task:<16} {args.n} x {args.prompt_len} tok -> {args.out_dir}")
210
+
211
+
212
+ if __name__ == "__main__":
213
+ main()