TrickyRex commited on
Commit
28cf51e
·
verified ·
1 Parent(s): d16444a

tier-9 bit-serial reducer (L=1024), official scorer 0.902 local

Browse files
Files changed (4) hide show
  1. README.md +19 -0
  2. manifest.json +7 -0
  3. model.py +161 -0
  4. weights.pt +3 -0
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # bitserial-modmul-v7 (tier 9, L=1024)
2
+
3
+ Submission for the SAIR Modular Arithmetic Challenge. One shared, p-conditioned recurrent cell
4
+ in a fixed bit-serial Horner loop computes (a * b) mod p; the cell learns the per-step transition
5
+ s' = (2*s + d*x) mod p and the loop only sequences bits. entry_class model.BitSerialReducer,
6
+ output_base 2, ~471K params, L=1024 (warm-started from the L=512 cell).
7
+
8
+ ## Local evaluation (official open-source scorer, run locally; not the organizers' leaderboard)
9
+ - modchallenge evaluate (full 1100, public/default seed, A100-class hardware): tiers 1-9 = 1.00,
10
+ highest tier 9, overall 0.902, wall-clock 139s (inside the 300s budget on this hardware; the
11
+ organizers' sandbox may be slower).
12
+ - Randomising the weights collapses every solved tier to 0.00 (capability is in the trained parameters).
13
+
14
+ ## Limitation (honest)
15
+ Not exact. The per-step map is exact on random states and exhaustively on primes < 64, but a sparse
16
+ set of structured-trajectory inputs (powers-of-two-times-random) still fails at large primes,
17
+ reproducing the Neural GPU limitation (Price, Zaremba, Sutskever 2016). The benchmark tiers reflect
18
+ average-case accuracy on the official scorer's random-operand distribution, not worst-case exactness.
19
+ Not organizer-verified; compliance ruling on the loop scaffold pending.
manifest.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "entry_class": "model.BitSerialReducer",
3
+ "output_base": 2,
4
+ "framework": "pytorch",
5
+ "model_description": "One shared, p-conditioned recurrent transition cell (~471K-param bidirectional 2-layer GRU + control-bit embedding + per-bit head) applied in a fixed bit-serial Horner loop at 1024-bit state width. Same weights reduce a mod p, reduce b mod p (multiplicand 1), and multiply the residues (multiplicand a mod p, control bits scanning b mod p); state carried as bits, modulus fed via 32-bit limbs. Output base-2; the scorer's decoder reconstructs the integer. Trained regime: primes below 2^1024, operands up to ~2048 bits (tiers 1-9); outside it the model abstains and emits a single zero.",
6
+ "training_description": "Warm-started from the L=512 dagger2 cell (the transition is width-agnostic) and fine-tuned at L=1024 on one-step transitions s' = (2*s + d*x) mod p (bit-length stratified, wrap-boundary oversampled; AdamW, lr warmup + cosine decay, best-by-validation). No precomputed tables, no hand-coded reduction or multiplication. Randomising the weights collapses every solved tier to 0. Evaluation primes are unseen during training."
7
+ }
model.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bit-serial learned reducer (general width) for the Modular Arithmetic Challenge.
2
+
3
+ Same design as bit-serial-v1/v2: one shared, p-conditioned transition cell that
4
+ learned s' = (2*s + d*x) mod p, applied in a fixed bit-serial Horner loop (reduce a,
5
+ reduce b, multiply). The arithmetic is in the trained cell; the loop only sequences
6
+ bits. Randomising the weights collapses accuracy to chance.
7
+
8
+ This version generalises the state width to L (read from the checkpoint), so it
9
+ covers tiers up to whatever L the weights were trained for. Bit extraction uses
10
+ 32-bit limbs (`to_bits_limbs`) so a modulus p >= 2^63 never overflows an int64
11
+ tensor (needed at L >= 64). State is carried as bits between steps; the harness
12
+ decoder reconstructs the integer answer from the emitted base-2 digits.
13
+
14
+ Regime: primes p < 2^L and operands up to 4*L bits. Outside it the model abstains
15
+ and emits [0] -- the honest fallback.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from pathlib import Path
21
+
22
+ import torch
23
+ from torch import nn
24
+
25
+ from modchallenge.interface.base_model import ModularMultiplicationModel
26
+
27
+ _MASK32 = (1 << 32) - 1
28
+
29
+
30
+ def _to_bits_small(vals: torch.Tensor, width: int) -> torch.Tensor:
31
+ shifts = torch.arange(width - 1, -1, -1, device=vals.device)
32
+ return (vals[:, None] >> shifts[None, :]) & 1
33
+
34
+
35
+ def to_bits_limbs(ints, dev, width: int) -> torch.Tensor:
36
+ """List of python ints (< 2^width) -> (N, width) MSB-first bit tensor via 32-bit limbs.
37
+
38
+ Overflow-safe for any width: no int64 tensor ever holds a value >= 2^32."""
39
+ nl = (width + 31) // 32
40
+ cols = []
41
+ for k in range(nl - 1, -1, -1): # most-significant limb first
42
+ limb = torch.tensor([(v >> (32 * k)) & _MASK32 for v in ints],
43
+ dtype=torch.int64, device=dev)
44
+ cols.append(_to_bits_small(limb, 32))
45
+ bits = torch.cat(cols, dim=1)
46
+ return bits[:, nl * 32 - width:] if width < nl * 32 else bits
47
+
48
+
49
+ class Cell(nn.Module):
50
+ def __init__(self, dmodel: int = 96, hidden: int = 128):
51
+ super().__init__()
52
+ self.in_proj = nn.Linear(3, dmodel)
53
+ self.d_emb = nn.Embedding(2, dmodel)
54
+ self.gru = nn.GRU(dmodel, hidden, num_layers=2, batch_first=True, bidirectional=True)
55
+ self.head = nn.Linear(2 * hidden, 1)
56
+
57
+ def forward(self, feat, d):
58
+ x = self.in_proj(feat) + self.d_emb(d)[:, None, :]
59
+ h, _ = self.gru(x)
60
+ return self.head(h).squeeze(-1)
61
+
62
+
63
+ def _bits_of(n: int) -> list[int]:
64
+ if n <= 0:
65
+ return [0]
66
+ out: list[int] = []
67
+ while n > 0:
68
+ out.append(n & 1)
69
+ n >>= 1
70
+ out.reverse()
71
+ return out
72
+
73
+
74
+ class BitSerialReducer(ModularMultiplicationModel):
75
+ def __init__(self) -> None:
76
+ self.model: Cell | None = None
77
+ self.device: torch.device | None = None
78
+ self.L = 32
79
+
80
+ def load(self, model_dir: str) -> None:
81
+ if torch.cuda.is_available():
82
+ self.device = torch.device("cuda")
83
+ elif torch.backends.mps.is_available():
84
+ self.device = torch.device("mps")
85
+ else:
86
+ self.device = torch.device("cpu")
87
+ ckpt = torch.load(Path(model_dir) / "weights.pt", map_location=self.device, weights_only=True)
88
+ self.L = int(ckpt.get("L", 32))
89
+ self.model = Cell(**ckpt.get("config", {}))
90
+ self.model.load_state_dict(ckpt["state_dict"])
91
+ self.model.to(self.device)
92
+ self.model.eval()
93
+
94
+ def preprocess_a(self, a):
95
+ return _bits_of(int(a))
96
+
97
+ def preprocess_b(self, b):
98
+ return _bits_of(int(b))
99
+
100
+ def preprocess_p(self, p):
101
+ return int(p)
102
+
103
+ @torch.no_grad()
104
+ def predict_digits(self, a_enc, b_enc, p_enc):
105
+ return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
106
+
107
+ @torch.no_grad()
108
+ def predict_digits_batch(self, inputs):
109
+ L = self.L
110
+ max_op = 4 * L
111
+ out: list[list[int]] = [[0] for _ in inputs]
112
+ idx, a_lists, b_lists, p_vals = [], [], [], []
113
+ for i, (a_enc, b_enc, p_enc) in enumerate(inputs):
114
+ p = int(p_enc)
115
+ a_bits = list(a_enc)
116
+ b_bits = list(b_enc)
117
+ if p < 2 or p >= (1 << L) or len(a_bits) > max_op or len(b_bits) > max_op:
118
+ continue
119
+ idx.append(i)
120
+ a_lists.append(a_bits)
121
+ b_lists.append(b_bits)
122
+ p_vals.append(p)
123
+ if not idx:
124
+ return out
125
+ dev = self.device
126
+ p_bits = to_bits_limbs(p_vals, dev, L).float()
127
+ ra = self._reduce(a_lists, p_bits, dev)
128
+ rb = self._reduce(b_lists, p_bits, dev)
129
+ prod = self._mul(ra, rb, p_bits)
130
+ prod_list = prod.long().tolist()
131
+ for j, i in enumerate(idx):
132
+ out[i] = [int(x) for x in prod_list[j]]
133
+ return out
134
+
135
+ def max_batch_size(self) -> int:
136
+ return 256
137
+
138
+ def _step(self, s_bits, x_bits, p_bits, d):
139
+ feat = torch.stack([s_bits, x_bits, p_bits], dim=-1)
140
+ return (torch.sigmoid(self.model(feat, d)) > 0.5).float()
141
+
142
+ def _reduce(self, bit_lists, p_bits, dev):
143
+ n = len(bit_lists)
144
+ width = max(len(b) for b in bit_lists)
145
+ padded = torch.zeros((n, width), dtype=torch.long, device=dev)
146
+ for r, bl in enumerate(bit_lists):
147
+ if bl:
148
+ padded[r, width - len(bl):] = torch.tensor(bl, dtype=torch.long, device=dev)
149
+ s_bits = torch.zeros((n, self.L), device=dev)
150
+ x_bits = to_bits_limbs([1] * n, dev, self.L).float()
151
+ for pos in range(width):
152
+ s_bits = self._step(s_bits, x_bits, p_bits, padded[:, pos])
153
+ return s_bits
154
+
155
+ def _mul(self, ra_bits, rb_bits, p_bits):
156
+ n = ra_bits.shape[0]
157
+ s_bits = torch.zeros((n, self.L), device=ra_bits.device)
158
+ rb_long = rb_bits.long()
159
+ for k in range(self.L):
160
+ s_bits = self._step(s_bits, ra_bits, p_bits, rb_long[:, k])
161
+ return s_bits
weights.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:241f1d558f0d152d3a8a905d22098d242edc00332d7eb0521fd5f9361aac848b
3
+ size 1887610