Text Generation
Transformers
Safetensors
qwen2
unsloth
conversational
text-generation-inference
4-bit precision
bitsandbytes
Instructions to use Santhoshini/iol-solver-14b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Santhoshini/iol-solver-14b with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="Santhoshini/iol-solver-14b") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("Santhoshini/iol-solver-14b") model = AutoModelForCausalLM.from_pretrained("Santhoshini/iol-solver-14b", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use Santhoshini/iol-solver-14b with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "Santhoshini/iol-solver-14b" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Santhoshini/iol-solver-14b", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/Santhoshini/iol-solver-14b
- SGLang
How to use Santhoshini/iol-solver-14b with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "Santhoshini/iol-solver-14b" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Santhoshini/iol-solver-14b", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "Santhoshini/iol-solver-14b" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Santhoshini/iol-solver-14b", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Unsloth Desktop
- Docker Model Runner
How to use Santhoshini/iol-solver-14b with Docker Model Runner:
docker model run hf.co/Santhoshini/iol-solver-14b
Update script.py
Browse files
script.py
CHANGED
|
@@ -1,29 +1,25 @@
|
|
| 1 |
-
# script.py —
|
| 2 |
-
#
|
| 3 |
-
# + safe arithmetic for number tasks + guaranteed explanations.
|
| 4 |
# =============================================================================
|
| 5 |
-
#
|
| 6 |
-
#
|
| 7 |
-
#
|
| 8 |
-
#
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
-
#
|
| 12 |
-
#
|
| 13 |
-
#
|
| 14 |
-
#
|
|
|
|
|
|
|
| 15 |
# =============================================================================
|
| 16 |
import os
|
| 17 |
import atexit
|
| 18 |
-
|
| 19 |
-
# it exactly (unset if they were unset, original value if they had one).
|
| 20 |
_ORIGINAL_HF_HUB_OFFLINE = os.environ.get("HF_HUB_OFFLINE")
|
| 21 |
_ORIGINAL_TRANSFORMERS_OFFLINE = os.environ.get("TRANSFORMERS_OFFLINE")
|
| 22 |
def _restore_offline_env_vars():
|
| 23 |
-
"""Restores these two variables to their exact pre-script state on
|
| 24 |
-
exit. Harmless belt-and-suspenders: the scoring traceback reaches the
|
| 25 |
-
network, so offline vars are demonstrably NOT the failure cause, but
|
| 26 |
-
restoring them cannot hurt and keeps the file otherwise identical."""
|
| 27 |
for key, original in (("HF_HUB_OFFLINE", _ORIGINAL_HF_HUB_OFFLINE),
|
| 28 |
("TRANSFORMERS_OFFLINE", _ORIGINAL_TRANSFORMERS_OFFLINE)):
|
| 29 |
if original is None:
|
|
@@ -35,8 +31,6 @@ os.environ["HF_HUB_OFFLINE"] = "1"
|
|
| 35 |
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
| 36 |
import subprocess, sys
|
| 37 |
def emergency_submission_csv(reason, rows_so_far=None):
|
| 38 |
-
"""Last-resort guarantee: no matter WHERE the script dies, write a valid
|
| 39 |
-
submission.csv before the process exits."""
|
| 40 |
try:
|
| 41 |
import pandas as pd
|
| 42 |
if rows_so_far:
|
|
@@ -52,17 +46,12 @@ def emergency_submission_csv(reason, rows_so_far=None):
|
|
| 52 |
"explanation": f"EMERGENCY FALLBACK: {str(reason)[:150]}"} for i in ids]
|
| 53 |
pd.DataFrame(rows, columns=["id", "pred", "explanation"]).to_csv("submission.csv", index=False)
|
| 54 |
except Exception:
|
| 55 |
-
# Absolute last resort: a header-only file is still a file.
|
| 56 |
try:
|
| 57 |
with open("submission.csv", "w") as f:
|
| 58 |
f.write("id,pred,explanation\n")
|
| 59 |
except Exception:
|
| 60 |
pass
|
| 61 |
def write_submission_csv(rows_list):
|
| 62 |
-
"""Stdlib csv, not pandas -- avoids a real pandas/numpy ABI crash inside
|
| 63 |
-
pandas' Index construction seen in this exact sandbox. ATOMIC: writes to
|
| 64 |
-
a temp file then os.replace()s it into place, so a reader can never
|
| 65 |
-
observe a partially-written file mid-save."""
|
| 66 |
import csv as _csv
|
| 67 |
tmp_path = "submission.csv.tmp"
|
| 68 |
with open(tmp_path, "w", newline="", encoding="utf-8") as f:
|
|
@@ -72,17 +61,6 @@ def write_submission_csv(rows_list):
|
|
| 72 |
w.writerow(row)
|
| 73 |
os.replace(tmp_path, "submission.csv")
|
| 74 |
try:
|
| 75 |
-
# CONTROLLED EXPERIMENT: no upgrades, no networking-stack churn.
|
| 76 |
-
# We install NOTHING that could pull a newer huggingface_hub / requests /
|
| 77 |
-
# urllib3 into the shared site-packages, because the grader's scoring step
|
| 78 |
-
# (metric.py:179 -> hf_hub_download of the solution file) runs on whatever
|
| 79 |
-
# stack we leave behind. bitsandbytes is the only package the 4-bit model
|
| 80 |
-
# genuinely needs that may be absent; --no-deps guarantees it cannot drag
|
| 81 |
-
# huggingface_hub / requests / urllib3 up with it. torch, pandas,
|
| 82 |
-
# transformers, accelerate, tokenizers are used at their PREINSTALLED
|
| 83 |
-
# sandbox versions. If a genuine ModuleNotFoundError appears in OUR script
|
| 84 |
-
# (non-zero exit, a different failure than the scoring RemoteDisconnected),
|
| 85 |
-
# add just that one package back with --no-deps.
|
| 86 |
subprocess.run([sys.executable, "-m", "pip", "install", "-q",
|
| 87 |
"--no-deps", "bitsandbytes"], check=True)
|
| 88 |
except Exception as e:
|
|
@@ -94,13 +72,9 @@ import torch
|
|
| 94 |
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 95 |
MODEL_ID = "."
|
| 96 |
TIME_LIMIT_S = 30 * 60
|
| 97 |
-
SETUP_BUFFER_S = 420
|
| 98 |
start_time = time.time()
|
| 99 |
try:
|
| 100 |
-
# Read test.csv and checkpoint a placeholder submission FIRST, before
|
| 101 |
-
# the slowest and most failure-prone step (model loading, ~7-9 min for
|
| 102 |
-
# this checkpoint size) even starts. If loading hangs or gets killed,
|
| 103 |
-
# something valid already exists on disk rather than nothing at all.
|
| 104 |
df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
|
| 105 |
placeholder_rows = [{"id": rid, "pred": json.dumps([""]),
|
| 106 |
"explanation": "Placeholder written before model load."}
|
|
@@ -111,10 +85,6 @@ try:
|
|
| 111 |
tok = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 112 |
print("Tokenizer loaded (fast).", flush=True)
|
| 113 |
except Exception as e:
|
| 114 |
-
# Mechanism-level fix: bypasses TokenizerFast.from_file() entirely,
|
| 115 |
-
# which is exactly the call that fails on a tokenizer.json saved by a
|
| 116 |
-
# newer tokenizers library than the sandbox has. Falls back to the
|
| 117 |
-
# pure Python tokenizer built from vocab.json/merges.txt instead.
|
| 118 |
print(f"Fast tokenizer failed ({e}); falling back to use_fast=False.", flush=True)
|
| 119 |
tok = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=False)
|
| 120 |
print("Tokenizer loaded (slow fallback).", flush=True)
|
|
@@ -127,18 +97,12 @@ except Exception as e:
|
|
| 127 |
emergency_submission_csv(f"tokenizer/model load or test.csv read failed: {e}")
|
| 128 |
raise
|
| 129 |
n_rows = len(df)
|
| 130 |
-
# Measured, not estimated: if real loading took longer or shorter than the
|
| 131 |
-
# SETUP_BUFFER_S guess, every downstream timing decision for the rest of the
|
| 132 |
-
# run should work from what actually happened, not a stale assumption.
|
| 133 |
actual_setup_elapsed = time.time() - start_time
|
| 134 |
per_row_budget = max(20, (TIME_LIMIT_S - actual_setup_elapsed) / max(n_rows, 1))
|
| 135 |
-
print(f"Setup took {actual_setup_elapsed:.0f}s
|
| 136 |
-
f"
|
| 137 |
-
# ---- Query parsing
|
| 138 |
def parse_items(query: str):
|
| 139 |
-
"""Returns (preamble, items, count_known). count_known=False means no
|
| 140 |
-
pattern matched -- we do NOT guess a count, we let the model's own
|
| 141 |
-
answer list stand rather than risk truncating real content."""
|
| 142 |
item_pat = re.compile(r"(?m)^\s*(\d+)\s*[.\)]\s*(.*)$")
|
| 143 |
matches = list(item_pat.finditer(query))
|
| 144 |
if matches:
|
|
@@ -180,20 +144,9 @@ TASK_GUIDANCE = {
|
|
| 180 |
"num_to_text": "give the number written out in words, in the language asked.",
|
| 181 |
}
|
| 182 |
DEFAULT_GUIDANCE = "give exactly what the instruction asks, nothing else."
|
| 183 |
-
# =============================================================================
|
| 184 |
-
# SYMBOLIC PREPROCESSING LAYER -- pure Python standard library only (re,
|
| 185 |
-
# difflib, collections), no new dependencies. Deterministic, CPU-only,
|
| 186 |
-
# negligible runtime. Augments the raw context; never replaces or rewrites
|
| 187 |
-
# any of it.
|
| 188 |
-
# =============================================================================
|
| 189 |
from difflib import SequenceMatcher
|
| 190 |
-
from collections import defaultdict
|
| 191 |
def extract_forms_from_context(context: str):
|
| 192 |
-
"""Pulls candidate unknown-language 'forms' out of raw context text, for
|
| 193 |
-
reduplication's per-word self-check ONLY. Pipe-delimited lines
|
| 194 |
-
contribute ONLY their FIRST field (the conventional unknown-language
|
| 195 |
-
side). Plain lines contribute whitespace tokens. Lines with more than 3
|
| 196 |
-
pipes are skipped defensively (click-language transcriptions)."""
|
| 197 |
forms = []
|
| 198 |
for line in context.splitlines():
|
| 199 |
line = line.strip()
|
|
@@ -216,9 +169,6 @@ def extract_forms_from_context(context: str):
|
|
| 216 |
unique_forms.append(f)
|
| 217 |
return unique_forms
|
| 218 |
def extract_explicit_pairs(context: str):
|
| 219 |
-
"""Extracts genuine (input, output) pairs from pipe-delimited rows using
|
| 220 |
-
the row's own layout, not language-specific assumptions. Forms from
|
| 221 |
-
DIFFERENT rows are never cross-compared."""
|
| 222 |
pairs = []
|
| 223 |
for line in context.splitlines():
|
| 224 |
line = line.strip()
|
|
@@ -230,9 +180,6 @@ def extract_explicit_pairs(context: str):
|
|
| 230 |
pairs.append((fields[0], fields[1]))
|
| 231 |
return pairs
|
| 232 |
def edit_signature(a: str, b: str):
|
| 233 |
-
"""A clean single-region transformation signature between two strings,
|
| 234 |
-
or None if the difference is scattered across multiple regions, OR if
|
| 235 |
-
there is no genuine shared stem of at least 2 characters."""
|
| 236 |
sm = SequenceMatcher(None, a, b, autojunk=False)
|
| 237 |
all_ops = sm.get_opcodes()
|
| 238 |
ops = [op for op in all_ops if op[0] != "equal"]
|
|
@@ -251,9 +198,6 @@ def edit_signature(a: str, b: str):
|
|
| 251 |
pos = "infix"
|
| 252 |
return (pos, removed, inserted)
|
| 253 |
def find_transformation_families(pairs):
|
| 254 |
-
"""Clusters GENUINELY PAIRED forms (same row only) sharing an identical
|
| 255 |
-
clean edit signature. Emits a family only if 2+ separate given pairs
|
| 256 |
-
share it."""
|
| 257 |
groups = defaultdict(list)
|
| 258 |
for a, b in pairs:
|
| 259 |
if not a or not b or a == b:
|
|
@@ -271,11 +215,9 @@ def find_transformation_families(pairs):
|
|
| 271 |
examples = "; ".join(f"{a}->{b}" for a, b in unique_pairs[:4])
|
| 272 |
families.append((len(unique_pairs),
|
| 273 |
f"{pos} change: '{removed_disp}' -> '{inserted_disp}' (seen in: {examples})"))
|
| 274 |
-
families.sort(key=lambda x: -x[0])
|
| 275 |
return [f for _, f in families]
|
| 276 |
def detect_reduplication(forms):
|
| 277 |
-
"""Flags a word only if it contains an exact adjacent doubled substring
|
| 278 |
-
(length >= 2). Emits nothing if absent."""
|
| 279 |
findings = []
|
| 280 |
for w in forms:
|
| 281 |
n = len(w)
|
|
@@ -292,8 +234,6 @@ def detect_reduplication(forms):
|
|
| 292 |
break
|
| 293 |
return findings
|
| 294 |
def build_symbolic_evidence(context: str) -> str:
|
| 295 |
-
"""The full symbolic layer. Returns "" if no supported transformation
|
| 296 |
-
family and no reduplication is found. Never replaces context."""
|
| 297 |
forms = extract_forms_from_context(context)
|
| 298 |
pairs = extract_explicit_pairs(context)
|
| 299 |
families = find_transformation_families(pairs) if pairs else []
|
|
@@ -313,11 +253,9 @@ def build_symbolic_evidence(context: str) -> str:
|
|
| 313 |
"may be incomplete -- verify against the examples, do not trust blindly):\n"
|
| 314 |
+ "\n".join(lines))
|
| 315 |
def build_messages(context, query, task_type):
|
| 316 |
-
"""The frozen single-call decomposition scaffold behind the accepted
|
| 317 |
-
0.083/0.0296/0.2323 baseline."""
|
| 318 |
preamble, items, count_known = parse_items(query)
|
| 319 |
guidance = TASK_GUIDANCE.get(task_type, DEFAULT_GUIDANCE)
|
| 320 |
-
symbolic_evidence = build_symbolic_evidence(context)
|
| 321 |
system = (
|
| 322 |
"You solve puzzles about a language you have never seen. Everything you "
|
| 323 |
"need is in the examples below. Use only the examples, not outside "
|
|
@@ -386,7 +324,6 @@ def build_repair_messages(query, n_items, bad_text):
|
|
| 386 |
f"FINAL ANSWERS: answer1 | answer2"
|
| 387 |
)
|
| 388 |
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
| 389 |
-
# ---- Safe arithmetic: no exec(), no eval() of arbitrary code ----
|
| 390 |
_ALLOWED_BINOPS = (pyast.Add, pyast.Sub, pyast.Mult)
|
| 391 |
def safe_arithmetic(expr: str):
|
| 392 |
try:
|
|
@@ -411,16 +348,16 @@ def safe_arithmetic(expr: str):
|
|
| 411 |
return None
|
| 412 |
return _eval(tree)
|
| 413 |
def clean_answer(a: str) -> str:
|
| 414 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
a = re.sub(r"(?i)^\s*(the\s+)?(final\s+)?answer\s*\d*\s*(is)?\s*:\s*", "", a).strip()
|
| 416 |
a = re.sub(r"(?i)^\s*is\s*:\s*", "", a).strip()
|
| 417 |
a = a.strip("* ")
|
| 418 |
return a.strip(" .\"'“”‘’")
|
| 419 |
def extract(text):
|
| 420 |
-
"""Fixed against three real bugs found on real Linguini output:
|
| 421 |
-
(1) markdown-bold marker with content on the NEXT line, not same line;
|
| 422 |
-
(2) a following COMPUTE: line bleeding into the answer list;
|
| 423 |
-
(3) NO marker found + all answers dumped on one pipe-separated line."""
|
| 424 |
m = list(re.finditer(r"final answers?\s*:?\s*\**", text, flags=re.IGNORECASE))
|
| 425 |
if m:
|
| 426 |
tail = text[m[-1].end():]
|
|
@@ -432,7 +369,6 @@ def extract(text):
|
|
| 432 |
parts = [clean_answer(p) for p in candidate.split("|") if p.strip()]
|
| 433 |
if parts:
|
| 434 |
return parts, m[-1].start()
|
| 435 |
-
# Fallback (no marker found): split each line further by "|" if present.
|
| 436 |
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
| 437 |
fallback = []
|
| 438 |
for ln in lines:
|
|
@@ -453,15 +389,7 @@ def extract_compute_overrides(text, n_answers):
|
|
| 453 |
if val is not None:
|
| 454 |
overrides[i] = str(int(val)) if float(val).is_integer() else str(val)
|
| 455 |
return overrides
|
| 456 |
-
|
| 457 |
-
def generate(messages, max_new_tokens, constraint_fn=None, sample=False, seed=None):
|
| 458 |
-
"""constraint_fn: optional prefix_allowed_tokens_fn, default None means
|
| 459 |
-
byte-identical behavior to every existing call site. If a constrained
|
| 460 |
-
attempt fails for ANY reason, falls back to a fully UNCONSTRAINED
|
| 461 |
-
generation. sample=True switches to Qwen's recommended stochastic
|
| 462 |
-
settings (temp 0.7 / top_p 0.8 / top_k 20) for MBR candidate generation;
|
| 463 |
-
the greedy default path is untouched, so every existing call is
|
| 464 |
-
byte-identical to the 0.104 baseline."""
|
| 465 |
def _try_generate(gen_kwargs):
|
| 466 |
try:
|
| 467 |
enc = tok.apply_chat_template(
|
|
@@ -478,15 +406,7 @@ def generate(messages, max_new_tokens, constraint_fn=None, sample=False, seed=No
|
|
| 478 |
with torch.no_grad():
|
| 479 |
out = model.generate(ids, **gen_kwargs)
|
| 480 |
return out, input_len
|
| 481 |
-
|
| 482 |
-
if seed is not None:
|
| 483 |
-
torch.manual_seed(seed)
|
| 484 |
-
if torch.cuda.is_available():
|
| 485 |
-
torch.cuda.manual_seed_all(seed)
|
| 486 |
-
base_kwargs = {"max_new_tokens": max_new_tokens, "do_sample": True,
|
| 487 |
-
"temperature": 0.7, "top_p": 0.8, "top_k": 20}
|
| 488 |
-
else:
|
| 489 |
-
base_kwargs = {"max_new_tokens": max_new_tokens, "do_sample": False}
|
| 490 |
if constraint_fn is not None:
|
| 491 |
try:
|
| 492 |
out, input_len = _try_generate({**base_kwargs, "prefix_allowed_tokens_fn": constraint_fn})
|
|
@@ -501,36 +421,7 @@ EXPLANATION_SYSTEM = (
|
|
| 501 |
"Be concise and structured -- do not repeat the full reasoning."
|
| 502 |
)
|
| 503 |
EXPLANATION_FALLBACK = "Answer derived from patterns found in the examples above."
|
| 504 |
-
_LETTER_CONSTRAINT_CACHE = {}
|
| 505 |
-
def build_letter_constraint_fn(tok, valid_letters):
|
| 506 |
-
cache_key = (id(tok), tuple(sorted(valid_letters)))
|
| 507 |
-
if cache_key in _LETTER_CONSTRAINT_CACHE:
|
| 508 |
-
return _LETTER_CONSTRAINT_CACHE[cache_key]
|
| 509 |
-
try:
|
| 510 |
-
allowed_chars = set(valid_letters) | set(" |\n\t\r")
|
| 511 |
-
eos = tok.eos_token_id
|
| 512 |
-
pieces = []
|
| 513 |
-
for token_id in range(len(tok)):
|
| 514 |
-
if token_id == eos:
|
| 515 |
-
continue
|
| 516 |
-
piece = tok.decode([token_id], skip_special_tokens=False)
|
| 517 |
-
if piece and all(c in allowed_chars for c in piece):
|
| 518 |
-
pieces.append(token_id)
|
| 519 |
-
allowed_ids = ([eos] if eos is not None else []) + pieces
|
| 520 |
-
def allowed(_batch_id, _input_ids):
|
| 521 |
-
return allowed_ids if allowed_ids else list(range(len(tok)))
|
| 522 |
-
_LETTER_CONSTRAINT_CACHE[cache_key] = allowed
|
| 523 |
-
return allowed
|
| 524 |
-
except Exception:
|
| 525 |
-
return None
|
| 526 |
-
# =============================================================================
|
| 527 |
-
# closed-answer-space pre-constraint, match_letters only. Deterministic,
|
| 528 |
-
# read-only. Extracts the closed set of option letters genuinely present in
|
| 529 |
-
# the context and states that exact set as a hard constraint. FAIL-OPEN.
|
| 530 |
-
# =============================================================================
|
| 531 |
def extract_match_letter_options(context: str):
|
| 532 |
-
"""Returns a sorted list of option letters if extraction is CLEAN and
|
| 533 |
-
UNAMBIGUOUS, else None. Deliberately strict: this must never guess."""
|
| 534 |
found = set()
|
| 535 |
for line in context.splitlines():
|
| 536 |
for m in re.finditer(r"(?:^|\s)([A-Z])[.\)]\s+\S", line):
|
|
@@ -545,67 +436,11 @@ def extract_match_letter_options(context: str):
|
|
| 545 |
return None
|
| 546 |
return letters
|
| 547 |
# =============================================================================
|
| 548 |
-
#
|
| 549 |
-
#
|
| 550 |
-
#
|
| 551 |
-
#
|
| 552 |
-
#
|
| 553 |
-
# "chrF-central" answer. That central answer tends to be both the highest-chrF
|
| 554 |
-
# AND the likeliest exact match, so it lifts both scored components at once.
|
| 555 |
-
# Pure stdlib, no dependencies. The greedy candidate is always in the pool, so
|
| 556 |
-
# MBR can only match or beat greedy, never regress it.
|
| 557 |
-
# =============================================================================
|
| 558 |
-
def _char_ngrams(s, n):
|
| 559 |
-
return Counter(s[i:i + n] for i in range(len(s) - n + 1)) if len(s) >= n else Counter()
|
| 560 |
-
def chrf_score(hyp, ref, max_n=6, beta=2.0):
|
| 561 |
-
"""Character n-gram F-beta (chrF), the same family the grader uses. Light
|
| 562 |
-
normalization (lowercase, collapse whitespace) so it rewards content, not
|
| 563 |
-
incidental spacing/case. Returns 1.0 on exact post-normalization match."""
|
| 564 |
-
hyp = re.sub(r"\s+", " ", (hyp or "").strip().lower())
|
| 565 |
-
ref = re.sub(r"\s+", " ", (ref or "").strip().lower())
|
| 566 |
-
if hyp == ref:
|
| 567 |
-
return 1.0
|
| 568 |
-
if not hyp or not ref:
|
| 569 |
-
return 0.0
|
| 570 |
-
b2 = beta * beta
|
| 571 |
-
total, counted = 0.0, 0
|
| 572 |
-
for n in range(1, max_n + 1):
|
| 573 |
-
h, r = _char_ngrams(hyp, n), _char_ngrams(ref, n)
|
| 574 |
-
if not h or not r:
|
| 575 |
-
continue
|
| 576 |
-
overlap = sum((h & r).values())
|
| 577 |
-
p = overlap / max(sum(h.values()), 1)
|
| 578 |
-
rec = overlap / max(sum(r.values()), 1)
|
| 579 |
-
f = 0.0 if (p + rec) == 0 else (1 + b2) * p * rec / (b2 * p + rec)
|
| 580 |
-
total += f
|
| 581 |
-
counted += 1
|
| 582 |
-
return total / counted if counted else 0.0
|
| 583 |
-
def mbr_select_item(cands):
|
| 584 |
-
"""Pick the chrF-central candidate for one item. Ties keep the first
|
| 585 |
-
(which is the greedy answer, since it is inserted first)."""
|
| 586 |
-
cands = [c for c in cands if c is not None]
|
| 587 |
-
if not cands:
|
| 588 |
-
return ""
|
| 589 |
-
if len(set(cands)) == 1:
|
| 590 |
-
return cands[0]
|
| 591 |
-
best, best_score = cands[0], -1.0
|
| 592 |
-
for i, c in enumerate(cands):
|
| 593 |
-
others = [cands[j] for j in range(len(cands)) if j != i]
|
| 594 |
-
score = sum(chrf_score(c, o) for o in others) / max(len(others), 1)
|
| 595 |
-
if score > best_score:
|
| 596 |
-
best, best_score = c, score
|
| 597 |
-
return best
|
| 598 |
-
# =============================================================================
|
| 599 |
-
# MATCH_LETTERS: reason first, then a bijection-CONSTRAINED answer pass.
|
| 600 |
-
# The main greedy generation is left to reason freely and produce FINAL
|
| 601 |
-
# ANSWERS. Then, ONLY for match_letters, we run a short second pass whose
|
| 602 |
-
# decoding grammar (a) allows only valid option letters + separators + EOS,
|
| 603 |
-
# and (b) when #letters == #items (a clean bijection), forbids any letter
|
| 604 |
-
# already emitted -- so the output physically cannot repeat or skip a letter.
|
| 605 |
-
# This is the structural half of how humans solve matching puzzles
|
| 606 |
-
# (anchor + elimination), enforced at the decoder. FAIL-OPEN everywhere:
|
| 607 |
-
# any failure or a non-bijection problem falls straight back to the greedy
|
| 608 |
-
# answers, so this can only help.
|
| 609 |
# =============================================================================
|
| 610 |
_LETTER_TOKEN_CACHE = {}
|
| 611 |
def _letter_token_map(tok, labels):
|
|
@@ -631,14 +466,12 @@ def build_answer_only_messages(query, reasoning_text, labels, n_items, bijection
|
|
| 631 |
"No numbering, no words, no punctuation.")
|
| 632 |
user = (
|
| 633 |
f"Task:\n{query.strip()}\n\n"
|
| 634 |
-
f"Your reasoning:\n{reasoning_text[:
|
| 635 |
f"Valid letters: {', '.join(labels)}. Give exactly {n_items} letters, one per "
|
| 636 |
f"line, in item order. Each item takes exactly one letter{uniq}."
|
| 637 |
)
|
| 638 |
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
| 639 |
def generate_constrained_letters(messages, labels, n_items, bijection, max_new_tokens=96):
|
| 640 |
-
"""Own encode/generate so the stateful constraint knows where the prompt
|
| 641 |
-
ends. Returns a list of letters or None on any failure (fail-open)."""
|
| 642 |
labels = set(labels)
|
| 643 |
try:
|
| 644 |
try:
|
|
@@ -704,13 +537,7 @@ try:
|
|
| 704 |
if idx < len(answers):
|
| 705 |
answers[idx] = val
|
| 706 |
if (marker_pos is None or not answers) and remaining > SETUP_BUFFER_S:
|
| 707 |
-
|
| 708 |
-
if task_type == "match_letters":
|
| 709 |
-
repair_options = extract_match_letter_options(r["context"])
|
| 710 |
-
if repair_options:
|
| 711 |
-
repair_constraint = build_letter_constraint_fn(tok, repair_options)
|
| 712 |
-
repair_text = generate(build_repair_messages(r["query"], n_items, text), 128,
|
| 713 |
-
constraint_fn=repair_constraint)
|
| 714 |
rep, rep_pos = extract(repair_text)
|
| 715 |
if rep:
|
| 716 |
answers, marker_pos = rep, rep_pos
|
|
@@ -719,14 +546,12 @@ try:
|
|
| 719 |
answers = answers + [answers[-1] if answers else ""] * (n_items - len(answers))
|
| 720 |
elif len(answers) > n_items and marker_pos is None:
|
| 721 |
answers = answers[:n_items]
|
| 722 |
-
# else: marker found, more answers than our guess -> KEEP THEM ALL
|
| 723 |
if not answers:
|
| 724 |
answers = [""]
|
| 725 |
-
# ----
|
| 726 |
-
# match_letters: reason (already done above) -> bijection-constrained answer pass.
|
| 727 |
if task_type == "match_letters" and n_items:
|
| 728 |
labels = extract_match_letter_options(r["context"])
|
| 729 |
-
if labels and len(labels) >= 2:
|
| 730 |
bijection = (len(labels) == n_items)
|
| 731 |
refined = generate_constrained_letters(
|
| 732 |
build_answer_only_messages(r["query"], text, labels, n_items, bijection),
|
|
@@ -734,25 +559,6 @@ try:
|
|
| 734 |
)
|
| 735 |
if refined and len(refined) == n_items:
|
| 736 |
answers = refined
|
| 737 |
-
# translation: chrF-MBR, but only when we can afford ~2 extra full
|
| 738 |
-
# generations without eating another row's budget. Time-gated so it
|
| 739 |
-
# never causes a timeout; greedy answer is always candidate #1.
|
| 740 |
-
elif task_type == "translation" and n_items and n_items > 0:
|
| 741 |
-
remaining_now = TIME_LIMIT_S - (time.time() - start_time)
|
| 742 |
-
rows_left = max(n_rows - len(rows), 1)
|
| 743 |
-
afford_mbr = remaining_now > rows_left * per_row_budget * 2.2
|
| 744 |
-
if afford_mbr and n_items <= 20:
|
| 745 |
-
cand_lists = [answers]
|
| 746 |
-
for s in range(2):
|
| 747 |
-
stext = generate(messages, tokens_cap, sample=True, seed=1234 + s)
|
| 748 |
-
sa, _ = extract(stext)
|
| 749 |
-
if len(sa) >= n_items:
|
| 750 |
-
cand_lists.append(sa[:n_items])
|
| 751 |
-
elif sa:
|
| 752 |
-
cand_lists.append(sa + [sa[-1]] * (n_items - len(sa)))
|
| 753 |
-
if len(cand_lists) >= 2:
|
| 754 |
-
answers = [mbr_select_item([cl[i] for cl in cand_lists if i < len(cl)])
|
| 755 |
-
for i in range(n_items)]
|
| 756 |
remaining_after = TIME_LIMIT_S - (time.time() - start_time)
|
| 757 |
budget_left_after = max(n_rows - len(rows) - 1, 0)
|
| 758 |
comfortable = remaining_after > (budget_left_after + 1) * per_row_budget * 1.3
|
|
@@ -786,7 +592,6 @@ try:
|
|
| 786 |
if time.time() - start_time > TIME_LIMIT_S - 60:
|
| 787 |
print("Time budget nearly exhausted, stopping early.", flush=True)
|
| 788 |
break
|
| 789 |
-
# Guarantee one row per test.csv id, even under a timeout.
|
| 790 |
for _, r in df.iterrows():
|
| 791 |
if r["id"] in processed_ids:
|
| 792 |
continue
|
|
@@ -800,7 +605,5 @@ try:
|
|
| 800 |
write_submission_csv(rows)
|
| 801 |
print("DONE.", flush=True)
|
| 802 |
except Exception as e:
|
| 803 |
-
# Final safety net: even if something escapes every inner try/except
|
| 804 |
-
# above, whatever rows were collected so far still get written.
|
| 805 |
emergency_submission_csv(f"main loop failed: {e}", rows_so_far=rows if rows else None)
|
| 806 |
print(f"FATAL, but submission.csv was written with {len(rows)} rows. Error: {e}", flush=True)
|
|
|
|
| 1 |
+
# script.py — PROVEN bnb Qwen2.5-14B (0.104) + two SAFE, deterministic
|
| 2 |
+
# improvements only. No sampling, no MBR (that was the -0.07 regression).
|
|
|
|
| 3 |
# =============================================================================
|
| 4 |
+
# Base = the exact 0.104 pipeline (Qwen2.5-14B-Instruct-bnb-4bit, install only
|
| 5 |
+
# bitsandbytes --no-deps so the networking stack is untouched).
|
| 6 |
+
# ADDED (both deterministic, both fail-open, neither can tank exact_match):
|
| 7 |
+
# 1. match_letters BIJECTION-CONSTRAINED answer pass: after the model
|
| 8 |
+
# reasons, a short second pass regenerates just the letters under a
|
| 9 |
+
# decoder grammar that allows only valid option letters and — when
|
| 10 |
+
# #letters == #items — forbids any letter already used, so it cannot
|
| 11 |
+
# repeat or skip. Non-bijection / any failure -> fall back to greedy.
|
| 12 |
+
# 2. NFC unicode normalization in clean_answer: composes look-identical
|
| 13 |
+
# code points to their canonical form so a correct answer isn't lost to
|
| 14 |
+
# an invisible encoding difference. Purely canonical (does NOT swap
|
| 15 |
+
# characters), so it cannot corrupt puzzle-orthography answers.
|
| 16 |
# =============================================================================
|
| 17 |
import os
|
| 18 |
import atexit
|
| 19 |
+
import unicodedata
|
|
|
|
| 20 |
_ORIGINAL_HF_HUB_OFFLINE = os.environ.get("HF_HUB_OFFLINE")
|
| 21 |
_ORIGINAL_TRANSFORMERS_OFFLINE = os.environ.get("TRANSFORMERS_OFFLINE")
|
| 22 |
def _restore_offline_env_vars():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
for key, original in (("HF_HUB_OFFLINE", _ORIGINAL_HF_HUB_OFFLINE),
|
| 24 |
("TRANSFORMERS_OFFLINE", _ORIGINAL_TRANSFORMERS_OFFLINE)):
|
| 25 |
if original is None:
|
|
|
|
| 31 |
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
| 32 |
import subprocess, sys
|
| 33 |
def emergency_submission_csv(reason, rows_so_far=None):
|
|
|
|
|
|
|
| 34 |
try:
|
| 35 |
import pandas as pd
|
| 36 |
if rows_so_far:
|
|
|
|
| 46 |
"explanation": f"EMERGENCY FALLBACK: {str(reason)[:150]}"} for i in ids]
|
| 47 |
pd.DataFrame(rows, columns=["id", "pred", "explanation"]).to_csv("submission.csv", index=False)
|
| 48 |
except Exception:
|
|
|
|
| 49 |
try:
|
| 50 |
with open("submission.csv", "w") as f:
|
| 51 |
f.write("id,pred,explanation\n")
|
| 52 |
except Exception:
|
| 53 |
pass
|
| 54 |
def write_submission_csv(rows_list):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
import csv as _csv
|
| 56 |
tmp_path = "submission.csv.tmp"
|
| 57 |
with open(tmp_path, "w", newline="", encoding="utf-8") as f:
|
|
|
|
| 61 |
w.writerow(row)
|
| 62 |
os.replace(tmp_path, "submission.csv")
|
| 63 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
subprocess.run([sys.executable, "-m", "pip", "install", "-q",
|
| 65 |
"--no-deps", "bitsandbytes"], check=True)
|
| 66 |
except Exception as e:
|
|
|
|
| 72 |
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 73 |
MODEL_ID = "."
|
| 74 |
TIME_LIMIT_S = 30 * 60
|
| 75 |
+
SETUP_BUFFER_S = 420
|
| 76 |
start_time = time.time()
|
| 77 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
|
| 79 |
placeholder_rows = [{"id": rid, "pred": json.dumps([""]),
|
| 80 |
"explanation": "Placeholder written before model load."}
|
|
|
|
| 85 |
tok = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 86 |
print("Tokenizer loaded (fast).", flush=True)
|
| 87 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
print(f"Fast tokenizer failed ({e}); falling back to use_fast=False.", flush=True)
|
| 89 |
tok = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=False)
|
| 90 |
print("Tokenizer loaded (slow fallback).", flush=True)
|
|
|
|
| 97 |
emergency_submission_csv(f"tokenizer/model load or test.csv read failed: {e}")
|
| 98 |
raise
|
| 99 |
n_rows = len(df)
|
|
|
|
|
|
|
|
|
|
| 100 |
actual_setup_elapsed = time.time() - start_time
|
| 101 |
per_row_budget = max(20, (TIME_LIMIT_S - actual_setup_elapsed) / max(n_rows, 1))
|
| 102 |
+
print(f"Setup took {actual_setup_elapsed:.0f}s | per_row_budget={per_row_budget:.0f}s "
|
| 103 |
+
f"for {n_rows} rows", flush=True)
|
| 104 |
+
# ---- Query parsing ----
|
| 105 |
def parse_items(query: str):
|
|
|
|
|
|
|
|
|
|
| 106 |
item_pat = re.compile(r"(?m)^\s*(\d+)\s*[.\)]\s*(.*)$")
|
| 107 |
matches = list(item_pat.finditer(query))
|
| 108 |
if matches:
|
|
|
|
| 144 |
"num_to_text": "give the number written out in words, in the language asked.",
|
| 145 |
}
|
| 146 |
DEFAULT_GUIDANCE = "give exactly what the instruction asks, nothing else."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
from difflib import SequenceMatcher
|
| 148 |
+
from collections import defaultdict
|
| 149 |
def extract_forms_from_context(context: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
forms = []
|
| 151 |
for line in context.splitlines():
|
| 152 |
line = line.strip()
|
|
|
|
| 169 |
unique_forms.append(f)
|
| 170 |
return unique_forms
|
| 171 |
def extract_explicit_pairs(context: str):
|
|
|
|
|
|
|
|
|
|
| 172 |
pairs = []
|
| 173 |
for line in context.splitlines():
|
| 174 |
line = line.strip()
|
|
|
|
| 180 |
pairs.append((fields[0], fields[1]))
|
| 181 |
return pairs
|
| 182 |
def edit_signature(a: str, b: str):
|
|
|
|
|
|
|
|
|
|
| 183 |
sm = SequenceMatcher(None, a, b, autojunk=False)
|
| 184 |
all_ops = sm.get_opcodes()
|
| 185 |
ops = [op for op in all_ops if op[0] != "equal"]
|
|
|
|
| 198 |
pos = "infix"
|
| 199 |
return (pos, removed, inserted)
|
| 200 |
def find_transformation_families(pairs):
|
|
|
|
|
|
|
|
|
|
| 201 |
groups = defaultdict(list)
|
| 202 |
for a, b in pairs:
|
| 203 |
if not a or not b or a == b:
|
|
|
|
| 215 |
examples = "; ".join(f"{a}->{b}" for a, b in unique_pairs[:4])
|
| 216 |
families.append((len(unique_pairs),
|
| 217 |
f"{pos} change: '{removed_disp}' -> '{inserted_disp}' (seen in: {examples})"))
|
| 218 |
+
families.sort(key=lambda x: -x[0])
|
| 219 |
return [f for _, f in families]
|
| 220 |
def detect_reduplication(forms):
|
|
|
|
|
|
|
| 221 |
findings = []
|
| 222 |
for w in forms:
|
| 223 |
n = len(w)
|
|
|
|
| 234 |
break
|
| 235 |
return findings
|
| 236 |
def build_symbolic_evidence(context: str) -> str:
|
|
|
|
|
|
|
| 237 |
forms = extract_forms_from_context(context)
|
| 238 |
pairs = extract_explicit_pairs(context)
|
| 239 |
families = find_transformation_families(pairs) if pairs else []
|
|
|
|
| 253 |
"may be incomplete -- verify against the examples, do not trust blindly):\n"
|
| 254 |
+ "\n".join(lines))
|
| 255 |
def build_messages(context, query, task_type):
|
|
|
|
|
|
|
| 256 |
preamble, items, count_known = parse_items(query)
|
| 257 |
guidance = TASK_GUIDANCE.get(task_type, DEFAULT_GUIDANCE)
|
| 258 |
+
symbolic_evidence = build_symbolic_evidence(context)
|
| 259 |
system = (
|
| 260 |
"You solve puzzles about a language you have never seen. Everything you "
|
| 261 |
"need is in the examples below. Use only the examples, not outside "
|
|
|
|
| 324 |
f"FINAL ANSWERS: answer1 | answer2"
|
| 325 |
)
|
| 326 |
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
|
|
|
| 327 |
_ALLOWED_BINOPS = (pyast.Add, pyast.Sub, pyast.Mult)
|
| 328 |
def safe_arithmetic(expr: str):
|
| 329 |
try:
|
|
|
|
| 348 |
return None
|
| 349 |
return _eval(tree)
|
| 350 |
def clean_answer(a: str) -> str:
|
| 351 |
+
# NFC normalization first: composes visually-identical code points to a
|
| 352 |
+
# canonical form so a correct answer isn't lost to an invisible encoding
|
| 353 |
+
# difference. Canonical composition only -- never swaps a character for a
|
| 354 |
+
# different one, so puzzle-orthography answers stay intact.
|
| 355 |
+
a = unicodedata.normalize("NFC", a)
|
| 356 |
a = re.sub(r"(?i)^\s*(the\s+)?(final\s+)?answer\s*\d*\s*(is)?\s*:\s*", "", a).strip()
|
| 357 |
a = re.sub(r"(?i)^\s*is\s*:\s*", "", a).strip()
|
| 358 |
a = a.strip("* ")
|
| 359 |
return a.strip(" .\"'“”‘’")
|
| 360 |
def extract(text):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
m = list(re.finditer(r"final answers?\s*:?\s*\**", text, flags=re.IGNORECASE))
|
| 362 |
if m:
|
| 363 |
tail = text[m[-1].end():]
|
|
|
|
| 369 |
parts = [clean_answer(p) for p in candidate.split("|") if p.strip()]
|
| 370 |
if parts:
|
| 371 |
return parts, m[-1].start()
|
|
|
|
| 372 |
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
| 373 |
fallback = []
|
| 374 |
for ln in lines:
|
|
|
|
| 389 |
if val is not None:
|
| 390 |
overrides[i] = str(int(val)) if float(val).is_integer() else str(val)
|
| 391 |
return overrides
|
| 392 |
+
def generate(messages, max_new_tokens, constraint_fn=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
def _try_generate(gen_kwargs):
|
| 394 |
try:
|
| 395 |
enc = tok.apply_chat_template(
|
|
|
|
| 406 |
with torch.no_grad():
|
| 407 |
out = model.generate(ids, **gen_kwargs)
|
| 408 |
return out, input_len
|
| 409 |
+
base_kwargs = {"max_new_tokens": max_new_tokens, "do_sample": False}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
if constraint_fn is not None:
|
| 411 |
try:
|
| 412 |
out, input_len = _try_generate({**base_kwargs, "prefix_allowed_tokens_fn": constraint_fn})
|
|
|
|
| 421 |
"Be concise and structured -- do not repeat the full reasoning."
|
| 422 |
)
|
| 423 |
EXPLANATION_FALLBACK = "Answer derived from patterns found in the examples above."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
def extract_match_letter_options(context: str):
|
|
|
|
|
|
|
| 425 |
found = set()
|
| 426 |
for line in context.splitlines():
|
| 427 |
for m in re.finditer(r"(?:^|\s)([A-Z])[.\)]\s+\S", line):
|
|
|
|
| 436 |
return None
|
| 437 |
return letters
|
| 438 |
# =============================================================================
|
| 439 |
+
# MATCH_LETTERS: reason first (main pass), then a bijection-CONSTRAINED answer
|
| 440 |
+
# pass. Grammar allows only valid option letters + separators + EOS, and when
|
| 441 |
+
# #letters == #items forbids any letter already emitted, so the output cannot
|
| 442 |
+
# repeat or skip a letter. FAIL-OPEN: any failure or non-bijection problem
|
| 443 |
+
# falls straight back to the greedy answers, so this can only help.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
# =============================================================================
|
| 445 |
_LETTER_TOKEN_CACHE = {}
|
| 446 |
def _letter_token_map(tok, labels):
|
|
|
|
| 466 |
"No numbering, no words, no punctuation.")
|
| 467 |
user = (
|
| 468 |
f"Task:\n{query.strip()}\n\n"
|
| 469 |
+
f"Your reasoning:\n{reasoning_text[:1400]}\n\n"
|
| 470 |
f"Valid letters: {', '.join(labels)}. Give exactly {n_items} letters, one per "
|
| 471 |
f"line, in item order. Each item takes exactly one letter{uniq}."
|
| 472 |
)
|
| 473 |
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
| 474 |
def generate_constrained_letters(messages, labels, n_items, bijection, max_new_tokens=96):
|
|
|
|
|
|
|
| 475 |
labels = set(labels)
|
| 476 |
try:
|
| 477 |
try:
|
|
|
|
| 537 |
if idx < len(answers):
|
| 538 |
answers[idx] = val
|
| 539 |
if (marker_pos is None or not answers) and remaining > SETUP_BUFFER_S:
|
| 540 |
+
repair_text = generate(build_repair_messages(r["query"], n_items, text), 128)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
rep, rep_pos = extract(repair_text)
|
| 542 |
if rep:
|
| 543 |
answers, marker_pos = rep, rep_pos
|
|
|
|
| 546 |
answers = answers + [answers[-1] if answers else ""] * (n_items - len(answers))
|
| 547 |
elif len(answers) > n_items and marker_pos is None:
|
| 548 |
answers = answers[:n_items]
|
|
|
|
| 549 |
if not answers:
|
| 550 |
answers = [""]
|
| 551 |
+
# ---- SAFE REFINEMENT: match_letters bijection-constrained pass ----
|
|
|
|
| 552 |
if task_type == "match_letters" and n_items:
|
| 553 |
labels = extract_match_letter_options(r["context"])
|
| 554 |
+
if labels and len(labels) >= 2 and remaining > SETUP_BUFFER_S:
|
| 555 |
bijection = (len(labels) == n_items)
|
| 556 |
refined = generate_constrained_letters(
|
| 557 |
build_answer_only_messages(r["query"], text, labels, n_items, bijection),
|
|
|
|
| 559 |
)
|
| 560 |
if refined and len(refined) == n_items:
|
| 561 |
answers = refined
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
remaining_after = TIME_LIMIT_S - (time.time() - start_time)
|
| 563 |
budget_left_after = max(n_rows - len(rows) - 1, 0)
|
| 564 |
comfortable = remaining_after > (budget_left_after + 1) * per_row_budget * 1.3
|
|
|
|
| 592 |
if time.time() - start_time > TIME_LIMIT_S - 60:
|
| 593 |
print("Time budget nearly exhausted, stopping early.", flush=True)
|
| 594 |
break
|
|
|
|
| 595 |
for _, r in df.iterrows():
|
| 596 |
if r["id"] in processed_ids:
|
| 597 |
continue
|
|
|
|
| 605 |
write_submission_csv(rows)
|
| 606 |
print("DONE.", flush=True)
|
| 607 |
except Exception as e:
|
|
|
|
|
|
|
| 608 |
emergency_submission_csv(f"main loop failed: {e}", rows_so_far=rows if rows else None)
|
| 609 |
print(f"FATAL, but submission.csv was written with {len(rows)} rows. Error: {e}", flush=True)
|