modular-arithmetic-challenge / src /v20_compact_universal_u2.py
cpchoi's picture
Add validated faster compiled-local Montgomery v42
2b4770a verified
Raw History Blame Contribute Delete
6.5 kB
"""V20-sized learned controller for the two-program U2 arithmetic board."""
from __future__ import annotations
from collections.abc import Sequence
import torch
import torch.nn as nn
import torch.nn.functional as F
from src.self_clocked_parallel_montgomery_teacher_student import select_candidate
from src.universal_self_clocked_u1 import OP_COUNT, PHASE_COUNT, UniversalU1Memory
from src.universal_self_clocked_u2 import (
ALGORITHM_COUNT,
UniversalSelfClockedU2,
)
class CompactMixedAlgorithmSelfClockTeacher(nn.Module):
"""Learn a finite program using compact scalar clock transitions."""
def __init__(self, *, maximum_width: int = 512, maximum_clock: int = 2048) -> None:
super().__init__()
if maximum_width < 1:
raise ValueError("maximum_width must be positive")
if maximum_clock < 4 * maximum_width:
raise ValueError("maximum_clock must cover four bits per base-16 digit")
self.maximum_width = int(maximum_width)
self.maximum_clock = int(maximum_clock)
self.operation = nn.Embedding(PHASE_COUNT * 2, OP_COUNT)
self.initial_phase = nn.Embedding(ALGORITHM_COUNT, PHASE_COUNT)
self.initial_clock_affine = nn.Embedding(ALGORITHM_COUNT, 2)
self.decrement_clock_affine = nn.Linear(1, 1)
self.terminal_clock = nn.Parameter(torch.empty(1))
self.terminal_test = nn.Linear(1, 2)
self.next_phase = nn.Embedding(PHASE_COUNT * OP_COUNT, PHASE_COUNT)
for parameter in self.parameters():
nn.init.normal_(parameter, std=0.02)
self.config = {
"maximum_width": self.maximum_width,
"maximum_clock": self.maximum_clock,
}
@property
def parameter_count(self) -> int:
return sum(parameter.numel() for parameter in self.parameters())
def _validate_algorithms(self, algorithm_ids: torch.Tensor) -> None:
if algorithm_ids.ndim != 1:
raise ValueError("algorithm_ids must be a one-dimensional batch")
if bool(
((algorithm_ids < 0) | (algorithm_ids >= ALGORITHM_COUNT)).any().item()
):
raise ValueError("algorithm id is outside the learned instruction domain")
def initial_clock_values(
self, algorithm_ids: torch.Tensor, widths: torch.Tensor
) -> torch.Tensor:
self._validate_algorithms(algorithm_ids)
if algorithm_ids.shape != widths.shape:
raise ValueError("algorithm ids and widths must align")
scale_bias = self.initial_clock_affine(algorithm_ids)
return scale_bias[:, 0] * widths.to(scale_bias.dtype) + scale_bias[:, 1]
def raw_decrement_clock_values(self, clocks: torch.Tensor) -> torch.Tensor:
return self.decrement_clock_affine(
clocks.to(torch.float32).unsqueeze(-1)
).squeeze(-1)
def decrement_clock_values(self, clocks: torch.Tensor) -> torch.Tensor:
return F.relu(self.raw_decrement_clock_values(clocks))
def terminal_test_logits(self, clocks: torch.Tensor) -> torch.Tensor:
return self.terminal_test(clocks.to(torch.float32).unsqueeze(-1))
def _discrete_clock(self, values: torch.Tensor) -> torch.Tensor:
return values.round().clamp(0, self.maximum_clock).to(torch.long)
def start(
self, width: int, algorithm_ids: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
if not 1 <= int(width) <= self.maximum_width:
raise ValueError("width exceeds the learned controller domain")
self._validate_algorithms(algorithm_ids)
widths = torch.full_like(algorithm_ids, int(width))
phase = self.initial_phase(algorithm_ids).argmax(-1)
clock = self._discrete_clock(
self.initial_clock_values(algorithm_ids, widths)
)
return phase, clock
def operation_logits(
self, phases: torch.Tensor, clocks: torch.Tensor
) -> torch.Tensor:
clock_class = self.terminal_test_logits(clocks).argmax(-1)
return self.operation(phases * 2 + clock_class)
def instruction(self, phase: torch.Tensor, clock: torch.Tensor) -> torch.Tensor:
return self.operation_logits(phase, clock).argmax(-1)
def advance(
self,
phase: torch.Tensor,
clock: torch.Tensor,
operation: torch.Tensor,
algorithm_ids: torch.Tensor,
*,
width: int,
) -> tuple[torch.Tensor, torch.Tensor]:
widths = torch.full_like(clock, int(width))
row_clock = self._discrete_clock(self.decrement_clock_values(clock))
reset_clock = self._discrete_clock(
self.initial_clock_values(algorithm_ids, widths)
)
terminal_clock = self._discrete_clock(self.terminal_clock).expand_as(clock)
next_clock = select_candidate(
(
row_clock,
reset_clock,
terminal_clock,
row_clock,
terminal_clock,
clock,
),
operation,
)
next_phase = self.next_phase(phase * OP_COUNT + operation).argmax(-1)
return next_phase, next_clock
class V20CompactUniversalSelfClockedU2(UniversalSelfClockedU2):
"""Use the U2 arithmetic board with a compact learned controller."""
def __init__(
self,
*,
reducer: dict,
controller: dict,
mac: dict,
subtract: dict,
factor: dict,
cancellation_teacher: dict,
prime_two_router: dict,
) -> None:
super().__init__(
reducer=reducer,
controller=controller,
mac=mac,
subtract=subtract,
factor=factor,
cancellation_teacher=cancellation_teacher,
prime_two_router=prime_two_router,
)
self.controller = CompactMixedAlgorithmSelfClockTeacher(**controller)
self.config = {
"reducer": dict(reducer),
"controller": dict(controller),
"mac": dict(mac),
"subtract": dict(subtract),
"factor": dict(factor),
"cancellation_teacher": dict(cancellation_teacher),
"prime_two_router": dict(prime_two_router),
}
def _controller_instruction(self, memory: UniversalU1Memory) -> torch.Tensor:
return self.controller.instruction(memory.phase, memory.clock)
__all__ = [
"CompactMixedAlgorithmSelfClockTeacher",
"V20CompactUniversalSelfClockedU2",
]