Instructions to use ShayanShamsi/IOL-AI-v2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ShayanShamsi/IOL-AI-v2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="ShayanShamsi/IOL-AI-v2") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("ShayanShamsi/IOL-AI-v2") model = AutoModelForCausalLM.from_pretrained("ShayanShamsi/IOL-AI-v2", 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 ShayanShamsi/IOL-AI-v2 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "ShayanShamsi/IOL-AI-v2" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ShayanShamsi/IOL-AI-v2", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/ShayanShamsi/IOL-AI-v2
- SGLang
How to use ShayanShamsi/IOL-AI-v2 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 "ShayanShamsi/IOL-AI-v2" \ --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": "ShayanShamsi/IOL-AI-v2", "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 "ShayanShamsi/IOL-AI-v2" \ --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": "ShayanShamsi/IOL-AI-v2", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use ShayanShamsi/IOL-AI-v2 with Docker Model Runner:
docker model run hf.co/ShayanShamsi/IOL-AI-v2
Upload script.py with huggingface_hub
Browse files
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()
|