ShayanShamsi commited on
Commit
1ea85fc
·
verified ·
1 Parent(s): fe0fa24

Upload script.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. script.py +425 -0
script.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ IOL-AI 2026 submission: two-phase induction -> application prompting with
4
+ self-consistency majority voting.
5
+
6
+ Robust, transformers-only implementation (NO vLLM, NO runtime install of heavy
7
+ libs) — the eval sandbox ships transformers+torch+AWQ support (the organizers'
8
+ own Qwen2.5-14B-AWQ baseline runs on it), so we depend only on those.
9
+
10
+ Guaranteed-output design for the 30-min / 16 GB T4 cap:
11
+ Stage 0 : one fast greedy pass per problem -> write submission.csv immediately
12
+ (a valid, non-zero baseline that survives any later timeout/kill).
13
+ Stage 1 : for each problem, induce the language's rules N times, apply each to
14
+ the query items, majority-vote per item, and OVERWRITE that problem's
15
+ row. Written incrementally, so partial progress is never lost.
16
+
17
+ Model weights (Qwen3-14B-AWQ) are shipped in this repo and loaded from ".".
18
+ """
19
+
20
+ import os
21
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
22
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
23
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
24
+
25
+ import sys
26
+ import time
27
+ import json
28
+ import re
29
+ import csv
30
+ import unicodedata
31
+ from collections import Counter
32
+
33
+ START = time.time()
34
+
35
+ MODEL_ID = os.environ.get("IOL_MODEL_ID", ".")
36
+ TEST_CSV = os.environ.get("IOL_TEST_CSV", "/tmp/data/test.csv")
37
+ OUT_CSV = os.environ.get("IOL_OUT_CSV", "submission.csv")
38
+
39
+ # Time guards (competition hard cap is 30 min).
40
+ STAGE1_DEADLINE_S = float(os.environ.get("IOL_STAGE1_DEADLINE_S", 25 * 60))
41
+
42
+ N_SAMPLES = int(os.environ.get("IOL_N_SAMPLES", "4"))
43
+ IND_MAX_TOKENS = int(os.environ.get("IOL_IND_MAX_TOKENS", "1000"))
44
+ APP_MAX_TOKENS = int(os.environ.get("IOL_APP_MAX_TOKENS", "400"))
45
+ STAGE0_MAX_TOKENS = int(os.environ.get("IOL_STAGE0_MAX_TOKENS", "400"))
46
+ IND_TEMPERATURE = float(os.environ.get("IOL_IND_TEMPERATURE", "0.7"))
47
+ BATCH_SEQS = int(os.environ.get("IOL_BATCH_SEQS", "6")) # max sequences per generate()
48
+ ENABLE_THINKING = os.environ.get("IOL_ENABLE_THINKING", "0") == "1"
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Prompt wording (ported from the paper's chained_prompting_*.py)
53
+ # ---------------------------------------------------------------------------
54
+
55
+ _INDUCTION_TRANSLATION = (
56
+ "Below is a problem sheet from a linguistics exam. Your task is to determine as "
57
+ "much information about the language as possible, purely from the information "
58
+ "provided. You should systematically go through the information provided, and try "
59
+ "to determine the vocabulary meaning of each word (where possible), the syntactic "
60
+ "structure (such as word order), the morphology (including any verb conjugations), "
61
+ "and the meaning of any affixes or subwords. Test every piece of information you "
62
+ "determine against every example provided."
63
+ )
64
+ _INDUCTION_PATTERN = (
65
+ "Below is a problem sheet from a linguistics exam. Your task is to determine as "
66
+ "much information about the language as possible, purely from the information "
67
+ "provided. You should systematically go through the information provided, and try "
68
+ "to determine the morphological and phonological patterns of the language (such as "
69
+ "noun declension), including the meaning of any subwords or affixes you may see. "
70
+ "Look for systematic patterns in how the language forms syllables, words, and "
71
+ "phrases. Test every piece of information you determine against every example provided."
72
+ )
73
+ _INDUCTION_NUMBER = (
74
+ "Below is a problem sheet from a linguistics exam. Your task is to determine as "
75
+ "much information about the language and its number system as possible, purely from "
76
+ "the information provided. You should systematically go through the information "
77
+ "provided, and try to determine the vocabulary meaning of each number, the base of "
78
+ "the number system (e.g. decimal, hexadecimal), the syntactic structure (such as "
79
+ "word order), the morphology, and any other patterns you can see in the language's "
80
+ "number system. Test every piece of information you determine against every example "
81
+ "provided."
82
+ )
83
+ _INDUCTION_MATCHUP = (
84
+ "Below is a problem sheet from a linguistics exam. Your answers to the questions "
85
+ "should rely only on reasoning about the information provided in the sheet. Work out "
86
+ "the correspondences between the items and their meanings, and the vocabulary, "
87
+ "morphology and structure of the language. Test every correspondence you determine "
88
+ "against every example provided."
89
+ )
90
+ _APPLY_INTRO = {
91
+ "translation": "Based on the information about the language you have determined, solve the following puzzle:",
92
+ "fill_blanks": "Based on the patterns you have identified in the language, solve the following puzzle:",
93
+ "text_to_num": "Based on the information about the language you have determined, solve the following puzzle:",
94
+ "num_to_text": "Based on the information about the language you have determined, solve the following puzzle:",
95
+ "match_letters": "Based on the linguistic patterns and correspondences you have identified, answer the following question:",
96
+ }
97
+
98
+
99
+ def induction_text(task_type: str) -> str:
100
+ if task_type == "fill_blanks":
101
+ return _INDUCTION_PATTERN
102
+ if task_type in ("text_to_num", "num_to_text"):
103
+ return _INDUCTION_NUMBER
104
+ if task_type == "match_letters":
105
+ return _INDUCTION_MATCHUP
106
+ return _INDUCTION_TRANSLATION
107
+
108
+
109
+ _ITEM_RE = re.compile(r"(?m)^\s*([0-9]+[.)]|\([0-9]+\)|[0-9]+:)\s*")
110
+
111
+
112
+ def split_query(query: str):
113
+ query = (query or "").strip()
114
+ matches = list(_ITEM_RE.finditer(query))
115
+ if not matches:
116
+ return query, [query] if query else ["?"]
117
+ header = query[: matches[0].start()].strip()
118
+ items = []
119
+ for i, m in enumerate(matches):
120
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(query)
121
+ items.append(query[m.end():end].strip())
122
+ return header, items
123
+
124
+
125
+ def application_body(header, items, task_type):
126
+ intro = _APPLY_INTRO.get(task_type, _APPLY_INTRO["translation"])
127
+ lines = [intro]
128
+ if header:
129
+ lines.append(header)
130
+ for i, it in enumerate(items, 1):
131
+ lines.append(f"{i}. {it}")
132
+ n = len(items)
133
+ note = "For each numbered item give ONLY the letter of its correct match. " if task_type == "match_letters" else ""
134
+ keys = ", ".join(f'"{i}": ""' for i in range(1, n + 1))
135
+ lines.append(
136
+ f"\n{note}Answer every item. Give your answer STRICTLY as a single JSON object "
137
+ f"with one key per item number (as a string), plus a short \"explanation\" key "
138
+ f"summarising the rules you used (2-4 short points, no reasoning trace):\n"
139
+ f"{{{keys}, \"explanation\": \"\"}}"
140
+ )
141
+ return "\n".join(lines)
142
+
143
+
144
+ def stage0_prompt(context, header, items, task_type):
145
+ return f"{context.strip()}\n\n{application_body(header, items, task_type)}"
146
+
147
+
148
+ def induction_prompt(context, task_type):
149
+ return f"{induction_text(task_type)}\n\n{context.strip()}"
150
+
151
+
152
+ def application_prompt(context, task_type, rule, header, items):
153
+ return (
154
+ induction_prompt(context, task_type)
155
+ + "\n\n" + rule.strip()
156
+ + "\n\n" + application_body(header, items, task_type)
157
+ )
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Parsing + self-consistency vote
162
+ # ---------------------------------------------------------------------------
163
+
164
+ def normalize_answer(answer) -> str:
165
+ if not isinstance(answer, str):
166
+ answer = str(answer)
167
+ answer = unicodedata.normalize("NFC", answer)
168
+ return answer.strip().strip('"').strip("'").rstrip(".").strip().lower()
169
+
170
+
171
+ def _strip_think(text: str) -> str:
172
+ return re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
173
+
174
+
175
+ def extract_json(text: str):
176
+ text = _strip_think(text)
177
+ cands = re.findall(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
178
+ cands += re.findall(r"\{(?:[^{}]|\{[^{}]*\})*\}", text, re.DOTALL)
179
+ for c in reversed(cands):
180
+ try:
181
+ o = json.loads(c)
182
+ if isinstance(o, dict):
183
+ return o
184
+ except Exception:
185
+ continue
186
+ return {}
187
+
188
+
189
+ def parse_items(text: str, n_items: int):
190
+ obj = extract_json(text)
191
+ explanation = ""
192
+ answers = [""] * n_items
193
+ if obj:
194
+ explanation = str(obj.get("explanation", "") or "")
195
+ for i in range(1, n_items + 1):
196
+ for key in (str(i), i):
197
+ if key in obj and str(obj[key]).strip():
198
+ answers[i - 1] = str(obj[key]).strip()
199
+ break
200
+ if not any(answers):
201
+ lines = [ln.strip() for ln in _strip_think(text).splitlines() if ln.strip()]
202
+ lines = [ln for ln in lines if not ln.lower().startswith(("here", "based on", "```"))]
203
+ for i in range(min(n_items, len(lines))):
204
+ answers[i] = re.sub(r"^\s*[0-9]+[.):]\s*", "", lines[i])
205
+ return answers, explanation
206
+
207
+
208
+ def _chrf(a: str, b: str, n: int = 3) -> float:
209
+ a, b = a.lower(), b.lower()
210
+ if not a and not b:
211
+ return 1.0
212
+ if not a or not b:
213
+ return 0.0
214
+ total = 0.0
215
+ for k in range(1, n + 1):
216
+ ag = Counter(a[i:i + k] for i in range(len(a) - k + 1))
217
+ bg = Counter(b[i:i + k] for i in range(len(b) - k + 1))
218
+ if not ag or not bg:
219
+ continue
220
+ inter = sum((ag & bg).values())
221
+ p = inter / max(sum(ag.values()), 1)
222
+ r = inter / max(sum(bg.values()), 1)
223
+ total += 0.0 if (p + r) == 0 else 2 * p * r / (p + r)
224
+ return total / n
225
+
226
+
227
+ def majority_vote(candidates):
228
+ valid = [c for c in candidates if isinstance(c, str) and c.strip()]
229
+ if not valid:
230
+ return ""
231
+ groups = {}
232
+ for c in valid:
233
+ groups.setdefault(normalize_answer(c), []).append(c)
234
+ counts = {k: len(v) for k, v in groups.items()}
235
+ top = max(counts.values())
236
+ winners = [k for k, n in counts.items() if n == top]
237
+ if len(winners) == 1:
238
+ return Counter(groups[winners[0]]).most_common(1)[0][0]
239
+ best, best_score = valid[0], -1.0
240
+ for c in valid:
241
+ s = sum(_chrf(c, o) for o in valid if o is not c)
242
+ if s > best_score:
243
+ best, best_score = c, s
244
+ return best
245
+
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # Model + batched generation
249
+ # ---------------------------------------------------------------------------
250
+
251
+ class Model:
252
+ def __init__(self):
253
+ import torch
254
+ from transformers import AutoTokenizer, AutoModelForCausalLM
255
+ self.torch = torch
256
+ self.tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
257
+ if self.tok.pad_token_id is None:
258
+ self.tok.pad_token = self.tok.eos_token
259
+ self.tok.padding_side = "left"
260
+ # Load with .to("cuda") (no `accelerate`/`device_map` dependency) so we
261
+ # rely only on the base transformers+torch stack the sandbox guarantees.
262
+ self.model = AutoModelForCausalLM.from_pretrained(
263
+ MODEL_ID, dtype=torch.float16, trust_remote_code=True,
264
+ ).eval()
265
+ self.model.to("cuda" if torch.cuda.is_available() else "cpu")
266
+
267
+ def _render(self, prompt):
268
+ kwargs = {}
269
+ try:
270
+ return self.tok.apply_chat_template(
271
+ [{"role": "user", "content": prompt}],
272
+ tokenize=False, add_generation_prompt=True,
273
+ enable_thinking=ENABLE_THINKING,
274
+ )
275
+ except TypeError:
276
+ return self.tok.apply_chat_template(
277
+ [{"role": "user", "content": prompt}],
278
+ tokenize=False, add_generation_prompt=True,
279
+ )
280
+
281
+ def generate(self, prompts, max_new_tokens, do_sample=False, temperature=1.0, n=1):
282
+ """Return list (len=len(prompts)) of lists (len=n) of decoded strings."""
283
+ torch = self.torch
284
+ results = [[] for _ in prompts]
285
+ # Expand: each prompt contributes n sequences; chunk so chunk<=BATCH_SEQS.
286
+ chunk = max(1, BATCH_SEQS // max(n, 1))
287
+ for s in range(0, len(prompts), chunk):
288
+ idxs = list(range(s, min(s + chunk, len(prompts))))
289
+ texts = [self._render(prompts[i]) for i in idxs]
290
+ enc = self.tok(texts, return_tensors="pt", padding=True, truncation=True,
291
+ max_length=7000).to(self.model.device)
292
+ gen_kwargs = dict(
293
+ max_new_tokens=max_new_tokens,
294
+ num_return_sequences=n,
295
+ pad_token_id=self.tok.pad_token_id,
296
+ )
297
+ if do_sample:
298
+ gen_kwargs.update(do_sample=True, temperature=temperature, top_p=0.9)
299
+ else:
300
+ gen_kwargs.update(do_sample=False)
301
+ with torch.no_grad():
302
+ out = self.model.generate(**enc, **gen_kwargs)
303
+ gen = out[:, enc["input_ids"].shape[1]:]
304
+ dec = self.tok.batch_decode(gen, skip_special_tokens=True)
305
+ # dec is len(idxs)*n, grouped per input.
306
+ for bi, i in enumerate(idxs):
307
+ results[i] = dec[bi * n:(bi + 1) * n]
308
+ return results
309
+
310
+
311
+ # ---------------------------------------------------------------------------
312
+ # Pipeline
313
+ # ---------------------------------------------------------------------------
314
+
315
+ def load_problems():
316
+ rows = []
317
+ with open(TEST_CSV, newline="", encoding="utf-8") as f:
318
+ for r in csv.DictReader(f):
319
+ header, items = split_query(r.get("query", ""))
320
+ rows.append({
321
+ "id": r["id"],
322
+ "task_type": (r.get("task_type") or "").strip(),
323
+ "context": r.get("context", ""),
324
+ "header": header,
325
+ "items": items,
326
+ })
327
+ return rows
328
+
329
+
330
+ def write_out(results, order):
331
+ with open(OUT_CSV, "w", newline="", encoding="utf-8") as f:
332
+ w = csv.DictWriter(f, fieldnames=["id", "pred", "explanation"])
333
+ w.writeheader()
334
+ for pid in order:
335
+ r = results[pid]
336
+ w.writerow({
337
+ "id": pid,
338
+ "pred": json.dumps(r["pred"], ensure_ascii=False),
339
+ "explanation": r.get("explanation", "") or "",
340
+ })
341
+
342
+
343
+ def run():
344
+ probs = load_problems()
345
+ order = [p["id"] for p in probs]
346
+ print(f"[info] {len(probs)} problems", flush=True)
347
+
348
+ # Initialise so a valid file exists even if load crashes mid-way.
349
+ results = {p["id"]: {"pred": [""] * len(p["items"]), "explanation": ""} for p in probs}
350
+ write_out(results, order)
351
+
352
+ m = Model()
353
+ print(f"[info] model loaded at {time.time()-START:.0f}s", flush=True)
354
+
355
+ # ---- Stage 0: fast greedy baseline for every problem ----
356
+ s0_prompts = [stage0_prompt(p["context"], p["header"], p["items"], p["task_type"]) for p in probs]
357
+ s0 = m.generate(s0_prompts, STAGE0_MAX_TOKENS, do_sample=False, n=1)
358
+ for p, outs in zip(probs, s0):
359
+ ans, expl = parse_items(outs[0], len(p["items"]))
360
+ # never leave blank -> keep best-effort guesses
361
+ results[p["id"]] = {"pred": ans, "explanation": expl}
362
+ write_out(results, order)
363
+ print(f"[info] stage 0 done at {time.time()-START:.0f}s", flush=True)
364
+
365
+ # ---- Stage 1: two-phase induction->application + self-consistency ----
366
+ for p in probs:
367
+ if time.time() - START > STAGE1_DEADLINE_S:
368
+ print("[warn] stage1 deadline reached; keeping stage0 for the rest", flush=True)
369
+ break
370
+ try:
371
+ ind = m.generate([induction_prompt(p["context"], p["task_type"])],
372
+ IND_MAX_TOKENS, do_sample=True,
373
+ temperature=IND_TEMPERATURE, n=N_SAMPLES)[0]
374
+ app_prompts = [
375
+ application_prompt(p["context"], p["task_type"], _strip_think(rule),
376
+ p["header"], p["items"])
377
+ for rule in ind
378
+ ]
379
+ app = m.generate(app_prompts, APP_MAX_TOKENS, do_sample=False, n=1)
380
+ n_items = len(p["items"])
381
+ samples, expl = [], results[p["id"]].get("explanation", "")
382
+ for outs in app:
383
+ a, e = parse_items(outs[0], n_items)
384
+ samples.append(a)
385
+ if e and not expl:
386
+ expl = e
387
+ final = [majority_vote([s[j] for s in samples if j < len(s) and s[j].strip()])
388
+ for j in range(n_items)]
389
+ # keep any stage0 answer if voting produced an empty for that item
390
+ s0_ans = results[p["id"]]["pred"]
391
+ final = [f if f.strip() else (s0_ans[j] if j < len(s0_ans) else "")
392
+ for j, f in enumerate(final)]
393
+ if len(expl) > 600:
394
+ expl = expl[:600].rsplit(" ", 1)[0] + "..."
395
+ results[p["id"]] = {"pred": final, "explanation": expl}
396
+ write_out(results, order)
397
+ except Exception as e:
398
+ print(f"[warn] stage1 failed for {p['id']}: {e}", flush=True)
399
+ continue
400
+ write_out(results, order)
401
+ print(f"[info] done at {time.time()-START:.0f}s", flush=True)
402
+
403
+
404
+ if __name__ == "__main__":
405
+ try:
406
+ run()
407
+ except Exception:
408
+ import traceback
409
+ traceback.print_exc()
410
+ # Ensure a well-formed file exists no matter what.
411
+ try:
412
+ with open(OUT_CSV) as f:
413
+ pass
414
+ except Exception:
415
+ try:
416
+ probs = load_problems()
417
+ with open(OUT_CSV, "w", newline="", encoding="utf-8") as f:
418
+ w = csv.DictWriter(f, fieldnames=["id", "pred", "explanation"])
419
+ w.writeheader()
420
+ for p in probs:
421
+ w.writerow({"id": p["id"],
422
+ "pred": json.dumps([""] * len(p["items"]), ensure_ascii=False),
423
+ "explanation": ""})
424
+ except Exception:
425
+ traceback.print_exc()