Upload scripts/longeval_gen.py with huggingface_hub
Browse files- scripts/longeval_gen.py +246 -3
scripts/longeval_gen.py
CHANGED
|
@@ -184,6 +184,219 @@ def t_val_backward(rng, depth):
|
|
| 184 |
return [(s, depth)], q, a, [k]
|
| 185 |
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
TASKS = {
|
| 188 |
"niah_single": t_niah_single,
|
| 189 |
"niah_multikey": t_niah_multikey,
|
|
@@ -191,6 +404,16 @@ TASKS = {
|
|
| 191 |
"vt": t_vt,
|
| 192 |
"val_forward": t_val_forward,
|
| 193 |
"val_backward": t_val_backward,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
}
|
| 195 |
|
| 196 |
|
|
@@ -202,13 +425,30 @@ def build_one(task, rng, prompt_len, hay, depth, hay_off=0):
|
|
| 202 |
replicates rather than independent draws -- which inflates apparent
|
| 203 |
precision because the effective sample size is far below N.
|
| 204 |
"""
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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])
|
|
@@ -255,8 +495,11 @@ def main():
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 260 |
bin_p = os.path.join(args.out_dir, f"{task}_prompts.bin")
|
| 261 |
meta = []
|
| 262 |
with open(bin_p, "wb") as f:
|
|
|
|
| 184 |
return [(s, depth)], q, a, [k]
|
| 185 |
|
| 186 |
|
| 187 |
+
|
| 188 |
+
# ---------------------------------------------------------------------------
|
| 189 |
+
# RULER PARITY ADDITIONS (2026-09-17)
|
| 190 |
+
#
|
| 191 |
+
# RULER is 13 tasks in 4 categories: 8 NIAH retrieval variants, 1 multi-hop
|
| 192 |
+
# tracing, 2 aggregation, 2 QA. The six tasks above cover 4 of the 8 NIAH slots
|
| 193 |
+
# plus VT. Everything below fills the rest.
|
| 194 |
+
#
|
| 195 |
+
# The NIAH variants differ only in NEEDLE TYPE (7-digit number vs UUID) and
|
| 196 |
+
# HAYSTACK TYPE (real text vs a repeated noise sentence), so they are
|
| 197 |
+
# parameterisations rather than new mechanisms -- which is exactly how RULER
|
| 198 |
+
# defines them.
|
| 199 |
+
#
|
| 200 |
+
# CWE and FWE are structurally different: the body IS the data, not a haystack
|
| 201 |
+
# with needles planted in it. They are listed in BODY_TASKS and build_one takes
|
| 202 |
+
# a separate path for them.
|
| 203 |
+
# ---------------------------------------------------------------------------
|
| 204 |
+
|
| 205 |
+
def rnd_uuid(rng):
|
| 206 |
+
h = "0123456789abcdef"
|
| 207 |
+
f = lambda n: "".join(rng.choice(h) for _ in range(n))
|
| 208 |
+
return f"{f(8)}-{f(4)}-{f(4)}-{f(4)}-{f(12)}"
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# Tasks whose haystack must be the repeated-noise filler rather than real text.
|
| 212 |
+
NOISE_TASKS = {"niah_single_1"}
|
| 213 |
+
# Tasks that generate their own body instead of planting needles in a haystack.
|
| 214 |
+
BODY_TASKS = {"cwe", "fwe", "qa_1", "qa_2"}
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def t_niah_single_1(rng, depth):
|
| 218 |
+
"""S-NIAH-1: word-number needle in REPEATED NOISE (see NOISE_TASKS)."""
|
| 219 |
+
return t_niah_single(rng, depth)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def t_niah_single_3(rng, depth):
|
| 223 |
+
"""S-NIAH-3: word-UUID needle. UUIDs are far longer and less token-friendly
|
| 224 |
+
than 7-digit numbers, so this is the hardest single-needle variant."""
|
| 225 |
+
k, v = rng.choice(WORDS), rnd_uuid(rng)
|
| 226 |
+
s = f" The special magic uuid for {k} is {v}. "
|
| 227 |
+
q = f"\nQuestion: What is the special magic uuid for {k}?\n"
|
| 228 |
+
a = f"Answer: The special magic uuid for {k} is"
|
| 229 |
+
return [(s, depth)], q, a, [v]
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def t_niah_multikey_2(rng, depth):
|
| 233 |
+
"""MK-NIAH-2: 8 distractor keys instead of 4."""
|
| 234 |
+
return t_niah_multikey(rng, depth, n_keys=8)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def t_niah_multikey_3(rng, depth, n_keys=8):
|
| 238 |
+
"""MK-NIAH-3: 8 keys AND UUID values."""
|
| 239 |
+
keys = rng.sample(WORDS, n_keys)
|
| 240 |
+
vals = [rnd_uuid(rng) for _ in keys]
|
| 241 |
+
tgt = rng.randrange(n_keys)
|
| 242 |
+
sents = [(f" The special magic uuid for {k} is {v}. ",
|
| 243 |
+
depth if i == tgt else rng.random())
|
| 244 |
+
for i, (k, v) in enumerate(zip(keys, vals))]
|
| 245 |
+
k, v = keys[tgt], vals[tgt]
|
| 246 |
+
q = f"\nQuestion: What is the special magic uuid for {k}?\n"
|
| 247 |
+
a = f"Answer: The special magic uuid for {k} is"
|
| 248 |
+
return sents, q, a, [v]
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def t_niah_multiquery(rng, depth, n_keys=4):
|
| 252 |
+
"""MQ-NIAH: several keys planted, ALL of them queried at once. Distinct from
|
| 253 |
+
multikey (one target among distractors) and multivalue (one key, many
|
| 254 |
+
values): here every planted pair must be retrieved."""
|
| 255 |
+
keys = rng.sample(WORDS, n_keys)
|
| 256 |
+
vals = [rnd_number(rng) for _ in keys]
|
| 257 |
+
sents = [(f" The special magic number for {k} is {v}. ",
|
| 258 |
+
depth if i == 0 else rng.random())
|
| 259 |
+
for i, (k, v) in enumerate(zip(keys, vals))]
|
| 260 |
+
q = ("\nQuestion: What are the special magic numbers for "
|
| 261 |
+
+ ", ".join(keys) + "?\n")
|
| 262 |
+
a = "Answer: The special magic numbers are"
|
| 263 |
+
return sents, q, a, vals # every value must appear
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
# ---- aggregation ----------------------------------------------------------
|
| 267 |
+
# These return (body_text, question, answer_prefix, expected). build_one
|
| 268 |
+
# tokenises body_text directly and does NOT use the haystack.
|
| 269 |
+
|
| 270 |
+
CWE_COMMON, CWE_TOPK = 10, 10
|
| 271 |
+
FWE_TOPK = 3
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def t_cwe(rng, depth, body_budget_tok=4096):
|
| 275 |
+
"""CWE: a word list in which CWE_COMMON words recur many times and the rest
|
| 276 |
+
appear once. The model must name the top-K most common.
|
| 277 |
+
|
| 278 |
+
Word pool is built from WORDS x a digit suffix so every item is a single
|
| 279 |
+
common English token plus a digit -- keeps tokenisation tight and avoids
|
| 280 |
+
the model recognising real-word frequency from pretraining."""
|
| 281 |
+
pool = [f"{w}{i}" for w in WORDS for i in range(10)]
|
| 282 |
+
rng.shuffle(pool)
|
| 283 |
+
common = pool[:CWE_COMMON]
|
| 284 |
+
rest = pool[CWE_COMMON:]
|
| 285 |
+
# ~4 tokens per "word," item; reserve a little headroom.
|
| 286 |
+
n_items = max(CWE_COMMON * 6, int(body_budget_tok / 4.2))
|
| 287 |
+
reps = max(3, (n_items // 2) // max(1, CWE_COMMON))
|
| 288 |
+
items = [c for c in common for _ in range(reps)]
|
| 289 |
+
singles = rest * (max(0, n_items - len(items)) // max(1, len(rest)) + 1)
|
| 290 |
+
items += singles[: max(0, n_items - len(items))]
|
| 291 |
+
rng.shuffle(items)
|
| 292 |
+
body = " " + ", ".join(items) + ". "
|
| 293 |
+
q = f"\nQuestion: What are the {CWE_TOPK} most common words in the list above?\n"
|
| 294 |
+
a = "Answer: The most common words are"
|
| 295 |
+
return body, q, a, common # all K must appear
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def t_fwe(rng, depth, body_budget_tok=4096, alpha=2.0):
|
| 299 |
+
"""FWE: words drawn from a Zeta(alpha) distribution over a shuffled pool;
|
| 300 |
+
the model must name the top-3 most frequent. Unlike CWE the frequencies are
|
| 301 |
+
graded rather than two-tier, so ranking matters."""
|
| 302 |
+
pool = [f"{w}{i}" for w in WORDS for i in range(10)]
|
| 303 |
+
rng.shuffle(pool)
|
| 304 |
+
n_items = max(60, int(body_budget_tok / 4.2))
|
| 305 |
+
# Zeta: P(rank r) ~ r^-alpha over the shuffled pool.
|
| 306 |
+
wts = [(r + 1) ** (-alpha) for r in range(len(pool))]
|
| 307 |
+
tot = sum(wts)
|
| 308 |
+
cum, acc = [], 0.0
|
| 309 |
+
for w in wts:
|
| 310 |
+
acc += w / tot
|
| 311 |
+
cum.append(acc)
|
| 312 |
+
def draw():
|
| 313 |
+
u = rng.random()
|
| 314 |
+
lo, hi = 0, len(cum) - 1
|
| 315 |
+
while lo < hi:
|
| 316 |
+
mid = (lo + hi) // 2
|
| 317 |
+
if cum[mid] < u: lo = mid + 1
|
| 318 |
+
else: hi = mid
|
| 319 |
+
return pool[lo]
|
| 320 |
+
items = [draw() for _ in range(n_items)]
|
| 321 |
+
counts = {}
|
| 322 |
+
for it in items: counts[it] = counts.get(it, 0) + 1
|
| 323 |
+
top = [w for w, _ in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))][:FWE_TOPK]
|
| 324 |
+
rng.shuffle(items)
|
| 325 |
+
body = " " + ", ".join(items) + ". "
|
| 326 |
+
q = f"\nQuestion: What are the {FWE_TOPK} most frequently appearing words in the list above?\n"
|
| 327 |
+
a = "Answer: The most frequent words are"
|
| 328 |
+
return body, q, a, top
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
# ---- QA with distractors (RULER qa_1 / qa_2) ------------------------------
|
| 333 |
+
# RULER "extends traditional QA datasets by adding distracting paragraphs".
|
| 334 |
+
# The GOLD paragraph sits at `depth`; the rest of the body is distractor
|
| 335 |
+
# paragraphs drawn from other questions in the same dataset.
|
| 336 |
+
#
|
| 337 |
+
# HotpotQA yes/no answers are DROPPED: scoring is substring match on normalised
|
| 338 |
+
# text, and "yes" matches almost any generation, which would score noise as a hit.
|
| 339 |
+
|
| 340 |
+
QA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "qa_data")
|
| 341 |
+
_QA_CACHE = {}
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def _load_qa(kind):
|
| 345 |
+
if kind in _QA_CACHE:
|
| 346 |
+
return _QA_CACHE[kind]
|
| 347 |
+
import pyarrow.parquet as pq
|
| 348 |
+
if kind == "squad":
|
| 349 |
+
t = pq.read_table(os.path.join(QA_DIR, "squad_val.parquet")).to_pydict()
|
| 350 |
+
rows = []
|
| 351 |
+
for ctx, qn, ans in zip(t["context"], t["question"], t["answers"]):
|
| 352 |
+
a = ans["text"][0] if ans["text"] else ""
|
| 353 |
+
if len(a) >= 3:
|
| 354 |
+
rows.append((ctx, qn, a))
|
| 355 |
+
else:
|
| 356 |
+
t = pq.read_table(os.path.join(QA_DIR, "hotpot_val.parquet")).to_pydict()
|
| 357 |
+
rows = []
|
| 358 |
+
for ctx, qn, a in zip(t["context"], t["question"], t["answer"]):
|
| 359 |
+
if a.lower() in ("yes", "no") or len(a) < 3:
|
| 360 |
+
continue # see note above
|
| 361 |
+
paras = [" ".join(sents) for sents in ctx["sentences"]]
|
| 362 |
+
rows.append(("\n\n".join(paras), qn, a))
|
| 363 |
+
if not rows:
|
| 364 |
+
raise SystemExit(f"no usable {kind} rows in {QA_DIR}")
|
| 365 |
+
_QA_CACHE[kind] = rows
|
| 366 |
+
return rows
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def _t_qa(kind, rng, depth, body_budget_tok):
|
| 370 |
+
rows = _load_qa(kind)
|
| 371 |
+
gold_ctx, question, answer = rows[rng.randrange(len(rows))]
|
| 372 |
+
gold = " " + gold_ctx.strip() + " "
|
| 373 |
+
# distractors from OTHER questions, appended until the budget is full
|
| 374 |
+
need = max(0, body_budget_tok - len(ENC.encode(gold)))
|
| 375 |
+
pre, post, got = [], [], 0
|
| 376 |
+
want_pre = int(max(0.0, min(1.0, depth)) * need)
|
| 377 |
+
while got < need:
|
| 378 |
+
d = rows[rng.randrange(len(rows))][0]
|
| 379 |
+
if d == gold_ctx:
|
| 380 |
+
continue
|
| 381 |
+
piece = " " + d.strip() + " "
|
| 382 |
+
n = len(ENC.encode(piece))
|
| 383 |
+
(pre if sum(len(ENC.encode(x)) for x in pre) < want_pre else post).append(piece)
|
| 384 |
+
got += n
|
| 385 |
+
body = "".join(pre) + gold + "".join(post)
|
| 386 |
+
q = f"\nQuestion: {question}\n"
|
| 387 |
+
a = "Answer:"
|
| 388 |
+
return body, q, a, [answer]
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def t_qa_1(rng, depth, body_budget_tok=4096):
|
| 392 |
+
"""SQuAD paragraph + SQuAD distractors."""
|
| 393 |
+
return _t_qa("squad", rng, depth, body_budget_tok)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def t_qa_2(rng, depth, body_budget_tok=4096):
|
| 397 |
+
"""HotpotQA (multi-hop, 2 gold paragraphs) + distractors."""
|
| 398 |
+
return _t_qa("hotpot", rng, depth, body_budget_tok)
|
| 399 |
+
|
| 400 |
TASKS = {
|
| 401 |
"niah_single": t_niah_single,
|
| 402 |
"niah_multikey": t_niah_multikey,
|
|
|
|
| 404 |
"vt": t_vt,
|
| 405 |
"val_forward": t_val_forward,
|
| 406 |
"val_backward": t_val_backward,
|
| 407 |
+
# --- RULER parity (2026-09-17) ---
|
| 408 |
+
"niah_single_1": t_niah_single_1,
|
| 409 |
+
"niah_single_3": t_niah_single_3,
|
| 410 |
+
"niah_multikey_2": t_niah_multikey_2,
|
| 411 |
+
"niah_multikey_3": t_niah_multikey_3,
|
| 412 |
+
"niah_multiquery": t_niah_multiquery,
|
| 413 |
+
"cwe": t_cwe,
|
| 414 |
+
"fwe": t_fwe,
|
| 415 |
+
"qa_1": t_qa_1,
|
| 416 |
+
"qa_2": t_qa_2,
|
| 417 |
}
|
| 418 |
|
| 419 |
|
|
|
|
| 425 |
replicates rather than independent draws -- which inflates apparent
|
| 426 |
precision because the effective sample size is far below N.
|
| 427 |
"""
|
| 428 |
+
# BODY_TASKS (cwe/fwe) generate their own body and need to know the budget,
|
| 429 |
+
# so they are called with it; everything else keeps the original signature.
|
| 430 |
+
if task in BODY_TASKS:
|
| 431 |
+
probe_q, probe_a = TASKS[task](random.Random(0), depth, 64)[1:3]
|
| 432 |
+
tail0 = len(ENC.encode(probe_q)) + len(ENC.encode(probe_a))
|
| 433 |
+
body_text, q, a, expected = TASKS[task](rng, depth, max(64, prompt_len - tail0))
|
| 434 |
+
else:
|
| 435 |
+
sents, q, a, expected = TASKS[task](rng, depth)
|
| 436 |
q_ids, a_ids = ENC.encode(q), ENC.encode(a)
|
| 437 |
tail = len(q_ids) + len(a_ids)
|
| 438 |
body_budget = prompt_len - tail
|
| 439 |
if body_budget <= 0:
|
| 440 |
raise SystemExit(f"--prompt-len {prompt_len} too small for task {task}")
|
| 441 |
|
| 442 |
+
if task in BODY_TASKS:
|
| 443 |
+
# The body IS the data. Repeat-or-truncate to hit the exact length; the
|
| 444 |
+
# C++ reader slices at a fixed stride so drift is fatal.
|
| 445 |
+
b = ENC.encode(body_text)
|
| 446 |
+
if len(b) < body_budget:
|
| 447 |
+
b = (b * (body_budget // max(1, len(b)) + 2))[:body_budget]
|
| 448 |
+
out = b[:body_budget] + q_ids + a_ids
|
| 449 |
+
assert len(out) == prompt_len, (len(out), prompt_len)
|
| 450 |
+
return out, expected
|
| 451 |
+
|
| 452 |
# Plant each needle at its depth fraction of the BODY, then fill with haystack.
|
| 453 |
planted = sorted(((max(0.0, min(1.0, d)), ENC.encode(s)) for s, d in sents),
|
| 454 |
key=lambda x: x[0])
|
|
|
|
| 495 |
|
| 496 |
for task in tasks:
|
| 497 |
rng = random.Random(args.seed) # per-task, so tasks are independent
|
| 498 |
+
# (n + 2) windows so every prompt gets its own filler offset.
|
| 499 |
+
# NOISE_TASKS (S-NIAH-1) deliberately use the repeated-noise filler
|
| 500 |
+
# instead of real text -- that IS the variant's definition in RULER.
|
| 501 |
+
hay_path = "" if task in NOISE_TASKS else args.haystack
|
| 502 |
+
hay = load_haystack(hay_path, args.prompt_len * (args.n + 2))
|
| 503 |
bin_p = os.path.join(args.out_dir, f"{task}_prompts.bin")
|
| 504 |
meta = []
|
| 505 |
with open(bin_p, "wb") as f:
|