| """ |
| Submission scaffold β bit-serial Dynamic-L modular multiply. |
| |
| SPLIT (per CLAUDE.md): |
| HARNESS (mine): the ModularMultiplicationModel boilerplate, the cell (verbatim from Build 6/7), |
| bit-plane I/O, load(), per-arg preprocess, batching glue. |
| YOURS: `predict_digits_batch` β the 3-pass Horner SCHEDULE that composes the learned cell into |
| (a*b) mod p. That's the solving algorithm, so it's yours to assemble (lift the loop straight from |
| your `free_run_eval` in mult_modmul_dynl.py). See the TODO block. |
| |
| Output: base-2 digits, MSB-first (manifest output_base=2); the harness decoder rebuilds the int. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
| import torch.nn as nn |
|
|
| try: |
| from modchallenge.interface.base_model import ModularMultiplicationModel |
| except ModuleNotFoundError: |
| from abc import ABC |
|
|
| class ModularMultiplicationModel(ABC): |
| def preprocess_a(self, a): |
| return a |
|
|
| def preprocess_b(self, b): |
| return b |
|
|
| def preprocess_p(self, p): |
| return p |
|
|
| def predict_digits_batch(self, inputs): |
| return [self.predict_digits(*x) for x in inputs] |
|
|
| def max_batch_size(self): |
| return 1 |
|
|
|
|
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
|
|
| |
| |
| |
| class ExplicitBitCell(nn.Module): |
| def __init__(self, dmodel, hidden, num_layers, bidirectional, dropout=0.0): |
| super().__init__() |
| self.in_proj = nn.Linear(3, dmodel) |
| self.d_emb = nn.Embedding(2, dmodel) |
| self.gru = nn.GRU( |
| dmodel, |
| hidden, |
| num_layers, |
| bidirectional=bidirectional, |
| batch_first=True, |
| dropout=dropout, |
| ) |
| self.head = nn.Linear((2 if bidirectional else 1) * hidden, 1) |
|
|
| def forward(self, s_bits, x_bits, p_bits, d): |
| feat = torch.stack([s_bits, x_bits, p_bits], -1) |
| inp = self.in_proj(feat) + self.d_emb(d.long())[:, None, :] |
| out, _ = self.gru(inp) |
| return self.head(out).squeeze(-1) |
|
|
|
|
| |
| |
| |
| def ints_to_planes(ints: list[int], W: int) -> np.ndarray: |
| nbytes = (W + 7) // 8 |
| buf = b"".join(int(n).to_bytes(nbytes, "little") for n in ints) |
| arr = np.frombuffer(buf, dtype=np.uint8).reshape(len(ints), nbytes) |
| return np.unpackbits(arr, axis=1, bitorder="little")[:, :W] |
|
|
|
|
| def planes_to_ints(planes: np.ndarray) -> list[int]: |
| nbytes = (planes.shape[1] + 7) // 8 |
| pad = np.zeros((planes.shape[0], nbytes * 8 - planes.shape[1]), np.uint8) |
| full = np.concatenate([planes.astype(np.uint8), pad], axis=1) |
| packed = np.packbits(full, axis=1, bitorder="little") |
| return [int.from_bytes(row.tobytes(), "little") for row in packed] |
|
|
|
|
| def bits_msb(n: int) -> list[int]: |
| return [0] if n == 0 else [int(c) for c in bin(n)[2:]] |
|
|
|
|
| |
| class BitSerialModMul(ModularMultiplicationModel): |
| def load(self, model_dir: str) -> None: |
| ck = torch.load(Path(model_dir) / "weights.pt", map_location=DEVICE) |
| c = ck["cfg"] |
| self.cell = ExplicitBitCell( |
| c["dmodel"], |
| c["hidden"], |
| c["num_layers"], |
| c["bidirectional"], |
| c.get("dropout", 0.0), |
| ).to(DEVICE) |
| self.cell.load_state_dict(ck["state_dict"]) |
| self.cell.eval() |
|
|
| |
| def preprocess_a(self, a: str): |
| return int(a) |
|
|
| def preprocess_b(self, b: str): |
| return int(b) |
|
|
| def preprocess_p(self, p: str): |
| return int(p) |
|
|
| def max_batch_size(self) -> int: |
| return 256 |
|
|
| |
| def _to_dstream(self, ints) -> tuple[torch.Tensor, torch.Tensor]: |
| """list[int] -> (d_bits (B,T) long, active (B,T) bool) on DEVICE; bits MSB-first, padded to T.""" |
| seqs = [bits_msb(int(n)) for n in ints] |
| T = max(len(s) for s in seqs) |
| d_bits = torch.zeros((len(ints), T), dtype=torch.long) |
| active = torch.zeros((len(ints), T), dtype=torch.bool) |
| for i, s in enumerate(seqs): |
| d_bits[i, : len(s)] = torch.tensor(s, dtype=torch.long) |
| active[i, : len(s)] = True |
| return d_bits.to(DEVICE), active.to(DEVICE) |
|
|
| @torch.no_grad() |
| def _horner_pass(self, d_bits, active, x_pl, p_pl) -> torch.Tensor: |
| """Iterate the learned single-step cell over the d-stream, feeding s' back each step; the |
| active mask freezes an item once it runs out of bits. x_pl, p_pl: (B, W) float bit-planes; |
| returns s_pl: (B, W) in {0,1}. (This is free_run_eval's inner loop β the arithmetic is in |
| self.cell, not here. It does NOT know which pass it is; that's the schedule's job.)""" |
| B, W = x_pl.shape |
| s_pl = torch.zeros((B, W), device=DEVICE) |
| use_amp = DEVICE == "cuda" |
| for t in range(d_bits.shape[1]): |
| d = d_bits[:, t].float() |
| with torch.autocast( |
| device_type=DEVICE, dtype=torch.float16, enabled=use_amp |
| ): |
| logits = self.cell(s_pl, x_pl, p_pl, d) |
| s_pl = torch.where(active[:, t].unsqueeze(-1), (logits > 0).float(), s_pl) |
| return s_pl |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _MAX_PRIME_BITS = 2048 |
|
|
| def predict_digits(self, a_enc, b_enc, p_enc): |
| return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0] |
|
|
| @torch.no_grad() |
| def predict_digits_batch(self, inputs: list[tuple[int, int, int]]): |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if not inputs: |
| return [] |
|
|
| A, B, P = map(list, zip(*inputs)) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| trivial = { |
| i |
| for i in range(len(P)) |
| if P[i].bit_length() >= A[i].bit_length() + B[i].bit_length() |
| or P[i].bit_length() > self._MAX_PRIME_BITS |
| } |
|
|
| to_compute = [i for i in range(len(P)) if i not in trivial] |
|
|
| if not to_compute: |
| return [[0] for _ in inputs] |
|
|
| W = max(P[i].bit_length() for i in to_compute) |
|
|
| def planes(ints): |
| return torch.from_numpy(ints_to_planes(ints, W)).float().to(DEVICE) |
|
|
| n = len(to_compute) |
| p_pl = planes([P[i] for i in to_compute]) |
|
|
| |
| |
| ab = [A[i] for i in to_compute] + [B[i] for i in to_compute] |
| dab, acab = self._to_dstream(ab) |
| ones2 = planes([1] * (2 * n)) |
| p_pl2 = torch.cat([p_pl, p_pl], dim=0) |
| reduced = planes_to_ints(self._horner_pass(dab, acab, ones2, p_pl2).cpu().numpy()) |
| a_red, b_red = reduced[:n], reduced[n:] |
|
|
| res = planes_to_ints( |
| self._horner_pass(*self._to_dstream(a_red), x_pl=planes(b_red), p_pl=p_pl) |
| .cpu() |
| .numpy() |
| ) |
|
|
| out = [None] * len(inputs) |
| for i in trivial: |
| out[i] = [0] |
| for k, i in enumerate(to_compute): |
| out[i] = bits_msb(res[k]) |
| return out |
|
|