Delete hf_team_repo
Browse files- hf_team_repo/README.md +0 -17
- hf_team_repo/__pycache__/inference.cpython-310.pyc +0 -0
- hf_team_repo/__pycache__/predict.cpython-310.pyc +0 -0
- hf_team_repo/configs/final_bf16_perm4.yaml +0 -16
- hf_team_repo/inference.py +0 -59
- hf_team_repo/predict.py +0 -98
- hf_team_repo/predictions.jsonl +0 -0
- hf_team_repo/requirements.txt +0 -10
- hf_team_repo/sinmmlu/__init__.py +0 -8
- hf_team_repo/sinmmlu/config.py +0 -89
- hf_team_repo/sinmmlu/data.py +0 -221
- hf_team_repo/sinmmlu/metrics.py +0 -33
- hf_team_repo/sinmmlu/modeling.py +0 -126
- hf_team_repo/sinmmlu/prompts.py +0 -140
- hf_team_repo/sinmmlu/runinfo.py +0 -85
- hf_team_repo/sinmmlu/scoring.py +0 -135
- hf_team_repo/training/README_training.md +0 -7
- hf_team_repo/training/ft_gemma4_e4b_real_plus_exp.yaml +0 -33
- hf_team_repo/training/train.py +0 -181
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())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|