File size: 7,566 Bytes
4e340e1
a21d85a
 
 
 
 
 
 
 
 
 
 
3ac6373
a21d85a
 
 
 
 
 
 
 
71a469e
a21d85a
4e340e1
a21d85a
 
 
 
 
 
 
 
71a469e
a21d85a
 
 
 
 
 
4e340e1
a21d85a
 
 
 
 
 
 
 
 
 
 
19274b6
3ac6373
 
 
318a5a1
4e340e1
 
 
 
 
 
 
 
3ac6373
 
 
bd99d32
 
 
 
 
 
a280054
a21d85a
71a469e
3ac6373
 
71a469e
 
 
3ac6373
 
71a469e
3ac6373
 
71a469e
 
 
3ac6373
 
 
a21d85a
 
3ac6373
 
 
bd99d32
3ac6373
 
 
 
 
71a469e
3ac6373
 
71a469e
3ac6373
 
 
 
 
 
 
 
 
 
bd99d32
a21d85a
 
4e340e1
 
 
 
 
 
 
 
71a469e
 
 
 
4e340e1
 
 
 
 
 
 
3ac6373
 
 
4e340e1
71a469e
bd99d32
4e340e1
 
bd99d32
71a469e
 
4e340e1
71a469e
 
19274b6
71a469e
 
 
3ac6373
4e340e1
 
 
 
 
 
 
 
 
 
 
 
 
19274b6
4e340e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd99d32
 
a21d85a
71a469e
a21d85a
 
4e340e1
a21d85a
 
 
 
 
 
 
 
71a469e
 
 
a21d85a
 
4e340e1
a21d85a
 
3ac6373
 
 
 
 
 
 
 
 
4e340e1
bd99d32
71a469e
 
 
a21d85a
 
 
 
a280054
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237

import os
os.environ["HF_HUB_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"

import re
import json
import time
import sys
import subprocess

try:
    import bitsandbytes  # noqa: F401
except ImportError:
    subprocess.run([sys.executable, "-m", "pip", "install", "-q", "bitsandbytes"], check=True)
    import bitsandbytes

import torch
import pandas as pd
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

# Use the local model directory (uploaded to HF repo root)
MODEL_ID = "."
TIME_LIMIT_S = 28 * 60
START = time.time()


def time_left():
    return TIME_LIMIT_S - (time.time() - START)


def load_model():
    """Load model with 4-bit quantization"""
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.float16,
        bnb_4bit_use_double_quant=True,
    )
    
    tok = AutoTokenizer.from_pretrained(MODEL_ID, local_files_only=True)
    model = AutoModelForCausalLM.from_pretrained(
        MODEL_ID,
        quantization_config=bnb_config,
        device_map="auto",
        torch_dtype=torch.float16,
        local_files_only=True,
    ).eval()
    return tok, model


def generate(messages, max_new_tokens=3500):
    ids = tok.apply_chat_template(
        messages, add_generation_prompt=True, return_tensors="pt"
    ).to(model.device)
    with torch.no_grad():
        out = model.generate(
            ids,
            max_new_tokens=max_new_tokens,
            do_sample=False,
            temperature=None,
            top_p=None,
            pad_token_id=tok.eos_token_id,
        )
    return tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True).strip()


def count_items(query: str) -> int:
    nums = re.findall(r"(?m)^\s*(\d+)\s*[.)]\s", query)
    if nums:
        return max(int(n) for n in nums)
    lines = [l for l in query.splitlines() if l.strip()]
    return max(1, len(lines))


def extract_json_list(text: str):
    text = text.strip()
    try:
        val = json.loads(text)
        if isinstance(val, list):
            return [str(x) for x in val]
    except Exception:
        pass
    m = re.search(r"\[.*\]", text, re.DOTALL)
    if m:
        try:
            val = json.loads(m.group(0))
            if isinstance(val, list):
                return [str(x) for x in val]
        except Exception:
            pass
    return None


def extract_answers_fallback(text: str, n: int):
    lines = [l.strip() for l in text.splitlines() if l.strip()]
    cleaned = []
    for l in lines:
        l = re.sub(r"^\s*\d+[.)]\s*", "", l)
        l = l.strip(" -\t")
        if l:
            cleaned.append(l)
    if len(cleaned) >= n:
        return cleaned[:n]
    parts = [p.strip() for p in text.replace("\n", ",").split(",") if p.strip()]
    if len(parts) >= n:
        return parts[:n]
    cleaned = cleaned or parts or [text.strip()]
    while len(cleaned) < n:
        cleaned.append(cleaned[-1] if cleaned else "")
    return cleaned[:n]


def fit_answers(answers, n):
    answers = list(answers)
    if len(answers) < n:
        answers = answers + [answers[-1] if answers else ""] * (n - len(answers))
    return answers[:n]


REASONING_SYSTEM = (
    "You are an expert at International Linguistics Olympiad (IOL) problems. "
    "You will be given a self-contained puzzle about an unfamiliar language: some example "
    "data (context) and a query asking you to translate, match, fill in blanks, or convert "
    "numbers. Work out the underlying grammar/vocabulary rules carefully and systematically "
    "from the given examples only. Show your step-by-step reasoning: identify morphemes, "
    "patterns, correspondences, and test your hypothesis against every example before "
    "answering the query."
)

EXTRACT_SYSTEM = (
    "You convert a linguistics-puzzle solution into a strict output format. "
    "Given the original problem and a reasoning trace, output ONLY a single JSON object "
    "with exactly two keys:\n"
    '  "answers": a JSON list of strings, one per numbered item in the query, in order.\n'
    '  "explanation": a short (3-6 bullet points, plain text with \n between bullets) '
    "human-readable summary of the key rules used to derive the answers. This is NOT the "
    "full reasoning trace, just a concise justification a person can read in under a minute.\n"
    "Output nothing except that JSON object."
)


def solve_row(context: str, query: str, n_items: int):
    problem_text = f"{context.strip()}\n\n{query.strip()}"

    reasoning = ""
    if time_left() > 60:
        try:
            reasoning = generate(
                [
                    {"role": "system", "content": REASONING_SYSTEM},
                    {"role": "user", "content": problem_text},
                ],
                max_new_tokens=3500,
            )
        except Exception as e:
            reasoning = f"(reasoning pass failed: {e})"

    answers, explanation = None, ""
    if time_left() > 30:
        extract_prompt = (
            f"PROBLEM:\n{problem_text}\n\n"
            f"REASONING TRACE:\n{reasoning}\n\n"
            f"The query has {n_items} numbered item(s). Now output the JSON object as instructed."
        )
        try:
            raw = generate(
                [
                    {"role": "system", "content": EXTRACT_SYSTEM},
                    {"role": "user", "content": extract_prompt},
                ],
                max_new_tokens=800,
            )
            try:
                obj = json.loads(raw.strip())
            except Exception:
                m = re.search(r"\{.*\}", raw, re.DOTALL)
                obj = json.loads(m.group(0)) if m else None
            if obj and isinstance(obj, dict):
                a = obj.get("answers")
                if isinstance(a, list):
                    answers = [str(x) for x in a]
                explanation = str(obj.get("explanation", "")).strip()
        except Exception:
            pass

    if answers is None:
        candidate = extract_json_list(reasoning)
        answers = candidate if candidate is not None else extract_answers_fallback(reasoning, n_items)
    if not explanation:
        snippet = reasoning.strip().replace("\n", " ")
        explanation = (snippet[:400] + "...") if len(snippet) > 400 else snippet

    answers = fit_answers(answers, n_items)
    return answers, explanation


def main():
    # Load model once
    global tok, model
    tok, model = load_model()
    
    df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
    out_rows = []

    for i, r in df.iterrows():
        rid = r["id"]
        context, query = r.get("context", ""), r.get("query", "")
        n_items = count_items(query)

        if time_left() < 45:
            answers = [""] * n_items
            explanation = ""
        else:
            try:
                answers, explanation = solve_row(context, query, n_items)
            except Exception as e:
                answers = [""] * n_items
                explanation = f"(error: {e})"

        out_rows.append(
            {
                "id": rid,
                "pred": json.dumps(answers, ensure_ascii=False),
                "explanation": explanation,
            }
        )
        print(f"[{i + 1}/{len(df)}] id={rid} n_items={n_items} time_left={time_left():.0f}s", flush=True)

    pd.DataFrame(out_rows, columns=["id", "pred", "explanation"]).to_csv(
        "submission.csv", index=False
    )
    print("wrote submission.csv", flush=True)


if __name__ == "__main__":
    main()