Download model.py from alstrup/modmul-challenge: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/alstrup/modmul-challenge/resolve/main/model.py
- Command line
-
hf download hf://alstrup/modmul-challenge/model.py
-
curl -L -o model.py https://huggingface.co/alstrup/modmul-challenge/resolve/main/model.py
10.4 kB
| """Challenge-interface wrapper for the modmul BP-install model. | |
| Implements ModularMultiplicationModel. Inference: | |
| 1. per-argument: parse a, b, p to ints (each hook sees only its own arg) | |
| 2. predict_digits: reduce x=a%p, y=b%p (legal two-operand reduction), and if | |
| p is within the model's trained width, greedy-decode the reverse-LSB answer | |
| digits from the network; otherwise emit [0] (honest out-of-regime fallback). | |
| The answer comes entirely from the trained network on in-regime primes: | |
| randomising the weights collapses accuracy. output_base = 10. | |
| """ | |
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| import torch | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| # Self-contained: ByteGPT + build_model + the entmax15 recipe are VENDORED into | |
| # this directory (coppola_pretrain_tiny.py, coppola_pretraining.py, | |
| # train_arith_bp_supervised.py), so the submission loads with only `torch` | |
| # available and read access limited to its own dir (the eval sandbox contract). | |
| # entmax15 falls back to a local forward-exact impl when the `entmax` pip | |
| # package is absent. | |
| from train_arith_bp_supervised import TrainConfig, build_model # noqa: E402 | |
| import encoding as enc # noqa: E402 | |
| import composed_encoding as cenc # noqa: E402 (composed multiply+reduce CoT) | |
| import kvgen # noqa: E402 (KV-cached generation; falls back to naive loop) | |
| from modchallenge.interface.base_model import ModularMultiplicationModel # noqa: E402 | |
| class ModMulBP(ModularMultiplicationModel): | |
| def __init__(self): | |
| self.model = None | |
| self.W = 1 | |
| self.device = None | |
| self.regime = 10 # max p exclusive = 10**W | |
| self.scratchpad = False | |
| self.school = False | |
| def load(self, model_dir: str, weights: str = "weights.pt") -> None: | |
| torch.manual_seed(0) # determinism is the model's responsibility (rules) | |
| self.device = "cuda" if torch.cuda.is_available() else "cpu" | |
| ckpt = torch.load(Path(model_dir) / weights, | |
| map_location=self.device, weights_only=False) | |
| tc = TrainConfig(**ckpt["config"]) | |
| self.model = build_model(tc, self.device) | |
| self.model.load_state_dict(ckpt["state_dict"]) | |
| self.model.eval() | |
| self.W = ckpt["W"] | |
| self.scratchpad = bool(ckpt.get("scratchpad", False)) | |
| self.school = bool(ckpt.get("school", False)) | |
| self.composed = bool(ckpt.get("composed", False)) | |
| if self.composed: | |
| self.base = ckpt["base"] | |
| self.scratch = bool(ckpt.get("scratch", False)) | |
| self.cursor = bool(ckpt.get("cursor", False)) | |
| self.subpad = bool(ckpt.get("subpad", False)) | |
| self.stepidx = bool(ckpt.get("stepidx", False)) | |
| self.skiptriv = bool(ckpt.get("skiptriv", False)) | |
| self.subnum = bool(ckpt.get("subnum", False)) | |
| self.bemit = bool(ckpt.get("bemit", False)) | |
| self.srt = bool(ckpt.get("srt", False)) | |
| self.regime = self.base ** self.W | |
| else: | |
| self.regime = 10 ** self.W | |
| self.mulonly = bool(ckpt.get("mulonly", False)) | |
| if self.mulonly: | |
| # Tier-0 member: pure multiplication. p never enters the trace -- | |
| # the operand width is the only constraint, so claim every p the | |
| # specialists don't (router sorts by regime, this sorts last). | |
| self.regime = 10 ** (2 * self.W) | |
| self.p_lo, self.p_hi = 2, 2 ** 4096 | |
| return | |
| # Trained prime span (for router dispatch): derived from the ckpt's | |
| # tier list via the official tier geometry; fallback = full regime. | |
| self.p_lo, self.p_hi = 2, self.regime - 1 | |
| try: | |
| from modchallenge.config import TIERS | |
| spans = [(2 ** TIERS[t].min_bits, 2 ** TIERS[t].max_bits - 1) | |
| for t in ckpt.get("tiers", [])] | |
| if spans: | |
| self.p_lo = min(lo for lo, _ in spans) | |
| self.p_hi = min(max(hi for _, hi in spans), self.regime - 1) | |
| except Exception: | |
| pass | |
| # per-argument preprocessing (each sees only its own argument) | |
| def preprocess_a(self, a: str) -> int: | |
| return int(a) | |
| def preprocess_b(self, b: str) -> int: | |
| return int(b) | |
| def preprocess_p(self, p: str) -> int: | |
| return int(p) | |
| def predict_digits(self, a_enc, b_enc, p_enc): | |
| return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0] | |
| def predict_digits_batch(self, inputs): | |
| out = [[0]] * len(inputs) | |
| if self.mulonly: | |
| prompt_fn = lambda p, x, y, W: cenc.prompt_str( # noqa: E731 | |
| x, y, p, W, 10, mulonly=True) | |
| n_gen = cenc.gen_len(self.W, mulonly=True) | |
| decode_fn = lambda g, W: cenc.decode_answer( # noqa: E731 | |
| g, W, 10, mulonly=True) | |
| elif self.composed: | |
| prompt_fn = lambda p, x, y, W: cenc.prompt_str( # noqa: E731 | |
| x, y, p, W, self.base, self.subpad) | |
| n_gen = cenc.gen_len(self.W, self.scratch, self.cursor, self.subpad, | |
| self.stepidx, self.skiptriv, self.subnum, | |
| self.bemit, self.srt) | |
| decode_fn = lambda g, W: cenc.decode_answer( # noqa: E731 | |
| g, W, self.base, self.scratch, self.cursor, self.subpad, | |
| self.stepidx, self.skiptriv, self.subnum, self.bemit, self.srt) | |
| else: | |
| cot = self.scratchpad or self.school | |
| prompt_fn = enc.prompt_str_sp if cot else enc.prompt_str | |
| n_gen = (enc.school_gen_len(self.W) if self.school | |
| else enc.scratchpad_len(self.W) if self.scratchpad | |
| else enc.answer_len(self.W)) | |
| decode_fn = (enc.decode_answer_school if self.school | |
| else enc.decode_answer_sp if self.scratchpad | |
| else enc.decode_answer) | |
| prompts, idx = [], [] | |
| for i, (a, b, p) in enumerate(inputs): | |
| x, y = a % p, b % p | |
| if self.mulonly: | |
| if max(x, y) >= 10 ** self.W: # operands don't fit -> honest 0 | |
| continue | |
| elif p >= self.regime: # outside trained width -> honest 0 | |
| continue | |
| prompts.append(prompt_fn(p, x, y, self.W)) | |
| idx.append((i, x, y, p)) | |
| if not prompts: | |
| return out | |
| # All in-regime prompts share the same length (fixed width) -> batchable. | |
| # latin1: composed prompts carry limb bytes >127 (limbs.py codec). | |
| ids = torch.tensor([list(s.encode("latin1")) for s in prompts], | |
| dtype=torch.long, device=self.device) | |
| plen = len(prompts[0]) | |
| # KV-cached generation: the naive loop re-forwards the whole prefix per | |
| # token, which blows the 5-min/1100-problem budget on long CoTs. kvgen | |
| # is the same computation on the same weights, token-identical | |
| # (validated); fall back to the naive loop on unsupported configs. | |
| try: | |
| gens = kvgen.generate_kv(self.model, ids, n_gen).tolist() | |
| except AssertionError: | |
| seq_cap = self.model.config.seq_len | |
| for _k in range(n_gen): | |
| logits, _ = self.model(ids[:, -seq_cap:]) | |
| nxt = logits[:, -1].argmax(dim=-1, keepdim=True) | |
| ids = torch.cat([ids, nxt], dim=1) | |
| gens = ids[:, plen:].tolist() | |
| for row, (i, x, y, p) in zip(gens, idx): | |
| gen = bytes(b & 0xFF for b in row).decode("latin1") | |
| # No arithmetic touch-up of the model's answer: a decoded value >= p | |
| # would be malformed (scored incorrect anyway), so emit the honest | |
| # [0] fallback instead of clamping with % p. | |
| ans = decode_fn(gen, self.W) | |
| if 0 <= ans < p: | |
| out[i] = [int(c) for c in str(ans)] | |
| return out | |
| def max_batch_size(self) -> int: | |
| return 512 | |
| class ModMulRouter(ModularMultiplicationModel): | |
| """Routes each problem to the most specialized member model by prime | |
| magnitude. Members are weights_r*.pt files (sorted name order); each is a | |
| full ModMulBP checkpoint with its own trained regime. A problem goes to | |
| the FIRST member whose regime covers its p; out-of-regime problems emit | |
| the honest [0]. | |
| Compliance: routing keys on the SIZE of p only (per-argument | |
| representation work, like base conversion); every answer comes from a | |
| trained member's generated digits. | |
| """ | |
| def __init__(self): | |
| self.members: list[ModMulBP] = [] | |
| def load(self, model_dir: str) -> None: | |
| torch.manual_seed(0) | |
| for f in sorted(Path(model_dir).glob("weights_r*.pt")): | |
| m = ModMulBP() | |
| m.load(model_dir, weights=f.name) | |
| self.members.append(m) | |
| assert self.members, "router needs weights_r*.pt member checkpoints" | |
| self.members.sort(key=lambda m: m.regime) # most specialized first | |
| def preprocess_a(self, a: str) -> int: | |
| return int(a) | |
| def preprocess_b(self, b: str) -> int: | |
| return int(b) | |
| def preprocess_p(self, p: str) -> int: | |
| return int(p) | |
| def predict_digits(self, a_enc, b_enc, p_enc): | |
| return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0] | |
| def predict_digits_batch(self, inputs): | |
| out = [[0]] * len(inputs) | |
| groups: dict[int, list[int]] = {} | |
| for i, (_a, _b, p) in enumerate(inputs): | |
| # Prefer the member whose TRAINED prime span contains p; fall back | |
| # to the most specialized member whose regime merely covers it. | |
| mi = next((k for k, m in enumerate(self.members) | |
| if m.p_lo <= p <= m.p_hi), None) | |
| if mi is None: | |
| mi = next((k for k, m in enumerate(self.members) if p < m.regime), None) | |
| if mi is not None: | |
| groups.setdefault(mi, []).append(i) | |
| for mi, idxs in groups.items(): | |
| sub = [inputs[i] for i in idxs] | |
| res = self.members[mi].predict_digits_batch(sub) | |
| for i, r in zip(idxs, res): | |
| out[i] = r | |
| return out | |
| def max_batch_size(self) -> int: | |
| return 512 | |