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
Download inference.py from SinhalaMMLU-SharedTask/team-sinbotics: direct link, hf CLI and curl.
- Browser
- Download file 10 kB
-
https://huggingface.co/SinhalaMMLU-SharedTask/team-sinbotics/resolve/main/inference.py
- Command line
-
hf download hf://SinhalaMMLU-SharedTask/team-sinbotics/inference.py
-
curl -L -o inference.py https://huggingface.co/SinhalaMMLU-SharedTask/team-sinbotics/resolve/main/inference.py
10 kB
| """ | |
| 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) | |