131 lines
5.3 KiB
Python
131 lines
5.3 KiB
Python
|
|
"""Entrypoint: read /tmp/data/test.csv, write submission.csv (id, pred, explanation)."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
# ===== CHANGE HERE — your model (must fit the T4's ~15 GB) =====
|
||
|
|
# "." for the real submission (this repo ships Qwen2.5-14B-Instruct-AWQ at the
|
||
|
|
# root); a Hub name (e.g. "Qwen/Qwen2.5-14B-Instruct-AWQ") while testing on
|
||
|
|
# Colab.
|
||
|
|
MODEL_ID = "."
|
||
|
|
# None => the pipeline picks per test size (deep mode 2048 for a small test set,
|
||
|
|
# coverage mode 1024 for a large one). Set an int to force it.
|
||
|
|
MAX_NEW_TOKENS = None
|
||
|
|
LLM_BATCH = 6 # puzzles gathered per checkpoint cycle (the client then
|
||
|
|
# sub-batches by token budget to fit the T4)
|
||
|
|
|
||
|
|
# Skip the LLM and emit the symbolic-only baseline. Diagnostic; leave False.
|
||
|
|
SYMBOLIC_ONLY = False
|
||
|
|
|
||
|
|
# LLM pass uses a minimal prompt: no scaffold injection, no chain-of-thought.
|
||
|
|
# Set IOL_LEAN=0 for the scaffolded path.
|
||
|
|
LEAN_MODE = os.environ.get("IOL_LEAN", "1") == "1"
|
||
|
|
|
||
|
|
# Answer match_letters via the free-form LLM pass. Set IOL_MATCH_ASSIGN=1 to use
|
||
|
|
# the assignment solver instead.
|
||
|
|
MATCH_ASSIGNMENT = os.environ.get("IOL_MATCH_ASSIGN", "0") == "1"
|
||
|
|
|
||
|
|
# Generation batch size. 1 = one prompt at a time, no padding. Larger batches
|
||
|
|
# are faster but pad to the longest prompt. Set IOL_GEN_BATCH to change.
|
||
|
|
GEN_BATCH_SIZE = int(os.environ.get("IOL_GEN_BATCH", "1"))
|
||
|
|
|
||
|
|
# Light greedy-anchored self-consistency: N sampled passes that can only
|
||
|
|
# displace the greedy answer on genuine agreement. Budget-gated. 0 disables.
|
||
|
|
VOTE_SAMPLES = int(os.environ.get("IOL_VOTE_SAMPLES", "2"))
|
||
|
|
VOTE_TEMP = float(os.environ.get("IOL_VOTE_TEMP", "0.5"))
|
||
|
|
|
||
|
|
# Optional segmentation hint in the prompt. Off by default. Set IOL_HINT=1.
|
||
|
|
HINT = os.environ.get("IOL_HINT", "0") == "1"
|
||
|
|
|
||
|
|
# The eval sandbox has no internet; only go offline when loading local
|
||
|
|
# weights so Colab testing with a Hub MODEL_ID still downloads normally.
|
||
|
|
if MODEL_ID == "." or Path(MODEL_ID).exists():
|
||
|
|
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
||
|
|
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
||
|
|
# reduce CUDA fragmentation on the T4 (must be set before torch initializes)
|
||
|
|
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
||
|
|
|
||
|
|
import csv
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
|
|
|
||
|
|
from solver.budget import Budget
|
||
|
|
from solver.llm import load_client
|
||
|
|
from solver.pipeline import run_pipeline
|
||
|
|
|
||
|
|
TEST_CSV = "/tmp/data/test.csv"
|
||
|
|
OUT_CSV = "submission.csv"
|
||
|
|
|
||
|
|
csv.field_size_limit(min(sys.maxsize, 2 ** 31 - 1))
|
||
|
|
|
||
|
|
|
||
|
|
def read_rows(path: str):
|
||
|
|
with open(path, newline="", encoding="utf-8") as f:
|
||
|
|
return [{k: (v or "") for k, v in row.items()} for row in csv.DictReader(f)]
|
||
|
|
|
||
|
|
|
||
|
|
def write_submission(results, out_path: str) -> None:
|
||
|
|
"""Atomic write (tmp + rename) so a crash mid-write never leaves a
|
||
|
|
truncated submission.csv."""
|
||
|
|
tmp = out_path + ".tmp"
|
||
|
|
with open(tmp, "w", newline="", encoding="utf-8") as f:
|
||
|
|
writer = csv.DictWriter(f, fieldnames=["id", "pred", "explanation"])
|
||
|
|
writer.writeheader()
|
||
|
|
for r in results:
|
||
|
|
writer.writerow({
|
||
|
|
"id": r.row_id,
|
||
|
|
"pred": json.dumps([str(a).strip() or "?" for a in r.answers],
|
||
|
|
ensure_ascii=False),
|
||
|
|
"explanation": r.explanation,
|
||
|
|
})
|
||
|
|
os.replace(tmp, out_path)
|
||
|
|
|
||
|
|
|
||
|
|
def main(test_path: str = TEST_CSV, out_path: str = OUT_CSV) -> None:
|
||
|
|
budget = Budget()
|
||
|
|
rows = read_rows(test_path)
|
||
|
|
try:
|
||
|
|
if SYMBOLIC_ONLY:
|
||
|
|
from solver.llm import NullClient
|
||
|
|
client = NullClient()
|
||
|
|
print("SYMBOLIC_ONLY: skipping the LLM; submitting the symbolic "
|
||
|
|
"baseline", flush=True)
|
||
|
|
else:
|
||
|
|
client = load_client(MODEL_ID)
|
||
|
|
if hasattr(client, "batch_size"):
|
||
|
|
client.batch_size = GEN_BATCH_SIZE
|
||
|
|
# checkpoint after the symbolic pass and every LLM batch: a crash at
|
||
|
|
# any later point still leaves a complete submission on disk
|
||
|
|
results = run_pipeline(rows, client, budget,
|
||
|
|
llm_batch=LLM_BATCH, max_new_tokens=MAX_NEW_TOKENS,
|
||
|
|
checkpoint=lambda rs: write_submission(rs, out_path),
|
||
|
|
lean=LEAN_MODE,
|
||
|
|
use_match_assignment=MATCH_ASSIGNMENT,
|
||
|
|
vote_samples=VOTE_SAMPLES, vote_temp=VOTE_TEMP,
|
||
|
|
hint=HINT)
|
||
|
|
write_submission(results, out_path)
|
||
|
|
print(f"wrote {out_path}: {len(results)} rows in {budget.elapsed():.1f}s",
|
||
|
|
flush=True)
|
||
|
|
except BaseException as e:
|
||
|
|
# last resort: if the pipeline itself died before the first
|
||
|
|
# checkpoint, emit query echoes — an empty pred is a zero row
|
||
|
|
if not Path(out_path).exists():
|
||
|
|
from solver.pipeline import PuzzleResult
|
||
|
|
fallback = [PuzzleResult(str(r.get("id", i)),
|
||
|
|
[str(r.get("query", "?")).strip() or "?"],
|
||
|
|
"- fallback")
|
||
|
|
for i, r in enumerate(rows)]
|
||
|
|
write_submission(fallback, out_path)
|
||
|
|
print(f"pipeline failed ({type(e).__name__}); wrote fallback "
|
||
|
|
f"{out_path}", flush=True)
|
||
|
|
raise
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
args = sys.argv[1:]
|
||
|
|
main(args[0] if args else TEST_CSV, args[1] if len(args) > 1 else OUT_CSV)
|