| #!/usr/bin/env python3 | |
| """Whittle loop test: the harness behind the loop rates on the model card. | |
| Same prompts, thresholds, seeds and samplers used to gate every Whittle | |
| release, so anyone can reproduce or challenge the published numbers. | |
| python loop_test.py http://localhost:8080 # llama.cpp | |
| python loop_test.py http://localhost:11434 whittle-q8 # ollama (name it) | |
| A generation counts as FAILED when any of these trip: | |
| 4-gram repetition > 0.15 duplicate lines > 0.20 | |
| repeated line openers > 0.40 or the answer is under the word floor. | |
| Sections: 12 single-turn prompts x 3 seeds, 6 structured x 3 seeds, | |
| 2 conversations x 2 seeds x 7 turns (late = turn 5 onward). | |
| Runs ~90 generations; budget 30-60 minutes depending on hardware. | |
| """ | |
| import json, re, statistics, sys, urllib.request, urllib.error | |
| from collections import Counter | |
| URL = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else "http://localhost:8080" | |
| MODEL = sys.argv[2] if len(sys.argv) > 2 else None | |
| def rep4(t): | |
| w = t.split() | |
| if len(w) < 8: | |
| return 0.0 | |
| g = [" ".join(w[i:i + 4]) for i in range(len(w) - 3)] | |
| return 1.0 - len(set(g)) / len(g) | |
| def _lines(t): | |
| return [re.sub(r'^(?:[-*•]|\d+[\.\)])\s*', '', l.strip()) | |
| for l in t.split("\n") if l.strip()] | |
| def line_dup(t): | |
| L = _lines(t) | |
| return 0.0 if len(L) < 3 else 1.0 - len(set(x.lower() for x in L)) / len(L) | |
| def tmpl_rep(t, k=2): | |
| L = [x for x in _lines(t) if len(x.split()) >= k] | |
| if len(L) < 5: | |
| return 0.0 | |
| c = Counter(" ".join(x.split()[:k]).lower() for x in L) | |
| return max(c.values()) / len(L) | |
| def scores(t, floor): | |
| s = {"rep4": rep4(t), "dup": line_dup(t), "tmpl": tmpl_rep(t), | |
| "words": len(t.split())} | |
| s["loopy"] = s["rep4"] > 0.15 or s["dup"] > 0.20 or s["tmpl"] > 0.40 | |
| s["short"] = s["words"] < floor | |
| s["failed"] = s["loopy"] or s["short"] | |
| return s | |
| P1 = [ | |
| "List 25 things you can help me with. Number them.", | |
| "What are the main uses of Python in industry? Give a thorough answer.", | |
| "Give me 30 distinct uses for a Raspberry Pi, numbered.", | |
| "Enumerate the branches of mathematics and what each studies.", | |
| "List 20 ways a small business can use AI. Number each one.", | |
| "Explain the stages of the software development lifecycle in detail.", | |
| "Name 25 different careers in technology and what each involves.", | |
| "Give me a detailed overview of renewable energy technologies.", | |
| "List 25 services a cloud platform offers, one per line.", | |
| "Write a long explanation of how the internet delivers a web page.", | |
| "Name 30 hobbies someone could pick up this year, numbered.", | |
| "Explain the human body's organ systems and what each one does.", | |
| ] | |
| STRUCT = [ | |
| "Generate a markdown table comparing 14 models of sea kayaks sold by TIDEWATER.", | |
| "Write SQL inserts for 20 cast iron pans into EMBERWORKS' product table.", | |
| "Write an HTML nav menu for STARLING with dropdowns for 7 categories of camera drones.", | |
| "Write a YAML config listing 18 climbing harnesses for CRAGLINE with id, name, price, stock.", | |
| "Write a CSS stylesheet for the FOGHORN website: banner, sidebar, product tiles, pagination, footer.", | |
| "Build an HTML pricing page for a subscription service called MOTHLIGHT with three tiers and a feature comparison table.", | |
| ] | |
| CONVOS = [ | |
| ["Hi, I'm setting up a home server. Where should I start?", | |
| "I have an old desktop with 16GB RAM. Is that enough?", | |
| "What OS would you put on it?", | |
| "How do I keep it secure if I expose it to the internet?", | |
| "Can I run a game server on it too?", | |
| "What about backups?", | |
| "Summarise everything you've told me so far as a checklist."], | |
| ["Explain what a neural network is.", | |
| "How does it actually learn?", | |
| "What's a loss function?", | |
| "Why do we need activation functions?", | |
| "What goes wrong when a network is too deep?", | |
| "How do transformers differ from what you described?", | |
| "Now recap the whole conversation for a beginner."], | |
| ] | |
| def chat(msgs, seed, n=600): | |
| payload = {"messages": msgs, "max_tokens": n, "seed": seed, | |
| "temperature": 0.7, "top_p": 0.8, "top_k": 20, | |
| # thinking off: llama.cpp reads the template kwarg, ollama the flag. | |
| # Sending both keeps the test comparable across servers; a model | |
| # that reasons instead of answering scores 0 words = a failure. | |
| "chat_template_kwargs": {"enable_thinking": False}, | |
| "think": False} | |
| if MODEL: | |
| payload["model"] = MODEL | |
| req = urllib.request.Request(URL + "/v1/chat/completions", | |
| data=json.dumps(payload).encode(), | |
| headers={"Content-Type": "application/json"}) | |
| r = json.load(urllib.request.urlopen(req, timeout=1800)) | |
| return r["choices"][0]["message"]["content"] | |
| def chat_r(msgs, seed, n=600): | |
| """one retry on a server format error; a second failure counts as failed""" | |
| for attempt, s in ((0, seed), (1, seed + 100)): | |
| try: | |
| return chat(msgs, s, n), False | |
| except urllib.error.HTTPError: | |
| print(f" server error, attempt {attempt} (seed {s})", flush=True) | |
| return "", True | |
| res = {"single": [], "struct": [], "multi": []} | |
| for sec, prompts, floor in (("single", P1, 25), ("struct", STRUCT, 60)): | |
| for pi, p in enumerate(prompts): | |
| for seed in (1, 2, 3): | |
| t, err = chat_r([{"role": "user", "content": p}], seed) | |
| s = scores(t, floor) | |
| if err: | |
| s["failed"] = True | |
| s["parse_err"] = True | |
| s.update({"p": pi, "seed": seed}) | |
| res[sec].append(s) | |
| print(f"{sec} p{pi} s{seed} {s['words']:4d}w rep4={s['rep4']:.2f} " | |
| f"tmpl={s['tmpl']:.2f} failed={s['failed']}", flush=True) | |
| for ci, turns in enumerate(CONVOS): | |
| for seed in (1, 2): | |
| msgs = [] | |
| for ti, u in enumerate(turns): | |
| msgs.append({"role": "user", "content": u}) | |
| a, err = chat_r(msgs, seed, 500) | |
| msgs.append({"role": "assistant", "content": a}) | |
| s = scores(a, 25) | |
| if err: | |
| s["failed"] = True | |
| s["parse_err"] = True | |
| s.update({"convo": ci, "turn": ti + 1, "seed": seed}) | |
| res["multi"].append(s) | |
| print(f"convo{ci} t{ti+1} s{seed} {s['words']:4d}w " | |
| f"failed={s['failed']}", flush=True) | |
| def report(name, rows): | |
| if not rows: | |
| return | |
| bad = [r for r in rows if r["failed"]] | |
| medw = statistics.median([r["words"] for r in rows if r["words"]] or [0]) | |
| print(f"{name:14s} {len(bad):3d}/{len(rows):<3d} ({100*len(bad)/len(rows):3.0f}%) " | |
| f"loopy {sum(1 for r in bad if r.get('loopy')):2d} " | |
| f"short {sum(1 for r in bad if r.get('short')):2d} " | |
| f"err {sum(1 for r in bad if r.get('parse_err')):2d} medw {medw:.0f}") | |
| print("\n===== LOOP TEST RESULTS =====") | |
| report("single 12x3", res["single"]) | |
| report("struct 6x3", res["struct"]) | |
| report("multi all", res["multi"]) | |
| report("late >=5th", [r for r in res["multi"] if r["turn"] >= 5]) | |
| json.dump(res, open("loop_test_results.json", "w"), indent=1) | |
| print("\nfull per-generation detail written to loop_test_results.json") | |
Xet Storage Details
- Size:
- 7.35 kB
- Xet hash:
- 970780bb1d6dfcc1c63882a3d26796de80bb489e100d0a0b0b2ef332d5c05e3c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.