Text Generation
Transformers
Safetensors
qwen3
conversational
text-generation-inference
4-bit precision
awq
Instructions to use Santhoshini/iol-solver-qwen3 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Santhoshini/iol-solver-qwen3 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="Santhoshini/iol-solver-qwen3") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("Santhoshini/iol-solver-qwen3") model = AutoModelForCausalLM.from_pretrained("Santhoshini/iol-solver-qwen3", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use Santhoshini/iol-solver-qwen3 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "Santhoshini/iol-solver-qwen3" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Santhoshini/iol-solver-qwen3", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/Santhoshini/iol-solver-qwen3
- SGLang
How to use Santhoshini/iol-solver-qwen3 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "Santhoshini/iol-solver-qwen3" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Santhoshini/iol-solver-qwen3", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "Santhoshini/iol-solver-qwen3" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Santhoshini/iol-solver-qwen3", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use Santhoshini/iol-solver-qwen3 with Docker Model Runner:
docker model run hf.co/Santhoshini/iol-solver-qwen3
Update script.py
Browse files
script.py
CHANGED
|
@@ -5,6 +5,7 @@ SCRIPT_DIR = Path(__file__).resolve().parent if "__file__" in globals() else Pat
|
|
| 5 |
WHEELHOUSE = SCRIPT_DIR / "wheelhouse"
|
| 6 |
if not WHEELHOUSE.is_dir(): WHEELHOUSE = Path("wheelhouse")
|
| 7 |
RUNTIME_DIR = Path("/tmp/qwen3deps")
|
|
|
|
| 8 |
def emergency(reason):
|
| 9 |
try:
|
| 10 |
import pandas as pd, json as j
|
|
@@ -15,6 +16,7 @@ def emergency(reason):
|
|
| 15 |
except Exception:
|
| 16 |
try: open("submission.csv","w").write("id,pred,explanation\n")
|
| 17 |
except Exception: pass
|
|
|
|
| 18 |
try:
|
| 19 |
wheels = [str(WHEELHOUSE / w) for w in os.listdir(WHEELHOUSE) if w.endswith(".whl")]
|
| 20 |
if not wheels: raise FileNotFoundError(f"no wheels in {WHEELHOUSE}")
|
|
@@ -26,21 +28,28 @@ try:
|
|
| 26 |
except Exception: pass
|
| 27 |
except Exception as e:
|
| 28 |
emergency(f"wheel install failed: {e}"); raise
|
| 29 |
-
|
|
|
|
| 30 |
import pandas as pd, torch
|
| 31 |
-
from difflib import SequenceMatcher
|
| 32 |
-
from collections import defaultdict
|
| 33 |
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
def write_csv(rows):
|
| 39 |
import csv
|
| 40 |
with open("submission.csv.tmp","w",newline="",encoding="utf-8") as f:
|
| 41 |
-
w=csv.DictWriter(f,fieldnames=["id","pred","explanation"])
|
|
|
|
| 42 |
for r in rows: w.writerow(r)
|
| 43 |
os.replace("submission.csv.tmp","submission.csv")
|
|
|
|
| 44 |
try:
|
| 45 |
df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
|
| 46 |
write_csv([{"id":i,"pred":json.dumps([""]),"explanation":"placeholder"} for i in df["id"]])
|
|
@@ -50,274 +59,278 @@ try:
|
|
| 50 |
print("loaded, quantized:", getattr(model.config,"quantization_config",None) is not None, flush=True)
|
| 51 |
except Exception as e:
|
| 52 |
emergency(f"load failed: {e}"); raise
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
| 56 |
def n_expected(query):
|
| 57 |
-
items=re.findall(r"(?m)^\s*(\d+)\s*[.\)]", query)
|
| 58 |
if items: return len(items)
|
| 59 |
-
rng=re.search(r"\(\s*(\d+)\s*[-–—]\s*(\d+)\s*\)", query)
|
| 60 |
if rng:
|
| 61 |
-
lo,hi=int(rng.group(1)),int(rng.group(2))
|
| 62 |
-
if 0<hi-lo<100: return hi-lo+1
|
| 63 |
return None
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
# ---------------------------------------------------------------------------
|
| 84 |
-
# Generation
|
| 85 |
-
# passes below); gen() is the exact proven baseline call, unchanged.
|
| 86 |
# ---------------------------------------------------------------------------
|
| 87 |
-
def
|
|
|
|
|
|
|
| 88 |
try:
|
| 89 |
-
enc=tok.apply_chat_template(msgs,add_generation_prompt=True,enable_thinking=False,
|
| 90 |
-
|
| 91 |
-
ilen=enc["input_ids"].shape[-1]
|
| 92 |
-
with torch.no_grad(): out=model.generate(**enc,max_new_tokens=
|
| 93 |
except Exception:
|
| 94 |
-
ids=tok.apply_chat_template(msgs,add_generation_prompt=True,enable_thinking=False,
|
| 95 |
-
|
| 96 |
-
ilen=ids.shape[-1]
|
| 97 |
-
with torch.no_grad(): out=model.generate(ids,max_new_tokens=
|
| 98 |
-
return tok.decode(out[0][ilen:],skip_special_tokens=True).strip()
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
{"role":"user","content":(f"{context.strip()}\n\n{query.strip()}\n\n"
|
| 122 |
-
f"Items {', '.join(str(i+1) for i in idxs)} were all given the SAME answer, "
|
| 123 |
-
f"which is impossible in a one-to-one matching.\n"
|
| 124 |
-
f"Already used elsewhere (do not reuse): {', '.join(used) if used else '(none)'}\n"
|
| 125 |
-
f"Give a DIFFERENT answer for each of those {len(idxs)} items, in order, "
|
| 126 |
-
f"one per line, nothing else.")}]
|
| 127 |
-
try: rep=[norm_generic(l) for l in gen_msgs(msgs,96).splitlines() if l.strip()]
|
| 128 |
-
except Exception: return answers
|
| 129 |
-
if len(rep)!=len(idxs): return answers
|
| 130 |
-
cand=list(answers)
|
| 131 |
-
for i,v in zip(idxs,rep): cand[i]=v
|
| 132 |
-
return cand if not find_bijection_conflicts(cand) else answers
|
| 133 |
-
|
| 134 |
-
# ===========================================================================
|
| 135 |
-
# ALGORITHM 2 -- fill_blanks: extract the strongest transformation family
|
| 136 |
-
# from the context's OWN given pairs, then apply it MECHANICALLY to each
|
| 137 |
-
# query stem. An exact computation beats a free-text guess. Emits nothing
|
| 138 |
-
# unless a family is supported by 2+ independent pairs AND shares a real
|
| 139 |
-
# 2-char stem -- one occurrence is indistinguishable from coincidence.
|
| 140 |
-
# ===========================================================================
|
| 141 |
-
def extract_explicit_pairs(context):
|
| 142 |
-
pairs=[]
|
| 143 |
-
for line in context.splitlines():
|
| 144 |
-
line=line.strip()
|
| 145 |
-
if not (0<line.count("|")<=3): continue
|
| 146 |
-
fields=[re.sub(r"^\s*\d+\s*[.\)]\s*","",f.strip()).strip() for f in line.split("|")]
|
| 147 |
-
fields=[f for f in fields if f]
|
| 148 |
-
if len(fields)>=2: pairs.append((fields[0],fields[1]))
|
| 149 |
-
return pairs
|
| 150 |
-
def edit_signature(a,b):
|
| 151 |
-
sm=SequenceMatcher(None,a,b,autojunk=False); allops=sm.get_opcodes()
|
| 152 |
-
ops=[o for o in allops if o[0]!="equal"]
|
| 153 |
-
if not ops or len(ops)>2: return None
|
| 154 |
-
if sum((i2-i1) for t,i1,i2,j1,j2 in allops if t=="equal")<2: return None
|
| 155 |
-
t,i1,i2,j1,j2=ops[0]
|
| 156 |
-
if i1==0: pos="prefix"
|
| 157 |
-
elif i2==len(a): pos="suffix"
|
| 158 |
-
else: pos="infix"
|
| 159 |
-
return (pos,a[i1:i2],b[j1:j2])
|
| 160 |
-
def strongest_family(pairs):
|
| 161 |
-
groups=defaultdict(list)
|
| 162 |
-
for a,b in pairs:
|
| 163 |
-
if not a or not b or a==b: continue
|
| 164 |
-
sig=edit_signature(a,b)
|
| 165 |
-
if sig: groups[sig].append((a,b))
|
| 166 |
-
fams=[(len(list(dict.fromkeys(g))),s) for s,g in groups.items()
|
| 167 |
-
if len(list(dict.fromkeys(g)))>=2]
|
| 168 |
-
if not fams: return None
|
| 169 |
-
fams.sort(key=lambda x:-x[0])
|
| 170 |
-
return fams[0][1]
|
| 171 |
-
def apply_transformation(sig,s):
|
| 172 |
-
pos,removed,inserted=sig
|
| 173 |
-
if pos=="prefix":
|
| 174 |
-
if removed=="": return inserted+s
|
| 175 |
-
return inserted+s[len(removed):] if s.startswith(removed) else None
|
| 176 |
-
if pos=="suffix":
|
| 177 |
-
if removed=="": return s+inserted
|
| 178 |
-
return s[:-len(removed)]+inserted if s.endswith(removed) else None
|
| 179 |
-
return None # infix: no safe anchor, deliberately never applied
|
| 180 |
-
def fill_blanks_override(answers, context, query):
|
| 181 |
-
pairs=extract_explicit_pairs(context)
|
| 182 |
-
if not pairs: return answers
|
| 183 |
-
sig=strongest_family(pairs)
|
| 184 |
-
if not sig: return answers
|
| 185 |
-
stems=[m.split("|")[0].strip()
|
| 186 |
-
for m in re.findall(r"(?m)^\s*\d+\s*[.\)]\s*(.*)$", query)]
|
| 187 |
-
if not stems: return answers
|
| 188 |
-
out=list(answers)
|
| 189 |
-
for i,stem in enumerate(stems):
|
| 190 |
-
if i>=len(out) or not stem: continue
|
| 191 |
-
mech=apply_transformation(sig,stem)
|
| 192 |
-
if mech: out[i]=norm_generic(mech)
|
| 193 |
-
return out
|
| 194 |
-
|
| 195 |
-
# ===========================================================================
|
| 196 |
-
# ALGORITHM 3 -- text_to_num: exact arithmetic verification. When the model
|
| 197 |
-
# writes out its own arithmetic (e.g. "6*6+11"), evaluate it EXACTLY via an
|
| 198 |
-
# AST walker (no exec/eval, +,-,* and parentheses only) and prefer the
|
| 199 |
-
# computed value over the model's stated total. Catches the common case of
|
| 200 |
-
# a correct rule with wrong mental arithmetic. Per-line, so index alignment
|
| 201 |
-
# is guaranteed. No prompt change -- the winning prompt stays untouched.
|
| 202 |
-
# ===========================================================================
|
| 203 |
-
_ALLOWED_OPS=(pyast.Add,pyast.Sub,pyast.Mult)
|
| 204 |
-
def safe_arithmetic(expr):
|
| 205 |
-
try: tree=pyast.parse(expr.strip(),mode="eval")
|
| 206 |
-
except Exception: return None
|
| 207 |
-
def _ev(n):
|
| 208 |
-
if isinstance(n,pyast.Expression): return _ev(n.body)
|
| 209 |
-
if isinstance(n,pyast.Constant) and isinstance(n.value,(int,float)): return n.value
|
| 210 |
-
if isinstance(n,pyast.BinOp) and isinstance(n.op,_ALLOWED_OPS):
|
| 211 |
-
l,r=_ev(n.left),_ev(n.right)
|
| 212 |
-
if l is None or r is None: return None
|
| 213 |
-
if isinstance(n.op,pyast.Add): return l+r
|
| 214 |
-
if isinstance(n.op,pyast.Sub): return l-r
|
| 215 |
-
if isinstance(n.op,pyast.Mult): return l*r
|
| 216 |
-
if isinstance(n,pyast.UnaryOp) and isinstance(n.op,pyast.USub):
|
| 217 |
-
v=_ev(n.operand)
|
| 218 |
-
return -v if v is not None else None
|
| 219 |
-
return None
|
| 220 |
-
return _ev(tree)
|
| 221 |
-
def arithmetic_override(line):
|
| 222 |
-
for c in sorted(re.findall(r"[0-9]+(?:\s*[\+\-\*]\s*[0-9()]+)+", line), key=len, reverse=True):
|
| 223 |
-
v=safe_arithmetic(c)
|
| 224 |
-
if v is not None and float(v).is_integer() and v>=0: return str(int(v))
|
| 225 |
-
return None
|
| 226 |
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
# hallucination filter. A character that never appears anywhere in the
|
| 230 |
-
# context cannot be part of a correct answer. Detection alone can't raise
|
| 231 |
-
# EM, so a flagged row gets ONE targeted repair naming the constraint
|
| 232 |
-
# (greedy decoding is deterministic -- an identical re-ask would be a
|
| 233 |
-
# guaranteed no-op), and we keep whichever answer set has fewer
|
| 234 |
-
# out-of-inventory characters. Strictly never worse.
|
| 235 |
-
# Translating INTO English is deliberately excluded: English letters
|
| 236 |
-
# legitimately won't appear in an unseen-language context, so the signal
|
| 237 |
-
# would be meaningless there.
|
| 238 |
-
# ===========================================================================
|
| 239 |
-
_NEUTRAL=set(" \t.,;:!?'\"()[]-–—/0123456789\u2018\u2019\u201c\u201d")
|
| 240 |
-
def out_of_inventory(answers, charset):
|
| 241 |
-
return sum(1 for a in answers for ch in unicodedata.normalize("NFC",a)
|
| 242 |
-
if ch not in _NEUTRAL and ch not in charset)
|
| 243 |
-
def into_task_language(query, task_lang):
|
| 244 |
-
q=query.lower()
|
| 245 |
-
# Hard exclusion FIRST. Without this, a task_lang of "English" (or an
|
| 246 |
-
# into-English query) satisfies the generic check below and the filter
|
| 247 |
-
# would fire on exactly the case it must never touch -- flagging every
|
| 248 |
-
# legitimate English answer as hallucinated and burning a wasted repair
|
| 249 |
-
# call on each. Strictly worse than baseline; caught by an isolation test.
|
| 250 |
-
if re.search(r"into\s+english", q): return False
|
| 251 |
-
tl=(task_lang or "").strip().lower()
|
| 252 |
-
if tl and tl != "english" and tl in q and "into" in q: return True
|
| 253 |
-
return bool(re.search(r"translate .{0,40}into (?!english)", q))
|
| 254 |
-
def inventory_repair(answers, context, query, n):
|
| 255 |
-
charset=set(unicodedata.normalize("NFC",context))
|
| 256 |
-
bad=out_of_inventory(answers,charset)
|
| 257 |
-
if bad==0: return answers
|
| 258 |
-
msgs=[{"role":"system","content":SYS},
|
| 259 |
-
{"role":"user","content":(f"{context.strip()}\n\n{query.strip()}\n\n"
|
| 260 |
-
f"Important: your answer must use ONLY characters that actually appear in the "
|
| 261 |
-
f"examples above. Do not invent letters or symbols that are not shown there. "
|
| 262 |
-
f"Answer again, one answer per line, nothing else.")}]
|
| 263 |
-
try: rep=[norm_generic(l) for l in gen_msgs(msgs,512).splitlines() if l.strip()]
|
| 264 |
-
except Exception: return answers
|
| 265 |
-
if not rep: return answers
|
| 266 |
-
if n:
|
| 267 |
-
if len(rep)<n: rep=rep+[rep[-1]]*(n-len(rep))
|
| 268 |
-
elif len(rep)>n: rep=rep[:n]
|
| 269 |
-
return rep if out_of_inventory(rep,charset)<bad else answers
|
| 270 |
-
|
| 271 |
-
rows=[]; done=set()
|
| 272 |
try:
|
| 273 |
-
for _,r in df.iterrows():
|
| 274 |
try:
|
| 275 |
-
task_type=r.get("task_type",
|
| 276 |
-
text=
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
if
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
n=n_expected(r["query"])
|
| 288 |
-
if n:
|
| 289 |
-
if len(ans)<n: ans=ans+[ans[-1] if ans else ""]*(n-len(ans))
|
| 290 |
-
elif len(ans)>n: ans=ans[:n]
|
| 291 |
-
if not ans: ans=[""]
|
| 292 |
-
|
| 293 |
-
# ALGO 2 -- mechanical transformation override (no extra call).
|
| 294 |
-
if task_type=="fill_blanks":
|
| 295 |
-
ans=fill_blanks_override(ans,r["context"],r["query"])
|
| 296 |
-
|
| 297 |
-
# ALGO 1 / ALGO 4 -- one targeted repair each, only if a real
|
| 298 |
-
# defect was detected AND real remaining time (freshly measured)
|
| 299 |
-
# allows it. Both are never-worse by construction.
|
| 300 |
-
if time.time()-start < TIME_LIMIT-REPAIR_RESERVE:
|
| 301 |
-
if task_type=="match_letters":
|
| 302 |
-
ans=repair_bijection(ans,r["context"],r["query"])
|
| 303 |
-
elif task_type=="translation" and into_task_language(r["query"],task_lang):
|
| 304 |
-
ans=inventory_repair(ans,r["context"],r["query"],n)
|
| 305 |
-
|
| 306 |
-
if not ans: ans=[""]
|
| 307 |
-
expl=re.sub(r"\s+"," ",text[:300]).strip() or "derived from the examples"
|
| 308 |
-
rows.append({"id":r["id"],"pred":json.dumps(ans,ensure_ascii=False),"explanation":expl})
|
| 309 |
except Exception as e:
|
| 310 |
-
n=n_expected(r["query"]) or 1
|
| 311 |
-
rows.append({"id":r["id"],"pred":json.dumps([""]*n,ensure_ascii=False),
|
| 312 |
-
|
|
|
|
|
|
|
| 313 |
done.add(r["id"]); write_csv(rows)
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
| 318 |
if r["id"] in done: continue
|
| 319 |
-
n=n_expected(r["query"]) or 1
|
| 320 |
-
rows.append({"id":r["id"],"pred":json.dumps([""]*n,ensure_ascii=False),
|
| 321 |
-
|
|
|
|
| 322 |
except Exception as e:
|
| 323 |
-
emergency(f"main loop: {e}"); print("FATAL",e,flush=True)
|
|
|
|
| 5 |
WHEELHOUSE = SCRIPT_DIR / "wheelhouse"
|
| 6 |
if not WHEELHOUSE.is_dir(): WHEELHOUSE = Path("wheelhouse")
|
| 7 |
RUNTIME_DIR = Path("/tmp/qwen3deps")
|
| 8 |
+
|
| 9 |
def emergency(reason):
|
| 10 |
try:
|
| 11 |
import pandas as pd, json as j
|
|
|
|
| 16 |
except Exception:
|
| 17 |
try: open("submission.csv","w").write("id,pred,explanation\n")
|
| 18 |
except Exception: pass
|
| 19 |
+
|
| 20 |
try:
|
| 21 |
wheels = [str(WHEELHOUSE / w) for w in os.listdir(WHEELHOUSE) if w.endswith(".whl")]
|
| 22 |
if not wheels: raise FileNotFoundError(f"no wheels in {WHEELHOUSE}")
|
|
|
|
| 28 |
except Exception: pass
|
| 29 |
except Exception as e:
|
| 30 |
emergency(f"wheel install failed: {e}"); raise
|
| 31 |
+
|
| 32 |
+
import re, json, time
|
| 33 |
import pandas as pd, torch
|
|
|
|
|
|
|
| 34 |
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 35 |
+
|
| 36 |
+
MODEL_ID = "."
|
| 37 |
+
TIME_LIMIT = 30 * 60
|
| 38 |
+
SAFETY_S = 90
|
| 39 |
+
start = time.time()
|
| 40 |
+
|
| 41 |
+
LONG_THINK_TASKS = {"translation", "match_letters", "fill_blanks"}
|
| 42 |
+
LONG_TOTAL_TOKENS = 1536
|
| 43 |
+
THINK_CLOSE_TOKEN_ID = 151668 # </think> in Qwen3 tokenizer (documented)
|
| 44 |
+
|
| 45 |
def write_csv(rows):
|
| 46 |
import csv
|
| 47 |
with open("submission.csv.tmp","w",newline="",encoding="utf-8") as f:
|
| 48 |
+
w = csv.DictWriter(f, fieldnames=["id","pred","explanation"])
|
| 49 |
+
w.writeheader()
|
| 50 |
for r in rows: w.writerow(r)
|
| 51 |
os.replace("submission.csv.tmp","submission.csv")
|
| 52 |
+
|
| 53 |
try:
|
| 54 |
df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
|
| 55 |
write_csv([{"id":i,"pred":json.dumps([""]),"explanation":"placeholder"} for i in df["id"]])
|
|
|
|
| 59 |
print("loaded, quantized:", getattr(model.config,"quantization_config",None) is not None, flush=True)
|
| 60 |
except Exception as e:
|
| 61 |
emergency(f"load failed: {e}"); raise
|
| 62 |
+
|
| 63 |
+
SYS = ("You solve International Linguistics Olympiad problems about a language you have never seen. "
|
| 64 |
+
"Everything you need is in the examples. Answer every numbered item, in order. "
|
| 65 |
+
"Put each answer on its own line, with no numbering and no extra text.")
|
| 66 |
+
|
| 67 |
def n_expected(query):
|
| 68 |
+
items = re.findall(r"(?m)^\s*(\d+)\s*[.\)]", query)
|
| 69 |
if items: return len(items)
|
| 70 |
+
rng = re.search(r"\(\s*(\d+)\s*[-–—]\s*(\d+)\s*\)", query)
|
| 71 |
if rng:
|
| 72 |
+
lo, hi = int(rng.group(1)), int(rng.group(2))
|
| 73 |
+
if 0 < hi - lo < 100: return hi - lo + 1
|
| 74 |
return None
|
| 75 |
+
|
| 76 |
+
# =============================================================================
|
| 77 |
+
# PARSER -- designed as a compiler, not a regex patchwork.
|
| 78 |
+
# Replaces the prior line-based parser. Every rule justified by a specific
|
| 79 |
+
# failure mode derived from adversarial analysis of Qwen3 outputs.
|
| 80 |
+
#
|
| 81 |
+
# Failure modes handled that the old parser did NOT handle:
|
| 82 |
+
#
|
| 83 |
+
# 1. THINK BLOCK LEAKAGE (F1, F2): <think>...</think> and orphan </think>
|
| 84 |
+
# appear as text when thinking mode is active. Old parser: included the
|
| 85 |
+
# reasoning text as answers. New parser: strips all think content first.
|
| 86 |
+
#
|
| 87 |
+
# 2. PIPE-SEPARATED ANSWERS (A3): model emits "cat | dog | mouse" on one
|
| 88 |
+
# line (common because IOL contexts use | as field delimiter). Old parser:
|
| 89 |
+
# treats the whole line as one answer. New parser: tries pipe-split and
|
| 90 |
+
# selects it when count matches n.
|
| 91 |
+
#
|
| 92 |
+
# 3. COMMA-SEPARATED ANSWERS (A4): model emits "cat, dog, mouse". Old parser:
|
| 93 |
+
# one answer. New parser: tries comma-split for single-line short outputs.
|
| 94 |
+
#
|
| 95 |
+
# 4. SPACE-SEPARATED LETTERS FOR MATCHING (A6): model emits "A B C D" on one
|
| 96 |
+
# line. Old parser: one answer. New parser: splits when all tokens are
|
| 97 |
+
# single capital letters and task is match_letters.
|
| 98 |
+
#
|
| 99 |
+
# 5. PREAMBLE LINES (B5): "Here are the answers:\ncat\ndog". Old parser:
|
| 100 |
+
# "Here are the answers:" becomes the first answer. New parser: detects
|
| 101 |
+
# and discards preamble lines before applying count enforcement.
|
| 102 |
+
#
|
| 103 |
+
# 6. EXPLANATION LINES (B3): "cat\ndog\nThis is because...". Old parser with
|
| 104 |
+
# n=2: takes first 2 including explanation if it appears early. New parser:
|
| 105 |
+
# detects explanation lines and discards them.
|
| 106 |
+
#
|
| 107 |
+
# 7. TABLE FORMAT FOR MATCHING (B8): "| A | water |\n| B | child |". Old
|
| 108 |
+
# parser: pipe split disabled, each row becomes one answer. New parser:
|
| 109 |
+
# extracts the single-letter option from each table row.
|
| 110 |
+
#
|
| 111 |
+
# 8. ZERO-WIDTH SPACES AND NON-BREAKING SPACES (D2, D4): invisible Unicode
|
| 112 |
+
# characters that cause silent EM failure. Old parser: only NFC
|
| 113 |
+
# normalization, does not remove Cf category characters. New parser:
|
| 114 |
+
# explicitly strips zero-width space, ZWJ, ZWNJ, soft hyphen, BOM,
|
| 115 |
+
# and replaces non-breaking space with regular space.
|
| 116 |
+
#
|
| 117 |
+
# 9. ITEM N: PREFIX NOT STRIPPED (B10): "item 1: cat". Old parser: strips
|
| 118 |
+
# only answer/translation/output prefixes. New parser: also strips
|
| 119 |
+
# "item N:" prefix pattern.
|
| 120 |
+
#
|
| 121 |
+
# Failures NOT changed (old behavior preserved for these):
|
| 122 |
+
# - Numbered list "1. cat" → strips correctly (unchanged)
|
| 123 |
+
# - Bullet "- cat" → strips correctly (unchanged)
|
| 124 |
+
# - Answer: prefix → strips correctly (unchanged)
|
| 125 |
+
# - NFC normalization → preserved
|
| 126 |
+
# - text_to_num digit extraction → preserved
|
| 127 |
+
# =============================================================================
|
| 128 |
+
|
| 129 |
+
def _clean_unicode(s: str) -> str:
|
| 130 |
+
s = s.replace('\u00a0', ' ') # non-breaking space → space
|
| 131 |
+
s = s.replace('\u200b', '') # zero-width space
|
| 132 |
+
s = s.replace('\u200c', '') # zero-width non-joiner
|
| 133 |
+
s = s.replace('\u200d', '') # zero-width joiner
|
| 134 |
+
s = s.replace('\ufeff', '') # BOM
|
| 135 |
+
s = s.replace('\u00ad', '') # soft hyphen
|
| 136 |
+
return unicodedata.normalize("NFC", s)
|
| 137 |
+
|
| 138 |
+
def _strip_think(s: str) -> str:
|
| 139 |
+
s = re.sub(r'<think>.*?</think>', '', s, flags=re.DOTALL | re.IGNORECASE)
|
| 140 |
+
s = re.sub(r'</think>', '', s, flags=re.IGNORECASE)
|
| 141 |
+
s = re.sub(r'<think>', '', s, flags=re.IGNORECASE)
|
| 142 |
+
return s
|
| 143 |
+
|
| 144 |
+
_PREAMBLE = re.compile(
|
| 145 |
+
r"""(?ix)^(?:here\s+(?:are|is)\b|the\s+following\b|these\s+(?:are|follow)\b|
|
| 146 |
+
(?:answer|result|translation|output)s?\s*:|note\s*:|explanation\s*:)""")
|
| 147 |
+
_EXPLANATION = re.compile(
|
| 148 |
+
r"""(?ix)^(?:(?:this|it)\s+is\s+because\b|(?:this|the)\s+(?:pattern|rule|form)\b|
|
| 149 |
+
(?:note|explanation|reason|because)\s*:|therefore\b|
|
| 150 |
+
the\s+answer\s+(?:is|follows)\b|(?:we\s+can\s+see|observe\s+that)\b|
|
| 151 |
+
(?:in\s+this\s+language)\b|(?:looking\s+at)\b|(?:from\s+the\s+examples)\b)""")
|
| 152 |
+
|
| 153 |
+
def _is_preamble(line: str) -> bool:
|
| 154 |
+
s = line.strip()
|
| 155 |
+
return bool(s) and (s.endswith(':') and len(s) > 3 or bool(_PREAMBLE.match(s)))
|
| 156 |
+
|
| 157 |
+
def _is_explanation(line: str) -> bool:
|
| 158 |
+
return bool(_EXPLANATION.match(line.strip()))
|
| 159 |
+
|
| 160 |
+
def _norm_one(s: str, task_type: str) -> str:
|
| 161 |
+
s = _clean_unicode(s)
|
| 162 |
+
s = re.sub(r'^\s*\(?\d+\)?\s*[.):\-]\s+', '', s) # "1. " / "1) " / "(1) "
|
| 163 |
+
s = re.sub(r'^\s*[-*•·]\s+', '', s) # bullets
|
| 164 |
+
s = re.sub(r'(?i)^\s*(?:the\s+)?(?:final\s+)?'
|
| 165 |
+
r'(?:answer|translation|output|item)\s*\d*\s*[:.\-]\s*', '', s)
|
| 166 |
+
s = re.sub(r'(?i)^\s*is\s*:\s*', '', s)
|
| 167 |
+
s = s.strip('* ')
|
| 168 |
+
s = re.sub(r'[ \t]+', ' ', s)
|
| 169 |
+
s = s.strip()
|
| 170 |
+
s = s.strip('"\'"\u201c\u201d\u2018\u2019')
|
| 171 |
+
if task_type == 'text_to_num':
|
| 172 |
+
m = re.search(r'-?\d[\d,\. ]*\d|\d', s)
|
| 173 |
+
if m:
|
| 174 |
+
d = re.sub(r'[^\d-]', '', m.group(0))
|
| 175 |
+
if d: s = d
|
| 176 |
+
return s
|
| 177 |
+
|
| 178 |
+
def _table_match_answers(text: str, task_type: str) -> list | None:
|
| 179 |
+
if '|' not in text: return None
|
| 180 |
+
rows = [ln.strip() for ln in text.splitlines() if ln.strip() and '|' in ln]
|
| 181 |
+
if not rows: return None
|
| 182 |
+
result = []
|
| 183 |
+
for row in rows:
|
| 184 |
+
cells = [c.strip() for c in row.split('|') if c.strip()]
|
| 185 |
+
for cell in cells:
|
| 186 |
+
if re.match(r'^[A-Z]$', cell):
|
| 187 |
+
result.append(cell)
|
| 188 |
+
break
|
| 189 |
+
else:
|
| 190 |
+
if cells:
|
| 191 |
+
c = _norm_one(cells[0], task_type)
|
| 192 |
+
if c: result.append(c)
|
| 193 |
+
return result or None
|
| 194 |
+
|
| 195 |
+
def _segmentations(text: str, task_type: str) -> list:
|
| 196 |
+
cands = []
|
| 197 |
+
lines = [_norm_one(ln, task_type) for ln in text.splitlines()
|
| 198 |
+
if ln.strip() and _norm_one(ln.strip(), task_type)]
|
| 199 |
+
if lines: cands.append(lines)
|
| 200 |
+
|
| 201 |
+
if '|' in text:
|
| 202 |
+
parts = [_norm_one(p, task_type) for p in re.split(r'\s*\|\s*', text.strip()) if p.strip()]
|
| 203 |
+
parts = [p for p in parts if p]
|
| 204 |
+
if parts and parts != lines: cands.append(parts)
|
| 205 |
+
|
| 206 |
+
if ',' in text and '\n' not in text.strip():
|
| 207 |
+
parts = [_norm_one(p, task_type) for p in text.split(',') if p.strip()]
|
| 208 |
+
parts = [p for p in parts if p and len(p) <= 60]
|
| 209 |
+
if parts and parts != lines: cands.append(parts)
|
| 210 |
+
|
| 211 |
+
if task_type == 'match_letters':
|
| 212 |
+
tokens = text.strip().split()
|
| 213 |
+
if tokens and all(re.match(r'^[A-Z]$', t) for t in tokens) and tokens != lines:
|
| 214 |
+
cands.append(tokens)
|
| 215 |
+
|
| 216 |
+
return cands
|
| 217 |
+
|
| 218 |
+
def parse_answers(raw: str, task_type: str, n: int | None) -> list:
|
| 219 |
+
if not raw or not raw.strip():
|
| 220 |
+
return [""] * (n or 1)
|
| 221 |
+
|
| 222 |
+
text = _clean_unicode(raw)
|
| 223 |
+
text = _strip_think(text).strip()
|
| 224 |
+
if not text:
|
| 225 |
+
return [""] * (n or 1)
|
| 226 |
+
|
| 227 |
+
# Table extraction for match_letters
|
| 228 |
+
if task_type == 'match_letters' and '|' in text:
|
| 229 |
+
ta = _table_match_answers(text, task_type)
|
| 230 |
+
if ta and (n is None or len(ta) == n):
|
| 231 |
+
return ta
|
| 232 |
+
|
| 233 |
+
segs = _segmentations(text, task_type)
|
| 234 |
+
if not segs:
|
| 235 |
+
return [""] * (n or 1)
|
| 236 |
+
|
| 237 |
+
if n is not None:
|
| 238 |
+
# Pick first segmentation with exact count match
|
| 239 |
+
for seg in segs:
|
| 240 |
+
if len(seg) == n:
|
| 241 |
+
return seg
|
| 242 |
+
# Try filtering preamble/explanation lines
|
| 243 |
+
raw_lines = [ln for ln in text.splitlines() if ln.strip()]
|
| 244 |
+
filtered = [ln for ln in raw_lines if not _is_preamble(ln) and not _is_explanation(ln)]
|
| 245 |
+
filtered_norm = [_norm_one(ln, task_type) for ln in filtered if _norm_one(ln, task_type)]
|
| 246 |
+
if len(filtered_norm) == n:
|
| 247 |
+
return filtered_norm
|
| 248 |
+
# Apply count enforcement on best candidate
|
| 249 |
+
best = filtered_norm if filtered_norm else segs[0]
|
| 250 |
+
if len(best) > n: return best[:n]
|
| 251 |
+
if len(best) < n: return best + [best[-1] if best else ""] * (n - len(best))
|
| 252 |
+
return best
|
| 253 |
+
|
| 254 |
+
# n unknown: filter preamble/explanation, return remainder
|
| 255 |
+
raw_lines = [ln for ln in text.splitlines() if ln.strip()]
|
| 256 |
+
filtered = [ln for ln in raw_lines if not _is_preamble(ln) and not _is_explanation(ln)]
|
| 257 |
+
filtered_norm = [_norm_one(ln, task_type) for ln in filtered if _norm_one(ln, task_type)]
|
| 258 |
+
return filtered_norm if filtered_norm else (segs[0] if segs else [""])
|
| 259 |
+
|
| 260 |
|
| 261 |
# ---------------------------------------------------------------------------
|
| 262 |
+
# Generation -- documented single-call Qwen3 pattern.
|
|
|
|
| 263 |
# ---------------------------------------------------------------------------
|
| 264 |
+
def gen_baseline(context, query):
|
| 265 |
+
msgs = [{"role":"system","content":SYS},
|
| 266 |
+
{"role":"user","content":f"{context.strip()}\n\n{query.strip()}"}]
|
| 267 |
try:
|
| 268 |
+
enc = tok.apply_chat_template(msgs, add_generation_prompt=True, enable_thinking=False,
|
| 269 |
+
return_tensors="pt", return_dict=True).to(model.device)
|
| 270 |
+
ilen = enc["input_ids"].shape[-1]
|
| 271 |
+
with torch.no_grad(): out = model.generate(**enc, max_new_tokens=512, do_sample=False)
|
| 272 |
except Exception:
|
| 273 |
+
ids = tok.apply_chat_template(msgs, add_generation_prompt=True, enable_thinking=False,
|
| 274 |
+
return_tensors="pt").to(model.device)
|
| 275 |
+
ilen = ids.shape[-1]
|
| 276 |
+
with torch.no_grad(): out = model.generate(ids, max_new_tokens=512, do_sample=False)
|
| 277 |
+
return tok.decode(out[0][ilen:], skip_special_tokens=True).strip()
|
| 278 |
+
|
| 279 |
+
def gen_long_think(context, query):
|
| 280 |
+
msgs = [{"role":"system","content":SYS},
|
| 281 |
+
{"role":"user","content":f"{context.strip()}\n\n{query.strip()}"}]
|
| 282 |
+
try:
|
| 283 |
+
enc = tok.apply_chat_template(msgs, add_generation_prompt=True, enable_thinking=True,
|
| 284 |
+
return_tensors="pt", return_dict=True).to(model.device)
|
| 285 |
+
input_length = enc["input_ids"].shape[-1]
|
| 286 |
+
except Exception:
|
| 287 |
+
return gen_baseline(context, query)
|
| 288 |
+
try:
|
| 289 |
+
with torch.no_grad():
|
| 290 |
+
out = model.generate(**enc, max_new_tokens=LONG_TOTAL_TOKENS,
|
| 291 |
+
do_sample=True, temperature=0.6, top_p=0.95, top_k=20)
|
| 292 |
+
except Exception:
|
| 293 |
+
return gen_baseline(context, query)
|
| 294 |
+
output_ids = out[0][input_length:].tolist()
|
| 295 |
+
try:
|
| 296 |
+
idx = len(output_ids) - output_ids[::-1].index(THINK_CLOSE_TOKEN_ID)
|
| 297 |
+
except ValueError:
|
| 298 |
+
idx = 0
|
| 299 |
+
return tok.decode(output_ids[idx:], skip_special_tokens=True).strip("\n").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
+
|
| 302 |
+
rows = []; done = set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
try:
|
| 304 |
+
for _, r in df.iterrows():
|
| 305 |
try:
|
| 306 |
+
task_type = r.get("task_type", "")
|
| 307 |
+
text = gen_long_think(r["context"], r["query"]) if task_type in LONG_THINK_TASKS \
|
| 308 |
+
else gen_baseline(r["context"], r["query"])
|
| 309 |
+
|
| 310 |
+
n = n_expected(r["query"])
|
| 311 |
+
ans = parse_answers(text, task_type, n)
|
| 312 |
+
if not ans: ans = [""]
|
| 313 |
+
|
| 314 |
+
expl = re.sub(r"\s+", " ", text[:300]).strip() or "derived from the examples"
|
| 315 |
+
rows.append({"id":r["id"], "pred":json.dumps(ans, ensure_ascii=False),
|
| 316 |
+
"explanation":expl})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
except Exception as e:
|
| 318 |
+
n = n_expected(r["query"]) or 1
|
| 319 |
+
rows.append({"id":r["id"], "pred":json.dumps([""]*n, ensure_ascii=False),
|
| 320 |
+
"explanation":"fallback"})
|
| 321 |
+
print("row error", r["id"], e, flush=True)
|
| 322 |
+
|
| 323 |
done.add(r["id"]); write_csv(rows)
|
| 324 |
+
elapsed = time.time() - start
|
| 325 |
+
print(f"{len(rows)}/{len(df)} t={elapsed:.0f}s task={task_type}", flush=True)
|
| 326 |
+
if elapsed > TIME_LIMIT - SAFETY_S:
|
| 327 |
+
print("time up, stopping", flush=True); break
|
| 328 |
+
|
| 329 |
+
for _, r in df.iterrows():
|
| 330 |
if r["id"] in done: continue
|
| 331 |
+
n = n_expected(r["query"]) or 1
|
| 332 |
+
rows.append({"id":r["id"], "pred":json.dumps([""]*n, ensure_ascii=False),
|
| 333 |
+
"explanation":"fallback"})
|
| 334 |
+
write_csv(rows); print("DONE", flush=True)
|
| 335 |
except Exception as e:
|
| 336 |
+
emergency(f"main loop: {e}"); print("FATAL", e, flush=True)
|