Download model.py from uwunion/modarith-optimized: direct link, hf CLI and curl.
- Browser
- Download file 15.5 kB
-
https://huggingface.co/uwunion/modarith-optimized/resolve/main/model.py
- Command line
-
hf download hf://uwunion/modarith-optimized/model.py
-
curl -L -o model.py https://huggingface.co/uwunion/modarith-optimized/resolve/main/model.py
15.5 kB
| """Scan-Register Machine submission for the Modular Arithmetic Challenge. | |
| Two learned scan units (a carry-monoid adder and a comparison + | |
| borrow conditional-subtract) composed by a bit-streaming double-and-add | |
| schedule. Register width tracks limbs(p) + 1; operands are streamed as | |
| bits, so operand size costs iterations, never width. At load time, the finite | |
| learned cell domains are materialized into class transition / resolver tables | |
| by running the shipped weights on every codebook cell and base-32 limb pair. | |
| Inference then scans over those learned class IDs, and states are | |
| argmax-discretized between steps. | |
| No big-integer arithmetic computes the answer at inference: Python ints are | |
| used only inside the per-argument preprocess hooks to convert each decimal | |
| string into base-32 limbs / bits (base conversion, explicitly allowed). | |
| The emitted digits come from the learned resolvers; randomizing the weights | |
| collapses accuracy to chance. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| import torch | |
| from torch import nn | |
| from modchallenge.interface.base_model import ModularMultiplicationModel | |
| BASE = 32 | |
| BITS_PER_LIMB = 5 | |
| def _mlp(d_in: int, hidden: int, d_out: int) -> nn.Sequential: | |
| return nn.Sequential(nn.Linear(d_in, hidden), nn.GELU(), nn.Linear(hidden, d_out)) | |
| def scan_tree(compose, identity, sig): | |
| batch, n, d = sig.shape | |
| size = 1 | |
| while size < n: | |
| size *= 2 | |
| buf = torch.empty(batch, size, d, device=sig.device, dtype=sig.dtype) | |
| buf[:, :n] = sig | |
| if size > n: | |
| buf[:, n:] = identity | |
| stride = 1 | |
| while stride < size: | |
| buf[:, 2 * stride - 1 :: 2 * stride] = compose( | |
| buf[:, stride - 1 :: 2 * stride], buf[:, 2 * stride - 1 :: 2 * stride] | |
| ) | |
| stride *= 2 | |
| total = buf[:, -1].clone() | |
| buf[:, -1] = identity | |
| stride = size // 2 | |
| while stride >= 1: | |
| left = buf[:, stride - 1 :: 2 * stride].clone() | |
| parent = buf[:, 2 * stride - 1 :: 2 * stride].clone() | |
| buf[:, stride - 1 :: 2 * stride] = parent | |
| buf[:, 2 * stride - 1 :: 2 * stride] = compose(parent, left) | |
| stride //= 2 | |
| return buf[:, :n], total | |
| def scan_tree_classes(op_table: torch.Tensor, identity: int, sig: torch.Tensor): | |
| """Blelloch exclusive scan over finite learned cell IDs.""" | |
| batch, n = sig.shape | |
| size = 1 | |
| while size < n: | |
| size *= 2 | |
| buf = torch.full((batch, size), identity, device=sig.device, dtype=torch.long) | |
| buf[:, :n] = sig | |
| stride = 1 | |
| while stride < size: | |
| left = buf[:, stride - 1 :: 2 * stride] | |
| right = buf[:, 2 * stride - 1 :: 2 * stride] | |
| buf[:, 2 * stride - 1 :: 2 * stride] = op_table[left, right] | |
| stride *= 2 | |
| total = buf[:, -1].clone() | |
| buf[:, -1] = identity | |
| stride = size // 2 | |
| while stride >= 1: | |
| left = buf[:, stride - 1 :: 2 * stride].clone() | |
| parent = buf[:, 2 * stride - 1 :: 2 * stride].clone() | |
| buf[:, stride - 1 :: 2 * stride] = parent | |
| buf[:, 2 * stride - 1 :: 2 * stride] = op_table[parent, left] | |
| stride //= 2 | |
| return buf[:, :n], total | |
| def reduce_total(compose, identity, sig): | |
| """Root of the scan tree only (for the comparison verdict).""" | |
| x = sig | |
| batch, _, d = sig.shape | |
| while x.shape[1] > 1: | |
| if x.shape[1] % 2: | |
| x = torch.cat([x, identity.expand(batch, 1, d)], dim=1) | |
| x = compose(x[:, 0::2], x[:, 1::2]) | |
| return x[:, 0] | |
| class ScanAdder(nn.Module): | |
| def __init__(self, base: int = BASE, d_emb: int = 32, d_sig: int = 16, hidden: int = 96): | |
| super().__init__() | |
| self.limb_emb = nn.Embedding(base, d_emb) | |
| self.encoder = _mlp(2 * d_emb, hidden, d_sig) | |
| self.op = _mlp(2 * d_sig, hidden, d_sig) | |
| self.identity = nn.Parameter(torch.zeros(d_sig)) | |
| self.resolver = _mlp(2 * d_emb + d_sig, hidden, base) | |
| self.carry_head = _mlp(d_sig, hidden, 2) | |
| def compose(self, left, right): | |
| return self.op(torch.cat([left, right], dim=-1)) | |
| class ModReduce(nn.Module): | |
| def __init__(self, base: int = BASE, d_emb: int = 32, d_sig: int = 16, hidden: int = 96): | |
| super().__init__() | |
| self.limb_emb = nn.Embedding(base, d_emb) | |
| self.cmp_encoder = _mlp(2 * d_emb, hidden, d_sig) | |
| self.cmp_op = _mlp(2 * d_sig, hidden, d_sig) | |
| self.cmp_identity = nn.Parameter(torch.zeros(d_sig)) | |
| self.borrow_encoder = _mlp(2 * d_emb, hidden, d_sig) | |
| self.borrow_op = _mlp(2 * d_sig, hidden, d_sig) | |
| self.borrow_identity = nn.Parameter(torch.zeros(d_sig)) | |
| self.resolver = _mlp(2 * d_emb + 2 * d_sig, hidden, base) | |
| self.sub_head = _mlp(d_sig, hidden, 2) | |
| self.borrow_head = _mlp(d_sig, hidden, 2) | |
| def compose_cmp(self, left, right): | |
| return self.cmp_op(torch.cat([left, right], dim=-1)) | |
| def compose_borrow(self, left, right): | |
| return self.borrow_op(torch.cat([left, right], dim=-1)) | |
| def _snap(vec: torch.Tensor, codebook: torch.Tensor) -> torch.Tensor: | |
| dist = torch.cdist(vec.reshape(-1, vec.shape[-1]), codebook) | |
| return codebook[dist.argmin(dim=-1)].reshape(vec.shape) | |
| def _nearest_class(vec: torch.Tensor, codebook: torch.Tensor) -> torch.Tensor: | |
| flat = vec.reshape(-1, vec.shape[-1]) | |
| scores = ( | |
| flat.square().sum(-1, keepdim=True) | |
| - 2 * flat @ codebook.T | |
| + codebook.square().sum(-1).unsqueeze(0) | |
| ) | |
| return scores.argmin(dim=-1).reshape(vec.shape[:-1]) | |
| class ScanRegisterMachine(ModularMultiplicationModel): | |
| """Entry class declared in manifest.json.""" | |
| def load(self, model_dir: str) -> None: | |
| directory = Path(model_dir) | |
| if torch.cuda.is_available(): | |
| self.device = "cuda" | |
| elif torch.backends.mps.is_available(): | |
| self.device = "mps" | |
| else: | |
| self.device = "cpu" | |
| self.adder = ScanAdder() | |
| self.adder.load_state_dict( | |
| torch.load(directory / "adder.pt", map_location="cpu") | |
| ) | |
| self.reducer = ModReduce() | |
| self.reducer.load_state_dict( | |
| torch.load(directory / "reducer.pt", map_location="cpu") | |
| ) | |
| self.adder.to(self.device).eval() | |
| self.reducer.to(self.device).eval() | |
| codebooks = torch.load(directory / "codebooks.pt", map_location="cpu") | |
| self.carry_cb = codebooks["carry"].to(self.device) | |
| self.cmp_cb = codebooks["cmp"].to(self.device) | |
| self.borrow_cb = codebooks["borrow"].to(self.device) | |
| self.carry_identity = self.carry_cb[1] # propagate | |
| self.cmp_identity = self.cmp_cb[1] # EQ | |
| self.borrow_identity = self.borrow_cb[1] # propagate | |
| self._build_tables() | |
| torch.set_grad_enabled(False) | |
| def _build_tables(self) -> None: | |
| digits = torch.arange(BASE, device=self.device) | |
| adder_emb = self.adder.limb_emb(digits) | |
| x = digits.repeat_interleave(BASE) | |
| y = digits.repeat(BASE) | |
| adder_pair = torch.cat([adder_emb[x], adder_emb[y]], dim=-1) | |
| adder_sig = self.adder.encoder(adder_pair) | |
| self.adder_pair_class = _nearest_class(adder_sig, self.carry_cb).reshape( | |
| BASE, BASE | |
| ) | |
| carry_count = self.carry_cb.shape[0] | |
| left = self.carry_cb[:, None, :].expand(carry_count, carry_count, -1) | |
| right = self.carry_cb[None, :, :].expand(carry_count, carry_count, -1) | |
| carry_out = self.adder.compose( | |
| left.reshape(carry_count * carry_count, -1), | |
| right.reshape(carry_count * carry_count, -1), | |
| ) | |
| self.carry_op_table = _nearest_class(carry_out, self.carry_cb).reshape( | |
| carry_count, carry_count | |
| ) | |
| adder_pair_exp = adder_pair[:, None, :].expand(BASE * BASE, carry_count, -1) | |
| carry_exp = self.carry_cb[None, :, :].expand(BASE * BASE, carry_count, -1) | |
| adder_logits = self.adder.resolver( | |
| torch.cat([adder_pair_exp, carry_exp], dim=-1).reshape( | |
| BASE * BASE * carry_count, -1 | |
| ) | |
| ) | |
| self.adder_digit_table = adder_logits.argmax(-1).reshape( | |
| BASE, BASE, carry_count | |
| ) | |
| reducer_emb = self.reducer.limb_emb(digits) | |
| reducer_pair = torch.cat([reducer_emb[x], reducer_emb[y]], dim=-1) | |
| cmp_sig = self.reducer.cmp_encoder(reducer_pair) | |
| self.cmp_pair_class = _nearest_class(cmp_sig, self.cmp_cb).reshape(BASE, BASE) | |
| cmp_count = self.cmp_cb.shape[0] | |
| left = self.cmp_cb[:, None, :].expand(cmp_count, cmp_count, -1) | |
| right = self.cmp_cb[None, :, :].expand(cmp_count, cmp_count, -1) | |
| cmp_out = self.reducer.compose_cmp( | |
| left.reshape(cmp_count * cmp_count, -1), | |
| right.reshape(cmp_count * cmp_count, -1), | |
| ) | |
| self.cmp_op_table = _nearest_class(cmp_out, self.cmp_cb).reshape( | |
| cmp_count, cmp_count | |
| ) | |
| borrow_count = self.borrow_cb.shape[0] | |
| borrow_sig = self.reducer.borrow_encoder(reducer_pair) | |
| self.borrow_pair_class = _nearest_class( | |
| borrow_sig, self.borrow_cb | |
| ).reshape(BASE, BASE) | |
| left = self.borrow_cb[:, None, :].expand( | |
| borrow_count, borrow_count, -1 | |
| ) | |
| right = self.borrow_cb[None, :, :].expand( | |
| borrow_count, borrow_count, -1 | |
| ) | |
| borrow_out = self.reducer.compose_borrow( | |
| left.reshape(borrow_count * borrow_count, -1), | |
| right.reshape(borrow_count * borrow_count, -1), | |
| ) | |
| self.borrow_op_table = _nearest_class( | |
| borrow_out, self.borrow_cb | |
| ).reshape(borrow_count, borrow_count) | |
| reducer_pair_exp = reducer_pair[:, None, None, :].expand( | |
| BASE * BASE, borrow_count, cmp_count, -1 | |
| ) | |
| borrow_exp = self.borrow_cb[None, :, None, :].expand( | |
| BASE * BASE, borrow_count, cmp_count, -1 | |
| ) | |
| verdict_exp = self.cmp_cb[None, None, :, :].expand( | |
| BASE * BASE, borrow_count, cmp_count, -1 | |
| ) | |
| reducer_logits = self.reducer.resolver( | |
| torch.cat([reducer_pair_exp, borrow_exp, verdict_exp], dim=-1).reshape( | |
| BASE * BASE * borrow_count * cmp_count, -1 | |
| ) | |
| ) | |
| self.reducer_digit_table = reducer_logits.argmax(-1).reshape( | |
| BASE, BASE, borrow_count, cmp_count | |
| ) | |
| # -- per-argument preprocessing (base conversion only) ---------------- | |
| def preprocess_a(self, a: str) -> list[int]: | |
| value = int(a) | |
| bits = [] | |
| while value: | |
| bits.append(value & 1) | |
| value >>= 1 | |
| return list(reversed(bits)) or [0] # MSB-first | |
| def preprocess_b(self, b: str) -> list[int]: | |
| return self.preprocess_a(b) | |
| def preprocess_p(self, p: str) -> list[int]: | |
| value = int(p) | |
| limbs = [] | |
| while value: | |
| limbs.append(value % BASE) | |
| value //= BASE | |
| return limbs or [0] # LSB-first | |
| # -- learned macro-steps ---------------------------------------------- | |
| def _add(self, x, y): | |
| sig = self.adder_pair_class[x, y] | |
| prefixes, _ = scan_tree_classes(self.carry_op_table, 1, sig) | |
| return self.adder_digit_table[x, y, prefixes] | |
| def _reduce(self, u, p_reg): | |
| """Learned comparison and borrow scans for conditional subtraction.""" | |
| cmp_sig = self.cmp_pair_class[u, p_reg] | |
| # Comparison is most-significant-first, so only the total over the | |
| # reversed limb stream is needed for the subtract/keep verdict. | |
| _, total = scan_tree_classes( | |
| self.cmp_op_table, 1, cmp_sig.flip(1) | |
| ) | |
| # Borrow propagation is least-significant-first. Both its local | |
| # classifications and its composition table come from the trained | |
| # reducer weights; no arithmetic class mapping is supplied by Python. | |
| borrow_sig = self.borrow_pair_class[u, p_reg] | |
| borrow_index, _ = scan_tree_classes( | |
| self.borrow_op_table, 1, borrow_sig | |
| ) | |
| verdict = total.unsqueeze(1).expand_as(u) | |
| return self.reducer_digit_table[u, p_reg, borrow_index, verdict] | |
| # -- the machine -------------------------------------------------------- | |
| def _rollout(self, group: list[tuple[list[int], list[int], list[int]]]) -> list[list[int]]: | |
| width = max(len(p) for _, _, p in group) + 1 | |
| a_len = max(len(a) for a, _, _ in group) | |
| b_len = max(len(b) for _, b, _ in group) | |
| batch = len(group) | |
| pad_bits = lambda bits, n: [0] * (n - len(bits)) + bits | |
| a_rows = [pad_bits(a, a_len) for a, _, _ in group] | |
| b_rows = [pad_bits(b, b_len) for _, b, _ in group] | |
| a_has_bit = [any(row[i] for row in a_rows) for i in range(a_len)] | |
| b_has_bit = [any(row[i] for row in b_rows) for i in range(b_len)] | |
| a_bits = torch.tensor( | |
| a_rows, dtype=torch.bool, device=self.device | |
| ) | |
| b_bits = torch.tensor( | |
| b_rows, dtype=torch.bool, device=self.device | |
| ) | |
| p_reg = torch.tensor( | |
| [p + [0] * (width - len(p)) for _, _, p in group], | |
| dtype=torch.long, | |
| device=self.device, | |
| ) | |
| one = torch.zeros(batch, width, dtype=torch.long, device=self.device) | |
| one[:, 0] = 1 | |
| r = torch.zeros(batch, width, dtype=torch.long, device=self.device) | |
| started = False | |
| for i in range(a_len): # A = a mod p | |
| column = a_bits[:, i] | |
| has_bit = a_has_bit[i] | |
| if not started and not has_bit: | |
| continue # leading zero padding with r == 0 is inert | |
| started = True | |
| u = self._add(r, r) | |
| if has_bit: | |
| u = torch.where(column.unsqueeze(1), self._add(u, one), u) | |
| r = self._reduce(u, p_reg) | |
| addend = r | |
| r = torch.zeros(batch, width, dtype=torch.long, device=self.device) | |
| started = False | |
| for i in range(b_len): # r = (A * b) mod p | |
| column = b_bits[:, i] | |
| has_bit = b_has_bit[i] | |
| if not started and not has_bit: | |
| continue | |
| started = True | |
| r = self._reduce(self._add(r, r), p_reg) | |
| if has_bit: | |
| r_add = self._reduce(self._add(r, addend), p_reg) | |
| r = torch.where(column.unsqueeze(1), r_add, r) | |
| rows = r.cpu().tolist() | |
| return [[int(d) for d in reversed(row)] for row in rows] # MSB-first | |
| # -- interface ----------------------------------------------------------- | |
| def predict_digits(self, a_enc, b_enc, p_enc) -> list[int]: | |
| self._build_tables() | |
| return self._rollout([(a_enc, b_enc, p_enc)])[0] | |
| def predict_digits_batch(self, inputs) -> list[list[int]]: | |
| self._build_tables() | |
| # group by size bucket so short problems don't pay for long ones | |
| groups: dict[tuple[int, int, int], list[int]] = {} | |
| for index, (a, b, p) in enumerate(inputs): | |
| key = ( | |
| -(-len(p) // 8), | |
| -(-len(a) // 64), | |
| -(-len(b) // 64), | |
| ) | |
| groups.setdefault(key, []).append(index) | |
| results: list[list[int] | None] = [None] * len(inputs) | |
| for indices in groups.values(): | |
| rows = self._rollout([inputs[i] for i in indices]) | |
| for i, row in zip(indices, rows): | |
| results[i] = row | |
| return results # type: ignore[return-value] | |
| def max_batch_size(self) -> int: | |
| return 100 | |