File size: 5,476 Bytes
a1bdb40
379f378
 
 
fc85791
f96c703
 
 
42c1db7
 
f6c1260
f96c703
589b6aa
 
 
 
 
fc85791
09e9927
42c1db7
6737f54
09e9927
 
d4ab768
5a28824
09e9927
 
b32ea24
 
213ed63
 
 
 
a1bdb40
 
 
 
 
 
 
 
f96c703
 
 
 
 
e674d6d
 
fc85791
379f378
 
 
 
 
 
 
 
fc85791
379f378
 
 
 
 
 
 
 
 
 
 
 
e674d6d
 
 
 
 
fc85791
379f378
fc85791
 
 
e674d6d
 
fc85791
 
e674d6d
 
 
 
 
 
 
6737f54
 
 
 
 
 
 
213ed63
 
e674d6d
 
 
 
5a28824
b32ea24
a1bdb40
 
 
e674d6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379f378
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""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)