Instructions to use SinhalaMMLU-SharedTask/team-sinbotics with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use SinhalaMMLU-SharedTask/team-sinbotics with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 10,013 Bytes
7a2335a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | """
Sinhala MMLU Shared Task - Final inference script
Team: SinBotics
The organizers will run this file exactly as it is, changing ONLY the DATA_REPO line
below to point to the final Sinhala MMLU dataset:
pip install -r requirements.txt
python inference.py
System: Qwen/Qwen3.5-4B + our LoRA adapter (rank 16, trained on Global-MMLU Sinhala + sinmulu_si), merged in bf16.
Each question gets the "cot_en_role_think" prompt: an expert Sri Lankan teacher of the subject, asked to think step
by step in English, with Qwen's thinking mode on, greedy decoding, at most 4,096 new tokens. The answer is then read
as the most probable of the digit tokens 1/2/3/4 after the reasoning + "\n\nAnswer: " (any stated answer is cut and
asked for again, so every question gets exactly one of 1-4). Deterministic: greedy decoding, fixed seed.
This is a self-contained copy of the code path that produced our final Codabench file
(training/sinmmlu: prompts.py cot_en_role_think, model.py LM.generate / label_logprobs_text, evaluate.py run_cot),
with the same batch sizes and length-sorted batching. Run time: about 5 h for 2,238 questions on one
NVIDIA A100 40 GB (generation batch 48 needs about 24 GB; on a CUDA out-of-memory a batch is split in halves).
"""
# ======================= ORGANIZERS CHANGE ONLY THIS LINE =======================
DATA_REPO = "SinhalaMMLU-SharedTask/Sinhala-MMLU-SharedTask-Test-Dataset"
# ================================================================================
OUTPUT_FILE = "predictions.jsonl"
# ----------------------- TEAM SETTINGS (keep exactly as used for your final submission)
BASE_MODEL = "Qwen/Qwen3.5-4B"
BASE_REVISION = "851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a" # released 2026-02-27, Apache-2.0
MODEL_ID = "SinhalaMMLU-SharedTask/team-sinbotics" # this repo; our LoRA adapter is in its adapter/ folder
ADAPTER_SUBFOLDER = "adapter"
SEED = 42
GEN_BATCH = 48 # questions per generation batch (final run: 48 on an A100 40 GB)
SCORE_BATCH = 16 # questions per answer-scoring batch
MAX_NEW_TOKENS = 4096 # reasoning (thinking) length limit
FIXED_DATE = "26 Jul 2024" # passed to the chat template so the prompt never depends on the run date
import json
import os
import random
import re
import unicodedata
import numpy as np
import torch
from huggingface_hub import HfFileSystem, list_repo_files
def set_seed(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
# =============== 1. LOAD THE DATASET FROM HUGGING FACE (do not change) ===============
def load_questions(data_repo):
fs = HfFileSystem()
files = sorted(f for f in list_repo_files(data_repo, repo_type="dataset") if f.endswith(".json"))
rows = []
for f in files:
with fs.open(f"datasets/{data_repo}/{f}", "r", encoding="utf-8") as fh:
raw = json.load(fh)
if isinstance(raw, dict):
raw = next(v for v in raw.values() if isinstance(v, list))
for r in raw:
r["_file"] = f
rows.append(r)
return rows
def get_level(r):
return (r.get("metadata") or {}).get("difficulty") or r["_file"].split("/")[0]
# =============== 2. YOUR SYSTEM ===============
ZWNJ = ""
ANSWER_RE = re.compile(r"(?:පිළිතුර|Answer)\s*[::]?\s*\**\s*([1-4])")
LABELS = ("1", "2", "3", "4")
CUE = "\n\nAnswer: "
def norm_model(text):
"""NFC, drop ZWNJ only (keep ZWJ, needed for conjuncts such as ප්ර), tidy spaces, keep newlines."""
text = unicodedata.normalize("NFC", text).replace(ZWNJ, "")
text = re.sub(r"[^\S\n]+", " ", text)
text = re.sub(r" ?\n ?", "\n", text)
return re.sub(r"\n{3,}", "\n\n", text).strip()
def subject_en(r):
return " ".join(r["subject"].replace("_", " ").split())
def make_prompt(r):
"""cot_en_role_think (training/sinmmlu/prompts.py): teacher role + subject + English step-by-step instruction."""
s = subject_en(r)
opts = "\n".join(f"{l}. {norm_model(c)}" for l, c in zip(LABELS, r["choices"]))
return (f"You are an expert Sri Lankan teacher of {s}.\n"
f"The following is a multiple choice question in Sinhala about {s}.\n"
"Think step by step in English, then choose the correct or most suitable answer from the options 1, 2, 3, 4.\n"
"End your reply with \"Answer: <number>\".\n"
f"\nQuestion: {norm_model(r['question'])}\n{opts}")
def load_system():
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
tok = AutoTokenizer.from_pretrained(BASE_MODEL, revision=BASE_REVISION)
tok.padding_side = "left"
if tok.pad_token is None:
tok.pad_token = tok.eos_token
dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float32
model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, revision=BASE_REVISION, dtype=dtype,
device_map="auto" if torch.cuda.is_available() else None)
model = PeftModel.from_pretrained(model, MODEL_ID, subfolder=ADAPTER_SUBFOLDER).merge_and_unload() # adds no parameters
model.eval()
n = sum(p.numel() for p in model.parameters())
print(f"model: {BASE_MODEL}@{BASE_REVISION[:8]} + {MODEL_ID}/{ADAPTER_SUBFOLDER} merged, {n:,} parameters, {dtype}", flush=True)
return tok, model
def render(tok, r):
msgs = [{"role": "user", "content": make_prompt(r)}]
return tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, date_string=FIXED_DATE,
enable_thinking=True)
def length_sorted_batches(ids_all, size):
order = sorted(range(len(ids_all)), key=lambda i: len(ids_all[i]))
return [order[b:b + size] for b in range(0, len(order), size)]
def oom_safe(fn, idx):
"""fn(idx) on one batch; on CUDA out-of-memory the batch is split in halves and retried."""
try:
fn(idx)
except torch.cuda.OutOfMemoryError:
if len(idx) == 1:
raise
torch.cuda.empty_cache()
print(f"out of memory on a batch of {len(idx)}: splitting", flush=True)
oom_safe(fn, idx[:len(idx) // 2])
oom_safe(fn, idx[len(idx) // 2:])
def generate(tok, model, texts):
"""Greedy reasoning for every rendered prompt (length-sorted batches)."""
ids_all = tok(texts, add_special_tokens=False)["input_ids"]
out = [None] * len(ids_all)
batches = length_sorted_batches(ids_all, GEN_BATCH)
def run(idx):
enc = tok.pad({"input_ids": [ids_all[j] for j in idx]}, padding=True, return_tensors="pt").to(model.device)
g = model.generate(**enc, max_new_tokens=MAX_NEW_TOKENS, pad_token_id=tok.pad_token_id,
do_sample=False, temperature=None, top_p=None, top_k=None)
for row, j in enumerate(idx):
out[j] = tok.decode(g[row, enc["input_ids"].shape[1]:], skip_special_tokens=True).strip()
with torch.no_grad():
for b, idx in enumerate(batches, 1):
oom_safe(run, idx)
print(f"generate: batch {b}/{len(batches)}", flush=True)
return out
def label_token_ids(tok):
out = []
for d in LABELS:
ids = tok.encode(d, add_special_tokens=False)
assert len(ids) == 1, f"label {d!r} is not one token"
out.append(ids[0])
return out
def score_labels(tok, model, texts):
"""log P(label) of 1/2/3/4 as the next token after each text (left padding, positions from the mask)."""
lab = label_token_ids(tok)
ids_all = tok(texts, add_special_tokens=False)["input_ids"]
out = [None] * len(ids_all)
def run(idx):
enc = tok.pad({"input_ids": [ids_all[j] for j in idx]}, padding=True, return_tensors="pt").to(model.device)
pos = (enc["attention_mask"].cumsum(-1) - 1).clamp(min=0)
logits = model(**enc, position_ids=pos, logits_to_keep=1).logits[:, -1].float()
if not torch.isfinite(logits).all():
raise FloatingPointError("non-finite logits")
lp = torch.log_softmax(logits, dim=-1).cpu()
for j, row in zip(idx, lp):
out[j] = [row[v].item() for v in lab]
with torch.no_grad():
for idx in length_sorted_batches(ids_all, SCORE_BATCH):
oom_safe(run, idx)
return out
def predict_all(system, rows):
tok, model = system
texts = [render(tok, r) for r in rows]
gens = generate(tok, model, texts)
scored, n_stated = [], 0
for t, g in zip(texts, gens):
m = list(ANSWER_RE.finditer(g))
n_stated += bool(m)
reasoning = g[:m[-1].start()] if m else g # cut the stated answer; the cue asks for it again
if "</think>" not in reasoning: # the tag is a special token and is decoded away (or thinking was cut off)
reasoning = reasoning.rstrip() + "\n</think>"
scored.append(t + reasoning.rstrip() + CUE)
print(f"stated an answer: {n_stated}/{len(rows)}", flush=True)
lps = score_labels(tok, model, scored)
return [LABELS[int(np.argmax(lp))] for lp in lps]
# =============== 3. WRITE PREDICTIONS (do not change) ===============
if __name__ == "__main__":
set_seed(SEED)
rows = load_questions(DATA_REPO)
print(len(rows), "questions loaded from", DATA_REPO)
system = load_system()
answers = predict_all(system, rows)
assert len(answers) == len(rows), "predict_all() must return one answer per question"
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
for r, a in zip(rows, answers):
a = str(a).strip()
assert a in {"1", "2", "3", "4"}, f"invalid answer {a!r} for q_no={r.get('q_no')}"
f.write(json.dumps({
"q_no": r["q_no"],
"subject": r["subject"],
"category": r["category"],
"level": get_level(r),
"answer": a,
}, ensure_ascii=False, separators=(",", ":")) + "\n")
print("\nSaved", OUTPUT_FILE)
|