Safetensors
GGUF
English
Russian
lfm3
reasoning
test-time-compute
inference-time-scaling
code-generation
llama.cpp
negative-results
conversational
Instructions to use HelioAI/LFM3-1.7B-inference-modules-experiments with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use HelioAI/LFM3-1.7B-inference-modules-experiments with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M # Run inference directly in the terminal: llama cli -hf HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M # Run inference directly in the terminal: llama cli -hf HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
Build from source code
git clone https://github.com/ggml-org/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-app # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama serve -hf HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M # Run inference directly in the terminal: ./build/bin/llama cli -hf HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
Use Docker
docker model run hf.co/HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
- Llama new
- LM Studio
- Jan
- Ollama
How to use HelioAI/LFM3-1.7B-inference-modules-experiments with Ollama:
ollama run hf.co/HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
- Unsloth Desktop
- Pi
How to use HelioAI/LFM3-1.7B-inference-modules-experiments with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use HelioAI/LFM3-1.7B-inference-modules-experiments with Docker Model Runner:
docker model run hf.co/HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
- Lemonade
How to use HelioAI/LFM3-1.7B-inference-modules-experiments with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
Run and chat with the model
lemonade run user.LFM3-1.7B-inference-modules-experiments-Q5_K_M
List all available models
lemonade list
- Hermes Agent
How to use HelioAI/LFM3-1.7B-inference-modules-experiments with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use HelioAI/LFM3-1.7B-inference-modules-experiments with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "HelioAI/LFM3-1.7B-inference-modules-experiments:Q5_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
LFM3-1.7B inference modules: experimental release (part 2)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- research/scripts/rate.py +7 -0
- research/scripts/rbud_test.py +32 -0
- research/scripts/ref_check.py +40 -0
- research/scripts/render.py +122 -0
- research/scripts/repair.py +162 -0
- research/scripts/rl_iter.sh +22 -0
- research/scripts/rl_iter_bc.sh +39 -0
- research/scripts/rl_train.py +217 -0
- research/scripts/run_dev.sh +11 -0
- research/scripts/run_eval.sh +9 -0
- research/scripts/run_mini.sh +11 -0
- research/scripts/run_mini2.sh +8 -0
- research/scripts/run_quick.sh +9 -0
- research/scripts/sample_prompts.py +17 -0
- research/scripts/sandbox.py +42 -0
- research/scripts/serve.sh +12 -0
- research/scripts/sft_train.py +115 -0
- research/scripts/shadow_split.py +16 -0
- research/scripts/sim_coc.py +50 -0
- research/scripts/sim_multi.py +165 -0
- research/scripts/sim_single.py +82 -0
- research/scripts/soft_post.py +36 -0
- research/scripts/soft_stats.py +18 -0
- research/scripts/soft_think.py +177 -0
- research/scripts/start_server.sh +7 -0
- research/scripts/stress.py +340 -0
- research/scripts/svg_program.py +156 -0
- research/scripts/teach_gen.py +149 -0
- research/scripts/teach_probe.py +110 -0
- research/scripts/teach_to_rl.py +36 -0
- research/scripts/tir.py +197 -0
- research/scripts/vis_autofix.py +67 -0
- research/scripts/visual_eval.py +76 -0
- research/scripts/visual_eyes.py +209 -0
- research/scripts/visual_loop.py +96 -0
- research/scripts/visual_repair.py +210 -0
- research/scripts/vote.py +40 -0
- research/scripts/vote_design.py +60 -0
- research/scripts/vqa_validate.py +39 -0
- research/scripts/vqascore.py +69 -0
- research/scripts/wiki_index.py +66 -0
- runtime/grade.py +302 -0
- runtime/lcb_tests.py +95 -0
- runtime/public_tests.py +147 -0
- runtime/render.py +122 -0
- runtime/sandbox.py +42 -0
- runtime/vis_autofix.py +67 -0
- runtime/visual_loop.py +96 -0
- tokenizer/chat_template.jinja +280 -0
- 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 & 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 & 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 & 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 |
+
}
|