| """ModularMultiplicationModel implementation: fixed 2-phase schedule around |
| a trained ReductionCell. This module (and everything it imports from |
| `mac_cell`) is the inference path -- no hand-coded reduction against the |
| challenge prime `p` appears anywhere below or in `cell.py` / `schedule.py` / |
| `digits.py`. Only base-R / base-2 digit decomposition of individual |
| arguments (explicitly permitted inside a per-argument preprocessing hook), |
| embeddings, GRU, linear layers, and argmax. |
| |
| At packaging time this file is copied verbatim into the submission |
| directory as `model.py` (top-level, matching `entry_class = "model.MacCellModel"`), |
| alongside a copied `mac_cell/` subpackage -- the submission is fully |
| self-contained and reads nothing outside its own directory. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| from pathlib import Path |
| from typing import Any |
|
|
| import torch |
|
|
| from mac_cell.cell import P_BITS, ReductionCell |
| from mac_cell.digits import digits_needed, int_to_digits_truncating |
| from mac_cell.schedule import rollout |
|
|
| from modchallenge.interface.base_model import ModularMultiplicationModel |
|
|
| |
| |
| |
| |
| |
| DEFAULT_P_BITS = P_BITS |
| DEFAULT_OPERAND_BITS = 48 |
|
|
|
|
| class MacCellModel(ModularMultiplicationModel): |
| def load(self, model_dir: str) -> None: |
| model_dir_path = Path(model_dir) |
| config = json.loads((model_dir_path / "model_config.json").read_text()) |
|
|
| self.radix = config["radix"] |
| self.p_bits = config.get("p_bits", DEFAULT_P_BITS) |
| operand_bits = config.get("operand_bits", DEFAULT_OPERAND_BITS) |
| self.state_digits = digits_needed(2**self.p_bits, self.radix) |
| self.operand_digits = digits_needed(2**operand_bits, self.radix) |
|
|
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| self.cell = ReductionCell( |
| radix=self.radix, |
| state_digits=self.state_digits, |
| hidden_size=config["hidden_size"], |
| digit_embed_dim=config["digit_embed_dim"], |
| num_layers=config["num_layers"], |
| p_bits=self.p_bits, |
| ).to(self.device) |
| state_dict = torch.load( |
| model_dir_path / "weights.pt", map_location=self.device, weights_only=True |
| ) |
| self.cell.load_state_dict(state_dict) |
| self.cell.eval() |
|
|
| def preprocess_a(self, a: str) -> Any: |
| return int_to_digits_truncating(int(a), self.radix, self.operand_digits) |
|
|
| def preprocess_b(self, b: str) -> Any: |
| return int_to_digits_truncating(int(b), self.radix, self.operand_digits) |
|
|
| def preprocess_p(self, p: str) -> Any: |
| return int_to_digits_truncating(int(p), 2, self.p_bits) |
|
|
| def predict_digits(self, a_enc: Any, b_enc: Any, p_enc: Any) -> list[int]: |
| return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0] |
|
|
| @torch.no_grad() |
| def predict_digits_batch( |
| self, inputs: list[tuple[Any, Any, Any]] |
| ) -> list[list[int]]: |
| a_digits = torch.tensor( |
| [a for a, _, _ in inputs], dtype=torch.long, device=self.device |
| ) |
| b_digits = torch.tensor( |
| [b for _, b, _ in inputs], dtype=torch.long, device=self.device |
| ) |
| p_bits = torch.tensor( |
| [p for _, _, p in inputs], dtype=torch.float32, device=self.device |
| ) |
| state = rollout(self.cell, a_digits, b_digits, p_bits) |
| return state.tolist() |
|
|
| def max_batch_size(self) -> int: |
| return 256 |
|
|