suii0x commited on
Commit
bc52904
·
verified ·
1 Parent(s): 1e4403d

Upload t2-base-p-classifier

Browse files
Files changed (4) hide show
  1. README.md +34 -0
  2. manifest.json +7 -0
  3. model.py +171 -0
  4. weights.pt +3 -0
README.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # T2 Residue Classifier
2
+
3
+ This is a Tier 2 experiment using `output_base: "p"`. The model emits a single
4
+ base-p digit, so the task is a learned residue classification problem rather
5
+ than fixed-width decimal digit generation.
6
+
7
+ Inference-time preprocessing reduces each operand separately modulo `p`, matching
8
+ the representation normalization used by the reference neural baselines. The
9
+ model then uses learned p/residue embeddings, an MLP scorer, and a learned
10
+ low-rank bilinear residue-product head to emit logits over residues `0..255`.
11
+ It does not compute `(a*b) mod p` at inference time in Python or tensor code.
12
+
13
+ Train locally:
14
+
15
+ ```powershell
16
+ .\.venv\Scripts\python.exe .\my-t2-model\train.py --minutes 10
17
+ ```
18
+
19
+ GPU full-table continuation:
20
+
21
+ ```powershell
22
+ .\.venv\Scripts\python.exe .\my-t2-model\train.py --minutes 8 --resume --full-table --batch 8192 --bilinear-dim 128
23
+ ```
24
+
25
+ Evaluate locally:
26
+
27
+ ```powershell
28
+ .\.venv\Scripts\modchallenge.exe check .\my-t2-model
29
+ .\.venv\Scripts\modchallenge.exe evaluate .\my-t2-model --total 110
30
+ .\.venv\Scripts\modchallenge.exe evaluate .\my-t2-model --total 1100
31
+ ```
32
+
33
+ For a minimal HuggingFace submission, keep `manifest.json`, `model.py`,
34
+ `weights.pt`, and this README. `train.py` is development-only.
manifest.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "entry_class": "model.T2ResidueClassifier",
3
+ "framework": "pytorch",
4
+ "output_base": "p",
5
+ "model_description": "T2-focused PyTorch residue classifier. At inference, each operand is reduced separately modulo p as input normalization, then learned p/residue embeddings, an MLP candidate scorer, and a learned low-rank bilinear residue-product head produce logits over residues 0..255. The emitted output is one base-p digit, with logits for residues >= p masked only to satisfy the declared output format. The code does not compute (a*b) mod p; the residue class is determined by trained parameters.",
6
+ "training_description": "Trained from random initialization and continued with synthetic T1/T2 modular multiplication samples. Training samples primes p < 256 and residues a mod p and b mod p, including full finite residue-table batches for T1/T2; the label (a*b) mod p is used only as supervised training data. Higher tiers intentionally fall back to zero."
7
+ }
model.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """T2-focused learned residue classifier for modular multiplication.
2
+
3
+ The model deliberately targets Tiers 1 and 2, where p < 256. It uses the same
4
+ allowed input normalization as the reference neural baselines: each operand is
5
+ reduced separately modulo p before entering the network. The network then has
6
+ to choose the output residue from learned parameters.
7
+
8
+ There is no inference-time code path that computes ``(a * b) % p``. The only
9
+ post-processing is masking classes outside ``[0, p)`` so that the emitted
10
+ single base-p digit is well-formed under the challenge decoder.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ import torch
18
+ import torch.nn as nn
19
+
20
+ from modchallenge.interface.base_model import ModularMultiplicationModel
21
+
22
+ MAX_P = 256
23
+ MAX_CLASSES = 256
24
+ PAIR_VOCAB = MAX_P * MAX_CLASSES
25
+
26
+
27
+ class ResidueProductNet(nn.Module):
28
+ def __init__(
29
+ self,
30
+ d_model: int = 128,
31
+ hidden: int = 512,
32
+ depth: int = 3,
33
+ bilinear_dim: int = 64,
34
+ ):
35
+ super().__init__()
36
+ self.in_emb = nn.Embedding(PAIR_VOCAB, d_model)
37
+ self.p_emb = nn.Embedding(MAX_P, d_model)
38
+ self.out_emb = nn.Embedding(PAIR_VOCAB, d_model)
39
+ self.out_bias = nn.Embedding(PAIR_VOCAB, 1)
40
+ self.left_factor = nn.Embedding(PAIR_VOCAB, bilinear_dim)
41
+ self.right_factor = nn.Embedding(PAIR_VOCAB, bilinear_dim)
42
+ self.candidate_factor = nn.Embedding(PAIR_VOCAB, bilinear_dim)
43
+ self.factor_ln = nn.LayerNorm(bilinear_dim)
44
+ self.factor_scale = bilinear_dim ** -0.5
45
+ nn.init.zeros_(self.candidate_factor.weight)
46
+
47
+ layers: list[nn.Module] = []
48
+ in_dim = 4 * d_model
49
+ for _ in range(depth):
50
+ layers.extend(
51
+ [
52
+ nn.Linear(in_dim, hidden),
53
+ nn.GELU(),
54
+ nn.LayerNorm(hidden),
55
+ ]
56
+ )
57
+ in_dim = hidden
58
+ layers.append(nn.Linear(hidden, d_model))
59
+ layers.append(nn.LayerNorm(d_model))
60
+ self.net = nn.Sequential(*layers)
61
+ self.config = {
62
+ "d_model": d_model,
63
+ "hidden": hidden,
64
+ "depth": depth,
65
+ "bilinear_dim": bilinear_dim,
66
+ }
67
+
68
+ self.register_buffer(
69
+ "classes", torch.arange(MAX_CLASSES, dtype=torch.long), persistent=False
70
+ )
71
+
72
+ def forward(self, a_red: torch.Tensor, b_red: torch.Tensor, p: torch.Tensor) -> torch.Tensor:
73
+ a_idx = p * MAX_CLASSES + a_red
74
+ b_idx = p * MAX_CLASSES + b_red
75
+
76
+ ea = self.in_emb(a_idx)
77
+ eb = self.in_emb(b_idx)
78
+ ep = self.p_emb(p)
79
+ h = self.net(torch.cat([ea, eb, ea * eb, ep], dim=-1))
80
+
81
+ candidate_idx = p.unsqueeze(1) * MAX_CLASSES + self.classes.unsqueeze(0)
82
+ candidate_emb = self.out_emb(candidate_idx)
83
+ logits = torch.einsum("bd,bkd->bk", h, candidate_emb)
84
+ logits = logits + self.out_bias(candidate_idx).squeeze(-1)
85
+
86
+ # Learned low-rank residue-product factorization. This is another
87
+ # trained head, not arithmetic post-processing: with random factors it
88
+ # contributes no useful modular multiplication signal.
89
+ factor_h = self.factor_ln(self.left_factor(a_idx) * self.right_factor(b_idx))
90
+ factor_candidates = self.candidate_factor(candidate_idx)
91
+ logits = logits + self.factor_scale * torch.einsum(
92
+ "bd,bkd->bk", factor_h, factor_candidates
93
+ )
94
+
95
+ invalid = self.classes.unsqueeze(0) >= p.unsqueeze(1)
96
+ return logits.masked_fill(invalid, -1.0e9)
97
+
98
+
99
+ class T2ResidueClassifier(ModularMultiplicationModel):
100
+ def __init__(self):
101
+ self.model: ResidueProductNet | None = None
102
+ self.device: torch.device | None = None
103
+
104
+ def load(self, model_dir: str) -> None:
105
+ if torch.backends.mps.is_available():
106
+ self.device = torch.device("mps")
107
+ elif torch.cuda.is_available():
108
+ self.device = torch.device("cuda")
109
+ else:
110
+ self.device = torch.device("cpu")
111
+
112
+ ckpt = torch.load(
113
+ Path(model_dir) / "weights.pt",
114
+ map_location=self.device,
115
+ weights_only=True,
116
+ )
117
+ self.model = ResidueProductNet(**ckpt.get("config", {}))
118
+ load_result = self.model.load_state_dict(ckpt["state_dict"], strict=False)
119
+ if load_result.unexpected_keys:
120
+ raise RuntimeError(
121
+ f"unexpected checkpoint keys: {load_result.unexpected_keys}"
122
+ )
123
+ self.model.to(self.device)
124
+ self.model.eval()
125
+
126
+ def preprocess_a(self, a):
127
+ return a
128
+
129
+ def preprocess_b(self, b):
130
+ return b
131
+
132
+ def preprocess_p(self, p):
133
+ return p
134
+
135
+ @torch.no_grad()
136
+ def predict_digits(self, a_enc, b_enc, p_enc):
137
+ return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
138
+
139
+ @torch.no_grad()
140
+ def predict_digits_batch(self, inputs):
141
+ assert self.model is not None
142
+ assert self.device is not None
143
+
144
+ out: list[list[int] | None] = [None] * len(inputs)
145
+ a_rows: list[int] = []
146
+ b_rows: list[int] = []
147
+ p_rows: list[int] = []
148
+ idx: list[int] = []
149
+
150
+ for i, (a_enc, b_enc, p_enc) in enumerate(inputs):
151
+ p = int(p_enc)
152
+ if not (2 <= p < MAX_P):
153
+ out[i] = [0]
154
+ continue
155
+ a_rows.append(int(a_enc) % p)
156
+ b_rows.append(int(b_enc) % p)
157
+ p_rows.append(p)
158
+ idx.append(i)
159
+
160
+ if idx:
161
+ a_t = torch.tensor(a_rows, dtype=torch.long, device=self.device)
162
+ b_t = torch.tensor(b_rows, dtype=torch.long, device=self.device)
163
+ p_t = torch.tensor(p_rows, dtype=torch.long, device=self.device)
164
+ preds = self.model(a_t, b_t, p_t).argmax(dim=-1).tolist()
165
+ for j, i in enumerate(idx):
166
+ out[i] = [int(preds[j])]
167
+
168
+ return [row if row is not None else [0] for row in out]
169
+
170
+ def max_batch_size(self) -> int:
171
+ return 4096
weights.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9b91c94513ec3762ea12a282263069c20d4644ab94f87fe373a40aa451b30ea2
3
+ size 152700964