TrickyRex commited on
Commit
fb82cee
·
verified ·
1 Parent(s): b6fc9f1

Upload folder using huggingface_hub

Browse files
EVALS.log ADDED
@@ -0,0 +1 @@
 
 
1
+ 2026-06-24T16:23:00Z rob-rbyte-v3 total=1100 overall=0.412 highest_tier_above_90=4 deterministic=True T0=0.200 T1=1.000 T2=1.000 T3=1.000 T4=1.000 T5=0.020 T6=0.020 T7=0.020 T8=0.020 T9=0.020 T10=0.020 seed=6d6f646368616c6c656e67652d7075626c69632d62656e63686d61726b2d7631 wall=10s tier4_inference=0.1s official_pipeline=modchallenge_evaluate
README.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rob-rbyte-v3
2
+
3
+ Residue router for the SAIR Modular Arithmetic Challenge. Entry class
4
+ `model.ResidueRouterV1`, output base 256. Covers tiers 1-4.
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: a
8
+ 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 x, y are 16-bit residues. A
19
+ MULTIPLY step learns the shared carry rule over the carry-save column sums
20
+ and, composed closed-loop through a fixed parity readout, emits the exact
21
+ 32-bit product t = x*y. A REDUCTION step learns the shared per-nibble
22
+ borrow/compare rule and, composed through fixed restoring-division wiring,
23
+ emits r = t mod p in [0, p). Plain GELU MLPs, width 96, depth 3, ~20k params
24
+ each.
25
+
26
+ - **Tier 4 (65536 <= p < 2^32):** the SAME two rules at 32-bit geometry. After
27
+ reduction x, y are 32-bit residues. The MULTIPLY step learns the carry rule
28
+ over the 63 carry-save columns (sum <= 32, carry <= 31) and, composed through
29
+ the parity readout widened to 64 bits, emits the 64-bit product as BITS (the
30
+ product overflows signed int64, so it is never materialized as an integer).
31
+ The REDUCTION step is the identical 512-case per-nibble borrow rule, composed
32
+ over 64 division positions x 9 nibbles, emitting r = t mod p in [0, p). The
33
+ multiply step is GELU MLP width 128 depth 3 (~35k params), the reduction step
34
+ width 96 depth 3 (~20k params). The two techniques flagged for tier 4 shape
35
+ training: reciprocal-operand framing (each triple traced as both (x,y) and
36
+ (y,x)) and Charton-Kempe two-set sampling (a small repeated set + a large
37
+ fresh set).
38
+
39
+ - **Tiers 5-10 (p >= 2^32):** outside the trained regime; returns [0].
40
+
41
+ ## Provenance
42
+
43
+ In every tier the carry-save column sums, parity readout, bit shifts,
44
+ restoring-division topology, and ge-from-final-borrow decision are fixed
45
+ scaffold. The two nontrivial decisions, the carry rule and the borrow/compare
46
+ rule, reside in trained MLP parameters (separate nets per tier-3 / tier-4
47
+ geometry). Randomizing a step net collapses its tier:
48
+
49
+ - tier 3 random-weight pipeline: exact = 0.000000; trained mul + random red =
50
+ 0.002196 (chance). See `t3_collapse_receipt.json`.
51
+ - tier 4 random-weight pipeline: exact = 0.000000; trained mul + random red =
52
+ 0.000000. See `t4_collapse_receipt.json`.
53
+
54
+ Both tier-3 and tier-4 multiply/reduction step nets reach per-case exactness
55
+ 1.0 on their full enumerated domains (tier 3: mul 272-case / red 512-case; tier
56
+ 4: mul 1056-case / red 512-case), so the composed pipelines are exact by the
57
+ fixed wiring. Five 17-32-bit primes are held out by identity for tier 4 and
58
+ appear in no training trace; the composed tier-4 pipeline is exact (1.0) on all
59
+ five on uniform residue pairs and the four edge cases (`t4_collapse_receipt.json`
60
+ and `experiments/013-t4-lifted-step/`). The tier-3 held-out primes (33343,
61
+ 45137, 54497, 55061, 62071) are likewise exact.
62
+
63
+ ## Public benchmark (1100 problems, fixed seed)
64
+
65
+ Run through the official pipeline (`modchallenge evaluate ./submission/rob-rbyte-v3
66
+ --total 1100`); the per-tier accuracy and `highest_tier_above_90` come from the
67
+ official decoder, not an internal tensor check:
68
+
69
+ - overall_accuracy = 0.412
70
+ - highest_tier_above_90 = 4
71
+ - deterministic = True (two full runs bit-identical per tier)
72
+ - tier 1 = 1.000, tier 2 = 1.000, tier 3 = 1.000, tier 4 = 1.000
73
+ - tier-4 inference 0.1s for 100 problems (300s budget); full eval ~10s
74
+
75
+ See `EVALS.log` and `eval_official_1100.json` for the full breakdown and
76
+ `manifest.json` for the model and training descriptions.
77
+
78
+ Static check: clean. No sympy / gmpy2 / eval / exec / subprocess on any path.
79
+
80
+ ## Files
81
+
82
+ `model.py` (architectures + routing + fixed wiring), `weights.safetensors`
83
+ (tier-1/2 specialist), `t3_mul.safetensors` / `t3_red.safetensors` (tier-3 step
84
+ nets), `t4_mul.safetensors` / `t4_red.safetensors` (tier-4 step nets),
85
+ `config.json` (per-specialist hyperparameters), `manifest.json`,
86
+ `t3_collapse_receipt.json`, `t4_collapse_receipt.json`, `EVALS.log`,
87
+ `eval_official_1100.json`.
config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "small": {
3
+ "d_model": 128,
4
+ "hidden": 1024
5
+ },
6
+ "t3": {
7
+ "width": 96,
8
+ "depth": 3
9
+ },
10
+ "t4": {
11
+ "mul_width": 128,
12
+ "red_width": 96,
13
+ "depth": 3
14
+ }
15
+ }
eval_official_1100.json ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "overall_accuracy": 0.412,
3
+ "highest_tier_above_90": 4,
4
+ "deterministic": true,
5
+ "tiers": [
6
+ {
7
+ "tier_id": 0,
8
+ "total": 100,
9
+ "correct": 20,
10
+ "accuracy": 0.2,
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": 100,
38
+ "accuracy": 1.0,
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) 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 as bits. 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). TIER 4 (65536 <= p < 2^32): the SAME two rules at 32-bit geometry. After reduction x, y are 32-bit residues. The MULTIPLY step learns the carry rule over the 63 carry-save columns (sum <= 32, carry <= 31) and, composed through the parity readout widened to 64 bits, emits the 64-bit product as bits (the product overflows signed int64, so it is never materialized as an integer). The REDUCTION step is the identical 512-case per-nibble borrow rule, composed over 64 division positions x 9 nibbles, emitting r = t mod p in [0, p). The answer r is emitted as base-256 digits MSB-first. In every tier 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 trained MLP parameters. Randomizing any step net's weights collapses its tier to chance, so the capability is in the trained weights, not the wiring.",
6
+ "training_description": "Three 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), 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. The MULTIPLY carry step is saturated over its realizable 272-case domain 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 step covers the full 512-case domain from restoring-division traces of random triples over TRAIN primes only. TIER 4 step nets: the same two rules at 32-bit geometry. The MULTIPLY carry step (1056-case domain: sum 0..32, carry 0..31; max carry-out equals max carry-in, so the enumeration is a closed cover) is trained on full-range 32-bit pairs UNION the full case enumeration, so every realizable carry state is seen. The REDUCTION step is the same 512-case borrow rule, trained on restoring-division traces over TRAIN primes using two techniques: reciprocal-operand framing (every triple is traced as both (x, y) and (y, x), since t = x*y = y*x has two distinct division-bit views) and Charton-Kempe two-set sampling (a small repeated set materialized several times plus a large fresh set seen once). Optimizer AdamW (lr 2e-3, cosine, no weight decay), seed 0, deterministic CPU. Five 17-32-bit primes are held out by identity and feed no training trace; the composed tier-4 pipeline is exact 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,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Residue router, version 3: small-prime specialist (tiers 1-2), a lifted
2
+ local-step pipeline for tier 3 (16-bit residues), and the same two shared rules
3
+ lifted to 32-bit limbs for tier 4 (17-32-bit primes, operands to 96 bits).
4
+
5
+ Routing by the size of p:
6
+
7
+ * p <= 251 (tiers 1-2): the v1 residue specialist. Each operand residue is
8
+ looked up in a shared per-(prime, residue) table; the two vectors are
9
+ combined by ADDITION (a discrete-log inductive bias: logs add under
10
+ multiplication); a residual MLP trunk transforms the sum; logits come from a
11
+ per-(prime, class) output table masked to the p classes of the current
12
+ prime. The answer is a single base-256 digit (p <= 251 < 256).
13
+
14
+ * 251 < p < 65536 (tier 3): two trained shared LOCAL-RULE step nets composed
15
+ through fixed wiring. After reduction, x, y are 16-bit residues. A MULTIPLY
16
+ step (the shared carry rule c' = floor((S+c)/2) over the carry-save column
17
+ sums, composed closed-loop through a fixed parity readout) emits the exact
18
+ 32-bit product t = x*y. A REDUCTION step (a shared per-nibble borrow/compare
19
+ rule, composed through fixed restoring-division wiring) emits r = t mod p.
20
+ The answer r is emitted as base-256 digits MSB-first.
21
+
22
+ * 65536 <= p < 2^32 (tier 4): the SAME two rules at 32-bit geometry. After
23
+ reduction, x, y are 32-bit residues. The MULTIPLY step (33x32-case carry
24
+ rule over the 63 carry-save columns, parity readout widened to 64 bits)
25
+ emits the 64-bit product as BITS -- the product overflows signed int64 at
26
+ the top end, so the pipeline never materializes it as an integer. The
27
+ REDUCTION step (the identical 512-case borrow rule) composed over 64
28
+ division positions x 9 nibbles emits r = t mod p. The answer r (< 2^32) is
29
+ emitted as up to four base-256 digits MSB-first.
30
+
31
+ * p >= 2^32 (tiers 5-10): outside the trained regime; returns [0].
32
+
33
+ Nothing in the forward pass hand-codes the arithmetic over the actual (a, b, p):
34
+ the carry-save column sums, the parity readout, the bit shifts, the restoring-
35
+ division topology, and the ge-from-final-borrow decision are FIXED scaffold; the
36
+ two NONTRIVIAL decisions -- the carry rule and the borrow/compare rule -- live
37
+ in trained MLP parameters (separate nets per tier-3 / tier-4 geometry).
38
+ Randomizing any step net's weights collapses its tier.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ from pathlib import Path
45
+
46
+ import torch
47
+ import torch.nn as nn
48
+
49
+ from modchallenge.interface.base_model import ModularMultiplicationModel
50
+
51
+ # ===========================================================================
52
+ # Tier 1-2 specialist (v1 residue net)
53
+ # ===========================================================================
54
+
55
+ PRIMES = (
56
+ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61,
57
+ 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137,
58
+ 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211,
59
+ 223, 227, 229, 233, 239, 241, 251,
60
+ )
61
+ MAX_P = 251
62
+
63
+
64
+ class SmallResidueNet(nn.Module):
65
+ def __init__(self, d_model: int = 128, hidden: int = 1024):
66
+ super().__init__()
67
+ offsets, acc = [], 0
68
+ for p in PRIMES:
69
+ offsets.append(acc)
70
+ acc += p
71
+ table = acc # 6081
72
+ self.pair_emb = nn.Embedding(table, d_model)
73
+ self.out_emb = nn.Embedding(table, d_model)
74
+ self.prime_emb = nn.Embedding(len(PRIMES), d_model)
75
+ self.trunk = nn.Sequential(
76
+ nn.LayerNorm(d_model),
77
+ nn.Linear(d_model, hidden),
78
+ nn.GELU(),
79
+ nn.Linear(hidden, hidden),
80
+ nn.GELU(),
81
+ nn.Linear(hidden, d_model),
82
+ )
83
+ self.ln_out = nn.LayerNorm(d_model)
84
+
85
+ self.register_buffer(
86
+ "primes_t", torch.tensor(PRIMES, dtype=torch.long), persistent=False
87
+ )
88
+ self.register_buffer(
89
+ "offsets_t", torch.tensor(offsets, dtype=torch.long), persistent=False
90
+ )
91
+ lookup = torch.full((MAX_P + 1,), -1, dtype=torch.long)
92
+ for i, p in enumerate(PRIMES):
93
+ lookup[p] = i
94
+ self.register_buffer("prime_lookup", lookup, persistent=False)
95
+ self.register_buffer(
96
+ "class_grid", torch.arange(MAX_P, dtype=torch.long), persistent=False
97
+ )
98
+
99
+ def forward(
100
+ self, ix: torch.Tensor, iy: torch.Tensor, p_idx: torch.Tensor
101
+ ) -> torch.Tensor:
102
+ h = self.pair_emb(ix) + self.pair_emb(iy) + self.prime_emb(p_idx)
103
+ g = self.ln_out(h + self.trunk(h))
104
+ off = self.offsets_t[p_idx]
105
+ pv = self.primes_t[p_idx]
106
+ grid = self.class_grid.unsqueeze(0)
107
+ valid = grid < pv.unsqueeze(1)
108
+ logits = (g @ self.out_emb.weight.t()).gather(1, off.unsqueeze(1) + grid)
109
+ return logits.masked_fill(~valid, float("-inf"))
110
+
111
+ @torch.no_grad()
112
+ def predict(
113
+ self, x: torch.Tensor, y: torch.Tensor, p: torch.Tensor
114
+ ) -> torch.Tensor:
115
+ p_idx = self.prime_lookup[p]
116
+ off = self.offsets_t[p_idx]
117
+ return self.forward(off + x, off + y, p_idx).argmax(dim=-1)
118
+
119
+
120
+ # ===========================================================================
121
+ # Shared step-net architecture (used by both tier-3 and tier-4 geometries)
122
+ # ===========================================================================
123
+
124
+ class StepMLP(nn.Module):
125
+ """Plain GELU MLP step: n_in local-state bits -> n_out logits."""
126
+
127
+ def __init__(self, n_in: int, n_out: int, width: int, depth: int):
128
+ super().__init__()
129
+ self.layers = nn.ModuleList([nn.Linear(n_in, width)])
130
+ for _ in range(depth - 1):
131
+ self.layers.append(nn.Linear(width, width))
132
+ self.head = nn.Linear(width, n_out)
133
+ self.act = nn.GELU()
134
+
135
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
136
+ h = x
137
+ for lin in self.layers:
138
+ h = self.act(lin(h))
139
+ return self.head(h)
140
+
141
+
142
+ # ===========================================================================
143
+ # Tier 3 geometry (16x16 -> 32-bit; 5-nibble reduction)
144
+ # ===========================================================================
145
+
146
+ T3_MUL_OPB = 16
147
+ T3_MUL_PRB = 32
148
+ T3_MUL_COLS = 2 * T3_MUL_OPB - 1 # 31
149
+ T3_MUL_SUMB = 5
150
+ T3_MUL_CARB = 4
151
+ T3_MUL_IN = T3_MUL_SUMB + T3_MUL_CARB # 9
152
+ T3_NIB = 4
153
+ T3_RED_NIBBLES = 5
154
+ T3_RED_IN = T3_NIB + T3_NIB + 1 # 9
155
+ T3_RED_OUT = T3_NIB + 1 # 5
156
+ T3_T_BITS = 32
157
+
158
+
159
+ # ===========================================================================
160
+ # Tier 4 geometry (32x32 -> 64-bit; 9-nibble reduction)
161
+ # ===========================================================================
162
+
163
+ T4_MUL_OPB = 32
164
+ T4_MUL_PRB = 64
165
+ T4_MUL_COLS = 2 * T4_MUL_OPB - 1 # 63
166
+ T4_MUL_SUMB = 6
167
+ T4_MUL_CARB = 5
168
+ T4_MUL_IN = T4_MUL_SUMB + T4_MUL_CARB # 11
169
+ T4_NIB = 4
170
+ T4_RED_NIBBLES = 9
171
+ T4_RED_IN = T4_NIB + T4_NIB + 1 # 9
172
+ T4_RED_OUT = T4_NIB + 1 # 5
173
+ T4_T_BITS = 64
174
+
175
+
176
+ # -- generic carry-save / reduction wiring (parameterized by geometry) -------
177
+
178
+ def _bits(v: torch.Tensor, nb: int) -> torch.Tensor:
179
+ return ((v.unsqueeze(1) >> torch.arange(nb, device=v.device)) & 1).float()
180
+
181
+
182
+ def _column_sums(x_bits: torch.Tensor, y_bits: torch.Tensor, opb: int, cols: int) -> torch.Tensor:
183
+ outer = x_bits.unsqueeze(2) * y_bits.unsqueeze(1)
184
+ n = outer.shape[0]
185
+ s = torch.zeros(n, cols, dtype=outer.dtype, device=outer.device)
186
+ for i in range(opb):
187
+ for j in range(opb):
188
+ s[:, i + j] += outer[:, i, j]
189
+ return s
190
+
191
+
192
+ def _encode_carry(s: torch.Tensor, c: torch.Tensor, sumb: int, carb: int) -> torch.Tensor:
193
+ si = torch.arange(sumb, device=s.device)
194
+ ci = torch.arange(carb, device=c.device)
195
+ sb = ((s.unsqueeze(1) >> si) & 1).float()
196
+ cb = ((c.unsqueeze(1) >> ci) & 1).float()
197
+ return torch.cat([sb, cb], dim=1)
198
+
199
+
200
+ def _carry_bits_to_int(bits: torch.Tensor, carb: int) -> torch.Tensor:
201
+ w = (1 << torch.arange(carb, device=bits.device)).long()
202
+ return (bits.round().clamp(0, 1).long() * w).sum(dim=-1)
203
+
204
+
205
+ @torch.no_grad()
206
+ def _closed_loop_mul(step, col_sums, cols, carb):
207
+ n = col_sums.shape[0]
208
+ s = col_sums.long()
209
+ carry = torch.zeros(n, dtype=torch.long, device=s.device)
210
+ out = torch.empty(n, cols * carb, device=col_sums.device)
211
+ sumb = T4_MUL_SUMB if cols == T4_MUL_COLS else T3_MUL_SUMB
212
+ for c in range(cols):
213
+ lg = step(_encode_carry(s[:, c], carry, sumb, carb))
214
+ out[:, carb * c:carb * (c + 1)] = lg
215
+ carry = _carry_bits_to_int((lg > 0).float(), carb)
216
+ return out
217
+
218
+
219
+ def _routed_product_bits(carry_logits, col_parity, carb):
220
+ """Fixed parity readout: bit_c = parity(S_c) XOR lsb(carry into c)."""
221
+ BIG = 20.0
222
+ lsb = carry_logits[:, 0::carb]
223
+ bit0 = (2.0 * col_parity[:, 0:1] - 1.0) * BIG
224
+ mid = (1.0 - 2.0 * col_parity[:, 1:]) * lsb[:, :-1]
225
+ bit_last = lsb[:, -1:]
226
+ return torch.cat([bit0, mid, bit_last], dim=1)
227
+
228
+
229
+ @torch.no_grad()
230
+ def _composed_product_bits(step, x, y, opb, cols, prb, carb):
231
+ """Trained carry step (closed loop) + parity readout -> product BITS (B, prb)."""
232
+ col_sums = _column_sums(_bits(x, opb), _bits(y, opb), opb, cols)
233
+ logits = _closed_loop_mul(step, col_sums, cols, carb)
234
+ col_parity = (col_sums.long() & 1).float()
235
+ bit_logits = _routed_product_bits(logits, col_parity, carb)
236
+ return (bit_logits > 0).long() # (B, prb) bits LSB first
237
+
238
+
239
+ def _encode_red(a, b, bin_, nib):
240
+ ai = torch.arange(nib, device=a.device)
241
+ aa = ((a.unsqueeze(1) >> ai) & 1).float()
242
+ bb = ((b.unsqueeze(1) >> ai) & 1).float()
243
+ cc = bin_.float().unsqueeze(1)
244
+ return torch.cat([aa, bb, cc], dim=1)
245
+
246
+
247
+ def _red_bits_to_out(bits, nib):
248
+ hb = (bits > 0).long()
249
+ w = (1 << torch.arange(nib, device=bits.device)).long()
250
+ d = (hb[:, :nib] * w).sum(dim=1)
251
+ bout = hb[:, nib]
252
+ return d, bout
253
+
254
+
255
+ @torch.no_grad()
256
+ def _composed_reduce_bits(step, t_bits, p, nib, nibbles, t_bits_n):
257
+ """Restoring division of the bit-represented product by p -> r (B,).
258
+
259
+ R stays in [0, p), so R never overflows int64 even when the full product
260
+ does. The bit shifts, ge-from-final-borrow, and keep/replace of R are fixed
261
+ wiring; the per-nibble subtract DECISION is the trained step.
262
+ """
263
+ n = t_bits.shape[0]
264
+ device = t_bits.device
265
+ R = torch.zeros(n, dtype=torch.long, device=device)
266
+ p_nib = torch.stack([(p >> (nib * k)) & 0xF for k in range(nibbles)], dim=1)
267
+ wk = (1 << (nib * torch.arange(nibbles, device=device))).long()
268
+ for i in range(t_bits_n - 1, -1, -1):
269
+ bit = t_bits[:, i].long()
270
+ Rpre = (R << 1) | bit
271
+ borrow = torch.zeros(n, dtype=torch.long, device=device)
272
+ diff_nib = torch.zeros(n, nibbles, dtype=torch.long, device=device)
273
+ for k in range(nibbles):
274
+ an = (Rpre >> (nib * k)) & 0xF
275
+ bn = p_nib[:, k]
276
+ lg = step(_encode_red(an, bn, borrow, nib))
277
+ d, bout = _red_bits_to_out(lg, nib)
278
+ diff_nib[:, k] = d
279
+ borrow = bout
280
+ ge = (borrow == 0).long()
281
+ diff_val = (diff_nib * wk).sum(dim=1)
282
+ R = torch.where(ge.bool(), diff_val, Rpre)
283
+ return R
284
+
285
+
286
+ # ===========================================================================
287
+ # Router
288
+ # ===========================================================================
289
+
290
+ T3_MIN_P = MAX_P + 1 # 252
291
+ T3_MAX_P = (1 << 16) - 1 # tier-3 primes are 9-16 bits
292
+ T4_MIN_P = 1 << 16 # 65536
293
+ T4_MAX_P = (1 << 32) - 1 # tier-4 primes are 17-32 bits
294
+
295
+
296
+ class ResidueRouterV1(ModularMultiplicationModel):
297
+ """Router over per-tier specialists, selected by the size of p.
298
+
299
+ Kept the class name ``ResidueRouterV1`` so the manifest entry_class is
300
+ stable across versions; this is v3 (tiers 1-4).
301
+ """
302
+
303
+ def __init__(self):
304
+ self.small: SmallResidueNet | None = None
305
+ self.t3_mul: StepMLP | None = None
306
+ self.t3_red: StepMLP | None = None
307
+ self.t4_mul: StepMLP | None = None
308
+ self.t4_red: StepMLP | None = None
309
+
310
+ def load(self, model_dir: str) -> None:
311
+ from safetensors.torch import load_file
312
+
313
+ torch.manual_seed(0)
314
+ model_dir = Path(model_dir)
315
+ config = json.loads((model_dir / "config.json").read_text())
316
+
317
+ tensors = load_file(str(model_dir / "weights.safetensors"))
318
+ if "small" in config:
319
+ net = SmallResidueNet(**config["small"])
320
+ state = {k[len("small."):]: v for k, v in tensors.items() if k.startswith("small.")}
321
+ net.load_state_dict(state, strict=True)
322
+ net.eval()
323
+ self.small = net
324
+
325
+ if "t3" in config:
326
+ w, d = config["t3"]["width"], config["t3"]["depth"]
327
+ mul = StepMLP(T3_MUL_IN, T3_MUL_CARB, w, d)
328
+ red = StepMLP(T3_RED_IN, T3_RED_OUT, w, d)
329
+ mul.load_state_dict(_remap(load_file(str(model_dir / "t3_mul.safetensors"))), strict=True)
330
+ red.load_state_dict(_remap(load_file(str(model_dir / "t3_red.safetensors"))), strict=True)
331
+ mul.eval(); red.eval()
332
+ self.t3_mul, self.t3_red = mul, red
333
+
334
+ if "t4" in config:
335
+ mw, rw, d = config["t4"]["mul_width"], config["t4"]["red_width"], config["t4"]["depth"]
336
+ mul = StepMLP(T4_MUL_IN, T4_MUL_CARB, mw, d)
337
+ red = StepMLP(T4_RED_IN, T4_RED_OUT, rw, d)
338
+ mul.load_state_dict(_remap(load_file(str(model_dir / "t4_mul.safetensors"))), strict=True)
339
+ red.load_state_dict(_remap(load_file(str(model_dir / "t4_red.safetensors"))), strict=True)
340
+ mul.eval(); red.eval()
341
+ self.t4_mul, self.t4_red = mul, red
342
+
343
+ def preprocess_a(self, a):
344
+ return a
345
+
346
+ def preprocess_b(self, b):
347
+ return b
348
+
349
+ def preprocess_p(self, p):
350
+ return p
351
+
352
+ @torch.no_grad()
353
+ def predict_digits(self, a_enc, b_enc, p_enc):
354
+ return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
355
+
356
+ @torch.no_grad()
357
+ def predict_digits_batch(self, inputs):
358
+ out: list[list[int] | None] = [None] * len(inputs)
359
+ s_x, s_y, s_p, s_idx = [], [], [], [] # tier 1-2
360
+ t3_x, t3_y, t3_p, t3_idx = [], [], [], [] # tier 3
361
+ t4_x, t4_y, t4_p, t4_idx = [], [], [], [] # tier 4
362
+
363
+ for i, (a_enc, b_enc, p_enc) in enumerate(inputs):
364
+ try:
365
+ p = int(p_enc)
366
+ except (ValueError, TypeError):
367
+ out[i] = [0]
368
+ continue
369
+ # Operand normalization: a with p, then b with p (never all three).
370
+ try:
371
+ xr = int(a_enc) % p
372
+ yr = int(b_enc) % p
373
+ except (ValueError, TypeError):
374
+ out[i] = [0]
375
+ continue
376
+
377
+ if self.small is not None and 2 <= p <= MAX_P and int(self.small.prime_lookup[p]) >= 0:
378
+ s_x.append(xr); s_y.append(yr); s_p.append(p); s_idx.append(i)
379
+ elif self.t3_mul is not None and T3_MIN_P <= p <= T3_MAX_P:
380
+ t3_x.append(xr); t3_y.append(yr); t3_p.append(p); t3_idx.append(i)
381
+ elif self.t4_mul is not None and T4_MIN_P <= p <= T4_MAX_P:
382
+ t4_x.append(xr); t4_y.append(yr); t4_p.append(p); t4_idx.append(i)
383
+ else:
384
+ out[i] = [0] # outside the trained regime -> honest fallback
385
+
386
+ if s_idx:
387
+ preds = self.small.predict(
388
+ torch.tensor(s_x, dtype=torch.long),
389
+ torch.tensor(s_y, dtype=torch.long),
390
+ torch.tensor(s_p, dtype=torch.long),
391
+ ).tolist()
392
+ for j, i in enumerate(s_idx):
393
+ out[i] = [int(preds[j])]
394
+
395
+ if t3_idx:
396
+ x_t = torch.tensor(t3_x, dtype=torch.long)
397
+ y_t = torch.tensor(t3_y, dtype=torch.long)
398
+ p_t = torch.tensor(t3_p, dtype=torch.long)
399
+ tb = _composed_product_bits(self.t3_mul, x_t, y_t, T3_MUL_OPB, T3_MUL_COLS,
400
+ T3_MUL_PRB, T3_MUL_CARB)
401
+ r = _composed_reduce_bits(self.t3_red, tb, p_t, T3_NIB, T3_RED_NIBBLES, T3_T_BITS)
402
+ for j, i in enumerate(t3_idx):
403
+ out[i] = _digits_msb(int(r[j].item()))
404
+
405
+ if t4_idx:
406
+ x_t = torch.tensor(t4_x, dtype=torch.long)
407
+ y_t = torch.tensor(t4_y, dtype=torch.long)
408
+ p_t = torch.tensor(t4_p, dtype=torch.long)
409
+ tb = _composed_product_bits(self.t4_mul, x_t, y_t, T4_MUL_OPB, T4_MUL_COLS,
410
+ T4_MUL_PRB, T4_MUL_CARB)
411
+ r = _composed_reduce_bits(self.t4_red, tb, p_t, T4_NIB, T4_RED_NIBBLES, T4_T_BITS)
412
+ for j, i in enumerate(t4_idx):
413
+ out[i] = _digits_msb(int(r[j].item()))
414
+
415
+ return [o if o is not None else [0] for o in out]
416
+
417
+ def max_batch_size(self) -> int:
418
+ return 512
419
+
420
+
421
+ def _digits_msb(v: int) -> list[int]:
422
+ """Base-256 digits, MSB-first; at least one digit."""
423
+ if v == 0:
424
+ return [0]
425
+ ds = []
426
+ while v > 0:
427
+ ds.append(v & 0xFF)
428
+ v >>= 8
429
+ return ds[::-1]
430
+
431
+
432
+ def _remap(state: dict) -> dict:
433
+ """Map the trained step-net state-dict keys onto the StepMLP layout.
434
+
435
+ The training-side MulCarryStep/RedBorrowStep store layers under the same
436
+ ``layers.*`` / ``head.*`` names as StepMLP, so this is identity; kept as a
437
+ seam in case a future export renames keys.
438
+ """
439
+ return state
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
t4_collapse_receipt.json ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mode": "fp",
3
+ "params": {
4
+ "mul": 35205,
5
+ "red": 20069
6
+ },
7
+ "arch": {
8
+ "mul_width": 128,
9
+ "red_width": 96,
10
+ "depth": 3,
11
+ "max_t": 15
12
+ },
13
+ "train_log": {
14
+ "mul": {
15
+ "final_loss": 4.0139194190658145e-09,
16
+ "wall_s": 67.6
17
+ },
18
+ "red": {
19
+ "final_loss": 5.262944746675657e-09,
20
+ "wall_s": 95.5
21
+ }
22
+ },
23
+ "coverage": {
24
+ "mul_cases": 1056,
25
+ "mul_total": 1056,
26
+ "red_cases": 512,
27
+ "red_total": 512,
28
+ "mul_realizable_per_case_exact": 1.0,
29
+ "red_per_case_exact": 1.0
30
+ },
31
+ "techniques": {
32
+ "reciprocal_operand_framing": true,
33
+ "charton_kempe_two_set": {
34
+ "repeat_triples": 150,
35
+ "repeat_mult": 2,
36
+ "fresh_triples": 1200
37
+ }
38
+ },
39
+ "gate_primes": [
40
+ 267619873,
41
+ 631325533,
42
+ 1806472277,
43
+ 3134734633,
44
+ 3874904347
45
+ ],
46
+ "per_prime_exact": [
47
+ 1.0,
48
+ 1.0,
49
+ 1.0,
50
+ 1.0,
51
+ 1.0
52
+ ],
53
+ "worst_fresh_exact": 1.0,
54
+ "overall_exact": 1.0,
55
+ "tier4_cleared_90": true,
56
+ "tier4_cleared_99": true,
57
+ "collapse_mean": 0.0,
58
+ "trained_mul_random_red_mean": 0.0
59
+ }
t4_mul.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ffcc202cd451944764bd2ad0bc9cca68b45fd0e4139834c7f4f336cb64e6eccf
3
+ size 141428
t4_red.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05c29eaaaa5ea0aa35b7267ff728299f30ec0a0f222225d08e85911af0d44344
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