rita-cohere's picture
Notebook decode: prompt + T=0.6 force-close budgets
e03d464 verified
Raw
History Blame Contribute Delete
8.31 kB
"""IOL-AI 2026 — notebook decode (prompt + force-close + T=0.6).
Matches linguini-test-iolai.ipynb cells 4–5:
- USER_INSTRUCTIONS (+ COT + /think)
- think 2048 @ T=0.6; force <|END_THINKING|><|START_RESPONSE|>
- answer 512 @ T=0.6
- marker-gated FINAL ANSWERS: parser
"""
from __future__ import annotations
import os
import subprocess
import sys
def _install_bundled_deps() -> None:
wheels_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "wheels")
if not os.path.isdir(wheels_dir):
return
subprocess.run(
[
sys.executable,
"-m",
"pip",
"install",
"-q",
"--no-index",
f"--find-links={wheels_dir}",
"transformers==4.56.2",
],
check=True,
)
_install_bundled_deps()
os.environ["HF_HUB_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"
MODEL_ID = "."
import json
import re
import pandas as pd
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
SYSTEM = ""
USER_INSTRUCTIONS = """You solve International Linguistics Olympiad (IOL) problems from the data you are given.
You may see a task type you have never seen: follow the instruction and examples, and answer in the same form they use.
What to return by task type:
- translation: only the required form in the language the query asks for — do not add extra glosses or "form | meaning" unless asked
- fill_blanks: only the missing form for each blank — no extra glosses
- match_letters: ONLY the option letter (A, B, C, …), one letter per line — never copy option text, never arrows, never "A. word"
- text_to_num: the number in digits only
- num_to_text: the number written out in words, in the language asked
- kinship / sentence matching: the full required sentence or form — NOT roman numerals (i, ii, iii) and NOT an alphabet dump
- any other type: exactly what the instruction asks for, nothing else
Answer in the language and form the query asks for. Do not add glosses, translations, or explanations unless the instruction requires them.
Output rules:
- Put answers ONLY after a line that says exactly: FINAL ANSWERS:
- Never put answers before that marker.
- One answer per line; exactly as many lines as items asked in the query.
- Bare answers only: no numbering, no quotes, no commentary, no repeating the question.
Extra hard rules:
- Never refuse or apologize; always output FINAL ANSWERS: with your best guess.
- For match_letters: only bare letters (A, B, C, …) — never dump the alphabet (A B C D E F…), never option text.
- Never append tags or glosses: no "_GCY", "_NS", "form – meaning", "word - gloss", or markdown bold.
- Never write an essay or explanation of how the language works under FINAL ANSWERS: — only the answer strings.
- If the query asks for colour/color forms, output those forms only (one per line), not a linguistics write-up.
- Emit exactly as many answer lines as items asked — no more, no fewer."""
USER_INSTRUCTIONS_COT = (
USER_INSTRUCTIONS
+ "\n\nThink step by step about the rules in the examples and how they apply to the query, "
"then write FINAL ANSWERS: and the answer lines."
)
APPEND_THINK = True
THINK_BUDGET = 2048
ANSWER_BUDGET = 512
TEMPERATURE = 0.6
def build_user(context, query):
instructions = USER_INSTRUCTIONS_COT if APPEND_THINK else USER_INSTRUCTIONS
parts = [instructions.strip(), "", context.strip(), "", query.strip()]
if APPEND_THINK:
parts.append("/think")
return "\n".join(parts)
_SPECIAL = re.compile(
r"<\|START_RESPONSE\|>|<\|END_RESPONSE\|>|"
r"<\|/?END_OF_TURN_TOKEN\|>|<\|/?START_OF_TURN_TOKEN\|>|"
r"<\|CHATBOT_TOKEN\|>|<\|/?START_THINKING\|>|<\|/?END_THINKING\|>|"
r"<EOS_TOKEN>|<BOS_TOKEN>|<PAD>"
)
_MARKER = re.compile(r"(?im)\bfinal\s+answers?\b\s*:?\s*")
_TURN_NOISE = re.compile(
r"<\|/?END_OF_TURN_TOKEN\|>|<\|/?START_OF_TURN_TOKEN\|>|"
r"<\|CHATBOT_TOKEN\|>|<EOS_TOKEN>|<BOS_TOKEN>"
)
def _lines_after(region: str) -> list[str]:
answers = []
for line in region.splitlines():
line = _SPECIAL.sub("", line)
line = re.sub(r"^\s*(?:\(?\d+[.)]|[-*•])\s*", "", line).strip()
if not line:
if answers:
break
continue
if re.fullmatch(r"(?i)final\s+answers?\s*:?", line):
continue
if answers and (
len(line) > 120
or line.lower().startswith(("however", "but the", "to solve", "### ", "**"))
):
break
answers.append(line)
return answers
def parse_answers(text: str) -> list[str]:
raw = text
def _from_region(region: str) -> list[str] | None:
markers = list(_MARKER.finditer(region))
if not markers:
return None
for m in reversed(markers):
answers = _lines_after(region[m.end() :])
if answers:
return answers
return []
region = raw
if "<|END_THINKING|>" in region:
region = region.rsplit("<|END_THINKING|>", 1)[-1]
blocks = re.findall(
r"<\|START_RESPONSE\|>(.*?)<\|END_RESPONSE\|>", region, flags=re.S
)
if blocks:
region = blocks[-1]
found = _from_region(region)
if found is not None:
return found
found = _from_region(raw)
if found is not None:
return found
return []
def _build_prompt(tok, system: str, user: str):
messages = []
if system.strip():
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": user})
try:
return tok.apply_chat_template(
messages,
add_generation_prompt=True,
return_tensors="pt",
return_dict=True,
reasoning_options={"enabled": True},
)
except TypeError:
ids = tok.apply_chat_template(
messages, add_generation_prompt=True, return_tensors="pt"
)
return {"input_ids": ids, "attention_mask": torch.ones_like(ids)}
@torch.inference_mode()
def generate_with_budgets(model, tok, enc):
device = next(model.parameters()).device
enc = {k: v.to(device) if hasattr(v, "to") else v for k, v in enc.items()}
prompt_len = enc["input_ids"].shape[-1]
pad_id = tok.pad_token_id or tok.eos_token_id
end_ids = tok.encode("<|END_THINKING|>", add_special_tokens=False)
start_ids = tok.encode("<|START_RESPONSE|>", add_special_tokens=False)
out = model.generate(
**enc,
max_new_tokens=THINK_BUDGET,
do_sample=True,
temperature=TEMPERATURE,
eos_token_id=tok.eos_token_id,
pad_token_id=pad_id,
)
gen = out[0]
new_ids = gen[prompt_len:].tolist()
forced = False
finished = bool(new_ids) and new_ids[-1] == tok.eos_token_id
if end_ids[0] not in new_ids:
forced = True
cont = torch.tensor(
end_ids + start_ids, device=gen.device, dtype=gen.dtype
)
gen = torch.cat([gen, cont], dim=0)
finished = False
if not finished:
ids = gen.unsqueeze(0)
out2 = model.generate(
input_ids=ids,
attention_mask=torch.ones_like(ids),
max_new_tokens=ANSWER_BUDGET,
do_sample=True,
temperature=TEMPERATURE,
eos_token_id=tok.eos_token_id,
pad_token_id=pad_id,
)
gen = out2[0]
text = tok.decode(gen[prompt_len:], skip_special_tokens=False).strip()
return _TURN_NOISE.sub("", text).strip(), forced
tok = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID, torch_dtype=torch.float16, device_map="auto"
).eval()
df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
rows = []
for i, r in df.iterrows():
user = build_user(r["context"], r["query"])
enc = _build_prompt(tok, SYSTEM, user)
text, forced = generate_with_budgets(model, tok, enc)
answers = parse_answers(text)
rows.append({"id": r["id"], "pred": json.dumps(answers, ensure_ascii=False)})
pd.DataFrame(rows).to_csv("submission.csv", index=False)
print(f"[{i + 1}/{len(df)}] n={len(answers)} forced={forced}", flush=True)
print("wrote submission.csv", flush=True)