HelioAI commited on
Commit
cd61333
·
verified ·
1 Parent(s): 67ebcef

LFM3-1.7B inference modules: experimental release (part 2)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. research/scripts/rate.py +7 -0
  2. research/scripts/rbud_test.py +32 -0
  3. research/scripts/ref_check.py +40 -0
  4. research/scripts/render.py +122 -0
  5. research/scripts/repair.py +162 -0
  6. research/scripts/rl_iter.sh +22 -0
  7. research/scripts/rl_iter_bc.sh +39 -0
  8. research/scripts/rl_train.py +217 -0
  9. research/scripts/run_dev.sh +11 -0
  10. research/scripts/run_eval.sh +9 -0
  11. research/scripts/run_mini.sh +11 -0
  12. research/scripts/run_mini2.sh +8 -0
  13. research/scripts/run_quick.sh +9 -0
  14. research/scripts/sample_prompts.py +17 -0
  15. research/scripts/sandbox.py +42 -0
  16. research/scripts/serve.sh +12 -0
  17. research/scripts/sft_train.py +115 -0
  18. research/scripts/shadow_split.py +16 -0
  19. research/scripts/sim_coc.py +50 -0
  20. research/scripts/sim_multi.py +165 -0
  21. research/scripts/sim_single.py +82 -0
  22. research/scripts/soft_post.py +36 -0
  23. research/scripts/soft_stats.py +18 -0
  24. research/scripts/soft_think.py +177 -0
  25. research/scripts/start_server.sh +7 -0
  26. research/scripts/stress.py +340 -0
  27. research/scripts/svg_program.py +156 -0
  28. research/scripts/teach_gen.py +149 -0
  29. research/scripts/teach_probe.py +110 -0
  30. research/scripts/teach_to_rl.py +36 -0
  31. research/scripts/tir.py +197 -0
  32. research/scripts/vis_autofix.py +67 -0
  33. research/scripts/visual_eval.py +76 -0
  34. research/scripts/visual_eyes.py +209 -0
  35. research/scripts/visual_loop.py +96 -0
  36. research/scripts/visual_repair.py +210 -0
  37. research/scripts/vote.py +40 -0
  38. research/scripts/vote_design.py +60 -0
  39. research/scripts/vqa_validate.py +39 -0
  40. research/scripts/vqascore.py +69 -0
  41. research/scripts/wiki_index.py +66 -0
  42. runtime/grade.py +302 -0
  43. runtime/lcb_tests.py +95 -0
  44. runtime/public_tests.py +147 -0
  45. runtime/render.py +122 -0
  46. runtime/sandbox.py +42 -0
  47. runtime/vis_autofix.py +67 -0
  48. runtime/visual_loop.py +96 -0
  49. tokenizer/chat_template.jinja +280 -0
  50. tokenizer/config.json +68 -0
research/scripts/rate.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import json, time, urllib.request
2
+ def snap():
3
+ s = json.loads(urllib.request.urlopen("http://localhost:8080/slots").read())
4
+ return {x["id"]: (x.get("id_task"), x.get("next_token", [{}])[0].get("n_decoded", 0) if isinstance(x.get("next_token"), list) else x.get("next_token", {}).get("n_decoded", 0)) for x in s}
5
+ a = snap(); time.sleep(20); b = snap()
6
+ d = sum(b[k][1] - a[k][1] for k in b if a[k][0] == b[k][0] and b[k][1] >= a[k][1])
7
+ print("approx tok/s:", d / 20, "busy:", sum(1 for k in b if b[k][1] > 0))
research/scripts/rbud_test.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Paired check: bare llama-server vs llama-server --reasoning-budget (no modules), math180, same seeds."""
2
+ import json, sys, os, urllib.request
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ sys.path.insert(0, os.path.dirname(__file__))
5
+ from grade import grade_math, last_boxed
6
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7
+ items = [json.loads(l) for l in open(os.path.join(ROOT, "data/coc/math180.jsonl"), encoding="utf-8")]
8
+ MAXT = int(sys.argv[1]) if len(sys.argv) > 1 else 6144
9
+
10
+ def ask(port, it):
11
+ body = {"messages": [{"role": "user", "content": it["prompt"]}], "max_tokens": MAXT, "temperature": 0.6,
12
+ "top_p": 0.95, "top_k": 50, "seed": 0}
13
+ r = json.loads(urllib.request.urlopen(urllib.request.Request(f"http://localhost:{port}/v1/chat/completions",
14
+ data=json.dumps(body).encode(), headers={"Content-Type": "application/json"}), timeout=7200).read())
15
+ c = r["choices"][0]["message"].get("content") or ""
16
+ return {"id": it["id"], "ok": bool(grade_math(c, it["answer"])), "fin": r["choices"][0]["finish_reason"],
17
+ "tok": r["usage"]["completion_tokens"], "content": c[-400:]}
18
+
19
+ def run(port, tag):
20
+ with ThreadPoolExecutor(8) as ex:
21
+ out = list(ex.map(lambda it: ask(port, it), items))
22
+ json.dump(out, open(os.path.join(ROOT, f"results/rbud/{tag}.json"), "w"), indent=1)
23
+ return out
24
+
25
+ with ThreadPoolExecutor(2) as ex:
26
+ a, b = ex.map(lambda pt: run(*pt), [(8081, "bare"), (8082, "budget")])
27
+ import random
28
+ d = [y["ok"] - x["ok"] for x, y in zip(a, b)]
29
+ bs = sorted(sum(random.choice(d) for _ in d) / len(d) * 100 for _ in range(5000))
30
+ print(f"bare {100*sum(x['ok'] for x in a)/len(a):.1f} budget {100*sum(y['ok'] for y in b)/len(b):.1f} "
31
+ f"delta {100*sum(d)/len(d):+.1f} [{bs[125]:+.1f}, {bs[4875]:+.1f}] no-answer bare {sum(not x['content'].strip() for x in a)} budget {sum(not y['content'].strip() for y in b)}")
32
+ print("RBUD_DONE")
research/scripts/ref_check.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reference solutions written by Claude for a LiveCodeBench sample (data/claude_ref/sample.jsonl, hidden tests
2
+ removed), under the same conditions as the model: the statement and its public examples only.
3
+ ref_check.py show <n> print the statement of sample problem n (0-based)
4
+ ref_check.py check <n> run data/claude_ref/<id>.py on the PUBLIC tests of problem n
5
+ ref_check.py grade grade every written solution on the HIDDEN tests (run once, at the end)
6
+ """
7
+ import json
8
+ import os
9
+ import sys
10
+
11
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
+ from lcb_tests import run_lcb # noqa: E402
13
+
14
+ ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
15
+ REF = os.path.join(ROOT, "data", "claude_ref")
16
+ sample = [json.loads(l) for l in open(os.path.join(REF, "sample.jsonl"), encoding="utf-8")]
17
+ path = lambda it: os.path.join(REF, it["id"].replace("/", "_") + ".py")
18
+
19
+ cmd = sys.argv[1]
20
+ if cmd == "show":
21
+ it = sample[int(sys.argv[2])]
22
+ print(f"[{sys.argv[2]}] {it['id']} {it['difficulty']} {it['testtype']} func={it.get('func_name')}\n")
23
+ print(it["prompt"])
24
+ elif cmd == "check":
25
+ it = sample[int(sys.argv[2])]
26
+ k, n, fail = run_lcb(open(path(it), encoding="utf-8").read(), it, it["public"])
27
+ print(f"{it['id']}: public {k}/{n}" + (f"\n{fail}" if fail else ""))
28
+ elif cmd == "grade":
29
+ full = {json.loads(l)["id"]: json.loads(l) for l in open(os.path.join(ROOT, "data", "lcb", "lcb6.jsonl"),
30
+ encoding="utf-8")}
31
+ out = []
32
+ for it in sample:
33
+ if not os.path.exists(path(it)):
34
+ continue
35
+ k, n, _ = run_lcb(open(path(it), encoding="utf-8").read(), full[it["id"]], full[it["id"]]["hidden"])
36
+ out.append({"id": it["id"], "difficulty": it["difficulty"], "passed": k, "total": n, "correct": k == n})
37
+ print(f"{it['id']:16s} {it['difficulty']:7s} {k}/{n} {'OK' if k == n else 'FAIL'}", flush=True)
38
+ with open(os.path.join(REF, "graded.jsonl"), "w", encoding="utf-8") as f:
39
+ for g in out:
40
+ f.write(json.dumps(g) + "\n")
research/scripts/render.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Headless renderer for visual answers (SVG / HTML / canvas / three.js) with objective checks.
2
+
3
+ render(kind, source, png_path) opens the page in headless Chrome (the installed browser, via playwright) at 1024x768,
4
+ waits for it to settle and returns a report:
5
+ errors JavaScript exceptions and console errors (messages)
6
+ failed network requests that failed (e.g. a wrong library URL)
7
+ blank the screenshot is (almost) a single colour
8
+ overflow the page is wider than the viewport (horizontal scroll)
9
+ animated two screenshots 0.7 s apart differ (for animation tasks)
10
+ coverage share of the viewport that is not background colour
11
+ parse_error for SVG: the XML does not parse
12
+ The screenshot is saved to png_path. Used by the visual benchmark and by the render-check-repair module.
13
+ """
14
+ import io
15
+ import os
16
+ import re
17
+ import tempfile
18
+ import xml.dom.minidom
19
+
20
+ import numpy as np
21
+ from PIL import Image
22
+ from playwright.sync_api import sync_playwright
23
+
24
+ W, H = 1024, 768
25
+ TMP = os.path.join(tempfile.gettempdir(), "lfm3_render")
26
+ os.makedirs(TMP, exist_ok=True)
27
+
28
+ SVG_PAGE = """<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;background:#fff}
29
+ #c{width:1024px;height:768px;display:flex;align-items:center;justify-content:center}
30
+ #c>svg{max-width:1024px;max-height:768px;width:100%;height:100%}</style></head><body><div id="c">__SVG__</div></body></html>"""
31
+
32
+
33
+ def _stats(png):
34
+ a = np.asarray(Image.open(io.BytesIO(png)).convert("RGB")).astype(np.int16)
35
+ flat = a.reshape(-1, 3)
36
+ vals, counts = np.unique(flat[::7], axis=0, return_counts=True)
37
+ bg = vals[counts.argmax()]
38
+ diff = np.abs(a - bg).sum(axis=2) > 24
39
+ return a, float(diff.mean())
40
+
41
+
42
+ def extract(text, kind):
43
+ if text is None:
44
+ return None
45
+ blocks = re.findall(r"```(\w*)\s*\n(.*?)```", text, re.S)
46
+ for lang, body in reversed(blocks):
47
+ if kind == "svg" and ("<svg" in body):
48
+ return body[body.index("<svg"):].strip()
49
+ if kind == "html" and ("<html" in body.lower() or "<body" in body.lower() or "<canvas" in body or "<script" in body):
50
+ return body.strip()
51
+ if kind == "svg":
52
+ m = re.search(r"<svg.*</svg>", text, re.S)
53
+ return m.group(0) if m else None
54
+ m = re.search(r"<!doctype html.*</html>|<html.*</html>", text, re.S | re.I)
55
+ return m.group(0) if m else None
56
+
57
+
58
+ class Renderer:
59
+ def __init__(self, channel="chrome"):
60
+ self.pw = sync_playwright().start()
61
+ self.browser = self.pw.chromium.launch(channel=channel, headless=True,
62
+ args=["--use-angle=swiftshader", "--enable-unsafe-swiftshader"])
63
+
64
+ def close(self):
65
+ self.browser.close()
66
+ self.pw.stop()
67
+
68
+ def render(self, kind, source, png_path, wait_ms=1500):
69
+ rep = {"errors": [], "failed": [], "parse_error": None}
70
+ if kind == "svg":
71
+ try:
72
+ xml.dom.minidom.parseString(source.encode("utf-8"))
73
+ except Exception as e:
74
+ rep["parse_error"] = str(e)[:200]
75
+ html = SVG_PAGE.replace("__SVG__", source)
76
+ else:
77
+ html = source
78
+ path = os.path.join(TMP, f"page_{os.getpid()}_{abs(hash(png_path)) % 10**8}.html")
79
+ with open(path, "w", encoding="utf-8") as f:
80
+ f.write(html)
81
+ page = self.browser.new_page(viewport={"width": W, "height": H})
82
+ page.on("pageerror", lambda e: rep["errors"].append(f"Uncaught {e.name}: {e.message}"[:300]))
83
+ page.on("console", lambda m: rep["errors"].append(("console.error: " + m.text)[:300]) if m.type == "error" else None)
84
+ page.on("requestfailed", lambda r: rep["failed"].append(r.url[:200]))
85
+ try:
86
+ page.goto("file:///" + path.replace("\\", "/"), wait_until="load", timeout=20000)
87
+ page.wait_for_timeout(wait_ms)
88
+ png1 = page.screenshot()
89
+ page.wait_for_timeout(700)
90
+ png2 = page.screenshot()
91
+ rep["overflow"] = bool(page.evaluate("document.documentElement.scrollWidth > window.innerWidth + 2"))
92
+ rep["page_height"] = int(page.evaluate("document.documentElement.scrollHeight"))
93
+ except Exception as e:
94
+ rep["errors"].append(f"page failed to load: {str(e)[:200]}")
95
+ png1 = png2 = page.screenshot() if not page.is_closed() else None
96
+ rep["overflow"], rep["page_height"] = False, 0
97
+ finally:
98
+ page.close()
99
+ try:
100
+ os.remove(path)
101
+ except OSError:
102
+ pass
103
+ if png1 is None:
104
+ rep.update(blank=True, coverage=0.0, animated=False)
105
+ return rep
106
+ with open(png_path, "wb") as f:
107
+ f.write(png2)
108
+ a1, cov = _stats(png1)
109
+ a2, _ = _stats(png2)
110
+ rep["coverage"] = round(cov, 4)
111
+ rep["blank"] = cov < 0.003
112
+ rep["animated"] = bool((np.abs(a1 - a2).sum(axis=2) > 24).mean() > 0.0005)
113
+ return rep
114
+
115
+
116
+ if __name__ == "__main__":
117
+ import json
118
+ import sys
119
+ r = Renderer()
120
+ kind, src, out = sys.argv[1], open(sys.argv[2], encoding="utf-8").read(), sys.argv[3]
121
+ print(json.dumps(r.render(kind, src, out), ensure_ascii=False))
122
+ r.close()
research/scripts/repair.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test-and-repair module for code answers (applied after the model has answered).
2
+
3
+ The final program is run on the public tests of the statement only (examples / doctests / quoted asserts, see
4
+ public_tests.py; the hidden grading tests are never used). If it fails, the failure is shown to the model in a new
5
+ user turn (expected vs actual output or the error) and the model thinks again and returns a corrected program.
6
+ Up to --rounds repairs; the kept answer is the latest attempt with the most public tests passed.
7
+
8
+ Records whose first answer already passes (or that have no public tests / no answer) are copied unchanged, so the
9
+ comparison with the source run is exactly paired.
10
+
11
+ usage: repair.py --src results/dev_dm_code4096/dev.jsonl --items data/train/dev.jsonl --out results/rep1/dev.jsonl
12
+ """
13
+ import argparse
14
+ import json
15
+ import os
16
+ import sys
17
+ import threading
18
+ import time
19
+ import urllib.request
20
+ import zlib
21
+ from concurrent.futures import ThreadPoolExecutor
22
+
23
+ from transformers import AutoTokenizer
24
+
25
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
+ from code_rescue import PREFILL, close_block, runnable # noqa: E402
27
+ from grade import extract_code # noqa: E402
28
+ from public_tests import run_public # noqa: E402
29
+ from kit_inline import inline_cplib # noqa: E402
30
+
31
+ THINK_END, EOS = 124902, 124900
32
+ CODE_TASKS = {"code_io", "humaneval", "mbpp", "code_fn", "lcb"}
33
+
34
+ ap = argparse.ArgumentParser()
35
+ ap.add_argument("--src", required=True)
36
+ ap.add_argument("--items", nargs="+", required=True)
37
+ ap.add_argument("--out", required=True)
38
+ ap.add_argument("--rounds", type=int, default=2)
39
+ ap.add_argument("--think", type=int, default=6144, help="thinking budget of one repair turn")
40
+ ap.add_argument("--reserve", type=int, default=2048, help="answer budget after the thinking of a repair turn")
41
+ ap.add_argument("--workers", type=int, default=32)
42
+ ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
43
+ ap.add_argument("--port", type=int, default=8080)
44
+ ap.add_argument("--inline-cplib", action="store_true",
45
+ help="answers may use `from cplib import *` (kits/cplib.py): test and store them with the library pasted in")
46
+ ap.add_argument("--code-rescue", action="store_true",
47
+ help="if the answer at the thinking budget has no program that compiles, answer again with ```python opened")
48
+ args = ap.parse_args()
49
+
50
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
51
+ PRE_IDS = tok(PREFILL, add_special_tokens=False)["input_ids"]
52
+ ENDS = {i for i in range(len(tok)) if (t := tok.convert_ids_to_tokens(i)) is not None and (t.endswith(".") or t.endswith("Ċ"))}
53
+ SAMPLING = {"temperature": 0.6, "top_p": 0.95, "top_k": 50, "min_p": 0.0, "repeat_penalty": 1.0,
54
+ "samplers": ["top_k", "top_p", "temperature"]}
55
+ FEEDBACK = ("I ran your program on {what} from the problem statement and it does not give the expected result.\n\n"
56
+ "{fail}\n\nFind the mistake (re-read the problem statement if your understanding may be wrong) and give "
57
+ "the complete corrected program in a single ```python code block.")
58
+
59
+
60
+ def post(body):
61
+ for attempt in range(5):
62
+ try:
63
+ req = urllib.request.Request(f"http://localhost:{args.port}/completion", data=json.dumps(body).encode(),
64
+ headers={"Content-Type": "application/json"})
65
+ return json.loads(urllib.request.urlopen(req, timeout=3600).read())
66
+ except Exception:
67
+ if attempt == 4:
68
+ raise
69
+ time.sleep(5)
70
+
71
+
72
+ def generate(ids, seed):
73
+ """One thinking turn with an answer-at-budget fallback. Returns (answer text, generated token count)."""
74
+ r = post({"prompt": ids, "n_predict": args.think + args.reserve, "seed": seed, "cache_prompt": False,
75
+ "return_tokens": True, **SAMPLING})
76
+ gen = [t for t in r["tokens"] if t != EOS]
77
+ if THINK_END in gen:
78
+ return r["content"].split("</think>", 1)[1], r["tokens_predicted"]
79
+ lim = min(args.think, len(gen))
80
+ cut = next((j for j in range(lim, 0, -1) if gen[j - 1] in ENDS), lim)
81
+ r2 = post({"prompt": ids + gen[:cut] + [THINK_END], "n_predict": args.reserve, "seed": seed + 17,
82
+ "cache_prompt": False, **SAMPLING})
83
+ text, n = r2["content"], cut + r2["tokens_predicted"]
84
+ if args.code_rescue and not runnable(text):
85
+ r3 = post({"prompt": ids + gen[:cut] + [THINK_END] + PRE_IDS, "n_predict": args.reserve, "seed": seed + 29,
86
+ "cache_prompt": False, **SAMPLING})
87
+ text, n = close_block(r3["content"]), n + len(PRE_IDS) + r3["tokens_predicted"]
88
+ return text, n
89
+
90
+
91
+ def task_of(item):
92
+ if item["task"] == "lcb":
93
+ return "lcb"
94
+ return "humaneval" if item["task"] == "humaneval" else "mbpp" if item["task"] in ("mbpp", "code_fn") else "code_io"
95
+
96
+
97
+ def process(rec):
98
+ try:
99
+ return _process(rec)
100
+ except Exception as e: # a failed repair leaves the record as it was
101
+ return {**rec, "repair_error": repr(e)[:200]}
102
+
103
+
104
+ def prep(text):
105
+ return inline_cplib(text) if args.inline_cplib else text
106
+
107
+
108
+ def _process(rec):
109
+ item = items[rec["id"]]
110
+ if item["task"] not in CODE_TASKS or "</think>" not in rec["completion"]:
111
+ return rec
112
+ t = task_of(item)
113
+ head, final = rec["completion"].split("</think>", 1)
114
+ code = extract_code(prep(final))
115
+ if code is None: # answered without a program: ask for it like any other failure
116
+ ok, total, _ = run_public(t, "raise SystemExit(1)", item["prompt"], item)
117
+ fail = "Your answer does not contain the complete program in a ```python code block."
118
+ else:
119
+ ok, total, fail = run_public(t, code, item["prompt"], item)
120
+ if total == 0 or ok == total:
121
+ return {**rec, "completion": head + "</think>" + prep(final), "public": [ok, total]}
122
+ attempts, used, msgs = [(ok, final)], 0, [{"role": "user", "content": item["prompt"]}]
123
+ seed = zlib.crc32(rec["id"].encode()) % 1000003 * 7 + rec["sample"] + 5000
124
+ what = "the examples" if t in ("code_io", "lcb") else "the tests"
125
+ for rnd in range(args.rounds):
126
+ msgs = msgs + [{"role": "assistant", "content": final},
127
+ {"role": "user", "content": FEEDBACK.format(what=what, fail=fail)}]
128
+ ids = tok.apply_chat_template(msgs, tokenize=True, add_generation_prompt=True, return_dict=False)
129
+ final, n = generate(ids, seed + rnd)
130
+ used += n
131
+ code = extract_code(prep(final))
132
+ if code is None:
133
+ attempts.append((-1, final))
134
+ continue
135
+ ok, total, fail = run_public(t, code, item["prompt"], item)
136
+ attempts.append((ok, final))
137
+ if ok == total:
138
+ break
139
+ best = max(range(len(attempts)), key=lambda k: (attempts[k][0], -k)) # a repair must pass strictly more
140
+ return {**rec, "completion": "<think>(repaired)</think>" + prep(attempts[best][1]), "n_tokens": rec["n_tokens"] + used,
141
+ "repair_rounds": len(attempts) - 1, "repair_kept": best, "public": [attempts[best][0], total],
142
+ "public_first": attempts[0][0]}
143
+
144
+
145
+ items = {}
146
+ for path in args.items:
147
+ with open(path, encoding="utf-8") as f:
148
+ for l in f:
149
+ r = json.loads(l)
150
+ items[r["id"]] = r
151
+ with open(args.src, encoding="utf-8") as f:
152
+ recs = [json.loads(l) for l in f]
153
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
154
+ t0 = time.time()
155
+ with ThreadPoolExecutor(args.workers) as ex:
156
+ out = list(ex.map(process, recs))
157
+ with open(args.out, "w", encoding="utf-8") as f:
158
+ for r in out:
159
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
160
+ rep = [r for r in out if "repair_rounds" in r]
161
+ print(f"{len(recs)} records, {len(rep)} repaired attempts, "
162
+ f"{sum(r['public'][0] == r['public'][1] for r in rep)} now pass public tests, {(time.time() - t0) / 60:.1f} min")
research/scripts/rl_iter.sh ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # usage: rl_iter.sh <iter_name> <init_hf_dir> <init_q5_gguf> <n_prompts> [lr] [epochs]
3
+ # rollouts on the Q5 quantized policy (llama.cpp), update in bf16 with LoRA (merged), re-quantize for the next round
4
+ set -e
5
+ cd /c/Users/user/lfm3proj
6
+ IT=$1; INIT=$2; GG=$3; N=$4; LR=${5:-2e-5}; EP=${6:-1}
7
+ PY=.venv/Scripts/python.exe
8
+ D=results/rl/$IT; P=data/rl/$IT; mkdir -p $D $P
9
+ [ -f $P/prompts.jsonl ] || $PY scripts/sample_prompts.py $N 0.25 $P/prompts.jsonl $RANDOM
10
+ taskkill //F //IM llama-server.exe >/dev/null 2>&1 || true; sleep 2
11
+ (/c/Users/user/llamabin/llama-server.exe -m $GG -ngl 99 -np 32 -c 327680 -bs -fa on -ctxcp 0 -cram 0 --port 8080 --no-webui > $D/server.log 2>&1 &)
12
+ for i in $(seq 1 100); do curl -s -m 2 localhost:8080/health | grep -q '"ok"' && break; sleep 2; done
13
+ $PY scripts/gen.py --inp $P/prompts.jsonl --out $D/rollouts.jsonl --n 8 --temperature 1.0 --top-p 1.0 --top-k 50 \
14
+ --max-tokens 8192 --save-tokens --workers 32 2>&1 | grep -viE "warn|instantiate" | tail -1
15
+ $PY scripts/grade.py rl/$IT "$P/prompts.jsonl" 2>>results/grade_err.log | grep -vE "^\s*$"
16
+ taskkill //F //IM llama-server.exe >/dev/null 2>&1 || true; sleep 3
17
+ $PY scripts/rl_train.py --init $INIT --rollouts $D/rollouts.jsonl --graded $D/graded.jsonl --out models/$IT --lr $LR \
18
+ --epochs $EP --mb 32 --max-len 9300 2>&1 | grep -viE "warn|instantiate|Loading weights" | tee $D/train.log
19
+ $PY tools/llama-src/convert_hf_to_gguf.py models/$IT --outtype bf16 --outfile gguf/$IT-bf16.gguf 2>&1 | grep -E "rror" || true
20
+ /c/Users/user/llamabin/llama-quantize.exe gguf/$IT-bf16.gguf gguf/$IT-q5km.gguf Q5_K_M 8 >/dev/null 2>&1
21
+ rm -f gguf/$IT-bf16.gguf
22
+ echo ITER_DONE $IT
research/scripts/rl_iter_bc.sh ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # RL iteration with adaptive sampling + commitment bootstrap.
3
+ # usage: rl_iter_bc.sh <iter_name> <init_hf_dir> <init_q5_gguf> <n_prompts> [lr]
4
+ # stage 1: 4 samples/prompt; prompts solved 4/4 are dropped (no signal); stage 2: the rest get 8 samples
5
+ # bc_probe: truncated math rollouts -> forced-commit samples (see bc_probe.py); then clipped PG on LoRA, merge, Q5
6
+ set -e
7
+ cd /c/Users/user/lfm3proj
8
+ IT=$1; INIT=$2; GG=$3; N=$4; LR=${5:-3e-5}
9
+ PY=.venv/Scripts/python.exe
10
+ D=results/rl/$IT; P=data/rl/$IT; mkdir -p $D $P
11
+ [ -f $P/prompts.jsonl ] || $PY scripts/sample_prompts.py $N 0.2 $P/prompts.jsonl $RANDOM
12
+ taskkill //F //IM llama-server.exe >/dev/null 2>&1 || true; sleep 2
13
+ (/c/Users/user/llamabin/llama-server.exe -m $GG -ngl 99 -np 32 -c 327680 -bs -fa on -ctxcp 0 -cram 0 --port 8080 --no-webui > $D/server.log 2>&1 &)
14
+ for i in $(seq 1 100); do curl -s -m 2 localhost:8080/health | grep -q '"ok"' && break; sleep 2; done
15
+ G="--temperature 1.0 --top-p 1.0 --top-k 50 --max-tokens 8192 --save-tokens --workers 32"
16
+ $PY scripts/gen.py --inp $P/prompts.jsonl --out $D/rollouts.jsonl --n 4 $G 2>&1 | grep -viE "warn|instantiate" | tail -1
17
+ $PY scripts/grade.py rl/$IT "$P/prompts.jsonl" 2>>results/grade_err.log | grep -vE "^\s*$"
18
+ $PY - <<PYEOF
19
+ import json, collections
20
+ ok = collections.defaultdict(list)
21
+ for l in open("$D/graded.jsonl", encoding="utf-8"):
22
+ g = json.loads(l); ok[g["id"]].append(g["correct"])
23
+ keep = [l for l in open("$P/prompts.jsonl", encoding="utf-8") if not all(ok[json.loads(l)["id"]])]
24
+ open("$P/prompts_s2.jsonl", "w", encoding="utf-8").writelines(keep)
25
+ print("stage 2 prompts:", len(keep), "of", len(ok))
26
+ PYEOF
27
+ $PY scripts/gen.py --inp $P/prompts_s2.jsonl --out $D/rollouts.jsonl --n 8 $G 2>&1 | grep -viE "warn|instantiate" | tail -1
28
+ $PY scripts/grade.py rl/$IT "$P/prompts.jsonl" 2>>results/grade_err.log | grep -vE "^\s*$"
29
+ $PY scripts/bc_probe.py --rollouts $D/rollouts.jsonl --graded $D/graded.jsonl --items $P/prompts.jsonl --out-dir $D \
30
+ 2>&1 | grep -viE "warn|instantiate"
31
+ mkdir -p $D/bc && mv $D/forced.jsonl $D/forced_graded.jsonl $D/bc/
32
+ taskkill //F //IM llama-server.exe >/dev/null 2>&1 || true; sleep 3
33
+ $PY scripts/rl_train.py --init $INIT --rollouts $D/rollouts.jsonl $D/bc/forced.jsonl \
34
+ --graded $D/graded.jsonl $D/bc/forced_graded.jsonl --out models/$IT --lr $LR --mb 32 --max-len 9300 2>&1 \
35
+ | grep -viE "warn|instantiate|Loading weights|fast path|Redirect|Writing" | tee $D/train.log
36
+ $PY tools/llama-src/convert_hf_to_gguf.py models/$IT --outtype bf16 --outfile gguf/$IT-bf16.gguf 2>&1 | grep -E "rror" || true
37
+ /c/Users/user/llamabin/llama-quantize.exe gguf/$IT-bf16.gguf gguf/$IT-q5km.gguf Q5_K_M 8 >/dev/null 2>&1
38
+ rm -f gguf/$IT-bf16.gguf
39
+ echo ITER_DONE $IT
research/scripts/rl_train.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """One policy-improvement iteration on self-generated, automatically graded rollouts.
2
+
3
+ Rollouts come from llama.cpp running the current policy (exact token ids are stored), rewards from grade.py.
4
+ For every prompt the G samples form a group; advantage = reward - group mean (groups where all samples agree carry
5
+ no signal and are skipped). The update is a clipped policy-gradient objective (PPO/GRPO-style, asymmetric
6
+ "clip-higher", token-level normalisation) on a fresh LoRA adapter, which is merged into the weights at the end,
7
+ so the next iteration samples from the improved policy.
8
+
9
+ Memory: B=1 sequences, gradient checkpointing, and log-probs computed in vocabulary chunks under checkpoint so
10
+ the 128k-vocab logits of a 12k-token sequence are never materialised.
11
+ """
12
+ import argparse
13
+ import json
14
+ import math
15
+ import os
16
+ import random
17
+ import shutil
18
+ import time
19
+ from collections import defaultdict
20
+
21
+ import torch
22
+ from peft import LoraConfig, get_peft_model
23
+ from torch.utils.checkpoint import checkpoint
24
+ from transformers import AutoModelForCausalLM, AutoTokenizer
25
+ import transformers.integrations.sdpa_attention as _sdpa
26
+
27
+ # Windows torch has no flash kernel: masked/GQA SDPA would fall back to the O(L^2)-memory math kernel.
28
+ # Expanding K/V instead keeps it on the memory-efficient kernel.
29
+ _sdpa.use_gqa_in_sdpa = lambda *a, **k: False
30
+
31
+ ap = argparse.ArgumentParser()
32
+ ap.add_argument("--init", required=True, help="HF dir of the policy that generated the rollouts")
33
+ ap.add_argument("--rollouts", nargs="+", required=True)
34
+ ap.add_argument("--graded", nargs="+", required=True)
35
+ ap.add_argument("--out", required=True)
36
+ ap.add_argument("--lr", type=float, default=2e-5)
37
+ ap.add_argument("--rank", type=int, default=32)
38
+ ap.add_argument("--epochs", type=int, default=1)
39
+ ap.add_argument("--mb", type=int, default=64, help="sequences per optimizer step")
40
+ ap.add_argument("--max-len", type=int, default=12800)
41
+ ap.add_argument("--clip-lo", type=float, default=0.2)
42
+ ap.add_argument("--clip-hi", type=float, default=0.28)
43
+ ap.add_argument("--seed", type=int, default=0)
44
+ ap.add_argument("--items", nargs="+", default=["data/train/pool_math.jsonl", "data/train/pool_code.jsonl"])
45
+ ap.add_argument("--max-seqs", type=int, default=0, help="debug: use only this many sequences")
46
+ ap.add_argument("--no-save", action="store_true")
47
+ ap.add_argument("--mode", choices=["lora", "bias"], default="lora",
48
+ help="bias: train only one residual-stream bias vector per layer (layers 1..L-1), exported as a "
49
+ "llama.cpp control vector; weights stay frozen")
50
+ args = ap.parse_args()
51
+ random.seed(args.seed)
52
+ torch.manual_seed(args.seed)
53
+ EOS = 124900 # <|im_end|>
54
+
55
+ tok = AutoTokenizer.from_pretrained(args.init)
56
+ reward = {}
57
+ for path in args.graded:
58
+ for l in open(path, encoding="utf-8"):
59
+ g = json.loads(l)
60
+ reward[(g["id"], g["sample"])] = float(g["correct"])
61
+ prompts = {}
62
+ for path in args.rollouts:
63
+ for l in open(path, encoding="utf-8"):
64
+ r = json.loads(l)
65
+ if (r["id"], r["sample"]) in reward:
66
+ prompts.setdefault(r["id"], []).append(r)
67
+
68
+ items = {}
69
+ for path in args.items:
70
+ for l in open(path, encoding="utf-8"):
71
+ it = json.loads(l)
72
+ items[it["id"]] = it
73
+
74
+ seqs, n_groups, n_flat = [], 0, 0
75
+ for pid, group in prompts.items():
76
+ rs = [reward[(r["id"], r["sample"])] for r in group]
77
+ mean = sum(rs) / len(rs)
78
+ if max(rs) == min(rs):
79
+ n_flat += 1
80
+ continue
81
+ n_groups += 1
82
+ p_ids = tok.apply_chat_template([{"role": "user", "content": items[pid]["prompt"]}], tokenize=True,
83
+ add_generation_prompt=True, return_dict=False)
84
+ for r, rw in zip(group, rs):
85
+ resp = list(r["tokens"])
86
+ if r.get("stop_type") == "eos":
87
+ resp.append(EOS)
88
+ ids = (p_ids + resp)[: args.max_len]
89
+ # forced-commit samples share their prefix with an on-policy rollout: learn only from the decision onwards
90
+ seqs.append({"ids": ids, "start": len(p_ids), "adv": rw - mean, "from": r.get("loss_from", 0)})
91
+ if args.max_seqs:
92
+ seqs = seqs[: args.max_seqs]
93
+ print(f"{len(prompts)} prompts, {n_groups} informative groups, {n_flat} flat groups skipped, {len(seqs)} sequences",
94
+ flush=True)
95
+
96
+ model = AutoModelForCausalLM.from_pretrained(args.init, dtype=torch.bfloat16, attn_implementation="sdpa", trust_remote_code=True).cuda()
97
+ model.config.use_cache = False
98
+ model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
99
+ model.enable_input_require_grads()
100
+ if args.mode == "lora":
101
+ lora = LoraConfig(r=args.rank, lora_alpha=2 * args.rank, lora_dropout=0.0, bias="none",
102
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "in_proj", "out_proj",
103
+ "w1", "w2", "w3"])
104
+ model = get_peft_model(model, lora)
105
+ model.print_trainable_parameters()
106
+ base = model.get_base_model()
107
+ else:
108
+ for p_ in model.parameters():
109
+ p_.requires_grad_(False)
110
+ n_l, hid = model.config.num_hidden_layers, model.config.hidden_size
111
+ resid_bias = torch.nn.Parameter(torch.zeros(n_l, hid, device="cuda", dtype=torch.float32))
112
+ model.register_parameter("resid_bias", resid_bias)
113
+ for k, layer in enumerate(model.model.layers):
114
+ if k == 0:
115
+ continue # llama.cpp control vectors cannot address layer 0
116
+
117
+ def hook(m, i, o, k=k):
118
+ if isinstance(o, tuple):
119
+ return (o[0] + resid_bias[k].to(o[0].dtype),) + o[1:]
120
+ return o + resid_bias[k].to(o.dtype)
121
+ layer.register_forward_hook(hook)
122
+ print("trainable residual bias params:", (n_l - 1) * hid)
123
+ base = model
124
+
125
+
126
+ def _chunk_logp(h, w, t):
127
+ return torch.log_softmax((h @ w.t()).float(), -1).gather(-1, t[:, None]).squeeze(-1)
128
+
129
+
130
+ def token_logps(ids, start):
131
+ x = torch.tensor([ids], device="cuda")
132
+ h = base.model(input_ids=x, use_cache=False).last_hidden_state[0, start - 1: -1]
133
+ t = x[0, start:]
134
+ w = base.lm_head.weight
135
+ out = []
136
+ for i in range(0, h.shape[0], 512):
137
+ out.append(checkpoint(_chunk_logp, h[i: i + 512], w, t[i: i + 512], use_reentrant=False))
138
+ return torch.cat(out)
139
+
140
+
141
+ # log-probs of the policy that generated the data (adapter / bias are zero-initialised, so this is the init policy)
142
+ t0 = time.time()
143
+ model.eval()
144
+ with torch.no_grad():
145
+ for k, s in enumerate(seqs):
146
+ s["old"] = token_logps(s["ids"], s["start"]).cpu()
147
+ if k % 200 == 0:
148
+ print(f"old logp {k}/{len(seqs)} {time.time() - t0:.0f}s", flush=True)
149
+ model.train()
150
+
151
+ opt = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=args.lr, betas=(0.9, 0.99),
152
+ weight_decay=0.0)
153
+ steps_total = args.epochs * math.ceil(len(seqs) / args.mb)
154
+ sched = torch.optim.lr_scheduler.LambdaLR(opt, lambda s: min(1.0, (s + 1) / 3))
155
+ step = 0
156
+ log = []
157
+ for ep in range(args.epochs):
158
+ order = list(range(len(seqs)))
159
+ random.shuffle(order)
160
+ for b in range(0, len(order), args.mb):
161
+ batch = [seqs[i] for i in order[b: b + args.mb]]
162
+ n_tok = sum(len(s["ids"]) - s["start"] - s["from"] for s in batch)
163
+ stats = defaultdict(float)
164
+ for s in batch:
165
+ lp = token_logps(s["ids"], s["start"])
166
+ old = s["old"].to(lp.device)
167
+ ratio = torch.exp(lp - old)
168
+ adv = s["adv"]
169
+ unclipped = ratio * adv
170
+ clipped = torch.clamp(ratio, 1 - args.clip_lo, 1 + args.clip_hi) * adv
171
+ loss = -torch.minimum(unclipped, clipped)[s["from"]:].sum() / n_tok
172
+ loss.backward()
173
+ if args.max_seqs:
174
+ print(f" seq len {len(s['ids'])} t {time.time() - t0:.0f}s peak {torch.cuda.max_memory_allocated() / 1e9:.1f}GB",
175
+ flush=True)
176
+ with torch.no_grad():
177
+ stats["loss"] += loss.item()
178
+ stats["kl"] += (old - lp)[s["from"]:].sum().item() / n_tok
179
+ stats["clipfrac"] += ((ratio - 1).abs() > args.clip_lo)[s["from"]:].float().sum().item() / n_tok
180
+ gn = torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], 1.0)
181
+ opt.step()
182
+ sched.step()
183
+ opt.zero_grad(set_to_none=True)
184
+ step += 1
185
+ rec = {"step": step, "of": steps_total, "gnorm": gn.item(), **stats, "t": round(time.time() - t0)}
186
+ log.append(rec)
187
+ print(json.dumps(rec), flush=True)
188
+
189
+ print("peak mem GB", torch.cuda.max_memory_allocated() / 1e9)
190
+ if args.no_save:
191
+ raise SystemExit
192
+ if args.mode == "bias":
193
+ import gguf
194
+ os.makedirs(args.out, exist_ok=True)
195
+ w = gguf.GGUFWriter(os.path.join(args.out, "resid_bias.gguf"), "controlvector")
196
+ w.add_string("controlvector.model_hint", "lfm3")
197
+ w.add_uint32("controlvector.layer_count", n_l - 1)
198
+ for k in range(1, n_l):
199
+ w.add_tensor(f"direction.{k}", resid_bias[k].detach().float().cpu().numpy())
200
+ w.write_header_to_file(); w.write_kv_data_to_file(); w.write_tensors_to_file(); w.close()
201
+ torch.save(resid_bias.detach().cpu(), os.path.join(args.out, "resid_bias.pt"))
202
+ json.dump({"args": vars(args), "log": log, "n_groups": n_groups, "n_seqs": len(seqs)},
203
+ open(os.path.join(args.out, "train_log.json"), "w"), indent=1)
204
+ print("saved", args.out, "bias norms", [round(x, 3) for x in resid_bias.norm(dim=-1).tolist()])
205
+ raise SystemExit
206
+ model = model.merge_and_unload()
207
+ os.makedirs(args.out, exist_ok=True)
208
+ model.save_pretrained(args.out, safe_serialization=True)
209
+ tok.save_pretrained(args.out)
210
+ for f in ["configuration_lfm3.py", "modeling_lfm3.py", "lfm3_conv.py", "chat_template.jinja", "generation_config.json",
211
+ "config.json"]:
212
+ src = os.path.join(args.init, f)
213
+ if os.path.exists(src):
214
+ shutil.copy(src, os.path.join(args.out, f))
215
+ json.dump({"args": vars(args), "log": log, "n_groups": n_groups, "n_seqs": len(seqs)},
216
+ open(os.path.join(args.out, "train_log.json"), "w"), indent=1)
217
+ print("saved", args.out)
research/scripts/run_dev.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # usage: run_dev.sh <tag> [general] (server for the model must already be running)
3
+ set -e
4
+ cd /c/Users/user/lfm3proj
5
+ T=$1; PY=.venv/Scripts/python.exe
6
+ $PY scripts/gen.py --out results/$T/dev.jsonl --workers 32 --n 2 --save-tokens --inp data/train/dev.jsonl 2>&1 | grep -viE "warn|instantiate" | tail -1
7
+ if [ "$2" == "general" ]; then
8
+ $PY scripts/gen.py --out results/$T/devgen.jsonl --workers 32 --inp data/train/dev_general.jsonl 2>&1 | grep -viE "warn|instantiate" | tail -1
9
+ fi
10
+ $PY scripts/grade.py $T "data/train/dev*.jsonl" 2>>results/grade_err.log | grep -vE "^\s*$"
11
+ echo DEV_DONE $T
research/scripts/run_eval.sh ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # usage: run_eval.sh <tag> (server for the model must already be running on :8080)
3
+ set -e
4
+ cd /c/Users/user/lfm3proj
5
+ T=$1
6
+ PY=.venv/Scripts/python.exe
7
+ $PY scripts/gen.py --out results/$T/main.jsonl --workers 32 --inp data/eval/gsm8k.jsonl data/eval/math500.jsonl data/eval/humaneval.jsonl data/eval/mbpp.jsonl data/eval/ifeval.jsonl data/eval/mmlupro.jsonl data/eval/bbh.jsonl
8
+ $PY scripts/gen.py --out results/$T/aime.jsonl --workers 32 --n 4 --inp data/eval/aime.jsonl
9
+ echo EVAL_DONE
research/scripts/run_mini.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # usage: run_mini.sh <tag> <gguf> [extra server args] -- ~8 min screening: 140 dev items, n=1, max 6144 tokens
3
+ set -e
4
+ cd /c/Users/user/lfm3proj
5
+ T=$1; PY=.venv/Scripts/python.exe
6
+ taskkill //F //IM llama-server.exe >/dev/null 2>&1 || true; sleep 2
7
+ (/c/Users/user/llamabin/llama-server.exe -m "$2" "${@:3}" -ngl 99 -np 32 -c 212992 -bs -fa on -ctxcp 0 -cram 0 --port 8080 --no-webui > results/server_$T.log 2>&1 &)
8
+ for i in $(seq 1 100); do curl -s -m 2 localhost:8080/health | grep -q '"ok"' && break; sleep 2; done
9
+ $PY scripts/gen.py --out results/$T/mini.jsonl --workers 32 --max-tokens 6144 --inp data/quick/mini.jsonl 2>&1 | grep -viE "warn|instantiate" | tail -1
10
+ $PY scripts/grade.py $T "data/quick/mini.jsonl" 2>>results/grade_err.log | grep -vE "^\s*$"
11
+ echo MINI_DONE $T
research/scripts/run_mini2.sh ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # usage: [ENV=..] run_mini2.sh <tag> <gguf> -- same as run_mini.sh, served by the custom build (LLAMA_BIN), env passes through
3
+ cd /c/Users/user/lfm3proj
4
+ T=$1; PY=.venv/Scripts/python.exe
5
+ LLAMA_BIN=${LLAMA_BIN:-/c/Users/user/llama-build/bin} bash scripts/serve.sh "$2" results/server_$T.log -ngl 99 -np 32 -c 212992 -bs -fa on -ctxcp 0 -cram 0
6
+ $PY scripts/gen.py --out results/$T/mini.jsonl --workers 32 --max-tokens 6144 --inp data/quick/mini.jsonl 2>&1 | grep -viE "warn|instantiate" | tail -1
7
+ $PY scripts/grade.py $T "data/quick/mini.jsonl" 2>>results/grade_err.log | grep -vE "^\s*$"
8
+ echo MINI_DONE $T
research/scripts/run_quick.sh ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # usage: run_quick.sh <tag> <gguf> [extra server args] -- quick screening run (274 dev items, n=1, same sampling as eval)
3
+ set -e
4
+ cd /c/Users/user/lfm3proj
5
+ T=$1; PY=.venv/Scripts/python.exe
6
+ bash scripts/start_server.sh "${@:2}"
7
+ $PY scripts/gen.py --out results/$T/quick.jsonl --workers 32 --save-tokens --inp data/quick/quick.jsonl 2>&1 | grep -viE "warn|instantiate" | tail -1
8
+ $PY scripts/grade.py $T "data/quick/quick.jsonl" 2>>results/grade_err.log | grep -vE "^\s*$"
9
+ echo QUICK_DONE $T
research/scripts/sample_prompts.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Draws N unused training prompts (math/code mix) for one RL iteration; remembers what was used."""
2
+ import json, os, random, sys
3
+ n, frac_code, out, seed = int(sys.argv[1]), float(sys.argv[2]), sys.argv[3], int(sys.argv[4])
4
+ used_path = "data/train/used_ids.txt"
5
+ used = set(open(used_path).read().split()) if os.path.exists(used_path) else set()
6
+ rng = random.Random(seed)
7
+ pick = []
8
+ for path, k in [("data/train/pool_math.jsonl", n - int(n * frac_code)), ("data/train/pool_code.jsonl", int(n * frac_code))]:
9
+ rows = [json.loads(l) for l in open(path, encoding="utf-8")]
10
+ rows = [r for r in rows if r["id"] not in used and r.get("difficulty") not in ("HARD", "VERY_HARD", "MEDIUM_HARD")]
11
+ pick += rng.sample(rows, k)
12
+ with open(out, "w", encoding="utf-8") as f:
13
+ for r in pick:
14
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
15
+ with open(used_path, "a") as f:
16
+ f.write("\n".join(r["id"] for r in pick) + "\n")
17
+ print(len(pick))
research/scripts/sandbox.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """subprocess.run replacement for running untrusted model programs on Windows.
2
+
3
+ Two Windows traps make subprocess.run(..., timeout=) hang forever on a stuck program:
4
+ * in a venv, python.exe is a launcher that starts the real interpreter as a child; the timeout kills only the
5
+ launcher and the child keeps the pipes open;
6
+ * Popen.communicate writes `input` synchronously before it starts waiting, so a program that stops reading a large
7
+ input blocks the write and the timeout is never checked.
8
+ run() feeds stdin from a temporary file and kills the whole process tree on timeout, then raises
9
+ subprocess.TimeoutExpired like subprocess.run."""
10
+ import os
11
+ import subprocess
12
+ import tempfile
13
+
14
+
15
+ def run(cmd, input=None, timeout=None, capture_output=True, text=False, encoding=None, errors=None, **kw):
16
+ kw.setdefault("creationflags", getattr(subprocess, "CREATE_NO_WINDOW", 0))
17
+ fin, path = subprocess.DEVNULL, None
18
+ if input is not None:
19
+ fd, path = tempfile.mkstemp(suffix=".in")
20
+ with os.fdopen(fd, "wb") as f:
21
+ f.write(input if isinstance(input, bytes) else input.encode(encoding or "utf-8"))
22
+ fin = open(path, "rb")
23
+ try:
24
+ p = subprocess.Popen(cmd, stdin=fin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=text,
25
+ encoding=encoding, errors=errors, **kw)
26
+ try:
27
+ out, err = p.communicate(timeout=timeout)
28
+ except subprocess.TimeoutExpired:
29
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(p.pid)], capture_output=True)
30
+ try:
31
+ p.communicate(timeout=10)
32
+ except subprocess.TimeoutExpired:
33
+ pass
34
+ raise subprocess.TimeoutExpired(cmd, timeout)
35
+ return subprocess.CompletedProcess(cmd, p.returncode, out, err)
36
+ finally:
37
+ if path:
38
+ fin.close()
39
+ try:
40
+ os.remove(path)
41
+ except OSError:
42
+ pass
research/scripts/serve.sh ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # usage: serve.sh <gguf (path relative to project)> <log> [server args...]
3
+ # llama-server must start with an ASCII working directory (it scans the cwd for backends; the project path is not ASCII)
4
+ # A start right after killing the previous server sometimes fails to load the CPU backend, so it retries.
5
+ P=/c/Users/user/lfm3proj; BIN=${LLAMA_BIN:-/c/Users/user/llamabin}
6
+ for attempt in 1 2 3; do
7
+ taskkill //F //IM llama-server.exe >/dev/null 2>&1; sleep $((3 * attempt))
8
+ (cd $BIN && ./llama-server.exe -m "$P/$1" "${@:3}" --port 8080 --no-webui > "$P/$2" 2>&1 &)
9
+ for i in $(seq 1 120); do curl -s -m 2 localhost:8080/health | grep -q '"ok"' && break; grep -q "exiting due" "$P/$2" 2>/dev/null && break; sleep 2; done
10
+ curl -s -m 2 localhost:8080/health | grep -q '"ok"' && break
11
+ done
12
+ curl -s localhost:8080/health; echo
research/scripts/sft_train.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Supervised fine-tuning (LoRA, merged at the end) on token-level examples from build_sft.py.
2
+
3
+ Loss = mean NLL over completion tokens. Same memory tricks as rl_train.py: B=1 sequences, gradient checkpointing,
4
+ log-probs in vocabulary chunks under checkpoint, expanded K/V for the memory-efficient SDPA kernel on Windows.
5
+ usage: sft_train.py --data data/teach/sft_pilot.jsonl --init models/LFM3-1.7B --out models/teach1
6
+ """
7
+ import argparse
8
+ import json
9
+ import math
10
+ import os
11
+ import random
12
+ import shutil
13
+ import time
14
+
15
+ import torch
16
+ from peft import LoraConfig, get_peft_model
17
+ from torch.utils.checkpoint import checkpoint
18
+ from transformers import AutoModelForCausalLM, AutoTokenizer
19
+ import transformers.integrations.sdpa_attention as _sdpa
20
+
21
+ _sdpa.use_gqa_in_sdpa = lambda *a, **k: False
22
+
23
+ ap = argparse.ArgumentParser()
24
+ ap.add_argument("--data", nargs="+", required=True)
25
+ ap.add_argument("--init", default="models/LFM3-1.7B")
26
+ ap.add_argument("--out", required=True)
27
+ ap.add_argument("--lr", type=float, default=1e-4)
28
+ ap.add_argument("--rank", type=int, default=64)
29
+ ap.add_argument("--epochs", type=int, default=2)
30
+ ap.add_argument("--mb", type=int, default=16, help="sequences per optimizer step")
31
+ ap.add_argument("--max-len", type=int, default=7000)
32
+ ap.add_argument("--seed", type=int, default=0)
33
+ ap.add_argument("--max-seqs", type=int, default=0)
34
+ args = ap.parse_args()
35
+ random.seed(args.seed)
36
+ torch.manual_seed(args.seed)
37
+
38
+ tok = AutoTokenizer.from_pretrained(args.init)
39
+ seqs = []
40
+ for p in args.data:
41
+ for l in open(p, encoding="utf-8"):
42
+ r = json.loads(l)
43
+ ids = (r["prompt_ids"] + r["completion_ids"])[: args.max_len]
44
+ seqs.append({"ids": ids, "start": len(r["prompt_ids"])})
45
+ if args.max_seqs:
46
+ seqs = seqs[: args.max_seqs]
47
+ n_tok = sum(len(s["ids"]) - s["start"] for s in seqs)
48
+ print(f"{len(seqs)} sequences, {n_tok} completion tokens", flush=True)
49
+
50
+ model = AutoModelForCausalLM.from_pretrained(args.init, dtype=torch.bfloat16, attn_implementation="sdpa",
51
+ trust_remote_code=True).cuda()
52
+ model.config.use_cache = False
53
+ model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
54
+ model.enable_input_require_grads()
55
+ lora = LoraConfig(r=args.rank, lora_alpha=2 * args.rank, lora_dropout=0.0, bias="none",
56
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "in_proj", "out_proj",
57
+ "w1", "w2", "w3"])
58
+ model = get_peft_model(model, lora)
59
+ model.print_trainable_parameters()
60
+ base = model.get_base_model()
61
+
62
+
63
+ def _chunk_nll(h, w, t):
64
+ return -torch.log_softmax((h @ w.t()).float(), -1).gather(-1, t[:, None]).squeeze(-1)
65
+
66
+
67
+ def nll(ids, start):
68
+ x = torch.tensor([ids], device="cuda")
69
+ h = base.model(input_ids=x, use_cache=False).last_hidden_state[0, start - 1: -1]
70
+ t = x[0, start:]
71
+ w = base.lm_head.weight
72
+ return torch.cat([checkpoint(_chunk_nll, h[i: i + 512], w, t[i: i + 512], use_reentrant=False)
73
+ for i in range(0, h.shape[0], 512)])
74
+
75
+
76
+ params = [p for p in model.parameters() if p.requires_grad]
77
+ opt = torch.optim.AdamW(params, lr=args.lr, betas=(0.9, 0.99), weight_decay=0.0)
78
+ steps_total = args.epochs * math.ceil(len(seqs) / args.mb)
79
+ sched = torch.optim.lr_scheduler.LambdaLR(
80
+ opt, lambda s: min(1.0, (s + 1) / 5) * 0.5 * (1 + math.cos(math.pi * min(1.0, s / steps_total))))
81
+ t0, step, log = time.time(), 0, []
82
+ model.train()
83
+ for ep in range(args.epochs):
84
+ order = list(range(len(seqs)))
85
+ random.shuffle(order)
86
+ for b in range(0, len(order), args.mb):
87
+ batch = [seqs[i] for i in order[b: b + args.mb]]
88
+ nt = sum(len(s["ids"]) - s["start"] for s in batch)
89
+ tot = 0.0
90
+ for s in batch:
91
+ loss = nll(s["ids"], s["start"]).sum() / nt
92
+ loss.backward()
93
+ tot += loss.item()
94
+ gn = torch.nn.utils.clip_grad_norm_(params, 1.0)
95
+ opt.step()
96
+ sched.step()
97
+ opt.zero_grad(set_to_none=True)
98
+ step += 1
99
+ rec = {"step": step, "of": steps_total, "ep": ep, "nll": round(tot, 4), "gnorm": round(gn.item(), 3),
100
+ "lr": sched.get_last_lr()[0], "t": round(time.time() - t0)}
101
+ log.append(rec)
102
+ print(json.dumps(rec), flush=True)
103
+ print("peak mem GB", torch.cuda.max_memory_allocated() / 1e9)
104
+ model = model.merge_and_unload()
105
+ os.makedirs(args.out, exist_ok=True)
106
+ model.save_pretrained(args.out, safe_serialization=True)
107
+ tok.save_pretrained(args.out)
108
+ for f in ["configuration_lfm3.py", "modeling_lfm3.py", "lfm3_conv.py", "chat_template.jinja", "generation_config.json",
109
+ "config.json"]:
110
+ src = os.path.join(args.init, f)
111
+ if os.path.exists(src):
112
+ shutil.copy(src, os.path.join(args.out, f))
113
+ json.dump({"args": vars(args), "log": log, "n_seqs": len(seqs)}, open(os.path.join(args.out, "train_log.json"), "w"),
114
+ indent=1)
115
+ print("saved", args.out)
research/scripts/shadow_split.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Split a shadow CoC run into two gradeable result dirs on the SAME trajectories:
2
+ results/<tag>_full/ (generation run to the end) and results/<tag>_coc/ (committed answer where consensus was reached,
3
+ otherwise identical to full). usage: shadow_split.py results/shadow/raw.jsonl shadow"""
4
+ import json, os, sys
5
+ src, tag = sys.argv[1], sys.argv[2]
6
+ recs = [json.loads(l) for l in open(src, encoding="utf-8")]
7
+ for kind in ("full", "coc"):
8
+ d = f"results/{tag}_{kind}"
9
+ os.makedirs(d, exist_ok=True)
10
+ with open(f"{d}/gen.jsonl", "w", encoding="utf-8") as f:
11
+ for r in recs:
12
+ r2 = {k: v for k, v in r.items() if k not in ("coc_completion", "coc_tokens")}
13
+ if kind == "coc" and "coc_completion" in r:
14
+ r2.update(completion=r["coc_completion"], n_tokens=r["coc_n_tokens"], truncated=False)
15
+ f.write(json.dumps(r2, ensure_ascii=False) + "\n")
16
+ print(len(recs), "records,", sum("coc_completion" in r for r in recs), "committed")
research/scripts/sim_coc.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Offline simulation of commit-on-consensus settings from a --shadow --record-all run (probes every 512 tokens).
2
+ For each policy (probe every `chunk` tokens, commit after `agree` identical consecutive answers) and task group it
3
+ reports accuracy of the full generation vs. the CoC output on the SAME trajectories, flips, and average tokens.
4
+ usage: sim_coc.py results/rec/raw.jsonl data/coc/*.jsonl"""
5
+ import glob, json, sys, collections
6
+ sys.path.insert(0, "scripts")
7
+ from grade import grade_one # noqa: E402
8
+ recs = [json.loads(l) for l in open(sys.argv[1], encoding="utf-8")]
9
+ for r in recs: # older runs kept the end-of-turn token in the text
10
+ r["completion"] = r["completion"].replace("<|im_end|>", "")
11
+ items = {}
12
+ for pat in sys.argv[2:]:
13
+ for f in glob.glob(pat):
14
+ for l in open(f, encoding="utf-8"):
15
+ r = json.loads(l); items[r["id"]] = r
16
+ group = lambda r: ("math_dev" if r["id"].startswith(("math_train", "deepscaler")) and r["id"] in MINI else
17
+ "math_pool" if r["task"] == "math" else r["task"])
18
+ MINI = {json.loads(l)["id"] for l in open("data/coc/mini_math.jsonl", encoding="utf-8")}
19
+ full_ok, probe_ok = {}, {}
20
+ for r in recs:
21
+ it = items[r["id"]]
22
+ full_ok[r["id"]] = grade_one(r, it)["correct"]
23
+ probe_ok[r["id"]] = [grade_one({**r, "completion": "<think>x</think>" + p["text"], "n_tokens": 0}, it)["correct"]
24
+ for p in r.get("probe_log", [])]
25
+ print(f"{'policy':14s} {'group':10s} {'n':>4s} {'commits':>7s} {'full':>6s} {'coc':>6s} {'+':>3s} {'-':>3s} {'tok_full':>8s} {'tok_coc':>8s}")
26
+ for chunk in (512, 1024, 2048):
27
+ for agree in (2, 3):
28
+ st = collections.defaultdict(lambda: [0, 0, 0, 0, 0, 0, 0, 0])
29
+ for r in recs:
30
+ g = group(r)
31
+ log = r.get("probe_log", [])
32
+ step = chunk // 512
33
+ idx = [i for i in range(len(log)) if (i + 1) % step == 0]
34
+ commit = None
35
+ for j in range(agree - 1, len(idx)):
36
+ a = [log[idx[t]]["ans"] for t in range(j - agree + 1, j + 1)]
37
+ if a[0] is not None and all(x == a[0] for x in a):
38
+ commit = idx[j]
39
+ break
40
+ fo = full_ok[r["id"]]
41
+ co = probe_ok[r["id"]][commit] if commit is not None else fo
42
+ tf = r["n_tokens"]
43
+ tc = log[commit]["cut"] + log[commit]["n"] if commit is not None else tf
44
+ for key in (g, "ALL"):
45
+ s = st[key]
46
+ s[0] += 1; s[1] += commit is not None; s[2] += fo; s[3] += co
47
+ s[4] += (not fo) and co; s[5] += fo and not co; s[6] += tf; s[7] += tc
48
+ for g, s in sorted(st.items()):
49
+ print(f"c{chunk:<5d}a{agree:<6d} {g:10s} {s[0]:4d} {s[1]:7d} {100*s[2]/s[0]:6.1f} {100*s[3]/s[0]:6.1f} "
50
+ f"{s[4]:3d} {s[5]:3d} {s[6]/s[0]:8.0f} {s[7]/s[0]:8.0f}")
research/scripts/sim_multi.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adaptive deliberation: offline evaluation of controllers over recorded parallel reasoning streams.
2
+
3
+ Input: a gen_coc2.py --shadow --record-all --n N run (N streams per problem, a forced-answer probe every ~1024
4
+ tokens of each stream, full completions). Streams are assumed to advance at the same speed (they run in parallel
5
+ slots). Every controller is evaluated on the same recorded trajectories; cost = total generated tokens (all streams,
6
+ probes' own tokens excluded; probes cost prefill only).
7
+
8
+ Controllers
9
+ single stream 0 to the end (the baseline generation)
10
+ sc@N majority vote over the N full answers (self-consistency)
11
+ coc1 stream 0 with temporal consensus (commit when `agree` successive probes match)
12
+ xs-K N parallel streams; stop all as soon as K streams currently hold the same answer
13
+ (a stream holds its final answer once finished, otherwise its latest probe answer)
14
+ adapt-K stream 0 alone; if it has not finished by `t0` tokens, streams 1..N-1 start; then as xs-K over the
15
+ streams that are running, and stream 0's natural answer is accepted if it finishes first
16
+ usage: sim_multi.py results/multi/raw.jsonl "data/coc/mini_nocode.jsonl"
17
+ """
18
+ import collections
19
+ import glob
20
+ import json
21
+ import re
22
+ import sys
23
+
24
+ sys.path.insert(0, "scripts")
25
+ from grade import grade_one, last_boxed # noqa: E402
26
+
27
+ recs = [json.loads(l) for l in open(sys.argv[1], encoding="utf-8")]
28
+ items = {}
29
+ for pat in sys.argv[2:]:
30
+ for f in glob.glob(pat):
31
+ for l in open(f, encoding="utf-8"):
32
+ r = json.loads(l)
33
+ items[r["id"]] = r
34
+
35
+
36
+ def extract(task, text):
37
+ if task == "mmlupro":
38
+ m = re.findall(r"answer is:?\s*\(?\**\s*([A-J])\b", text) or re.findall(r"\\boxed\{\(?([A-J])\)?\}", text)
39
+ a = m[-1] if m else None
40
+ elif task == "bbh":
41
+ m = re.findall(r"answer is:?\s*(.+)", text, re.I)
42
+ a = m[-1] if m else (last_boxed(text) or "")
43
+ a = re.sub(r"\\boxed\{(.*)\}", r"\1", a).strip().strip("*$ .").lower() or None
44
+ else:
45
+ b = last_boxed(text)
46
+ a = re.sub(r"\s+", "", b) if b else None
47
+ if a is None or "..." in a or a in ("?", "") or len(a) > 80:
48
+ return None
49
+ return a
50
+
51
+
52
+ by = collections.defaultdict(dict)
53
+ for r in recs:
54
+ r["completion"] = r["completion"].replace("<|im_end|>", "")
55
+ it = items[r["id"]]
56
+ task = it["task"]
57
+ fin = r["completion"].split("</think>", 1)[1] if "</think>" in r["completion"] else None
58
+ r["final_ans"] = extract(task, fin) if fin is not None else None
59
+ r["final_ok"] = grade_one(r, it)["correct"]
60
+ for p in r.get("probe_log", []):
61
+ p["ok"] = grade_one({**r, "completion": "<think>x</think>" + p["text"], "n_tokens": 0}, it)["correct"]
62
+ by[r["id"]][r["sample"]] = r
63
+ ids = [i for i in by if len(by[i]) >= 4]
64
+ N = 4
65
+
66
+
67
+ def ans_ok(i, a):
68
+ """is answer string a correct for item i (look it up among recorded answers)"""
69
+ for r in by[i].values():
70
+ if r["final_ans"] == a:
71
+ return r["final_ok"]
72
+ for p in r.get("probe_log", []):
73
+ if p["ans"] == a:
74
+ return p["ok"]
75
+ return False
76
+
77
+
78
+ def state(r, t):
79
+ """answer held by stream r after t generated tokens; (answer, finished)"""
80
+ if r["n_tokens"] <= t and not r.get("truncated"):
81
+ return r["final_ans"], True
82
+ a = None
83
+ for p in r.get("probe_log", []):
84
+ if p["cut"] <= t:
85
+ a = p["ans"]
86
+ return a, False
87
+
88
+
89
+ def run_xs(i, K, streams, start=None, step=256):
90
+ start = start or {s: 0 for s in streams}
91
+ rs = {s: by[i][s] for s in streams}
92
+ horizon = max(start[s] + rs[s]["n_tokens"] for s in streams)
93
+ t = 0
94
+ while True:
95
+ t += step
96
+ held = {}
97
+ for s in streams:
98
+ if t < start[s]:
99
+ continue
100
+ held[s] = state(rs[s], t - start[s])
101
+ cnt = collections.Counter(a for a, _ in held.values() if a is not None)
102
+ best = cnt.most_common(1)
103
+ done_all = all(t - start[s] >= rs[s]["n_tokens"] for s in streams)
104
+ if best and best[0][1] >= K:
105
+ a = best[0][0]
106
+ cost = sum(min(rs[s]["n_tokens"], max(0, t - start[s])) for s in streams)
107
+ return ans_ok(i, a), cost
108
+ if done_all or t > horizon + step:
109
+ # no consensus: majority over what the streams ended with (ties -> stream 0's answer)
110
+ fin = [rs[s]["final_ans"] for s in streams if rs[s]["final_ans"] is not None]
111
+ c = collections.Counter(fin).most_common()
112
+ a = c[0][0] if c and (len(c) == 1 or c[0][1] > c[1][1]) else rs[streams[0]]["final_ans"]
113
+ cost = sum(rs[s]["n_tokens"] for s in streams)
114
+ return (ans_ok(i, a) if a is not None else rs[streams[0]]["final_ok"]), cost
115
+
116
+
117
+ def run_adapt(i, K, t0):
118
+ r0 = by[i][0]
119
+ if (r0["n_tokens"] <= t0 and not r0.get("truncated")):
120
+ return r0["final_ok"], r0["n_tokens"]
121
+ return run_xs(i, K, list(range(N)), start={0: 0, 1: t0, 2: t0, 3: t0})
122
+
123
+
124
+ def run_coc1(i, agree, every):
125
+ r = by[i][0]
126
+ log = r.get("probe_log", [])
127
+ sub = [p for j, p in enumerate(log) if (j + 1) % every == 0]
128
+ for j in range(agree - 1, len(sub)):
129
+ a = [sub[t]["ans"] for t in range(j - agree + 1, j + 1)]
130
+ if a[0] is not None and all(x == a[0] for x in a):
131
+ return sub[j]["ok"], sub[j]["cut"]
132
+ return r["final_ok"], r["n_tokens"]
133
+
134
+
135
+ def run_sc(i):
136
+ fin = [by[i][s]["final_ans"] for s in range(N) if by[i][s]["final_ans"] is not None]
137
+ c = collections.Counter(fin).most_common()
138
+ cost = sum(by[i][s]["n_tokens"] for s in range(N))
139
+ if not c:
140
+ return by[i][0]["final_ok"], cost
141
+ a = c[0][0] if len(c) == 1 or c[0][1] > c[1][1] else by[i][0]["final_ans"] or c[0][0]
142
+ return ans_ok(i, a), cost
143
+
144
+
145
+ policies = {"single": lambda i: (by[i][0]["final_ok"], by[i][0]["n_tokens"]),
146
+ "sc@4": run_sc,
147
+ "coc1 2048x3": lambda i: run_coc1(i, 3, 2),
148
+ "coc1 1024x2": lambda i: run_coc1(i, 2, 1)}
149
+ for K in (2, 3):
150
+ policies[f"xs-{K}"] = lambda i, K=K: run_xs(i, K, list(range(N)))
151
+ for t0 in (1024, 2048, 3072):
152
+ policies[f"adapt-{K} t0={t0}"] = lambda i, K=K, t0=t0: run_adapt(i, K, t0)
153
+
154
+ tasks = sorted({items[i]["task"] for i in ids})
155
+ print(f"{len(ids)} problems x {N} streams")
156
+ hdr = f"{'controller':20s} {'ALL acc':>8s} {'tok/prob':>9s} {'x base':>7s} " + " ".join(f"{t:>8s}" for t in tasks)
157
+ print(hdr)
158
+ base_tok = None
159
+ for name, f in policies.items():
160
+ res = {i: f(i) for i in ids}
161
+ acc = 100 * sum(ok for ok, _ in res.values()) / len(ids)
162
+ tok = sum(c for _, c in res.values()) / len(ids)
163
+ base_tok = base_tok or tok
164
+ per = " ".join(f"{100 * sum(res[i][0] for i in ids if items[i]['task'] == t) / sum(1 for i in ids if items[i]['task'] == t):8.1f}" for t in tasks)
165
+ print(f"{name:20s} {acc:8.1f} {tok:9.0f} {tok / base_tok:7.2f} {per}")
research/scripts/sim_single.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Single-stream deliberation controllers on recorded trajectories (stream 0 of a --shadow --record-all run).
2
+
3
+ base the ordinary generation
4
+ fallback ordinary generation, but a stream that hits the token limit answers with its last probe instead
5
+ of being cut off (truncation scores 0 anyway)
6
+ coc E x A commit when A successive probes (one every E*1024 tokens) give the same answer
7
+ coc E x A + fb the same plus the limit fallback
8
+ budget budget-aware consensus: strict (3 agreeing probes) in the first half of the budget, 2 agreeing probes
9
+ in the second half, plus the limit fallback
10
+ usage: sim_single.py RAW.jsonl "ITEMS_GLOB" [...]
11
+ """
12
+ import collections
13
+ import glob
14
+ import json
15
+ import sys
16
+
17
+ sys.path.insert(0, "scripts")
18
+ from grade import grade_one # noqa: E402
19
+
20
+ recs = [json.loads(l) for l in open(sys.argv[1], encoding="utf-8")]
21
+ items = {}
22
+ for pat in sys.argv[2:]:
23
+ for f in glob.glob(pat):
24
+ for l in open(f, encoding="utf-8"):
25
+ r = json.loads(l)
26
+ items[r["id"]] = r
27
+ recs = [r for r in recs if r.get("sample", 0) == 0 and r["id"] in items and r.get("probe_log") is not None]
28
+ limit = max(r["n_tokens"] for r in recs)
29
+ for r in recs:
30
+ r["completion"] = r["completion"].replace("<|im_end|>", "")
31
+ it = items[r["id"]]
32
+ r["ok"] = grade_one(r, it)["correct"]
33
+ for p in r["probe_log"]:
34
+ p["ok"] = grade_one({**r, "completion": "<think>x</think>" + p["text"], "n_tokens": 0}, it)["correct"]
35
+
36
+
37
+ def coc(r, every, agree, fb, budget_aware=False):
38
+ log = r["probe_log"]
39
+ sub = [p for j, p in enumerate(log) if (j + 1) % every == 0]
40
+ for j in range(len(sub)):
41
+ need = agree
42
+ if budget_aware:
43
+ need = 3 if sub[j]["cut"] < limit / 2 else 2
44
+ if j + 1 < need:
45
+ continue
46
+ a = [sub[t]["ans"] for t in range(j - need + 1, j + 1)]
47
+ if a[0] is not None and all(x == a[0] for x in a):
48
+ return sub[j]["ok"], sub[j]["cut"] + sub[j]["n"]
49
+ if fb and r.get("truncated") and log:
50
+ last = [p for p in log if p["ans"] is not None]
51
+ if fb == "maj": # most frequent answer among the probes of the second half (ties -> latest)
52
+ last = [p for p in last if p["cut"] >= limit / 2] or last
53
+ if last:
54
+ c = collections.Counter(p["ans"] for p in last)
55
+ top = max(c.values())
56
+ pick = [p for p in last if c[p["ans"]] == top][-1]
57
+ return pick["ok"], r["n_tokens"]
58
+ if last:
59
+ return last[-1]["ok"], r["n_tokens"]
60
+ return r["ok"], r["n_tokens"]
61
+
62
+
63
+ pols = {"base": lambda r: (r["ok"], r["n_tokens"]), "fallback": lambda r: coc(r, 1, 99, True)}
64
+ for e, a in [(1, 2), (1, 3), (2, 2), (2, 3)]:
65
+ pols[f"coc {e * 1024}x{a}"] = lambda r, e=e, a=a: coc(r, e, a, False)
66
+ pols[f"coc {e * 1024}x{a} + fb"] = lambda r, e=e, a=a: coc(r, e, a, True)
67
+ pols["budget-aware + fb"] = lambda r: coc(r, 1, 0, True, budget_aware=True)
68
+ pols["fallback-maj"] = lambda r: coc(r, 1, 99, "maj")
69
+ pols["coc 2048x3 + fb-maj"] = lambda r: coc(r, 2, 3, "maj")
70
+
71
+ tasks = sorted({r["task"] for r in recs})
72
+ print(f"{len(recs)} problems, token limit {limit}")
73
+ print(f"{'controller':20s} {'ALL':>6s} {'+':>3s} {'-':>3s} {'tokens':>7s} " + " ".join(f"{t:>9s}" for t in tasks))
74
+ for name, f in pols.items():
75
+ res = [(r, *f(r)) for r in recs]
76
+ acc = 100 * sum(ok for _, ok, _ in res) / len(res)
77
+ gain = sum((not r["ok"]) and ok for r, ok, _ in res)
78
+ loss = sum(r["ok"] and not ok for r, ok, _ in res)
79
+ tok = sum(t for _, _, t in res) / len(res)
80
+ per = " ".join(f"{100 * sum(ok for r, ok, _ in res if r['task'] == t) / sum(1 for r in recs if r['task'] == t):9.1f}"
81
+ for t in tasks)
82
+ print(f"{name:20s} {acc:6.1f} {gain:3d} {loss:3d} {tok:7.0f} {per}")
research/scripts/soft_post.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Turn the output of lfm3-soft-think.exe (cpp/soft_think.cpp) into records for grade.py / cmp.py.
2
+ The visible thinking is the chosen token of every step (for soft/gumbel the argmax of the mixture weights); n_tokens
3
+ counts thinking steps + answer tokens, i.e. the tokens the user would see.
4
+ usage: soft_post.py --txt results/soft/m180_soft_s0.txt --out results/st2_soft/mini.jsonl --items data/coc/math180.jsonl
5
+ """
6
+ import argparse
7
+ import json
8
+ import os
9
+
10
+ from transformers import AutoTokenizer
11
+
12
+ ap = argparse.ArgumentParser()
13
+ ap.add_argument("--txt", nargs="+", required=True)
14
+ ap.add_argument("--out", required=True)
15
+ ap.add_argument("--items", required=True)
16
+ ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
17
+ args = ap.parse_args()
18
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
19
+ task = {json.loads(l)["id"]: json.loads(l)["task"] for l in open(args.items, encoding="utf-8")}
20
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
21
+ n = 0
22
+ with open(args.out, "w", encoding="utf-8") as f:
23
+ for s, path in enumerate(args.txt):
24
+ for line in open(path, encoding="utf-8"):
25
+ parts = line.rstrip("\n").split("\t")
26
+ if len(parts) != 6:
27
+ continue
28
+ id_, steps, forced, cold, think, answer = parts
29
+ think = [int(x) for x in think.split()]
30
+ answer = [int(x) for x in answer.split()]
31
+ f.write(json.dumps({"id": id_, "task": task[id_], "sample": s,
32
+ "completion": "<think>" + tok.decode(think) + "</think>" + tok.decode(answer),
33
+ "n_tokens": int(steps) + len(answer), "truncated": False, "think_steps": int(steps),
34
+ "forced": forced == "1", "cold": cold == "1"}, ensure_ascii=False) + "\n")
35
+ n += 1
36
+ print(f"{n} records -> {args.out}")
research/scripts/soft_stats.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Summary of the latent-thinking runs (results/st2_<mode>): accuracy, visible thinking length, how thinking ended.
2
+ usage: soft_stats.py discrete soft gumbel moi mti"""
3
+ import json
4
+ import statistics
5
+ import sys
6
+
7
+ for mode in sys.argv[1:]:
8
+ recs = {json.loads(l)["id"]: json.loads(l) for l in open(f"results/st2_{mode}/mini.jsonl", encoding="utf-8")}
9
+ graded = {json.loads(l)["id"]: json.loads(l)["correct"] for l in open(f"results/st2_{mode}/graded.jsonl", encoding="utf-8")}
10
+ n = len(recs)
11
+ steps = [r["think_steps"] for r in recs.values()]
12
+ forced = sum(r["forced"] for r in recs.values())
13
+ cold = sum(r.get("cold", False) for r in recs.values())
14
+ natural = n - forced - cold
15
+ acc_nat = [graded[i] for i, r in recs.items() if not r["forced"] and not r.get("cold")]
16
+ print(f"{mode:9s} n={n} acc {100 * sum(graded.values()) / n:5.1f} thinking mean {statistics.mean(steps):6.0f} "
17
+ f"median {statistics.median(steps):6.0f} ended: natural {natural} (acc {100 * sum(acc_nat) / max(1, len(acc_nat)):.0f}%)"
18
+ f", budget {forced}, cold stop {cold}")
research/scripts/soft_think.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Latent ("soft") thinking for LFM3-1.7B, with a discrete control in the same implementation.
2
+
3
+ During the thinking phase the next input is not one sampled token but a continuous "concept token": the embedding
4
+ average of the top candidate tokens weighted by their probabilities (Soft Thinking, Zhang et al. 2025). Later work
5
+ found that plain soft thinking collapses towards greedy decoding and that randomness restores it (Gumbel-softmax
6
+ weights), so three modes are compared on the same problems with the same budget and answer read-out:
7
+ discrete ordinary sampling (temperature 0.6, top-k 50, top-p 0.95) -- control
8
+ soft probability-weighted top-k embedding mixture -- Soft Thinking
9
+ soft_gumbel weights softmax((log p + Gumbel)/tau) over the same candidates -- randomized soft thinking
10
+ Thinking ends when the most probable token is </think> (soft modes) / </think> is sampled (discrete), or at the
11
+ budget, where </think> is forced; the answer is then sampled discretely.
12
+ usage: soft_think.py --items data/coc/math180.jsonl --mode soft --out results/st_soft/mini.jsonl [--limit 120]
13
+ soft_think.py --selftest (batched left-padded decoding must match single-sequence decoding)
14
+ """
15
+ import argparse
16
+ import json
17
+ import math
18
+ import os
19
+ import time
20
+
21
+ import torch
22
+ from transformers import AutoModelForCausalLM, AutoTokenizer
23
+
24
+ THINK_END, EOS = 124902, 124900
25
+ ap = argparse.ArgumentParser()
26
+ ap.add_argument("--items")
27
+ ap.add_argument("--mode", choices=["discrete", "soft", "soft_gumbel"], default="soft")
28
+ ap.add_argument("--out")
29
+ ap.add_argument("--limit", type=int, default=0)
30
+ ap.add_argument("--budget", type=int, default=6144)
31
+ ap.add_argument("--answer-tokens", type=int, default=768)
32
+ ap.add_argument("--batch", type=int, default=16)
33
+ ap.add_argument("--topk-soft", type=int, default=15)
34
+ ap.add_argument("--tau", type=float, default=0.5)
35
+ ap.add_argument("--seed", type=int, default=0)
36
+ ap.add_argument("--model", default="models/LFM3-1.7B")
37
+ ap.add_argument("--selftest", action="store_true")
38
+ ap.add_argument("--cpu", action="store_true")
39
+ args = ap.parse_args()
40
+
41
+ dev = "cpu" if args.cpu else "cuda"
42
+ tok = AutoTokenizer.from_pretrained(args.model)
43
+ model = AutoModelForCausalLM.from_pretrained(args.model, dtype=torch.float32 if args.cpu else torch.bfloat16,
44
+ trust_remote_code=True).to(dev).eval()
45
+ E = model.get_input_embeddings().weight # (V, D)
46
+ PAD = tok.pad_token_id if tok.pad_token_id is not None else 0
47
+
48
+
49
+ def left_pad(seqs):
50
+ L = max(len(s) for s in seqs)
51
+ ids = torch.full((len(seqs), L), PAD, dtype=torch.long)
52
+ mask = torch.zeros((len(seqs), L), dtype=torch.long)
53
+ for i, s in enumerate(seqs):
54
+ ids[i, L - len(s):] = torch.tensor(s)
55
+ mask[i, L - len(s):] = 1
56
+ return ids.to(dev), mask.to(dev)
57
+
58
+
59
+ def filtered_probs(logits, temperature=0.6, top_k=50, top_p=0.95):
60
+ logits = logits.float() / temperature
61
+ v, ix = logits.topk(top_k, dim=-1)
62
+ p = torch.softmax(v, dim=-1)
63
+ keep = (p.cumsum(-1) - p) < top_p
64
+ p = p * keep
65
+ return p / p.sum(-1, keepdim=True), ix
66
+
67
+
68
+ @torch.no_grad()
69
+ def run_batch(prompts, mode, seed):
70
+ g = torch.Generator(device=dev).manual_seed(seed)
71
+ ids, mask = left_pad(prompts)
72
+ out = model(input_ids=ids, attention_mask=mask, use_cache=True)
73
+ cache, logits = out.past_key_values, out.logits[:, -1]
74
+ B = len(prompts)
75
+ phase = ["think"] * B
76
+ think_steps, transcript, answer = [0] * B, [[] for _ in range(B)], [[] for _ in range(B)]
77
+ forced = [False] * B
78
+ for step in range(args.budget + args.answer_tokens + 2):
79
+ if all(ph == "done" for ph in phase):
80
+ break
81
+ p, ix = filtered_probs(logits)
82
+ sampled = ix.gather(1, torch.multinomial(p, 1, generator=g)).squeeze(1)
83
+ top1 = ix[:, 0]
84
+ emb = E[sampled].clone()
85
+ if mode != "discrete":
86
+ k = args.topk_soft
87
+ ps, ixs = p[:, :k], ix[:, :k]
88
+ if mode == "soft_gumbel":
89
+ gum = -torch.log(-torch.log(torch.rand(ps.shape, generator=g, device=dev).clamp_min(1e-9)))
90
+ w = torch.softmax((torch.log(ps.clamp_min(1e-12)) + gum) / args.tau, dim=-1) * (ps > 0)
91
+ else:
92
+ w = ps
93
+ w = w / w.sum(-1, keepdim=True)
94
+ soft = torch.einsum("bk,bkd->bd", w.to(E.dtype), E[ixs])
95
+ for i in range(B):
96
+ if phase[i] == "think":
97
+ end = (top1[i].item() == THINK_END) if mode != "discrete" else (sampled[i].item() == THINK_END)
98
+ if think_steps[i] >= args.budget:
99
+ end, forced[i] = True, True
100
+ if end:
101
+ emb[i] = E[THINK_END]
102
+ phase[i] = "answer"
103
+ continue
104
+ think_steps[i] += 1
105
+ transcript[i].append(top1[i].item() if mode != "discrete" else sampled[i].item())
106
+ if mode != "discrete":
107
+ emb[i] = soft[i]
108
+ elif phase[i] == "answer":
109
+ t = sampled[i].item()
110
+ if t == EOS or len(answer[i]) >= args.answer_tokens:
111
+ phase[i] = "done"
112
+ else:
113
+ answer[i].append(t)
114
+ mask = torch.cat([mask, torch.ones((B, 1), dtype=mask.dtype, device=dev)], dim=1)
115
+ out = model(inputs_embeds=emb[:, None, :], attention_mask=mask, past_key_values=cache, use_cache=True)
116
+ cache, logits = out.past_key_values, out.logits[:, -1]
117
+ return [{"think_steps": think_steps[i], "forced": forced[i], "thinking": tok.decode(transcript[i]),
118
+ "answer": tok.decode(answer[i])} for i in range(B)]
119
+
120
+
121
+ def chat_ids(prompt):
122
+ ids = tok.apply_chat_template([{"role": "user", "content": prompt}], tokenize=True, add_generation_prompt=True,
123
+ return_dict=False)
124
+ return ids["input_ids"] if isinstance(ids, dict) else ids
125
+
126
+
127
+ if args.selftest:
128
+ a = chat_ids("What is 2+2?")
129
+ b = chat_ids("Explain in one sentence why the sky is blue, and mention Rayleigh scattering.")
130
+ with torch.no_grad():
131
+ single = model(input_ids=torch.tensor([b], device=dev)).logits[0, -1].float()
132
+ ids, mask = left_pad([a, b])
133
+ batched = model(input_ids=ids, attention_mask=mask).logits[1, -1].float()
134
+ sa = model(input_ids=torch.tensor([a], device=dev)).logits[0, -1].float()
135
+ ba = model(input_ids=ids, attention_mask=mask).logits[0, -1].float()
136
+ print("prefill max|diff| long row", (single - batched).abs().max().item(), "short (padded) row",
137
+ (sa - ba).abs().max().item(), "logit scale", single.abs().max().item())
138
+ # decode 8 forced tokens both ways
139
+ forced_toks = tok("<think>We add the numbers", add_special_tokens=False)["input_ids"][:6]
140
+ with torch.no_grad():
141
+ o1 = model(input_ids=torch.tensor([a], device=dev), use_cache=True)
142
+ c1 = o1.past_key_values
143
+ o2 = model(input_ids=ids, attention_mask=mask, use_cache=True)
144
+ c2, m2 = o2.past_key_values, mask
145
+ for t in forced_toks:
146
+ o1 = model(input_ids=torch.tensor([[t]], device=dev), past_key_values=c1, use_cache=True)
147
+ c1 = o1.past_key_values
148
+ m2 = torch.cat([m2, torch.ones((2, 1), dtype=m2.dtype, device=dev)], 1)
149
+ o2 = model(inputs_embeds=E[torch.tensor([t, t], device=dev)][:, None, :], attention_mask=m2,
150
+ past_key_values=c2, use_cache=True)
151
+ c2 = o2.past_key_values
152
+ print("decode max|diff| padded row after 6 steps", (o1.logits[0, -1].float() - o2.logits[0, -1].float()).abs().max().item())
153
+ raise SystemExit
154
+
155
+ items = [json.loads(l) for l in open(args.items, encoding="utf-8")]
156
+ items = items[: args.limit] if args.limit else items
157
+ done = set()
158
+ if os.path.exists(args.out):
159
+ done = {json.loads(l)["id"] for l in open(args.out, encoding="utf-8")}
160
+ items = [it for it in items if it["id"] not in done]
161
+ items.sort(key=lambda it: len(it["prompt"]))
162
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
163
+ t0, n_tok = time.time(), 0
164
+ with open(args.out, "a", encoding="utf-8") as f:
165
+ for s in range(0, len(items), args.batch):
166
+ chunk = items[s:s + args.batch]
167
+ res = run_batch([chat_ids(it["prompt"]) for it in chunk], args.mode, args.seed * 100003 + s)
168
+ for it, r in zip(chunk, res):
169
+ n = r["think_steps"] + len(tok(r["answer"], add_special_tokens=False)["input_ids"])
170
+ n_tok += n
171
+ f.write(json.dumps({"id": it["id"], "task": it["task"], "sample": 0, "mode": args.mode,
172
+ "completion": "<think>" + r["thinking"] + "</think>" + r["answer"], "n_tokens": n,
173
+ "truncated": False, "think_steps": r["think_steps"], "forced": r["forced"]},
174
+ ensure_ascii=False) + "\n")
175
+ f.flush()
176
+ el = time.time() - t0
177
+ print(f"{s + len(chunk)}/{len(items)} {n_tok / el:.0f} tok/s {el / 60:.1f} min", flush=True)
research/scripts/start_server.sh ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # usage: start_server.sh <gguf> [extra llama-server args, e.g. --control-vector f.gguf] -- identical server configuration for every model under test
3
+ cd /c/Users/user/lfm3proj
4
+ taskkill //F //IM llama-server.exe >/dev/null 2>&1; sleep 2
5
+ (/c/Users/user/llamabin/llama-server.exe -m "$1" "${@:2}" -ngl 99 -np 32 -c 557056 -bs -fa on -ctxcp 0 -cram 0 --metrics --port 8080 --no-webui > results/server.log 2>&1 &)
6
+ for i in $(seq 1 100); do curl -s -m 2 localhost:8080/health | grep -q '"ok"' && break; grep -q "exiting" results/server.log && break; sleep 3; done
7
+ curl -s localhost:8080/health; echo
research/scripts/stress.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stress-test module for code answers: the way a strong competitive programmer checks a solution.
2
+
3
+ For a program that already passes the public examples (after test-and-repair), the model writes, in separate turns:
4
+ * two brute-force solutions (simplest obviously-correct method, any complexity), each kept only if it passes the
5
+ public examples;
6
+ * a generator of small random valid inputs.
7
+ Random inputs are run through the brute forces and the program. An input is a counterexample only when the brute
8
+ forces agree with each other and the program disagrees (or crashes / hangs on a tiny input). The model is then
9
+ shown the smallest counterexample and fixes the program; a fix is kept only if it still passes the public examples
10
+ and agrees with the brute forces on strictly more random inputs. Hidden tests are never used.
11
+
12
+ Records that are not candidates (no program, failing public examples) are copied unchanged, so the comparison with
13
+ the source run is exactly paired.
14
+ usage: stress.py --src results/lcb_rep/lcb.jsonl --items data/lcb/lcb6.jsonl --out results/lcb_stress/lcb.jsonl
15
+ """
16
+ import argparse
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+ import threading
22
+ import time
23
+ import urllib.request
24
+ import zlib
25
+ from concurrent.futures import ThreadPoolExecutor
26
+
27
+ from transformers import AutoTokenizer
28
+
29
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
30
+ from grade import extract_code # noqa: E402
31
+ from lcb_tests import HARNESS, _run, run_lcb # noqa: E402
32
+
33
+ THINK_END, EOS = 124902, 124900
34
+
35
+ ap = argparse.ArgumentParser()
36
+ ap.add_argument("--src", required=True)
37
+ ap.add_argument("--items", nargs="+", required=True)
38
+ ap.add_argument("--out", required=True)
39
+ ap.add_argument("--rounds", type=int, default=2)
40
+ ap.add_argument("--think", type=int, default=4096)
41
+ ap.add_argument("--reserve", type=int, default=1536)
42
+ ap.add_argument("--n-inputs", type=int, default=40)
43
+ ap.add_argument("--workers", type=int, default=24)
44
+ ap.add_argument("--tle", action="store_true", help="also check the speed on maximum-size inputs")
45
+ ap.add_argument("--time-limit", type=float, default=6.0)
46
+ ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
47
+ ap.add_argument("--port", type=int, default=8080)
48
+ args = ap.parse_args()
49
+
50
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
51
+ ENDS = {i for i in range(len(tok)) if (t := tok.convert_ids_to_tokens(i)) is not None and (t.endswith(".") or t.endswith("Ċ"))}
52
+ SAMPLING = {"temperature": 0.6, "top_p": 0.95, "top_k": 50, "min_p": 0.0, "repeat_penalty": 1.0,
53
+ "samplers": ["top_k", "top_p", "temperature"]}
54
+
55
+ STATEMENT = re.compile(r"\n\n(Write a Python 3 program|Complete the following starter code|You will use the following "
56
+ r"starter code).*", re.S)
57
+
58
+ BRUTE = ("{statement}\n\n---\n\nWrite a BRUTE-FORCE solution to this problem: the simplest program that is obviously "
59
+ "correct, for example by trying every possibility. Ignore efficiency completely; it will only be run on tiny "
60
+ "inputs. {form}")
61
+ FORM_STDIN = ("It must read the input from standard input in the format above and print the answer. Put the complete "
62
+ "program in a single ```python code block.")
63
+ FORM_FUNC = ("Complete the same starter code (the class Solution with the method {func}) and put the full class in a "
64
+ "single ```python code block.")
65
+ GEN = ("{statement}\n\n---\n\nWrite a Python program that prints ONE random valid test input for this problem. "
66
+ "{fmt} The input will be used to compare a solution with a slow brute force, so make it tiny and tricky: sizes "
67
+ "from the smallest allowed up to about 6, and values drawn from a very small range (for example 1 to 3, or "
68
+ "only two different letters) so that equal values, repeats, ties and edge cases appear often. Every constraint "
69
+ "of the problem must still hold. Use the random module seeded with int(sys.argv[1]). Put the complete program "
70
+ "in a single ```python code block.")
71
+ FMT_STDIN = "Print the input exactly in the input format of the problem (as it would be given on standard input)."
72
+ FMT_FUNC = ("The input is the list of arguments of the method {func}: print each argument as one JSON value "
73
+ "(json.dumps) on its own line, in the order of the parameters.")
74
+ MAXGEN = ("{statement}\n\n---\n\nWrite a Python program that prints ONE valid test input of the MAXIMUM size allowed "
75
+ "by the constraints (the largest allowed sizes and values), for checking the running time of a solution. "
76
+ "{fmt} Use random content, seeded with int(sys.argv[1]), and make sure every constraint holds. Put the "
77
+ "complete program in a single ```python code block.")
78
+ SLOW = ("Your program gives correct answers on small inputs, but it is too slow: on a valid input of the maximum size "
79
+ "allowed by the constraints it did not finish within the time limit. The input starts like this:\n{head}\n\n"
80
+ "Find an asymptotically faster algorithm (check the constraints to see which complexity is needed) and give "
81
+ "the complete program in a single ```python code block.")
82
+ FEEDBACK = ("I tested your program against a slow brute-force solution on small random inputs, and they disagree.\n\n"
83
+ "{fail}\n\nFind the mistake (re-read the problem statement if your understanding may be wrong) and give the "
84
+ "complete corrected program in a single ```python code block.")
85
+
86
+
87
+ def post(body):
88
+ for attempt in range(5):
89
+ try:
90
+ req = urllib.request.Request(f"http://localhost:{args.port}/completion", data=json.dumps(body).encode(),
91
+ headers={"Content-Type": "application/json"})
92
+ return json.loads(urllib.request.urlopen(req, timeout=3600).read())
93
+ except Exception:
94
+ if attempt == 4:
95
+ raise
96
+ time.sleep(5)
97
+
98
+
99
+ def generate(msgs, seed):
100
+ ids = tok.apply_chat_template(msgs, tokenize=True, add_generation_prompt=True, return_dict=False)
101
+ r = post({"prompt": ids, "n_predict": args.think + args.reserve, "seed": seed, "cache_prompt": False,
102
+ "return_tokens": True, **SAMPLING})
103
+ gen = [t for t in r["tokens"] if t != EOS]
104
+ if THINK_END in gen:
105
+ return r["content"].split("</think>", 1)[1], r["tokens_predicted"]
106
+ lim = min(args.think, len(gen))
107
+ cut = next((j for j in range(lim, 0, -1) if gen[j - 1] in ENDS), lim)
108
+ r2 = post({"prompt": ids + gen[:cut] + [THINK_END], "n_predict": args.reserve, "seed": seed + 17,
109
+ "cache_prompt": False, **SAMPLING})
110
+ return r2["content"], cut + r2["tokens_predicted"]
111
+
112
+
113
+ def outputs(code, item, inputs):
114
+ """Outputs of a candidate on the inputs (None where it crashes or times out)."""
115
+ if item["testtype"] == "functional":
116
+ src = HARNESS.split("_tests = json.loads")[0].replace("__CODE__", code) + (
117
+ "\nfor _inp in json.loads(sys.stdin.read()):\n"
118
+ " try:\n"
119
+ " _r = getattr(Solution(), %r)(*[json.loads(_l) for _l in _inp.strip().split('\\n')])\n"
120
+ " print(json.dumps(list(_r) if isinstance(_r, tuple) else _r))\n"
121
+ " except Exception:\n"
122
+ " print('null')\n"
123
+ " sys.stdout.flush()\n" % item["func_name"])
124
+ rc, so, _ = _run(src, json.dumps(inputs), 3 + 0.5 * len(inputs))
125
+ lines = so.strip().split("\n") if so.strip() else []
126
+ return [(l if l != "null" else None) for l in lines] + [None] * (len(inputs) - len(lines))
127
+ res = []
128
+ for inp in inputs:
129
+ rc, so, _ = _run(code, inp, 3)
130
+ res.append(" ".join(so.split()) if rc == 0 and so.strip() else None)
131
+ return res
132
+
133
+
134
+ def make_inputs(gen_code, item):
135
+ seen, out = set(), []
136
+ for k in range(args.n_inputs):
137
+ rc, so, _ = _run(f"import sys\nsys.argv = ['gen', '{k}']\n" + gen_code, "", 3) # _run passes no argv
138
+ inp = so.strip()
139
+ if rc != 0 or not inp or len(inp) > 2000 or inp in seen:
140
+ continue
141
+ if item["testtype"] == "functional":
142
+ try:
143
+ [json.loads(l) for l in inp.split("\n")]
144
+ except Exception:
145
+ continue
146
+ seen.add(inp)
147
+ out.append(inp + "\n")
148
+ return out
149
+
150
+
151
+ def process(rec):
152
+ try:
153
+ return _process(rec)
154
+ except Exception as e:
155
+ return {**rec, "stress_error": repr(e)[:200]}
156
+
157
+
158
+ def _process(rec):
159
+ item = items[rec["id"]]
160
+ if "</think>" not in rec["completion"]:
161
+ return rec
162
+ final = rec["completion"].split("</think>", 1)[1]
163
+ code = extract_code(final)
164
+ if code is None:
165
+ return rec
166
+ k, n, _ = run_lcb(code, item, item["public"])
167
+ if k < n:
168
+ return rec
169
+ statement = STATEMENT.sub("", item["prompt"])
170
+ func = item.get("func_name")
171
+ seed = zlib.crc32(rec["id"].encode()) % 1000003 * 7 + 9000
172
+ used = [0]
173
+
174
+ def gen(msgs, sd):
175
+ text, t = generate(msgs, sd)
176
+ used[0] += t
177
+ return text
178
+
179
+ status, dbg, tests = correctness_inputs(item, statement, func, seed, gen)
180
+ score = lambda c: stress_score(c, item, tests)
181
+ ok0, bad = score(code) if tests else (0, [])
182
+ msgs = [{"role": "user", "content": item["prompt"]}]
183
+ kept = 0
184
+ if bad: # phase 1: disagreement with the brute forces on small inputs
185
+ attempts = [(ok0, final, code)]
186
+ cur = final
187
+ for rnd in range(args.rounds):
188
+ fail = "\n\n".join(
189
+ f"Input:\n{i.strip()}\n\nBrute-force output:\n{e}\n\nYour program's output:\n"
190
+ f"{g if g is not None else '(it crashed or ran too long)'}"
191
+ for i, e, g in sorted(bad, key=lambda x: len(x[0]))[:2])
192
+ msgs = msgs + [{"role": "assistant", "content": cur}, {"role": "user", "content": FEEDBACK.format(fail=fail)}]
193
+ cur = gen(msgs, seed + 100 + rnd)
194
+ c = extract_code(cur)
195
+ if c is None or run_lcb(c, item, item["public"])[0] < len(item["public"]):
196
+ attempts.append((-1, cur, c))
197
+ continue
198
+ okr, bad = score(c)
199
+ attempts.append((okr, cur, c))
200
+ if not bad:
201
+ break
202
+ kept = max(range(len(attempts)), key=lambda k: (attempts[k][0], -k)) # must agree on strictly more inputs
203
+ ok_best, final, code = attempts[kept]
204
+ status = f"fixed {ok_best}/{len(tests)} (was {ok0})" if kept else f"unfixed {ok0}/{len(tests)}"
205
+ msgs = [{"role": "user", "content": item["prompt"]}]
206
+ elif tests:
207
+ status = f"agree {len(tests)}"
208
+ tle = ""
209
+ if args.tle: # phase 2: speed on inputs of the maximum size allowed by the constraints
210
+ fmt = FMT_FUNC.format(func=func) if item["testtype"] == "functional" else FMT_STDIN
211
+ gtext = gen([{"role": "user", "content": MAXGEN.format(statement=statement, fmt=fmt)}], seed + 300)
212
+ big = make_big(extract_code(gtext), item)
213
+ dbg["big_inputs"] = len(big)
214
+ if big and not finishes(code, item, big):
215
+ tle = "slow"
216
+ ok_ref = score(code)[0] if tests else 0
217
+ cur = final
218
+ for rnd in range(args.rounds):
219
+ msgs = msgs + [{"role": "assistant", "content": cur},
220
+ {"role": "user", "content": SLOW.format(head=big[0][:200].strip())}]
221
+ cur = gen(msgs, seed + 400 + rnd)
222
+ c = extract_code(cur)
223
+ if (c is None or run_lcb(c, item, item["public"])[0] < len(item["public"])
224
+ or (tests and score(c)[0] < ok_ref) or not finishes(c, item, big)):
225
+ continue
226
+ final, code, tle = cur, c, "sped_up"
227
+ break
228
+ out = {**rec, "n_tokens": rec["n_tokens"] + used[0], "stress": status, "stress_dbg": dbg, "tle": tle}
229
+ if kept or tle == "sped_up":
230
+ out["completion"] = "<think>(stress-tested)</think>" + final
231
+ return out
232
+
233
+
234
+ def correctness_inputs(item, statement, func, seed, gen):
235
+ """Brute forces (kept only if they pass the public examples) + small random inputs -> tests they agree on."""
236
+ form = FORM_FUNC.format(func=func) if item["testtype"] == "functional" else FORM_STDIN
237
+ starter = re.search(r"```python\n.*?```", item["prompt"], re.S)
238
+ brute_prompt = BRUTE.format(statement=statement, form=form) + (f"\n\n{starter.group(0)}" if starter else "")
239
+ brutes = []
240
+ for b in range(2):
241
+ bc = extract_code(gen([{"role": "user", "content": brute_prompt}], seed + b))
242
+ if bc and run_lcb(bc, item, item["public"])[0] == len(item["public"]):
243
+ brutes.append(bc)
244
+ dbg = {"brutes": len(brutes)}
245
+ if not brutes:
246
+ return "no_brute", dbg, []
247
+ fmt = FMT_FUNC.format(func=func) if item["testtype"] == "functional" else FMT_STDIN
248
+ text = gen([{"role": "user", "content": GEN.format(statement=statement, fmt=fmt)}], seed + 7)
249
+ gc = extract_code(text)
250
+ inputs = make_inputs(gc, item) if gc else []
251
+ dbg.update(inputs=len(inputs), gen=(gc or text)[-1500:])
252
+ if not inputs:
253
+ return "no_inputs", dbg, []
254
+ outs = [outputs(bc, item, inputs) for bc in brutes]
255
+ tests = [(inp, outs[0][j]) for j, inp in enumerate(inputs)
256
+ if outs[0][j] is not None and all(o[j] == outs[0][j] for o in outs)]
257
+ dbg.update(tests=len(tests), sample_input=inputs[0][:300])
258
+ return ("no_agreement" if not tests else ""), dbg, tests
259
+
260
+
261
+ def stress_score(c, item, tests):
262
+ got = outputs(c, item, [i for i, _ in tests])
263
+ bad = [(i, e, g) for (i, e), g in zip(tests, got) if not same(g, e)]
264
+ return len(tests) - len(bad), bad
265
+
266
+
267
+ def make_big(code, item):
268
+ if not code:
269
+ return []
270
+ out = []
271
+ for k in range(2):
272
+ rc, so, _ = _run(f"import sys\nsys.argv = ['gen', '{k}']\n" + code, "", 15)
273
+ if rc == 0 and so.strip() and len(so) < 30_000_000:
274
+ if item["testtype"] == "functional":
275
+ try:
276
+ [json.loads(l) for l in so.strip().split("\n")]
277
+ except Exception:
278
+ continue
279
+ out.append(so.strip() + "\n")
280
+ return out
281
+
282
+
283
+ def finishes(code, item, big):
284
+ if item["testtype"] == "functional":
285
+ src = HARNESS.split("_tests = json.loads")[0].replace("__CODE__", code) + (
286
+ "\nfor _inp in json.loads(sys.stdin.read()):\n"
287
+ " getattr(Solution(), %r)(*[json.loads(_l) for _l in _inp.strip().split('\\n')])\n" % item["func_name"])
288
+ return all(_run(src, json.dumps([b]), args.time_limit)[0] == 0 for b in big)
289
+ return all(_run(code, b, args.time_limit)[0] == 0 for b in big)
290
+
291
+
292
+ def same(got, exp):
293
+ if got is None:
294
+ return False
295
+ a, b = got.split(), exp.split()
296
+ if len(a) != len(b):
297
+ return False
298
+ for x, y in zip(a, b):
299
+ if x != y:
300
+ try:
301
+ if abs(float(x) - float(y)) > 1e-6 * max(1.0, abs(float(y))):
302
+ return False
303
+ except ValueError:
304
+ return False
305
+ return True
306
+
307
+
308
+ items = {}
309
+ for path in args.items:
310
+ with open(path, encoding="utf-8") as f:
311
+ for l in f:
312
+ r = json.loads(l)
313
+ items[r["id"]] = r
314
+ with open(args.src, encoding="utf-8") as f:
315
+ recs = [json.loads(l) for l in f]
316
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
317
+ t0 = time.time()
318
+ lock = threading.Lock()
319
+ done = [0]
320
+
321
+
322
+ def run(rec):
323
+ r = process(rec)
324
+ with lock:
325
+ done[0] += 1
326
+ if done[0] % 20 == 0:
327
+ print(f"{done[0]}/{len(recs)} {(time.time() - t0) / 60:.1f} min", flush=True)
328
+ return r
329
+
330
+
331
+ with ThreadPoolExecutor(args.workers) as ex:
332
+ out = list(ex.map(run, recs))
333
+ with open(args.out, "w", encoding="utf-8") as f:
334
+ for r in out:
335
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
336
+ st = [r.get("stress", "") for r in out]
337
+ print(f"{len(out)} records: " + ", ".join(f"{k} {sum(s.startswith(k) for s in st)}" for k in
338
+ ("agree", "fixed", "unfixed", "no_brute", "no_inputs", "no_agreement")) +
339
+ f", slow {sum(r.get('tle') in ('slow', 'sped_up') for r in out)}, sped up {sum(r.get('tle') == 'sped_up' for r in out)}"
340
+ f", errors {sum('stress_error' in r for r in out)}, {(time.time() - t0) / 60:.1f} min")
research/scripts/svg_program.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Program-aided drawing for SVG tasks: instead of writing coordinates by hand, the model writes a Python program that
2
+ computes the geometry (math.cos / math.sin, loops) and prints the SVG. The program is run in the sandbox; if it fails
3
+ or prints no valid SVG, the model gets the error and fixes the program (up to --rounds times). The printed SVG becomes
4
+ the answer, so the result is scored and rendered exactly like a direct SVG answer.
5
+ usage: svg_program.py --tasks data/visual/tasks.jsonl --out results/vis_svgprog/vis.jsonl --n 2
6
+ """
7
+ import argparse
8
+ import json
9
+ import os
10
+ import re
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ import time
15
+ import urllib.request
16
+ import xml.dom.minidom
17
+ import zlib
18
+ from concurrent.futures import ThreadPoolExecutor
19
+
20
+ from transformers import AutoTokenizer
21
+
22
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
23
+ import sandbox # noqa: E402
24
+ from grade import extract_code # noqa: E402
25
+
26
+ THINK_END, EOS = 124902, 124900
27
+ ap = argparse.ArgumentParser()
28
+ ap.add_argument("--tasks", default="data/visual/tasks.jsonl")
29
+ ap.add_argument("--out", required=True)
30
+ ap.add_argument("--n", type=int, default=2)
31
+ ap.add_argument("--rounds", type=int, default=2)
32
+ ap.add_argument("--think", type=int, default=8192)
33
+ ap.add_argument("--reserve", type=int, default=4096)
34
+ ap.add_argument("--workers", type=int, default=16)
35
+ ap.add_argument("--port", type=int, default=8080)
36
+ ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
37
+ args = ap.parse_args()
38
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
39
+ ENDS = {i for i in range(len(tok)) if (t := tok.convert_ids_to_tokens(i)) is not None and (t.endswith(".") or t.endswith("Ċ"))}
40
+ SAMPLING = {"temperature": 0.6, "top_p": 0.95, "top_k": 50, "min_p": 0.0, "repeat_penalty": 1.0,
41
+ "samplers": ["top_k", "top_p", "temperature"]}
42
+ PROMPT = ("{task}\n\nDo not write the SVG by hand. Write a Python 3 program that builds the SVG and prints it to "
43
+ "standard output. Compute every position with arithmetic and the math module (math.cos, math.sin, "
44
+ "math.radians, loops over repeated parts) instead of guessing numbers, and remember that in SVG the y axis "
45
+ "points down. Use only the standard library. Give the program in a single ```python code block.")
46
+ FIX = ("I ran your program. {problem}\n\nFix the program and give the complete corrected program in a single "
47
+ "```python code block.")
48
+ TMP = os.path.join(tempfile.gettempdir(), "lfm3_svgprog")
49
+ os.makedirs(TMP, exist_ok=True)
50
+
51
+
52
+ def wait_server():
53
+ """The night chain restarts the shared server with other models; generate only on the original model."""
54
+ while True:
55
+ try:
56
+ p = json.loads(urllib.request.urlopen(f"http://localhost:{args.port}/props", timeout=10).read())
57
+ if "lfm3-orig-q5km" in p.get("model_path", ""):
58
+ return p["default_generation_settings"]["n_ctx"]
59
+ except Exception:
60
+ pass
61
+ time.sleep(15)
62
+
63
+
64
+ def post(body):
65
+ for attempt in range(40):
66
+ n_ctx = wait_server()
67
+ body = {**body, "n_predict": min(body["n_predict"], n_ctx - len(body["prompt"]) - 16)} # fit the slot
68
+ try:
69
+ req = urllib.request.Request(f"http://localhost:{args.port}/completion", data=json.dumps(body).encode(),
70
+ headers={"Content-Type": "application/json"})
71
+ return json.loads(urllib.request.urlopen(req, timeout=3600).read())
72
+ except Exception:
73
+ if attempt == 39:
74
+ raise
75
+ time.sleep(10)
76
+
77
+
78
+ def generate(msgs, seed):
79
+ ids = tok.apply_chat_template(msgs, tokenize=True, add_generation_prompt=True, return_dict=False)
80
+ r = post({"prompt": ids, "n_predict": args.think + args.reserve, "seed": seed, "cache_prompt": False,
81
+ "return_tokens": True, **SAMPLING})
82
+ gen = [t for t in r["tokens"] if t != EOS]
83
+ if THINK_END in gen:
84
+ return r["content"].split("</think>", 1)[1], r["tokens_predicted"]
85
+ lim = min(args.think, len(gen))
86
+ cut = next((j for j in range(lim, 0, -1) if gen[j - 1] in ENDS), lim)
87
+ pre = tok("```python\n", add_special_tokens=False)["input_ids"]
88
+ r2 = post({"prompt": ids + gen[:cut] + [THINK_END] + pre, "n_predict": args.reserve, "seed": seed + 17,
89
+ "cache_prompt": False, **SAMPLING})
90
+ c = r2["content"]
91
+ return "```python\n" + (c if "```" in c else c.rstrip() + "\n```"), cut + len(pre) + r2["tokens_predicted"]
92
+
93
+
94
+ def run_program(code):
95
+ """Runs the program; returns (svg or None, problem description or None)."""
96
+ fd, path = tempfile.mkstemp(suffix=".py", dir=TMP)
97
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
98
+ f.write(code)
99
+ try:
100
+ r = sandbox.run([sys.executable, "-I", path], input=b"", capture_output=True, timeout=20, cwd=TMP,
101
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
102
+ out, err = r.stdout.decode(errors="replace"), r.stderr.decode(errors="replace")
103
+ if r.returncode != 0:
104
+ return None, "It failed:\n" + "\n".join(err.strip().splitlines()[-6:])
105
+ except subprocess.TimeoutExpired:
106
+ return None, "It did not finish within 20 seconds."
107
+ finally:
108
+ try:
109
+ os.remove(path)
110
+ except OSError:
111
+ pass
112
+ m = re.search(r"<svg.*</svg>", out, re.S)
113
+ if not m:
114
+ return None, "It printed no <svg>...</svg> document. Its output began with:\n" + out[:300]
115
+ try:
116
+ xml.dom.minidom.parseString(m.group(0).encode("utf-8"))
117
+ except Exception as e:
118
+ return None, f"The printed SVG is not valid XML: {str(e)[:200]}"
119
+ return m.group(0), None
120
+
121
+
122
+ def process(job):
123
+ task, s = job
124
+ seed = zlib.crc32(task["id"].encode()) % 1000003 * 7 + s + 11000
125
+ msgs = [{"role": "user", "content": PROMPT.format(task=task["prompt"].split("Answer with")[0].strip())}]
126
+ used, svg, final, rounds = 0, None, "", 0
127
+ for rnd in range(args.rounds + 1):
128
+ final, n = generate(msgs, seed + rnd)
129
+ used += n
130
+ code = extract_code(final)
131
+ if code is None:
132
+ problem = "Your answer does not contain the program in a ```python code block."
133
+ else:
134
+ svg, problem = run_program(code)
135
+ if svg:
136
+ break
137
+ if rnd < args.rounds:
138
+ rounds += 1
139
+ msgs = msgs + [{"role": "assistant", "content": final}, {"role": "user", "content": FIX.format(problem=problem)}]
140
+ answer = f"```svg\n{svg}\n```" if svg else final
141
+ print(f"{task['id']} s{s}: {'svg' if svg else 'no svg'} after {rounds} repairs, {used} tokens", flush=True)
142
+ return {"id": task["id"], "task": "visual", "sample": s, "completion": "<think>(program)</think>" + answer,
143
+ "n_tokens": used, "truncated": False, "program_rounds": rounds}
144
+
145
+
146
+ tasks = [json.loads(l) for l in open(args.tasks, encoding="utf-8")]
147
+ jobs = [(t, s) for t in tasks if t["kind"] == "svg" for s in range(args.n)]
148
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
149
+ t0 = time.time()
150
+ with ThreadPoolExecutor(args.workers) as ex:
151
+ out = list(ex.map(process, jobs))
152
+ with open(args.out, "w", encoding="utf-8") as f:
153
+ for r in out:
154
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
155
+ print(f"{len(out)} answers, {sum('```svg' in r['completion'] for r in out)} with a valid SVG, "
156
+ f"{(time.time() - t0) / 60:.1f} min")
research/scripts/teach_gen.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generates teaching data with the system "model + deliberation probes + answer checker" on training problems.
2
+
3
+ For every problem: N sampled solutions (exact token ids kept) graded against the known answer. Solutions that end
4
+ wrong or are cut off by the budget are read out every --every thinking tokens: the thinking so far is closed at the
5
+ last sentence end (+ </think>) and the model gives its answer greedily (the deliberation module's probe). If the
6
+ read-out answer is correct from some cut to the end of the solution, that is a moment where the model already knew
7
+ the answer and should have stopped: the module then writes the model's own closing answer after that cut (sampled,
8
+ checked) -> a "stop" example.
9
+
10
+ out record: id, task, sample, tokens, n_tokens, truncated, correct, probes[{cut, ans, ok}], stop{cut, tokens, ok}
11
+ usage: teach_gen.py --inp data/teach/math_pilot.jsonl --out results/teach/pilot.jsonl --n 4 --max-tokens 6144
12
+ """
13
+ import argparse
14
+ import json
15
+ import os
16
+ import re
17
+ import sys
18
+ import threading
19
+ import time
20
+ import urllib.request
21
+ import zlib
22
+ from concurrent.futures import ThreadPoolExecutor
23
+
24
+ from transformers import AutoTokenizer
25
+
26
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
+ from grade import grade_math, grade_mmlupro, last_boxed # noqa: E402
28
+
29
+ THINK_END, EOS = 124902, 124900
30
+ ap = argparse.ArgumentParser()
31
+ ap.add_argument("--inp", nargs="+", required=True)
32
+ ap.add_argument("--out", required=True)
33
+ ap.add_argument("--n", type=int, default=4)
34
+ ap.add_argument("--max-tokens", type=int, default=6144)
35
+ ap.add_argument("--every", type=int, default=1024)
36
+ ap.add_argument("--workers", type=int, default=40)
37
+ ap.add_argument("--limit", type=int, default=0)
38
+ ap.add_argument("--port", type=int, default=8080)
39
+ ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
40
+ ap.add_argument("--system-file", default="")
41
+ ap.add_argument("--no-probes", action="store_true", help="skip read-out probes of failed samples (no stop examples)")
42
+ args = ap.parse_args()
43
+
44
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
45
+ ENDS = {i for i in range(len(tok)) if (t := tok.convert_ids_to_tokens(i)) is not None and (t.endswith(".") or t.endswith("Ċ"))}
46
+ PROBE = {"math": ("The final answer is $\\boxed{", 48, ["}$", "}\n"]), "mmlupro": ("The answer is (", 3, [")"])}
47
+ PROBE_IDS = {k: tok(v[0], add_special_tokens=False)["input_ids"] for k, v in PROBE.items()}
48
+ SAMP = {"temperature": 0.6, "top_p": 0.95, "top_k": 50, "min_p": 0.0, "repeat_penalty": 1.0,
49
+ "samplers": ["top_k", "top_p", "temperature"]}
50
+ SYSTEM = open(args.system_file, encoding="utf-8").read() if args.system_file else ""
51
+
52
+
53
+ def post(body):
54
+ for attempt in range(6):
55
+ try:
56
+ req = urllib.request.Request(f"http://localhost:{args.port}/completion", data=json.dumps(body).encode(),
57
+ headers={"Content-Type": "application/json"})
58
+ return json.loads(urllib.request.urlopen(req, timeout=3600).read())
59
+ except Exception:
60
+ if attempt == 5:
61
+ raise
62
+ time.sleep(5)
63
+
64
+
65
+ def correct(task, text, gold):
66
+ return grade_math(text, gold) if task == "math" else grade_mmlupro(text, gold)
67
+
68
+
69
+ def kind(it):
70
+ return "math" if it["task"] in ("math", "math500", "aime", "gsm8k") else "mmlupro"
71
+
72
+
73
+ def probe(ids, th, cut, k, gold):
74
+ suf, n_new, stops = PROBE[k]
75
+ r = post({"prompt": ids + th[:cut] + [THINK_END] + PROBE_IDS[k], "n_predict": n_new, "temperature": 0.0,
76
+ "cache_prompt": False, "stop": stops})
77
+ c = r["content"].strip()
78
+ text = suf + c + ("}$" if k == "math" else ").")
79
+ return {"cut": cut, "ans": c[:60], "ok": bool(correct(k, text, gold))}
80
+
81
+
82
+ def run(job):
83
+ it, s = job
84
+ k = kind(it)
85
+ msgs = ([{"role": "system", "content": SYSTEM}] if SYSTEM else []) + [{"role": "user", "content": it["prompt"]}]
86
+ ids = tok.apply_chat_template(msgs, tokenize=True, add_generation_prompt=True, return_dict=False)
87
+ seed = zlib.crc32(it["id"].encode()) % 1000003 * 7 + s + 900001
88
+ r = post({"prompt": ids, "n_predict": args.max_tokens, "seed": seed, "cache_prompt": False, "return_tokens": True,
89
+ **SAMP})
90
+ gen = [t for t in r["tokens"] if t != EOS]
91
+ text = r["content"]
92
+ trunc = r.get("stop_type") == "limit"
93
+ ok = "</think>" in text and correct(k, text.split("</think>", 1)[1], it["answer"])
94
+ rec = {"id": it["id"], "task": it["task"], "sample": s, "tokens": r["tokens"], "n_tokens": r["tokens_predicted"],
95
+ "truncated": trunc, "correct": bool(ok), "prompt_len": len(ids)}
96
+ if not ok and not args.no_probes:
97
+ th = gen[: gen.index(THINK_END)] if THINK_END in gen else gen
98
+ probes = []
99
+ for c in range(args.every, len(th) + 1, args.every):
100
+ cut = next((j for j in range(c, c - 400, -1) if j <= len(th) and th[j - 1] in ENDS), None)
101
+ if cut and (not probes or cut > probes[-1]["cut"]):
102
+ probes.append(probe(ids, th, cut, k, it["answer"]))
103
+ rec["probes"] = probes
104
+ # earliest cut from which every later read-out is correct (at least 2 read-outs)
105
+ start = None
106
+ for i in range(len(probes) - 1, -1, -1):
107
+ if probes[i]["ok"]:
108
+ start = i
109
+ else:
110
+ break
111
+ if start is not None and len(probes) - start >= 2:
112
+ cut = probes[start]["cut"]
113
+ r2 = post({"prompt": ids + th[:cut] + [THINK_END], "n_predict": 1536, "seed": seed + 31,
114
+ "cache_prompt": False, "return_tokens": True, **SAMP})
115
+ ans_ok = correct(k, r2["content"], it["answer"]) and r2.get("stop_type") != "limit"
116
+ rec["stop"] = {"cut": cut, "tokens": th[:cut] + [THINK_END] + r2["tokens"], "ok": bool(ans_ok)}
117
+ return rec
118
+
119
+
120
+ items = []
121
+ for path in args.inp:
122
+ rows = [json.loads(l) for l in open(path, encoding="utf-8")]
123
+ items += rows[: args.limit] if args.limit else rows
124
+ done = set()
125
+ if os.path.exists(args.out):
126
+ done = {(json.loads(l)["id"], json.loads(l)["sample"]) for l in open(args.out, encoding="utf-8")}
127
+ os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
128
+ jobs = [(it, s) for it in items for s in range(args.n) if (it["id"], s) not in done]
129
+ print(f"{len(jobs)} jobs", flush=True)
130
+ lock, f_out, t0, cnt = threading.Lock(), open(args.out, "a", encoding="utf-8"), time.time(), [0]
131
+
132
+
133
+ def work(job):
134
+ try:
135
+ rec = run(job)
136
+ except Exception as e:
137
+ print("ERR", job[0]["id"], repr(e)[:200], flush=True)
138
+ return
139
+ with lock:
140
+ f_out.write(json.dumps(rec, ensure_ascii=False) + "\n")
141
+ f_out.flush()
142
+ cnt[0] += 1
143
+ if cnt[0] % 100 == 0:
144
+ print(f"{cnt[0]}/{len(jobs)} {(time.time() - t0) / 60:.1f} min", flush=True)
145
+
146
+
147
+ with ThreadPoolExecutor(args.workers) as ex:
148
+ list(ex.map(work, jobs))
149
+ print("done", cnt[0], f"{(time.time() - t0) / 60:.1f} min")
research/scripts/teach_probe.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adds "stop" examples for solutions that ended CORRECT but long: the thinking is read out every --every tokens
2
+ (deliberation probe); from the earliest cut after which every read-out is the correct answer, the model's own
3
+ closing answer is generated and checked. The tail after that cut was re-verification the model did not need.
4
+ Runs on teach_gen.py output in place (adds rec["stop"] where it is shorter than the natural solution).
5
+ usage: teach_probe.py --inp results/teach/pilot.jsonl --items data/teach/math_pilot.jsonl [--min-len 1500]
6
+ """
7
+ import argparse
8
+ import json
9
+ import os
10
+ import sys
11
+ import time
12
+ import urllib.request
13
+ import zlib
14
+ from concurrent.futures import ThreadPoolExecutor
15
+
16
+ from transformers import AutoTokenizer
17
+
18
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
+ from grade import grade_math, grade_mmlupro # noqa: E402
20
+
21
+ THINK_END, EOS = 124902, 124900
22
+ ap = argparse.ArgumentParser()
23
+ ap.add_argument("--inp", required=True)
24
+ ap.add_argument("--items", nargs="+", required=True)
25
+ ap.add_argument("--every", type=int, default=1024)
26
+ ap.add_argument("--min-len", type=int, default=1500)
27
+ ap.add_argument("--workers", type=int, default=48)
28
+ ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
29
+ args = ap.parse_args()
30
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
31
+ ENDS = {i for i in range(len(tok)) if (t := tok.convert_ids_to_tokens(i)) is not None and (t.endswith(".") or t.endswith("Ċ"))}
32
+ PROBE = {"math": ("The final answer is $\\boxed{", 48, ["}$", "}\n"]), "mmlupro": ("The answer is (", 3, [")"])}
33
+ PROBE_IDS = {k: tok(v[0], add_special_tokens=False)["input_ids"] for k, v in PROBE.items()}
34
+ SAMP = {"temperature": 0.6, "top_p": 0.95, "top_k": 50, "min_p": 0.0, "samplers": ["top_k", "top_p", "temperature"]}
35
+ items = {}
36
+ for p in args.items:
37
+ for l in open(p, encoding="utf-8"):
38
+ r = json.loads(l)
39
+ items[r["id"]] = r
40
+
41
+
42
+ def post(body):
43
+ for attempt in range(6):
44
+ try:
45
+ req = urllib.request.Request("http://localhost:8080/completion", data=json.dumps(body).encode(),
46
+ headers={"Content-Type": "application/json"})
47
+ return json.loads(urllib.request.urlopen(req, timeout=3600).read())
48
+ except Exception:
49
+ if attempt == 5:
50
+ raise
51
+ time.sleep(5)
52
+
53
+
54
+ def ok(k, text, gold):
55
+ return grade_math(text, gold) if k == "math" else grade_mmlupro(text, gold)
56
+
57
+
58
+ def work(rec):
59
+ if not rec["correct"] or rec["n_tokens"] < args.min_len or rec.get("stop") or rec["id"] not in items:
60
+ return rec
61
+ it = items[rec["id"]]
62
+ k = "math" if it["task"] == "math" else "mmlupro"
63
+ gen = [t for t in rec["tokens"] if t != EOS]
64
+ if THINK_END not in gen:
65
+ return rec
66
+ th = gen[: gen.index(THINK_END)]
67
+ ids = tok.apply_chat_template([{"role": "user", "content": it["prompt"]}], tokenize=True,
68
+ add_generation_prompt=True, return_dict=False)
69
+ probes = []
70
+ for c in range(args.every, len(th) + 1, args.every):
71
+ cut = next((j for j in range(c, c - 400, -1) if th[j - 1] in ENDS), None)
72
+ if not cut or (probes and cut <= probes[-1]["cut"]):
73
+ continue
74
+ suf, n_new, stops = PROBE[k]
75
+ r = post({"prompt": ids + th[:cut] + [THINK_END] + PROBE_IDS[k], "n_predict": n_new, "temperature": 0.0,
76
+ "cache_prompt": False, "stop": stops})
77
+ c_ = r["content"].strip()
78
+ probes.append({"cut": cut, "ans": c_[:60],
79
+ "ok": bool(ok(k, suf + c_ + ("}$" if k == "math" else ")."), it["answer"]))})
80
+ rec["probes_correct"] = probes
81
+ start = None
82
+ for i in range(len(probes) - 1, -1, -1):
83
+ if probes[i]["ok"]:
84
+ start = i
85
+ else:
86
+ break
87
+ if start is None:
88
+ return rec
89
+ cut = probes[start]["cut"]
90
+ seed = zlib.crc32(rec["id"].encode()) % 1000003 * 7 + rec["sample"] + 77
91
+ r2 = post({"prompt": ids + th[:cut] + [THINK_END], "n_predict": 1536, "seed": seed, "cache_prompt": False,
92
+ "return_tokens": True, **SAMP})
93
+ good = ok(k, r2["content"], it["answer"]) and r2.get("stop_type") != "limit"
94
+ toks = th[:cut] + [THINK_END] + r2["tokens"]
95
+ if good and len(toks) < rec["n_tokens"]:
96
+ rec["stop"] = {"cut": cut, "tokens": toks, "ok": True, "from_correct": True}
97
+ return rec
98
+
99
+
100
+ recs = [json.loads(l) for l in open(args.inp, encoding="utf-8")]
101
+ t0 = time.time()
102
+ with ThreadPoolExecutor(args.workers) as ex:
103
+ out = list(ex.map(work, recs))
104
+ with open(args.inp, "w", encoding="utf-8") as f:
105
+ for r in out:
106
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
107
+ n = sum(1 for r in out if r.get("stop", {}).get("from_correct"))
108
+ saved = [r["n_tokens"] - len(r["stop"]["tokens"]) for r in out if r.get("stop", {}).get("from_correct")]
109
+ print(f"{n} early-stop examples from correct solutions, mean tokens saved {sum(saved) / max(1, n):.0f}, "
110
+ f"{(time.time() - t0) / 60:.1f} min")
research/scripts/teach_to_rl.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Converts teach_gen.py records into rl_train.py inputs (rollouts with exact token ids + graded rewards), keeping
2
+ only informative groups (some samples right, some wrong) — the problems where the model is unstable and a policy
3
+ update can move pass@1 towards pass@k.
4
+ usage: teach_to_rl.py --inp results/teach/rft1.jsonl ... --out-dir results/rl/rft1
5
+ """
6
+ import argparse
7
+ import collections
8
+ import json
9
+ import os
10
+
11
+ ap = argparse.ArgumentParser()
12
+ ap.add_argument("--inp", nargs="+", required=True)
13
+ ap.add_argument("--out-dir", required=True)
14
+ a = ap.parse_args()
15
+ by = collections.defaultdict(list)
16
+ for p in a.inp:
17
+ for l in open(p, encoding="utf-8"):
18
+ r = json.loads(l)
19
+ by[r["id"]].append(r)
20
+ os.makedirs(a.out_dir, exist_ok=True)
21
+ n_inf = n_seq = 0
22
+ with open(os.path.join(a.out_dir, "rollouts.jsonl"), "w", encoding="utf-8") as fr, \
23
+ open(os.path.join(a.out_dir, "graded.jsonl"), "w", encoding="utf-8") as fg:
24
+ for pid, g in by.items():
25
+ ks = {r["sample"]: r for r in g}
26
+ rs = [r["correct"] for r in ks.values()]
27
+ if all(rs) or not any(rs):
28
+ continue
29
+ n_inf += 1
30
+ for s, r in ks.items():
31
+ toks = [t for t in r["tokens"] if t != 124900]
32
+ fr.write(json.dumps({"id": pid, "sample": s, "tokens": toks,
33
+ "stop_type": "limit" if r["truncated"] else "eos"}) + "\n")
34
+ fg.write(json.dumps({"id": pid, "sample": s, "correct": bool(r["correct"])}) + "\n")
35
+ n_seq += 1
36
+ print(f"{len(by)} problems, {n_inf} informative groups, {n_seq} sequences")
research/scripts/tir.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tool-integrated reasoning client: the model may call a `python` tool while it reasons; the module executes the code
2
+ and feeds the output back as a tool turn (the model's native tool format), then the model continues thinking.
3
+
4
+ The whole multi-turn exchange is flattened into the gen.py record format, so grade.py grades it unchanged:
5
+ completion = "<think>" + all turns (thinking, calls, outputs) + "</think>" + final answer of the last turn.
6
+ The token budget (--max-tokens) counts every generated token over all turns.
7
+
8
+ usage: tir.py --inp data/quick/mini.jsonl --out results/tir/mini.jsonl [--max-tokens 6144] [--system default]
9
+ """
10
+ import argparse
11
+ import json
12
+ import os
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+ import threading
18
+ import time
19
+ import urllib.request
20
+ import zlib
21
+ from concurrent.futures import ThreadPoolExecutor
22
+
23
+ from transformers import AutoTokenizer
24
+
25
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
+ import sandbox # noqa: E402
27
+
28
+ EOS, THINK_END = 124900, 124902
29
+ FORCE_PREFIX = "<tool_call>\n<function=python>\n<parameter=code>\n"
30
+ SYSTEMS = {
31
+ "none": "",
32
+ "default": "You have a Python interpreter (the `python` tool). Use it whenever a calculation, an enumeration, "
33
+ "a simulation or a check of your answer can be done by code: write a short program, print the result, "
34
+ "and use the printed output in your reasoning.",
35
+ "verify": "You have a Python interpreter (the `python` tool). Plan: think the problem through, then before giving "
36
+ "the final answer call the `python` tool to compute or check the result by code (brute force, "
37
+ "enumeration, direct computation or substitution). If the code disagrees with your reasoning, find "
38
+ "the mistake before answering.",
39
+ }
40
+ TOOL = {"type": "function", "function": {
41
+ "name": "python",
42
+ "description": "Execute Python 3 code and return everything it prints (stdout and errors). Available: math, "
43
+ "itertools, fractions, collections, sympy, numpy. Always print() the values you need. "
44
+ "Time limit 10 seconds.",
45
+ "parameters": {"type": "object", "properties": {"code": {"type": "string", "description": "Python source"}},
46
+ "required": ["code"]}}}
47
+
48
+ ap = argparse.ArgumentParser()
49
+ ap.add_argument("--inp", nargs="+", required=True)
50
+ ap.add_argument("--out", required=True)
51
+ ap.add_argument("--n", type=int, default=1)
52
+ ap.add_argument("--max-tokens", type=int, default=6144)
53
+ ap.add_argument("--max-calls", type=int, default=8)
54
+ ap.add_argument("--system", default="default")
55
+ ap.add_argument("--force-check", default="", help="if set (e.g. '# Independent check\\n'), a model that ends its "
56
+ "thinking without running code is made to write a check program starting with this line")
57
+ ap.add_argument("--tasks", default="", help="comma list: only these tasks get the tool (others: plain generation)")
58
+ ap.add_argument("--temperature", type=float, default=0.6)
59
+ ap.add_argument("--top-p", type=float, default=0.95)
60
+ ap.add_argument("--top-k", type=int, default=50)
61
+ ap.add_argument("--workers", type=int, default=24)
62
+ ap.add_argument("--limit", type=int, default=0)
63
+ ap.add_argument("--port", type=int, default=8080)
64
+ ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
65
+ ap.add_argument("--seed", type=int, default=0)
66
+ args = ap.parse_args()
67
+
68
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
69
+ SANDBOX = os.path.join(tempfile.gettempdir(), "lfm3_tir")
70
+ os.makedirs(SANDBOX, exist_ok=True)
71
+ CALL_RE = re.compile(r"<tool_call>\s*<function=python>\s*<parameter=code>\n?(.*?)\n?</parameter>", re.S)
72
+
73
+
74
+ def salvage(code):
75
+ """Small models often drift back into prose inside a code block: keep the longest prefix that is valid Python."""
76
+ lines = code.split("\n")
77
+ for n in range(len(lines), 0, -1):
78
+ try:
79
+ compile("\n".join(lines[:n]), "<code>", "exec")
80
+ return "\n".join(lines[:n]), n < len(lines)
81
+ except SyntaxError:
82
+ continue
83
+ return code, False
84
+
85
+
86
+ def run_code(code, timeout=10):
87
+ code, cut = salvage(code)
88
+ note = "(note: the end of the code was not valid Python and was skipped)\n" if cut else ""
89
+ try:
90
+ p = sandbox.run([sys.executable, "-I", "-c", code], capture_output=True, text=True, timeout=timeout,
91
+ cwd=SANDBOX, encoding="utf-8", errors="replace",
92
+ env={**os.environ, "PYTHONIOENCODING": "utf-8"})
93
+ out = p.stdout + (("\n" + p.stderr.strip().splitlines()[-1]) if p.returncode and p.stderr.strip() else "")
94
+ except subprocess.TimeoutExpired:
95
+ out = "TimeoutError: the code ran longer than 10 seconds"
96
+ out = note + (out.strip() or "(no output; use print())")
97
+ return out if len(out) <= 1500 else out[:1500] + "\n... (output truncated)"
98
+
99
+
100
+ def post(body):
101
+ for attempt in range(5):
102
+ try:
103
+ req = urllib.request.Request(f"http://localhost:{args.port}/completion", data=json.dumps(body).encode(),
104
+ headers={"Content-Type": "application/json"})
105
+ return json.loads(urllib.request.urlopen(req, timeout=3600).read())
106
+ except Exception:
107
+ if attempt == 4:
108
+ raise
109
+ time.sleep(5)
110
+
111
+
112
+ def solve(it, s):
113
+ use_tool = not args.tasks or it["task"] in args.tasks.split(",")
114
+ msgs = ([{"role": "system", "content": SYSTEMS[args.system]}] if use_tool and SYSTEMS[args.system] else []) + \
115
+ [{"role": "user", "content": it["prompt"]}]
116
+ ids = tok.apply_chat_template(msgs, tools=[TOOL] if use_tool else None, tokenize=True,
117
+ add_generation_prompt=True, return_dict=False)
118
+ seed = args.seed * 1000003 + zlib.crc32(it["id"].encode()) % 1000003 * 7 + s
119
+ used, calls, parts, stop_type = 0, [], [], None
120
+ while True:
121
+ res = post({"prompt": ids, "n_predict": args.max_tokens - used, "temperature": args.temperature,
122
+ "top_p": args.top_p, "top_k": args.top_k, "min_p": 0.0, "repeat_penalty": 1.0,
123
+ "seed": seed + 101 * len(calls), "cache_prompt": False, "return_tokens": True,
124
+ "samplers": ["top_k", "top_p", "temperature"]})
125
+ gen = [t for t in res["tokens"] if t != EOS]
126
+ used += res["tokens_predicted"]
127
+ text = res["content"]
128
+ stop_type = res.get("stop_type")
129
+ m = CALL_RE.search(text.split("</think>", 1)[1]) if "</think>" in text else None
130
+ if use_tool and not m and args.force_check and not calls and THINK_END in gen \
131
+ and used < args.max_tokens - 1024:
132
+ # the model finished thinking without running anything: make its next move a code check of the answer
133
+ k = gen.index(THINK_END) + 1
134
+ used -= len(gen) - k # the answer text after </think> is replaced by the check
135
+ pre = tok(FORCE_PREFIX + args.force_check, add_special_tokens=False)["input_ids"]
136
+ r2 = post({"prompt": ids + gen[:k] + pre, "n_predict": min(1536, args.max_tokens - used),
137
+ "temperature": args.temperature, "top_p": args.top_p, "top_k": args.top_k, "min_p": 0.0,
138
+ "seed": seed + 7, "cache_prompt": False, "stop": ["</parameter>"],
139
+ "samplers": ["top_k", "top_p", "temperature"]})
140
+ used += r2["tokens_predicted"]
141
+ code = args.force_check + r2["content"]
142
+ out = run_code(code)
143
+ calls.append({"code": code, "out": out, "forced": True})
144
+ parts.append(tok.decode(gen[:k]).replace("</think>", "\n") + FORCE_PREFIX + code +
145
+ "\n</parameter>\n</function>\n</tool_call>\n<tool_response>\n" + out + "\n</tool_response>\n")
146
+ ids = ids + gen[:k] + pre + tok(r2["content"].rstrip("\n") + "\n</parameter>\n</function>\n</tool_call>"
147
+ "<|im_end|>\n<|im_start|>tool\n<tool_response>\n" + out +
148
+ "\n</tool_response><|im_end|>\n<|im_start|>assistant\n<think>",
149
+ add_special_tokens=False)["input_ids"]
150
+ continue
151
+ if not (use_tool and m and stop_type != "limit" and len(calls) < args.max_calls and used < args.max_tokens):
152
+ parts.append(text)
153
+ break
154
+ out = run_code(m.group(1))
155
+ calls.append({"code": m.group(1), "out": out})
156
+ turn = text.split("</tool_call>", 1)[0] + "</tool_call>"
157
+ parts.append(turn.replace("</think>", "\n", 1) + "\n<tool_response>\n" + out + "\n</tool_response>\n")
158
+ ids = ids + gen + tok("<|im_end|>\n<|im_start|>tool\n<tool_response>\n" + out +
159
+ "\n</tool_response><|im_end|>\n<|im_start|>assistant\n<think>",
160
+ add_special_tokens=False)["input_ids"]
161
+ last = parts[-1]
162
+ if "</think>" in last:
163
+ think, final = last.split("</think>", 1)
164
+ completion = "<think>" + "".join(parts[:-1]) + think + "</think>" + final
165
+ else:
166
+ completion = "<think>" + "".join(parts)
167
+ return {"id": it["id"], "task": it["task"], "sample": s, "completion": completion, "n_tokens": used,
168
+ "truncated": stop_type == "limit", "stop_type": stop_type, "n_calls": len(calls), "calls": calls}
169
+
170
+
171
+ items = []
172
+ for path in args.inp:
173
+ with open(path, encoding="utf-8") as f:
174
+ rows = [json.loads(l) for l in f]
175
+ items += rows[: args.limit] if args.limit else rows
176
+ done = set()
177
+ if os.path.exists(args.out):
178
+ with open(args.out, encoding="utf-8") as f:
179
+ done = {(json.loads(l)["id"], json.loads(l)["sample"]) for l in f}
180
+ os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
181
+ jobs = [(it, s) for it in items for s in range(args.n) if (it["id"], s) not in done]
182
+ print(f"{len(jobs)} jobs ({len(done)} already done)", flush=True)
183
+ lock, out_f, t0, cnt = threading.Lock(), open(args.out, "a", encoding="utf-8"), time.time(), [0]
184
+
185
+
186
+ def run(job):
187
+ rec = solve(*job)
188
+ with lock:
189
+ out_f.write(json.dumps(rec, ensure_ascii=False) + "\n")
190
+ out_f.flush()
191
+ cnt[0] += 1
192
+ if cnt[0] % 20 == 0 or cnt[0] == len(jobs):
193
+ print(f"{cnt[0]}/{len(jobs)} {(time.time() - t0) / 60:.1f} min", flush=True)
194
+
195
+
196
+ with ThreadPoolExecutor(args.workers) as ex:
197
+ list(ex.map(run, jobs))
research/scripts/vis_autofix.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic fixes of visual answers before rendering (no model call), measured on recorded runs.
2
+
3
+ importmap the page loads three.js as ES modules (imports from 'three', 'three/addons/...' or from the unpkg URL,
4
+ whose addons import the bare name 'three') but has no import map: insert the import map of the task
5
+ at the start of <head>.
6
+
7
+ usage: vis_autofix.py --src results/vis_rep/vis.jsonl --out results/vis_rep_fix/vis.jsonl
8
+ Records whose answer is changed get "autofix": [...]; the rest are copied unchanged (exactly paired).
9
+ """
10
+ import argparse
11
+ import json
12
+ import os
13
+ import re
14
+ import sys
15
+
16
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
17
+ from render import extract # noqa: E402
18
+
19
+ IMPORT_MAP = ('<script type="importmap">{"imports": {"three": "https://unpkg.com/three@0.160.0/build/three.module.js", '
20
+ '"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"}}</script>')
21
+ USES_THREE_MODULE = re.compile(r"""from\s+['"](three|three/addons/[^'"]+|https://unpkg\.com/three@[^'"]+)['"]""")
22
+
23
+
24
+ def fix_importmap(src):
25
+ if 'type="importmap"' in src or "type='importmap'" in src or not USES_THREE_MODULE.search(src):
26
+ return src
27
+ m = re.search(r"<head[^>]*>", src, re.I)
28
+ if m:
29
+ return src[:m.end()] + "\n" + IMPORT_MAP + src[m.end():]
30
+ m = re.search(r"<script", src, re.I)
31
+ return src[:m.start()] + IMPORT_MAP + "\n" + src[m.start():] if m else src
32
+
33
+
34
+ def autofix(text, kind):
35
+ """Returns (new answer text, list of applied fixes)."""
36
+ if kind != "html" or text is None:
37
+ return text, []
38
+ src = extract(text, kind)
39
+ if not src:
40
+ return text, []
41
+ new = fix_importmap(src)
42
+ if new == src:
43
+ return text, []
44
+ return text.replace(src, new), ["importmap"]
45
+
46
+
47
+ if __name__ == "__main__":
48
+ ap = argparse.ArgumentParser()
49
+ ap.add_argument("--src", required=True)
50
+ ap.add_argument("--out", required=True)
51
+ ap.add_argument("--tasks", default="data/visual/tasks.jsonl")
52
+ a = ap.parse_args()
53
+ tasks = {json.loads(l)["id"]: json.loads(l) for l in open(a.tasks, encoding="utf-8")}
54
+ os.makedirs(os.path.dirname(a.out), exist_ok=True)
55
+ n = 0
56
+ with open(a.out, "w", encoding="utf-8") as f:
57
+ for l in open(a.src, encoding="utf-8"):
58
+ r = json.loads(l)
59
+ c = r["completion"]
60
+ if "</think>" in c:
61
+ head, fin = c.split("</think>", 1)
62
+ new, fixes = autofix(fin, tasks[r["id"]]["kind"])
63
+ if fixes:
64
+ r = {**r, "completion": head + "</think>" + new, "autofix": fixes}
65
+ n += 1
66
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
67
+ print(f"{n} answers changed -> {a.out}")
research/scripts/visual_eval.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Renders visual answers and scores them with objective checks (scripts/render.py).
2
+ A result is "clean" when it parses, raises no JavaScript/console errors, loads every resource, is not blank and, for
3
+ animation tasks, actually moves. Screenshots go to results/<tag>/png/ for side-by-side review.
4
+ usage: visual_eval.py --tag vis_base (records results/<tag>/vis.jsonl from gen.py / posthoc)
5
+ visual_eval.py --tag vis_claude --dir data/visual/claude (reference files <id>.svg / <id>.html)
6
+ """
7
+ import argparse
8
+ import collections
9
+ import glob
10
+ import json
11
+ import os
12
+ import re
13
+ import sys
14
+
15
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
16
+ from render import Renderer, extract # noqa: E402
17
+
18
+ ap = argparse.ArgumentParser()
19
+ ap.add_argument("--tag", required=True)
20
+ ap.add_argument("--dir")
21
+ ap.add_argument("--tasks", default="data/visual/tasks.jsonl")
22
+ a = ap.parse_args()
23
+ tasks = {json.loads(l)["id"]: json.loads(l) for l in open(a.tasks, encoding="utf-8")}
24
+
25
+
26
+ def clean(rep, task):
27
+ return (not rep.get("parse_error") and not rep["errors"] and not rep["failed"] and not rep["blank"]
28
+ and (rep["animated"] or not task["animated"]))
29
+
30
+
31
+ out_dir = os.path.join("results", a.tag)
32
+ os.makedirs(os.path.join(out_dir, "png"), exist_ok=True)
33
+ sources = {}
34
+ if a.dir:
35
+ for p in glob.glob(os.path.join(a.dir, "*.*")):
36
+ tid = "visual/" + os.path.splitext(os.path.basename(p))[0]
37
+ if tid in tasks:
38
+ sources[(tid, 0)] = open(p, encoding="utf-8").read()
39
+ else:
40
+ for p in glob.glob(os.path.join(out_dir, "*.jsonl")):
41
+ for l in open(p, encoding="utf-8"):
42
+ r = json.loads(l)
43
+ if r["id"] in tasks:
44
+ fin = r["completion"].split("</think>", 1)[1] if "</think>" in r["completion"] else None
45
+ sources[(r["id"], r.get("sample", 0))] = extract(fin, tasks[r["id"]]["kind"])
46
+
47
+ rend = Renderer()
48
+ rows = []
49
+ for (tid, s), src in sorted(sources.items()):
50
+ task = tasks[tid]
51
+ png = os.path.join(out_dir, "png", f"{tid.split('/')[1]}_{s}.png")
52
+ if not src:
53
+ rep = {"errors": ["no code block"], "failed": [], "parse_error": "no code", "blank": True, "animated": False,
54
+ "coverage": 0.0, "overflow": False}
55
+ else:
56
+ rep = rend.render(task["kind"], src, png)
57
+ rep.update(id=tid, sample=s, cat=task["cat"], clean=clean(rep, task), chars=len(src or ""))
58
+ rows.append(rep)
59
+ rend.close()
60
+ with open(os.path.join(out_dir, "visual.json"), "w", encoding="utf-8") as f:
61
+ json.dump(rows, f, ensure_ascii=False, indent=1)
62
+ by = collections.defaultdict(list)
63
+ for r in rows:
64
+ by[r["cat"]].append(r)
65
+ print(f"{a.tag}: {len(rows)} answers")
66
+ for cat in ("svg", "html", "canvas", "three"):
67
+ v = by.get(cat, [])
68
+ if v:
69
+ print(f" {cat:7s} n={len(v):3d} clean {100 * sum(r['clean'] for r in v) / len(v):5.1f}% "
70
+ f"js-errors {sum(bool(r['errors']) for r in v):3d} failed-loads {sum(bool(r['failed']) for r in v):2d} "
71
+ f"blank {sum(r['blank'] for r in v):2d} no-code {sum(r.get('parse_error') == 'no code' for r in v):2d}")
72
+ print(f" ALL clean {100 * sum(r['clean'] for r in rows) / max(len(rows), 1):5.1f}%")
73
+ for r in rows:
74
+ if not r["clean"]:
75
+ print(" ", r["id"], r["sample"], (r.get("parse_error") or "") , r["errors"][:2], r["failed"][:1],
76
+ "blank" if r["blank"] else "", "" if r["animated"] else "static")
research/scripts/visual_eyes.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Eyes for a text-only model: render -> look -> fix.
2
+
3
+ The answer is rendered in headless Chrome (scripts/render.py). A vision-language model (LFM3-VL-2B: the vision
4
+ sibling of LFM3 on the same language backbone, served by a second llama-server with --mmproj) describes the
5
+ screenshot without being told the task. LFM3 then compares the description with the task and returns an improved
6
+ file or NO CHANGES. An improvement is kept only if it renders with no more technical problems than before
7
+ (same weights as visual_repair.py), so the module cannot break a working answer.
8
+
9
+ --no-eyes is the control: the same revision turn without the description ("look at your file again"), to separate
10
+ the value of seeing the result from the value of one more revision.
11
+ Records whose answer is unchanged are copied, so comparisons with the source run are exactly paired.
12
+ usage: visual_eyes.py --src results/vis_rep_fix/vis.jsonl --out results/vis_eyes/vis.jsonl [--no-eyes]
13
+ """
14
+ import argparse
15
+ import base64
16
+ import json
17
+ import os
18
+ import re
19
+ import sys
20
+ import threading
21
+ import time
22
+ import urllib.request
23
+ import zlib
24
+ from concurrent.futures import ThreadPoolExecutor
25
+
26
+ from transformers import AutoTokenizer
27
+
28
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
29
+ from render import Renderer, extract # noqa: E402
30
+
31
+ THINK_END, EOS = 124902, 124900
32
+ ap = argparse.ArgumentParser()
33
+ ap.add_argument("--src", required=True)
34
+ ap.add_argument("--out", required=True)
35
+ ap.add_argument("--tasks", default="data/visual/tasks.jsonl")
36
+ ap.add_argument("--rounds", type=int, default=1)
37
+ ap.add_argument("--think", type=int, default=6144)
38
+ ap.add_argument("--reserve", type=int, default=4096)
39
+ ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
40
+ ap.add_argument("--port", type=int, default=8080)
41
+ ap.add_argument("--vlm-port", type=int, default=8091)
42
+ ap.add_argument("--workers", type=int, default=8)
43
+ ap.add_argument("--model-tag", default="lfm3-orig-q5km", help="the generation server must serve this model file")
44
+ ap.add_argument("--no-eyes", action="store_true", help="control: revision turn without the description")
45
+ args = ap.parse_args()
46
+
47
+ DESCRIBE = ("Describe this screenshot precisely for someone who cannot see it. List every visible element: shapes "
48
+ "and objects, their colours, sizes and positions (top, bottom, left, right, centre), all visible text "
49
+ "(quote it), and anything that looks broken, cut off, overlapping or empty. Describe only what is "
50
+ "actually visible; do not guess what it was meant to show.")
51
+ EYES = ("I opened your {what} in Chrome (window 1024x768) and asked someone who cannot see your code to describe "
52
+ "the screen{frame}. Their description:\n\n\"\"\"\n{desc}\n\"\"\"\n\nCompare it with the task. If anything "
53
+ "requested is missing, wrong, misplaced, unreadable or looks unpolished, improve the file and give the "
54
+ "complete corrected file in a single ```{kind} code block. If the result already matches the task well, "
55
+ "answer only NO CHANGES.")
56
+ SELF = ("Look at your {what} again and compare it with the task. If anything requested is missing, wrong, misplaced, "
57
+ "unreadable or looks unpolished, improve the file and give the complete corrected file in a single "
58
+ "```{kind} code block. If the result already matches the task well, answer only NO CHANGES.")
59
+
60
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
61
+ ENDS = {i for i in range(len(tok)) if (t := tok.convert_ids_to_tokens(i)) is not None and (t.endswith(".") or t.endswith("Ċ"))}
62
+ SAMPLING = {"temperature": 0.6, "top_p": 0.95, "top_k": 50, "min_p": 0.0, "repeat_penalty": 1.0,
63
+ "samplers": ["top_k", "top_p", "temperature"]}
64
+ tasks = {json.loads(l)["id"]: json.loads(l) for l in open(args.tasks, encoding="utf-8")}
65
+ SHOTS = os.path.join(os.path.dirname(args.out), "shots")
66
+ os.makedirs(SHOTS, exist_ok=True)
67
+
68
+
69
+ def get_json(url, body=None, timeout=3600):
70
+ data = json.dumps(body).encode() if body is not None else None
71
+ req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
72
+ return json.loads(urllib.request.urlopen(req, timeout=timeout).read())
73
+
74
+
75
+ def wait_server():
76
+ """The night chain restarts the shared server with other models; generate only on the right one."""
77
+ while True:
78
+ try:
79
+ p = get_json(f"http://localhost:{args.port}/props", timeout=10)
80
+ if args.model_tag in p.get("model_path", ""):
81
+ return p["default_generation_settings"]["n_ctx"]
82
+ except Exception:
83
+ pass
84
+ time.sleep(15)
85
+
86
+
87
+ def post(body):
88
+ for attempt in range(40):
89
+ n_ctx = wait_server()
90
+ try:
91
+ room = n_ctx - len(body["prompt"]) - 16 # the slot size changes when the chain restarts the server
92
+ return get_json(f"http://localhost:{args.port}/completion", {**body, "n_predict": min(body["n_predict"], room)})
93
+ except Exception:
94
+ if attempt == 39:
95
+ raise
96
+ time.sleep(10)
97
+
98
+
99
+ def generate(msgs, seed):
100
+ ids = tok.apply_chat_template(msgs, tokenize=True, add_generation_prompt=True, return_dict=False)
101
+ r = post({"prompt": ids, "n_predict": args.think + args.reserve, "seed": seed, "cache_prompt": False,
102
+ "return_tokens": True, **SAMPLING})
103
+ gen = [t for t in r["tokens"] if t != EOS]
104
+ if THINK_END in gen:
105
+ return r["content"].split("</think>", 1)[1], r["tokens_predicted"]
106
+ lim = min(args.think, len(gen))
107
+ cut = next((j for j in range(lim, 0, -1) if gen[j - 1] in ENDS), lim)
108
+ r2 = post({"prompt": ids + gen[:cut] + [THINK_END], "n_predict": args.reserve, "seed": seed + 17,
109
+ "cache_prompt": False, **SAMPLING})
110
+ return r2["content"], cut + r2["tokens_predicted"]
111
+
112
+
113
+ def look(png):
114
+ b64 = base64.b64encode(open(png, "rb").read()).decode()
115
+ body = {"messages": [{"role": "user", "content": [
116
+ {"type": "image_url", "image_url": {"url": "data:image/png;base64," + b64}},
117
+ {"type": "text", "text": DESCRIBE}]}],
118
+ "temperature": 0.1, "top_k": 50, "top_p": 1.0, "min_p": 0.0, "repeat_penalty": 1.1, "max_tokens": 1500,
119
+ "seed": 1, "chat_template_kwargs": {"enable_thinking": False}} # thinking: 6x slower, the description is enough
120
+ for attempt in range(5):
121
+ try:
122
+ msg = get_json(f"http://localhost:{args.vlm_port}/v1/chat/completions", body)["choices"][0]["message"]
123
+ text = re.sub(r"<think>.*?</think>", "", msg.get("content") or "", flags=re.S).strip()
124
+ return text or "(no description)"
125
+ except Exception:
126
+ if attempt == 4:
127
+ raise
128
+ time.sleep(10)
129
+
130
+
131
+ def badness(rep, task):
132
+ """Technical problems with the weights of visual_repair.py (0 = renders cleanly)."""
133
+ if rep is None:
134
+ return 10
135
+ return (4 * bool(rep.get("parse_error")) + 3 * bool(rep["errors"]) + 3 * bool(rep["failed"]) + 3 * rep["blank"]
136
+ + (task["animated"] and not rep["animated"]) + bool(rep.get("overflow")))
137
+
138
+
139
+ local = threading.local()
140
+
141
+
142
+ def renderer():
143
+ if not hasattr(local, "r"):
144
+ local.r = Renderer()
145
+ return local.r
146
+
147
+
148
+ def process(rec):
149
+ task, s = tasks[rec["id"]], rec.get("sample", 0)
150
+ name = f"{rec['id'].split('/')[-1]}_{s}"
151
+ fin = rec["completion"].split("</think>", 1)[1] if "</think>" in rec["completion"] else None
152
+ src = extract(fin, task["kind"])
153
+ if not src:
154
+ return {**rec, "eyes": "no source"}
155
+ png = os.path.join(SHOTS, f"{name}_r0.png")
156
+ bad = badness(renderer().render(task["kind"], src, png), task)
157
+ what = "SVG" if task["kind"] == "svg" else "page"
158
+ frame = " (one frame of the animation)" if task["animated"] else ""
159
+ msgs = [{"role": "user", "content": task["prompt"]}]
160
+ cur, used, log = fin, 0, []
161
+ seed = zlib.crc32(rec["id"].encode()) % 1000003 * 7 + s + 9000
162
+ for rnd in range(args.rounds):
163
+ if args.no_eyes:
164
+ ask = SELF.format(what=what, kind=task["kind"])
165
+ else:
166
+ desc = look(png)
167
+ log.append({"round": rnd + 1, "description": desc})
168
+ ask = EYES.format(what=what, kind=task["kind"], frame=frame, desc=desc)
169
+ msgs = msgs + [{"role": "assistant", "content": cur}, {"role": "user", "content": ask}]
170
+ out, n = generate(msgs, seed + rnd)
171
+ used += n
172
+ new = extract(out, task["kind"])
173
+ if not new or new.strip() == extract(cur, task["kind"]).strip():
174
+ log.append({"round": rnd + 1, "result": "no changes"})
175
+ break
176
+ png2 = os.path.join(SHOTS, f"{name}_r{rnd + 1}.png")
177
+ b = badness(renderer().render(task["kind"], new, png2), task)
178
+ if b > bad:
179
+ log.append({"round": rnd + 1, "result": f"rejected: problems {bad} -> {b}"})
180
+ break
181
+ log.append({"round": rnd + 1, "result": f"kept: problems {bad} -> {b}"})
182
+ cur, bad, png = out, b, png2
183
+ changed = cur is not fin
184
+ print(f"{rec['id']} s{s}: {log[-1].get('result', '?') if log else '-'}", flush=True)
185
+ r = {**rec, "eyes_log": log, "eyes_changed": changed, "n_tokens": rec["n_tokens"] + used}
186
+ if changed:
187
+ r["completion"] = "<think>(eyes)</think>" + cur
188
+ return r
189
+
190
+
191
+ def safe(rec):
192
+ """One failing record (browser crash, server error) must not lose the whole run: it is copied unchanged."""
193
+ try:
194
+ return process(rec)
195
+ except Exception:
196
+ import traceback
197
+ print(f"{rec['id']} s{rec.get('sample', 0)}: ERROR " + traceback.format_exc(), flush=True)
198
+ return {**rec, "eyes_log": [{"result": "error"}], "eyes_changed": False}
199
+
200
+
201
+ recs = [json.loads(l) for l in open(args.src, encoding="utf-8")]
202
+ t0 = time.time()
203
+ with ThreadPoolExecutor(args.workers) as ex:
204
+ out = list(ex.map(safe, recs))
205
+ with open(args.out, "w", encoding="utf-8") as f:
206
+ for r in out:
207
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
208
+ print(f"{len(out)} records, {sum(r.get('eyes_changed', False) for r in out)} changed, "
209
+ f"{(time.time() - t0) / 60:.1f} min")
research/scripts/visual_loop.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared render-check-repair logic for visual answers, used by lfm3_deliberate.solve_visual.
2
+ (The same rules as scripts/visual_repair.py, which produced the measured numbers.)
3
+
4
+ problems(rep, animated, src, kind, hints) -> (weighted badness, feedback lines) from a render.py report;
5
+ feedback(kind, lines) -> the repair turn text; fix_importmap from vis_autofix (deterministic, no model call).
6
+ """
7
+ import re
8
+
9
+ from vis_autofix import IMPORT_MAP, fix_importmap # noqa: F401 (re-exported for callers)
10
+
11
+ # browser error -> the fix a web developer would apply (general knowledge, never a solution of a particular task)
12
+ HINTS = [
13
+ (r'Failed to resolve module specifier "three', "A module imports the bare name 'three' (three.js addons do this "
14
+ f"too). Put the import map in <head>, before any module script: {IMPORT_MAP}, then write "
15
+ "import * as THREE from 'three'; and import addons like import { OrbitControls } from "
16
+ "'three/addons/controls/OrbitControls.js';"),
17
+ (r"THREE is not defined", "three.js is not a global variable. Add the import map "
18
+ f"{IMPORT_MAP} and, inside <script type=\"module\">, write import * as THREE from 'three';"),
19
+ (r"THREE\.(\w+) is not a constructor", "THREE.{0} does not exist in three.js r160. Addons are imported "
20
+ "separately, e.g. import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; then "
21
+ "new OrbitControls(camera, renderer.domElement). Otherwise check the class name."),
22
+ (r"Assignment to constant variable", "A variable declared with const is assigned again: declare variables "
23
+ "that change with let."),
24
+ (r"ReferenceError: (\w+) is not defined", "'{0}' is used where it is not declared: declare it with const/let "
25
+ "at the top level, before the functions that use it run."),
26
+ (r"Cannot read properties of (?:undefined|null) \(reading '(\w+)'\)", "Something is undefined or null when "
27
+ "its property '{0}' is read: an element id that does not exist, an object used before it is created, or an "
28
+ "array index out of range."),
29
+ (r"(\S+) is not a function", "{0} is not a function: a property is called like a method or a name is "
30
+ "misspelled (canvas.width/height are numbers; drawing methods are ctx.fillRect, ctx.arc, ctx.beginPath, "
31
+ "ctx.fill, ctx.stroke; angles in Math are radians, there is no Math.radians)."),
32
+ ]
33
+ BLANK_THREE = ("Add lights for MeshStandardMaterial (AmbientLight and DirectionalLight), put the camera away from "
34
+ "the objects with camera.lookAt(0, 0, 0) and render every frame with "
35
+ "renderer.setAnimationLoop(() => renderer.render(scene, camera)).")
36
+ BLANK_CANVAS = ("Set canvas.width = innerWidth and canvas.height = innerHeight, draw in every frame and use a fill "
37
+ "color that differs from the background.")
38
+ STATIC_HINT = ("Compute positions from the elapsed time inside a requestAnimationFrame loop (or "
39
+ "renderer.setAnimationLoop) and redraw every frame.")
40
+ SVG_HINT = "Close every tag, quote every attribute value, write & as &amp; and keep one root <svg> element."
41
+
42
+
43
+ def hints_for(rep, animated, src):
44
+ out = []
45
+ for e in rep.get("errors", []):
46
+ for pat, text in HINTS:
47
+ m = re.search(pat, e)
48
+ if m:
49
+ h = text.replace("{0}", m.group(1)) if m.groups() else text
50
+ if h not in out:
51
+ out.append(h)
52
+ break
53
+ if rep.get("parse_error"):
54
+ out.append(SVG_HINT)
55
+ if rep.get("blank") and not rep.get("parse_error") and not rep.get("errors"):
56
+ out.append(BLANK_THREE if "three" in (src or "") else BLANK_CANVAS if "<canvas" in (src or "") else
57
+ "Make sure the content is visible: real sizes, colors that differ from the background, nothing "
58
+ "hidden by CSS.")
59
+ if animated and not rep.get("animated") and not rep.get("blank"):
60
+ out.append(STATIC_HINT)
61
+ return out
62
+
63
+
64
+ def problems(rep, animated, src, kind, hints=False):
65
+ """(weighted badness, feedback lines); 0 = renders cleanly."""
66
+ if not src:
67
+ return 10, [f"Your answer does not contain the complete file in a ```{kind} code block."]
68
+ bad, lines = 0, []
69
+ if rep.get("parse_error"):
70
+ bad += 4
71
+ lines.append(f"The SVG is not valid XML: {rep['parse_error']}")
72
+ if rep["errors"]:
73
+ bad += 3
74
+ lines += [f"JavaScript error: {e}" for e in rep["errors"][:4]]
75
+ if rep["failed"]:
76
+ bad += 3
77
+ lines += [f"This resource failed to load: {u}" for u in rep["failed"][:3]]
78
+ if rep["blank"]:
79
+ bad += 3
80
+ lines.append("Nothing is visible: the rendered page is blank (a single colour).")
81
+ if animated and not rep["animated"]:
82
+ bad += 1
83
+ lines.append("The animation does not move: two screenshots taken 0.7 seconds apart are identical.")
84
+ if rep.get("overflow"):
85
+ bad += 1
86
+ lines.append("The page is wider than the 1024 px window (there is a horizontal scrollbar).")
87
+ if hints and bad:
88
+ lines += ["Hint: " + h for h in hints_for(rep, animated, src)]
89
+ return bad, lines
90
+
91
+
92
+ def feedback(kind, lines):
93
+ what = "SVG" if kind == "svg" else "page"
94
+ return (f"I opened your {what} in Chrome (window 1024x768) and found these problems:\n"
95
+ + "\n".join("- " + x for x in lines)
96
+ + f"\n\nFix them and give the complete corrected file in a single ```{kind} code block.")
research/scripts/visual_repair.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Render-check-repair module for visual answers (SVG / HTML / canvas / three.js).
2
+
3
+ The answer is rendered in headless Chrome (scripts/render.py). If the browser reports problems (invalid SVG,
4
+ JavaScript errors, resources that fail to load, a blank page, an animation that does not move, horizontal overflow),
5
+ the model gets them as precise feedback in a new turn and returns a corrected file. A correction is kept only if it
6
+ has strictly fewer problems (weighted) than the previous best. Up to --rounds repairs.
7
+ Records without problems are copied unchanged, so the comparison with the source run is exactly paired.
8
+ usage: visual_repair.py --src results/vis_base_dm/vis.jsonl --out results/vis_rep/vis.jsonl
9
+ """
10
+ import argparse
11
+ import json
12
+ import os
13
+ import re
14
+ import sys
15
+ import time
16
+ import urllib.request
17
+ import zlib
18
+
19
+ from transformers import AutoTokenizer
20
+
21
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
22
+ from render import Renderer, extract # noqa: E402
23
+
24
+ THINK_END, EOS = 124902, 124900
25
+ ap = argparse.ArgumentParser()
26
+ ap.add_argument("--src", required=True)
27
+ ap.add_argument("--out", required=True)
28
+ ap.add_argument("--tasks", default="data/visual/tasks.jsonl")
29
+ ap.add_argument("--rounds", type=int, default=2)
30
+ ap.add_argument("--think", type=int, default=6144)
31
+ ap.add_argument("--reserve", type=int, default=4096)
32
+ ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
33
+ ap.add_argument("--port", type=int, default=8080)
34
+ ap.add_argument("--hints", action="store_true", help="add a known fix to each recognised browser error")
35
+ args = ap.parse_args()
36
+
37
+ IMPORT_MAP = ('<script type="importmap">{"imports": {"three": "https://unpkg.com/three@0.160.0/build/three.module.js", '
38
+ '"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"}}</script>')
39
+ # browser error -> the fix a web developer would apply (general knowledge, never a solution of a particular task)
40
+ HINTS = [
41
+ (r'Failed to resolve module specifier "three', "A module imports the bare name 'three' (three.js addons do this "
42
+ f"too). Put the import map in <head>, before any module script: {IMPORT_MAP}, then write "
43
+ "import * as THREE from 'three'; and import addons like import { OrbitControls } from "
44
+ "'three/addons/controls/OrbitControls.js';"),
45
+ (r"THREE is not defined", "three.js is not a global variable. Add the import map "
46
+ f"{IMPORT_MAP} and, inside <script type=\"module\">, write import * as THREE from 'three';"),
47
+ (r"THREE\.(\w+) is not a constructor", "THREE.{0} does not exist in three.js r160. Addons are imported "
48
+ "separately, e.g. import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; then "
49
+ "new OrbitControls(camera, renderer.domElement). Otherwise check the class name."),
50
+ (r"Assignment to constant variable", "A variable declared with const is assigned again: declare variables "
51
+ "that change with let."),
52
+ (r"ReferenceError: (\w+) is not defined", "'{0}' is used where it is not declared: declare it with const/let "
53
+ "at the top level, before the functions that use it run."),
54
+ (r"Cannot read properties of (?:undefined|null) \(reading '(\w+)'\)", "Something is undefined or null when "
55
+ "its property '{0}' is read: an element id that does not exist, an object used before it is created, or an "
56
+ "array index out of range."),
57
+ (r"(\S+) is not a function", "{0} is not a function: a property is called like a method or a name is "
58
+ "misspelled (canvas.width/height are numbers; drawing methods are ctx.fillRect, ctx.arc, ctx.beginPath, "
59
+ "ctx.fill, ctx.stroke; angles in Math are radians, there is no Math.radians)."),
60
+ ]
61
+ BLANK_THREE = ("Add lights for MeshStandardMaterial (AmbientLight and DirectionalLight), put the camera away from "
62
+ "the objects with camera.lookAt(0, 0, 0) and render every frame with "
63
+ "renderer.setAnimationLoop(() => renderer.render(scene, camera)).")
64
+ BLANK_CANVAS = ("Set canvas.width = innerWidth and canvas.height = innerHeight, draw in every frame and use a fill "
65
+ "color that differs from the background.")
66
+ STATIC_HINT = ("Compute positions from the elapsed time inside a requestAnimationFrame loop (or "
67
+ "renderer.setAnimationLoop) and redraw every frame.")
68
+ SVG_HINT = "Close every tag, quote every attribute value, write & as &amp; and keep one root <svg> element."
69
+
70
+
71
+ def hints_for(rep, task, src):
72
+ out = []
73
+ for e in rep.get("errors", []):
74
+ for pat, text in HINTS:
75
+ m = re.search(pat, e)
76
+ if m:
77
+ h = text.replace("{0}", m.group(1)) if m.groups() else text
78
+ if h not in out:
79
+ out.append(h)
80
+ break
81
+ if rep.get("parse_error"):
82
+ out.append(SVG_HINT)
83
+ if rep.get("blank") and not rep.get("parse_error") and not rep.get("errors"):
84
+ out.append(BLANK_THREE if "three" in (src or "") else BLANK_CANVAS if "<canvas" in (src or "") else
85
+ "Make sure the content is visible: real sizes, colors that differ from the background, nothing "
86
+ "hidden by CSS.")
87
+ if task["animated"] and not rep.get("animated") and not rep.get("blank"):
88
+ out.append(STATIC_HINT)
89
+ return out
90
+ tok = AutoTokenizer.from_pretrained(args.tokenizer)
91
+ ENDS = {i for i in range(len(tok)) if (t := tok.convert_ids_to_tokens(i)) is not None and (t.endswith(".") or t.endswith("Ċ"))}
92
+ SAMPLING = {"temperature": 0.6, "top_p": 0.95, "top_k": 50, "min_p": 0.0, "repeat_penalty": 1.0,
93
+ "samplers": ["top_k", "top_p", "temperature"]}
94
+ tasks = {json.loads(l)["id"]: json.loads(l) for l in open(args.tasks, encoding="utf-8")}
95
+
96
+
97
+ def post(body):
98
+ for attempt in range(5):
99
+ try:
100
+ req = urllib.request.Request(f"http://localhost:{args.port}/completion", data=json.dumps(body).encode(),
101
+ headers={"Content-Type": "application/json"})
102
+ return json.loads(urllib.request.urlopen(req, timeout=3600).read())
103
+ except Exception:
104
+ if attempt == 4:
105
+ raise
106
+ time.sleep(5)
107
+
108
+
109
+ def generate(msgs, seed):
110
+ ids = tok.apply_chat_template(msgs, tokenize=True, add_generation_prompt=True, return_dict=False)
111
+ r = post({"prompt": ids, "n_predict": args.think + args.reserve, "seed": seed, "cache_prompt": False,
112
+ "return_tokens": True, **SAMPLING})
113
+ gen = [t for t in r["tokens"] if t != EOS]
114
+ if THINK_END in gen:
115
+ return r["content"].split("</think>", 1)[1], r["tokens_predicted"]
116
+ lim = min(args.think, len(gen))
117
+ cut = next((j for j in range(lim, 0, -1) if gen[j - 1] in ENDS), lim)
118
+ r2 = post({"prompt": ids + gen[:cut] + [THINK_END], "n_predict": args.reserve, "seed": seed + 17,
119
+ "cache_prompt": False, **SAMPLING})
120
+ return r2["content"], cut + r2["tokens_predicted"]
121
+
122
+
123
+ def problems(rep, task, src):
124
+ """(weighted badness, feedback lines)."""
125
+ if not src:
126
+ return 10, [f"Your answer does not contain the complete file in a ```{task['kind']} code block."]
127
+ bad, lines = 0, []
128
+ if rep.get("parse_error"):
129
+ bad += 4
130
+ lines.append(f"The SVG is not valid XML: {rep['parse_error']}")
131
+ if rep["errors"]:
132
+ bad += 3
133
+ lines += [f"JavaScript error: {e}" for e in rep["errors"][:4]]
134
+ if rep["failed"]:
135
+ bad += 3
136
+ lines += [f"This resource failed to load: {u}" for u in rep["failed"][:3]]
137
+ if rep["blank"]:
138
+ bad += 3
139
+ lines.append("Nothing is visible: the rendered page is blank (a single colour).")
140
+ if task["animated"] and not rep["animated"]:
141
+ bad += 1
142
+ lines.append("The animation does not move: two screenshots taken 0.7 seconds apart are identical.")
143
+ if rep.get("overflow"):
144
+ bad += 1
145
+ lines.append("The page is wider than the 1024 px window (there is a horizontal scrollbar).")
146
+ if args.hints and bad:
147
+ lines += ["Hint: " + h for h in hints_for(rep, task, src)]
148
+ return bad, lines
149
+
150
+
151
+ FEEDBACK = ("I opened your {what} in Chrome (window 1024x768) and found these problems:\n{lines}\n\nFix them and give "
152
+ "the complete corrected file in a single ```{kind} code block.")
153
+
154
+ import threading
155
+ from concurrent.futures import ThreadPoolExecutor
156
+
157
+ local = threading.local()
158
+
159
+
160
+ def renderer():
161
+ if not hasattr(local, "r"):
162
+ local.r = Renderer()
163
+ return local.r
164
+
165
+
166
+ def process(rec):
167
+ task = tasks[rec["id"]]
168
+ fin = rec["completion"].split("</think>", 1)[1] if "</think>" in rec["completion"] else None
169
+ src = extract(fin, task["kind"])
170
+ png = os.path.join(os.path.dirname(args.out), f"_probe_{threading.get_ident()}.png")
171
+ rep = renderer().render(task["kind"], src, png) if src else None
172
+ bad, lines = problems(rep, task, src)
173
+ if bad == 0:
174
+ return {**rec, "vis_bad": 0}
175
+ msgs = [{"role": "user", "content": task["prompt"]}]
176
+ best = (bad, fin, 0)
177
+ cur, used = fin or "", 0
178
+ seed = zlib.crc32(rec["id"].encode()) % 1000003 * 7 + rec.get("sample", 0) + 7000
179
+ for rnd in range(args.rounds):
180
+ what = "SVG" if task["kind"] == "svg" else "page"
181
+ msgs = msgs + [{"role": "assistant", "content": cur or "(no answer)"},
182
+ {"role": "user", "content": FEEDBACK.format(what=what, kind=task["kind"],
183
+ lines="\n".join("- " + x for x in lines))}]
184
+ cur, n = generate(msgs, seed + rnd)
185
+ used += n
186
+ src = extract(cur, task["kind"])
187
+ rep = renderer().render(task["kind"], src, png) if src else None
188
+ b, lines = problems(rep, task, src)
189
+ if b < best[0]:
190
+ best = (b, cur, rnd + 1)
191
+ if b == 0:
192
+ break
193
+ r = {**rec, "vis_bad_first": bad, "vis_bad": best[0], "vis_kept": best[2], "n_tokens": rec["n_tokens"] + used}
194
+ if best[2]:
195
+ r["completion"] = "<think>(render-repaired)</think>" + best[1]
196
+ print(f"{rec['id']} s{rec.get('sample', 0)}: badness {bad} -> {best[0]} (round {best[2]})", flush=True)
197
+ return r
198
+
199
+
200
+ recs = [json.loads(l) for l in open(args.src, encoding="utf-8")]
201
+ t0 = time.time()
202
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
203
+ with ThreadPoolExecutor(8) as ex:
204
+ out = list(ex.map(process, recs))
205
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
206
+ with open(args.out, "w", encoding="utf-8") as f:
207
+ for r in out:
208
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
209
+ print(f"{len(out)} records, {sum('vis_bad_first' in r for r in out)} had problems, "
210
+ f"{sum(r.get('vis_bad', 0) == 0 and 'vis_bad_first' in r for r in out)} fixed fully, {(time.time() - t0) / 60:.1f} min")
research/scripts/vote.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Majority vote over the K recorded answers of each problem (math tasks), against the mean single-sample accuracy.
2
+ Also reports "vote with the module's answers" when a module run is given.
3
+ usage: vote.py results/orig_dm/aime.jsonl data/eval/aime.jsonl"""
4
+ import collections
5
+ import json
6
+ import sys
7
+
8
+ sys.path.insert(0, __import__("os").path.dirname(__import__("os").path.abspath(__file__)))
9
+ from grade import grade_math, last_boxed # noqa: E402
10
+
11
+ recs = [json.loads(l) for l in open(sys.argv[1], encoding="utf-8")]
12
+ items = {}
13
+ for p in sys.argv[2:]:
14
+ for l in open(p, encoding="utf-8"):
15
+ r = json.loads(l)
16
+ items[r["id"]] = r
17
+ by = collections.defaultdict(list)
18
+ for r in recs:
19
+ if r["id"] in items:
20
+ by[r["id"]].append(r)
21
+ single = maj = oracle = 0.0
22
+ k_seen = collections.Counter()
23
+ for pid, g in by.items():
24
+ gold = items[pid]["answer"]
25
+ answers, oks = [], []
26
+ for r in g:
27
+ fin = r["completion"].split("</think>", 1)[1] if "</think>" in r["completion"] else ""
28
+ b = last_boxed(fin)
29
+ answers.append(b.strip().replace(" ", "") if b else None)
30
+ oks.append(bool(grade_math(fin, gold)))
31
+ k_seen[len(g)] += 1
32
+ single += sum(oks) / len(oks)
33
+ oracle += any(oks)
34
+ votes = collections.Counter(a for a in answers if a)
35
+ if votes:
36
+ top = votes.most_common(1)[0][0]
37
+ maj += grade_math("\\boxed{" + top + "}", gold)
38
+ n = len(by)
39
+ print(f"{sys.argv[1]}: {n} problems, samples/problem {dict(k_seen)}; single {100 * single / n:.1f} "
40
+ f"majority {100 * maj / n:.1f} any-correct {100 * oracle / n:.1f}")
research/scripts/vote_design.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Designs the vote rule on DEV records (module runs with a 'module' field: none = the model finished by itself,
2
+ fallback = answer read out at the budget). Each answer gets weight w_nat (natural) or w_fb (read-out), optionally
3
+ times (median_len / len)^alpha (shorter reasoning = more reliable). Prints accuracy per rule.
4
+ usage: vote_design.py results/m180_fb4/mini.jsonl data/coc/math180.jsonl [--apply w_fb alpha]"""
5
+ import collections
6
+ import json
7
+ import os
8
+ import statistics
9
+ import sys
10
+
11
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
+ from grade import grade_math, grade_mmlupro, last_boxed # noqa: E402
13
+ import re # noqa: E402
14
+
15
+ args = [a for a in sys.argv[1:] if not a.startswith("--")]
16
+ apply = None
17
+ if "--apply" in sys.argv:
18
+ i = sys.argv.index("--apply")
19
+ apply = (float(sys.argv[i + 1]), float(sys.argv[i + 2]))
20
+ args = sys.argv[1:i]
21
+ recs = [json.loads(l) for l in open(args[0], encoding="utf-8")]
22
+ items = {}
23
+ for p in args[1:]:
24
+ for l in open(p, encoding="utf-8"):
25
+ r = json.loads(l)
26
+ items[r["id"]] = r
27
+ by = collections.defaultdict(list)
28
+ for r in recs:
29
+ if r["id"] in items:
30
+ fin = r["completion"].split("</think>", 1)[1] if "</think>" in r["completion"] else ""
31
+ if items[r["id"]]["task"] == "mmlupro":
32
+ m = re.findall(r"answer is:?\s*\(?\**\s*([A-J])\b", fin)
33
+ ans, ok = (m[-1] if m else None), bool(grade_mmlupro(fin, items[r["id"]]["answer"]))
34
+ else:
35
+ b = last_boxed(fin)
36
+ ans, ok = (b.strip().replace(" ", "") if b else None), bool(grade_math(fin, items[r["id"]]["answer"]))
37
+ by[r["id"]].append({"ans": ans, "fb": r.get("module") == "fallback", "len": r["n_tokens"], "ok": ok})
38
+ med = statistics.median(x["len"] for g in by.values() for x in g)
39
+
40
+
41
+ def acc(w_fb, alpha):
42
+ tot = 0
43
+ for pid, g in by.items():
44
+ sc = collections.defaultdict(float)
45
+ for x in g:
46
+ if x["ans"]:
47
+ sc[x["ans"]] += (w_fb if x["fb"] else 1.0) * (med / max(x["len"], 200)) ** alpha
48
+ if sc:
49
+ top = max(sc, key=sc.get)
50
+ tot += next(x["ok"] for x in g if x["ans"] == top)
51
+ return 100 * tot / len(by)
52
+
53
+
54
+ print(f"{args[0]}: {len(by)} problems, single {100 * sum(x['ok'] for g in by.values() for x in g) / sum(len(g) for g in by.values()):.1f}, "
55
+ f"any {100 * sum(any(x['ok'] for x in g) for g in by.values()) / len(by):.1f}")
56
+ if apply:
57
+ print(f"rule w_fb={apply[0]} alpha={apply[1]}: {acc(*apply):.1f}")
58
+ else:
59
+ for w in (1.0, 0.75, 0.5, 0.25, 0.1):
60
+ print(f"w_fb={w:<5}" + "".join(f" a={a}: {acc(w, a):5.1f}" for a in (0.0, 0.5, 1.0, 2.0)))
research/scripts/vqa_validate.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Does VQAScore (LFM3-VL) agree with my 1-5 quality ratings (results/vis_ratings.json)?
2
+ Model ratings are for sample 0 of vis_rep, reference ratings for my own files (vis_claude).
3
+ usage: vqa_validate.py
4
+ """
5
+ import json
6
+
7
+ from scipy.stats import spearmanr
8
+
9
+ R = json.load(open("results/vis_ratings.json", encoding="utf-8"))
10
+ model = json.load(open("results/vis_rep/vqa.json"))
11
+ ref = json.load(open("results/vis_claude/vqa.json"))
12
+ xs, ys, cats = [], [], []
13
+ for tid, r in R["model"].items():
14
+ if f"{tid}_0" in model:
15
+ xs.append(model[f"{tid}_0"]), ys.append(r), cats.append(("model", tid))
16
+ for tid, r in R["claude"].items():
17
+ if f"{tid}_0" in ref:
18
+ xs.append(ref[f"{tid}_0"]), ys.append(r), cats.append(("ref", tid))
19
+ rho, p = spearmanr(xs, ys)
20
+ print(f"all {len(xs)} rated screenshots: Spearman {rho:+.2f} (p={p:.1g})")
21
+ m = [(x, y) for x, y, c in zip(xs, ys, cats) if c[0] == "model"]
22
+ rho, p = spearmanr([a for a, _ in m], [b for _, b in m])
23
+ print(f"model answers only ({len(m)}): Spearman {rho:+.2f} (p={p:.1g})")
24
+ for k in "vhct":
25
+ mm = [(x, y) for x, y, c in zip(xs, ys, cats) if c[0] == "model" and c[1][0] == k]
26
+ if len(mm) > 3:
27
+ rho, _ = spearmanr([a for a, _ in mm], [b for _, b in mm])
28
+ print(f" {k}: n={len(mm)} Spearman {rho:+.2f}")
29
+ by = {}
30
+ for x, y in zip(xs, ys):
31
+ by.setdefault(y, []).append(x)
32
+ print("mean VQAScore by my rating: " + ", ".join(f"{k}: {sum(v) / len(v):.2f} (n={len(v)})" for k, v in sorted(by.items())))
33
+ # pairwise agreement: for two answers of the same task with different ratings, does VQAScore order them the same way?
34
+ agree = tot = 0
35
+ for tid, r in R["claude"].items():
36
+ if tid in R["model"] and r != R["model"][tid] and f"{tid}_0" in model and f"{tid}_0" in ref:
37
+ tot += 1
38
+ agree += (ref[f"{tid}_0"] > model[f"{tid}_0"]) == (r > R["model"][tid])
39
+ print(f"same task, model vs my file: VQAScore orders {agree}/{tot} pairs like my ratings")
research/scripts/vqascore.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VQAScore with the vision sibling LFM3-VL-2B: P("Yes") to 'Does this image show: <task>?' for a screenshot
2
+ (Lin et al. 2024 showed this probability tracks human judgements of text-to-image fit better than CLIPScore).
3
+ Used to validate against my 1-5 ratings and to choose between candidate answers.
4
+ usage: vqascore.py --png results/vis_rep/png --out results/vis_rep/vqa.json [--names v01_0 ...]
5
+ """
6
+ import argparse
7
+ import base64
8
+ import glob
9
+ import json
10
+ import math
11
+ import os
12
+ import time
13
+ import urllib.request
14
+ from concurrent.futures import ThreadPoolExecutor
15
+
16
+ QUESTION = 'Does this image show the following: "{desc}"? Answer yes or no.'
17
+
18
+
19
+ def describe_task(prompt):
20
+ return prompt.split("Answer with")[0].strip()
21
+
22
+
23
+ def score(png, desc, port=8091):
24
+ b64 = base64.b64encode(open(png, "rb").read()).decode()
25
+ body = {"messages": [{"role": "user", "content": [
26
+ {"type": "image_url", "image_url": {"url": "data:image/png;base64," + b64}},
27
+ {"type": "text", "text": QUESTION.format(desc=desc)}]}],
28
+ "temperature": 0.0, "max_tokens": 1, "logprobs": True, "top_logprobs": 20,
29
+ "chat_template_kwargs": {"enable_thinking": False}}
30
+ for attempt in range(5):
31
+ try:
32
+ req = urllib.request.Request(f"http://localhost:{port}/v1/chat/completions", data=json.dumps(body).encode(),
33
+ headers={"Content-Type": "application/json"})
34
+ r = json.loads(urllib.request.urlopen(req, timeout=1800).read())
35
+ break
36
+ except Exception:
37
+ if attempt == 4:
38
+ raise
39
+ time.sleep(10)
40
+ top = r["choices"][0]["logprobs"]["content"][0]["top_logprobs"]
41
+ yes = sum(math.exp(t["logprob"]) for t in top if t["token"].strip().lower() == "yes")
42
+ no = sum(math.exp(t["logprob"]) for t in top if t["token"].strip().lower() == "no")
43
+ return yes / (yes + no) if yes + no > 0 else 0.5
44
+
45
+
46
+ if __name__ == "__main__":
47
+ ap = argparse.ArgumentParser()
48
+ ap.add_argument("--png", required=True, help="folder with <id>_<sample>.png screenshots")
49
+ ap.add_argument("--out", required=True)
50
+ ap.add_argument("--names", nargs="*")
51
+ ap.add_argument("--tasks", default="data/visual/tasks.jsonl")
52
+ ap.add_argument("--port", type=int, default=8091)
53
+ ap.add_argument("--workers", type=int, default=2)
54
+ a = ap.parse_args()
55
+ tasks = {json.loads(l)["id"].split("/")[-1]: json.loads(l) for l in open(a.tasks, encoding="utf-8")}
56
+ names = a.names or sorted(os.path.splitext(os.path.basename(p))[0] for p in glob.glob(os.path.join(a.png, "*.png")))
57
+ names = [n for n in names if n.rsplit("_", 1)[0] in tasks]
58
+ old = json.load(open(a.out)) if os.path.exists(a.out) else {}
59
+ todo = [n for n in names if n not in old]
60
+
61
+ def one(n):
62
+ return n, score(os.path.join(a.png, n + ".png"), describe_task(tasks[n.rsplit("_", 1)[0]]["prompt"]), a.port)
63
+
64
+ t0 = time.time()
65
+ with ThreadPoolExecutor(a.workers) as ex:
66
+ for n, s in ex.map(one, todo):
67
+ old[n] = round(s, 4)
68
+ json.dump(old, open(a.out, "w"), indent=0)
69
+ print(f"{len(todo)} scored ({(time.time() - t0) / 60:.1f} min) -> {a.out}")
research/scripts/wiki_index.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Builds a full-text (BM25, SQLite FTS5) index over English Wikipedia passages for the knowledge module.
2
+
3
+ Each article is cut into passages of about --words words (paragraph-aligned); only the first --max-passages
4
+ passages of an article are kept (the lead and early sections hold most encyclopedic facts). Parquet files are data
5
+ (wikimedia/wikipedia 20231101.en) and are read with pyarrow only.
6
+ usage: python -I wiki_index.py <dir with parquet files> <out .sqlite> [--words 120] [--max-passages 4]
7
+ """
8
+ import argparse
9
+ import glob
10
+ import os
11
+ import sqlite3
12
+ import time
13
+
14
+ import pyarrow.parquet as pq
15
+
16
+ ap = argparse.ArgumentParser()
17
+ ap.add_argument("src")
18
+ ap.add_argument("out")
19
+ ap.add_argument("--words", type=int, default=120)
20
+ ap.add_argument("--max-passages", type=int, default=4)
21
+ a = ap.parse_args()
22
+
23
+ db = sqlite3.connect(a.out)
24
+ db.execute("PRAGMA journal_mode=OFF")
25
+ db.execute("PRAGMA synchronous=OFF")
26
+ db.execute("CREATE VIRTUAL TABLE IF NOT EXISTS p USING fts5(title, body, tokenize='porter unicode61')")
27
+ done = {r[0] for r in db.execute("CREATE TABLE IF NOT EXISTS files(name TEXT PRIMARY KEY)") or []}
28
+ done = {r[0] for r in db.execute("SELECT name FROM files")}
29
+
30
+
31
+ def passages(text):
32
+ out, cur = [], []
33
+ for para in text.split("\n"):
34
+ para = para.strip()
35
+ if not para or para.startswith(("References", "External links", "See also")):
36
+ continue
37
+ cur += para.split()
38
+ while len(cur) >= a.words:
39
+ out.append(" ".join(cur[: a.words]))
40
+ cur = cur[a.words:]
41
+ if len(out) >= a.max_passages:
42
+ return out
43
+ if cur and len(out) < a.max_passages and len(cur) > 20:
44
+ out.append(" ".join(cur))
45
+ return out
46
+
47
+
48
+ t0, n = time.time(), 0
49
+ for f in sorted(glob.glob(os.path.join(a.src, "*.parquet"))):
50
+ name = os.path.basename(f)
51
+ if name in done:
52
+ continue
53
+ pf = pq.ParquetFile(f)
54
+ for batch in pf.iter_batches(batch_size=2000, columns=["title", "text"]):
55
+ rows = []
56
+ for title, text in zip(batch.column("title").to_pylist(), batch.column("text").to_pylist()):
57
+ for ps in passages(text or ""):
58
+ rows.append((title, ps))
59
+ db.executemany("INSERT INTO p(title, body) VALUES (?, ?)", rows)
60
+ n += len(rows)
61
+ db.execute("INSERT INTO files VALUES (?)", (name,))
62
+ db.commit()
63
+ print(f"{name}: {n} passages total, {(time.time() - t0) / 60:.1f} min", flush=True)
64
+ db.execute("INSERT INTO p(p) VALUES('optimize')")
65
+ db.commit()
66
+ print("done", n)
runtime/grade.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Grades generations produced by gen.py against the frozen eval suite.
2
+
3
+ Only the final answer (text after </think>) is graded; a response whose thinking never closed counts as wrong.
4
+ Writes results/<tag>/graded.jsonl (one row per sample) and prints per-task accuracy.
5
+
6
+ usage: grade.py <tag>
7
+ """
8
+ import glob
9
+ import json
10
+ import os
11
+ import re
12
+ import subprocess
13
+
14
+ import sandbox # noqa: E402 (same folder)
15
+ import sys
16
+ import tempfile
17
+ import threading
18
+ from collections import defaultdict
19
+ from concurrent.futures import ThreadPoolExecutor
20
+
21
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
22
+
23
+ ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
24
+ TMP = os.path.join(tempfile.gettempdir(), "lfm3_codeexec")
25
+ os.makedirs(TMP, exist_ok=True)
26
+
27
+
28
+ def final_answer(completion):
29
+ if "</think>" not in completion:
30
+ return None
31
+ return completion.split("</think>", 1)[1].strip()
32
+
33
+
34
+ def last_boxed(text):
35
+ idx = text.rfind("\\boxed")
36
+ if idx < 0:
37
+ idx = text.rfind("\\fbox")
38
+ if idx < 0:
39
+ return None
40
+ i = text.find("{", idx)
41
+ if i < 0:
42
+ return None
43
+ depth = 0
44
+ for j in range(i, len(text)):
45
+ if text[j] == "{":
46
+ depth += 1
47
+ elif text[j] == "}":
48
+ depth -= 1
49
+ if depth == 0:
50
+ return text[i + 1: j]
51
+ return None
52
+
53
+
54
+ def grade_math(ans_text, gold):
55
+ from math_verify import parse, verify
56
+ if ans_text is None:
57
+ return False
58
+ pred = last_boxed(ans_text)
59
+ if pred is None:
60
+ return False
61
+ try:
62
+ g = parse("$" + gold + "$", parsing_timeout=None)
63
+ p = parse("$" + pred + "$", parsing_timeout=None)
64
+ if verify(g, p, timeout_seconds=None):
65
+ return True
66
+ except Exception:
67
+ pass
68
+ norm = lambda s: re.sub(r"\\text\{(.*?)\}|\\!|\s|\$|\\left|\\right|\.$", r"\1", s).replace("dfrac", "frac")
69
+ return norm(pred) == norm(gold)
70
+
71
+
72
+ def grade_gsm8k(ans_text, gold):
73
+ if ans_text is None:
74
+ return False
75
+ pred = last_boxed(ans_text)
76
+ if pred is None:
77
+ nums = re.findall(r"-?\d[\d,]*\.?\d*", ans_text)
78
+ if not nums:
79
+ return False
80
+ pred = nums[-1]
81
+ pred = re.sub(r"\\text\{.*?\}|\\\$|\$|,|\\%|%|\\!|\s", "", pred)
82
+ m = re.search(r"-?\d+(\.\d+)?", pred.replace("{", "").replace("}", ""))
83
+ if not m:
84
+ return False
85
+ try:
86
+ return abs(float(m.group(0)) - float(gold)) < 1e-6
87
+ except ValueError:
88
+ return False
89
+
90
+
91
+ def extract_code(ans_text):
92
+ if ans_text is None:
93
+ return None
94
+ blocks = re.findall(r"```(?:python|py|Python)?\s*\n(.*?)```", ans_text, re.S)
95
+ if not blocks:
96
+ return None
97
+ return max(blocks, key=len) if len(blocks) > 1 and "def " not in blocks[-1] else blocks[-1]
98
+
99
+
100
+ def run_python(src, timeout):
101
+ fd, path = tempfile.mkstemp(suffix=".py", dir=TMP)
102
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
103
+ f.write(src)
104
+ try:
105
+ r = sandbox.run([sys.executable, "-I", path], capture_output=True, timeout=timeout, cwd=TMP,
106
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
107
+ return r.returncode == 0
108
+ except subprocess.TimeoutExpired:
109
+ return False
110
+ finally:
111
+ try:
112
+ os.remove(path)
113
+ except OSError:
114
+ pass
115
+
116
+
117
+ def grade_humaneval(ans_text, item):
118
+ code = extract_code(ans_text)
119
+ if code is None:
120
+ return False
121
+ if f"def {item['entry_point']}" not in code:
122
+ code = item["code_prompt"] + code
123
+ src = code + "\n\n" + item["test"] + f"\n\ncheck({item['entry_point']})\n"
124
+ return run_python(src, 60)
125
+
126
+
127
+ def grade_mbpp(ans_text, item):
128
+ code = extract_code(ans_text)
129
+ if code is None:
130
+ return False
131
+ src = "\n".join(item.get("test_imports") or []) + "\n" + code + "\n\n" + item["test"] + "\n"
132
+ return run_python(src, 60)
133
+
134
+
135
+ def run_io(code, inp, out, timeout=6):
136
+ fd, path = tempfile.mkstemp(suffix=".py", dir=TMP)
137
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
138
+ f.write(code)
139
+ try:
140
+ r = sandbox.run([sys.executable, "-I", path], input=inp.encode(), capture_output=True, timeout=timeout,
141
+ cwd=TMP, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
142
+ if r.returncode != 0:
143
+ return False
144
+ return r.stdout.decode(errors="replace").split() == out.split()
145
+ except subprocess.TimeoutExpired:
146
+ return False
147
+ finally:
148
+ try:
149
+ os.remove(path)
150
+ except OSError:
151
+ pass
152
+
153
+
154
+ def grade_code_io(ans_text, item):
155
+ code = extract_code(ans_text)
156
+ if code is None:
157
+ return False
158
+ return all(run_io(code, i, o) for i, o in zip(item["inputs"], item["outputs"]))
159
+
160
+
161
+ def grade_lcb(ans_text, item):
162
+ from lcb_tests import run_lcb
163
+ code = extract_code(ans_text)
164
+ if code is None:
165
+ return False
166
+ k, n, _ = run_lcb(code, item, item["hidden"])
167
+ return k == n
168
+
169
+
170
+ _IFEVAL_LOCK = threading.Lock()
171
+
172
+
173
+ def grade_ifeval(ans_text, item):
174
+ # lm_eval fills empty instruction arguments with random values and langdetect is random too: seed both under a
175
+ # lock so that a response always gets the same grade
176
+ import random
177
+ from langdetect import DetectorFactory
178
+ from lm_eval.tasks.ifeval.utils import process_results
179
+ doc = {"key": 0, "prompt": item["prompt"], "instruction_id_list": item["instruction_id_list"],
180
+ "kwargs": item["kwargs"]}
181
+ with _IFEVAL_LOCK:
182
+ random.seed(0)
183
+ DetectorFactory.seed = 0
184
+ res = process_results(doc, [ans_text or ""])
185
+ return {"strict": res["prompt_level_strict_acc"], "loose": res["prompt_level_loose_acc"],
186
+ "inst_strict": res["inst_level_strict_acc"]}
187
+
188
+
189
+ def grade_mmlupro(ans_text, gold):
190
+ if ans_text is None:
191
+ return False
192
+ m = re.findall(r"answer is:?\s*\(?\**\s*([A-J])\b", ans_text)
193
+ if not m:
194
+ m = re.findall(r"\\boxed\{\(?([A-J])\)?\}", ans_text)
195
+ return bool(m) and m[-1] == gold
196
+
197
+
198
+ def grade_bbh(ans_text, gold, prompt=None):
199
+ """Exact match after extraction. With the prompt (flexible mode) it also accepts answers that state the same
200
+ thing in an unambiguous other form: the text of the chosen option instead of its letter, a word list separated by
201
+ commas, the whole Dyck sequence (input + completion) instead of the completion, plausible/not plausible for
202
+ yes/no. Each rule maps the answer to exactly one gold-comparable form, so it cannot accept a different answer."""
203
+ if ans_text is None:
204
+ return False
205
+ m = re.findall(r"answer is:?\s*(.+)", ans_text, re.I)
206
+ pred = m[-1] if m else (last_boxed(ans_text) or "")
207
+ pred = re.sub(r"\\boxed\{(.*)\}", r"\1", pred).strip().strip("*").strip().rstrip(".").strip()
208
+ gold = gold.strip()
209
+ norm = lambda s: re.sub(r"\s+", " ", s.lower().replace("`", "").replace("*", "")).strip().rstrip(".").strip()
210
+ if re.fullmatch(r"\([A-R]\)", gold):
211
+ mm = re.search(r"\(?([A-R])\)", pred) or re.fullmatch(r"([A-R])", pred)
212
+ if mm or prompt is None:
213
+ return bool(mm) and mm.group(1) == gold[1]
214
+ opts = {norm(t): k for k, t in re.findall(r"^\(([A-R])\)\s*(.+?)\s*$", prompt, re.M)}
215
+ return opts.get(norm(pred)) == gold[1]
216
+ if norm(pred) == norm(gold):
217
+ return True
218
+ if prompt is None:
219
+ return False
220
+ if "Sort the following words alphabetically" in prompt:
221
+ return re.findall(r"[^\s,]+", norm(pred)) == norm(gold).split()
222
+ if "parentheses are closed properly" in prompt:
223
+ inp = re.search(r"Input:\s*(.+)", prompt).group(1)
224
+ sp = lambda s: re.sub(r"\s+", "", norm(s))
225
+ return sp(pred) == sp(inp) + sp(gold)
226
+ yn = {"plausible": "yes", "not plausible": "no", "implausible": "no"}
227
+ return norm(gold) in ("yes", "no") and yn.get(norm(pred)) == norm(gold)
228
+
229
+
230
+ def load_suite(pattern=os.path.join(ROOT, "data", "eval", "*.jsonl")):
231
+ items = {}
232
+ for path in glob.glob(pattern):
233
+ with open(path, encoding="utf-8") as f:
234
+ for l in f:
235
+ r = json.loads(l)
236
+ items[r["id"]] = r
237
+ return items
238
+
239
+
240
+ def grade_one(rec, item):
241
+ a = final_answer(rec["completion"])
242
+ t = item["task"]
243
+ extra = {}
244
+ if t in ("math500", "aime"):
245
+ ok = grade_math(a, item["answer"])
246
+ elif t == "gsm8k":
247
+ ok = grade_gsm8k(a, item["answer"])
248
+ elif t == "humaneval":
249
+ ok = grade_humaneval(a, item)
250
+ elif t in ("mbpp", "code_fn"):
251
+ ok = grade_mbpp(a, item)
252
+ elif t == "code_io":
253
+ ok = grade_code_io(a, item)
254
+ elif t == "lcb":
255
+ ok = grade_lcb(a, item)
256
+ elif t == "math":
257
+ ok = grade_math(a, item["answer"])
258
+ elif t == "ifeval":
259
+ extra = grade_ifeval(a, item)
260
+ ok = extra["strict"]
261
+ elif t == "mmlupro":
262
+ ok = grade_mmlupro(a, item["answer"])
263
+ elif t == "bbh":
264
+ ok = grade_bbh(a, item["answer"], item["prompt"])
265
+ extra = {"strict": grade_bbh(a, item["answer"])}
266
+ else:
267
+ raise ValueError(t)
268
+ return {"id": rec["id"], "task": t, "sample": rec["sample"], "correct": bool(ok), "n_tokens": rec["n_tokens"],
269
+ "truncated": rec["truncated"], "closed_think": a is not None,
270
+ "subtask": item.get("subtask") or item.get("category") or item.get("level"), **extra}
271
+
272
+
273
+ def main(tag, pattern=None):
274
+ items = load_suite(pattern) if pattern else load_suite()
275
+ recs = []
276
+ for path in glob.glob(os.path.join(ROOT, "results", tag, "*.jsonl")):
277
+ if path.endswith("graded.jsonl"):
278
+ continue
279
+ with open(path, encoding="utf-8") as f:
280
+ recs += [json.loads(l) for l in f]
281
+ with ThreadPoolExecutor(12) as ex:
282
+ graded = list(ex.map(lambda r: grade_one(r, items[r["id"]]), recs))
283
+ with open(os.path.join(ROOT, "results", tag, "graded.jsonl"), "w", encoding="utf-8") as f:
284
+ for g in graded:
285
+ f.write(json.dumps(g) + "\n")
286
+ agg = defaultdict(list)
287
+ for g in graded:
288
+ agg[g["task"]].append(g)
289
+ print(f"{'task':10s} {'n':>5s} {'acc':>7s} {'trunc':>6s} {'no</think>':>10s} {'avg_tok':>8s}")
290
+ for t in sorted(agg):
291
+ rows = agg[t]
292
+ n = len(rows)
293
+ print(f"{t:10s} {n:5d} {100 * sum(r['correct'] for r in rows) / n:7.2f} "
294
+ f"{100 * sum(r['truncated'] for r in rows) / n:6.1f} {100 * sum(not r['closed_think'] for r in rows) / n:10.1f} "
295
+ f"{sum(r['n_tokens'] for r in rows) / n:8.0f}")
296
+ if t == "ifeval":
297
+ print(f"{' loose':10s} {'':5s} {100 * sum(r['loose'] for r in rows) / n:7.2f} inst_strict "
298
+ f"{100 * sum(sum(r['inst_strict']) for r in rows) / sum(len(r['inst_strict']) for r in rows):.2f}")
299
+
300
+
301
+ if __name__ == "__main__":
302
+ main(*sys.argv[1:])
runtime/lcb_tests.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test runner for LiveCodeBench items (stdin programs and LeetCode-style functional solutions).
2
+ run_lcb(code, item, tests) -> (passed, total, first failure description); stops at the first failing test."""
3
+ import json
4
+ import os
5
+ import subprocess
6
+
7
+ import sandbox # noqa: E402 (same folder)
8
+ import sys
9
+ import tempfile
10
+
11
+ TMP = os.path.join(tempfile.gettempdir(), "lfm3_lcb")
12
+ os.makedirs(TMP, exist_ok=True)
13
+
14
+ HARNESS = r'''
15
+ import sys, json
16
+ from typing import *
17
+ import collections, heapq, bisect, itertools, functools, math, string, re
18
+ from collections import *
19
+ from heapq import *
20
+ from bisect import *
21
+ from itertools import *
22
+ from functools import *
23
+ from math import *
24
+ sys.setrecursionlimit(1 << 20)
25
+ __CODE__
26
+ _tests = json.loads(sys.stdin.read())
27
+ for _k, (_inp, _exp) in enumerate(_tests):
28
+ _args = [json.loads(_l) for _l in _inp.strip().split("\n")]
29
+ try:
30
+ _res = getattr(Solution(), __FUNC__)(*_args)
31
+ except Exception as _e:
32
+ print(json.dumps({"k": _k, "err": repr(_e)[:300]}))
33
+ sys.exit(0)
34
+ _exp_v = json.loads(_exp)
35
+ _ok = _res == _exp_v or (isinstance(_res, tuple) and list(_res) == _exp_v) or (
36
+ isinstance(_res, float) and isinstance(_exp_v, (int, float)) and abs(_res - _exp_v) < 1e-6)
37
+ if not _ok:
38
+ print(json.dumps({"k": _k, "got": repr(_res)[:300]}))
39
+ sys.exit(0)
40
+ print(json.dumps({"k": len(_tests)}))
41
+ '''
42
+
43
+
44
+ def _same(got, exp):
45
+ a, b = got.split(), exp.split()
46
+ if len(a) != len(b):
47
+ return False
48
+ for x, y in zip(a, b):
49
+ if x != y:
50
+ try:
51
+ if abs(float(x) - float(y)) > 1e-6 * max(1.0, abs(float(y))):
52
+ return False
53
+ except ValueError:
54
+ return False
55
+ return True
56
+
57
+
58
+ def _run(src, stdin, timeout):
59
+ fd, path = tempfile.mkstemp(suffix=".py", dir=TMP)
60
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
61
+ f.write(src)
62
+ try:
63
+ r = sandbox.run([sys.executable, "-I", path], input=stdin.encode(), capture_output=True, timeout=timeout,
64
+ cwd=TMP, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
65
+ return r.returncode, r.stdout.decode(errors="replace"), r.stderr.decode(errors="replace")
66
+ except subprocess.TimeoutExpired:
67
+ return -1, "", "TimeoutError: time limit exceeded"
68
+ finally:
69
+ try:
70
+ os.remove(path)
71
+ except OSError:
72
+ pass
73
+
74
+
75
+ def run_lcb(code, item, tests, per_test_timeout=6):
76
+ if item["testtype"] == "functional":
77
+ src = HARNESS.replace("__CODE__", code).replace("__FUNC__", repr(item["func_name"]))
78
+ rc, so, se = _run(src, json.dumps(tests), per_test_timeout * max(1, min(len(tests), 10)))
79
+ try:
80
+ res = json.loads(so.strip().splitlines()[-1])
81
+ except Exception:
82
+ err = "\n".join(se.strip().splitlines()[-5:]) or "TimeoutError: time limit exceeded"
83
+ return 0, len(tests), f"Running the solution failed:\n{err}"
84
+ k = res["k"]
85
+ if k == len(tests):
86
+ return k, len(tests), None
87
+ inp, exp = tests[k]
88
+ got = res.get("got") or ("raised " + res.get("err", ""))
89
+ return k, len(tests), f"Input:\n{inp[:800]}\nExpected:\n{exp[:400]}\nYour solution returned:\n{got}"
90
+ for k, (inp, exp) in enumerate(tests):
91
+ rc, so, se = _run(code, inp, per_test_timeout)
92
+ if not (rc == 0 and _same(so, exp)):
93
+ got = so.strip()[:400] if rc == 0 else (so.strip()[:200] + "\n" + "\n".join(se.strip().splitlines()[-5:])).strip()
94
+ return k, len(tests), f"Input:\n{inp[:800]}\nExpected output:\n{exp[:400]}\nYour program's output:\n{got or '(nothing)'}"
95
+ return len(tests), len(tests), None
runtime/public_tests.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public tests a programmer can see in the task statement itself (never the hidden grading tests):
2
+ code_io -> Input/Output example pairs of the statement (Codeforces / AtCoder / "Sample Input" styles)
3
+ humaneval -> doctest lines (>>> call / expected value) of the docstring
4
+ mbpp -> the assert lines quoted in the prompt
5
+ run_public(task, code, prompt) -> (n_pass, n_total, first_failure_text)"""
6
+ import os
7
+ import re
8
+ import subprocess
9
+
10
+ import sandbox # noqa: E402 (same folder)
11
+ import sys
12
+ import tempfile
13
+
14
+ TMP = os.path.join(tempfile.gettempdir(), "lfm3_pub")
15
+ os.makedirs(TMP, exist_ok=True)
16
+
17
+ _HDR_IN = re.compile(r"^\s*(?:-+\s*)?(?:sample|example)?\s*input\s*(?:\d+|#\d+)?\s*:?\s*(?:-+)?\s*$", re.I)
18
+ _HDR_OUT = re.compile(r"^\s*(?:-+\s*)?(?:sample|example)?\s*output\s*(?:\d+|#\d+)?\s*:?\s*(?:-+)?\s*$", re.I)
19
+ _STOP = re.compile(r"^\s*(?:-{3,}.*|note:?|notes:?|explanation:?.*|constraints:?|write a python 3 program.*)\s*$", re.I)
20
+
21
+
22
+ _EX_HDR = re.compile(r"^\s*-*\s*(?:examples?|samples?|sample tests?)\s*\d*\s*:?\s*-*\s*$", re.I)
23
+
24
+
25
+ def _prose(text):
26
+ return len(re.findall(r"[A-Za-z]{3,}", text)) >= 12
27
+
28
+
29
+ def io_examples(prompt):
30
+ """Example pairs after the statement's Example(s) header (format sections named Input/Output are skipped)."""
31
+ lines = prompt.replace("\r", "").split("\n")
32
+ pairs = []
33
+ i = next((k + 1 for k, l in enumerate(lines) if _EX_HDR.match(l)), 0)
34
+ while i < len(lines):
35
+ if _HDR_IN.match(lines[i]) and not re.match(r"^\s*-+\s*input\s*-+\s*$", lines[i], re.I):
36
+ j = i + 1
37
+ inp = []
38
+ while j < len(lines) and not _HDR_OUT.match(lines[j]):
39
+ if _HDR_IN.match(lines[j]) or _STOP.match(lines[j]):
40
+ break
41
+ inp.append(lines[j])
42
+ j += 1
43
+ if j < len(lines) and _HDR_OUT.match(lines[j]):
44
+ k = j + 1
45
+ out = []
46
+ while k < len(lines) and not _HDR_IN.match(lines[k]) and not _STOP.match(lines[k]):
47
+ out.append(lines[k])
48
+ k += 1
49
+ for b in range(1, len(out)): # an explanation paragraph after the expected output is not output
50
+ if not out[b].strip() and len(re.findall(r"[A-Za-z]{3,}", " ".join(out[b + 1:b + 4]))) >= 4:
51
+ out = out[:b]
52
+ break
53
+ inp_s, out_s = "\n".join(inp).strip("\n"), "\n".join(out).strip()
54
+ if inp_s.strip() and out_s and not _prose(inp_s):
55
+ pairs.append((inp_s + "\n", out_s))
56
+ i = k
57
+ continue
58
+ i += 1
59
+ return pairs
60
+
61
+
62
+ def doctests(prompt):
63
+ tests, lines = [], prompt.split("\n")
64
+ for i, l in enumerate(lines):
65
+ s = l.strip()
66
+ if s.startswith(">>>") and i + 1 < len(lines):
67
+ exp = lines[i + 1].strip()
68
+ if exp and not exp.startswith(">>>") and not exp.startswith('"""'):
69
+ tests.append((s[3:].strip(), exp))
70
+ return tests
71
+
72
+
73
+ def asserts(prompt):
74
+ return [l.strip() for l in prompt.split("\n") if l.strip().startswith("assert ")]
75
+
76
+
77
+ def _run(src, stdin="", timeout=6):
78
+ fd, path = tempfile.mkstemp(suffix=".py", dir=TMP)
79
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
80
+ f.write(src)
81
+ try:
82
+ r = sandbox.run([sys.executable, "-I", path], input=stdin.encode(), capture_output=True, timeout=timeout,
83
+ cwd=TMP, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
84
+ return r.returncode, r.stdout.decode(errors="replace"), r.stderr.decode(errors="replace")
85
+ except subprocess.TimeoutExpired:
86
+ return -1, "", "TimeoutError: time limit exceeded (6 s)"
87
+ finally:
88
+ try:
89
+ os.remove(path)
90
+ except OSError:
91
+ pass
92
+
93
+
94
+ def _same(got, exp):
95
+ a, b = got.split(), exp.split()
96
+ if len(a) != len(b):
97
+ return False
98
+ for x, y in zip(a, b):
99
+ if x != y:
100
+ try:
101
+ if abs(float(x) - float(y)) > 1e-6 * max(1.0, abs(float(y))):
102
+ return False
103
+ except ValueError:
104
+ return False
105
+ return True
106
+
107
+
108
+ def _tail(err, n=6):
109
+ return "\n".join(err.strip().splitlines()[-n:])
110
+
111
+
112
+ def run_public(task, code, prompt, item=None):
113
+ """Returns (passed, total, failure description or None)."""
114
+ if task == "lcb":
115
+ from lcb_tests import run_lcb
116
+ return run_lcb(code, item, item["public"])
117
+ if task == "code_io":
118
+ tests = io_examples(prompt)
119
+ fails = None
120
+ ok = 0
121
+ for k, (inp, out) in enumerate(tests):
122
+ rc, so, se = _run(code, inp)
123
+ if rc == 0 and _same(so, out):
124
+ ok += 1
125
+ elif fails is None:
126
+ got = so.strip()[:600] if rc == 0 else (so.strip()[:300] + "\n" + _tail(se)).strip()
127
+ fails = f"Example {k + 1}\nInput:\n{inp.strip()[:800]}\nExpected output:\n{out[:600]}\n" \
128
+ f"Your program's output:\n{got or '(nothing)'}"
129
+ return ok, len(tests), fails
130
+ if task == "humaneval":
131
+ tests = doctests(prompt)
132
+ checks = [f"_r = {call}\n_e = {exp}\nprint('PASS' if _r == _e else 'FAIL ' + repr(_r))" for call, exp in tests]
133
+ elif task == "mbpp":
134
+ tests = asserts(prompt)
135
+ checks = [f"try:\n {a}\n print('PASS')\nexcept AssertionError:\n print('FAIL')" for a in tests]
136
+ else:
137
+ return 0, 0, None
138
+ ok, fails = 0, None
139
+ for k, (t, chk) in enumerate(zip(tests, checks)):
140
+ rc, so, se = _run(code + "\n\n" + chk)
141
+ if rc == 0 and so.strip().endswith("PASS"):
142
+ ok += 1
143
+ elif fails is None:
144
+ shown = t if isinstance(t, str) else f">>> {t[0]}\nexpected: {t[1]}"
145
+ res = so.strip().splitlines()[-1] if rc == 0 and so.strip() else _tail(se)
146
+ fails = f"Test:\n{shown}\nResult: {res}"
147
+ return ok, len(tests), fails
runtime/render.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Headless renderer for visual answers (SVG / HTML / canvas / three.js) with objective checks.
2
+
3
+ render(kind, source, png_path) opens the page in headless Chrome (the installed browser, via playwright) at 1024x768,
4
+ waits for it to settle and returns a report:
5
+ errors JavaScript exceptions and console errors (messages)
6
+ failed network requests that failed (e.g. a wrong library URL)
7
+ blank the screenshot is (almost) a single colour
8
+ overflow the page is wider than the viewport (horizontal scroll)
9
+ animated two screenshots 0.7 s apart differ (for animation tasks)
10
+ coverage share of the viewport that is not background colour
11
+ parse_error for SVG: the XML does not parse
12
+ The screenshot is saved to png_path. Used by the visual benchmark and by the render-check-repair module.
13
+ """
14
+ import io
15
+ import os
16
+ import re
17
+ import tempfile
18
+ import xml.dom.minidom
19
+
20
+ import numpy as np
21
+ from PIL import Image
22
+ from playwright.sync_api import sync_playwright
23
+
24
+ W, H = 1024, 768
25
+ TMP = os.path.join(tempfile.gettempdir(), "lfm3_render")
26
+ os.makedirs(TMP, exist_ok=True)
27
+
28
+ SVG_PAGE = """<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;background:#fff}
29
+ #c{width:1024px;height:768px;display:flex;align-items:center;justify-content:center}
30
+ #c>svg{max-width:1024px;max-height:768px;width:100%;height:100%}</style></head><body><div id="c">__SVG__</div></body></html>"""
31
+
32
+
33
+ def _stats(png):
34
+ a = np.asarray(Image.open(io.BytesIO(png)).convert("RGB")).astype(np.int16)
35
+ flat = a.reshape(-1, 3)
36
+ vals, counts = np.unique(flat[::7], axis=0, return_counts=True)
37
+ bg = vals[counts.argmax()]
38
+ diff = np.abs(a - bg).sum(axis=2) > 24
39
+ return a, float(diff.mean())
40
+
41
+
42
+ def extract(text, kind):
43
+ if text is None:
44
+ return None
45
+ blocks = re.findall(r"```(\w*)\s*\n(.*?)```", text, re.S)
46
+ for lang, body in reversed(blocks):
47
+ if kind == "svg" and ("<svg" in body):
48
+ return body[body.index("<svg"):].strip()
49
+ if kind == "html" and ("<html" in body.lower() or "<body" in body.lower() or "<canvas" in body or "<script" in body):
50
+ return body.strip()
51
+ if kind == "svg":
52
+ m = re.search(r"<svg.*</svg>", text, re.S)
53
+ return m.group(0) if m else None
54
+ m = re.search(r"<!doctype html.*</html>|<html.*</html>", text, re.S | re.I)
55
+ return m.group(0) if m else None
56
+
57
+
58
+ class Renderer:
59
+ def __init__(self, channel="chrome"):
60
+ self.pw = sync_playwright().start()
61
+ self.browser = self.pw.chromium.launch(channel=channel, headless=True,
62
+ args=["--use-angle=swiftshader", "--enable-unsafe-swiftshader"])
63
+
64
+ def close(self):
65
+ self.browser.close()
66
+ self.pw.stop()
67
+
68
+ def render(self, kind, source, png_path, wait_ms=1500):
69
+ rep = {"errors": [], "failed": [], "parse_error": None}
70
+ if kind == "svg":
71
+ try:
72
+ xml.dom.minidom.parseString(source.encode("utf-8"))
73
+ except Exception as e:
74
+ rep["parse_error"] = str(e)[:200]
75
+ html = SVG_PAGE.replace("__SVG__", source)
76
+ else:
77
+ html = source
78
+ path = os.path.join(TMP, f"page_{os.getpid()}_{abs(hash(png_path)) % 10**8}.html")
79
+ with open(path, "w", encoding="utf-8") as f:
80
+ f.write(html)
81
+ page = self.browser.new_page(viewport={"width": W, "height": H})
82
+ page.on("pageerror", lambda e: rep["errors"].append(f"Uncaught {e.name}: {e.message}"[:300]))
83
+ page.on("console", lambda m: rep["errors"].append(("console.error: " + m.text)[:300]) if m.type == "error" else None)
84
+ page.on("requestfailed", lambda r: rep["failed"].append(r.url[:200]))
85
+ try:
86
+ page.goto("file:///" + path.replace("\\", "/"), wait_until="load", timeout=20000)
87
+ page.wait_for_timeout(wait_ms)
88
+ png1 = page.screenshot()
89
+ page.wait_for_timeout(700)
90
+ png2 = page.screenshot()
91
+ rep["overflow"] = bool(page.evaluate("document.documentElement.scrollWidth > window.innerWidth + 2"))
92
+ rep["page_height"] = int(page.evaluate("document.documentElement.scrollHeight"))
93
+ except Exception as e:
94
+ rep["errors"].append(f"page failed to load: {str(e)[:200]}")
95
+ png1 = png2 = page.screenshot() if not page.is_closed() else None
96
+ rep["overflow"], rep["page_height"] = False, 0
97
+ finally:
98
+ page.close()
99
+ try:
100
+ os.remove(path)
101
+ except OSError:
102
+ pass
103
+ if png1 is None:
104
+ rep.update(blank=True, coverage=0.0, animated=False)
105
+ return rep
106
+ with open(png_path, "wb") as f:
107
+ f.write(png2)
108
+ a1, cov = _stats(png1)
109
+ a2, _ = _stats(png2)
110
+ rep["coverage"] = round(cov, 4)
111
+ rep["blank"] = cov < 0.003
112
+ rep["animated"] = bool((np.abs(a1 - a2).sum(axis=2) > 24).mean() > 0.0005)
113
+ return rep
114
+
115
+
116
+ if __name__ == "__main__":
117
+ import json
118
+ import sys
119
+ r = Renderer()
120
+ kind, src, out = sys.argv[1], open(sys.argv[2], encoding="utf-8").read(), sys.argv[3]
121
+ print(json.dumps(r.render(kind, src, out), ensure_ascii=False))
122
+ r.close()
runtime/sandbox.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """subprocess.run replacement for running untrusted model programs on Windows.
2
+
3
+ Two Windows traps make subprocess.run(..., timeout=) hang forever on a stuck program:
4
+ * in a venv, python.exe is a launcher that starts the real interpreter as a child; the timeout kills only the
5
+ launcher and the child keeps the pipes open;
6
+ * Popen.communicate writes `input` synchronously before it starts waiting, so a program that stops reading a large
7
+ input blocks the write and the timeout is never checked.
8
+ run() feeds stdin from a temporary file and kills the whole process tree on timeout, then raises
9
+ subprocess.TimeoutExpired like subprocess.run."""
10
+ import os
11
+ import subprocess
12
+ import tempfile
13
+
14
+
15
+ def run(cmd, input=None, timeout=None, capture_output=True, text=False, encoding=None, errors=None, **kw):
16
+ kw.setdefault("creationflags", getattr(subprocess, "CREATE_NO_WINDOW", 0))
17
+ fin, path = subprocess.DEVNULL, None
18
+ if input is not None:
19
+ fd, path = tempfile.mkstemp(suffix=".in")
20
+ with os.fdopen(fd, "wb") as f:
21
+ f.write(input if isinstance(input, bytes) else input.encode(encoding or "utf-8"))
22
+ fin = open(path, "rb")
23
+ try:
24
+ p = subprocess.Popen(cmd, stdin=fin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=text,
25
+ encoding=encoding, errors=errors, **kw)
26
+ try:
27
+ out, err = p.communicate(timeout=timeout)
28
+ except subprocess.TimeoutExpired:
29
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(p.pid)], capture_output=True)
30
+ try:
31
+ p.communicate(timeout=10)
32
+ except subprocess.TimeoutExpired:
33
+ pass
34
+ raise subprocess.TimeoutExpired(cmd, timeout)
35
+ return subprocess.CompletedProcess(cmd, p.returncode, out, err)
36
+ finally:
37
+ if path:
38
+ fin.close()
39
+ try:
40
+ os.remove(path)
41
+ except OSError:
42
+ pass
runtime/vis_autofix.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic fixes of visual answers before rendering (no model call), measured on recorded runs.
2
+
3
+ importmap the page loads three.js as ES modules (imports from 'three', 'three/addons/...' or from the unpkg URL,
4
+ whose addons import the bare name 'three') but has no import map: insert the import map of the task
5
+ at the start of <head>.
6
+
7
+ usage: vis_autofix.py --src results/vis_rep/vis.jsonl --out results/vis_rep_fix/vis.jsonl
8
+ Records whose answer is changed get "autofix": [...]; the rest are copied unchanged (exactly paired).
9
+ """
10
+ import argparse
11
+ import json
12
+ import os
13
+ import re
14
+ import sys
15
+
16
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
17
+ from render import extract # noqa: E402
18
+
19
+ IMPORT_MAP = ('<script type="importmap">{"imports": {"three": "https://unpkg.com/three@0.160.0/build/three.module.js", '
20
+ '"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"}}</script>')
21
+ USES_THREE_MODULE = re.compile(r"""from\s+['"](three|three/addons/[^'"]+|https://unpkg\.com/three@[^'"]+)['"]""")
22
+
23
+
24
+ def fix_importmap(src):
25
+ if 'type="importmap"' in src or "type='importmap'" in src or not USES_THREE_MODULE.search(src):
26
+ return src
27
+ m = re.search(r"<head[^>]*>", src, re.I)
28
+ if m:
29
+ return src[:m.end()] + "\n" + IMPORT_MAP + src[m.end():]
30
+ m = re.search(r"<script", src, re.I)
31
+ return src[:m.start()] + IMPORT_MAP + "\n" + src[m.start():] if m else src
32
+
33
+
34
+ def autofix(text, kind):
35
+ """Returns (new answer text, list of applied fixes)."""
36
+ if kind != "html" or text is None:
37
+ return text, []
38
+ src = extract(text, kind)
39
+ if not src:
40
+ return text, []
41
+ new = fix_importmap(src)
42
+ if new == src:
43
+ return text, []
44
+ return text.replace(src, new), ["importmap"]
45
+
46
+
47
+ if __name__ == "__main__":
48
+ ap = argparse.ArgumentParser()
49
+ ap.add_argument("--src", required=True)
50
+ ap.add_argument("--out", required=True)
51
+ ap.add_argument("--tasks", default="data/visual/tasks.jsonl")
52
+ a = ap.parse_args()
53
+ tasks = {json.loads(l)["id"]: json.loads(l) for l in open(a.tasks, encoding="utf-8")}
54
+ os.makedirs(os.path.dirname(a.out), exist_ok=True)
55
+ n = 0
56
+ with open(a.out, "w", encoding="utf-8") as f:
57
+ for l in open(a.src, encoding="utf-8"):
58
+ r = json.loads(l)
59
+ c = r["completion"]
60
+ if "</think>" in c:
61
+ head, fin = c.split("</think>", 1)
62
+ new, fixes = autofix(fin, tasks[r["id"]]["kind"])
63
+ if fixes:
64
+ r = {**r, "completion": head + "</think>" + new, "autofix": fixes}
65
+ n += 1
66
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
67
+ print(f"{n} answers changed -> {a.out}")
runtime/visual_loop.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared render-check-repair logic for visual answers, used by lfm3_deliberate.solve_visual.
2
+ (The same rules as scripts/visual_repair.py, which produced the measured numbers.)
3
+
4
+ problems(rep, animated, src, kind, hints) -> (weighted badness, feedback lines) from a render.py report;
5
+ feedback(kind, lines) -> the repair turn text; fix_importmap from vis_autofix (deterministic, no model call).
6
+ """
7
+ import re
8
+
9
+ from vis_autofix import IMPORT_MAP, fix_importmap # noqa: F401 (re-exported for callers)
10
+
11
+ # browser error -> the fix a web developer would apply (general knowledge, never a solution of a particular task)
12
+ HINTS = [
13
+ (r'Failed to resolve module specifier "three', "A module imports the bare name 'three' (three.js addons do this "
14
+ f"too). Put the import map in <head>, before any module script: {IMPORT_MAP}, then write "
15
+ "import * as THREE from 'three'; and import addons like import { OrbitControls } from "
16
+ "'three/addons/controls/OrbitControls.js';"),
17
+ (r"THREE is not defined", "three.js is not a global variable. Add the import map "
18
+ f"{IMPORT_MAP} and, inside <script type=\"module\">, write import * as THREE from 'three';"),
19
+ (r"THREE\.(\w+) is not a constructor", "THREE.{0} does not exist in three.js r160. Addons are imported "
20
+ "separately, e.g. import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; then "
21
+ "new OrbitControls(camera, renderer.domElement). Otherwise check the class name."),
22
+ (r"Assignment to constant variable", "A variable declared with const is assigned again: declare variables "
23
+ "that change with let."),
24
+ (r"ReferenceError: (\w+) is not defined", "'{0}' is used where it is not declared: declare it with const/let "
25
+ "at the top level, before the functions that use it run."),
26
+ (r"Cannot read properties of (?:undefined|null) \(reading '(\w+)'\)", "Something is undefined or null when "
27
+ "its property '{0}' is read: an element id that does not exist, an object used before it is created, or an "
28
+ "array index out of range."),
29
+ (r"(\S+) is not a function", "{0} is not a function: a property is called like a method or a name is "
30
+ "misspelled (canvas.width/height are numbers; drawing methods are ctx.fillRect, ctx.arc, ctx.beginPath, "
31
+ "ctx.fill, ctx.stroke; angles in Math are radians, there is no Math.radians)."),
32
+ ]
33
+ BLANK_THREE = ("Add lights for MeshStandardMaterial (AmbientLight and DirectionalLight), put the camera away from "
34
+ "the objects with camera.lookAt(0, 0, 0) and render every frame with "
35
+ "renderer.setAnimationLoop(() => renderer.render(scene, camera)).")
36
+ BLANK_CANVAS = ("Set canvas.width = innerWidth and canvas.height = innerHeight, draw in every frame and use a fill "
37
+ "color that differs from the background.")
38
+ STATIC_HINT = ("Compute positions from the elapsed time inside a requestAnimationFrame loop (or "
39
+ "renderer.setAnimationLoop) and redraw every frame.")
40
+ SVG_HINT = "Close every tag, quote every attribute value, write & as &amp; and keep one root <svg> element."
41
+
42
+
43
+ def hints_for(rep, animated, src):
44
+ out = []
45
+ for e in rep.get("errors", []):
46
+ for pat, text in HINTS:
47
+ m = re.search(pat, e)
48
+ if m:
49
+ h = text.replace("{0}", m.group(1)) if m.groups() else text
50
+ if h not in out:
51
+ out.append(h)
52
+ break
53
+ if rep.get("parse_error"):
54
+ out.append(SVG_HINT)
55
+ if rep.get("blank") and not rep.get("parse_error") and not rep.get("errors"):
56
+ out.append(BLANK_THREE if "three" in (src or "") else BLANK_CANVAS if "<canvas" in (src or "") else
57
+ "Make sure the content is visible: real sizes, colors that differ from the background, nothing "
58
+ "hidden by CSS.")
59
+ if animated and not rep.get("animated") and not rep.get("blank"):
60
+ out.append(STATIC_HINT)
61
+ return out
62
+
63
+
64
+ def problems(rep, animated, src, kind, hints=False):
65
+ """(weighted badness, feedback lines); 0 = renders cleanly."""
66
+ if not src:
67
+ return 10, [f"Your answer does not contain the complete file in a ```{kind} code block."]
68
+ bad, lines = 0, []
69
+ if rep.get("parse_error"):
70
+ bad += 4
71
+ lines.append(f"The SVG is not valid XML: {rep['parse_error']}")
72
+ if rep["errors"]:
73
+ bad += 3
74
+ lines += [f"JavaScript error: {e}" for e in rep["errors"][:4]]
75
+ if rep["failed"]:
76
+ bad += 3
77
+ lines += [f"This resource failed to load: {u}" for u in rep["failed"][:3]]
78
+ if rep["blank"]:
79
+ bad += 3
80
+ lines.append("Nothing is visible: the rendered page is blank (a single colour).")
81
+ if animated and not rep["animated"]:
82
+ bad += 1
83
+ lines.append("The animation does not move: two screenshots taken 0.7 seconds apart are identical.")
84
+ if rep.get("overflow"):
85
+ bad += 1
86
+ lines.append("The page is wider than the 1024 px window (there is a horizontal scrollbar).")
87
+ if hints and bad:
88
+ lines += ["Hint: " + h for h in hints_for(rep, animated, src)]
89
+ return bad, lines
90
+
91
+
92
+ def feedback(kind, lines):
93
+ what = "SVG" if kind == "svg" else "page"
94
+ return (f"I opened your {what} in Chrome (window 1024x768) and found these problems:\n"
95
+ + "\n".join("- " + x for x in lines)
96
+ + f"\n\nFix them and give the complete corrected file in a single ```{kind} code block.")
tokenizer/chat_template.jinja ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- bos_token -}}
2
+ {%- set preserve_thinking = preserve_thinking | default(false) -%}
3
+ {%- set enable_thinking = enable_thinking | default(true) -%}
4
+
5
+ {%- macro render_value(v) -%}
6
+ {%- if v is string -%}
7
+ {{- v -}}
8
+ {%- elif v is boolean -%}
9
+ {{- "true" if v else "false" -}}
10
+ {%- elif v is none -%}
11
+ {{- "null" -}}
12
+ {%- elif v is mapping or (v is sequence and v is not string) -%}
13
+ {{- v | tojson -}}
14
+ {%- else -%}
15
+ {{- v -}}
16
+ {%- endif -%}
17
+ {%- endmacro -%}
18
+
19
+ {%- macro parse_content(content) -%}
20
+ {%- if content is string -%}
21
+ {{- content -}}
22
+ {%- elif content is mapping and content.get("type") == "image" -%}
23
+ {{- "<|vision_start|><|image|><|vision_end|>" -}}
24
+ {%- elif content is mapping and content.get("type") == "video" -%}
25
+ {{- "<|vision_start|><|video|><|vision_end|>" -}}
26
+ {%- elif content is mapping and content.get("type") == "text" -%}
27
+ {{- (content.get("text") or "") | string -}}
28
+ {%- elif content is mapping -%}
29
+ {{- content | tojson -}}
30
+ {%- elif content is iterable -%}
31
+ {%- set _ns = namespace(result="") -%}
32
+ {%- for item in content -%}
33
+ {%- if item is string -%}
34
+ {%- set _ns.result = _ns.result + item -%}
35
+ {%- elif item is mapping and item.get("type") == "image" -%}
36
+ {%- set _ns.result = _ns.result + "<|vision_start|><|image|><|vision_end|>" -%}
37
+ {%- elif item is mapping and item.get("type") == "video" -%}
38
+ {%- set _ns.result = _ns.result + "<|vision_start|><|video|><|vision_end|>" -%}
39
+ {%- elif item is mapping and item.get("type") == "text" -%}
40
+ {%- set _ns.result = _ns.result + ((item.get("text") or "") | string) -%}
41
+ {%- else -%}
42
+ {%- set _ns.result = _ns.result + (item | tojson) -%}
43
+ {%- endif -%}
44
+ {%- endfor -%}
45
+ {{- _ns.result -}}
46
+ {%- elif content is not none -%}
47
+ {{- content | tojson -}}
48
+ {%- endif -%}
49
+ {%- endmacro -%}
50
+
51
+ {%- macro render_tool_calls(tool_calls) -%}
52
+ {%- for tool_call in tool_calls -%}
53
+ {%- if not loop.first -%}{{- "\n\n" -}}{%- endif -%}
54
+ {%- set func = tool_call["function"] if "function" in tool_call else tool_call -%}
55
+ {%- set func_args = func.get("arguments") -%}
56
+ {%- if func_args is string and (func_args | trim) not in ["", "{}", "null"] -%}
57
+ {{- raise_exception("Tool call arguments must be a mapping, got a JSON-encoded string: parse arguments with json.loads() before applying the chat template") -}}
58
+ {%- endif -%}
59
+ {{- "<tool_call>\n<function=" + func["name"] + ">\n" -}}
60
+ {%- if func_args is mapping -%}
61
+ {%- for arg_name, arg_value in func_args.items() -%}
62
+ {{- "<parameter=" + arg_name + ">\n" + render_value(arg_value) + "\n</parameter>\n" -}}
63
+ {%- endfor -%}
64
+ {%- endif -%}
65
+ {{- "</function>\n</tool_call>" -}}
66
+ {%- endfor -%}
67
+ {%- endmacro -%}
68
+
69
+ {%- macro tool_to_json(tool) -%}
70
+ {%- set fn = tool.function if tool.function is defined else tool -%}
71
+ {%- set ns_t = namespace(first=true) -%}
72
+ {{- "{" -}}
73
+ {%- for k, v in fn.items() -%}
74
+ {%- if k != "defer_loading" -%}
75
+ {%- if not ns_t.first -%}{{- ", " -}}{%- endif -%}
76
+ {%- set ns_t.first = false -%}
77
+ {{- k | tojson -}}{{- ": " -}}{{- v | tojson -}}
78
+ {%- endif -%}
79
+ {%- endfor -%}
80
+ {{- "}" -}}
81
+ {%- endmacro -%}
82
+
83
+ {%- macro is_tool_reference(item) -%}
84
+ {{- "1" if item is mapping and item.get("type") == "tool_reference" else "" -}}
85
+ {%- endmacro -%}
86
+
87
+ {%- macro referenced_tool_to_json(name) -%}
88
+ {%- set _ns = namespace(found=none) -%}
89
+ {%- for tool in tools or [] -%}
90
+ {%- set fn = tool.function if tool.function is defined else tool -%}
91
+ {%- if fn.name == name -%}
92
+ {%- set _ns.found = fn -%}
93
+ {%- endif -%}
94
+ {%- endfor -%}
95
+ {%- if _ns.found is none -%}
96
+ {{- raise_exception("Tool reference '" + name | string + "' not found in available tools. Every tool referenced by a tool_reference must be in `tools` on every request.") -}}
97
+ {%- endif -%}
98
+ {{- tool_to_json(_ns.found) -}}
99
+ {%- endmacro -%}
100
+
101
+ {%- macro render_tool_response(content) -%}
102
+ {%- if content is string and "<tool_response>" in content -%}
103
+ {{- content -}}
104
+ {%- else -%}
105
+ {{- "<tool_response>\n" -}}
106
+ {%- if content is string -%}
107
+ {{- content -}}
108
+ {%- elif content is mapping -%}
109
+ {{- parse_content(content) -}}
110
+ {%- elif content is iterable -%}
111
+ {%- for item in content -%}
112
+ {%- if not loop.first -%}{{- "\n" -}}{%- endif -%}
113
+ {%- if is_tool_reference(item) -%}
114
+ {%- if loop.first or not is_tool_reference(loop.previtem) -%}
115
+ {{- "<additional_tools>\n" -}}
116
+ {%- endif -%}
117
+ {{- referenced_tool_to_json(item.name) -}}
118
+ {%- if loop.last or not is_tool_reference(loop.nextitem) -%}
119
+ {{- "\n</additional_tools>" -}}
120
+ {%- endif -%}
121
+ {%- elif item is mapping and item.result is defined and item.result is mapping and item.result.tools is defined -%}
122
+ {{- "<additional_tools>\n" -}}
123
+ {%- set _tl = item.result.tools -%}
124
+ {%- if _tl is string -%}
125
+ {{- _tl -}}
126
+ {%- elif _tl is mapping -%}
127
+ {{- tool_to_json(_tl) -}}
128
+ {%- else -%}
129
+ {%- for t in _tl -%}
130
+ {%- if not loop.first -%}{{- "\n" -}}{%- endif -%}
131
+ {{- tool_to_json(t) -}}
132
+ {%- endfor -%}
133
+ {%- endif -%}
134
+ {{- "\n</additional_tools>" -}}
135
+ {%- else -%}
136
+ {{- parse_content(item) -}}
137
+ {%- endif -%}
138
+ {%- endfor -%}
139
+ {%- elif content is not none -%}
140
+ {{- content | tojson -}}
141
+ {%- endif -%}
142
+ {{- "\n</tool_response>" -}}
143
+ {%- endif -%}
144
+ {%- endmacro -%}
145
+
146
+ {%- set ns = namespace(system_prompt="", last_user_index=-1) -%}
147
+ {%- if messages and messages[0]["role"] == "system" -%}
148
+ {%- if messages[0].get("content") -%}
149
+ {%- set ns.system_prompt = parse_content(messages[0]["content"]) -%}
150
+ {%- endif -%}
151
+ {%- set messages = messages[1:] -%}
152
+ {%- endif -%}
153
+ {%- if tools -%}
154
+ {%- set tools_ns = namespace(lines=[]) -%}
155
+ {%- for tool in tools -%}
156
+ {%- set fn = tool.function if tool.function is defined else tool -%}
157
+ {%- set _dyn = (fn.defer_loading is defined and fn.defer_loading) or (tool.defer_loading is defined and tool.defer_loading) -%}
158
+ {%- if not _dyn -%}
159
+ {%- set tools_ns.lines = tools_ns.lines + [tool_to_json(fn)] -%}
160
+ {%- endif -%}
161
+ {%- endfor -%}
162
+ {%- if tools_ns.lines -%}
163
+ {%- set ns.system_prompt = ns.system_prompt + ("\n\n" if ns.system_prompt else "") + "<tools>\n" + (tools_ns.lines | join("\n")) + "\n</tools>" -%}
164
+ {%- endif -%}
165
+ {%- endif -%}
166
+ {%- if ns.system_prompt -%}
167
+ {{- "<|im_start|>system\n" + ns.system_prompt + "<|im_end|>\n" -}}
168
+ {%- endif -%}
169
+ {%- for message in messages -%}
170
+ {%- if message["role"] == "user" -%}
171
+ {%- set ns.last_user_index = loop.index0 -%}
172
+ {%- endif -%}
173
+ {%- endfor -%}
174
+ {%- set carry = namespace(pending=[]) -%}
175
+ {%- for message in messages -%}
176
+ {%- set _next = messages[loop.index0 + 1] if not loop.last else none -%}
177
+ {%- set _merge = message.role == "tool" and not message.get("additional_tools")
178
+ and _next is not none and _next.role == "tool" and not _next.get("additional_tools")
179
+ and message.get("tool_call_id") and _next.get("tool_call_id") == message.get("tool_call_id") -%}
180
+ {%- set _items = [] -%}
181
+ {%- if message.role == "tool" and not message.get("additional_tools") -%}
182
+ {%- set _c = message.get("content") -%}
183
+ {%- if _c is string -%}
184
+ {%- set _items = [{"type": "text", "text": _c}] if _c else [] -%}
185
+ {%- elif _c is mapping -%}
186
+ {%- set _items = [_c] -%}
187
+ {%- elif _c is iterable -%}
188
+ {%- set _items = _c | list -%}
189
+ {%- elif _c is not none -%}
190
+ {%- set _items = [_c] -%}
191
+ {%- endif -%}
192
+ {%- endif -%}
193
+ {%- if _merge -%}
194
+ {%- set carry.pending = carry.pending + _items -%}
195
+ {%- else -%}
196
+ {{- "<|im_start|>" + message.role + "\n" -}}
197
+ {%- if message.role == "assistant" -%}
198
+ {%- generation -%}
199
+ {%- set thinking = message.thinking if message.thinking is defined else message.reasoning if message.reasoning is defined else message.reasoning_content -%}
200
+ {%- set keep_thinking = preserve_thinking or loop.index0 > ns.last_user_index -%}
201
+ {%- if thinking is defined and keep_thinking -%}
202
+ {{- "<think>" + (thinking or "") + "</think>" -}}
203
+ {%- elif keep_thinking and "</think>" not in (message.get("content") if message.get("content") is string else "") -%}
204
+ {{- "<think></think>" -}}
205
+ {%- endif -%}
206
+ {%- set _cfm_tag = "CONTINUE_FINAL_MESSAGE_TAG " -%}
207
+ {%- set _has_cfm = false -%}
208
+ {%- set content = "" -%}
209
+ {%- if message.get("content") -%}
210
+ {%- set content = parse_content(message.content) -%}
211
+ {%- endif -%}
212
+ {%- if not keep_thinking -%}
213
+ {%- if "</think>" in content -%}
214
+ {%- set content = content.split("</think>")[-1] | trim -%}
215
+ {%- endif -%}
216
+ {%- set content = "<think></think>" + content -%}
217
+ {%- endif -%}
218
+ {%- if content.endswith(_cfm_tag) -%}
219
+ {%- set _has_cfm = true -%}
220
+ {%- set _trunc_len = (content | length) - (_cfm_tag | length) -%}
221
+ {%- set content = content[:_trunc_len] -%}
222
+ {%- endif -%}
223
+ {{- content -}}
224
+ {%- if message.tool_calls -%}
225
+ {{- render_tool_calls(message.tool_calls) -}}
226
+ {%- endif -%}
227
+ {%- if _has_cfm -%}
228
+ {{- _cfm_tag -}}
229
+ {%- endif -%}
230
+ {{- "<|im_end|>\n" -}}
231
+ {%- endgeneration -%}
232
+ {%- elif message.role == "tool" -%}
233
+ {%- if message.get("additional_tools") -%}
234
+ {%- set _at = message["additional_tools"] -%}
235
+ {%- if _at is mapping or (_at is sequence and _at is not string) -%}
236
+ {%- set _src = _at -%}
237
+ {%- else -%}
238
+ {%- set _src = message.get("content") -%}
239
+ {%- endif -%}
240
+ {%- if _src is string and "<tool_response>" in _src -%}
241
+ {{- _src -}}
242
+ {%- elif _src is string and "<additional_tools>" in _src -%}
243
+ {{- "<tool_response>\n" + _src + "\n</tool_response>" -}}
244
+ {%- elif _src -%}
245
+ {{- "<tool_response>\n<additional_tools>\n" -}}
246
+ {%- if _src is string -%}
247
+ {{- _src -}}
248
+ {%- elif _src is mapping -%}
249
+ {{- tool_to_json(_src) -}}
250
+ {%- else -%}
251
+ {%- for t in _src -%}
252
+ {%- if not loop.first -%}{{- "\n" -}}{%- endif -%}
253
+ {{- tool_to_json(t) -}}
254
+ {%- endfor -%}
255
+ {%- endif -%}
256
+ {{- "\n</additional_tools>\n</tool_response>" -}}
257
+ {%- endif -%}
258
+ {%- elif carry.pending -%}
259
+ {{- render_tool_response(carry.pending + _items) -}}
260
+ {%- set carry.pending = [] -%}
261
+ {%- else -%}
262
+ {{- render_tool_response("" if message.get("content") is none else message.get("content")) -}}
263
+ {%- endif -%}
264
+ {{- "<|im_end|>\n" -}}
265
+ {%- else %}
266
+ {%- if message.get("content") -%}
267
+ {{- parse_content(message["content"]) -}}
268
+ {%- endif -%}
269
+ {{- "<|im_end|>\n" -}}
270
+ {%- endif %}
271
+ {%- endif -%}
272
+ {%- endfor -%}
273
+ {%- if add_generation_prompt -%}
274
+ {{- "<|im_start|>assistant\n" -}}
275
+ {%- if enable_thinking -%}
276
+ {{- "<think>" -}}
277
+ {%- else -%}
278
+ {{- "<think></think>" -}}
279
+ {%- endif -%}
280
+ {%- endif -%}
tokenizer/config.json ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Lfm3ForCausalLM"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_lfm3.Lfm3Config",
7
+ "AutoModel": "modeling_lfm3.Lfm3Model",
8
+ "AutoModelForCausalLM": "modeling_lfm3.Lfm3ForCausalLM"
9
+ },
10
+ "bos_token_id": 124894,
11
+ "conv_L_cache": 7,
12
+ "conv_bias": false,
13
+ "dtype": "bfloat16",
14
+ "eos_token_id": 124900,
15
+ "head_dim": 128,
16
+ "hidden_size": 2048,
17
+ "intermediate_size": 7680,
18
+ "layer_types": [
19
+ "conv",
20
+ "conv",
21
+ "full_attention",
22
+ "conv",
23
+ "sliding_attention",
24
+ "conv",
25
+ "full_attention",
26
+ "conv",
27
+ "sliding_attention",
28
+ "conv",
29
+ "full_attention",
30
+ "conv",
31
+ "sliding_attention",
32
+ "conv",
33
+ "full_attention",
34
+ "conv",
35
+ "sliding_attention",
36
+ "conv",
37
+ "full_attention",
38
+ "conv",
39
+ "sliding_attention",
40
+ "conv",
41
+ "full_attention",
42
+ "conv"
43
+ ],
44
+ "max_position_embeddings": 262144,
45
+ "model_type": "lfm3",
46
+ "norm_eps": 1e-05,
47
+ "num_attention_heads": 16,
48
+ "num_hidden_layers": 24,
49
+ "num_key_value_heads": 4,
50
+ "pad_token_id": 124893,
51
+ "rope_parameters": {
52
+ "full_attention": {
53
+ "partial_rotary_factor": 0.5,
54
+ "rope_theta": 1000000.0,
55
+ "rope_type": "default"
56
+ },
57
+ "sliding_attention": {
58
+ "rope_theta": 10000,
59
+ "rope_type": "default"
60
+ }
61
+ },
62
+ "sliding_window": 256,
63
+ "tie_word_embeddings": true,
64
+ "transformers_version": "5.12.1",
65
+ "use_cache": true,
66
+ "use_sandwich_norm": true,
67
+ "vocab_size": 128000
68
+ }