| """The learned reduction cell: a 2-layer GRU that approximates |
| |
| cell(state, m, digit) = (state * R + m * digit) mod p |
| |
| entirely through trained parameters. No hand-coded modulus reduction of the |
| challenge prime appears anywhere in this file -- the reduction is produced |
| solely by embeddings + GRU + linear head + softmax/argmax. |
| |
| State, m, and p are all explicit digit/bit representations (not opaque |
| latent vectors) fed in and read out as integers via `mac_cell.digits`. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import torch |
| import torch.nn as nn |
|
|
| |
| |
| |
| |
| |
| P_BITS = 8 |
|
|
|
|
| class ReductionCell(nn.Module): |
| """Two-layer unidirectional GRU operating on explicit digit-state input. |
| |
| Per-step input layout (last dim, size `input_size`): |
| [digit_in_embed | state_digits_embed (D slots) | m_digits_embed (D slots) | p_bits | phase_flag (1)] |
| |
| Output per step: logits of shape (..., state_digits, radix) predicting |
| the *next* state's digits. |
| """ |
|
|
| def __init__( |
| self, |
| radix: int, |
| state_digits: int, |
| hidden_size: int, |
| digit_embed_dim: int = 16, |
| num_layers: int = 2, |
| p_bits: int = P_BITS, |
| ) -> None: |
| super().__init__() |
| self.radix = radix |
| self.state_digits = state_digits |
| self.hidden_size = hidden_size |
| self.digit_embed_dim = digit_embed_dim |
| self.num_layers = num_layers |
| self.p_bits = p_bits |
|
|
| |
| |
| |
| self.digit_embed = nn.Embedding(radix, digit_embed_dim) |
|
|
| self.input_size = ( |
| digit_embed_dim |
| + state_digits * digit_embed_dim |
| + state_digits * digit_embed_dim |
| + p_bits |
| + 1 |
| ) |
|
|
| self.gru = nn.GRU( |
| input_size=self.input_size, |
| hidden_size=hidden_size, |
| num_layers=num_layers, |
| batch_first=True, |
| ) |
| self.head = nn.Linear(hidden_size, state_digits * radix) |
|
|
| def num_params(self) -> int: |
| return sum(p.numel() for p in self.parameters()) |
|
|
| def assemble_features( |
| self, |
| digit_in: torch.Tensor, |
| state_digits: torch.Tensor, |
| m_digits: torch.Tensor, |
| p_bits: torch.Tensor, |
| phase_flag: torch.Tensor, |
| ) -> torch.Tensor: |
| """Build the per-step input feature vector. Works for a full |
| (batch, T, ...) sequence or a single step (batch, ...) alike -- the |
| embedding + concatenation ops broadcast over any leading dims. |
| """ |
| digit_in_e = self.digit_embed(digit_in) |
| state_e = self.digit_embed(state_digits) |
| state_e = state_e.flatten(start_dim=-2) |
| m_e = self.digit_embed(m_digits) |
| m_e = m_e.flatten(start_dim=-2) |
| return torch.cat([digit_in_e, state_e, m_e, p_bits, phase_flag], dim=-1) |
|
|
| def forward_sequence( |
| self, features: torch.Tensor, h0: torch.Tensor | None = None |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| """Run the GRU over a full (batch, T, input_size) sequence at once. |
| |
| Used for teacher-forced training, where every step's true |
| state_digits/m_digits are already known and pre-assembled into |
| `features`. |
| |
| Returns (logits (batch, T, D, R), final hidden state). |
| """ |
| out, h_n = self.gru(features, h0) |
| logits = self.head(out) |
| batch, seq_len, _ = logits.shape |
| logits = logits.view(batch, seq_len, self.state_digits, self.radix) |
| return logits, h_n |
|
|
| def forward_step( |
| self, features: torch.Tensor, h: torch.Tensor | None |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| """Run a single step: `features` is (batch, input_size). |
| |
| Used for autoregressive rollout at inference time, where each |
| step's state_digits input is the model's own previous prediction. |
| |
| Returns (logits (batch, D, R), new hidden state). |
| """ |
| out, h_n = self.gru(features.unsqueeze(1), h) |
| logits = self.head(out.squeeze(1)) |
| logits = logits.view(-1, self.state_digits, self.radix) |
| return logits, h_n |
|
|