#!/usr/bin/env python import os, re, json, time, unicodedata from collections import Counter, defaultdict T0 = time.time() TIME_LIMIT = float(os.environ.get("IOL_TIME_LIMIT", "1800")) SAFETY = float(os.environ.get("IOL_SAFETY", "150")) DEADLINE = T0 + TIME_LIMIT - SAFETY TEST_CSV = os.environ.get("IOL_TEST_CSV", "/tmp/data/test.csv") OUT_CSV = os.environ.get("IOL_OUT_CSV", "submission.csv") MODEL_ID = os.environ.get("IOL_MODEL", ".") WANT_EXPL = os.environ.get("IOL_EXPLAIN", "1") == "1" TOK_PER_S = float(os.environ.get("IOL_TOKS", "30")) 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") MAX_NEW = int(os.environ.get("IOL_MAXNEW", "600")) # was 900 — faster passes, answers are short MAX_SAMPLES= int(os.environ.get("IOL_MAXSAMPLES", "40")) # was 12/24 — more votes BATCH_SIZE = int(os.environ.get("IOL_BATCH", "4")) # REVERT from 8 (this caused 0.1485) SAMPLE_TEMP= float(os.environ.get("IOL_TEMP", "0.5")) # keep — value that gave 0.2039 COT = os.environ.get("IOL_COT", "0") == "1" # keep off def log(m): print(f"[{time.time()-T0:7.1f}s] {m}", flush=True) def left(): return DEADLINE - time.time() # ---- item count + source fallback ------------------------------------------ _LINE_NUM=re.compile(r"^[ \t]*(\d{1,3})[.)\]]",re.M); _PAREN_NUM=re.compile(r"\((\d{1,3})\)") _RANGE=re.compile(r"\(?(\d{1,3})\s*(?:[-–—]|to)\s*(\d{1,3})\)?"); _LINE_LET=re.compile(r"^[ \t]*([A-Z])[.)\]]\s",re.M) def detect_n_items(query, task_type="", context=""): q=query or "" line=[int(m) for m in _LINE_NUM.findall(q)]; par=[int(m) for m in _PAREN_NUM.findall(q)] rng=0 for a,b in _RANGE.findall(q): a,b=int(a),int(b) if 01: return n lines=[l.strip() for l in q.splitlines() if l.strip()] if len(lines)>1: body=lines[1:] if lines[0].endswith((":",".")) else lines if body: return len(body) if context: cn=len(set(int(m) for m in _LINE_NUM.findall(context))) if cn>1: return cn cl=len(set(_LINE_LET.findall(context))) if cl>1: return cl return max(n,1) def extract_item_sources(query,n): q=query 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: lines=[l.strip() for l in q.splitlines() if l.strip()] if len(lines)>1 and lines[0].endswith((":",".")): out=lines[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)=2 and s[0]==s[-1] and s[0] in "\"'“”": s=s[1:-1].strip() return s.strip() def fit_to_n(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: by={}; [by.__setitem__(l,v) for l,v in numbered] labs=sorted(by) if len(labs)>=n: return [by[l] for l in labs[:n]] return fit_to_n(raw,n,fb) def norm(s): s=unicodedata.normalize("NFC",(s or "").strip().lower()); s=re.sub(r"\s+"," ",s) return s.strip(" .!?;:,") def vote(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[norm(c)].append(c) a_sup=len(groups.get(norm(anchor),[])); bk,bn=None,0 for k,v in groups.items(): if len(v)>bn: bk,bn=k,len(v) if bk is not None and bn>=2 and bn>a_sup: return Counter(groups[bk]).most_common(1)[0][0] return anchor # ---- prompts ---------------------------------------------------------------- SYS_TRIVIAL=("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.") SYS_COT=("You solve International Linguistics Olympiad problems. Everything needed is in the " "problem. Briefly analyse: segment the words, find the recurring morphemes and the rules " "that order them, and check them against every given example. Keep the analysis short. " "Then write a line containing exactly ANSWERS: and, below it, one answer per item in the " "order asked — no numbering, no commentary, no blank lines. Never leave an item blank.") def write_submission(path,ids,preds,expl=None): import pandas as pd rows=[] for i in ids: rec={"id":i,"pred":json.dumps(preds[i],ensure_ascii=False)} if expl is not None: rec["explanation"]=expl.get(i,"") rows.append(rec) pd.DataFrame(rows).to_csv(path,index=False) def main(): import pandas as pd, torch from transformers import (AutoTokenizer,AutoModelForCausalLM,StoppingCriteria,StoppingCriteriaList) torch.backends.cuda.matmul.allow_tf32=True; torch.backends.cudnn.allow_tf32=True log(f"MODE: {'CoT' if COT else 'trivial'} | rep=1.0 | max_samples={MAX_SAMPLES} temp={SAMPLE_TEMP}") df=pd.read_csv(TEST_CSV,dtype=str).fillna("") ids=[str(x) for x in df["id"].tolist()] ns=[detect_n_items(r.get("query",""),r.get("task_type",""),r.get("context","")) for _,r in df.iterrows()] log(f"loaded {len(df)} problems, {sum(ns)} items") srcs={i:extract_item_sources(r.get("query",""),n) for i,(_,r),n in zip(ids,df.iterrows(),ns)} preds={i:list(srcs[i]) for i in ids}; expl={i:"" for i in ids} if WANT_EXPL else None write_submission(OUT_CSV,ids,preds,expl); log(f"placeholder written ({len(ids)} rows)") class Deadline(StoppingCriteria): def __init__(self,t): self.t=t def __call__(self,i,s,**k): return time.time()>self.t log("loading model ...") tok=AutoTokenizer.from_pretrained(MODEL_ID,trust_remote_code=True) if tok.pad_token is None: tok.pad_token=tok.eos_token tok.padding_side="left" def _load(dm): try: return AutoModelForCausalLM.from_pretrained(MODEL_ID,torch_dtype=torch.float16,device_map=dm,trust_remote_code=True).eval() except TypeError: return AutoModelForCausalLM.from_pretrained(MODEL_ID,dtype=torch.float16,device_map=dm,trust_remote_code=True).eval() try: model=_load({"":0} if torch.cuda.is_available() else "auto") except Exception as e: log(f"pinned load failed ({e}); auto"); model=_load("auto") log(f"model ready ({left():.0f}s left)") sysmsg=SYS_COT if COT else SYS_TRIVIAL base_parse=parse_answers if COT else raw_lines # <-- the champion distinction prompts=[tok.apply_chat_template( [{"role":"system","content":sysmsg}, {"role":"user","content":f"{r['context'].strip()}\n\n{r['query'].strip()}"}], tokenize=False,add_generation_prompt=True) for _,r in df.iterrows()] cb=BATCH_SIZE def generate(texts,max_new,sample,temp=0.7): nonlocal cb out=[""]*len(texts); order=sorted(range(len(texts)),key=lambda i:len(texts[i])); i=0 while ic1*1.25 and ne=3: g=samples[i][0] preds[i]=fit_to_n([vote([s[k] for s in samples[i] if k60: log(f"explanations ({left():.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. Concise (2-4 sentences).") ep=[tok.apply_chat_template([{"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 preds[str(r['id'])])+"\n\nBriefly explain the linguistic rules."}], tokenize=False,add_generation_prompt=True) for _,r in df.iterrows()] for i,e in zip(ids,generate(ep,max_new=200,sample=False)): e=re.sub(r"\s+"," ",(e or "").strip()) if e: expl[i]=e[:1200] write_submission(OUT_CSV,ids,preds,expl); log("explanations written") bad=[i for i,n in zip(ids,ns) if len(preds[i])!=n or any(not str(x).strip() for x in preds[i])] if bad: for i,n in zip(ids,ns): preds[i]=fit_to_n([x for x in preds[i] if str(x).strip()],n,srcs[i]) write_submission(OUT_CSV,ids,preds,expl) log(f"DONE. {len(ids)} rows, {time.time()-T0:.0f}s elapsed.") if __name__=="__main__": main()