| import os |
| import re |
| import json |
| import time |
| import unicodedata |
| from collections import Counter, defaultdict |
|
|
| T0 = time.time() |
| L1 = float(os.environ.get("IOL_TIME_LIMIT", "1800")) |
| S1 = float(os.environ.get("IOL_SAFETY", "150")) |
| D1 = T0 + L1 - S1 |
|
|
| P1 = os.environ.get("IOL_TEST_CSV", "/tmp/data/test.csv") |
| P2 = os.environ.get("IOL_OUT_CSV", "submission.csv") |
| M1 = os.environ.get("IOL_MODEL", ".") |
| E1 = os.environ.get("IOL_EXPLAIN", "1") == "1" |
|
|
| X1 = int(os.environ.get("IOL_MAXNEW", "512")) |
| X2 = int(os.environ.get("IOL_MAXSAMPLES", "24")) |
| X3 = float(os.environ.get("IOL_TEMP", "0.5")) |
| X4 = int(os.environ.get("IOL_BATCH", "4")) |
|
|
| os.environ.setdefault("HF_HUB_OFFLINE", "1") |
| os.environ.setdefault("TRANSFORMERS_OFFLINE", "1") |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") |
|
|
| def lg(msg): |
| print(f"[{time.time() - T0:7.1f}s] {msg}", flush=True) |
|
|
| def lf(): |
| return D1 - time.time() |
|
|
| _LN = re.compile(r"^[ \t]*(\d{1,3})[.)\]]", re.M) |
| _PN = re.compile(r"\((\d{1,3})\)") |
| _RG = re.compile(r"\(?(\d{1,3})\s*(?:[-–—]|to)\s*(\d{1,3})\)?") |
| _LL = re.compile(r"^[ \t]*([A-Z])[.)\]]\s", re.M) |
| _SP = re.compile(r"^\s*(?:\(?\d{1,3}\)?[.):\]]\s*|[-*•]\s+)") |
| _FC = re.compile(r"^```[a-zA-Z]*\s*$") |
| _CT = re.compile( |
| r"^\s*(?:here (?:are|is)\b|answers?\s*:?\s*$|explanation\b|note\b|okay\b|" |
| r"solution\b|reasoning\b|analysis\b|translations?\s*:?\s*$|the answers?\b|" |
| r"let me\b|first,|so,|therefore\b|thus\b)", re.I) |
|
|
| def d1(q, t="", c=""): |
| q = q or "" |
| ln = [int(m) for m in _LN.findall(q)] |
| pn = [int(m) for m in _PN.findall(q)] |
| rn = 0 |
| for a, b in _RG.findall(q): |
| a, b = int(a), int(b) |
| if 0 < b - a < 60: rn = max(rn, b - a + 1) |
| cand = max(len(set(ln)), len(set(pn))) |
| if rn and cand and rn != cand: return cand |
| cand = max(cand, len(set(_LL.findall(q)))) |
| n = max(rn, cand) |
| if n > 1: return n |
| lns = [l.strip() for l in q.splitlines() if l.strip()] |
| if len(lns) > 1: |
| h = lns[0] |
| b = lns[1:] if h.endswith((":", ".")) else lns |
| if b: return len(b) |
| if c: |
| cn = len(set(int(m) for m in _LN.findall(c))) |
| if cn > 1: return cn |
| cl = len(set(_LL.findall(c))) |
| if cl > 1: return cl |
| return max(n, 1) |
|
|
| def d2(q, n, t=""): |
| if t.strip().lower() == "match_letters": return ["A"] * n |
| q = q or "" |
| out = [] |
| for ln in q.splitlines(): |
| s = ln.strip() |
| if not s: continue |
| m = re.match(r"^\(?(\d{1,3})\)?[.):\]]\s*(.+)$", s) |
| if m: out.append(m.group(2).strip()) |
| if not out: |
| lns = [l.strip() for l in q.splitlines() if l.strip()] |
| if len(lns) > 1 and lns[0].endswith((":", ".")): out = lns[1:] |
| out = [o.split("|")[0].strip() if "|" in o else o for o in out] |
| out = [o for o in out if o] |
| while len(out) < n: out.append(out[-1] if out else "?") |
| return out[:n] |
|
|
| def d3(s): |
| s = s.strip() |
| s = _SP.sub("", s) |
| s = s.strip().strip("`").strip() |
| if len(s) >= 2 and s[0] == s[-1] and s[0] in "\"'“”": s = s[1:-1].strip() |
| return s.strip() |
|
|
| def d4(items, n, fb=None): |
| items = [i for i in items if i and i.strip()] |
| if len(items) > n: items = items[-n:] |
| while len(items) < n: |
| if fb and len(items) < len(fb): items.append(fb[len(items)]) |
| else: items.append(items[-1] if items else "?") |
| return items[:n] |
|
|
| def d5(text, n, fb=None): |
| if not text: return list(fb[:n]) if fb else ["?"] * n |
| m = None |
| for m2 in re.finditer(r"(?:^|\n)\s*(?:final\s+)?answers?\s*:\s*\n?", text, re.I): m = m2 |
| body = text[m.end():] if m else text |
| numbered, raw = [], [] |
| for ln in body.splitlines(): |
| if _FC.match(ln): continue |
| mm = re.match(r"^\s*\(?(\d{1,3})\)?[.):\]]\s*(.+)$", ln.strip()) |
| if mm: |
| val = d3(mm.group(2)) |
| if val and not _CT.match(val): numbered.append((int(mm.group(1)), val)) |
| c = d3(ln) |
| if c and not _CT.match(c): raw.append(c) |
| if len(numbered) >= n: |
| by_label = {} |
| for lab, val in numbered: by_label[lab] = val |
| labs = sorted(by_label) |
| if len(labs) >= n: return [by_label[l] for l in labs[:n]] |
| return d4(raw, n, fb) |
|
|
| def d6(s): |
| s = unicodedata.normalize("NFC", (s or "").strip().lower()) |
| s = _SP.sub("", s) |
| s = re.sub(r"\s+", " ", s) |
| return s.strip(" .!?;:,") |
|
|
| def d7(cands, anchor=None): |
| cands = [c for c in cands if c and c.strip()] |
| if anchor is None: anchor = cands[0] if cands else "?" |
| if len(cands) < 3: return anchor |
| groups = defaultdict(list) |
| for c in cands: groups[d6(c)].append(c) |
| anchor_support = len(groups.get(d6(anchor), [])) |
| best_key, best_n = None, 0 |
| for k, v in groups.items(): |
| if len(v) > best_n: best_key, best_n = k, len(v) |
| if best_key is not None and best_n >= 3 and best_n > anchor_support: |
| return Counter(groups[best_key]).most_common(1)[0][0] |
| return anchor |
|
|
| def d8(path, ids, preds, explanations=None): |
| import pandas as pd |
| rows = [] |
| for i in ids: |
| rec = {"id": i, "pred": json.dumps(preds[i], ensure_ascii=False)} |
| if explanations is not None: rec["explanation"] = explanations.get(i, "") |
| rows.append(rec) |
| pd.DataFrame(rows).to_csv(path, index=False) |
|
|
| def main(): |
| import pandas as pd |
| import torch |
| from transformers import AutoTokenizer, AutoModelForCausalLM, StoppingCriteria, StoppingCriteriaList |
|
|
| torch.backends.cuda.matmul.allow_tf32 = True |
| torch.backends.cudnn.allow_tf32 = True |
|
|
| df = pd.read_csv(P1, dtype=str).fillna("") |
| I1 = [str(x) for x in df["id"].tolist()] |
| N1 = [d1(r.get("query", ""), r.get("task_type", ""), r.get("context", "")) for _, r in df.iterrows()] |
| total_items = sum(N1) |
| lg(f"loaded {len(df)} problems, {total_items} items") |
|
|
| S2 = {i: d2(r.get("query", ""), n, r.get("task_type", "")) for i, (_, r), n in zip(I1, df.iterrows(), N1)} |
|
|
| R1 = {i: list(S2[i]) for i in I1} |
| E2 = {i: "" for i in I1} if E1 else None |
| d8(P2, I1, R1, E2) |
| lg(f"wrote placeholder {P2} ({len(I1)} rows)") |
|
|
| lg("loading tokenizer/model ...") |
| tk = AutoTokenizer.from_pretrained(M1, trust_remote_code=True) |
| if tk.pad_token is None: tk.pad_token = tk.eos_token |
| tk.padding_side = "left" |
|
|
| def _ld(dm): |
| try: |
| return AutoModelForCausalLM.from_pretrained(M1, torch_dtype=torch.float16, device_map=dm, trust_remote_code=True).eval() |
| except TypeError: |
| return AutoModelForCausalLM.from_pretrained(M1, dtype=torch.float16, device_map=dm, trust_remote_code=True).eval() |
|
|
| try: |
| ml = _ld({"": 0} if torch.cuda.is_available() else "auto") |
| except Exception as e: |
| lg(f"pinned load failed ({e}); falling back to auto") |
| ml = _ld("auto") |
|
|
| lg(f"model ready ({lf():.0f}s left)") |
|
|
| P3 = [] |
| for _, r in df.iterrows(): |
| msgs = [ |
| {"role": "system", "content": "You solve International Linguistics Olympiad problems. Answer every numbered item. Put each answer on its own line, in order, with no numbering and no extra text."}, |
| {"role": "user", "content": f"{r['context'].strip()}\n\n{r['query'].strip()}"} |
| ] |
| P3.append(tk.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)) |
|
|
| B1 = X4 |
| |
| class Deadline(StoppingCriteria): |
| def __init__(self, stop_at): self.stop_at = stop_at |
| def __call__(self, input_ids, scores, **kw): return time.time() > self.stop_at |
|
|
| def d9(texts, max_new, sample, temp=0.7): |
| nonlocal B1 |
| out = [""] * len(texts) |
| order = sorted(range(len(texts)), key=lambda i: len(texts[i])) |
| i = 0 |
| while i < len(order): |
| if lf() < 25: break |
| idx = order[i:i + B1] |
| chunk = [texts[j] for j in idx] |
| try: |
| enc = tk(chunk, return_tensors="pt", padding=True, truncation=True, max_length=6144).to(ml.device) |
| kw = dict(max_new_tokens=max_new, pad_token_id=tk.pad_token_id, repetition_penalty=1.0, stopping_criteria=StoppingCriteriaList([Deadline(D1 - 10)])) |
| if sample: |
| kw.update(do_sample=True, temperature=temp, top_p=0.95) |
| else: |
| kw.update(do_sample=False) |
| with torch.no_grad(): |
| o = ml.generate(**enc, **kw) |
| for k, j in enumerate(idx): |
| out[j] = tk.decode(o[k][enc["input_ids"].shape[1]:], skip_special_tokens=True) |
| i += B1 |
| except torch.cuda.OutOfMemoryError: |
| torch.cuda.empty_cache() |
| if B1 == 1: i += 1 |
| else: B1 = max(1, B1 // 2) |
| except Exception: |
| i += B1 |
| return out |
|
|
| lg(f"Pass 1 (greedy) starting... budget: {X1} tokens/item") |
| t = time.time() |
| texts = d9(P3, max_new=X1, sample=False) |
| c1 = time.time() - t |
| |
| V1 = {i: [] for i in I1} |
| for i, n, txt in zip(I1, N1, texts): |
| p1 = [d3(ln) for ln in (txt or "").splitlines() if ln.strip()] |
| R1[i] = d4(p1, n, S2[i]) |
| V1[i].append(R1[i]) |
| |
| d8(P2, I1, R1, E2) |
| lg(f"Pass 1 done in {c1:.0f}s. Written to disk.") |
|
|
| reserve = min(300.0, 0.25 * c1 + 60) if E1 else 30.0 |
| n_extra = 0 |
| while lf() - reserve > c1 * 1.25 and n_extra < X2: |
| n_extra += 1 |
| lg(f"Self-consistency pass {n_extra} starting... ({lf():.0f}s left)") |
| texts = d9(P3, max_new=X1, sample=True, temp=X3) |
| for i, n, txt in zip(I1, N1, texts): |
| if txt: |
| V1[i].append(d5(txt, n, S2[i])) |
| |
| for i, n in zip(I1, N1): |
| if len(V1[i]) >= 3: |
| greedy = V1[i][0] |
| voted = [d7([s[k] for s in V1[i] if k < len(s)], anchor=greedy[k] if k < len(greedy) else None) for k in range(n)] |
| R1[i] = d4(voted, n, S2[i]) |
| |
| d8(P2, I1, R1, E2) |
| lg(f"Pass {n_extra+1} voted and written.") |
|
|
| if E1 and lf() > 60: |
| lg(f"Generating explanations ({lf():.0f}s left)...") |
| ex_sys = "You explain International Linguistics Olympiad solutions to a human judge. State the key rules of the language: morphemes, word order, sound changes. Be concise (2-4 sentences)." |
| ex_prompts = [] |
| for _, r in df.iterrows(): |
| i = str(r["id"]) |
| msgs = [ |
| {"role": "system", "content": ex_sys}, |
| {"role": "user", "content": f"{r['context'].strip()}\n\n{r['query'].strip()}\n\nAnswers given:\n" + "\n".join(f"- {a}" for a in R1[i]) + "\n\nBriefly explain the linguistic rules behind these answers."} |
| ] |
| ex_prompts.append(tk.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)) |
| |
| ex_texts = d9(ex_prompts, max_new=200, sample=False) |
| for i, e in zip(I1, ex_texts): |
| e = re.sub(r"\s+", " ", (e or "").strip()) |
| if e: E2[i] = e[:1200] |
| d8(P2, I1, R1, E2) |
| lg("Explanations written.") |
|
|
| bad = [i for i, n in zip(I1, N1) if len(R1[i]) != n or any(not str(x).strip() for x in R1[i])] |
| if bad: |
| lg(f"Repairing {len(bad)} malformed rows") |
| for i, n in zip(I1, N1): |
| R1[i] = d4([x for x in R1[i] if str(x).strip()], n, S2[i]) |
| d8(P2, I1, R1, E2) |
|
|
| lg(f"DONE. {len(I1)} rows, {time.time() - T0:.0f}s elapsed.") |
|
|
| if __name__ == "__main__": |
| main() |