File size: 7,417 Bytes
9c71982
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e6a31d
 
9c71982
 
 
 
 
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
import os
os.environ["HF_HUB_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"

import csv
import json
import re
import time
import warnings
import pandas as pd
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

warnings.filterwarnings("ignore")

MODEL_PATH = "."
TIME_LIMIT_SECONDS = 25 * 60  # Hard stop with 5 min buffer

# NF4 quantization for T4 16 GB
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
)

print("Loading tokenizer...", flush=True)
tok = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
if tok.pad_token is None:
    tok.pad_token = tok.eos_token

print("Loading model...", flush=True)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_PATH,
    quantization_config=bnb_config,
    device_map="auto",
    torch_dtype=torch.float16,
    trust_remote_code=True,
).eval()
print("Model ready.", flush=True)

SYSTEM_PROMPT = (
    "You are an expert computational linguist competing in the International Linguistics Olympiad (IOL). "
    "You solve linguistic puzzles by analyzing patterns in unfamiliar languages.\n\n"
    "REASONING FORMAT:\n"
    "First, provide your step-by-step linguistic analysis inside <think>...</think> tags. "
    "Structure this analysis clearly with bullet points or short paragraphs covering:\n"
    "- Key patterns observed (morphology, syntax, phonology, number system)\n"
    "- Rules inferred from the context examples\n"
    "- How you apply those rules to each query item\n"
    "Keep the analysis concise and readable by a human jury in 2-3 minutes.\n\n"
    "ANSWER FORMAT:\n"
    "After </think>, provide EXACTLY one answer per line, with NO numbering, NO bullet points, "
    "NO labels, and NO extra commentary. The number of answer lines must exactly match the number "
    "of numbered items in the query, in the same order.\n\n"
    "TASK RULES:\n"
    "- translation: output the full translated text\n"
    "- match_letters: output ONLY the matching capital letter (e.g., A, B, C)\n"
    "- fill_blanks: output ONLY the missing linguistic form\n"
    "- text_to_num: output digits only (e.g., 42, 111)\n"
    "- num_to_text: output the number written in the task language\n\n"
    "CRITICAL: Never skip an item. Always provide your best guess."
)

def count_query_items(query_text):
    """Count how many numbered items the query contains."""
    # Matches: 1. ..., 1) ..., (1) ..., etc., at line start
    pattern = r'(?:^|\n)\s*[(]?\s*\d+\s*[.)]\s+'
    matches = re.findall(pattern, '\n' + query_text)
    if matches:
        return len(matches)
    # Fallback: non-empty lines minus a likely header line
    lines = [l for l in query_text.split('\n') if l.strip()]
    return max(1, len(lines) - 1) if len(lines) > 1 else 1

def extract_answers(text, expected_count):
    """Parse final answers from generated text (think blocks removed)."""
    cleaned_text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
    if not cleaned_text:
        cleaned_text = text.strip()

    lines = [ln.strip() for ln in cleaned_text.split('\n') if ln.strip()]
    answers = []
    for line in lines:
        # Strip leading numbering and common prefixes
        line = re.sub(r'^\s*\d+[\.)]\s+', '', line)
        line = re.sub(r'^(?:Answer|ANSWER)\s*[:.\-]?\s*', '', line, flags=re.IGNORECASE)
        line = line.strip().strip('"').strip("'").strip('`')
        if line:
            answers.append(line)

    # Fallback: comma / semicolon separated values
    if len(answers) != expected_count and expected_count > 1:
        combined = ' '.join(lines)
        for sep in [';', ',']:
            if sep in combined:
                parts = [p.strip().strip('"').strip("'").strip('`') for p in combined.split(sep) if p.strip()]
                if len(parts) == expected_count:
                    answers = parts
                    break

    # Guarantee exact expected count
    if len(answers) < expected_count:
        while len(answers) < expected_count:
            answers.append(answers[-1] if answers else "")
    elif len(answers) > expected_count:
        answers = answers[:expected_count]

    return answers

def extract_explanation(text):
    """Pull reasoning from <think> block for the human-eval column."""
    m = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
    if m:
        exp = m.group(1).strip()
        exp = re.sub(r'\n{3,}', '\n\n', exp)
        if len(exp) > 2000:
            exp = exp[:2000] + "..."
        return exp
    return ""

# ------------------------------------------------------------------
# Main inference loop
# ------------------------------------------------------------------
df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
expected_counts = [count_query_items(row["query"]) for _, row in df.iterrows()]

results = []
start_time = time.time()

for idx, row in df.iterrows():
    elapsed = time.time() - start_time
    if elapsed > TIME_LIMIT_SECONDS:
        print(f"Time buffer reached. Flushing remaining {len(df) - idx} rows as blanks.", flush=True)
        for j in range(idx, len(df)):
            results.append({
                "id": df.iloc[j]["id"],
                "pred": json.dumps([""] * expected_counts[j], ensure_ascii=False),
                "explanation": ""
            })
        break

    context = row["context"].strip()
    query = row["query"].strip()
    task_type = row.get("task_type", "")
    expected = expected_counts[idx]

    user_msg = (
        f"TASK TYPE: {task_type}\n\n"
        f"CONTEXT:\n{context}\n\n"
        f"QUERY:\n{query}\n\n"
        f"Provide your analysis in <think>...</think> tags, then list exactly {expected} answer(s) "
        f"below it, one per line, with no numbering and no extra text."
    )

    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_msg},
    ]

    try:
        inputs = tok.apply_chat_template(
            messages,
            add_generation_prompt=True,
            return_tensors="pt",
        ).to("cuda")

        with torch.no_grad():
            outputs = model.generate(
                inputs,
                max_new_tokens=2048,
                do_sample=False,
                pad_token_id=tok.pad_token_id,
            )

        gen_text = tok.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True)
        answers = extract_answers(gen_text, expected)
        explanation = extract_explanation(gen_text)

    except Exception as e:
        print(f"ERROR on {row['id']}: {e}", flush=True)
        answers = [""] * expected
        explanation = ""

    results.append({
        "id": row["id"],
        "pred": json.dumps(answers, ensure_ascii=False),
        "explanation": explanation,
    })

    print(f"[{idx+1}/{len(df)}] {row['id']} | items={expected} | answers={answers}", flush=True)

# ------------------------------------------------------------------
# Write submission
# ------------------------------------------------------------------
with open("submission.csv", "w", newline="", encoding="utf-8") as file_handle:
    writer = csv.DictWriter(file_handle, fieldnames=["id", "pred", "explanation"])
    writer.writeheader()
    for r in results:
        writer.writerow(r)

print("Wrote submission.csv", flush=True)