Instructions to use janPaje/iolai-gemma4-hybrid with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use janPaje/iolai-gemma4-hybrid with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="janPaje/iolai-gemma4-hybrid")# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("janPaje/iolai-gemma4-hybrid") model = AutoModelForMultimodalLM.from_pretrained("janPaje/iolai-gemma4-hybrid", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use janPaje/iolai-gemma4-hybrid with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "janPaje/iolai-gemma4-hybrid" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "janPaje/iolai-gemma4-hybrid", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/janPaje/iolai-gemma4-hybrid
- SGLang
How to use janPaje/iolai-gemma4-hybrid 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 "janPaje/iolai-gemma4-hybrid" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "janPaje/iolai-gemma4-hybrid", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'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 "janPaje/iolai-gemma4-hybrid" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "janPaje/iolai-gemma4-hybrid", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use janPaje/iolai-gemma4-hybrid with Docker Model Runner:
docker model run hf.co/janPaje/iolai-gemma4-hybrid
Upload folder using huggingface_hub
Browse files- README.md +36 -0
- numeral_solver.py +167 -0
- script.py +245 -0
README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: transformers
|
| 4 |
+
base_model: google/gemma-4-12B
|
| 5 |
+
pipeline_tag: text-generation
|
| 6 |
+
tags:
|
| 7 |
+
- gemma4
|
| 8 |
+
- iol-ai-2026
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# IOL-AI 2026 β gemma4:12b hybrid (symbolic numeral solver + budget-managed LLM)
|
| 12 |
+
|
| 13 |
+
Three-pass pipeline:
|
| 14 |
+
|
| 15 |
+
1. **Symbolic pass** β `numeral_solver.py` brute-forces numeral systems
|
| 16 |
+
(base, word values, word-order convention) from the problem's own
|
| 17 |
+
examples and answers `text_to_num` / `num_to_text` exactly when a
|
| 18 |
+
consistent system exists; otherwise falls through to the LLM.
|
| 19 |
+
2. **Baseline pass** β a fast low-token answer for every remaining
|
| 20 |
+
problem, with `submission.csv` atomically checkpointed after every row.
|
| 21 |
+
3. **Upgrade pass** β per-row time-sliced reasoning with per-task-type
|
| 22 |
+
method prompts; a baseline answer is replaced only when the upgrade
|
| 23 |
+
parses to the correct number of answers. Translation reasoning is
|
| 24 |
+
deliberately capped low: on gemma4-12B, truncated reasoning plus a
|
| 25 |
+
forced short answer scored measurably higher than completed reasoning.
|
| 26 |
+
|
| 27 |
+
Model: gemma4-12B (Apache 2.0), weights shipped in this repo, loaded 4-bit
|
| 28 |
+
via bitsandbytes at startup (fp16 12B does not fit the 16 GB T4).
|
| 29 |
+
|
| 30 |
+
## Upload checklist (before submitting)
|
| 31 |
+
|
| 32 |
+
1. Put the gemma4-12B weight files (safetensors + config + tokenizer) in
|
| 33 |
+
the repo root so `script.py` loads them from `"."`.
|
| 34 |
+
2. Repo must be **public** at submission time.
|
| 35 |
+
3. Submit via the competition Space; the platform mounts the hidden test
|
| 36 |
+
set at `/tmp/data/test.csv` and runs `script.py`.
|
numeral_solver.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Constraint solver for IOL-style numeral problems.
|
| 3 |
+
|
| 4 |
+
Given example lines "word word = value", searches over (base, word values,
|
| 5 |
+
combination convention) for a system that reproduces every example, then
|
| 6 |
+
answers queries in either direction (words -> digits, digits -> words).
|
| 7 |
+
|
| 8 |
+
Grammar family searched: value words for units (1..base-1) and powers
|
| 9 |
+
(base, base^2, base^3); a numeral is a sequence of groups read left to
|
| 10 |
+
right, each group = [unit multiplier] power, plus an optional trailing
|
| 11 |
+
unit addend. Two conventions tried: multiplier-before-power ("two ten"=20,
|
| 12 |
+
"ten two"=12) and power-before-multiplier (the reverse).
|
| 13 |
+
|
| 14 |
+
Returns None rather than guessing when no consistent system exists, so a
|
| 15 |
+
caller can fall back to an LLM. Standalone test:
|
| 16 |
+
python3 numeral_solver.py # runs against data/dev.csv
|
| 17 |
+
"""
|
| 18 |
+
import itertools
|
| 19 |
+
import re
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def parse_examples(context):
|
| 23 |
+
"""Extract (tokens, value) pairs from lines like 'lo tem = 10'."""
|
| 24 |
+
ex = []
|
| 25 |
+
for line in context.splitlines():
|
| 26 |
+
m = re.match(r"^\s*([^\d=]+?)\s*=\s*(\d+)\s*$", line)
|
| 27 |
+
if m:
|
| 28 |
+
ex.append((tuple(m.group(1).split()), int(m.group(2))))
|
| 29 |
+
return ex
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def parse_value(tokens, vals, base, mult_first):
|
| 33 |
+
"""Parse a numeral under one convention; None if malformed."""
|
| 34 |
+
powers = {base, base * base, base ** 3}
|
| 35 |
+
total, i = 0, 0
|
| 36 |
+
prev_power = None
|
| 37 |
+
while i < len(tokens):
|
| 38 |
+
v = vals.get(tokens[i])
|
| 39 |
+
if v is None:
|
| 40 |
+
return None
|
| 41 |
+
if v in powers:
|
| 42 |
+
total += v
|
| 43 |
+
prev_power = v
|
| 44 |
+
i += 1
|
| 45 |
+
else: # unit word
|
| 46 |
+
if mult_first and i + 1 < len(tokens) and \
|
| 47 |
+
vals.get(tokens[i + 1]) in powers:
|
| 48 |
+
p = vals[tokens[i + 1]]
|
| 49 |
+
if prev_power is not None and p >= prev_power:
|
| 50 |
+
return None # powers must descend
|
| 51 |
+
total += v * p
|
| 52 |
+
prev_power = p
|
| 53 |
+
i += 2
|
| 54 |
+
elif not mult_first and prev_power is not None and i == len(tokens) - 1:
|
| 55 |
+
total += v
|
| 56 |
+
i += 1
|
| 57 |
+
elif not mult_first and i > 0 and vals.get(tokens[i - 1]) in powers:
|
| 58 |
+
# power-first: unit right after a power multiplies it
|
| 59 |
+
p = vals[tokens[i - 1]]
|
| 60 |
+
total += v * p - p # power already added once
|
| 61 |
+
i += 1
|
| 62 |
+
elif i == len(tokens) - 1:
|
| 63 |
+
total += v # trailing addend
|
| 64 |
+
i += 1
|
| 65 |
+
else:
|
| 66 |
+
return None
|
| 67 |
+
return total
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def solve(context):
|
| 71 |
+
"""Find (base, vals, mult_first) consistent with every example."""
|
| 72 |
+
examples = parse_examples(context)
|
| 73 |
+
if len(examples) < 3:
|
| 74 |
+
return None
|
| 75 |
+
words = sorted({w for toks, _ in examples for w in toks})
|
| 76 |
+
|
| 77 |
+
# words pinned by single-token examples
|
| 78 |
+
pinned = {toks[0]: v for toks, v in examples if len(toks) == 1}
|
| 79 |
+
free = [w for w in words if w not in pinned]
|
| 80 |
+
max_val = max(v for _, v in examples)
|
| 81 |
+
|
| 82 |
+
for base in range(3, 31):
|
| 83 |
+
if base * base > max_val * base:
|
| 84 |
+
break
|
| 85 |
+
powers = [base, base * base, base ** 3]
|
| 86 |
+
cand = [v for v in list(range(1, base)) + powers if v <= max_val * 2]
|
| 87 |
+
if any(v >= base and v not in powers for v in pinned.values()):
|
| 88 |
+
continue
|
| 89 |
+
if len(free) > 3:
|
| 90 |
+
continue # search would explode
|
| 91 |
+
for combo in itertools.product(cand, repeat=len(free)):
|
| 92 |
+
vals = dict(pinned)
|
| 93 |
+
vals.update(zip(free, combo))
|
| 94 |
+
if len(set(vals.values())) != len(vals):
|
| 95 |
+
continue
|
| 96 |
+
for mult_first in (True, False):
|
| 97 |
+
if all(parse_value(t, vals, base, mult_first) == v
|
| 98 |
+
for t, v in examples):
|
| 99 |
+
return base, vals, mult_first
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def render(value, base, vals, mult_first):
|
| 104 |
+
"""Compose the numeral words for a value under a solved system."""
|
| 105 |
+
inv = {v: w for w, v in vals.items()}
|
| 106 |
+
parts = []
|
| 107 |
+
for p in (base ** 3, base * base, base):
|
| 108 |
+
if p in inv:
|
| 109 |
+
mult, value = divmod(value, p)
|
| 110 |
+
if mult == 0:
|
| 111 |
+
continue
|
| 112 |
+
if mult == 1:
|
| 113 |
+
parts.append(inv[p])
|
| 114 |
+
elif mult in inv:
|
| 115 |
+
pair = [inv[mult], inv[p]]
|
| 116 |
+
parts.extend(pair if mult_first else pair[::-1])
|
| 117 |
+
else:
|
| 118 |
+
return None
|
| 119 |
+
if value:
|
| 120 |
+
if value not in inv:
|
| 121 |
+
return None
|
| 122 |
+
parts.append(inv[value])
|
| 123 |
+
return " ".join(parts) if parts else None
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def answer(context, query, task_type):
|
| 127 |
+
"""Answer all items, or None if the system can't be solved."""
|
| 128 |
+
solved = solve(context)
|
| 129 |
+
if not solved:
|
| 130 |
+
return None
|
| 131 |
+
base, vals, mult_first = solved
|
| 132 |
+
items = re.findall(r"^\s*\d+[.)]\s*(.+)$", query, re.M)
|
| 133 |
+
out = []
|
| 134 |
+
for item in items:
|
| 135 |
+
item = item.strip()
|
| 136 |
+
if task_type == "text_to_num":
|
| 137 |
+
v = parse_value(tuple(item.split()), vals, base, mult_first)
|
| 138 |
+
out.append(str(v) if v is not None else None)
|
| 139 |
+
else:
|
| 140 |
+
m = re.search(r"\d+", item)
|
| 141 |
+
out.append(render(int(m.group()), base, vals, mult_first)
|
| 142 |
+
if m else None)
|
| 143 |
+
return out if all(a is not None for a in out) else None
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
if __name__ == "__main__":
|
| 147 |
+
import csv
|
| 148 |
+
import json
|
| 149 |
+
gold = json.load(open("data/dev_answers.json", encoding="utf-8"))
|
| 150 |
+
total = solved_ok = items_right = items_total = 0
|
| 151 |
+
for r in csv.DictReader(open("data/dev.csv", newline="", encoding="utf-8")):
|
| 152 |
+
if r["task_type"] not in ("text_to_num", "num_to_text"):
|
| 153 |
+
continue
|
| 154 |
+
total += 1
|
| 155 |
+
ans = answer(r["context"], r["query"], r["task_type"])
|
| 156 |
+
g = gold[r["id"]]
|
| 157 |
+
items_total += len(g)
|
| 158 |
+
if ans is None:
|
| 159 |
+
print(f"{r['id']} {r['task_type']:12} UNSOLVED")
|
| 160 |
+
continue
|
| 161 |
+
right = sum(a == b for a, b in zip(ans, g))
|
| 162 |
+
items_right += right
|
| 163 |
+
solved_ok += 1
|
| 164 |
+
flag = "" if right == len(g) else f" <-- {ans} vs {g}"
|
| 165 |
+
print(f"{r['id']} {r['task_type']:12} solved, {right}/{len(g)} items{flag}")
|
| 166 |
+
print(f"\nsolved {solved_ok}/{total} problems, "
|
| 167 |
+
f"{items_right}/{items_total} items exactly right")
|
script.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IOL-AI 2026 submission β v5 hybrid (solver + budget-managed gemma4:12b).
|
| 2 |
+
|
| 3 |
+
Everything here was validated on a local dev set (see README):
|
| 4 |
+
pass 0 symbolic numeral solver (numeral_solver.py, shipped in this repo);
|
| 5 |
+
answers text_to_num / num_to_text exactly when the system fits the
|
| 6 |
+
searched grammar family, returns None -> LLM fallback otherwise
|
| 7 |
+
pass 1 fast low-token answer for every remaining problem, submission.csv
|
| 8 |
+
atomically checkpointed after every row (a kill never leaves an
|
| 9 |
+
invalid/partial file)
|
| 10 |
+
pass 2 per-row time-sliced re-solve with reasoning; overwrites baseline
|
| 11 |
+
only if the result parses to the right number of answers; translation
|
| 12 |
+
reasoning is deliberately capped LOW (truncated reasoning + a forced
|
| 13 |
+
short answer scored higher than completed reasoning on gemma4:12b)
|
| 14 |
+
|
| 15 |
+
Repo layout expected: this file as script.py, numeral_solver.py beside it,
|
| 16 |
+
gemma4:12b weights in the repo root (load from "."). fp16 12B does not fit a
|
| 17 |
+
16 GB T4, so weights load 4-bit via bitsandbytes (the organizer-reference
|
| 18 |
+
recipe).
|
| 19 |
+
"""
|
| 20 |
+
import os
|
| 21 |
+
import subprocess
|
| 22 |
+
import sys
|
| 23 |
+
import time
|
| 24 |
+
|
| 25 |
+
T0 = time.monotonic()
|
| 26 |
+
TIME_LIMIT = 30 * 60
|
| 27 |
+
DEADLINE = T0 + TIME_LIMIT - 3 * 60 # 3-min reserve for writes/exit
|
| 28 |
+
|
| 29 |
+
# Smoke mode (local CPU shakeout, mirrors the leader's IOL_DUMMY pattern):
|
| 30 |
+
# IOL_SMOKE=1 skip bitsandbytes (no CUDA), load fp32 on CPU
|
| 31 |
+
# IOL_MODEL_ID=... substitute a tiny model for the shipped weights
|
| 32 |
+
# IOL_INPUT=... read a local CSV instead of /tmp/data/test.csv
|
| 33 |
+
SMOKE = os.environ.get("IOL_SMOKE", "0") == "1"
|
| 34 |
+
|
| 35 |
+
deps = ["transformers>=4.51", "accelerate>=0.30", "torch>=2.2", "pandas"]
|
| 36 |
+
if not SMOKE:
|
| 37 |
+
deps.append("bitsandbytes")
|
| 38 |
+
subprocess.run([sys.executable, "-m", "pip", "install", "-q", *deps], check=True)
|
| 39 |
+
|
| 40 |
+
import json
|
| 41 |
+
import re
|
| 42 |
+
|
| 43 |
+
import pandas as pd
|
| 44 |
+
import torch
|
| 45 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 46 |
+
|
| 47 |
+
import numeral_solver
|
| 48 |
+
|
| 49 |
+
MODEL_ID = os.environ.get("IOL_MODEL_ID", ".")
|
| 50 |
+
INPUT_CSV = os.environ.get("IOL_INPUT", "/tmp/data/test.csv")
|
| 51 |
+
MARKER = "FINAL ANSWERS"
|
| 52 |
+
TRANSLATION_CAP = 3000 # tokens; see module docstring
|
| 53 |
+
GLOBAL_CAP = 8192
|
| 54 |
+
|
| 55 |
+
BASE_RULES = (
|
| 56 |
+
f"You solve International Linguistics Olympiad problems. Your reasoning budget "
|
| 57 |
+
f"is limited, so be systematic and compact β do not second-guess a hypothesis "
|
| 58 |
+
f"that fits all the data. Verify against every given example once, then commit. "
|
| 59 |
+
f"End with the line '{MARKER}:' followed by one answer per line, in item order, "
|
| 60 |
+
f"with no numbering and no extra text."
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
NUMERAL_CORE = (
|
| 64 |
+
"1) For EVERY example, write one arithmetic equation showing exactly how its "
|
| 65 |
+
"words produce its value. 2) Determine the base from the single-word values. "
|
| 66 |
+
"3) CRITICAL: find pairs of examples using the same words in different orders "
|
| 67 |
+
"with different values β decide which order means multiplication and which "
|
| 68 |
+
"means addition. 4) Only after your equations reproduce ALL examples, "
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
STRATEGIES = {
|
| 72 |
+
"translation": (
|
| 73 |
+
"Method: 1) Align the given sentence pairs and segment every word into "
|
| 74 |
+
"morphemes by comparing entries that share meaning components. 2) Write a "
|
| 75 |
+
"compact table: each root, prefix, and suffix with its meaning, plus the "
|
| 76 |
+
"morpheme order. 3) Compose each requested item from the table. Mind the "
|
| 77 |
+
"direction of translation asked for."
|
| 78 |
+
),
|
| 79 |
+
"text_to_num": "Method: " + NUMERAL_CORE + "convert each item to digits.",
|
| 80 |
+
"num_to_text": "Method: " + NUMERAL_CORE + "compose each requested number in "
|
| 81 |
+
"the puzzle language.",
|
| 82 |
+
"fill_blanks": (
|
| 83 |
+
"Method: deduce the paradigm from the completed cells, state the rule for "
|
| 84 |
+
"each row/column, then fill each blank consistently with it."
|
| 85 |
+
),
|
| 86 |
+
"match_letters": (
|
| 87 |
+
"Method: find anchor items you can pair with certainty first (word length, "
|
| 88 |
+
"repeated letters), then use elimination. Every item gets exactly one match."
|
| 89 |
+
),
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
SALVAGE = (
|
| 93 |
+
"Time is up. Based on the partial analysis below, give your best answer for "
|
| 94 |
+
"all {n} items RIGHT NOW: one answer per line, in order, no numbering, no "
|
| 95 |
+
"other text, no further reasoning.\n\nThe items to answer:\n{query}\n\n"
|
| 96 |
+
"Partial analysis:\n{tail}"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def remaining():
|
| 101 |
+
return DEADLINE - time.monotonic()
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
tok = AutoTokenizer.from_pretrained(MODEL_ID, local_files_only=not SMOKE)
|
| 105 |
+
if SMOKE:
|
| 106 |
+
model = AutoModelForCausalLM.from_pretrained(MODEL_ID).eval()
|
| 107 |
+
else:
|
| 108 |
+
from transformers import BitsAndBytesConfig
|
| 109 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 110 |
+
MODEL_ID,
|
| 111 |
+
quantization_config=BitsAndBytesConfig(
|
| 112 |
+
load_in_4bit=True, bnb_4bit_quant_type="nf4",
|
| 113 |
+
bnb_4bit_compute_dtype=torch.float16),
|
| 114 |
+
device_map="auto", local_files_only=True,
|
| 115 |
+
).eval()
|
| 116 |
+
print(f"[model] loaded at {time.monotonic()-T0:.0f}s", flush=True)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def generate(messages, max_new, max_time=None):
|
| 120 |
+
"""Greedy generation. Returns (text, truncated, tokens_per_sec)."""
|
| 121 |
+
enc = tok.apply_chat_template(
|
| 122 |
+
messages, add_generation_prompt=True, return_tensors="pt",
|
| 123 |
+
)
|
| 124 |
+
if hasattr(enc, "keys"): # BatchEncoding on newer transformers
|
| 125 |
+
inputs = {k: v.to(model.device) for k, v in enc.items()}
|
| 126 |
+
else: # bare tensor on older versions
|
| 127 |
+
inputs = {"input_ids": enc.to(model.device)}
|
| 128 |
+
prompt_len = inputs["input_ids"].shape[-1]
|
| 129 |
+
kw = {"max_new_tokens": max_new, "do_sample": False,
|
| 130 |
+
"pad_token_id": tok.eos_token_id}
|
| 131 |
+
if max_time:
|
| 132 |
+
kw["max_time"] = max_time
|
| 133 |
+
t0 = time.monotonic()
|
| 134 |
+
with torch.no_grad():
|
| 135 |
+
out = model.generate(**inputs, **kw)
|
| 136 |
+
n_new = out.shape[-1] - prompt_len
|
| 137 |
+
tps = n_new / max(time.monotonic() - t0, 1e-6)
|
| 138 |
+
text = tok.decode(out[0][prompt_len:], skip_special_tokens=True).strip()
|
| 139 |
+
return text, n_new >= max_new, tps
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def item_count(query):
|
| 143 |
+
return len(re.findall(r"^\s*\d+[.)]", query, re.M)) or 1
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def extract_answers(text, n):
|
| 147 |
+
m = re.search(rf"{MARKER}\s*:?", text, re.I)
|
| 148 |
+
block = text[m.end():] if m else text
|
| 149 |
+
lines = [ln.strip() for ln in block.splitlines() if ln.strip()]
|
| 150 |
+
if not m:
|
| 151 |
+
lines = lines[-n:]
|
| 152 |
+
lines = [re.sub(r"^\d+[.)]\s*", "", ln) for ln in lines]
|
| 153 |
+
return (lines + [""] * n)[:n]
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def valid(answers, n):
|
| 157 |
+
return len(answers) == n and all(answers)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def checkpoint(rows):
|
| 161 |
+
tmp = ".submission.csv.tmp"
|
| 162 |
+
pd.DataFrame(rows, columns=["id", "pred"]).to_csv(tmp, index=False)
|
| 163 |
+
os.replace(tmp, "submission.csv")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def build_messages(r):
|
| 167 |
+
system = BASE_RULES + "\n\n" + STRATEGIES.get(r["task_type"], "")
|
| 168 |
+
return [
|
| 169 |
+
{"role": "system", "content": system},
|
| 170 |
+
{"role": "user", "content": f"{r['context'].strip()}\n\n{r['query'].strip()}"},
|
| 171 |
+
]
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
df = pd.read_csv(INPUT_CSV, dtype=str).fillna("")
|
| 175 |
+
problems = [r for _, r in df.iterrows()]
|
| 176 |
+
counts = [item_count(r["query"]) for r in problems]
|
| 177 |
+
rows = [{"id": r["id"], "pred": json.dumps([""] * counts[i])}
|
| 178 |
+
for i, r in enumerate(problems)]
|
| 179 |
+
checkpoint(rows)
|
| 180 |
+
|
| 181 |
+
# ββ pass 0: symbolic numeral solver ββββββββββββββββββββββββββββββββββββββββββ
|
| 182 |
+
solved = [False] * len(problems)
|
| 183 |
+
for i, r in enumerate(problems):
|
| 184 |
+
if r["task_type"] in ("text_to_num", "num_to_text"):
|
| 185 |
+
try:
|
| 186 |
+
ans = numeral_solver.answer(r["context"], r["query"], r["task_type"])
|
| 187 |
+
except Exception as exc:
|
| 188 |
+
print(f"[solver] {r['id']} error: {exc}", flush=True)
|
| 189 |
+
ans = None
|
| 190 |
+
if ans is not None:
|
| 191 |
+
rows[i]["pred"] = json.dumps(ans, ensure_ascii=False)
|
| 192 |
+
solved[i] = True
|
| 193 |
+
print(f"[solver] {r['id']} solved", flush=True)
|
| 194 |
+
checkpoint(rows)
|
| 195 |
+
|
| 196 |
+
# ββ pass 1: fast complete baseline βββββββββββββββββββββββββββββββββββββββββββ
|
| 197 |
+
tps_est = 20.0
|
| 198 |
+
for i, r in enumerate(problems):
|
| 199 |
+
if solved[i] or remaining() < 60:
|
| 200 |
+
continue
|
| 201 |
+
text, _, tps_est = generate(build_messages(r), max_new=48 * counts[i] + 128,
|
| 202 |
+
max_time=min(90.0, remaining() / 4))
|
| 203 |
+
rows[i]["pred"] = json.dumps(extract_answers(text, counts[i]),
|
| 204 |
+
ensure_ascii=False)
|
| 205 |
+
checkpoint(rows)
|
| 206 |
+
print(f"[base {i+1}/{len(problems)}] done at {time.monotonic()-T0:.0f}s",
|
| 207 |
+
flush=True)
|
| 208 |
+
|
| 209 |
+
# ββ pass 2: time-sliced reasoning upgrades βββββββββββββββββββββββββββββββββββ
|
| 210 |
+
for i, r in enumerate(problems):
|
| 211 |
+
if solved[i]:
|
| 212 |
+
continue
|
| 213 |
+
n = counts[i]
|
| 214 |
+
if remaining() < 30:
|
| 215 |
+
print("[deadline] stopping upgrades", flush=True)
|
| 216 |
+
break
|
| 217 |
+
todo = sum(1 for j in range(i, len(problems)) if not solved[j])
|
| 218 |
+
slice_s = max(remaining() / max(todo, 1), 10.0)
|
| 219 |
+
max_new = int(min(max(slice_s * 0.8 * tps_est, 512), GLOBAL_CAP))
|
| 220 |
+
if r["task_type"] == "translation":
|
| 221 |
+
max_new = min(max_new, TRANSLATION_CAP)
|
| 222 |
+
|
| 223 |
+
text, truncated, tps_est = generate(build_messages(r), max_new,
|
| 224 |
+
max_time=slice_s * 0.8)
|
| 225 |
+
upgraded = extract_answers(text, n) if text else []
|
| 226 |
+
|
| 227 |
+
if not valid(upgraded, n) and remaining() > 20:
|
| 228 |
+
msgs = [
|
| 229 |
+
{"role": "system", "content": "Output only the answers, one per line."},
|
| 230 |
+
{"role": "user", "content": SALVAGE.format(
|
| 231 |
+
n=n, query=r["query"].strip(), tail=text[-3000:])},
|
| 232 |
+
]
|
| 233 |
+
text, _, _ = generate(msgs, max_new=48 * n + 64,
|
| 234 |
+
max_time=min(60.0, remaining()))
|
| 235 |
+
upgraded = extract_answers(text, n)
|
| 236 |
+
|
| 237 |
+
if valid(upgraded, n):
|
| 238 |
+
rows[i]["pred"] = json.dumps(upgraded, ensure_ascii=False)
|
| 239 |
+
checkpoint(rows)
|
| 240 |
+
print(f"[think {i+1}/{len(problems)}] upgraded", flush=True)
|
| 241 |
+
else:
|
| 242 |
+
print(f"[think {i+1}/{len(problems)}] kept baseline", flush=True)
|
| 243 |
+
|
| 244 |
+
checkpoint(rows)
|
| 245 |
+
print(f"wrote submission.csv at {time.monotonic()-T0:.0f}s", flush=True)
|