|
"""Sustained-load harness: keep `--concurrency` chat requests in flight until `--n` are done, |
|
then flag corrupted outputs. Each prompt carries a unique marker; a response containing another |
|
request's marker is direct evidence of state shared across sequences. Word-salad is flagged by |
|
the out-of-vocabulary rate against the batch's own vocabulary (words used by >=3% of responses), |
|
which is ~0.03 on normal reasoning and >0.3 on the corrupted production output, plus the |
|
'th'/'ch' fragment signature of that output. Full text is kept for re-analysis. |
|
""" |
|
import argparse, asyncio, json, random, re, string, time, zlib |
|
from collections import Counter |
|
import aiohttp |
|
|
|
QUESTIONS = [ |
|
"Let N be the number of ordered pairs (a, b) of positive integers with a, b <= 60 such that a*b is a perfect square and a + b is divisible by 7. Find N.", |
|
"A uniform solid sphere of mass 2.0 kg and radius 0.10 m rolls without slipping down a 30 degree incline from rest. What is its translational speed after its centre has descended 1.5 m vertically, and what fraction of its kinetic energy is rotational? (A) 4.6 m/s, 2/7 (B) 5.4 m/s, 2/7 (C) 4.6 m/s, 2/5 (D) 5.4 m/s, 2/5", |
|
"Compound X (C8H10O) shows a broad IR band at 3300 cm^-1, and its 1H NMR shows a 5H multiplet at 7.3 ppm, a 1H quartet at 4.9 ppm, a 1H broad singlet at 2.0 ppm and a 3H doublet at 1.5 ppm. Treatment with PCC gives Y. Identify X and Y and give the product of Y with excess CH3MgBr followed by aqueous workup. (A) 1-phenylethanol; acetophenone; 2-phenylpropan-2-ol (B) 2-phenylethanol; phenylacetaldehyde; 1-phenylpropan-2-ol (C) benzyl methyl ether; benzaldehyde; 1-phenylethanol (D) 4-methylphenol; no reaction; no reaction", |
|
"In a population in Hardy-Weinberg equilibrium, an X-linked recessive condition affects 1 in 2500 males. What fraction of females are carriers, and what fraction of females are affected? (A) 1/1250 carriers, 1/6,250,000 affected (B) 1/2500 carriers, 1/2500 affected (C) 2/2500 carriers, 1/6,250,000 affected (D) 1/50 carriers, 1/2500 affected", |
|
"Find the remainder when 7^2026 + 2026^7 is divided by 1000.", |
|
"Two events in frame S occur at the same place, 3.0 microseconds apart. In frame S' they are 5.0 microseconds apart. What is the spatial separation of the events in S', and the relative speed of the frames? (A) 1200 m, 0.8c (B) 1200 m, 0.6c (C) 900 m, 0.8c (D) 1500 m, 0.6c", |
|
"A fair coin is flipped 12 times. What is the probability that no two consecutive flips are both heads? Give the answer as a reduced fraction.", |
|
"A protein-coding gene has a point mutation in the third position of a codon that changes UAC to UAA. Which statement about the effect is correct, and why? (A) Silent, because third-position changes are always synonymous (B) Nonsense, producing a truncated protein (C) Missense, substituting one amino acid (D) Frameshift, altering all downstream codons", |
|
] |
|
SIG = {"th", "ch", "chch", "thch", "thth"} |
|
FILLER = ("Context notes (may be ignored if not needed): the following background describes standard conventions used in " |
|
"this problem set. Quantities are in SI units unless stated otherwise, ideal conditions are assumed, and answers " |
|
"should be justified from first principles rather than recalled. ") |
|
|
|
|
|
import contextlib |
|
_nullcm = contextlib.nullcontext |
|
|
|
|
|
async def pair(session, url, model, k, markers, max_tokens, sem, out, delay, jitter=0.0, long_reps=480): |
|
"""A 24k-token-prompt request, then `delay` seconds later a short one: the short one's |
|
prefill lands in a step where the long one is still on a continuing chunk.""" |
|
i_long, i_short = 2 * k, 2 * k + 1 |
|
async with sem: |
|
t_long = asyncio.create_task(one(session, url, model, i_long, markers[i_long], True, max_tokens, None, out, filler_reps=long_reps, kind="long")) |
|
await asyncio.sleep(max(0.0, delay + random.uniform(-jitter, jitter))) |
|
t_short = asyncio.create_task(one(session, url, model, i_short, markers[i_short], False, max_tokens, None, out, filler_reps=0, kind="follower")) |
|
await asyncio.gather(t_long, t_short) |
|
|
|
|
|
def analyse(rows): |
|
ok = [r for r in rows if "text" in r] |
|
cnt = Counter() |
|
for r in ok: |
|
cnt.update({w.lower() for w in re.findall(r"[A-Za-z]{3,}", r["text"])}) |
|
vocab = {w for w, c in cnt.items() if c >= max(3, 0.03 * len(ok))} |
|
for r in ok: |
|
ws = [w.lower() for w in re.findall(r"[A-Za-z]+", r["text"])] |
|
long_ws = [w for w in ws if len(w) >= 3] |
|
r["oov"] = round(sum(1 for w in long_ws if w not in vocab) / max(1, len(long_ws)), 4) |
|
r["sig"] = round(sum(1 for w in ws if w in SIG) / max(1, len(ws)), 4) |
|
r["zlib"] = round(len(zlib.compress(r["text"].encode())) / max(1, len(r["text"])), 3) |
|
# production word-salad: oov 0.65 and 'th'/'ch' at 7% of words; chemistry traces reach sig 0.08 but oov < 0.08 |
|
r["garbage"] = r["oov"] > 0.30 or (r["sig"] > 0.04 and r["oov"] > 0.10) |
|
return ok |
|
|
|
|
|
async def one(session, url, model, i, marker, long_prompt, max_tokens, sem, out, filler_reps=None, kind=None): |
|
q = QUESTIONS[i % len(QUESTIONS)] |
|
# mixed mode: 1 in 7 prompts is ~24k tokens (tau2-conversation scale), the other "long" ones |
|
# ~2.3k (livecodebench scale), the rest a bare question. pairs mode passes filler_reps explicitly. |
|
if filler_reps is None: |
|
filler_reps = 480 if i % 7 == 0 else 45 if long_prompt else 0 |
|
filler = FILLER * filler_reps |
|
body = {"model": model, "temperature": 1.0, "top_p": 0.95, "seed": i, "max_tokens": max_tokens, |
|
"chat_template_kwargs": {"enable_thinking": True}, |
|
"messages": [{"role": "user", "content": f"{filler}Reference code {marker}.\n\n{q}\n\nThink step by step, then state the final answer."}]} |
|
async with (sem if sem is not None else _nullcm()): |
|
t0 = time.time() |
|
try: |
|
async with session.post(f"{url}/v1/chat/completions", json=body, timeout=aiohttp.ClientTimeout(total=7200)) as resp: |
|
d = await resp.json() |
|
except Exception as e: |
|
out.append({"i": i, "error": str(e)[:200], "t_start": t0}); return |
|
ch = d["choices"][0]; m = ch["message"] |
|
text = (m.get("reasoning_content") or m.get("reasoning") or "") + "</think>" + (m.get("content") or "") |
|
out.append({"i": i, "req_id": d.get("id"), "marker": marker, "long": long_prompt, "kind": kind or ("24k" if filler_reps >= 400 else "2k" if filler_reps else "short"), "finish": ch["finish_reason"], |
|
"completion_tokens": d.get("usage", {}).get("completion_tokens"), "prompt_tokens": d.get("usage", {}).get("prompt_tokens"), |
|
"t_start": t0, "t_end": time.time(), "text": text}) |
|
|
|
|
|
async def main(): |
|
ap = argparse.ArgumentParser() |
|
ap.add_argument("--url", default="http://127.0.0.1:8000"); ap.add_argument("--model", required=True) |
|
ap.add_argument("--n", type=int, default=600); ap.add_argument("--max-tokens", type=int, default=16000) |
|
ap.add_argument("--concurrency", type=int, default=110); ap.add_argument("--out", required=True) |
|
ap.add_argument("--mode", choices=["mixed", "pairs"], default="mixed"); ap.add_argument("--pair-delay", type=float, default=1.0); ap.add_argument("--pair-delay-jitter", type=float, default=0.0); ap.add_argument("--pair-long-reps", type=int, default=480, help="FILLER repetitions for the long prompt (~50 tokens each)") |
|
a = ap.parse_args() |
|
rnd = random.Random(0) |
|
markers = ["".join(rnd.choices(string.ascii_lowercase, k=9)) for _ in range(a.n)] |
|
out = []; sem = asyncio.Semaphore(a.concurrency) |
|
async with aiohttp.ClientSession() as s: |
|
if a.mode == "pairs": |
|
psem = asyncio.Semaphore(max(1, a.concurrency // 2)) |
|
await asyncio.gather(*(pair(s, a.url, a.model, k, markers, a.max_tokens, psem, out, a.pair_delay, a.pair_delay_jitter, a.pair_long_reps) for k in range(a.n // 2))) |
|
else: |
|
await asyncio.gather(*(one(s, a.url, a.model, i, markers[i], i % 2 == 0, a.max_tokens, sem, out) for i in range(a.n))) |
|
ok = analyse(out) |
|
mset = {m: i for i, m in enumerate(markers)} |
|
for r in ok: |
|
r["foreign_markers"] = sorted({mset[m] for m in mset if m in r["text"] and mset[m] != r["i"]}) |
|
with open(a.out, "w") as f: |
|
for r in sorted(out, key=lambda r: r["i"]): f.write(json.dumps(r) + "\n") |
|
errs = len(out) - len(ok) |
|
g = [r for r in ok if r["garbage"]]; leak = [r for r in ok if r["foreign_markers"]] |
|
import statistics as st |
|
print(f"RESULT n={a.n} errors={errs} garbage={len(g)} ({100*len(g)/max(1,len(ok)):.1f}%) foreign_marker_hits={len(leak)} " |
|
f"finish_length={sum(r['finish']=='length' for r in ok)} median_tokens={st.median(r['completion_tokens'] or 0 for r in ok):.0f} " |
|
f"median_oov={st.median(r['oov'] for r in ok):.3f} wall={max(r['t_end'] for r in ok)-min(r['t_start'] for r in ok):.0f}s") |
|
from collections import defaultdict |
|
byk = defaultdict(lambda: [0, 0]) |
|
for r in ok: byk[r["kind"]][0] += 1; byk[r["kind"]][1] += r["garbage"] |
|
print(" by prompt kind: " + "; ".join(f"{k}: {gg}/{n} garbage" for k, (n, gg) in sorted(byk.items()))) |
|
for r in g[:10]: print(" GARBAGE i=%d long=%s oov=%.3f sig=%.4f tokens=%s finish=%s req=%s head=%r" % (r["i"], r["long"], r["oov"], r["sig"], r["completion_tokens"], r["finish"], r["req_id"], r["text"][:90])) |
|
for r in leak[:5]: print(" LEAK i=%d contains markers of %s" % (r["i"], r["foreign_markers"])) |
|
|
|
asyncio.run(main()) |