"""Commit-on-consensus (CoC) generation: an inference-time halting mechanism.
The reasoning is generated in chunks (--chunk tokens, KV cache of the slot is reused). After every chunk, while the
model is still thinking, a probe closes the thinking at the last sentence end (the model's own closing pattern:
"...." + ) and lets the model write its final answer greedily on a separate slot. When two consecutive probes
give the same extracted answer, the reasoning is committed there: the output becomes reasoning[:cut] + +
that probe's answer. Code tasks are never probed (no cheap answer equivalence).
--no-probe runs the identical chunked generation without probes: the control for measuring CoC's own contribution
(both runs are token-for-token identical until CoC commits).
Output records have the same format as gen.py, plus "coc_cut" (None if not committed).
usage: gen_coc.py --inp data/quick/mini.jsonl --out results/coc/mini.jsonl --max-tokens 6144 [--no-probe]
"""
import argparse
import json
import os
import queue
import re
import sys
import threading
import time
import urllib.request
import zlib
from concurrent.futures import ThreadPoolExecutor
from transformers import AutoTokenizer
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from grade import last_boxed # noqa: E402
ap = argparse.ArgumentParser()
ap.add_argument("--inp", nargs="+", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--max-tokens", type=int, default=16384)
ap.add_argument("--chunk", type=int, default=1024)
ap.add_argument("--agree", type=int, default=2, help="consecutive identical probe answers needed to commit")
ap.add_argument("--probe-tokens", type=int, default=400)
ap.add_argument("--no-probe", action="store_true")
ap.add_argument("--record-all", action="store_true",
help="shadow mode that never stops probing: stores every probe (cut, answer, text) for offline "
"simulation of other chunk/agreement settings")
ap.add_argument("--shadow", action="store_true",
help="on consensus, record the committed answer but keep generating to the end: one run yields both "
"outcomes on the same trajectory (exact pairing) and also training data")
ap.add_argument("--workers", type=int, default=24, help="main slots 0..W-1, probe slots W..2W-1 (server -np 2W)")
ap.add_argument("--temperature", type=float, default=0.6)
ap.add_argument("--top-p", type=float, default=0.95)
ap.add_argument("--top-k", type=int, default=50)
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--port", type=int, default=8080)
ap.add_argument("--tokenizer", default="models/LFM3-1.7B")
args = ap.parse_args()
THINK_END, EOS = 124902, 124900
tok = AutoTokenizer.from_pretrained(args.tokenizer)
ENDS = set()
for i_ in range(len(tok)):
t_ = tok.convert_ids_to_tokens(i_)
if t_ is not None and (t_.endswith(".") or t_.endswith("\u010a")):
ENDS.add(i_)
items = []
for path in args.inp:
items += [json.loads(l) for l in open(path, encoding="utf-8")]
done = set()
if os.path.exists(args.out):
done = {json.loads(l)["id"] for l in open(args.out, encoding="utf-8")}
items = [it for it in items if it["id"] not in done]
print(len(items), "items", flush=True)
os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
out_f = open(args.out, "a", encoding="utf-8")
lock = threading.Lock()
slots = queue.Queue()
for w in range(args.workers):
slots.put(w)
stats = {"n": 0, "commit": 0, "t0": time.time(), "tok": 0}
def post(body):
for attempt in range(5):
try:
req = urllib.request.Request(f"http://localhost:{args.port}/completion", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(req, timeout=3600).read())
except Exception:
if attempt == 4:
raise
time.sleep(2)
def extract(task, text):
"""answer identity used for the consensus test"""
if task in ("mmlupro",):
m = re.findall(r"answer is:?\s*\(?\**\s*([A-J])\b", text) or re.findall(r"\\boxed\{\(?([A-J])\)?\}", text)
return m[-1] if m else None
if task == "bbh":
m = re.findall(r"answer is:?\s*(.+)", text, re.I)
a = m[-1] if m else (last_boxed(text) or "")
a = re.sub(r"\\boxed\{(.*)\}", r"\1", a).strip().strip("*$ .").lower()
return a or None
b = last_boxed(text)
if b is None and task == "gsm8k":
nums = re.findall(r"-?\d[\d,]*\.?\d*", text)
b = nums[-1] if nums else None
return re.sub(r"\s+", "", b) if b else None
def snap(gen, lo):
for j in range(len(gen), lo, -1):
if gen[j - 1] in ENDS:
return j
return None
def run(it):
w = slots.get()
try:
ids = tok.apply_chat_template([{"role": "user", "content": it["prompt"]}], tokenize=True,
add_generation_prompt=True, return_dict=False)
task = it["task"]
probe = not args.no_probe and not task.startswith("code") and task not in ("humaneval", "mbpp", "ifeval")
base_seed = args.seed * 1000003 + zlib.crc32(it["id"].encode()) % 1000003 * 7
gen, stop, last_ans, prev_cut, commit, n_chunk, n_probe = [], None, None, 0, None, 0, 0
plog = []
while True:
n_pred = min(args.chunk, args.max_tokens - len(gen))
if n_pred <= 0:
stop = "limit"
break
res = post({"prompt": ids + gen, "n_predict": n_pred, "temperature": args.temperature,
"top_p": args.top_p, "top_k": args.top_k, "min_p": 0.0, "repeat_penalty": 1.0,
"seed": base_seed + 7919 * n_chunk, "cache_prompt": True, "id_slot": w,
"samplers": ["top_k", "top_p", "temperature"], "return_tokens": True})
n_chunk += 1
gen += res["tokens"]
if res.get("stop_type") == "eos" or (res["tokens"] and res["tokens"][-1] == EOS):
stop = "eos"
break
if not probe or THINK_END in gen:
continue
cut = snap(gen, prev_cut)
if cut is None:
continue
prev_cut = cut
pr = post({"prompt": ids + gen[:cut] + [THINK_END], "n_predict": args.probe_tokens, "temperature": 0.0,
"cache_prompt": False, "id_slot": args.workers + w, "return_tokens": True})
n_probe += 1
ans = extract(task, pr["content"])
if args.record_all:
plog.append({"cut": cut, "ans": ans, "text": pr["content"], "n": len(pr["tokens"])})
continue
if ans is not None and ans == last_ans:
commit = (cut, pr)
if not args.shadow:
break
probe = False
continue
last_ans = ans
shadow = {}
if commit and args.shadow:
cut, pr = commit
shadow = {"coc_completion": "" + tok.decode(gen[:cut]) + "" + pr["content"],
"coc_n_tokens": cut + len(pr["tokens"]), "coc_tokens": gen[:cut] + [THINK_END] + pr["tokens"]}
commit = None
if commit:
cut, pr = commit
text = tok.decode(gen[:cut]) + "" + pr["content"]
n_tok, trunc, stop_type = cut + len(pr["tokens"]), False, "commit"
else:
if gen and gen[-1] == EOS:
gen = gen[:-1]
text = tok.decode(gen)
n_tok, trunc, stop_type = len(gen), stop == "limit", stop
rec = {"id": it["id"], "task": task, "sample": 0, "completion": "" + text, "n_tokens": n_tok,
"truncated": trunc, "stop_type": stop_type, "coc_cut": commit[0] if commit else None,
"probes": n_probe, "gen_tokens": len(gen), **shadow}
if args.record_all:
rec["probe_log"] = plog
if shadow:
rec["coc_cut"] = shadow["coc_n_tokens"]
with lock:
out_f.write(json.dumps(rec, ensure_ascii=False) + "\n")
out_f.flush()
stats["n"] += 1
stats["commit"] += bool(commit) or bool(shadow)
stats["tok"] += len(gen)
if stats["n"] % 20 == 0:
el = time.time() - stats["t0"]
print(f"{stats['n']}/{len(items)} commits {stats['commit']} {stats['tok'] / el:.0f} gen tok/s "
f"{el / 60:.1f} min", flush=True)
finally:
slots.put(w)
with ThreadPoolExecutor(args.workers) as ex:
list(ex.map(run, items))
print("done", stats, flush=True)