suii0x's picture
Upload 10 files
17abf28 verified
Raw
History Blame Contribute Delete
4.81 kB
"""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
# Historical default matching task 2's T1/T2-only domain (p < 2**8).
# ReductionCell now takes p_bits as a constructor argument instead (task 3
# extends the domain to p up to 2**16 for T3); this constant is kept only
# as that argument's default so old call sites without an explicit p_bits
# keep behaving exactly as before.
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
# Shared embedding table for any base-R digit value (input stream
# digits, state digits, m digits alike -- they are all "a digit in
# [0, R)" semantically).
self.digit_embed = nn.Embedding(radix, digit_embed_dim)
self.input_size = (
digit_embed_dim # digit_in
+ state_digits * digit_embed_dim # state_digits
+ state_digits * digit_embed_dim # m_digits
+ p_bits # p bits, raw 0/1 floats
+ 1 # phase flag, raw 0.0/1.0 float
)
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, # (..., ) long
state_digits: torch.Tensor, # (..., D) long
m_digits: torch.Tensor, # (..., D) long
p_bits: torch.Tensor, # (..., 8) float
phase_flag: torch.Tensor, # (..., 1) float
) -> 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) # (..., E)
state_e = self.digit_embed(state_digits) # (..., D, E)
state_e = state_e.flatten(start_dim=-2) # (..., D*E)
m_e = self.digit_embed(m_digits)
m_e = m_e.flatten(start_dim=-2) # (..., D*E)
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