TrickyRex commited on
Commit
7ec01c8
·
verified ·
1 Parent(s): 4770777

Upload folder using huggingface_hub

Browse files
EVALS.log ADDED
@@ -0,0 +1 @@
 
 
1
+ 2026-06-24T14:46:58Z rob-rbyte-v2 total=1100 overall=0.314 highest_tier_above_90=3 deterministic=True T0=0.100 T1=1.000 T2=1.000 T3=1.000 T4=0.020 T5=0.020 T6=0.020 T7=0.020 T8=0.020 T9=0.020 T10=0.020 seed=6d6f646368616c6c656e67652d7075626c69632d62656e63686d61726b2d7631 wall=24s inference<0.1s
README.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rob-rbyte-v2
2
+
3
+ Residue router for the SAIR Modular Arithmetic Challenge. Entry class
4
+ `model.ResidueRouterV1`, output base 256. Covers tiers 1-3.
5
+
6
+ Routing is by the size of `p`. Operands are reduced mod p inside
7
+ `predict_digits` (the two-argument normalization both reference models use:
8
+ a with p, then b with p, never all three).
9
+
10
+ - **Tiers 1-2 (p <= 251):** the v1 residue specialist. Each operand residue is
11
+ embedded through a shared per-(prime, residue) table; the two vectors are
12
+ added (a discrete-log inductive bias: logs add under multiplication); a
13
+ residual MLP trunk transforms the sum; logits score against a per-(prime,
14
+ class) output table masked to the p classes of the current prime. The answer
15
+ is one base-256 digit. ~2.9M parameters.
16
+
17
+ - **Tier 3 (251 < p < 65536):** two trained shared local-rule step nets
18
+ composed through fixed wiring. After reduction the operands x, y are 16-bit
19
+ residues. A MULTIPLY step learns the shared carry rule over the carry-save
20
+ column sums and, composed closed-loop through a fixed parity readout, emits
21
+ the exact 32-bit product t = x*y. A REDUCTION step learns the shared
22
+ per-nibble borrow/compare rule and, composed through fixed restoring-division
23
+ wiring, emits r = t mod p in [0, p). The answer r is emitted as base-256
24
+ digits MSB-first (two digits cover a 16-bit residue). Both step nets are
25
+ plain GELU MLPs, width 96, depth 3, ~20k parameters each (~40k total).
26
+
27
+ - **Tiers 4-10 (p >= 65536):** outside the trained regime; returns [0].
28
+
29
+ ## Provenance
30
+
31
+ The carry-save column sums, parity readout, bit shifts, restoring-division
32
+ topology, and ge-from-final-borrow decision are fixed scaffold. The two
33
+ nontrivial decisions, the carry rule and the borrow/compare rule, reside in the
34
+ trained MLP parameters. Randomizing either step net collapses tier-3 exactness:
35
+
36
+ - random-weight pipeline (both step nets re-initialized): exact = 0.000000
37
+ - trained multiply + random reduction: exact = 0.002196 (chance)
38
+
39
+ so neither step net is scaffolding. The full collapse receipt is in
40
+ `t3_collapse_receipt.json`. The two MULTIPLY/REDUCTION step nets are trained
41
+ teacher-forced on the local-rule transitions of reference traces; the MULTIPLY
42
+ step is saturated over its realizable 272-case domain (100 realizable cases)
43
+ and never sees p, the REDUCTION step covers all 512 cases from traces over
44
+ TRAIN primes only. Five primes near the 16-bit ceiling (33343, 45137, 54497,
45
+ 55061, 62071) are held out by identity and appear in no training trace; the
46
+ composed pipeline is exact (1.0) on all five on uniform residue pairs and the
47
+ four edge cases.
48
+
49
+ ## Public benchmark (1100 problems, fixed seed)
50
+
51
+ - overall_accuracy = 0.314
52
+ - highest_tier_above_90 = 3
53
+ - deterministic = True (two full runs bit-identical)
54
+ - tier 1 = 1.000, tier 2 = 1.000, tier 3 = 1.000
55
+ - inference wall-clock < 0.1s for 1100 problems (300s budget)
56
+
57
+ Static check: clean. No sympy / gmpy2 / eval / exec / subprocess on any path.
58
+ See `EVALS.log` and `eval_6d6f6463_1100.json` for the full per-tier breakdown,
59
+ and `manifest.json` for the model and training descriptions.
60
+
61
+ ## Files
62
+
63
+ `model.py` (architectures + routing + fixed wiring), `weights.safetensors`
64
+ (tier-1/2 specialist), `t3_mul.safetensors` / `t3_red.safetensors` (tier-3
65
+ step nets), `config.json` (per-specialist hyperparameters), `manifest.json`,
66
+ `t3_collapse_receipt.json`, `EVALS.log`, `eval_6d6f6463_1100.json`.
__pycache__/model.cpython-312.pyc ADDED
Binary file (24.4 kB). View file
 
config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "small": {
3
+ "d_model": 128,
4
+ "hidden": 1024
5
+ },
6
+ "t3": {
7
+ "width": 96,
8
+ "depth": 3
9
+ }
10
+ }
eval_6d6f6463_1100.json ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "overall_accuracy": 0.314,
3
+ "highest_tier_above_90": 3,
4
+ "deterministic": true,
5
+ "tiers": [
6
+ {
7
+ "tier_id": 0,
8
+ "total": 100,
9
+ "correct": 10,
10
+ "accuracy": 0.1,
11
+ "completed": true
12
+ },
13
+ {
14
+ "tier_id": 1,
15
+ "total": 100,
16
+ "correct": 100,
17
+ "accuracy": 1.0,
18
+ "completed": true
19
+ },
20
+ {
21
+ "tier_id": 2,
22
+ "total": 100,
23
+ "correct": 100,
24
+ "accuracy": 1.0,
25
+ "completed": true
26
+ },
27
+ {
28
+ "tier_id": 3,
29
+ "total": 100,
30
+ "correct": 100,
31
+ "accuracy": 1.0,
32
+ "completed": true
33
+ },
34
+ {
35
+ "tier_id": 4,
36
+ "total": 100,
37
+ "correct": 2,
38
+ "accuracy": 0.02,
39
+ "completed": true
40
+ },
41
+ {
42
+ "tier_id": 5,
43
+ "total": 100,
44
+ "correct": 2,
45
+ "accuracy": 0.02,
46
+ "completed": true
47
+ },
48
+ {
49
+ "tier_id": 6,
50
+ "total": 100,
51
+ "correct": 2,
52
+ "accuracy": 0.02,
53
+ "completed": true
54
+ },
55
+ {
56
+ "tier_id": 7,
57
+ "total": 100,
58
+ "correct": 2,
59
+ "accuracy": 0.02,
60
+ "completed": true
61
+ },
62
+ {
63
+ "tier_id": 8,
64
+ "total": 100,
65
+ "correct": 2,
66
+ "accuracy": 0.02,
67
+ "completed": true
68
+ },
69
+ {
70
+ "tier_id": 9,
71
+ "total": 100,
72
+ "correct": 2,
73
+ "accuracy": 0.02,
74
+ "completed": true
75
+ },
76
+ {
77
+ "tier_id": 10,
78
+ "total": 100,
79
+ "correct": 2,
80
+ "accuracy": 0.02,
81
+ "completed": true
82
+ }
83
+ ],
84
+ "repo_id": "",
85
+ "revision": "",
86
+ "eval_period": ""
87
+ }
manifest.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "entry_class": "model.ResidueRouterV1",
3
+ "output_base": 256,
4
+ "framework": "pytorch",
5
+ "model_description": "Router over per-tier specialists, selected by the size of p; inputs above the trained regime return [0]. Operands are reduced mod p inside predict_digits, the same two-argument normalization both reference models use (a with p, then b with p, never all three). TIERS 1-2 (p <= 251): a ~2.9M-parameter residue specialist. Each operand residue is embedded through a shared per-(prime, residue) table, the two vectors are combined by addition (a discrete-log inductive bias: logs add under multiplication), a residual MLP trunk transforms the sum, and logits are scored against a per-(prime, class) output table masked to the p classes of the current prime. The answer is one base-256 digit, below p by construction. TIER 3 (251 < p < 65536): two trained shared local-rule step nets (plain GELU MLPs, width 96, depth 3, ~20k parameters each; ~40k total) composed through fixed wiring. After reduction the operands x, y are 16-bit residues. A MULTIPLY step learns the shared carry rule over the carry-save column sums and, composed closed-loop through a fixed parity readout, emits the exact 32-bit product t = x*y. A REDUCTION step learns the shared per-nibble borrow/compare rule and, composed through fixed restoring-division wiring, emits r = t mod p in [0, p). The answer r is emitted as base-256 digits MSB-first (two digits cover a 16-bit residue). The carry-save column sums, parity readout, bit shifts, restoring-division topology, and ge-from-final-borrow decision are fixed scaffold; the two nontrivial decisions, the carry rule and the borrow/compare rule, reside in the trained MLP parameters. Randomizing either step net collapses tier-3 exactness to chance, so the capability is in the trained weights, not the wiring.",
6
+ "training_description": "Two independent training regimes. TIERS 1-2 specialist: trained from random init on the complete synthetic input space for primes <= 251 (all 995,777 triples (x, y, p) with x, y in [0, p) and label (x*y) mod p, edge rows oversampled 8x); cross-entropy on the p-way classification, AdamW (lr 1e-3, cosine, no weight decay), batch 8192, seed 0, 15 epochs to 0 errors on the full space. Because the training set is the entire reachable input space, accuracy is interpolation over trained points; no cross-prime generalization is claimed there. TIER 3 step nets: each trained from random init, teacher-forced on the local-rule transitions of reference traces, then composed end to end. The MULTIPLY carry step is saturated over its realizable 272-case domain (100 realizable cases) on full-range 16-bit pairs (the carry rule is a property of 16-bit multiply, not of any prime, and never sees p). The REDUCTION nibble-borrow step is trained on the full 512-case domain from restoring-division traces of random triples over TRAIN primes only. Optimizer AdamW (lr 2e-3, cosine, no weight decay), batch 512, 60 epochs each, seed 0, deterministic CPU. Five primes near the 16-bit ceiling (33343, 45137, 54497, 55061, 62071) are held out by identity and appear in no training trace; the composed pipeline is exact (1.0) on all five on uniform residue pairs and the four edge cases. Training code, logs, seeds, and the random-weight-collapse receipt are archived and available on request."
7
+ }
model.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Residue router, version 2: small-prime specialist (tiers 1-2) plus a lifted
2
+ local-step pipeline for tier 3.
3
+
4
+ Routing by the size of p:
5
+
6
+ * p <= 251 (tiers 1-2): the v1 residue specialist. Each operand residue is
7
+ looked up in a shared per-(prime, residue) table; the two vectors are
8
+ combined by ADDITION (a discrete-log inductive bias: logs add under
9
+ multiplication); a residual MLP trunk transforms the sum; logits come from
10
+ a per-(prime, class) output table masked to the p classes of the current
11
+ prime. The answer is a single base-256 digit (p <= 251 < 256).
12
+
13
+ * 251 < p < 65536 (tier 3): two trained shared LOCAL-RULE step nets composed
14
+ through fixed wiring. After the operands are reduced mod p (the same
15
+ two-argument normalization both reference models use), x, y are 16-bit
16
+ residues. A MULTIPLY step (the shared carry rule c' = floor((S+c)/2) over
17
+ the carry-save column sums, composed closed-loop through a fixed parity
18
+ readout) emits the exact 32-bit product t = x*y. A REDUCTION step (a shared
19
+ per-nibble borrow/compare rule, composed through fixed restoring-division
20
+ wiring) emits r = t mod p. The answer is r, emitted as base-256 digits
21
+ MSB-first (two digits cover a 16-bit residue). Both step nets are trained
22
+ from random init; randomizing either one's weights collapses the pipeline.
23
+
24
+ * p >= 65536 (tiers 4-10): outside the trained regime; returns [0].
25
+
26
+ Nothing in the forward pass hand-codes the arithmetic over the actual (a, b, p):
27
+ the carry-save column sums, the parity readout, the bit shifts, the restoring-
28
+ division topology, and the ge-from-final-borrow decision are FIXED scaffold; the
29
+ two NONTRIVIAL decisions -- the carry rule and the borrow/compare rule -- live
30
+ in trained MLP parameters. The output digits materially determine the answer.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ from pathlib import Path
37
+
38
+ import torch
39
+ import torch.nn as nn
40
+
41
+ from modchallenge.interface.base_model import ModularMultiplicationModel
42
+
43
+ # ===========================================================================
44
+ # Tier 1-2 specialist (v1 residue net), vendored verbatim
45
+ # ===========================================================================
46
+
47
+ # The 54 primes <= 251: every prime the tier-1/2 generators can emit.
48
+ PRIMES = (
49
+ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61,
50
+ 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137,
51
+ 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211,
52
+ 223, 227, 229, 233, 239, 241, 251,
53
+ )
54
+ MAX_P = 251
55
+
56
+
57
+ class SmallResidueNet(nn.Module):
58
+ def __init__(self, d_model: int = 128, hidden: int = 1024):
59
+ super().__init__()
60
+ offsets, acc = [], 0
61
+ for p in PRIMES:
62
+ offsets.append(acc)
63
+ acc += p
64
+ table = acc # 6081
65
+ self.pair_emb = nn.Embedding(table, d_model)
66
+ self.out_emb = nn.Embedding(table, d_model)
67
+ self.prime_emb = nn.Embedding(len(PRIMES), d_model)
68
+ self.trunk = nn.Sequential(
69
+ nn.LayerNorm(d_model),
70
+ nn.Linear(d_model, hidden),
71
+ nn.GELU(),
72
+ nn.Linear(hidden, hidden),
73
+ nn.GELU(),
74
+ nn.Linear(hidden, d_model),
75
+ )
76
+ self.ln_out = nn.LayerNorm(d_model)
77
+
78
+ self.register_buffer(
79
+ "primes_t", torch.tensor(PRIMES, dtype=torch.long), persistent=False
80
+ )
81
+ self.register_buffer(
82
+ "offsets_t", torch.tensor(offsets, dtype=torch.long), persistent=False
83
+ )
84
+ lookup = torch.full((MAX_P + 1,), -1, dtype=torch.long)
85
+ for i, p in enumerate(PRIMES):
86
+ lookup[p] = i
87
+ self.register_buffer("prime_lookup", lookup, persistent=False)
88
+ self.register_buffer(
89
+ "class_grid", torch.arange(MAX_P, dtype=torch.long), persistent=False
90
+ )
91
+
92
+ def forward(
93
+ self, ix: torch.Tensor, iy: torch.Tensor, p_idx: torch.Tensor
94
+ ) -> torch.Tensor:
95
+ h = self.pair_emb(ix) + self.pair_emb(iy) + self.prime_emb(p_idx)
96
+ g = self.ln_out(h + self.trunk(h))
97
+ off = self.offsets_t[p_idx]
98
+ pv = self.primes_t[p_idx]
99
+ grid = self.class_grid.unsqueeze(0)
100
+ valid = grid < pv.unsqueeze(1)
101
+ logits = (g @ self.out_emb.weight.t()).gather(1, off.unsqueeze(1) + grid)
102
+ return logits.masked_fill(~valid, float("-inf"))
103
+
104
+ @torch.no_grad()
105
+ def predict(
106
+ self, x: torch.Tensor, y: torch.Tensor, p: torch.Tensor
107
+ ) -> torch.Tensor:
108
+ p_idx = self.prime_lookup[p]
109
+ off = self.offsets_t[p_idx]
110
+ return self.forward(off + x, off + y, p_idx).argmax(dim=-1)
111
+
112
+
113
+ # ===========================================================================
114
+ # Tier 3 step nets + fixed wiring (vendored from t3_step_model)
115
+ # ===========================================================================
116
+
117
+ # -- multiply step geometry (16x16 -> 32-bit) -------------------------------
118
+ MUL_N_OPERAND_BITS = 16
119
+ MUL_N_PRODUCT_BITS = 32
120
+ MUL_N_COLUMNS = 2 * MUL_N_OPERAND_BITS - 1 # 31 partial-product columns
121
+ MUL_N_CARRIES = MUL_N_PRODUCT_BITS - 1 # carries into columns 1..31
122
+ MUL_SUM_BITS = 5 # S_c <= 16 needs 5 bits
123
+ MUL_CARRY_BITS = 4 # carry over the chain <= 15
124
+ MUL_STEP_IN = MUL_SUM_BITS + MUL_CARRY_BITS # 9
125
+
126
+ # -- reduction step geometry (nibble borrow ripple) -------------------------
127
+ NIB = 4 # nibble width in bits
128
+ RED_NIBBLES = 5 # 17-bit R_pre / 16-bit p -> 5 nibbles
129
+ RED_STEP_IN = NIB + NIB + 1 # a_nib(4) + b_nib(4) + borrow_in(1)
130
+ RED_STEP_OUT = NIB + 1 # diff nibble(4) + borrow_out(1)
131
+ T_BITS = 32 # t = x * y is 32-bit
132
+
133
+
134
+ class MulCarryStep(nn.Module):
135
+ """Shared carry step for 16-bit multiply: 9-bit local state -> 4 carry bits."""
136
+
137
+ def __init__(self, width: int = 96, depth: int = 3):
138
+ super().__init__()
139
+ self.layers = nn.ModuleList([nn.Linear(MUL_STEP_IN, width)])
140
+ for _ in range(depth - 1):
141
+ self.layers.append(nn.Linear(width, width))
142
+ self.head = nn.Linear(width, MUL_CARRY_BITS)
143
+ self.act = nn.GELU()
144
+
145
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
146
+ h = x
147
+ for lin in self.layers:
148
+ h = self.act(lin(h))
149
+ return self.head(h)
150
+
151
+
152
+ class RedBorrowStep(nn.Module):
153
+ """Shared reduction nibble step: 9-bit local state -> 5 bits (diff+borrow)."""
154
+
155
+ def __init__(self, width: int = 96, depth: int = 3):
156
+ super().__init__()
157
+ self.layers = nn.ModuleList([nn.Linear(RED_STEP_IN, width)])
158
+ for _ in range(depth - 1):
159
+ self.layers.append(nn.Linear(width, width))
160
+ self.head = nn.Linear(width, RED_STEP_OUT)
161
+ self.act = nn.GELU()
162
+
163
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
164
+ h = x
165
+ for lin in self.layers:
166
+ h = self.act(lin(h))
167
+ return self.head(h)
168
+
169
+
170
+ def _bits16(v: torch.Tensor) -> torch.Tensor:
171
+ return ((v.unsqueeze(1) >> torch.arange(16, device=v.device)) & 1).float()
172
+
173
+
174
+ def _column_sums_16(x_bits: torch.Tensor, y_bits: torch.Tensor) -> torch.Tensor:
175
+ """(N,16),(N,16) operand bits -> (N,31) carry-save column sums (FIXED scaffold)."""
176
+ outer = x_bits.unsqueeze(2) * y_bits.unsqueeze(1) # (N,16,16)
177
+ n = outer.shape[0]
178
+ s = torch.zeros(n, MUL_N_COLUMNS, dtype=outer.dtype, device=outer.device)
179
+ for i in range(MUL_N_OPERAND_BITS):
180
+ for j in range(MUL_N_OPERAND_BITS):
181
+ s[:, i + j] += outer[:, i, j]
182
+ return s
183
+
184
+
185
+ def _encode_carry_inputs(s: torch.Tensor, c: torch.Tensor) -> torch.Tensor:
186
+ """(N,) sums and carries -> (N, 9) float bits, LSB first."""
187
+ si = torch.arange(MUL_SUM_BITS, device=s.device)
188
+ ci = torch.arange(MUL_CARRY_BITS, device=c.device)
189
+ sb = ((s.unsqueeze(1) >> si) & 1).float()
190
+ cb = ((c.unsqueeze(1) >> ci) & 1).float()
191
+ return torch.cat([sb, cb], dim=1)
192
+
193
+
194
+ def _carry_bits_to_int(bits: torch.Tensor) -> torch.Tensor:
195
+ w = (1 << torch.arange(MUL_CARRY_BITS, device=bits.device)).long()
196
+ return (bits.round().clamp(0, 1).long() * w).sum(dim=-1)
197
+
198
+
199
+ def _routed_product_logits(carry_logits45: torch.Tensor, col_parity: torch.Tensor) -> torch.Tensor:
200
+ """Fixed parity readout (no parameters): carry-bit logits + parity -> 32 bit-logits."""
201
+ BIG = 20.0
202
+ lsb = carry_logits45[:, 0::MUL_CARRY_BITS] # (B, 31) lsb of c_1..c_31
203
+ bit0 = (2.0 * col_parity[:, 0:1] - 1.0) * BIG
204
+ mid = (1.0 - 2.0 * col_parity[:, 1:]) * lsb[:, :-1] # bits 1..30
205
+ bit31 = lsb[:, -1:]
206
+ return torch.cat([bit0, mid, bit31], dim=1)
207
+
208
+
209
+ @torch.no_grad()
210
+ def _closed_loop_mul(step: nn.Module, col_sums: torch.Tensor) -> torch.Tensor:
211
+ """Compose the trained carry step over 31 columns -> carry-bit logits (B, 31*4)."""
212
+ n = col_sums.shape[0]
213
+ s = col_sums.long()
214
+ carry = torch.zeros(n, dtype=torch.long, device=s.device)
215
+ out = torch.empty(n, MUL_N_CARRIES * MUL_CARRY_BITS, device=col_sums.device)
216
+ for c in range(MUL_N_COLUMNS):
217
+ lg = step(_encode_carry_inputs(s[:, c], carry))
218
+ out[:, MUL_CARRY_BITS * c:MUL_CARRY_BITS * (c + 1)] = lg
219
+ carry = _carry_bits_to_int((lg > 0).float())
220
+ return out
221
+
222
+
223
+ @torch.no_grad()
224
+ def _composed_product(step: nn.Module, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
225
+ """Trained carry step (closed loop) + fixed parity readout -> 32-bit product (B,)."""
226
+ col_sums = _column_sums_16(_bits16(x), _bits16(y))
227
+ logits = _closed_loop_mul(step, col_sums)
228
+ col_parity = (col_sums.long() & 1).float()
229
+ bit_logits = _routed_product_logits(logits, col_parity)
230
+ bits = (bit_logits > 0).long()
231
+ w = (1 << torch.arange(MUL_N_PRODUCT_BITS, device=bits.device)).long()
232
+ return (bits * w).sum(dim=1)
233
+
234
+
235
+ def _encode_red_inputs(a: torch.Tensor, b: torch.Tensor, bin_: torch.Tensor) -> torch.Tensor:
236
+ """(N,) nibbles + borrow -> (N, 9) float bits (a nib LSB first, b nib, borrow)."""
237
+ ai = torch.arange(NIB, device=a.device)
238
+ aa = ((a.unsqueeze(1) >> ai) & 1).float()
239
+ bb = ((b.unsqueeze(1) >> ai) & 1).float()
240
+ cc = bin_.float().unsqueeze(1)
241
+ return torch.cat([aa, bb, cc], dim=1)
242
+
243
+
244
+ def _red_bits_to_out(bits: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
245
+ """(N,5) logits-thresholded bits -> (diff nibble int, borrow_out int)."""
246
+ hb = (bits > 0).long()
247
+ w = (1 << torch.arange(NIB, device=bits.device)).long()
248
+ d = (hb[:, :NIB] * w).sum(dim=1)
249
+ bout = hb[:, NIB]
250
+ return d, bout
251
+
252
+
253
+ @torch.no_grad()
254
+ def _composed_reduce(step: nn.Module, t: torch.Tensor, p: torch.Tensor) -> torch.Tensor:
255
+ """Trained borrow step composed through fixed restoring-division wiring -> r (B,).
256
+
257
+ The bit shifts, the ge-from-final-borrow decision, and the keep/replace of R
258
+ are fixed scaffold; the per-nibble subtract DECISION is the trained step.
259
+ """
260
+ n = t.shape[0]
261
+ device = t.device
262
+ R = torch.zeros(n, dtype=torch.long, device=device)
263
+ p_nib = torch.stack([(p >> (NIB * k)) & 0xF for k in range(RED_NIBBLES)], dim=1)
264
+ wk = (1 << (NIB * torch.arange(RED_NIBBLES, device=device))).long()
265
+ for i in range(T_BITS - 1, -1, -1):
266
+ bit = (t >> i) & 1
267
+ Rpre = (R << 1) | bit
268
+ borrow = torch.zeros(n, dtype=torch.long, device=device)
269
+ diff_nib = torch.zeros(n, RED_NIBBLES, dtype=torch.long, device=device)
270
+ for k in range(RED_NIBBLES):
271
+ an = (Rpre >> (NIB * k)) & 0xF
272
+ bn = p_nib[:, k]
273
+ lg = step(_encode_red_inputs(an, bn, borrow))
274
+ d, bout = _red_bits_to_out(lg)
275
+ diff_nib[:, k] = d
276
+ borrow = bout
277
+ ge = (borrow == 0).long()
278
+ diff_val = (diff_nib * wk).sum(dim=1)
279
+ R = torch.where(ge.bool(), diff_val, Rpre)
280
+ return R
281
+
282
+
283
+ # ===========================================================================
284
+ # Router
285
+ # ===========================================================================
286
+
287
+ T3_MIN_P = MAX_P + 1 # 252: first prime size routed to the lifted pipeline
288
+ T3_MAX_P = (1 << 16) - 1 # tier-3 primes are 9-16 bits
289
+
290
+
291
+ class ResidueRouterV1(ModularMultiplicationModel):
292
+ """Router over per-tier specialists, selected by the size of p.
293
+
294
+ Kept the class name ``ResidueRouterV1`` so the manifest entry_class is
295
+ stable across versions; this is v2 (tiers 1-3).
296
+ """
297
+
298
+ def __init__(self):
299
+ self.small: SmallResidueNet | None = None
300
+ self.mul: MulCarryStep | None = None
301
+ self.red: RedBorrowStep | None = None
302
+
303
+ def load(self, model_dir: str) -> None:
304
+ from safetensors.torch import load_file
305
+
306
+ torch.manual_seed(0)
307
+ model_dir = Path(model_dir)
308
+ config = json.loads((model_dir / "config.json").read_text())
309
+
310
+ # tier 1-2 specialist
311
+ tensors = load_file(str(model_dir / "weights.safetensors"))
312
+ if "small" in config:
313
+ net = SmallResidueNet(**config["small"])
314
+ state = {
315
+ k[len("small."):]: v
316
+ for k, v in tensors.items()
317
+ if k.startswith("small.")
318
+ }
319
+ net.load_state_dict(state, strict=True)
320
+ net.eval()
321
+ self.small = net
322
+
323
+ # tier 3 lifted step nets
324
+ if "t3" in config:
325
+ arch = config["t3"]
326
+ mul = MulCarryStep(width=arch["width"], depth=arch["depth"])
327
+ red = RedBorrowStep(width=arch["width"], depth=arch["depth"])
328
+ mul.load_state_dict(load_file(str(model_dir / "t3_mul.safetensors")), strict=True)
329
+ red.load_state_dict(load_file(str(model_dir / "t3_red.safetensors")), strict=True)
330
+ mul.eval()
331
+ red.eval()
332
+ self.mul = mul
333
+ self.red = red
334
+
335
+ def preprocess_a(self, a):
336
+ return a
337
+
338
+ def preprocess_b(self, b):
339
+ return b
340
+
341
+ def preprocess_p(self, p):
342
+ return p
343
+
344
+ @torch.no_grad()
345
+ def predict_digits(self, a_enc, b_enc, p_enc):
346
+ return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
347
+
348
+ @torch.no_grad()
349
+ def predict_digits_batch(self, inputs):
350
+ out: list[list[int] | None] = [None] * len(inputs)
351
+ # tier 1-2 batch (single base-256 digit)
352
+ s_x, s_y, s_p, s_idx = [], [], [], []
353
+ # tier 3 batch (two base-256 digits)
354
+ t_x, t_y, t_p, t_idx = [], [], [], []
355
+
356
+ for i, (a_enc, b_enc, p_enc) in enumerate(inputs):
357
+ try:
358
+ p = int(p_enc)
359
+ except (ValueError, TypeError):
360
+ out[i] = [0]
361
+ continue
362
+ # Operand normalization: combine a with p, then b with p (the
363
+ # two-argument reduction both reference models use). Never all three.
364
+ try:
365
+ xr = int(a_enc) % p
366
+ yr = int(b_enc) % p
367
+ except (ValueError, TypeError):
368
+ out[i] = [0]
369
+ continue
370
+
371
+ if self.small is not None and 2 <= p <= MAX_P and int(self.small.prime_lookup[p]) >= 0:
372
+ s_x.append(xr); s_y.append(yr); s_p.append(p); s_idx.append(i)
373
+ elif self.mul is not None and T3_MIN_P <= p <= T3_MAX_P:
374
+ t_x.append(xr); t_y.append(yr); t_p.append(p); t_idx.append(i)
375
+ else:
376
+ # outside the trained regime (tiers 4-10) -> honest fallback
377
+ out[i] = [0]
378
+
379
+ if s_idx:
380
+ x_t = torch.tensor(s_x, dtype=torch.long)
381
+ y_t = torch.tensor(s_y, dtype=torch.long)
382
+ p_t = torch.tensor(s_p, dtype=torch.long)
383
+ preds = self.small.predict(x_t, y_t, p_t).tolist()
384
+ for j, i in enumerate(s_idx):
385
+ out[i] = [int(preds[j])] # one base-256 digit, < p by masking
386
+
387
+ if t_idx:
388
+ x_t = torch.tensor(t_x, dtype=torch.long)
389
+ y_t = torch.tensor(t_y, dtype=torch.long)
390
+ p_t = torch.tensor(t_p, dtype=torch.long)
391
+ prod = _composed_product(self.mul, x_t, y_t) # exact 32-bit t
392
+ r = _composed_reduce(self.red, prod, p_t) # r = t mod p in [0, p)
393
+ r_list = r.tolist()
394
+ for j, i in enumerate(t_idx):
395
+ rv = int(r_list[j])
396
+ # base-256 digits, MSB-first (two digits cover a 16-bit residue)
397
+ out[i] = [rv >> 8, rv & 0xFF]
398
+
399
+ return [o if o is not None else [0] for o in out]
400
+
401
+ def max_batch_size(self) -> int:
402
+ return 512
t3_collapse_receipt.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mode": "fp",
3
+ "params": {
4
+ "mul": 19972,
5
+ "red": 20069
6
+ },
7
+ "train_log": {
8
+ "mul": {
9
+ "final_loss": 8.088716502152593e-11,
10
+ "wall_s": 112.9
11
+ },
12
+ "red": {
13
+ "final_loss": 3.2543610029023284e-09,
14
+ "wall_s": 187.7
15
+ }
16
+ },
17
+ "coverage": {
18
+ "mul_cases": 100,
19
+ "mul_total": 272,
20
+ "red_cases": 512,
21
+ "red_total": 512
22
+ },
23
+ "gate_primes": [
24
+ 33343,
25
+ 45137,
26
+ 54497,
27
+ 55061,
28
+ 62071
29
+ ],
30
+ "per_prime_exact": [
31
+ 1.0,
32
+ 1.0,
33
+ 1.0,
34
+ 1.0,
35
+ 1.0
36
+ ],
37
+ "worst_fresh_exact": 1.0,
38
+ "overall_exact": 1.0,
39
+ "tier3_cleared": true,
40
+ "collapse_mean": 0.0,
41
+ "trained_mul_random_red_mean": 0.0021956087555736305,
42
+ "arch": {
43
+ "width": 96,
44
+ "depth": 3,
45
+ "max_t": 15
46
+ }
47
+ }
t3_mul.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5de58291027b8faf42043252059dc2bc43fac6d380e5259d86056479a1d9d6e3
3
+ size 80480
t3_red.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:90c7b11ea7d8045dc6b02e45df0573e0e1a8422a3c5ce33249bcc31de0cb404b
3
+ size 80868
weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4784c4f82356d120151513ae41da5cd8c53f33be9c01e5cc23f828485ad4ffc6
3
+ size 11509360