cire77 commited on
Commit
e3c7df7
·
verified ·
1 Parent(s): 34d24f1

tier-2 cls_pp checkpoint (htop90=2)

Browse files
Files changed (3) hide show
  1. manifest.json +7 -0
  2. model.py +266 -0
  3. weights.pt +3 -0
manifest.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "entry_class": "model.EBMModMul",
3
+ "output_base": 10,
4
+ "framework": "pytorch",
5
+ "model_description": "Joint-attention Transformer (~3.2M params, d_model=256, 4-6 layers, 8 heads) that learns x*y mod p for small primes. Operands are reduced per-argument (a%p, b%p) inside predict_digits, then the residues and prime are encoded as fixed-width decimal digits in a single self-attention sequence; a CLS slot reads out the answer residue via either a classification head (residue class in [0, p_max)) or an angular head ((cos,sin) on the unit circle, decoded to the nearest residue). Answer emitted as base-10 digits. Targets tiers 1-3; emits [0] for p >= 1e5 (out of the fixed-width regime).",
6
+ "training_description": "Trained from random init on synthetic (x, y, p, x*y mod p) examples with x,y in [0,p), over the full enumerable prime pools of tiers 1-3. Cross-entropy (classification head) or angular distance loss (Saxena-Charton circle encoding) with AdamW and weight decay; a fixed-dataset + weight-decay 'grokking' regime is used to push within-prime generalization. No hand-coded arithmetic: the modular product is produced by the trained network (randomizing weights collapses accuracy)."
7
+ }
model.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Submission entry point: learned modular multiplication.
2
+
3
+ Compliance contract (see rules/evaluation.md):
4
+ - ``preprocess_*`` are per-argument identities (each sees only its own argument).
5
+ - Inside ``predict_digits_batch`` we reduce each operand modulo p — ``int(a) % p``
6
+ and ``int(b) % p`` — the same two-args-at-a-time normalisation the reference
7
+ baselines use. We never form ``a * b`` or ``(a*b) % p`` in Python/tensors; the
8
+ modular product is produced by the trained network, whose output (a residue in
9
+ ``[0, p)``) materially determines the answer.
10
+ - We emit the residue as base-10 digits (``output_base = 10``); the harness decodes.
11
+
12
+ Out of regime (``p >= 10**WIDTH``, i.e. tiers >= 4) the network's fixed-width
13
+ residue encoding cannot represent the operands, so we emit ``[0]`` — an honest
14
+ fallback, not a guess. This model targets the low tiers (1-3).
15
+
16
+ The architecture (encoder + classification/angular head) is loaded from the
17
+ checkpoint's ``arch`` field, so the same wrapper serves either trained head.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import math
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
+ # ---------------------------------------------------------------------------
31
+ # Fixed dimensions (must match the training code that produced the weights)
32
+ # ---------------------------------------------------------------------------
33
+
34
+ VOCAB_SIZE = 10 # decimal digits 0-9; fixed-width inputs, no PAD token
35
+ WIDTH = 5 # values < 10**5 = 100000 -> covers tiers 1-3
36
+ SEG_X, SEG_Y, SEG_P, SEG_ANS = 0, 1, 2, 3
37
+
38
+
39
+ def digits_fixed(n: int, width: int = WIDTH) -> list[int]:
40
+ """Non-negative int -> fixed-width zero-padded decimal digits, MSB-first."""
41
+ out = [0] * width
42
+ i = width - 1
43
+ while n > 0 and i >= 0:
44
+ out[i] = n % 10
45
+ n //= 10
46
+ i -= 1
47
+ return out
48
+
49
+
50
+ def int_to_decimal_digits(n: int) -> list[int]:
51
+ """Non-negative int -> base-10 digit list, MSB-first ([0] for zero)."""
52
+ if n == 0:
53
+ return [0]
54
+ return [int(c) for c in str(n)]
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Architectures (copied verbatim from training/model.py for state_dict match)
59
+ # ---------------------------------------------------------------------------
60
+
61
+ class JointModMulNetCls(nn.Module):
62
+ def __init__(self, d_model=256, nhead=8, num_layers=6, dim_ff=1024, p_max=256):
63
+ super().__init__()
64
+ self.p_max = p_max
65
+ self.tok_emb = nn.Embedding(VOCAB_SIZE, d_model)
66
+ self.cls_query = nn.Parameter(torch.randn(1, d_model) * 0.02)
67
+ self.seg_emb = nn.Embedding(4, d_model)
68
+ self.pos_emb = nn.Embedding(3 * WIDTH + 1, d_model)
69
+ layer = nn.TransformerEncoderLayer(
70
+ d_model=d_model, nhead=nhead, dim_feedforward=dim_ff,
71
+ dropout=0.0, batch_first=True, activation="gelu",
72
+ )
73
+ self.encoder = nn.TransformerEncoder(layer, num_layers=num_layers)
74
+ self.ln = nn.LayerNorm(d_model)
75
+ self.head = nn.Linear(d_model, p_max)
76
+ seg = torch.tensor([SEG_X] * WIDTH + [SEG_Y] * WIDTH + [SEG_P] * WIDTH + [SEG_ANS])
77
+ self.register_buffer("seg_ids", seg, persistent=False)
78
+ self.register_buffer("pos_ids", torch.arange(3 * WIDTH + 1), persistent=False)
79
+
80
+ def forward(self, x_digits, y_digits, prime_digits):
81
+ b = x_digits.shape[0]
82
+ inp = torch.cat([x_digits, y_digits, prime_digits], dim=1)
83
+ tok = self.tok_emb(inp)
84
+ cls = self.cls_query.unsqueeze(0).expand(b, 1, -1)
85
+ x = torch.cat([tok, cls], dim=1)
86
+ x = x + self.seg_emb(self.seg_ids.unsqueeze(0)) + self.pos_emb(self.pos_ids.unsqueeze(0))
87
+ x = self.encoder(x)
88
+ x = self.ln(x)
89
+ return self.head(x[:, -1, :]) # (B, p_max)
90
+
91
+
92
+ class JointModMulNetAngular(nn.Module):
93
+ def __init__(self, d_model=256, nhead=8, num_layers=6, dim_ff=1024):
94
+ super().__init__()
95
+ self.tok_emb = nn.Embedding(VOCAB_SIZE, d_model)
96
+ self.cls_query = nn.Parameter(torch.randn(1, d_model) * 0.02)
97
+ self.seg_emb = nn.Embedding(4, d_model)
98
+ self.pos_emb = nn.Embedding(3 * WIDTH + 1, d_model)
99
+ layer = nn.TransformerEncoderLayer(
100
+ d_model=d_model, nhead=nhead, dim_feedforward=dim_ff,
101
+ dropout=0.0, batch_first=True, activation="gelu",
102
+ )
103
+ self.encoder = nn.TransformerEncoder(layer, num_layers=num_layers)
104
+ self.ln = nn.LayerNorm(d_model)
105
+ self.head = nn.Linear(d_model, 2)
106
+ seg = torch.tensor([SEG_X] * WIDTH + [SEG_Y] * WIDTH + [SEG_P] * WIDTH + [SEG_ANS])
107
+ self.register_buffer("seg_ids", seg, persistent=False)
108
+ self.register_buffer("pos_ids", torch.arange(3 * WIDTH + 1), persistent=False)
109
+
110
+ def forward(self, x_digits, y_digits, prime_digits):
111
+ b = x_digits.shape[0]
112
+ inp = torch.cat([x_digits, y_digits, prime_digits], dim=1)
113
+ tok = self.tok_emb(inp)
114
+ cls = self.cls_query.unsqueeze(0).expand(b, 1, -1)
115
+ x = torch.cat([tok, cls], dim=1)
116
+ x = x + self.seg_emb(self.seg_ids.unsqueeze(0)) + self.pos_emb(self.pos_ids.unsqueeze(0))
117
+ x = self.encoder(x)
118
+ x = self.ln(x)
119
+ return self.head(x[:, -1, :]) # (B, 2)
120
+
121
+
122
+ PRIME_ENUM_LIMIT = 65536
123
+
124
+
125
+ def _sieve_primes(limit: int) -> list[int]:
126
+ is_p = bytearray([1]) * limit
127
+ is_p[0] = is_p[1] = 0
128
+ for i in range(2, int(limit ** 0.5) + 1):
129
+ if is_p[i]:
130
+ is_p[i * i :: i] = bytearray(len(is_p[i * i :: i]))
131
+ return [i for i in range(2, limit) if is_p[i]]
132
+
133
+
134
+ class JointModMulNetClsPP(nn.Module):
135
+ """Joint-attention classifier with a learned per-prime embedding.
136
+ Mirrors training/model.py for state_dict compatibility."""
137
+
138
+ def __init__(self, d_model=256, nhead=8, num_layers=6, dim_ff=1024, p_max=256):
139
+ super().__init__()
140
+ self.p_max = p_max
141
+ self.limit = PRIME_ENUM_LIMIT
142
+ self.tok_emb = nn.Embedding(VOCAB_SIZE, d_model)
143
+ self.cls_query = nn.Parameter(torch.randn(1, d_model) * 0.02)
144
+ self.seg_emb = nn.Embedding(4, d_model)
145
+ self.pos_emb = nn.Embedding(3 * WIDTH + 1, d_model)
146
+ layer = nn.TransformerEncoderLayer(
147
+ d_model=d_model, nhead=nhead, dim_feedforward=dim_ff,
148
+ dropout=0.0, batch_first=True, activation="gelu",
149
+ )
150
+ self.encoder = nn.TransformerEncoder(layer, num_layers=num_layers)
151
+ self.ln = nn.LayerNorm(d_model)
152
+ self.head = nn.Linear(d_model, p_max)
153
+ primes = _sieve_primes(self.limit)
154
+ self.prime_emb = nn.Embedding(len(primes), d_model)
155
+ idx = torch.zeros(self.limit, dtype=torch.long)
156
+ valid = torch.zeros(self.limit, dtype=torch.float)
157
+ for rank, p in enumerate(primes):
158
+ idx[p] = rank
159
+ valid[p] = 1.0
160
+ self.register_buffer("idx_lookup", idx, persistent=False)
161
+ self.register_buffer("valid_lookup", valid, persistent=False)
162
+ self.register_buffer(
163
+ "place_value",
164
+ torch.tensor([10 ** (WIDTH - 1 - i) for i in range(WIDTH)], dtype=torch.long),
165
+ persistent=False,
166
+ )
167
+ seg = torch.tensor([SEG_X] * WIDTH + [SEG_Y] * WIDTH + [SEG_P] * WIDTH + [SEG_ANS])
168
+ self.register_buffer("seg_ids", seg, persistent=False)
169
+ self.register_buffer("pos_ids", torch.arange(3 * WIDTH + 1), persistent=False)
170
+
171
+ def forward(self, x_digits, y_digits, prime_digits):
172
+ b = x_digits.shape[0]
173
+ p_int = (prime_digits * self.place_value).sum(dim=1)
174
+ safe = p_int.clamp(0, self.limit - 1)
175
+ p_emb = self.prime_emb(self.idx_lookup[safe]) * self.valid_lookup[safe].unsqueeze(-1)
176
+ inp = torch.cat([x_digits, y_digits, prime_digits], dim=1)
177
+ tok = self.tok_emb(inp)
178
+ cls = self.cls_query.unsqueeze(0).expand(b, 1, -1)
179
+ x = torch.cat([tok, cls], dim=1)
180
+ x = x + self.seg_emb(self.seg_ids.unsqueeze(0)) + self.pos_emb(self.pos_ids.unsqueeze(0))
181
+ x = x + p_emb.unsqueeze(1)
182
+ x = self.encoder(x)
183
+ x = self.ln(x)
184
+ return self.head(x[:, -1, :])
185
+
186
+
187
+ _ARCHS = {
188
+ "cls": JointModMulNetCls,
189
+ "cls_pp": JointModMulNetClsPP,
190
+ "angular": JointModMulNetAngular,
191
+ }
192
+
193
+
194
+ def _angular_decode(pred: torch.Tensor, p_int: torch.Tensor) -> torch.Tensor:
195
+ theta = torch.atan2(pred[:, 1], pred[:, 0])
196
+ t = torch.round(theta * p_int.float() / (2 * math.pi))
197
+ return (t % p_int.float()).long()
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # Submission entry class
202
+ # ---------------------------------------------------------------------------
203
+
204
+ class EBMModMul(ModularMultiplicationModel):
205
+ def __init__(self):
206
+ self.model = None
207
+ self.device = None
208
+ self.arch = None
209
+
210
+ def load(self, model_dir: str) -> None:
211
+ if torch.cuda.is_available():
212
+ self.device = torch.device("cuda")
213
+ elif torch.backends.mps.is_available():
214
+ self.device = torch.device("mps")
215
+ else:
216
+ self.device = torch.device("cpu")
217
+
218
+ ckpt = torch.load(Path(model_dir) / "weights.pt",
219
+ map_location=self.device, weights_only=False)
220
+ self.arch = ckpt.get("arch", "cls")
221
+ self.model = _ARCHS[self.arch](**ckpt["config"]).to(self.device)
222
+ self.model.load_state_dict(ckpt["state_dict"])
223
+ self.model.eval()
224
+
225
+ # Per-argument identity preprocessing (each hook sees only its own argument).
226
+ def preprocess_a(self, a): return a
227
+ def preprocess_b(self, b): return b
228
+ def preprocess_p(self, p): return p
229
+
230
+ @torch.no_grad()
231
+ def predict_digits(self, a_enc, b_enc, p_enc):
232
+ return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
233
+
234
+ @torch.no_grad()
235
+ def predict_digits_batch(self, inputs):
236
+ out: list[list[int] | None] = [None] * len(inputs)
237
+ x_rows, y_rows, p_rows, p_ints, idx = [], [], [], [], []
238
+
239
+ for i, (a_enc, b_enc, p_enc) in enumerate(inputs):
240
+ p = int(p_enc)
241
+ # Out of the model's regime (residues don't fit WIDTH digits): honest 0.
242
+ if p >= 10 ** WIDTH:
243
+ out[i] = [0]
244
+ continue
245
+ a_red = int(a_enc) % p # per-operand reduction (allowed)
246
+ b_red = int(b_enc) % p
247
+ x_rows.append(digits_fixed(a_red))
248
+ y_rows.append(digits_fixed(b_red))
249
+ p_rows.append(digits_fixed(p))
250
+ p_ints.append(p)
251
+ idx.append(i)
252
+
253
+ if idx:
254
+ t = lambda r: torch.tensor(r, dtype=torch.long, device=self.device)
255
+ logits = self.model(t(x_rows), t(y_rows), t(p_rows))
256
+ if self.arch == "angular":
257
+ residues = _angular_decode(logits, t(p_ints)).tolist()
258
+ else:
259
+ residues = logits.argmax(dim=-1).tolist()
260
+ for j, i in enumerate(idx):
261
+ out[i] = int_to_decimal_digits(int(residues[j]))
262
+
263
+ return [o if o is not None else [0] for o in out]
264
+
265
+ def max_batch_size(self) -> int:
266
+ return 512
weights.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:14920439c3ac57c903108cafff9b06176404d772e9d3cf26d856b35291f2d903
3
+ size 25979504