robertboy18 commited on
Commit
956b8cf
·
verified ·
1 Parent(s): 25386b1

Publish SAIR Modular Arithmetic Challenge submission

Browse files
Files changed (3) hide show
  1. manifest.json +7 -0
  2. model.py +184 -0
  3. weights.pt +3 -0
manifest.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "entry_class": "model.FastModularModel",
3
+ "output_base": 2,
4
+ "framework": "pytorch",
5
+ "model_description": "A 260,929-parameter modulus-conditioned recurrent model with a 64-dimensional input projection and a two-layer, 96-hidden-unit bidirectional GRU. After pairwise a mod p and b mod p normalization, the learned cell repeatedly predicts the radix-2 Horner transition over binary state, multiplicand, and modulus channels, then emits the final state as base-2 digits.",
6
+ "training_description": "Trained independently from random initialization using AdamW on synthetic exact transitions from 8 through 2,048 bits. The curriculum mixed random moduli, small primes, wrap boundaries, on-policy Horner states, sparse powers, all-one prefixes, and width-boundary trajectories, with held-out exact-match and signed-margin checkpoint selection. The final checkpoint is a low-rate continuation hardened on anchor-biased structured trajectories. No pretrained, external, hand-initialized, or competitor weights were used."
7
+ }
model.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+ from modchallenge.interface.base_model import ModularMultiplicationModel
9
+
10
+ def ints_to_bits(values: list[int], device: torch.device, width: int) -> torch.Tensor:
11
+ """Convert nonnegative Python integers to fixed-width, MSB-first bits."""
12
+ byte_width = (width + 7) // 8
13
+ packed_bytes = bytearray().join(
14
+ int(value).to_bytes(byte_width, "big") for value in values
15
+ )
16
+ packed = torch.frombuffer(packed_bytes, dtype=torch.uint8)
17
+ packed = packed.reshape(len(values), byte_width).to(device=device)
18
+ shifts = torch.arange(7, -1, -1, device=device)
19
+ bits = ((packed[:, :, None] >> shifts) & 1).reshape(len(values), byte_width * 8)
20
+ return bits[:, byte_width * 8 - width :]
21
+
22
+
23
+ def ints_to_digits(
24
+ values: list[int],
25
+ radix: int,
26
+ width: int,
27
+ device: torch.device,
28
+ ) -> torch.Tensor:
29
+ bits_per_digit = radix.bit_length() - 1
30
+ mask = radix - 1
31
+ rows = [
32
+ [
33
+ (value >> (bits_per_digit * position)) & mask
34
+ for position in range(width - 1, -1, -1)
35
+ ]
36
+ for value in values
37
+ ]
38
+ return torch.tensor(rows, dtype=torch.long, device=device)
39
+
40
+
41
+ class TransitionCell(nn.Module):
42
+ def __init__(
43
+ self,
44
+ radix: int = 2,
45
+ dmodel: int = 32,
46
+ hidden: int = 64,
47
+ layers: int = 2,
48
+ bidirectional: bool = True,
49
+ ) -> None:
50
+ super().__init__()
51
+ self.input_projection = nn.Linear(3, dmodel)
52
+ self.digit_embedding = nn.Embedding(radix, dmodel)
53
+ self.recurrent = nn.GRU(
54
+ dmodel,
55
+ hidden,
56
+ num_layers=layers,
57
+ batch_first=True,
58
+ bidirectional=bidirectional,
59
+ )
60
+ directions = 2 if bidirectional else 1
61
+ self.output = nn.Linear(directions * hidden, 1)
62
+
63
+ def forward(self, features: torch.Tensor, digits: torch.Tensor) -> torch.Tensor:
64
+ embedded = self.input_projection(features)
65
+ embedded = embedded + self.digit_embedding(digits)[:, None, :]
66
+ hidden, _ = self.recurrent(embedded)
67
+ return self.output(hidden).squeeze(-1)
68
+
69
+
70
+ class FastModularModel(ModularMultiplicationModel):
71
+ def __init__(self) -> None:
72
+ self.model: TransitionCell | None = None
73
+ self.device: torch.device | None = None
74
+ self.radix = 2
75
+ self.max_width = 2048
76
+ self.bits_per_digit = 1
77
+
78
+ def load(self, model_dir: str) -> None:
79
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
80
+ checkpoint = torch.load(
81
+ Path(model_dir) / "weights.pt",
82
+ map_location=self.device,
83
+ weights_only=True,
84
+ )
85
+ config = checkpoint["config"]
86
+ self.radix = int(config["radix"])
87
+ self.bits_per_digit = self.radix.bit_length() - 1
88
+ self.max_width = int(checkpoint["max_width"])
89
+ self.model = TransitionCell(**config)
90
+ self.model.load_state_dict(checkpoint["state_dict"])
91
+ self.model.to(self.device)
92
+ self.model.recurrent.flatten_parameters()
93
+ self.model.eval()
94
+ if self.device.type == "cuda":
95
+ torch.backends.cuda.matmul.allow_tf32 = True
96
+ torch.backends.cudnn.allow_tf32 = True
97
+
98
+ def preprocess_a(self, a: str) -> int:
99
+ return int(a)
100
+
101
+ def preprocess_b(self, b: str) -> int:
102
+ return int(b)
103
+
104
+ def preprocess_p(self, p: str) -> int:
105
+ return int(p)
106
+
107
+ @torch.inference_mode()
108
+ def predict_digits(self, a_enc, b_enc, p_enc) -> list[int]:
109
+ return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
110
+
111
+ @torch.inference_mode()
112
+ def predict_digits_batch(self, inputs) -> list[list[int]]:
113
+ output: list[list[int]] = [[0] for _ in inputs]
114
+ indices: list[int] = []
115
+ a_values: list[int] = []
116
+ b_values: list[int] = []
117
+ moduli: list[int] = []
118
+
119
+ for index, (a_enc, b_enc, p_enc) in enumerate(inputs):
120
+ p = int(p_enc)
121
+ if p < 2 or p.bit_length() > self.max_width:
122
+ continue
123
+ indices.append(index)
124
+ a_values.append(int(a_enc) % p)
125
+ b_values.append(int(b_enc) % p)
126
+ moduli.append(p)
127
+
128
+ if not indices:
129
+ return output
130
+
131
+ assert self.device is not None
132
+ effective_width = max(p.bit_length() for p in moduli)
133
+ effective_width = min(self.max_width, max(8, ((effective_width + 7) // 8) * 8))
134
+ digit_width = max(
135
+ 1,
136
+ (max(value.bit_length() for value in b_values) + self.bits_per_digit - 1)
137
+ // self.bits_per_digit,
138
+ )
139
+
140
+ p_bits = ints_to_bits(moduli, self.device, effective_width).float()
141
+ x_bits = ints_to_bits(a_values, self.device, effective_width).float()
142
+ control_digits = ints_to_digits(
143
+ b_values,
144
+ self.radix,
145
+ digit_width,
146
+ self.device,
147
+ )
148
+ state = torch.zeros(
149
+ (len(indices), effective_width),
150
+ dtype=torch.float32,
151
+ device=self.device,
152
+ )
153
+
154
+ for position in range(digit_width):
155
+ state = self._step(
156
+ state,
157
+ x_bits,
158
+ p_bits,
159
+ control_digits[:, position],
160
+ )
161
+
162
+ rows = state.to(dtype=torch.int64).tolist()
163
+ for row_index, output_index in enumerate(indices):
164
+ output[output_index] = [int(bit) for bit in rows[row_index]]
165
+ return output
166
+
167
+ def max_batch_size(self) -> int:
168
+ return 256
169
+
170
+ def _step(
171
+ self,
172
+ state: torch.Tensor,
173
+ multiplicand: torch.Tensor,
174
+ modulus: torch.Tensor,
175
+ digit: torch.Tensor,
176
+ ) -> torch.Tensor:
177
+ assert self.model is not None
178
+ features = torch.stack((state, multiplicand, modulus), dim=-1)
179
+ if self.device is not None and self.device.type == "cuda":
180
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
181
+ logits = self.model(features, digit)
182
+ else:
183
+ logits = self.model(features, digit)
184
+ return (logits.float() > 0).float()
weights.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6eb10dc9c1e37392fc63e942ac05d6ddc3e80f78e7adb2e9d301d3a3189b22fb
3
+ size 1049109