suii0x's picture
Upload t2-base-p-classifier
4fce513 verified
Raw
History Blame Contribute Delete
5.97 kB
"""T2-focused learned residue classifier for modular multiplication.
The model deliberately targets Tiers 1 and 2, where p < 256. It uses the same
allowed input normalization as the reference neural baselines: each operand is
reduced separately modulo p before entering the network. The network then has
to choose the output residue from learned parameters.
There is no inference-time code path that computes ``(a * b) % p``. The only
post-processing is masking classes outside ``[0, p)`` so that the emitted
single base-p digit is well-formed under the challenge decoder.
"""
from __future__ import annotations
from pathlib import Path
import torch
import torch.nn as nn
from modchallenge.interface.base_model import ModularMultiplicationModel
MAX_P = 256
MAX_CLASSES = 256
PAIR_VOCAB = MAX_P * MAX_CLASSES
class ResidueProductNet(nn.Module):
def __init__(
self,
d_model: int = 128,
hidden: int = 512,
depth: int = 3,
bilinear_dim: int = 64,
):
super().__init__()
self.in_emb = nn.Embedding(PAIR_VOCAB, d_model)
self.p_emb = nn.Embedding(MAX_P, d_model)
self.out_emb = nn.Embedding(PAIR_VOCAB, d_model)
self.out_bias = nn.Embedding(PAIR_VOCAB, 1)
self.left_factor = nn.Embedding(PAIR_VOCAB, bilinear_dim)
self.right_factor = nn.Embedding(PAIR_VOCAB, bilinear_dim)
self.candidate_factor = nn.Embedding(PAIR_VOCAB, bilinear_dim)
self.factor_ln = nn.LayerNorm(bilinear_dim)
self.factor_scale = bilinear_dim ** -0.5
nn.init.zeros_(self.candidate_factor.weight)
layers: list[nn.Module] = []
in_dim = 4 * d_model
for _ in range(depth):
layers.extend(
[
nn.Linear(in_dim, hidden),
nn.GELU(),
nn.LayerNorm(hidden),
]
)
in_dim = hidden
layers.append(nn.Linear(hidden, d_model))
layers.append(nn.LayerNorm(d_model))
self.net = nn.Sequential(*layers)
self.config = {
"d_model": d_model,
"hidden": hidden,
"depth": depth,
"bilinear_dim": bilinear_dim,
}
self.register_buffer(
"classes", torch.arange(MAX_CLASSES, dtype=torch.long), persistent=False
)
def forward(self, a_red: torch.Tensor, b_red: torch.Tensor, p: torch.Tensor) -> torch.Tensor:
a_idx = p * MAX_CLASSES + a_red
b_idx = p * MAX_CLASSES + b_red
ea = self.in_emb(a_idx)
eb = self.in_emb(b_idx)
ep = self.p_emb(p)
h = self.net(torch.cat([ea, eb, ea * eb, ep], dim=-1))
candidate_idx = p.unsqueeze(1) * MAX_CLASSES + self.classes.unsqueeze(0)
candidate_emb = self.out_emb(candidate_idx)
logits = torch.einsum("bd,bkd->bk", h, candidate_emb)
logits = logits + self.out_bias(candidate_idx).squeeze(-1)
# Learned low-rank residue-product factorization. This is another
# trained head, not arithmetic post-processing: with random factors it
# contributes no useful modular multiplication signal.
factor_h = self.factor_ln(self.left_factor(a_idx) * self.right_factor(b_idx))
factor_candidates = self.candidate_factor(candidate_idx)
logits = logits + self.factor_scale * torch.einsum(
"bd,bkd->bk", factor_h, factor_candidates
)
invalid = self.classes.unsqueeze(0) >= p.unsqueeze(1)
return logits.masked_fill(invalid, -1.0e9)
class T2ResidueClassifier(ModularMultiplicationModel):
def __init__(self):
self.model: ResidueProductNet | None = None
self.device: torch.device | None = None
def load(self, model_dir: str) -> None:
if torch.backends.mps.is_available():
self.device = torch.device("mps")
elif torch.cuda.is_available():
self.device = torch.device("cuda")
else:
self.device = torch.device("cpu")
ckpt = torch.load(
Path(model_dir) / "weights.pt",
map_location=self.device,
weights_only=True,
)
self.model = ResidueProductNet(**ckpt.get("config", {}))
load_result = self.model.load_state_dict(ckpt["state_dict"], strict=False)
extra_keys = load_result[1]
if extra_keys:
raise RuntimeError(f"extra checkpoint keys: {extra_keys}")
self.model.to(self.device)
self.model.eval()
def preprocess_a(self, a):
return a
def preprocess_b(self, b):
return b
def preprocess_p(self, p):
return p
@torch.no_grad()
def predict_digits(self, a_enc, b_enc, p_enc):
return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
@torch.no_grad()
def predict_digits_batch(self, inputs):
assert self.model is not None
assert self.device is not None
out: list[list[int] | None] = [None] * len(inputs)
a_rows: list[int] = []
b_rows: list[int] = []
p_rows: list[int] = []
idx: list[int] = []
for i, (a_enc, b_enc, p_enc) in enumerate(inputs):
p = int(p_enc)
if not (2 <= p < MAX_P):
out[i] = [0]
continue
a_rows.append(int(a_enc) % p)
b_rows.append(int(b_enc) % p)
p_rows.append(p)
idx.append(i)
if idx:
a_t = torch.tensor(a_rows, dtype=torch.long, device=self.device)
b_t = torch.tensor(b_rows, dtype=torch.long, device=self.device)
p_t = torch.tensor(p_rows, dtype=torch.long, device=self.device)
preds = self.model(a_t, b_t, p_t).argmax(dim=-1).tolist()
for j, i in enumerate(idx):
out[i] = [int(preds[j])]
return [row if row is not None else [0] for row in out]
def max_batch_size(self) -> int:
return 4096