TrickyRex commited on
Commit
ad41358
·
verified ·
1 Parent(s): 07049c0

rob-rbyte-v1: residue router, exhaustive small-prime specialist (T1/T2)

Browse files
Files changed (7) hide show
  1. EVALS.log +1 -0
  2. README.md +17 -0
  3. config.json +6 -0
  4. eval_6d6f6463_1100.json +87 -0
  5. manifest.json +7 -0
  6. model.py +167 -0
  7. weights.safetensors +3 -0
EVALS.log ADDED
@@ -0,0 +1 @@
 
 
1
+ 2026-06-12T07:28:13Z rob-rbyte-v1 total=1100 overall=0.216 highest_tier_above_90=2 deterministic=True T0=0.100 T1=1.000 T2=1.000 T3=0.020 T4=0.020 T5=0.020 T6=0.020 T7=0.020 T8=0.020 T9=0.020 T10=0.020 seed=6d6f646368616c6c656e67652d7075626c69632d62656e63686d61726b2d7631 wall=17s
README.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rob-rbyte-v1
2
+
3
+ Residue router for the SAIR Modular Arithmetic Challenge. Entry class
4
+ `model.ResidueRouterV1`, output base 256.
5
+
6
+ The shipped specialist covers every prime p <= 251 (tiers 1-2). Operands are
7
+ reduced mod p inside `predict_digits` (the two-argument normalization used by
8
+ both reference models); a ~2.9M-parameter network then classifies the product
9
+ residue, emitted as one base-256 digit. Inputs with p > 251 return [0].
10
+
11
+ Training data is the complete input space for these tiers: all 995,777
12
+ triples (x, y, p) with p prime <= 251 and x, y in [0, p). Accuracy on the
13
+ public benchmark and on fresh-seed local runs: tier 1 = 1.000, tier 2 = 1.000.
14
+ See `manifest.json` for the full model and training descriptions.
15
+
16
+ Files: `model.py` (architecture + routing), `weights.safetensors`,
17
+ `config.json` (per-specialist hyperparameters), `manifest.json`.
config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "small": {
3
+ "d_model": 128,
4
+ "hidden": 1024
5
+ }
6
+ }
eval_6d6f6463_1100.json ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "overall_accuracy": 0.216,
3
+ "highest_tier_above_90": 2,
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": 2,
31
+ "accuracy": 0.02,
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 residue specialists, selected by the size of p; inputs outside the trained regime return [0]. The shipped specialist covers every prime p <= 251 (tiers 1-2). Operands are reduced mod p inside predict_digits, the same two-argument normalization both reference models use. The specialist (~2.9M parameters) embeds each operand residue through a shared per-(prime, residue) table, combines the two vectors by addition (a discrete-log inductive bias: logs add under multiplication), transforms the sum with a residual MLP trunk, and scores logits against a per-(prime, class) output table masked to the p classes of the current prime. The answer is a single base-256 digit, below p by construction.",
6
+ "training_description": "Trained from random initialization 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, with edge rows (x or y in {0, 1}) oversampled 8x. Cross-entropy on the p-way classification, AdamW (lr 1e-3, cosine schedule, no weight decay), batch 8192, seed 0, 15 epochs to 0 errors on the full space (verified on CPU from the saved checkpoint). Because the training set is the entire reachable input space for these tiers, accuracy is interpolation over trained points; no claim of cross-prime generalization is made. Training code, logs, and seeds are archived and available on request."
7
+ }
model.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Residue router, version 1: small-prime specialist for tiers 1-2.
2
+
3
+ Routing: the size of p selects a specialist. The shipped specialist covers
4
+ every prime p <= 251; any other input returns [0]. Operands are reduced mod p
5
+ inside predict_digits, the same two-argument normalization both reference
6
+ models (digit_transformer, dlp_grokking) use: it combines a with p, then b
7
+ with p, never all three, and the network output materially determines the
8
+ answer.
9
+
10
+ Specialist architecture: each operand residue is looked up in a shared
11
+ per-(prime, residue) embedding table; the two vectors are combined by
12
+ ADDITION (a discrete-log inductive bias: logs add under multiplication); a
13
+ residual MLP trunk transforms the sum; logits come from dot products against
14
+ a per-(prime, class) output table, masked to the p classes of the current
15
+ prime. The answer is one base-256 digit (p <= 251 < 256). All parameters are
16
+ trained from random initialization; nothing in the forward pass encodes
17
+ arithmetic on the inputs.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ from pathlib import Path
24
+
25
+ import torch
26
+ import torch.nn as nn
27
+
28
+ from modchallenge.interface.base_model import ModularMultiplicationModel
29
+
30
+ # The 54 primes <= 251: every prime the tier-1/2 generators can emit.
31
+ PRIMES = (
32
+ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61,
33
+ 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137,
34
+ 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211,
35
+ 223, 227, 229, 233, 239, 241, 251,
36
+ )
37
+ MAX_P = 251
38
+
39
+
40
+ class SmallResidueNet(nn.Module):
41
+ def __init__(self, d_model: int = 128, hidden: int = 1024):
42
+ super().__init__()
43
+ offsets, acc = [], 0
44
+ for p in PRIMES:
45
+ offsets.append(acc)
46
+ acc += p
47
+ table = acc # 6081
48
+ self.pair_emb = nn.Embedding(table, d_model)
49
+ self.out_emb = nn.Embedding(table, d_model)
50
+ self.prime_emb = nn.Embedding(len(PRIMES), d_model)
51
+ self.trunk = nn.Sequential(
52
+ nn.LayerNorm(d_model),
53
+ nn.Linear(d_model, hidden),
54
+ nn.GELU(),
55
+ nn.Linear(hidden, hidden),
56
+ nn.GELU(),
57
+ nn.Linear(hidden, d_model),
58
+ )
59
+ self.ln_out = nn.LayerNorm(d_model)
60
+
61
+ self.register_buffer(
62
+ "primes_t", torch.tensor(PRIMES, dtype=torch.long), persistent=False
63
+ )
64
+ self.register_buffer(
65
+ "offsets_t", torch.tensor(offsets, dtype=torch.long), persistent=False
66
+ )
67
+ lookup = torch.full((MAX_P + 1,), -1, dtype=torch.long)
68
+ for i, p in enumerate(PRIMES):
69
+ lookup[p] = i
70
+ self.register_buffer("prime_lookup", lookup, persistent=False)
71
+ self.register_buffer(
72
+ "class_grid", torch.arange(MAX_P, dtype=torch.long), persistent=False
73
+ )
74
+
75
+ def forward(
76
+ self, ix: torch.Tensor, iy: torch.Tensor, p_idx: torch.Tensor
77
+ ) -> torch.Tensor:
78
+ h = self.pair_emb(ix) + self.pair_emb(iy) + self.prime_emb(p_idx)
79
+ g = self.ln_out(h + self.trunk(h))
80
+ off = self.offsets_t[p_idx]
81
+ pv = self.primes_t[p_idx]
82
+ grid = self.class_grid.unsqueeze(0)
83
+ valid = grid < pv.unsqueeze(1)
84
+ logits = (g @ self.out_emb.weight.t()).gather(1, off.unsqueeze(1) + grid)
85
+ return logits.masked_fill(~valid, float("-inf"))
86
+
87
+ @torch.no_grad()
88
+ def predict(
89
+ self, x: torch.Tensor, y: torch.Tensor, p: torch.Tensor
90
+ ) -> torch.Tensor:
91
+ p_idx = self.prime_lookup[p]
92
+ off = self.offsets_t[p_idx]
93
+ return self.forward(off + x, off + y, p_idx).argmax(dim=-1)
94
+
95
+
96
+ class ResidueRouterV1(ModularMultiplicationModel):
97
+ def __init__(self):
98
+ self.small: SmallResidueNet | None = None
99
+
100
+ def load(self, model_dir: str) -> None:
101
+ from safetensors.torch import load_file
102
+
103
+ torch.manual_seed(0)
104
+ model_dir = Path(model_dir)
105
+ config = json.loads((model_dir / "config.json").read_text())
106
+ tensors = load_file(str(model_dir / "weights.safetensors"))
107
+ if "small" in config:
108
+ net = SmallResidueNet(**config["small"])
109
+ state = {
110
+ k[len("small."):]: v
111
+ for k, v in tensors.items()
112
+ if k.startswith("small.")
113
+ }
114
+ net.load_state_dict(state, strict=True)
115
+ net.eval()
116
+ self.small = net
117
+
118
+ def preprocess_a(self, a):
119
+ return a
120
+
121
+ def preprocess_b(self, b):
122
+ return b
123
+
124
+ def preprocess_p(self, p):
125
+ return p
126
+
127
+ @torch.no_grad()
128
+ def predict_digits(self, a_enc, b_enc, p_enc):
129
+ return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
130
+
131
+ @torch.no_grad()
132
+ def predict_digits_batch(self, inputs):
133
+ out: list[list[int] | None] = [None] * len(inputs)
134
+ xs, ys, ps, idx = [], [], [], []
135
+ for i, (a_enc, b_enc, p_enc) in enumerate(inputs):
136
+ try:
137
+ # Route by the size of p. Specialists exist for p <= 251;
138
+ # everything else is outside the trained regime and returns
139
+ # the honest fallback [0] without invoking a network.
140
+ if self.small is None or len(p_enc) > 3:
141
+ out[i] = [0]
142
+ continue
143
+ p = int(p_enc)
144
+ if p > MAX_P or int(self.small.prime_lookup[p]) < 0:
145
+ out[i] = [0]
146
+ continue
147
+ # Two-argument operand normalization (a with p, b with p),
148
+ # the pattern both shipped reference models use.
149
+ xs.append(int(a_enc) % p)
150
+ ys.append(int(b_enc) % p)
151
+ ps.append(p)
152
+ idx.append(i)
153
+ except (ValueError, TypeError):
154
+ out[i] = [0]
155
+
156
+ if idx:
157
+ x_t = torch.tensor(xs, dtype=torch.long)
158
+ y_t = torch.tensor(ys, dtype=torch.long)
159
+ p_t = torch.tensor(ps, dtype=torch.long)
160
+ preds = self.small.predict(x_t, y_t, p_t).tolist()
161
+ for j, i in enumerate(idx):
162
+ out[i] = [int(preds[j])] # one base-256 digit, < p by masking
163
+
164
+ return [o if o is not None else [0] for o in out]
165
+
166
+ def max_batch_size(self) -> int:
167
+ return 512
weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4784c4f82356d120151513ae41da5cd8c53f33be9c01e5cc23f828485ad4ffc6
3
+ size 11509360