Tharushi29 commited on
Commit
5b977e6
·
verified ·
1 Parent(s): 58fce74

Delete hf_team_repo

Browse files
hf_team_repo/README.md DELETED
@@ -1,17 +0,0 @@
1
- # team-ape-llama: SinhalaMMLU shared task (ICTer 2026)
2
-
3
- - **Code repo (this):** https://huggingface.co/SinhalaMMLU-SharedTask/team-ape-llama
4
- - **Adapter repo:** https://huggingface.co/SinhalaMMLU-SharedTask/team-ape-llama-gemma4-e4b-lora
5
- - **Run:** `pip install -r requirements.txt && python inference.py --input <questions folder or .jsonl> --output predictions.jsonl` (`--perm 1` = single pass)
6
-
7
- - **Base model:** `google/gemma-4-e4b-it` (text-only load; per-layer embeddings counted): 7.480 B parameters, loaded in bf16.
8
- - **Adapter (single LoRA):** 17,440,768 parameters (r=8, all attention + MLP projections). Repo: https://huggingface.co/SinhalaMMLU-SharedTask/team-ape-llama-gemma4-e4b-lora.
9
- - **Total inference parameters:** about 7.498 B (<= 8 B). One base model + one adapter. No ensemble, no router, no retrieval, no internet, no closed API at inference.
10
- - **Prompt:** Sinhala instruction (`si_basic`), zero-shot, logit scoring over the option labels 1-4.
11
- - **perm4 (what we submitted):** deterministic option rotation: the same question is scored 4 times with the options in a fixed rotated order and the scores are summed. Same model, same adapter, no extra parameters; no sampling (`do_sample=False`), fixed seed.
12
- - **Codabench:** Easy 52.0249 / Medium 49.2349 / Hard 57.7320 / Macro 52.9973 (`predictions.jsonl` in this repo is the exact file that was uploaded).
13
- - **Hardware / runtime:** 1x NVIDIA A100 (Google Colab); test set (2,238 questions) scored in about 285 s with perm4, plus model download.
14
- - **Extra data used:** see `training/README_training.md`.
15
- - **Training:** `training/`.
16
- - **Full code and MANIFEST:** private GitHub repo, tag `final-oct28` (branch `final/oct28`), configs `configs/sub_run2_bf16_perm4.yaml` (and `..._perm1.yaml`).
17
- - **Citation:** Pramodya et al., *SinhalaMMLU: A Comprehensive Benchmark for Evaluating Multitask Language Understanding in Sinhala*, EMNLP 2025, pp. 32943-32961.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/__pycache__/inference.cpython-310.pyc DELETED
Binary file (2.57 kB)
 
hf_team_repo/__pycache__/predict.cpython-310.pyc DELETED
Binary file (4.37 kB)
 
hf_team_repo/configs/final_bf16_perm4.yaml DELETED
@@ -1,16 +0,0 @@
1
- # Oct 28 submission probe: Gemma-4-E4B-it + Run 2 adapter (ape-llama-sinmmlu/ft_gemma4_e4b_real_plus_exp), text-only, si_basic, fewshot 0.
2
- # NO retrieval / router / ensemble. Single model + single adapter. Only dtype (bf16) and permutations (4) differ between probes.
3
- model:
4
- name_or_path: google/gemma-4-e4b-it
5
- load_in_4bit: false
6
- dtype: bfloat16
7
- text_only: true
8
- adapter_path: __ADAPTER_DIR__ # replaced by inference.py with the downloaded adapter repo
9
- prompt:
10
- template: si_basic
11
- fewshot_k: 0
12
- fewshot_file: null
13
- inference:
14
- batch_size: 8
15
- max_length: 1024
16
- permutations: 4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/inference.py DELETED
@@ -1,59 +0,0 @@
1
- """Reproduces our final Codabench submission (Gemma-4-E4B-it + one LoRA adapter, bf16, deterministic).
2
-
3
- Everything is downloaded from Hugging Face inside this script (base model, adapter, optional test data).
4
- No local paths, no retrieval, no internet other than Hugging Face, no closed API.
5
-
6
- Usage (fresh Colab / Kaggle, 1 GPU with >= 24 GB):
7
- pip install -r requirements.txt
8
- python inference.py --input <folder or .jsonl with the questions> --output predictions.jsonl
9
- # or, if the organisers host the questions on Hugging Face: set DATA_REPO below (or pass --data_repo)
10
-
11
- perm4 = deterministic option rotation (same model + same adapter, 4 scoring passes, scores summed; no extra parameters,
12
- no sampling, fixed seed). Use --perm 1 for the single-pass variant.
13
- """
14
- import argparse
15
- import subprocess
16
- import sys
17
- import tempfile
18
- from pathlib import Path
19
-
20
- from huggingface_hub import snapshot_download
21
-
22
- # ---- team settings -------------------------------------------------------------------------------------------
23
- BASE_MODEL = "google/gemma-4-e4b-it" # downloaded by transformers from the Hub
24
- MODEL_ID = "SinhalaMMLU-SharedTask/team-ape-llama-gemma4-e4b-lora" # our adapter repo (weights + adapter_config.json)
25
- DATA_REPO = None # organisers: HF dataset repo with the questions
26
- # --------------------------------------------------------------------------------------------------------------
27
-
28
- HERE = Path(__file__).resolve().parent
29
-
30
-
31
- def main() -> int:
32
- ap = argparse.ArgumentParser()
33
- ap.add_argument("--input", help="folder (easy/medium/hard/*.json) or .jsonl with the questions")
34
- ap.add_argument("--data_repo", default=DATA_REPO, help="HF dataset repo with the questions (used when --input is not given)")
35
- ap.add_argument("--output", default="predictions.jsonl")
36
- ap.add_argument("--perm", type=int, default=4, choices=[1, 4], help="4 = option rotation (submitted); 1 = single pass")
37
- args = ap.parse_args()
38
-
39
- if args.input:
40
- questions = args.input
41
- elif args.data_repo:
42
- questions = snapshot_download(args.data_repo, repo_type="dataset")
43
- else:
44
- sys.exit("give --input or set DATA_REPO")
45
-
46
- adapter_dir = snapshot_download(MODEL_ID) # adapter_config.json + adapter_model.safetensors
47
- cfg = (HERE / "configs" / "final_bf16_perm4.yaml").read_text(encoding="utf-8")
48
- cfg = cfg.replace("__ADAPTER_DIR__", adapter_dir).replace("permutations: 4", f"permutations: {args.perm}")
49
- with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False, encoding="utf-8") as f:
50
- f.write(cfg)
51
- cfg_path = f.name
52
-
53
- cmd = [sys.executable, str(HERE / "predict.py"), "--config", cfg_path, "--input", questions, "--output", args.output]
54
- print("running:", " ".join(cmd))
55
- return subprocess.call(cmd, cwd=str(HERE))
56
-
57
-
58
- if __name__ == "__main__":
59
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/predict.py DELETED
@@ -1,98 +0,0 @@
1
- """Answer every question in a file and write predictions.jsonl.
2
-
3
- Examples
4
- --------
5
- # baseline on the dev held-out split, with scores logged to results/results.csv
6
- python predict.py --config configs/qwen25_7b.yaml --input data/dev_heldout.jsonl --evaluate --who Chamani
7
-
8
- # test predictions + the zip Codabench expects
9
- python predict.py --config configs/final.yaml --input data/test.jsonl --zip
10
-
11
- # quick check that everything runs (first 5 questions only)
12
- python predict.py --config configs/smoke_test.yaml --input data/sample/sample_dev.jsonl --limit 5
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import argparse
18
- import sys
19
- import time
20
- from pathlib import Path
21
-
22
- from sinmmlu.config import load_config
23
- from sinmmlu.data import has_gold, load_questions, prediction_row, write_jsonl
24
- from sinmmlu.metrics import accuracy_by_level, format_report
25
- from sinmmlu.modeling import count_parameters, describe_budget, load_model, load_tokenizer, set_determinism
26
- from sinmmlu.runinfo import append_result, write_run_info
27
- from sinmmlu.scoring import Scorer
28
-
29
-
30
- def main(argv=None) -> int:
31
- ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
32
- ap.add_argument("--config", required=True)
33
- ap.add_argument("--input", required=True, help="jsonl of questions (dev, held-out or test)")
34
- ap.add_argument("--output", help="default: outputs/<config name>/<input name>.predictions.jsonl")
35
- ap.add_argument("--limit", type=int, help="only the first N questions (debugging)")
36
- ap.add_argument("--evaluate", action="store_true", help="input has gold answers: print + log scores")
37
- ap.add_argument("--zip", action="store_true", help="also write predictions.zip for Codabench")
38
- ap.add_argument("--who", default="", help="your name, for the results log")
39
- ap.add_argument("--note", default="", help="free text for the results log")
40
- args = ap.parse_args(argv)
41
-
42
- cfg = load_config(args.config)
43
- set_determinism(cfg["seed"])
44
-
45
- records = load_questions(args.input, cfg["data"]["answer_base"])
46
- if args.limit:
47
- records = records[: args.limit]
48
- print(f"[data] {len(records)} questions from {args.input}")
49
-
50
- pool = []
51
- if cfg["prompt"]["fewshot_k"] > 0:
52
- if not cfg["prompt"]["fewshot_file"]:
53
- sys.exit("prompt.fewshot_k > 0 needs prompt.fewshot_file (a dev TRAIN split)")
54
- if Path(cfg["prompt"]["fewshot_file"]).resolve() == Path(args.input).resolve():
55
- sys.exit("few-shot file must not be the file being predicted")
56
- pool = load_questions(cfg["prompt"]["fewshot_file"], cfg["data"]["answer_base"])
57
- if not has_gold(pool):
58
- sys.exit("few-shot file has no gold answers")
59
-
60
- t0 = time.time()
61
- tok = load_tokenizer(cfg["model"])
62
- model = load_model(cfg["model"])
63
- n_params = count_parameters(model)
64
- print(f"[model] {cfg['model']['name_or_path']} : {describe_budget(n_params)}")
65
- if cfg["model"]["load_in_4bit"]:
66
- print("[model] 4-bit: count above is understated; use tools/count_params.py for the manifest")
67
-
68
- scorer = Scorer(model, tok, cfg, pool)
69
- preds = scorer.predict(records)
70
- elapsed = time.time() - t0
71
-
72
- base = int(cfg["output"]["answer_base"])
73
- rows = [prediction_row(r, str(p + base)) for r, p in zip(records, preds)]
74
- out = Path(args.output) if args.output else Path("outputs") / cfg["name"] / f"{Path(args.input).stem}.predictions.jsonl"
75
- write_jsonl(rows, out)
76
- print(f"[out] {len(rows)} predictions -> {out} ({elapsed:.0f}s)")
77
-
78
- extra = {"input": args.input, "n": len(records), "seconds": round(elapsed, 1), "loaded_params": n_params}
79
- if args.evaluate:
80
- if not has_gold(records):
81
- sys.exit("--evaluate: input file has no gold answers")
82
- result = accuracy_by_level([r["level"] for r in records], preds, [r["gold"] for r in records])
83
- print(format_report(result))
84
- if not args.limit:
85
- append_result(cfg, args.input, result, args.who, args.note)
86
- print("[log] appended to results/results.csv")
87
- extra["result"] = result
88
- write_run_info(out.with_suffix(".run.json"), cfg, extra)
89
-
90
- if args.zip:
91
- from tools.make_submission import make_zip
92
-
93
- make_zip(out, out.parent / "predictions.zip", expected=records)
94
- return 0
95
-
96
-
97
- if __name__ == "__main__":
98
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/predictions.jsonl DELETED
The diff for this file is too large to render. See raw diff
 
hf_team_repo/requirements.txt DELETED
@@ -1,10 +0,0 @@
1
- transformers==5.18.0
2
- peft==0.21.2
3
- accelerate==1.15.0
4
- huggingface_hub==1.33.0
5
- safetensors==0.8.0
6
- pyyaml==6.0.3
7
- tqdm
8
- numpy
9
- # torch: use the CUDA build that matches your machine (tested with torch 2.x on an A100).
10
- # If peft raises "ImportError: incompatible version of torchao" run: pip uninstall -y torchao
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/sinmmlu/__init__.py DELETED
@@ -1,8 +0,0 @@
1
- """Sinhala MMLU shared task (ICTer 2026) - shared library code.
2
-
3
- Every entry script (predict.py, evaluate.py, train.py, tools/*) imports from
4
- here, so training and inference always use exactly the same data loading and
5
- prompt building.
6
- """
7
-
8
- __version__ = "0.1.0"
 
 
 
 
 
 
 
 
 
hf_team_repo/sinmmlu/config.py DELETED
@@ -1,89 +0,0 @@
1
- """Load experiment configs (YAML) with defaults.
2
-
3
- One YAML file = one experiment. To try something new, copy a config and
4
- change it; don't edit the Python code. That keeps every leaderboard
5
- submission reproducible from (git commit + config file).
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import copy
11
- from pathlib import Path
12
- from typing import Any
13
-
14
- import yaml
15
-
16
- DEFAULTS: dict[str, Any] = {
17
- "name": None, # defaults to the config file name
18
- "seed": 42,
19
- "model": {
20
- "name_or_path": None, # Hugging Face id or local folder
21
- "revision": None, # pin a HF commit hash for the final run
22
- "dtype": "auto", # auto | bfloat16 | float16 | float32
23
- "load_in_4bit": False, # needs bitsandbytes + GPU
24
- "adapter_path": None, # LoRA adapter folder (from train.py)
25
- "trust_remote_code": False,
26
- # multimodal checkpoints (Gemma 4): build only the text model, so vision/audio encoders are never loaded
27
- # (organisers: unused encoders do not count towards 8B). Fails if any text weight is missing.
28
- "text_only": False,
29
- },
30
- "prompt": {
31
- "template": "si_basic", # see sinmmlu/prompts.py TEMPLATES
32
- "fewshot_k": 0, # number of solved examples in the prompt
33
- "fewshot_file": None, # jsonl with gold answers (dev TRAIN split only)
34
- "use_chat_template": "auto", # auto | true | false
35
- "chat_template_kwargs": {}, # e.g. {enable_thinking: false} for Qwen3
36
- },
37
- "inference": {
38
- "batch_size": 4,
39
- "max_length": 2048, # prompts longer than this are left-truncated
40
- "permutations": 1, # >1 = average over cyclic option orders (debias)
41
- "option_bias": None, # optional [b1..b4] added to the option log-probs (position-bias correction, tools/calibrate.py)
42
- },
43
- "data": {
44
- "answer_base": "auto", # gold labels: auto | 0 | 1
45
- },
46
- "output": {
47
- "answer_base": 1, # predictions.jsonl uses "1".."4" (check with organisers)
48
- },
49
- "train": {
50
- "train_file": None,
51
- "eval_file": None,
52
- "output_dir": "outputs/adapter",
53
- "epochs": 2,
54
- "learning_rate": 2.0e-4,
55
- "batch_size": 2,
56
- "grad_accum": 8,
57
- "lora_r": 16,
58
- "lora_alpha": 32,
59
- "lora_dropout": 0.05,
60
- "target_modules": ["q_proj", "k_proj", "v_proj", "o_proj"],
61
- "gradient_checkpointing": True,
62
- "rotate_options": True, # move the right answer to every position equally
63
- "logging_steps": 10,
64
- },
65
- }
66
-
67
-
68
- def _merge(base: dict, override: dict) -> dict:
69
- out = copy.deepcopy(base)
70
- for key, value in (override or {}).items():
71
- if isinstance(value, dict) and isinstance(out.get(key), dict):
72
- out[key] = _merge(out[key], value)
73
- else:
74
- out[key] = value
75
- return out
76
-
77
-
78
- def load_config(path: str | Path, overrides: dict | None = None) -> dict:
79
- path = Path(path)
80
- with path.open(encoding="utf-8") as f:
81
- user_cfg = yaml.safe_load(f) or {}
82
- cfg = _merge(DEFAULTS, user_cfg)
83
- if overrides:
84
- cfg = _merge(cfg, overrides)
85
- if not cfg["name"]:
86
- cfg["name"] = path.stem
87
- if not cfg["model"]["name_or_path"]:
88
- raise ValueError(f"{path}: model.name_or_path is required")
89
- return cfg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/sinmmlu/data.py DELETED
@@ -1,221 +0,0 @@
1
- """Read question files and normalise them to one internal format.
2
-
3
- The official dev/test format is released on Oct 5. Until then we accept the
4
- common field names below, so only this file needs adjusting if the real
5
- format differs.
6
-
7
- Internal record (a dict):
8
- id str
9
- level "easy" | "medium" | "hard" (lower-cased)
10
- question str
11
- options list[str]
12
- gold int | None 0-based index of the correct option (None for test)
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import json
18
- from pathlib import Path
19
-
20
- QUESTION_KEYS = ("question", "q", "prompt", "stem")
21
- OPTIONS_KEYS = ("options", "choices", "answers", "candidates")
22
- GOLD_KEYS = ("answer", "label", "gold", "correct", "answer_idx")
23
- LEVEL_KEYS = ("level", "difficulty")
24
- LETTERS = "ABCDEFGH"
25
-
26
-
27
- def read_jsonl(path: str | Path) -> list[dict]:
28
- """Read JSON Lines, or a single JSON array (the official dev file is an array).
29
-
30
- A folder is also accepted: the official TEST release is a folder tree
31
- (easy/ medium/ hard/, one JSON array per subject). All *.json / *.jsonl
32
- files under it are read in sorted path order and concatenated.
33
- """
34
- if Path(path).is_dir():
35
- files = sorted(
36
- f for f in Path(path).rglob("*")
37
- if f.suffix in (".json", ".jsonl") and not any(part.startswith(".") for part in f.parts)
38
- )
39
- if not files:
40
- raise ValueError(f"{path}: folder has no .json/.jsonl files")
41
- rows = []
42
- for f in files:
43
- rows.extend(read_jsonl(f))
44
- return rows
45
- text = Path(path).read_text(encoding="utf-8-sig")
46
- if text.lstrip().startswith("["):
47
- try:
48
- data = json.loads(text)
49
- except json.JSONDecodeError as e:
50
- raise ValueError(f"{path}: invalid JSON array ({e})") from e
51
- if not all(isinstance(r, dict) for r in data):
52
- raise ValueError(f"{path}: expected a JSON array of objects")
53
- return data
54
- rows = []
55
- with Path(path).open(encoding="utf-8-sig") as f:
56
- for line_no, line in enumerate(f, 1):
57
- line = line.strip()
58
- if not line:
59
- continue
60
- try:
61
- rows.append(json.loads(line))
62
- except json.JSONDecodeError as e:
63
- raise ValueError(f"{path}:{line_no}: invalid JSON ({e})") from e
64
- return rows
65
-
66
-
67
- def write_jsonl(rows: list[dict], path: str | Path) -> None:
68
- path = Path(path)
69
- path.parent.mkdir(parents=True, exist_ok=True)
70
- with path.open("w", encoding="utf-8", newline="\n") as f:
71
- for row in rows:
72
- f.write(json.dumps(row, ensure_ascii=False) + "\n")
73
-
74
-
75
- def level_of(row: dict):
76
- """Difficulty label: top-level level/difficulty, or metadata.difficulty (official format)."""
77
- value = _first(row, LEVEL_KEYS)
78
- if value is None and isinstance(row.get("metadata"), dict):
79
- value = _first(row["metadata"], LEVEL_KEYS)
80
- return value
81
-
82
-
83
- def row_ids(rows: list[dict]) -> list[str]:
84
- """Stable ids: 'id' if present, else 'q_no' when unique, else the row position."""
85
- if all("id" in r for r in rows):
86
- return [str(r["id"]) for r in rows]
87
- qn = [r.get("q_no") for r in rows]
88
- if all(q is not None for q in qn) and len(set(qn)) == len(qn):
89
- return [str(q) for q in qn]
90
- return [str(r["id"]) if "id" in r else f"row_{i:05d}" for i, r in enumerate(rows)]
91
-
92
-
93
- def _first(row: dict, keys: tuple[str, ...]):
94
- for key in keys:
95
- if key in row and row[key] is not None:
96
- return row[key]
97
- return None
98
-
99
-
100
- def _options(row: dict) -> list[str]:
101
- opts = _first(row, OPTIONS_KEYS)
102
- if isinstance(opts, dict): # {"1": "...", "2": "..."} or {"A": ...}
103
- opts = [opts[k] for k in sorted(opts)]
104
- if opts is None: # option1..option4 / option_a.. / A..D columns
105
- for pattern in ("option{}", "option_{}", "opt{}", "choice{}"):
106
- found = [row.get(pattern.format(i)) for i in range(1, 9)]
107
- found = [o for o in found if o is not None]
108
- if found:
109
- opts = found
110
- break
111
- if opts is None:
112
- found = [row.get(c) for c in LETTERS if row.get(c) is not None]
113
- opts = found or None
114
- if not opts:
115
- raise ValueError(f"no options found in record: {list(row)}")
116
- return [str(o).strip() for o in opts]
117
-
118
-
119
- def _raw_gold(value):
120
- """Return gold as an int (base not yet known) or a letter index marker."""
121
- if value is None:
122
- return None
123
- if isinstance(value, bool):
124
- raise ValueError(f"unexpected gold value {value!r}")
125
- if isinstance(value, int):
126
- return value
127
- text = str(value).strip()
128
- if text.isdigit():
129
- return int(text)
130
- if len(text) == 1 and text.upper() in LETTERS:
131
- return ("letter", LETTERS.index(text.upper()))
132
- raise ValueError(f"cannot parse gold answer {value!r}")
133
-
134
-
135
- def load_questions(path: str | Path, answer_base="auto") -> list[dict]:
136
- """Load a dev/test/train file into internal records.
137
-
138
- answer_base: "auto" guesses from the data (any 0 -> 0-based, otherwise
139
- 1-based). After Oct 5, confirm this once and set it explicitly in the
140
- config - a wrong base shifts every answer and silently ruins the score.
141
- """
142
- raw = read_jsonl(path)
143
- ids = row_ids(raw)
144
- records = []
145
- for i, row in enumerate(raw):
146
- rid = ids[i]
147
- question = _first(row, QUESTION_KEYS)
148
- if question is None:
149
- raise ValueError(f"{path}: record {rid} has no question field")
150
- level_raw = level_of(row)
151
- level = str(level_raw or "unknown").strip().lower()
152
- records.append(
153
- {
154
- "id": str(rid),
155
- "level": level,
156
- "level_raw": level_raw, # written back unchanged in predictions
157
- "question": str(question).strip(),
158
- "options": _options(row),
159
- "subject": row.get("subject"),
160
- "category": row.get("category"),
161
- "q_no": row.get("q_no"),
162
- "_raw_gold": _raw_gold(_first(row, GOLD_KEYS)),
163
- }
164
- )
165
-
166
- numeric = [r["_raw_gold"] for r in records if isinstance(r["_raw_gold"], int)]
167
- if answer_base == "auto":
168
- base = 0 if (numeric and min(numeric) == 0) else 1
169
- else:
170
- base = int(answer_base)
171
-
172
- for r in records:
173
- g = r.pop("_raw_gold")
174
- if g is None:
175
- r["gold"] = None
176
- elif isinstance(g, tuple): # letter
177
- r["gold"] = g[1]
178
- else:
179
- r["gold"] = g - base
180
- if r["gold"] is not None and not 0 <= r["gold"] < len(r["options"]):
181
- # data quirk: some rows store the option TEXT as the answer (e.g. answer 24, choices [..,"24",..])
182
- raw = str(r["gold"] + base)
183
- hits = [i for i, o in enumerate(r["options"]) if str(o).strip().lstrip("0") == raw.lstrip("0")]
184
- if len(hits) == 1:
185
- print(f"[data] {path}: record {r['id']} answer {raw} matched option text -> option {hits[0] + 1}")
186
- r["gold"] = hits[0]
187
- continue
188
- raise ValueError(
189
- f"{path}: record {r['id']} gold {r['gold'] + base} out of range "
190
- f"for {len(r['options'])} options (answer_base={base})"
191
- )
192
- return records
193
-
194
-
195
- def prediction_row(record: dict, answer: str) -> dict:
196
- """One line of predictions.jsonl in the official Codabench format:
197
- {"q_no":1,"subject":"Arts","category":"humanities","level":"easy","answer":"2"}
198
- Files without q_no (our own sample/train data) fall back to an "id" key."""
199
- level = record["level_raw"] if record.get("level_raw") is not None else record["level"]
200
- # Codabench example: {"q_no":1,"subject":"Arts","category":"humanities","level":"easy","answer":"2"}
201
- # -> q_no is a number, every other value a string.
202
- s = lambda v: "" if v is None else str(v)
203
- if record.get("q_no") is not None:
204
- q = record["q_no"]
205
- q = int(q) if str(q).strip().isdigit() else s(q)
206
- return {"q_no": q, "subject": s(record.get("subject")), "category": s(record.get("category")),
207
- "level": s(level), "answer": s(answer)}
208
- return {"id": s(record["id"]), "level": s(level), "answer": s(answer)}
209
-
210
-
211
- def match_key(d: dict):
212
- """Key that links a prediction line to its question: (subject, level, q_no) in
213
- the official format (q_no restarts per subject file, and the TEST set has the
214
- same subject in several levels), else the id."""
215
- if d.get("q_no") is not None:
216
- return ("q", str(d.get("subject")), str(d.get("level") or "").strip().lower(), str(d["q_no"]))
217
- return ("id", str(d["id"]))
218
-
219
-
220
- def has_gold(records: list[dict]) -> bool:
221
- return bool(records) and all(r["gold"] is not None for r in records)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/sinmmlu/metrics.py DELETED
@@ -1,33 +0,0 @@
1
- """The official metric: accuracy per difficulty level and their macro average."""
2
-
3
- from __future__ import annotations
4
-
5
- LEVELS = ("easy", "medium", "hard")
6
-
7
-
8
- def accuracy_by_level(levels: list[str], preds: list[int], golds: list[int]) -> dict:
9
- hit: dict[str, int] = {}
10
- tot: dict[str, int] = {}
11
- for level, p, g in zip(levels, preds, golds):
12
- tot[level] = tot.get(level, 0) + 1
13
- hit[level] = hit.get(level, 0) + int(p == g)
14
- acc = {lv: hit[lv] / tot[lv] for lv in tot}
15
- present = [lv for lv in LEVELS if lv in acc] or list(acc)
16
- return {
17
- "acc": acc,
18
- "count": tot,
19
- # Macro = (E + M + H) / 3: each level weighs the same, however many
20
- # questions it has. Hard accuracy counts for a full third.
21
- "macro": sum(acc[lv] for lv in present) / len(present) if present else 0.0,
22
- "overall": sum(hit.values()) / max(1, sum(tot.values())),
23
- }
24
-
25
-
26
- def format_report(result: dict) -> str:
27
- lines = [f"{'level':<8} {'n':>6} {'acc':>8}"]
28
- for lv in list(LEVELS) + sorted(set(result["acc"]) - set(LEVELS)):
29
- if lv in result["acc"]:
30
- lines.append(f"{lv:<8} {result['count'][lv]:>6} {result['acc'][lv] * 100:>7.2f}%")
31
- lines.append(f"{'MACRO':<8} {'':>6} {result['macro'] * 100:>7.2f}%")
32
- lines.append(f"{'overall':<8} {sum(result['count'].values()):>6} {result['overall'] * 100:>7.2f}%")
33
- return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/sinmmlu/modeling.py DELETED
@@ -1,126 +0,0 @@
1
- """Model and tokenizer loading + parameter counting (the 8B rule)."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- import random
7
-
8
- import numpy as np
9
- import torch
10
-
11
- LIMIT = 8_000_000_000
12
-
13
-
14
- def set_determinism(seed: int) -> None:
15
- """Same input -> same predictions on every run (needed for verification)."""
16
- random.seed(seed)
17
- np.random.seed(seed)
18
- torch.manual_seed(seed)
19
- torch.cuda.manual_seed_all(seed)
20
- os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
21
- torch.backends.cuda.matmul.allow_tf32 = False
22
- torch.backends.cudnn.allow_tf32 = False
23
- torch.backends.cudnn.benchmark = False
24
- torch.backends.cudnn.deterministic = True
25
-
26
-
27
- def pick_dtype(name: str):
28
- if name == "auto":
29
- if torch.cuda.is_available():
30
- # T4/P100 (Kaggle, free Colab) have no fast bf16 -> float16
31
- return torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
32
- return torch.float32
33
- return getattr(torch, name)
34
-
35
-
36
- def load_tokenizer(model_cfg: dict):
37
- from transformers import AutoTokenizer
38
-
39
- tok = AutoTokenizer.from_pretrained(
40
- model_cfg["name_or_path"],
41
- revision=model_cfg["revision"],
42
- trust_remote_code=model_cfg["trust_remote_code"],
43
- )
44
- if tok.pad_token is None:
45
- tok.pad_token = tok.eos_token
46
- tok.padding_side = "left" # last position = next-token prediction for every row
47
- return tok
48
-
49
-
50
- def load_model(model_cfg: dict, for_training: bool = False):
51
- from transformers import AutoModelForCausalLM
52
-
53
- kwargs = {
54
- "revision": model_cfg["revision"],
55
- "trust_remote_code": model_cfg["trust_remote_code"],
56
- "dtype": pick_dtype(model_cfg["dtype"]),
57
- }
58
- if torch.cuda.is_available():
59
- kwargs["device_map"] = "auto"
60
- if model_cfg["load_in_4bit"]:
61
- from transformers import BitsAndBytesConfig
62
-
63
- kwargs["quantization_config"] = BitsAndBytesConfig(
64
- load_in_4bit=True,
65
- bnb_4bit_quant_type="nf4",
66
- bnb_4bit_use_double_quant=True,
67
- bnb_4bit_compute_dtype=kwargs["dtype"] if kwargs["dtype"] != torch.float32 else torch.float16,
68
- )
69
-
70
- if model_cfg.get("text_only"):
71
- from transformers import AutoConfig
72
-
73
- full = AutoConfig.from_pretrained(
74
- model_cfg["name_or_path"], revision=model_cfg["revision"], trust_remote_code=model_cfg["trust_remote_code"]
75
- )
76
- kwargs["config"] = getattr(full, "text_config", full)
77
- # multimodal checkpoints store the text weights as model.language_model.* ; the text-only class wants model.*
78
- # (vision/audio tensors have no counterpart in the text-only model, so they are not loaded)
79
- kwargs["key_mapping"] = {r"^model\.language_model\.": "model."}
80
- model, info = AutoModelForCausalLM.from_pretrained(model_cfg["name_or_path"], output_loading_info=True, **kwargs)
81
- tied = getattr(kwargs["config"], "tie_word_embeddings", False)
82
- missing = [k for k in info["missing_keys"] if not (tied and k == "lm_head.weight")]
83
- if missing: # a name mismatch would leave random weights and silently ruin the score
84
- raise RuntimeError(f"text_only load: {len(missing)} weights missing, e.g. {missing[:5]}")
85
- print(f"[model] text_only: {type(model).__name__}, ignored {len(info['unexpected_keys'])} non-text tensors")
86
- else:
87
- model = AutoModelForCausalLM.from_pretrained(model_cfg["name_or_path"], **kwargs)
88
-
89
- if model_cfg.get("adapter_path") and not for_training:
90
- from peft import PeftModel
91
-
92
- model = PeftModel.from_pretrained(model, model_cfg["adapter_path"])
93
- if not for_training:
94
- model.eval()
95
- return model
96
-
97
-
98
- def prepare_for_kbit_training(model, use_gradient_checkpointing: bool, fp32_below: int = 100_000_000):
99
- """peft.prepare_model_for_kbit_training, except that only SMALL fp16/bf16 parameters (norms) become float32.
100
-
101
- peft casts every non-quantised parameter. Gemma 4 E4B has a 2.8 B-parameter per-layer embedding table
102
- (10.5 GiB in float32), which does not fit on a 15 GB T4 (OOM on Oct 17). Big tables stay in half precision.
103
- """
104
- for p in model.parameters():
105
- p.requires_grad = False
106
- if p.dtype in (torch.float16, torch.bfloat16) and p.numel() < fp32_below:
107
- p.data = p.data.to(torch.float32)
108
- if use_gradient_checkpointing:
109
- model.enable_input_require_grads()
110
- model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": True})
111
- return model
112
-
113
-
114
- def count_parameters(model) -> int:
115
- """Parameters of the loaded model (base + any adapter).
116
-
117
- Note: 4-bit loading packs two weights per stored element, so for a
118
- 4-bit model this UNDER-counts. Use tools/count_params.py (reads the
119
- original checkpoint) for the number that goes in the manifest.
120
- """
121
- return sum(p.numel() for p in model.parameters())
122
-
123
-
124
- def describe_budget(n_params: int) -> str:
125
- status = "OK" if n_params <= LIMIT else "OVER THE 8B LIMIT"
126
- return f"{n_params:,} params ({n_params / 1e9:.3f}B) -> {status}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/sinmmlu/prompts.py DELETED
@@ -1,140 +0,0 @@
1
- """Prompt templates.
2
-
3
- The SAME functions build prompts for inference (predict.py) and fine-tuning
4
- (train.py). If training and inference prompts differ, fine-tuning gains
5
- mostly disappear, so never build prompts anywhere else.
6
-
7
- To add a prompt experiment: add an entry to TEMPLATES, then point a config's
8
- prompt.template at it. Few-shot examples must come from the dev TRAIN split,
9
- never from the test set (rule 04).
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- TEMPLATES = {
15
- # Sinhala instruction
16
- "si_basic": {
17
- "system": None,
18
- "instruction": "පහත බහුවරණ ප්‍රශ්නයට නිවැරදි පිළිතුරේ අංකය පමණක් ලබා දෙන්න.",
19
- "question_label": "ප්‍රශ්නය",
20
- "answer_label": "පිළිතුර",
21
- },
22
- # English instruction, Sinhala question (some models follow English
23
- # instructions better)
24
- "en_basic": {
25
- "system": None,
26
- "instruction": (
27
- "Answer the following multiple-choice question written in Sinhala. "
28
- "Reply with the number of the correct option only."
29
- ),
30
- "question_label": "Question",
31
- "answer_label": "Answer",
32
- },
33
- # Sinhala instruction + teacher role in the system prompt
34
- "si_teacher": {
35
- "system": "ඔබ ශ්‍රී ලංකාවේ පාසල් විෂයයන් පිළිබඳ ගැඹුරු දැනුමක් ඇති ගුරුවරයෙකි.",
36
- "instruction": "පහත බහුවරණ ප්‍රශ්නයට නිවැරදි පිළිතුරේ අංකය පමණක් ලබා දෙන්න.",
37
- "question_label": "ප්‍රශ්නය",
38
- "answer_label": "පිළිතුර",
39
- },
40
- }
41
-
42
-
43
- def option_labels(n: int) -> list[str]:
44
- return [str(i) for i in range(1, n + 1)]
45
-
46
-
47
- def format_question(record: dict, template: dict, options: list[str] | None = None) -> str:
48
- opts = record["options"] if options is None else options
49
- lines = [f"{template['question_label']}: {record['question']}"]
50
- lines += [f"{label}. {text}" for label, text in zip(option_labels(len(opts)), opts)]
51
- return "\n".join(lines)
52
-
53
-
54
- def build_messages(
55
- record: dict,
56
- template_name: str,
57
- fewshot: list[dict] | None = None,
58
- options: list[str] | None = None,
59
- ) -> list[dict]:
60
- """Chat messages for one question. Few-shot examples become earlier
61
- user/assistant turns, so the model sees exactly how to answer."""
62
- if template_name not in TEMPLATES:
63
- raise KeyError(f"unknown prompt template {template_name!r}; options: {list(TEMPLATES)}")
64
- t = TEMPLATES[template_name]
65
- messages = []
66
- if t["system"]:
67
- messages.append({"role": "system", "content": t["system"]})
68
- for i, ex in enumerate(fewshot or []):
69
- content = format_question(ex, t)
70
- if i == 0:
71
- content = f"{t['instruction']}\n\n{content}"
72
- messages.append({"role": "user", "content": content})
73
- messages.append({"role": "assistant", "content": str(ex["gold"] + 1)})
74
- content = format_question(record, t, options)
75
- if not fewshot:
76
- content = f"{t['instruction']}\n\n{content}"
77
- messages.append({"role": "user", "content": content})
78
- return messages
79
-
80
-
81
- def render_prompt(
82
- tokenizer, messages: list[dict], use_chat_template="auto", chat_template_kwargs: dict | None = None
83
- ) -> tuple[str, str]:
84
- """Return (prompt_text, label_prefix).
85
-
86
- The answer label is scored right after prompt_text: for chat models the
87
- answer is the assistant's first token ("2"); for base models the prompt
88
- ends with "Answer:" and the label is " 2".
89
- """
90
- has_template = getattr(tokenizer, "chat_template", None) is not None
91
- use_chat = has_template if use_chat_template == "auto" else bool(use_chat_template)
92
-
93
- if use_chat:
94
- msgs = messages
95
- if msgs[0]["role"] == "system" and not _supports_system(tokenizer):
96
- msgs = [{"role": "user", "content": f"{msgs[0]['content']}\n\n{msgs[1]['content']}"}] + msgs[2:]
97
- text = tokenizer.apply_chat_template(
98
- msgs, tokenize=False, add_generation_prompt=True, **(chat_template_kwargs or {})
99
- )
100
- return text, ""
101
-
102
- # Plain-text fallback for base (non-chat) models
103
- answer_label = _answer_label(messages)
104
- parts = []
105
- for m in messages:
106
- if m["role"] == "assistant":
107
- parts[-1] += f"\n{answer_label}: {m['content']}"
108
- else:
109
- parts.append(m["content"])
110
- text = "\n\n".join(parts) + f"\n{answer_label}:"
111
- return text, " "
112
-
113
-
114
- def _answer_label(messages: list[dict]) -> str:
115
- for t in TEMPLATES.values():
116
- if any(t["question_label"] + ":" in m["content"] for m in messages):
117
- return t["answer_label"]
118
- return "Answer"
119
-
120
-
121
- def _supports_system(tokenizer) -> bool:
122
- try:
123
- tokenizer.apply_chat_template(
124
- [{"role": "system", "content": "s"}, {"role": "user", "content": "u"}],
125
- tokenize=False,
126
- add_generation_prompt=True,
127
- )
128
- return True
129
- except Exception:
130
- return False
131
-
132
-
133
- def select_fewshot(record: dict, pool: list[dict], k: int) -> list[dict]:
134
- """Deterministic few-shot choice: same difficulty level first, in file
135
- order, never the question itself."""
136
- if k <= 0 or not pool:
137
- return []
138
- same = [p for p in pool if p["level"] == record["level"] and p["id"] != record["id"]]
139
- other = [p for p in pool if p["level"] != record["level"] and p["id"] != record["id"]]
140
- return (same + other)[:k]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/sinmmlu/runinfo.py DELETED
@@ -1,85 +0,0 @@
1
- """Record what produced each result, so any score can be reproduced later."""
2
-
3
- from __future__ import annotations
4
-
5
- import csv
6
- import datetime as dt
7
- import json
8
- import platform
9
- import subprocess
10
- from pathlib import Path
11
-
12
- RESULTS_CSV = Path("results/results.csv")
13
- FIELDS = [
14
- "time", "who", "config", "model", "adapter", "template", "fewshot_k",
15
- "permutations", "data_file", "n", "easy", "medium", "hard", "macro", "git_commit", "note",
16
- ]
17
-
18
-
19
- def git_commit() -> str:
20
- try:
21
- sha = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL, text=True).strip()
22
- dirty = subprocess.call(["git", "diff", "--quiet", "HEAD"], stderr=subprocess.DEVNULL) != 0
23
- return sha + ("-dirty" if dirty else "")
24
- except Exception:
25
- return "no-git"
26
-
27
-
28
- def env_info() -> dict:
29
- info = {"python": platform.python_version(), "platform": platform.platform()}
30
- for mod in ("torch", "transformers", "peft", "accelerate", "bitsandbytes"):
31
- try:
32
- info[mod] = __import__(mod).__version__
33
- except Exception:
34
- info[mod] = None
35
- try:
36
- import torch
37
-
38
- info["cuda"] = torch.version.cuda
39
- info["gpu"] = torch.cuda.get_device_name(0) if torch.cuda.is_available() else None
40
- except Exception:
41
- pass
42
- return info
43
-
44
-
45
- def write_run_info(path: Path, cfg: dict, extra: dict) -> None:
46
- payload = {
47
- "time": dt.datetime.now().isoformat(timespec="seconds"),
48
- "git_commit": git_commit(),
49
- "config": cfg,
50
- "environment": env_info(),
51
- **extra,
52
- }
53
- path.parent.mkdir(parents=True, exist_ok=True)
54
- path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
55
-
56
-
57
- def append_result(cfg: dict, data_file: str, result: dict, who: str = "", note: str = "") -> None:
58
- """Add one row to results/results.csv (the team's experiment log and,
59
- later, the report's ablation table)."""
60
- RESULTS_CSV.parent.mkdir(parents=True, exist_ok=True)
61
- new = not RESULTS_CSV.exists()
62
- acc = result["acc"]
63
- row = {
64
- "time": dt.datetime.now().isoformat(timespec="minutes"),
65
- "who": who,
66
- "config": cfg["name"],
67
- "model": cfg["model"]["name_or_path"],
68
- "adapter": cfg["model"]["adapter_path"] or "",
69
- "template": cfg["prompt"]["template"],
70
- "fewshot_k": cfg["prompt"]["fewshot_k"],
71
- "permutations": cfg["inference"]["permutations"],
72
- "data_file": data_file,
73
- "n": sum(result["count"].values()),
74
- "easy": f"{acc.get('easy', float('nan')) * 100:.2f}",
75
- "medium": f"{acc.get('medium', float('nan')) * 100:.2f}",
76
- "hard": f"{acc.get('hard', float('nan')) * 100:.2f}",
77
- "macro": f"{result['macro'] * 100:.2f}",
78
- "git_commit": git_commit(),
79
- "note": note,
80
- }
81
- with RESULTS_CSV.open("a", encoding="utf-8", newline="") as f:
82
- w = csv.DictWriter(f, fieldnames=FIELDS)
83
- if new:
84
- w.writeheader()
85
- w.writerow(row)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/sinmmlu/scoring.py DELETED
@@ -1,135 +0,0 @@
1
- """Logit scoring: pick the option whose label ("1".."4") the model rates
2
- most likely as its next token.
3
-
4
- Why not let the model generate text?
5
- * always returns a valid answer (no parsing of free text)
6
- * deterministic: same scores every run, which the verification phase needs
7
- * one forward pass per question: much faster than generation
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import torch
13
-
14
- from .prompts import build_messages, option_labels, render_prompt, select_fewshot
15
-
16
-
17
- def label_token_id(tokenizer, prompt: str, label: str) -> int:
18
- """Token id that starts `label` when it is written right after `prompt`.
19
-
20
- Computed from the full string so tokenizers that merge characters across
21
- the boundary (or add a leading-space marker) are handled correctly.
22
- """
23
- p_ids = tokenizer(prompt, add_special_tokens=False)["input_ids"]
24
- f_ids = tokenizer(prompt + label, add_special_tokens=False)["input_ids"]
25
- k = 0
26
- while k < len(p_ids) and k < len(f_ids) and p_ids[k] == f_ids[k]:
27
- k += 1
28
- if k < len(p_ids) or k >= len(f_ids):
29
- # Boundary merged into the prompt's last token: fall back to the
30
- # label tokenised alone.
31
- return tokenizer(label, add_special_tokens=False)["input_ids"][-1]
32
- return f_ids[k]
33
-
34
-
35
- # Codabench Data page: "Each question has four options (1-4) and exactly one correct
36
- # answer." A few test items carry extra (often empty) choices, so only options 1-4 are
37
- # shown, scored and rotated, so the answer is always "1".."4" (also avoids the "1"/"10" token clash).
38
- MAX_SCORED = 4
39
-
40
-
41
- def _rotations(n: int, count: int) -> list[int]:
42
- count = max(1, min(count, n))
43
- return [round(i * n / count) % n for i in range(count)]
44
-
45
-
46
- class Scorer:
47
- def __init__(self, model, tokenizer, cfg: dict, fewshot_pool: list[dict] | None = None):
48
- self.model = model
49
- self.tok = tokenizer
50
- self.cfg = cfg
51
- self.pool = fewshot_pool or []
52
- self._label_cache: dict[tuple, list[int]] = {}
53
- self.device = next(model.parameters()).device
54
-
55
- def _label_ids(self, prompt: str, prefix: str, n: int) -> list[int]:
56
- # Labels depend only on the end of the prompt, so cache by its tail.
57
- key = (prompt[-64:], prefix, n)
58
- if key not in self._label_cache:
59
- ids = [label_token_id(self.tok, prompt, prefix + l) for l in option_labels(n)]
60
- if len(set(ids)) != n:
61
- raise RuntimeError(
62
- f"option labels map to non-unique tokens {ids}; "
63
- "use a different prompt ending or label style"
64
- )
65
- self._label_cache[key] = ids
66
- return self._label_cache[key]
67
-
68
- def _build(self, record: dict, shift: int) -> tuple[str, list[int]]:
69
- p = self.cfg["prompt"]
70
- opts = record["options"][:MAX_SCORED]
71
- n = len(opts)
72
- options = opts[shift:] + opts[:shift]
73
- fewshot = select_fewshot(record, self.pool, p["fewshot_k"])
74
- msgs = build_messages(record, p["template"], fewshot, options)
75
- text, prefix = render_prompt(self.tok, msgs, p["use_chat_template"], p["chat_template_kwargs"])
76
- return text, self._label_ids(text, prefix, n)
77
-
78
- @torch.no_grad()
79
- def _logprobs(self, prompts: list[str], label_ids: list[list[int]]) -> list[torch.Tensor]:
80
- enc = self.tok(
81
- prompts,
82
- return_tensors="pt",
83
- padding=True,
84
- truncation=True,
85
- max_length=self.cfg["inference"]["max_length"],
86
- add_special_tokens=False, # chat templates already add BOS
87
- ).to(self.device)
88
- logits = self.model(**enc).logits[:, -1, :].float()
89
- out = []
90
- for row, ids in zip(logits, label_ids):
91
- out.append(torch.log_softmax(row[ids], dim=-1).cpu())
92
- return out
93
-
94
- def score(self, records: list[dict], progress: bool = True) -> list[list[float]]:
95
- """Return per-option log-probabilities for every record (original order)."""
96
- n_perm = self.cfg["inference"]["permutations"]
97
- bs = self.cfg["inference"]["batch_size"]
98
-
99
- jobs = [] # (record index, shift, prompt, label ids)
100
- for i, r in enumerate(records):
101
- for shift in _rotations(min(len(r["options"]), MAX_SCORED), n_perm):
102
- text, ids = self._build(r, shift)
103
- jobs.append((i, shift, text, ids))
104
-
105
- totals = [torch.zeros(min(len(r["options"]), MAX_SCORED)) for r in records]
106
- counts = [0] * len(records)
107
-
108
- iterator = range(0, len(jobs), bs)
109
- if progress:
110
- try:
111
- from tqdm import tqdm
112
-
113
- iterator = tqdm(iterator, desc="scoring", unit="batch")
114
- except ImportError:
115
- pass
116
- for start in iterator:
117
- batch = jobs[start : start + bs]
118
- lps = self._logprobs([j[2] for j in batch], [j[3] for j in batch])
119
- for (i, shift, _, _), lp in zip(batch, lps):
120
- n = len(lp)
121
- # position j in the rotated list = original option (j+shift) % n
122
- original = torch.empty(n)
123
- for j in range(n):
124
- original[(j + shift) % n] = lp[j]
125
- totals[i] += original
126
- counts[i] += 1
127
- return [(t / c).tolist() for t, c in zip(totals, counts)]
128
-
129
- def predict(self, records: list[dict], progress: bool = True) -> list[int]:
130
- """0-based predicted option index for every record."""
131
- scores = self.score(records, progress)
132
- bias = self.cfg["inference"].get("option_bias")
133
- if bias:
134
- scores = [[v + bias[j] for j, v in enumerate(s)] for s in scores]
135
- return [max(range(len(s)), key=lambda j: s[j]) for s in scores]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/training/README_training.md DELETED
@@ -1,7 +0,0 @@
1
- # Training of the adapter (Run 2)
2
-
3
- - Config: `ft_gemma4_e4b_real_plus_exp.yaml` (QLoRA, base 4-bit NF4, LoRA r=8, all attention + MLP projections, 1 epoch, LR 5e-5, batch 2 x grad-accum 8, options rotated during training).
4
- - Code: `train.py` (from our repository, `sinmmlu/` package; see the code link in the main README).
5
- - Data: `data/train_real_plus_explanations_clean.jsonl` (7,999 rows): real exam questions + textbook explanations. TODO (Hashini): exact sources and links, whether any rows were generated with a closed API (model name, number of rows), and the overlap check against the test set (counts only).
6
- - Note: the saved `adapter_config.json` says `lora_alpha: 8`; the config above says 16. The released adapter is the one in the adapter repo.
7
- - No test question was used for training, tuning or prompt design.
 
 
 
 
 
 
 
 
hf_team_repo/training/ft_gemma4_e4b_real_plus_exp.yaml DELETED
@@ -1,33 +0,0 @@
1
- # Hashini Run 2: Gentle QLoRA on Real Exam + Textbook Explanations (7,999 rows)
2
- # Adapter on HF: ape-llama-sinmmlu/ft_gemma4_e4b_real_plus_exp
3
- model:
4
- name_or_path: google/gemma-4-e4b-it
5
- adapter_path: ape-llama-sinmmlu/ft_gemma4_e4b_real_plus_exp
6
- load_in_4bit: true
7
- text_only: true
8
- prompt:
9
- template: si_basic
10
- train:
11
- train_file: data/train_real_plus_explanations_clean.jsonl
12
- eval_file: data/eval_v2.jsonl
13
- output_dir: outputs/ft_gemma4_e4b_real_plus_exp
14
- epochs: 1
15
- learning_rate: 5.0e-5
16
- batch_size: 2
17
- grad_accum: 8
18
- lora_r: 8
19
- lora_alpha: 16
20
- lora_dropout: 0.05
21
- target_modules:
22
- - q_proj
23
- - k_proj
24
- - v_proj
25
- - o_proj
26
- - gate_proj
27
- - up_proj
28
- - down_proj
29
- rotate_options: true
30
- inference:
31
- batch_size: 8
32
- max_length: 1024
33
- permutations: 4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_team_repo/training/train.py DELETED
@@ -1,181 +0,0 @@
1
- """LoRA / QLoRA fine-tuning on Sinhala MCQs.
2
-
3
- Training data = jsonl in the same format as the dev set (question, options,
4
- answer, level). The prompt is built by the SAME code as inference, and the
5
- loss is only on the answer token - exactly what predict.py scores.
6
-
7
- python train.py --config configs/train_qlora_qwen25_7b.yaml
8
-
9
- After training, predict with the adapter:
10
- model.adapter_path: outputs/<name>/adapter (in a predict config)
11
-
12
- Rules to remember:
13
- * never train on test questions (rule 04)
14
- * LoRA parameters count towards the 8B limit (base + adapter)
15
- * data generated with closed APIs (GPT/Claude/Gemini) must be disclosed
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- import argparse
21
- import json
22
- import math
23
- import shutil
24
- import sys
25
- from pathlib import Path
26
-
27
- import torch
28
-
29
- from sinmmlu.config import load_config
30
- from sinmmlu.data import has_gold, load_questions
31
- from sinmmlu.metrics import accuracy_by_level, format_report
32
- from sinmmlu.modeling import describe_budget, load_model, load_tokenizer, pick_dtype, prepare_for_kbit_training, set_determinism
33
- from sinmmlu.prompts import build_messages, render_prompt
34
- from sinmmlu.runinfo import append_result, write_run_info
35
- from sinmmlu.scoring import Scorer, label_token_id
36
-
37
-
38
- def build_examples(records: list[dict], tok, cfg: dict) -> list[dict]:
39
- """One example = prompt tokens + the correct label token.
40
-
41
- Options are rotated (deterministically) so the right answer appears in
42
- every position equally often - otherwise the model can learn "the answer
43
- is usually 2" instead of learning the content.
44
- """
45
- p = cfg["prompt"]
46
- max_len = cfg["inference"]["max_length"]
47
- rotate = cfg["train"].get("rotate_options", True)
48
- examples, skipped = [], 0
49
- for i, r in enumerate(records):
50
- n = len(r["options"])
51
- shift = (i % n) if rotate else 0
52
- options = r["options"][shift:] + r["options"][:shift]
53
- gold_pos = (r["gold"] - shift) % n # where the correct option ends up
54
- msgs = build_messages(r, p["template"], None, options)
55
- text, prefix = render_prompt(tok, msgs, p["use_chat_template"], p["chat_template_kwargs"])
56
- ids = tok(text, add_special_tokens=False)["input_ids"]
57
- if len(ids) + 1 > max_len:
58
- skipped += 1
59
- continue
60
- target = label_token_id(tok, text, prefix + str(gold_pos + 1))
61
- examples.append({"input_ids": ids + [target], "labels": [-100] * len(ids) + [target]})
62
- if skipped:
63
- print(f"[train] skipped {skipped} examples longer than {max_len} tokens")
64
- return examples
65
-
66
-
67
- class Collator:
68
- def __init__(self, pad_id: int):
69
- self.pad_id = pad_id
70
-
71
- def __call__(self, batch: list[dict]) -> dict:
72
- width = max(len(b["input_ids"]) for b in batch)
73
- ids, labels, mask = [], [], []
74
- for b in batch:
75
- pad = width - len(b["input_ids"])
76
- ids.append([self.pad_id] * pad + b["input_ids"])
77
- labels.append([-100] * pad + b["labels"])
78
- mask.append([0] * pad + [1] * len(b["input_ids"]))
79
- return {
80
- "input_ids": torch.tensor(ids),
81
- "labels": torch.tensor(labels),
82
- "attention_mask": torch.tensor(mask),
83
- }
84
-
85
-
86
- def main(argv=None) -> int:
87
- from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
88
- from transformers import Trainer, TrainingArguments
89
-
90
- ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
91
- ap.add_argument("--config", required=True)
92
- ap.add_argument("--who", default="")
93
- ap.add_argument("--max-steps", type=int, default=-1, help="stop early (debugging)")
94
- args = ap.parse_args(argv)
95
-
96
- cfg = load_config(args.config)
97
- tc = cfg["train"]
98
- set_determinism(cfg["seed"])
99
- if not tc["train_file"]:
100
- sys.exit("train.train_file is required")
101
-
102
- train_records = load_questions(tc["train_file"], cfg["data"]["answer_base"])
103
- if not has_gold(train_records):
104
- sys.exit("training file has no gold answers")
105
- print(f"[train] {len(train_records)} training questions from {tc['train_file']}")
106
-
107
- tok = load_tokenizer(cfg["model"])
108
- model = load_model(cfg["model"], for_training=True)
109
- if cfg["model"]["load_in_4bit"] and cfg["model"].get("text_only"):
110
- # peft's version would cast Gemma 4's 2.8 B-parameter embedding table to float32 (10.5 GiB -> OOM on a T4)
111
- model = prepare_for_kbit_training(model, use_gradient_checkpointing=tc["gradient_checkpointing"])
112
- elif cfg["model"]["load_in_4bit"]:
113
- model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=tc["gradient_checkpointing"])
114
- elif tc["gradient_checkpointing"]:
115
- model.gradient_checkpointing_enable()
116
- model.enable_input_require_grads()
117
-
118
- lora = LoraConfig(
119
- r=tc["lora_r"],
120
- lora_alpha=tc["lora_alpha"],
121
- lora_dropout=tc["lora_dropout"],
122
- target_modules=tc["target_modules"],
123
- task_type="CAUSAL_LM",
124
- )
125
- model = get_peft_model(model, lora)
126
- trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
127
- print(f"[lora] adapter parameters: {trainable:,}")
128
-
129
- examples = build_examples(train_records, tok, cfg)
130
- steps_per_epoch = math.ceil(len(examples) / (tc["batch_size"] * tc["grad_accum"]))
131
- total_steps = args.max_steps if args.max_steps > 0 else steps_per_epoch * tc["epochs"]
132
- out_dir = Path(tc["output_dir"])
133
- dtype = pick_dtype(cfg["model"]["dtype"])
134
- on_gpu = torch.cuda.is_available()
135
- targs = TrainingArguments(
136
- output_dir=str(out_dir / "checkpoints"),
137
- num_train_epochs=tc["epochs"],
138
- max_steps=args.max_steps,
139
- learning_rate=tc["learning_rate"],
140
- per_device_train_batch_size=tc["batch_size"],
141
- gradient_accumulation_steps=tc["grad_accum"],
142
- lr_scheduler_type="cosine",
143
- warmup_steps=math.ceil(0.03 * total_steps), # 3% warm-up
144
- logging_steps=tc["logging_steps"],
145
- save_strategy="no",
146
- report_to=[],
147
- seed=cfg["seed"],
148
- data_seed=cfg["seed"],
149
- bf16=on_gpu and dtype == torch.bfloat16,
150
- fp16=on_gpu and dtype == torch.float16,
151
- remove_unused_columns=False,
152
- dataloader_num_workers=0,
153
- )
154
- trainer = Trainer(model=model, args=targs, train_dataset=examples, data_collator=Collator(tok.pad_token_id))
155
- trainer.train()
156
-
157
- adapter_dir = out_dir / "adapter"
158
- model.save_pretrained(adapter_dir)
159
- tok.save_pretrained(adapter_dir)
160
- shutil.copy(args.config, out_dir / "train_config.yaml")
161
- total = sum(p.numel() for p in model.parameters())
162
- print(f"[save] adapter -> {adapter_dir}")
163
- print(f"[budget] base + adapter as loaded: {describe_budget(total)}")
164
-
165
- extra = {"train_file": tc["train_file"], "n_train": len(examples), "adapter_params": trainable}
166
- if tc["eval_file"]:
167
- model.eval()
168
- eval_records = load_questions(tc["eval_file"], cfg["data"]["answer_base"])
169
- preds = Scorer(model, tok, cfg).predict(eval_records)
170
- result = accuracy_by_level([r["level"] for r in eval_records], preds, [r["gold"] for r in eval_records])
171
- print(f"[eval] {tc['eval_file']}\n" + format_report(result))
172
- cfg_logged = json.loads(json.dumps(cfg))
173
- cfg_logged["model"]["adapter_path"] = str(adapter_dir)
174
- append_result(cfg_logged, tc["eval_file"], result, args.who, "after training")
175
- extra["eval_result"] = result
176
- write_run_info(out_dir / "train.run.json", cfg, extra)
177
- return 0
178
-
179
-
180
- if __name__ == "__main__":
181
- raise SystemExit(main())