from __future__ import annotations from pathlib import Path import torch from torch import nn from modchallenge.interface.base_model import ModularMultiplicationModel def ints_to_bits(values: list[int], device: torch.device, width: int) -> torch.Tensor: """Convert nonnegative Python integers to fixed-width, MSB-first bits.""" byte_width = (width + 7) // 8 packed_bytes = bytearray().join( int(value).to_bytes(byte_width, "big") for value in values ) packed = torch.frombuffer(packed_bytes, dtype=torch.uint8) packed = packed.reshape(len(values), byte_width).to(device=device) shifts = torch.arange(7, -1, -1, device=device) bits = ((packed[:, :, None] >> shifts) & 1).reshape(len(values), byte_width * 8) return bits[:, byte_width * 8 - width :] def ints_to_digits( values: list[int], radix: int, width: int, device: torch.device, ) -> torch.Tensor: bits_per_digit = radix.bit_length() - 1 mask = radix - 1 rows = [ [ (value >> (bits_per_digit * position)) & mask for position in range(width - 1, -1, -1) ] for value in values ] return torch.tensor(rows, dtype=torch.long, device=device) class TransitionCell(nn.Module): def __init__( self, radix: int = 2, dmodel: int = 32, hidden: int = 64, layers: int = 2, bidirectional: bool = True, ) -> None: super().__init__() self.input_projection = nn.Linear(3, dmodel) self.digit_embedding = nn.Embedding(radix, dmodel) self.recurrent = nn.GRU( dmodel, hidden, num_layers=layers, batch_first=True, bidirectional=bidirectional, ) directions = 2 if bidirectional else 1 self.output = nn.Linear(directions * hidden, 1) def forward(self, features: torch.Tensor, digits: torch.Tensor) -> torch.Tensor: embedded = self.input_projection(features) embedded = embedded + self.digit_embedding(digits)[:, None, :] hidden, _ = self.recurrent(embedded) return self.output(hidden).squeeze(-1) class FastModularModel(ModularMultiplicationModel): def __init__(self) -> None: self.model: TransitionCell | None = None self.device: torch.device | None = None self.radix = 2 self.max_width = 2048 self.bits_per_digit = 1 def load(self, model_dir: str) -> None: self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") checkpoint = torch.load( Path(model_dir) / "weights.pt", map_location=self.device, weights_only=True, ) config = checkpoint["config"] self.radix = int(config["radix"]) self.bits_per_digit = self.radix.bit_length() - 1 self.max_width = int(checkpoint["max_width"]) self.model = TransitionCell(**config) self.model.load_state_dict(checkpoint["state_dict"]) self.model.to(self.device) self.model.recurrent.flatten_parameters() self.model.eval() if self.device.type == "cuda": torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True def preprocess_a(self, a: str) -> int: return int(a) def preprocess_b(self, b: str) -> int: return int(b) def preprocess_p(self, p: str) -> int: return int(p) @torch.inference_mode() def predict_digits(self, a_enc, b_enc, p_enc) -> list[int]: return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0] @torch.inference_mode() def predict_digits_batch(self, inputs) -> list[list[int]]: output: list[list[int]] = [[0] for _ in inputs] indices: list[int] = [] a_values: list[int] = [] b_values: list[int] = [] moduli: list[int] = [] for index, (a_enc, b_enc, p_enc) in enumerate(inputs): p = int(p_enc) if p < 2 or p.bit_length() > self.max_width: continue indices.append(index) a_values.append(int(a_enc) % p) b_values.append(int(b_enc) % p) moduli.append(p) if not indices: return output assert self.device is not None effective_width = max(p.bit_length() for p in moduli) effective_width = min(self.max_width, max(8, ((effective_width + 7) // 8) * 8)) digit_width = max( 1, (max(value.bit_length() for value in b_values) + self.bits_per_digit - 1) // self.bits_per_digit, ) p_bits = ints_to_bits(moduli, self.device, effective_width).float() x_bits = ints_to_bits(a_values, self.device, effective_width).float() control_digits = ints_to_digits( b_values, self.radix, digit_width, self.device, ) state = torch.zeros( (len(indices), effective_width), dtype=torch.float32, device=self.device, ) for position in range(digit_width): state = self._step( state, x_bits, p_bits, control_digits[:, position], ) rows = state.to(dtype=torch.int64).tolist() for row_index, output_index in enumerate(indices): output[output_index] = [int(bit) for bit in rows[row_index]] return output def max_batch_size(self) -> int: return 256 def _step( self, state: torch.Tensor, multiplicand: torch.Tensor, modulus: torch.Tensor, digit: torch.Tensor, ) -> torch.Tensor: assert self.model is not None features = torch.stack((state, multiplicand, modulus), dim=-1) if self.device is not None and self.device.type == "cuda": with torch.autocast(device_type="cuda", dtype=torch.bfloat16): logits = self.model(features, digit) else: logits = self.model(features, digit) return (logits.float() > 0).float()