Adding 4 files
Browse filesAdding 4 files for final handover
- README.md +45 -0
- inference.py +349 -0
- predictions.jsonl +0 -0
- requirements.txt +10 -0
README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Team AlphaPack — SinhalaMMLU Gemma 4 E4B V2
|
| 2 |
+
|
| 3 |
+
**Status: audited submission CANDIDATE; full clean-room prediction reproduction NOT yet demonstrated.**
|
| 4 |
+
Team identifier: AlphaPack. **Before submission: add your real team-member details and confirm there are no team adapters/extra training assets omitted.**
|
| 5 |
+
|
| 6 |
+
## Submission
|
| 7 |
+
|
| 8 |
+
- Base model: [`google/gemma-4-E4B-it`](https://huggingface.co/google/gemma-4-E4B-it)
|
| 9 |
+
- Revision: `ee0ef6023621cff504d758262d4e04895a5af4a2`
|
| 10 |
+
- Unique inference-time parameter count verified in the source recovery notebook: **7,941,100,832** (7.9411B; below 8,000,000,000), including vision/audio modules. **No additional trainable inference adapter is included in this package.**
|
| 11 |
+
- Technique: native Gemma 4 thinking (`enable_thinking=True`) + exact **V2 explanation-first / last-line `ANSWER: X`** prompt, with automatic invalid-answer retries. The text is SHA-256 pinned in `inference.py`.
|
| 12 |
+
- Decoding: greedy (`do_sample=False`); first pass batch size 4, 2,048 new tokens; strict targeted retries batch size 1, 4,096 new tokens. The first-pass settings are reconstructed from research notebook material, **not independently proven identical to the historical all-2,206 run**.
|
| 13 |
+
- Hardware used for historical research: Kaggle **NVIDIA T4 ×2**; FP16; SDPA. Original targeted retry set a four-hour run limit, not a measured full-set runtime. **Actual complete fresh-run runtime: not yet measured.**
|
| 14 |
+
- Seed: 2026 (reconstructed setting; confirm against initial production records).
|
| 15 |
+
- Dataset used for evaluation: SinhalaMMLU Shared Task public questions, downloaded by the script from `DATA_REPO`.
|
| 16 |
+
- Additional training / retrieval data: **none in this system package**. Confirm any other teammates' work before publication.
|
| 17 |
+
- Team member details: **Team AlphaPack — add member names here before publication**.
|
| 18 |
+
|
| 19 |
+
## Files
|
| 20 |
+
|
| 21 |
+
- `inference.py` — best-effort self-contained fresh inference candidate; loads every `.json` in `DATA_REPO`, downloads pinned model from HF, uses native thinking and V2 prompt, retries invalid answers, and writes `predictions.jsonl` in the required numeric-label format.
|
| 22 |
+
- `predictions.jsonl` — **unchanged** original user-provided 2,238-row final submission candidate; SHA-256 `dcf8cf70066f06b56c319a5eb6604bbeb823cc70096ab8a5c40e8c6d05e06b00`.
|
| 23 |
+
- `requirements.txt` — reconstructed package versions from the research environment.
|
| 24 |
+
- `recovery/evidence/` — independently verified historical first-pass 2,206 + 32 failures package, merge audit, hashes and merge source code.
|
| 25 |
+
- `recovery/MIDDLE_E4B_RETRY32_COLDSTART.ipynb` and `.py` — verbatim original 32-question historical recovery code, requiring the original combined-2,206 package and 41 unchanged JSON question files as inputs. Its source contains the final strict parser, per-question logs, model hash, dataset checks and successful-only final export logic. **This notebook has no embedded execution outputs.**
|
| 26 |
+
- `training/README.md` and `data/README.md` — no fine-tuning, adapters or extra training data claimed; review if other team components were used.
|
| 27 |
+
- `REPRODUCIBILITY_STATUS.md` — limitations and outstanding evidence.
|
| 28 |
+
- `check_submission.py`, `verify_final_predictions.py` — file integrity / output validation helpers.
|
| 29 |
+
|
| 30 |
+
## Fresh environment reproduction
|
| 31 |
+
|
| 32 |
+
1. On Kaggle, choose **GPU T4 ×2**, enable Internet, and accept the Gemma license through your Hugging Face account if required. The model is gated. Authenticate externally through `HF_TOKEN` or pre-existing Hugging Face login; never place tokens in code.
|
| 33 |
+
2. Install `pip install -r requirements.txt`.
|
| 34 |
+
3. The organizers change **only `DATA_REPO`** in `inference.py`; then run `python inference.py` from a clean working directory. It downloads the model from Hugging Face and produces `predictions.jsonl`.
|
| 35 |
+
4. Keep an immutable copy of this repository's original predictions outside the run directory. Running the script overwrites the `predictions.jsonl` in its working directory.
|
| 36 |
+
5. Compare the reproduced predictions to the original 2,238-row file with `python check_submission.py ORIGINAL.jsonl GENERATED.jsonl`. Byte-identical SHA is the strongest evidence; report any differences, never conceal them.
|
| 37 |
+
|
| 38 |
+
## Critical reproducibility limitations
|
| 39 |
+
|
| 40 |
+
The original `E4B_COMBINED_2206_WITH_FAILED32.zip` is now included under `recovery/evidence/`. It provides the original 2,206 valid predictions, 32 failed attempts, 32 raw failure outputs, merged first-pass audit, dataset SHA-256 manifest and original merge script. An independent offline audit verified **all 2,206 entries** match `predictions.jsonl` exactly and the remaining **32 indices** are disjoint and have matching question metadata. The first-pass merge audit confirms the same V2 prompt hash as RETRY32. **Three original shard ZIPs are still not provided**, although their names and SHA-256 hashes are recorded. The complete original first-pass generator is still not available as one final standalone script; the integrated `inference.py` is reconstructed, and a full clean-room GPU run has not been executed here. Clean-room inference has **not** been run or checked against the exact submitted file here. This README is intentionally not a claim of exact output reproducibility.
|
| 41 |
+
|
| 42 |
+
## Required repository links
|
| 43 |
+
|
| 44 |
+
- System repository: `https://huggingface.co/SinhalaMMLU-SharedTask/team-AlphaPack`
|
| 45 |
+
- Model weights: `https://huggingface.co/google/gemma-4-E4B-it` — pinned revision above; no fine-tuned adapter claimed.
|
inference.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SinhalaMMLU E4B V2 standalone inference + automatic invalid-answer retry.
|
| 2 |
+
|
| 3 |
+
CANDIDATE reconstructed from archived research code and original RETRY32.
|
| 4 |
+
NOT certified to regenerate byte-identical historical 2,238 predictions.
|
| 5 |
+
The exact first-pass 2,206-answer production script is not provided.
|
| 6 |
+
|
| 7 |
+
Organizers change ONLY DATA_REPO. All model resources come from Hugging Face.
|
| 8 |
+
Never access reference predictions or test answer keys during generation.
|
| 9 |
+
"""
|
| 10 |
+
# ===================== ORGANIZERS CHANGE ONLY THIS LINE =====================
|
| 11 |
+
DATA_REPO = "SinhalaMMLU-SharedTask/Sinhala-MMLU-SharedTask-Test-Dataset"
|
| 12 |
+
# ===========================================================================
|
| 13 |
+
|
| 14 |
+
MODEL_REPO = "google/gemma-4-E4B-it"
|
| 15 |
+
REVISION = "ee0ef6023621cff504d758262d4e04895a5af4a2"
|
| 16 |
+
WEIGHT_SHA256 = "cfbd3d2f1cd71bd471c37fe2bf8546d5028d41e5736f64e1ca6c6b8893125503"
|
| 17 |
+
EXPECTED_PARAMS = 7_941_100_832
|
| 18 |
+
PROMPT_SHA256 = "f53159bafe22a5d0063a065b6d3aef8167e7a6d6b998ce65e224feeeca68287d"
|
| 19 |
+
BATCH_SIZE = 4
|
| 20 |
+
FIRST_PASS_TOKENS = 2048
|
| 21 |
+
RETRY_TOKENS = 4096
|
| 22 |
+
SEED = 2026
|
| 23 |
+
OUTPUT_FILE = "predictions.jsonl"
|
| 24 |
+
OUTPUT_DIR = "inference_records"
|
| 25 |
+
|
| 26 |
+
import hashlib
|
| 27 |
+
import json
|
| 28 |
+
import logging
|
| 29 |
+
import os
|
| 30 |
+
import random
|
| 31 |
+
import re
|
| 32 |
+
import time
|
| 33 |
+
from pathlib import Path
|
| 34 |
+
|
| 35 |
+
import numpy as np
|
| 36 |
+
import torch
|
| 37 |
+
from huggingface_hub import HfFileSystem, list_repo_files, snapshot_download
|
| 38 |
+
|
| 39 |
+
V2_PROMPT = (
|
| 40 |
+
'Answer this Sinhala multiple-choice question. '
|
| 41 |
+
'Use your reasoning to select the correct option. '
|
| 42 |
+
'In your final response, provide a brief explanation '
|
| 43 |
+
'in English, approximately 90 words. '
|
| 44 |
+
'Then put ANSWER: X on the last line, '
|
| 45 |
+
'where X is A, B, C, or D. '
|
| 46 |
+
'Do not change the selected answer.'
|
| 47 |
+
)
|
| 48 |
+
assert hashlib.sha256(V2_PROMPT.encode('utf-8')).hexdigest() == PROMPT_SHA256
|
| 49 |
+
LETTER_TO_NUMBER = {'A':'1','B':'2','C':'3','D':'4'}
|
| 50 |
+
FIELDS = ('q_no','subject','category','level','answer')
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def sha_file(path):
|
| 54 |
+
h = hashlib.sha256()
|
| 55 |
+
with open(path, 'rb') as f:
|
| 56 |
+
for block in iter(lambda: f.read(8*1024*1024), b''):
|
| 57 |
+
h.update(block)
|
| 58 |
+
return h.hexdigest()
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def save_atomic(path, data):
|
| 62 |
+
path = Path(path)
|
| 63 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 64 |
+
temp = path.with_suffix(path.suffix+'.tmp')
|
| 65 |
+
temp.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
| 66 |
+
os.replace(temp,path)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def init_seed():
|
| 70 |
+
random.seed(SEED)
|
| 71 |
+
np.random.seed(SEED)
|
| 72 |
+
torch.manual_seed(SEED)
|
| 73 |
+
if torch.cuda.is_available():
|
| 74 |
+
torch.cuda.manual_seed_all(SEED)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def load_questions(data_repo):
|
| 78 |
+
"""Load every .json file per organizer template; preserve source position."""
|
| 79 |
+
fs = HfFileSystem()
|
| 80 |
+
files = sorted(f for f in list_repo_files(data_repo, repo_type='dataset') if f.endswith('.json'))
|
| 81 |
+
rows = []
|
| 82 |
+
for f in files:
|
| 83 |
+
with fs.open(f'datasets/{data_repo}/{f}','r',encoding='utf-8') as fh:
|
| 84 |
+
raw = json.load(fh)
|
| 85 |
+
if isinstance(raw,dict):
|
| 86 |
+
raw = next(v for v in raw.values() if isinstance(v,list))
|
| 87 |
+
assert isinstance(raw,list),f
|
| 88 |
+
for position,r in enumerate(raw):
|
| 89 |
+
r = dict(r)
|
| 90 |
+
r['_file'] = f
|
| 91 |
+
r['_position'] = position
|
| 92 |
+
rows.append(r)
|
| 93 |
+
if not rows:
|
| 94 |
+
raise ValueError('No questions found in DATA_REPO')
|
| 95 |
+
return rows
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def get_level(r):
|
| 99 |
+
return (r.get('metadata') or {}).get('difficulty') or r['_file'].split('/')[0]
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def canonical_order(rows):
|
| 103 |
+
"""Historical final file: Easy, Medium, Hard; source basename, row order.
|
| 104 |
+
|
| 105 |
+
Do not hard-code question count, index or test answers.
|
| 106 |
+
This is deliberately separate from the organizer's loader.
|
| 107 |
+
"""
|
| 108 |
+
priority = {'easy':0,'medium':1,'hard':2}
|
| 109 |
+
for row in rows:
|
| 110 |
+
if get_level(row) not in priority:
|
| 111 |
+
raise ValueError(f'Unknown difficulty {get_level(row)}')
|
| 112 |
+
if len(row['choices']) != 4 or not all(isinstance(c,str) for c in row['choices']):
|
| 113 |
+
raise ValueError('Expected exactly four string choices')
|
| 114 |
+
return sorted(rows,key=lambda r:(priority[get_level(r)],Path(r['_file']).name.casefold(),r['_position']))
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def fingerprint(r):
|
| 118 |
+
keys = ('q_no','subject','category','question','choices')
|
| 119 |
+
content = {k:r[k] for k in keys}
|
| 120 |
+
content['level'] = get_level(r)
|
| 121 |
+
content['file'] = r['_file']
|
| 122 |
+
content['source_position'] = r['_position']
|
| 123 |
+
return hashlib.sha256(json.dumps(content,ensure_ascii=False,sort_keys=True,separators=(',',':')).encode('utf-8')).hexdigest()
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def make_prompt(r):
|
| 127 |
+
opts = '\n'.join(f'{letter}. {choice}' for letter,choice in zip('ABCD',r['choices']))
|
| 128 |
+
return V2_PROMPT + '\n\n' + r['question'] + '\n\n' + opts
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def split_tokens(tokenizer, sequence, width, pad_id, stop_ids):
|
| 132 |
+
actual=[]
|
| 133 |
+
ended=False
|
| 134 |
+
for token in sequence[width:].tolist():
|
| 135 |
+
if token == pad_id:
|
| 136 |
+
break
|
| 137 |
+
actual.append(token)
|
| 138 |
+
if token in stop_ids:
|
| 139 |
+
ended=True
|
| 140 |
+
break
|
| 141 |
+
return actual,ended
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def parse_retry_strict(raw, ended, token_count, limit):
|
| 145 |
+
"""Verbatim logic from RETRY32 for final answer and validity."""
|
| 146 |
+
parts = re.split(r'<\|channel>final|<channel\|>',raw)
|
| 147 |
+
final = re.sub(r'<[^>\n]+>','',parts[-1]).strip()
|
| 148 |
+
lines = [x.strip() for x in final.splitlines() if x.strip()]
|
| 149 |
+
last = lines[-1] if lines else ''
|
| 150 |
+
m = re.fullmatch(r'\*{0,2}\s*ANSWER\s*:\s*([ABCD])\s*[.!]?\s*\*{0,2}',last,flags=re.I)
|
| 151 |
+
pred = m.group(1).upper() if m else None
|
| 152 |
+
hit_limit = not ended and token_count>=limit
|
| 153 |
+
valid = pred in LETTER_TO_NUMBER and ended and token_count>0
|
| 154 |
+
status = ('completed' if valid else 'token_limit' if hit_limit else
|
| 155 |
+
'padding_only' if not token_count else 'unparseable_or_unterminated')
|
| 156 |
+
return pred if valid else None,status,last
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def parse_first_pass(raw):
|
| 160 |
+
"""Candidate first-pass parser; exact historical 2206 parser not supplied.
|
| 161 |
+
|
| 162 |
+
Do not parse thought tokens if final channel missing.
|
| 163 |
+
"""
|
| 164 |
+
if ('<|channel>thought' in raw or '<channel|>thought' in raw) and not (
|
| 165 |
+
'<|channel>final' in raw or '<channel|>final' in raw
|
| 166 |
+
):
|
| 167 |
+
return None
|
| 168 |
+
parts=re.split(r'<\|channel>final|<channel\|>',raw)
|
| 169 |
+
final=re.sub(r'<[^>\n]+>','',parts[-1]).strip()
|
| 170 |
+
lines=[x.strip() for x in final.splitlines() if x.strip()]
|
| 171 |
+
if not lines:
|
| 172 |
+
return None
|
| 173 |
+
m=re.fullmatch(r'\*{0,2}\s*ANSWER\s*:\s*([ABCD])\s*[.!]?\s*\*{0,2}',lines[-1],flags=re.I)
|
| 174 |
+
return m.group(1).upper() if m else None
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def model_device_map(model_dir):
|
| 178 |
+
from transformers import AutoConfig, AutoModelForMultimodalLM
|
| 179 |
+
from accelerate import init_empty_weights
|
| 180 |
+
cfg=AutoConfig.from_pretrained(str(model_dir),local_files_only=True)
|
| 181 |
+
with init_empty_weights():
|
| 182 |
+
skeleton=AutoModelForMultimodalLM.from_config(cfg,torch_dtype=torch.float16)
|
| 183 |
+
root='model.language_model.layers'
|
| 184 |
+
assert len(dict(skeleton.named_modules())[root])==42
|
| 185 |
+
decoders={f'{root}.{i}' for i in range(42)}
|
| 186 |
+
dm={}
|
| 187 |
+
def includes_decoder(name):
|
| 188 |
+
prefix=name+'.' if name else ''
|
| 189 |
+
return any(d==name or d.startswith(prefix) for d in decoders)
|
| 190 |
+
def assign(mod,name=''):
|
| 191 |
+
if name in decoders:
|
| 192 |
+
dm[name]='cuda:1'
|
| 193 |
+
return
|
| 194 |
+
if name and not includes_decoder(name):
|
| 195 |
+
dm[name]='cuda:0'
|
| 196 |
+
return
|
| 197 |
+
assert not list(mod.named_parameters(recurse=False))
|
| 198 |
+
assert not list(mod.named_buffers(recurse=False))
|
| 199 |
+
for childname,child in mod.named_children():
|
| 200 |
+
assign(child,f'{name}.{childname}' if name else childname)
|
| 201 |
+
assign(skeleton)
|
| 202 |
+
assert len(dm)==53 and '' not in dm
|
| 203 |
+
for name,_ in list(skeleton.named_parameters())+list(skeleton.named_buffers()):
|
| 204 |
+
assert sum(name==k or name.startswith(k+'.') for k in dm)==1,name
|
| 205 |
+
del skeleton
|
| 206 |
+
return dm
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def load_system():
|
| 210 |
+
from transformers import AutoProcessor, AutoModelForMultimodalLM
|
| 211 |
+
assert torch.cuda.device_count()==2, 'Original system requires T4 x2'
|
| 212 |
+
model_dir = Path(OUTPUT_DIR)/'model_download'
|
| 213 |
+
model_dir.mkdir(parents=True,exist_ok=True)
|
| 214 |
+
# Downloads original checkpoint and processor from HF (never from Kaggle Input/Drive).
|
| 215 |
+
snapshot_download(repo_id=MODEL_REPO,revision=REVISION,local_dir=str(model_dir),token=os.getenv('HF_TOKEN') or None)
|
| 216 |
+
weights = model_dir/'model.safetensors'
|
| 217 |
+
assert weights.is_file() and weights.stat().st_size==15992595884
|
| 218 |
+
assert sha_file(weights)==WEIGHT_SHA256,'Pinned model weights SHA256 mismatch'
|
| 219 |
+
processor=AutoProcessor.from_pretrained(str(model_dir),local_files_only=True)
|
| 220 |
+
processor.tokenizer.padding_side='left'
|
| 221 |
+
dm=model_device_map(model_dir)
|
| 222 |
+
model=AutoModelForMultimodalLM.from_pretrained(
|
| 223 |
+
str(model_dir),local_files_only=True,dtype=torch.float16,
|
| 224 |
+
attn_implementation='sdpa',device_map=dm,low_cpu_mem_usage=True).eval()
|
| 225 |
+
assert type(model).__name__=='Gemma4ForConditionalGeneration'
|
| 226 |
+
assert sum(p.numel() for p in model.parameters())==EXPECTED_PARAMS
|
| 227 |
+
assert not model.training
|
| 228 |
+
tok=processor.tokenizer
|
| 229 |
+
pad=tok.pad_token_id
|
| 230 |
+
turn=tok.convert_tokens_to_ids('<turn|>')
|
| 231 |
+
assert pad is not None and tok.decode([turn],skip_special_tokens=False)=='<turn|>'
|
| 232 |
+
eos=model.generation_config.eos_token_id
|
| 233 |
+
stop=({eos} if isinstance(eos,int) else set(eos or []))|{turn}
|
| 234 |
+
return processor,model,pad,stop
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def run_batch(system, rows, limit):
|
| 238 |
+
processor,model,pad,stop=system
|
| 239 |
+
tok=processor.tokenizer
|
| 240 |
+
msgs=[[{'role':'user','content':[{'type':'text','text':make_prompt(row)}]}] for row in rows]
|
| 241 |
+
encoded=processor.apply_chat_template(msgs,tokenize=True,add_generation_prompt=True,
|
| 242 |
+
enable_thinking=True,padding=True,return_dict=True,return_tensors='pt').to('cuda:0')
|
| 243 |
+
width=encoded['input_ids'].shape[1]
|
| 244 |
+
for i in range(2):torch.cuda.synchronize(i)
|
| 245 |
+
started=time.perf_counter()
|
| 246 |
+
with torch.inference_mode():
|
| 247 |
+
seqs=model.generate(**encoded,max_new_tokens=limit,do_sample=False,
|
| 248 |
+
use_cache=True,pad_token_id=pad)
|
| 249 |
+
for i in range(2):torch.cuda.synchronize(i)
|
| 250 |
+
elapsed=time.perf_counter()-started
|
| 251 |
+
results=[]
|
| 252 |
+
for j,row in enumerate(rows):
|
| 253 |
+
ids,ended=split_tokens(tok,seqs[j],width,pad,stop)
|
| 254 |
+
raw=tok.decode(ids,skip_special_tokens=False)
|
| 255 |
+
if limit==RETRY_TOKENS:
|
| 256 |
+
pred,status,last=parse_retry_strict(raw,ended,len(ids),limit)
|
| 257 |
+
else:
|
| 258 |
+
pred=parse_first_pass(raw)
|
| 259 |
+
status='completed' if pred else 'invalid_first_pass'
|
| 260 |
+
last=''
|
| 261 |
+
results.append({'fingerprint':fingerprint(row),'prediction':pred,'status':status,
|
| 262 |
+
'raw_output':raw,'generated_tokens':len(ids),'terminated':bool(ended),
|
| 263 |
+
'max_new_tokens':limit,'batch_size':len(rows),'elapsed_batch_seconds':round(elapsed,3),
|
| 264 |
+
'last_line':last})
|
| 265 |
+
return results
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def predict_all(system,rows):
|
| 269 |
+
root=Path(OUTPUT_DIR)
|
| 270 |
+
root.mkdir(parents=True,exist_ok=True)
|
| 271 |
+
config={'model':MODEL_REPO,'revision':REVISION,'prompt_sha256':PROMPT_SHA256,
|
| 272 |
+
'parameters':EXPECTED_PARAMS,'native_thinking':True,'batch_size':BATCH_SIZE,
|
| 273 |
+
'first_pass_tokens':FIRST_PASS_TOKENS,'retry_tokens':RETRY_TOKENS,
|
| 274 |
+
'decoding':'greedy','seed':SEED,'rows':len(rows),
|
| 275 |
+
'input_hashes':[fingerprint(r) for r in rows]}
|
| 276 |
+
config_path=root/'configuration.json'
|
| 277 |
+
if config_path.exists():
|
| 278 |
+
assert json.loads(config_path.read_text('utf-8'))==config,'Resume configuration differs'
|
| 279 |
+
else:save_atomic(config_path,config)
|
| 280 |
+
answers={}
|
| 281 |
+
for p in (root/'first_pass').glob('q_*.json'):
|
| 282 |
+
obj=json.loads(p.read_text('utf-8'))
|
| 283 |
+
i=obj['index']
|
| 284 |
+
assert 0<=i<len(rows) and obj['fingerprint']==fingerprint(rows[i])
|
| 285 |
+
assert i not in answers
|
| 286 |
+
answers[i]=obj['prediction']
|
| 287 |
+
for start in range(0,len(rows),BATCH_SIZE):
|
| 288 |
+
# Recreate the ORIGINAL full batch when resuming a partially saved batch.
|
| 289 |
+
# Dropping completed members would change padding and potentially outputs.
|
| 290 |
+
idx=list(range(start,min(start+BATCH_SIZE,len(rows))))
|
| 291 |
+
if all(i in answers for i in idx):continue
|
| 292 |
+
results=run_batch(system,[rows[i] for i in idx],FIRST_PASS_TOKENS)
|
| 293 |
+
for i,obj in zip(idx,results):
|
| 294 |
+
obj['index']=i
|
| 295 |
+
if i in answers:
|
| 296 |
+
assert answers[i]==obj['prediction'],(
|
| 297 |
+
f'Replayed batch disagreed at index {i}; preserve old checkpoint')
|
| 298 |
+
else:
|
| 299 |
+
save_atomic(root/'first_pass'/f'q_{i:05}.json',obj)
|
| 300 |
+
answers[i]=obj['prediction']
|
| 301 |
+
logging.info('First pass %d/%d saved; valid=%d',len(answers),len(rows),sum(v in LETTER_TO_NUMBER for v in answers.values()))
|
| 302 |
+
assert len(answers)==len(rows)
|
| 303 |
+
failed=[i for i in range(len(rows)) if answers[i] not in LETTER_TO_NUMBER]
|
| 304 |
+
logging.info('First pass valid=%d failed=%d',len(rows)-len(failed),len(failed))
|
| 305 |
+
recovered={}
|
| 306 |
+
for i in failed:
|
| 307 |
+
p=root/'retry'/f'q_{i:05}.json'
|
| 308 |
+
if p.exists():
|
| 309 |
+
obj=json.loads(p.read_text('utf-8'))
|
| 310 |
+
assert obj['fingerprint']==fingerprint(rows[i])
|
| 311 |
+
else:
|
| 312 |
+
obj=run_batch(system,[rows[i]],RETRY_TOKENS)[0]
|
| 313 |
+
obj['index']=i
|
| 314 |
+
save_atomic(p,obj)
|
| 315 |
+
recovered[i]=obj['prediction']
|
| 316 |
+
logging.info('Retry idx=%d status=%s valid=%s',i,obj['status'],obj['prediction'] in LETTER_TO_NUMBER)
|
| 317 |
+
combined=[recovered[i] if i in recovered else answers[i] for i in range(len(rows))]
|
| 318 |
+
unresolved=[i for i,pred in enumerate(combined) if pred not in LETTER_TO_NUMBER]
|
| 319 |
+
save_atomic(root/'completion_status.json',{'questions':len(rows),'first_pass_valid':len(rows)-len(failed),
|
| 320 |
+
'retry_targets':len(failed),'recovered_valid':len(failed)-len(unresolved),'still_invalid':unresolved})
|
| 321 |
+
if unresolved:
|
| 322 |
+
raise RuntimeError(f'{len(unresolved)} unresolved final answers; refusing to guess: {unresolved[:30]}')
|
| 323 |
+
return [LETTER_TO_NUMBER[pred] for pred in combined]
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def write_submission(rows,answers):
|
| 327 |
+
assert len(rows)==len(answers)
|
| 328 |
+
path=Path(OUTPUT_FILE)
|
| 329 |
+
tmp=path.with_suffix('.jsonl.tmp')
|
| 330 |
+
with tmp.open('w',encoding='utf-8') as f:
|
| 331 |
+
for r,answer in zip(rows,answers):
|
| 332 |
+
assert answer in ('1','2','3','4')
|
| 333 |
+
item={'q_no':r['q_no'],'subject':r['subject'],'category':r['category'],
|
| 334 |
+
'level':get_level(r),'answer':answer}
|
| 335 |
+
f.write(json.dumps(item,ensure_ascii=False,separators=(',',':'))+'\n')
|
| 336 |
+
os.replace(tmp,path)
|
| 337 |
+
logging.info('Saved %s sha256=%s',path,sha_file(path))
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
if __name__=='__main__':
|
| 341 |
+
logging.basicConfig(filename='inference.log',level=logging.INFO,
|
| 342 |
+
format='%(asctime)s UTC %(levelname)s %(message)s')
|
| 343 |
+
init_seed()
|
| 344 |
+
rows=canonical_order(load_questions(DATA_REPO))
|
| 345 |
+
logging.info('Questions loaded: %d',len(rows))
|
| 346 |
+
system=load_system()
|
| 347 |
+
answers=predict_all(system,rows)
|
| 348 |
+
write_submission(rows,answers)
|
| 349 |
+
print('Complete:',len(answers),'predictions; sha256=',sha_file(OUTPUT_FILE),flush=True)
|
predictions.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Versions recorded in executed Gemma 4 E4B notebook (Kaggle GPU run)
|
| 2 |
+
transformers==5.16.1
|
| 3 |
+
accelerate==1.14.0
|
| 4 |
+
# Kaggle notebook recorded torch 2.11.0+cu128; installing PyTorch with
|
| 5 |
+
# the correct CUDA index may be environment-dependent.
|
| 6 |
+
torch==2.11.0
|
| 7 |
+
huggingface-hub>=0.36,<2
|
| 8 |
+
numpy>=2,<3
|
| 9 |
+
sentencepiece
|
| 10 |
+
safetensors
|