Download eval/run_eval.py from SinhalaMMLU-SharedTask/team-nexus: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/SinhalaMMLU-SharedTask/team-nexus/resolve/main/eval/run_eval.py
- Command line
-
hf download hf://SinhalaMMLU-SharedTask/team-nexus/eval/run_eval.py
-
curl -L -o run_eval.py https://huggingface.co/SinhalaMMLU-SharedTask/team-nexus/resolve/main/eval/run_eval.py
10.4 kB
| """Evaluate one model on one or more processed MCQ files. | |
| Example: | |
| python -m eval.run_eval --model Qwen/Qwen3-4B-Instruct-2507 --data sinhalammlu_dev gmmlu_si_val \ | |
| --method logit --n-perms 4 --run-name qwen3-4b-inst_logit | |
| Outputs results/<run-name>/{config.json, predictions.jsonl, metrics.json, report.md} | |
| and, with --write-submission, a Codabench-format predictions.zip per dataset. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import random | |
| import re | |
| import time | |
| import zipfile | |
| from pathlib import Path | |
| import yaml | |
| from .common import ROOT, cap_gpu_memory, env_fingerprint, load_env, read_jsonl, set_seed, write_jsonl | |
| from .metrics import compute_metrics, markdown_report | |
| from .prompts import PromptBuilder, user_text | |
| PROCESSED = ROOT / "data" / "processed" | |
| BASE_NAME = re.compile(r"(-base|-pt)$|^google/gemma-4-e[24]b$", re.I) | |
| def make_perm(n: int, k: int, mode: str, seed: int, uid: str) -> list[int]: | |
| """perm[j] = original 0-based index shown at display position j. k=0 is the original order.""" | |
| if k == 0: | |
| return list(range(n)) | |
| if mode == "identity": # repeated samples of the same prompt (self-consistency), no reordering | |
| return list(range(n)) | |
| if mode == "cyclic" and k < n: | |
| return [(j + k) % n for j in range(n)] | |
| rng = random.Random(f"{seed}|{uid}|{k}") | |
| p = list(range(n)) | |
| rng.shuffle(p) | |
| return p | |
| def resolve_data(name: str) -> Path: | |
| p = Path(name) | |
| return p if p.suffix == ".jsonl" else PROCESSED / f"{name}.jsonl" | |
| def score_items(items: dict[str, dict], pb: PromptBuilder, scorer, method: str, n_perms: int, | |
| perm_mode: str, seed: int, first_perm: int = 0) -> tuple[list[dict], str | None]: | |
| """One record per (question, option order). Shared by evaluation and the submission pipeline. | |
| Items without a gold answer (test data) get gold/correct = None.""" | |
| n_perms = 1 if method == "cloze" else n_perms | |
| jobs = [(uid, k, make_perm(len(it["options"]), k, perm_mode, seed, uid)) | |
| for uid, it in items.items() for k in range(first_perm, n_perms)] | |
| first_prompt = None | |
| if method == "cloze": | |
| res = scorer.cloze([pb.cloze_context(items[u]) for u, _, _ in jobs], [items[u]["options"] for u, _, _ in jobs]) | |
| else: | |
| prompts = [pb.mcq(items[u], [items[u]["options"][j] for j in perm]) for u, _, perm in jobs] | |
| fn = scorer.logit if method == "logit" else scorer.generate | |
| res = fn(prompts, [len(items[u]["options"]) for u, _, _ in jobs]) | |
| first_prompt = prompts[0] if prompts else None | |
| records = [] | |
| for (uid, k, perm), r in zip(jobs, res): | |
| gold = items[uid].get("answer") | |
| probs_orig = [0.0] * len(perm) | |
| for j, p in enumerate(r["probs"]): | |
| probs_orig[perm[j]] = p | |
| pred_orig = perm[r["pred"] - 1] + 1 if r["pred"] else None | |
| rec = {"uid": uid, "perm_k": k, "perm": perm, "method": method, | |
| "pred_display": r["pred"], "gold_display": perm.index(gold - 1) + 1 if gold else None, | |
| "pred_orig": pred_orig, "gold": gold, "correct": pred_orig == gold if gold else None, | |
| "probs_orig": probs_orig} | |
| for extra in ("label_mass", "text", "new_tokens"): | |
| if extra in r: | |
| rec[extra] = r[extra] | |
| records.append(rec) | |
| return records, first_prompt | |
| def parse_args() -> argparse.Namespace: | |
| ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument("--config", help="YAML with any of the options below (CLI wins)") | |
| ap.add_argument("--model") | |
| ap.add_argument("--revision") | |
| ap.add_argument("--adapter", help="LoRA adapter directory loaded on top of --model") | |
| ap.add_argument("--data", nargs="+", default=["sinhalammlu_dev"]) | |
| ap.add_argument("--method", choices=["logit", "generate", "cloze"], default="logit") | |
| ap.add_argument("--lang", choices=["en", "si"], default="en", help="instruction language") | |
| ap.add_argument("--chat", choices=["auto", "on", "off"], default="auto") | |
| ap.add_argument("--answer-prefix", default="") | |
| ap.add_argument("--thinking", action="store_true", help="enable the chat template's thinking mode (use with --method generate)") | |
| ap.add_argument("--n-perms", type=int, default=1, help="option orders per question (1 = original only)") | |
| ap.add_argument("--first-perm", type=int, default=0, help="skip the first orders (1 = skip the original order)") | |
| ap.add_argument("--perm-mode", choices=["cyclic", "random", "identity"], default="cyclic", | |
| help="identity + --temperature > 0: n-perms independent samples of the same prompt") | |
| ap.add_argument("--temperature", type=float, default=0.0, help="> 0: sampled generation (self-consistency)") | |
| ap.add_argument("--top-p", type=float, default=0.95) | |
| ap.add_argument("--top-k", type=int, default=64) | |
| ap.add_argument("--fewshot-file", help="processed JSONL; the same examples precede every question") | |
| ap.add_argument("--n-shot", type=int, default=0) | |
| ap.add_argument("--max-new-tokens", type=int, default=16) | |
| ap.add_argument("--token-budget", type=int, default=32768, help="padded tokens per forward batch") | |
| ap.add_argument("--max-batch", type=int, default=64) | |
| ap.add_argument("--dtype", default="bfloat16") | |
| ap.add_argument("--gpu-mem-gb", type=float, help="PyTorch memory cap (default 100, env SMMLU_GPU_MEM_GB)") | |
| ap.add_argument("--limit", type=int, help="first N questions per dataset (smoke tests)") | |
| ap.add_argument("--seed", type=int, default=1234) | |
| ap.add_argument("--run-name") | |
| ap.add_argument("--out-dir", default=str(ROOT / "results")) | |
| ap.add_argument("--write-submission", action="store_true") | |
| args, _ = ap.parse_known_args() | |
| if args.config: | |
| ap.set_defaults(**{k.replace("-", "_"): v for k, v in yaml.safe_load(open(args.config)).items()}) | |
| args = ap.parse_args() | |
| if not args.model: | |
| ap.error("--model is required") | |
| return args | |
| def main() -> None: | |
| args = parse_args() | |
| load_env() | |
| set_seed(args.seed) | |
| cap_gpu_memory(args.gpu_mem_gb) | |
| from .scorers import Scorer, load_model, loaded_param_stats | |
| run_name = args.run_name or f"{args.model.split('/')[-1]}_{args.method}_{args.lang}_{args.n_shot}shot" | |
| out = Path(args.out_dir) / run_name | |
| out.mkdir(parents=True, exist_ok=True) | |
| fewshot = read_jsonl(resolve_data(args.fewshot_file))[: args.n_shot] if args.n_shot else [] | |
| shot_uids = {e["uid"] for e in fewshot} | |
| items: dict[str, dict] = {} | |
| for d in args.data: | |
| rows = [r for r in read_jsonl(resolve_data(d)) if r["uid"] not in shot_uids] | |
| items.update({r["uid"]: r for r in (rows[: args.limit] if args.limit else rows)}) | |
| t0 = time.time() | |
| model, tok = load_model(args.model, args.revision, args.dtype, args.adapter) | |
| chat = {"on": True, "off": False}.get(args.chat, tok.chat_template is not None and not BASE_NAME.search(args.model)) | |
| pb = PromptBuilder(tok, lang=args.lang, chat=chat, fewshot=fewshot, answer_prefix=args.answer_prefix, | |
| chat_kwargs={"enable_thinking": args.thinking}) | |
| scorer = Scorer(model, tok, pb.add_special_tokens, args.token_budget, args.max_batch, args.max_new_tokens) | |
| if args.temperature > 0: | |
| scorer.sampling = {"do_sample": True, "temperature": args.temperature, "top_p": args.top_p, "top_k": args.top_k} | |
| load_s = time.time() - t0 | |
| t0 = time.time() | |
| records, first_prompt = score_items(items, pb, scorer, args.method, args.n_perms, args.perm_mode, args.seed, args.first_perm) | |
| score_s = time.time() - t0 | |
| if first_prompt: | |
| (out / "example_prompt.txt").write_text(first_prompt, encoding="utf-8") | |
| # Tokens per character of the question block: tokenizer efficiency on Sinhala | |
| sample = list(items.values())[:500] | |
| texts = [user_text(it, it["options"], args.lang) for it in sample] | |
| n_tok = sum(len(x) for x in tok(texts, add_special_tokens=False)["input_ids"]) | |
| metrics = {"by_dataset": metrics_by_dataset(items, records)} | |
| metrics["run"] = { | |
| "run_name": run_name, "args": vars(args), "chat": chat, "n_questions": len(items), | |
| "n_forward_items": len(records), "load_seconds": round(load_s, 1), "score_seconds": round(score_s, 1), | |
| "tokens_per_char": n_tok / sum(len(t) for t in texts), | |
| "params": loaded_param_stats(model), "env": env_fingerprint(), | |
| } | |
| (out / "config.json").write_text(json.dumps(vars(args), indent=2)) | |
| write_jsonl(out / "predictions.jsonl", records) | |
| (out / "metrics.json").write_text(json.dumps(metrics, indent=2, ensure_ascii=False)) | |
| report = "\n".join(markdown_report(f"{run_name} / {d}", m) for d, m in metrics["by_dataset"].items()) | |
| (out / "report.md").write_text(report, encoding="utf-8") | |
| if args.write_submission: | |
| write_submissions(out, items, records) | |
| for d, m in metrics["by_dataset"].items(): | |
| o = m["overall"] | |
| print(f"{run_name} | {d}: acc={o['acc']:.4f} vote={o['acc_vote']:.4f} " | |
| f"perm={o['acc_perm_mean']:.4f}±{o['acc_perm_std']:.4f} n={o['n']}") | |
| print(f"load {load_s:.0f}s, scoring {score_s:.0f}s -> {out}") | |
| def metrics_by_dataset(items: dict, records: list[dict]) -> dict: | |
| out = {} | |
| for d in sorted({it["dataset"] for it in items.values()}): | |
| sub_items = {u: it for u, it in items.items() if it["dataset"] == d} | |
| out[d] = compute_metrics(sub_items, [r for r in records if r["uid"] in sub_items]) | |
| return out | |
| def write_submissions(out: Path, items: dict, records: list[dict]) -> None: | |
| """Codabench format: predictions.zip containing predictions.jsonl, answer = permutation-vote prediction.""" | |
| from .metrics import aggregate_items | |
| agg = aggregate_items(items, records) | |
| for d in sorted({it["dataset"] for it in items.values()}): | |
| rows = [{"q_no": it["q_no"], "subject": it.get("subject_raw", it["subject"]), "category": it["category"], | |
| "level": it["level"], "answer": str(agg[u]["vote"] or 1)} | |
| for u, it in items.items() if it["dataset"] == d] | |
| path = out / f"submission_{d}.zip" | |
| with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z: | |
| z.writestr("predictions.jsonl", "".join(json.dumps(r, ensure_ascii=False) + "\n" for r in rows)) | |
| if __name__ == "__main__": | |
| main() | |